1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 07:57:46 +02:00

feat: refactor flags extension

This commit is contained in:
Sami Mazouz
2024-02-26 11:31:14 +01:00
parent 82b9c54969
commit 8bcc2ffb40
24 changed files with 462 additions and 508 deletions

View File

@@ -7,25 +7,17 @@
* LICENSE file that was distributed with this source code.
*/
use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Controller\ListPostsController;
use Flarum\Api\Controller\ShowDiscussionController;
use Flarum\Api\Controller\ShowPostController;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource;
use Flarum\Extend;
use Flarum\Flags\Access\ScopeFlagVisibility;
use Flarum\Flags\AddCanFlagAttribute;
use Flarum\Flags\AddFlagsApiAttributes;
use Flarum\Flags\AddNewFlagCountAttribute;
use Flarum\Flags\Api\Controller\CreateFlagController;
use Flarum\Flags\Api\Controller\DeleteFlagsController;
use Flarum\Flags\Api\Controller\ListFlagsController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Api\ForumResourceFields;
use Flarum\Flags\Api\PostResourceFields;
use Flarum\Flags\Api\Resource\FlagResource;
use Flarum\Flags\Api\UserResourceFields;
use Flarum\Flags\Flag;
use Flarum\Flags\Listener;
use Flarum\Flags\PrepareFlagsApiData;
use Flarum\Forum\Content\AssertRegistered;
use Flarum\Post\Event\Deleted;
use Flarum\Post\Post;
@@ -41,8 +33,6 @@ return [
->js(__DIR__.'/js/dist/admin.js'),
(new Extend\Routes('api'))
->get('/flags', 'flags.index', ListFlagsController::class)
->post('/flags', 'flags.create', CreateFlagController::class)
->delete('/posts/{id}/flags', 'flags.delete', DeleteFlagsController::class),
(new Extend\Model(User::class))
@@ -51,27 +41,26 @@ return [
(new Extend\Model(Post::class))
->hasMany('flags', Flag::class, 'post_id'),
(new Extend\ApiSerializer(PostSerializer::class))
->hasMany('flags', FlagSerializer::class)
->attribute('canFlag', AddCanFlagAttribute::class),
(new Extend\ApiResource(FlagResource::class)),
(new Extend\ApiSerializer(CurrentUserSerializer::class))
->attribute('newFlagCount', AddNewFlagCountAttribute::class),
(new Extend\ApiResource(Resource\PostResource::class))
->fields(PostResourceFields::class),
(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(AddFlagsApiAttributes::class),
(new Extend\ApiResource(Resource\UserResource::class))
->fields(UserResourceFields::class),
(new Extend\ApiController(ShowDiscussionController::class))
->addInclude(['posts.flags', 'posts.flags.user']),
(new Extend\ApiResource(Resource\ForumResource::class))
->fields(ForumResourceFields::class),
(new Extend\ApiController(ListPostsController::class))
->addInclude(['flags', 'flags.user']),
(new Extend\ApiResource(Resource\DiscussionResource::class))
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) {
return $endpoint->addDefaultInclude(['posts.flags', 'posts.flags.user']);
}),
(new Extend\ApiController(ShowPostController::class))
->addInclude(['flags', 'flags.user']),
(new Extend\ApiController(AbstractSerializeController::class))
->prepareDataForSerialization(PrepareFlagsApiData::class),
(new Extend\ApiResource(Resource\PostResource::class))
->endpoint([Endpoint\Index::class, Endpoint\Show::class], function (Endpoint\Index|Endpoint\Show $endpoint) {
return $endpoint->addDefaultInclude(['flags', 'flags.user']);
}),
(new Extend\Settings())
->serializeToForum('guidelinesUrl', 'flarum-flags.guidelines_url'),

2
extensions/flags/js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -151,7 +151,6 @@ export default class FlagPostModal extends FormModal {
reason: this.reason() === 'other' ? null : this.reason(),
reasonDetail: this.reasonDetail(),
relationships: {
user: app.session.user,
post: this.attrs.post,
},
},

View File

@@ -23,31 +23,26 @@ class ScopeFlagVisibility
public function __invoke(User $actor, Builder $query): void
{
if ($this->extensions->isEnabled('flarum-tags')) {
$query
->select('flags.*')
->leftJoin('posts', 'posts.id', '=', 'flags.post_id')
->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id')
->whereNotExists(function ($query) use ($actor) {
return $query->selectRaw('1')
->from('discussion_tag')
->whereNotIn('tag_id', function ($query) use ($actor) {
Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id');
})
->whereColumn('discussions.id', 'discussion_id');
});
$query
->whereHas('post', function (Builder $query) use ($actor) {
$query->whereVisibleTo($actor);
})
->where(function (Builder $query) use ($actor) {
if ($this->extensions->isEnabled('flarum-tags')) {
$query
->select('flags.*')
->whereHas('post.discussion.tags', function ($query) use ($actor) {
$query->whereHasPermission($actor, 'discussion.viewFlags');
});
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->whereExists(function ($query) {
return $query->selectRaw('1')
->from('discussion_tag')
->whereColumn('discussions.id', 'discussion_id');
});
}
}
if ($actor->hasPermission('discussion.viewFlags')) {
$query->orWhereDoesntHave('post.discussion.tags');
}
}
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->orWhere('flags.user_id', $actor->id);
}
if (! $actor->hasPermission('discussion.viewFlags')) {
$query->orWhere('flags.user_id', $actor->id);
}
});
}
}

View File

@@ -1,39 +0,0 @@
<?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\Flags;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
class AddCanFlagAttribute
{
public function __construct(
protected SettingsRepositoryInterface $settings
) {
}
public function __invoke(PostSerializer $serializer, Post $post): bool
{
return $serializer->getActor()->can('flag', $post) && $this->checkFlagOwnPostSetting($serializer->getActor(), $post);
}
protected function checkFlagOwnPostSetting(User $actor, Post $post): bool
{
if ($actor->id === $post->user_id) {
// If $actor is the post author, check to see if the setting is enabled
return (bool) $this->settings->get('flarum-flags.can_flag_own');
}
// $actor is not the post author
return true;
}
}

View File

@@ -1,40 +0,0 @@
<?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\Flags;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\User;
class AddFlagsApiAttributes
{
public function __construct(
protected SettingsRepositoryInterface $settings
) {
}
public function __invoke(ForumSerializer $serializer): array
{
$attributes = [
'canViewFlags' => $serializer->getActor()->hasPermissionLike('discussion.viewFlags')
];
if ($attributes['canViewFlags']) {
$attributes['flagCount'] = (int) $this->getFlagCount($serializer->getActor());
}
return $attributes;
}
protected function getFlagCount(User $actor): int
{
return Flag::whereVisibleTo($actor)->distinct()->count('flags.post_id');
}
}

View File

@@ -1,32 +0,0 @@
<?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\Flags;
use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\User\User;
class AddNewFlagCountAttribute
{
public function __invoke(CurrentUserSerializer $serializer, User $user): int
{
return $this->getNewFlagCount($user);
}
protected function getNewFlagCount(User $actor): int
{
$query = Flag::whereVisibleTo($actor);
if ($time = $actor->read_flags_at) {
$query->where('flags.created_at', '>', $time);
}
return $query->distinct()->count('flags.post_id');
}
}

View File

@@ -1,43 +0,0 @@
<?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\Flags\Api\Controller;
use Flarum\Api\Controller\AbstractCreateController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Command\CreateFlag;
use Flarum\Flags\Flag;
use Flarum\Http\RequestUtil;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class CreateFlagController extends AbstractCreateController
{
public ?string $serializer = FlagSerializer::class;
public array $include = [
'post',
'post.flags',
'user'
];
public function __construct(
protected Dispatcher $bus
) {
}
protected function data(ServerRequestInterface $request, Document $document): Flag
{
return $this->bus->dispatch(
new CreateFlag(RequestUtil::getActor($request), Arr::get($request->getParsedBody(), 'data', []))
);
}
}

View File

@@ -1,81 +0,0 @@
<?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\Flags\Api\Controller;
use Carbon\Carbon;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Flags\Api\Serializer\FlagSerializer;
use Flarum\Flags\Flag;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListFlagsController extends AbstractListController
{
public ?string $serializer = FlagSerializer::class;
public array $include = [
'user',
'post',
'post.user',
'post.discussion'
];
public function __construct(
protected UrlGenerator $url
) {
}
protected function data(ServerRequestInterface $request, Document $document): iterable
{
$actor = RequestUtil::getActor($request);
$actor->assertRegistered();
$actor->read_flags_at = Carbon::now();
$actor->save();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$include = $this->extractInclude($request);
if (in_array('post.user', $include)) {
$include[] = 'post.user.groups';
}
$flags = Flag::whereVisibleTo($actor)
->latest('flags.created_at')
->groupBy('post_id')
->limit($limit + 1)
->offset($offset)
->get();
$this->loadRelations($flags, $include, $request);
$flags = $flags->all();
$areMoreResults = false;
if (count($flags) > $limit) {
array_pop($flags);
$areMoreResults = true;
}
$this->addPaginationData(
$document,
$request,
$this->url->to('api')->route('flags.index'),
$areMoreResults ? null : 0
);
return $flags;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Flarum\Flags\Api;
use Flarum\Api\Context;
use Flarum\Api\Schema;
use Flarum\Flags\Flag;
use Flarum\Settings\SettingsRepositoryInterface;
class ForumResourceFields
{
public function __construct(
protected SettingsRepositoryInterface $settings
) {
}
public function __invoke(): array
{
return [
Schema\Boolean::make('canViewFlags')
->get(function (object $model, Context $context) {
return $context->getActor()->hasPermissionLike('discussion.viewFlags');
}),
Schema\Integer::make('flagCount')
->visible(fn (object $model, Context $context) => $context->getActor()->hasPermissionLike('discussion.viewFlags'))
->get(function (object $model, Context $context) {
return Flag::whereVisibleTo($context->getActor())->distinct()->count('flags.post_id');
}),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Flarum\Flags\Api;
use Flarum\Api\Context;
use Flarum\Api\Schema;
use Flarum\Post\Post;
use Flarum\Settings\SettingsRepositoryInterface;
class PostResourceFields
{
public function __construct(
protected SettingsRepositoryInterface $settings
) {
}
public function __invoke(): array
{
return [
Schema\Boolean::make('canFlag')
->get(function (Post $post, Context $context) {
$actor = $context->getActor();
return $actor->can('flag', $post) && (
// $actor is not the post author
$actor->id !== $post->user_id
// If $actor is the post author, check to see if the setting is enabled
|| ((bool) $this->settings->get('flarum-flags.can_flag_own'))
);
}),
Schema\Relationship\ToMany::make('flags')
->includable(),
];
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Flarum\Flags\Api\Resource;
use Carbon\Carbon;
use Flarum\Api\Context as FlarumContext;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Api\Schema;
use Flarum\Api\Sort\SortColumn;
use Flarum\Flags\Event\Created;
use Flarum\Flags\Flag;
use Flarum\Http\Exception\InvalidParameterException;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\CommentPost;
use Flarum\Post\Post;
use Flarum\Post\PostRepository;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Tobyz\JsonApiServer\Context;
class FlagResource extends AbstractDatabaseResource
{
public function __construct(
protected PostRepository $posts,
protected TranslatorInterface $translator,
protected SettingsRepositoryInterface $settings,
) {
}
public function type(): string
{
return 'flags';
}
public function model(): string
{
return Flag::class;
}
public function query(Context $context): object
{
if ($context->collection instanceof self && $context->endpoint instanceof Endpoint\Index) {
$query = Flag::query()->groupBy('post_id');
$this->scope($query, $context);
return $query;
}
return parent::query($context);
}
public function scope(Builder $query, Context $context): void
{
$query->whereVisibleTo($context->getActor());
}
public function newModel(Context $context): object
{
if ($context->collection instanceof self && $context->endpoint instanceof Endpoint\Create) {
Flag::unguard();
return Flag::query()->firstOrNew([
'post_id' => (int) Arr::get($context->body(), 'data.relationships.post.data.id'),
'user_id' => $context->getActor()->id
], [
'type' => 'user',
]);
}
return parent::newModel($context);
}
public function endpoints(): array
{
return [
Endpoint\Create::make()
->authenticated()
->defaultInclude(['post', 'post.flags', 'user']),
Endpoint\Index::make()
->authenticated()
->defaultInclude(['user', 'post', 'post.user', 'post.discussion'])
->defaultSort('-createdAt')
->paginate()
->after(function (FlarumContext $context, $data) {
$actor = $context->getActor();
$actor->read_flags_at = Carbon::now();
$actor->save();
return $data;
}),
];
}
public function fields(): array
{
return [
Schema\Str::make('type'),
Schema\Str::make('reason')
->writableOnCreate()
->nullable()
->requiredOnCreateWithout(['reasonDetail'])
->validationMessages([
'reason.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
]),
Schema\Str::make('reasonDetail')
->writableOnCreate()
->nullable()
->requiredOnCreateWithout(['reason'])
->validationMessages([
'reasonDetail.required_without' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message'),
]),
Schema\DateTime::make('createdAt'),
Schema\Relationship\ToOne::make('post')
->includable()
->writable(fn (Flag $flag, FlarumContext $context) => $context->endpoint instanceof Endpoint\Create)
->set(function (Flag $flag, Post $post, FlarumContext $context) {
if (! ($post instanceof CommentPost)) {
throw new InvalidParameterException;
}
$actor = $context->getActor();
$actor->assertCan('flag', $post);
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
throw new PermissionDeniedException;
}
$flag->post_id = $post->id;
}),
Schema\Relationship\ToOne::make('user')
->includable(),
];
}
public function sorts(): array
{
return [
SortColumn::make('createdAt'),
];
}
public function created(object $model, Context $context): ?object
{
$this->events->dispatch(new Created($model, $context->getActor(), $context->body()));
return parent::created($model, $context);
}
}

View File

@@ -1,48 +0,0 @@
<?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\Flags\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\BasicUserSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Flags\Flag;
use InvalidArgumentException;
use Tobscure\JsonApi\Relationship;
class FlagSerializer extends AbstractSerializer
{
protected $type = 'flags';
protected function getDefaultAttributes(object|array $model): array
{
if (! ($model instanceof Flag)) {
throw new InvalidArgumentException(
$this::class.' can only serialize instances of '.Flag::class
);
}
return [
'type' => $model->type,
'reason' => $model->reason,
'reasonDetail' => $model->reason_detail,
'createdAt' => $this->formatDate($model->created_at),
];
}
protected function post(Flag $flag): ?Relationship
{
return $this->hasOne($flag, PostSerializer::class);
}
protected function user(Flag $flag): ?Relationship
{
return $this->hasOne($flag, BasicUserSerializer::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Flarum\Flags\Api;
use Flarum\Api\Context;
use Flarum\Api\Schema;
use Flarum\Flags\Flag;
use Flarum\User\User;
class UserResourceFields
{
public function __invoke(): array
{
return [
Schema\Integer::make('newFlagCount')
->visible(fn (User $user, Context $context) => $context->getActor()->id === $user->id)
->get(function (User $user, Context $context) {
$actor = $context->getActor();
$query = Flag::whereVisibleTo($actor);
if ($time = $actor->read_flags_at) {
$query->where('flags.created_at', '>', $time);
}
return $query->distinct()->count('flags.post_id');
}),
];
}
}

View File

@@ -1,79 +0,0 @@
<?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\Flags\Command;
use Carbon\Carbon;
use Flarum\Flags\Event\Created;
use Flarum\Flags\Flag;
use Flarum\Foundation\ValidationException;
use Flarum\Locale\TranslatorInterface;
use Flarum\Post\CommentPost;
use Flarum\Post\PostRepository;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Arr;
use Tobscure\JsonApi\Exception\InvalidParameterException;
class CreateFlagHandler
{
public function __construct(
protected PostRepository $posts,
protected TranslatorInterface $translator,
protected SettingsRepositoryInterface $settings,
protected Dispatcher $events
) {
}
public function handle(CreateFlag $command): Flag
{
$actor = $command->actor;
$data = $command->data;
$postId = Arr::get($data, 'relationships.post.data.id');
$post = $this->posts->findOrFail($postId, $actor);
if (! ($post instanceof CommentPost)) {
throw new InvalidParameterException;
}
$actor->assertCan('flag', $post);
if ($actor->id === $post->user_id && ! $this->settings->get('flarum-flags.can_flag_own')) {
throw new PermissionDeniedException();
}
if (Arr::get($data, 'attributes.reason') === null && Arr::get($data, 'attributes.reasonDetail') === '') {
throw new ValidationException([
'message' => $this->translator->trans('flarum-flags.forum.flag_post.reason_missing_message')
]);
}
Flag::unguard();
$flag = Flag::firstOrNew([
'post_id' => $post->id,
'user_id' => $actor->id
]);
$flag->post_id = $post->id;
$flag->user_id = $actor->id;
$flag->type = 'user';
$flag->reason = Arr::get($data, 'attributes.reason');
$flag->reason_detail = Arr::get($data, 'attributes.reasonDetail');
$flag->created_at = Carbon::now();
$flag->save();
$this->events->dispatch(new Created($flag, $actor, $data));
return $flag;
}
}

View File

@@ -31,6 +31,10 @@ class Flag extends AbstractModel
{
use ScopeVisibilityTrait;
public $timestamps = true;
public const UPDATED_AT = null;
protected $casts = ['created_at' => 'datetime'];
public function post(): BelongsTo

View File

@@ -1,64 +0,0 @@
<?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\Flags;
use Flarum\Api\Controller;
use Flarum\Flags\Api\Controller\CreateFlagController;
use Flarum\Http\RequestUtil;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ServerRequestInterface;
class PrepareFlagsApiData
{
public function __invoke(Controller\AbstractSerializeController $controller, mixed $data, ServerRequestInterface $request): void
{
// For any API action that allows the 'flags' relationship to be
// included, we need to preload this relationship onto the data (Post
// models) so that we can selectively expose only the flags that the
// user has permission to view.
if ($controller instanceof Controller\ShowDiscussionController) {
if ($data->relationLoaded('posts')) {
$posts = $data->getRelation('posts');
}
}
if ($controller instanceof Controller\ListPostsController) {
$posts = $data->all();
}
if ($controller instanceof Controller\ShowPostController) {
$posts = [$data];
}
if ($controller instanceof CreateFlagController) {
$posts = [$data->post];
}
if (isset($posts)) {
$actor = RequestUtil::getActor($request);
$postsWithPermission = [];
foreach ($posts as $post) {
if (is_object($post)) {
$post->setRelation('flags', null);
if ($actor->can('viewFlags', $post->discussion)) {
$postsWithPermission[] = $post;
}
}
}
if (count($postsWithPermission)) {
(new Collection($postsWithPermission))
->load('flags', 'flags.user');
}
}
}
}

View File

@@ -51,6 +51,7 @@ class ListTest extends TestCase
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
],
'flags' => [
['id' => 1, 'post_id' => 1, 'user_id' => 1],
@@ -58,6 +59,7 @@ class ListTest extends TestCase
['id' => 3, 'post_id' => 1, 'user_id' => 3],
['id' => 4, 'post_id' => 2, 'user_id' => 2],
['id' => 5, 'post_id' => 3, 'user_id' => 1],
['id' => 6, 'post_id' => 4, 'user_id' => 1],
]
]);
}
@@ -65,7 +67,7 @@ class ListTest extends TestCase
/**
* @test
*/
public function admin_can_see_one_flag_per_post()
public function admin_can_see_one_flag_per_visible_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
@@ -73,9 +75,9 @@ class ListTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
$data = json_decode($response->getBody()->getContents(), true)['data'];
$data = json_decode($body, true)['data'];
$ids = Arr::pluck($data, 'id');
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
@@ -84,7 +86,7 @@ class ListTest extends TestCase
/**
* @test
*/
public function regular_user_sees_own_flags()
public function regular_user_sees_own_flags_of_visible_posts()
{
$response = $this->send(
$this->request('GET', '/api/flags', [
@@ -103,7 +105,7 @@ class ListTest extends TestCase
/**
* @test
*/
public function mod_can_see_one_flag_per_post()
public function mod_can_see_one_flag_per_visible_post()
{
$response = $this->send(
$this->request('GET', '/api/flags', [

View File

@@ -50,9 +50,9 @@ class ListWithTagsTest extends TestCase
],
'group_permission' => [
['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewForum'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewForum'],
['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
],
'discussions' => [
@@ -149,9 +149,7 @@ class ListWithTagsTest extends TestCase
$data = json_decode($response->getBody()->getContents(), true)['data'];
$ids = Arr::pluck($data, 'id');
// 7 is included, even though mods can't view discussions.
// This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
$this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids);
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
}
/**

View File

@@ -0,0 +1,143 @@
<?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\Flags\Tests\integration\api\posts;
use Flarum\Group\Group;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
class IncludeFlagsVisibilityTest extends TestCase
{
use RetrievesAuthorizedUsers;
/**
* @inheritDoc
*/
protected function setup(): void
{
parent::setUp();
$this->extension('flarum-tags', 'flarum-flags');
$this->prepareDatabase([
'users' => [
$this->normalUser(),
[
'id' => 3,
'username' => 'mod',
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
'email' => 'normal2@machine.local',
'is_email_confirmed' => 1,
],
[
'id' => 4,
'username' => 'tod',
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
'email' => 'tod@machine.local',
'is_email_confirmed' => 1,
],
[
'id' => 5,
'username' => 'ted',
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
'email' => 'ted@machine.local',
'is_email_confirmed' => 1,
],
],
'group_user' => [
['group_id' => 5, 'user_id' => 2],
['group_id' => 6, 'user_id' => 3],
],
'groups' => [
['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false],
['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false],
],
'group_permission' => [
['group_id' => Group::MEMBER_ID, 'permission' => 'tag1.viewForum'],
['group_id' => 5, 'permission' => 'tag1.viewForum'],
['group_id' => 5, 'permission' => 'discussion.viewFlags'],
['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'],
['group_id' => 6, 'permission' => 'tag1.viewForum'],
],
'tags' => [
['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true],
['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false],
],
'discussions' => [
['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1],
['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1],
],
'discussion_tag' => [
['discussion_id' => 1, 'tag_id' => 1],
['discussion_id' => 2, 'tag_id' => 2],
],
'posts' => [
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
['id' => 5, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
],
'flags' => [
['id' => 1, 'post_id' => 1, 'user_id' => 1],
['id' => 2, 'post_id' => 1, 'user_id' => 5],
['id' => 3, 'post_id' => 1, 'user_id' => 3],
['id' => 4, 'post_id' => 2, 'user_id' => 5],
['id' => 5, 'post_id' => 3, 'user_id' => 1],
['id' => 6, 'post_id' => 4, 'user_id' => 1],
['id' => 7, 'post_id' => 5, 'user_id' => 5],
['id' => 8, 'post_id' => 5, 'user_id' => 5],
],
]);
}
/**
* @dataProvider listFlagsIncludesDataProvider
* @test
*/
public function user_sees_where_allowed_with_included_tags(int $actorId, array $expectedIncludes)
{
$response = $this->send(
$this->request('GET', '/api/posts', [
'authenticatedAs' => $actorId,
])->withQueryParams([
'include' => 'flags'
])
);
$this->assertEquals(200, $response->getStatusCode());
$responseBody = json_decode($response->getBody()->getContents(), true);
$data = $responseBody['data'];
$this->assertEquals(['1', '2', '3', '4', '5'], Arr::pluck($data, 'id'));
$this->assertEqualsCanonicalizing($expectedIncludes, collect($responseBody['included'] ?? [])
->filter(fn($include) => $include['type'] === 'flags')
->pluck('id')
->map(strval(...))
->all()
);
}
public function listFlagsIncludesDataProvider(): array
{
return [
'admin_sees_all' => [1, [1, 2, 3, 4, 5, 6, 7, 8]],
'user_with_general_permission_sees_where_unrestricted_tag' => [2, [6, 7, 8]],
'user_with_tag1_permission_sees_tag1_flags' => [3, [1, 2, 3, 4, 5]],
'normal_user_sees_none' => [4, []],
'normal_user_sees_own' => [5, [2, 7, 4, 8]],
];
}
}

View File

@@ -12,10 +12,9 @@ use Flarum\Api\Endpoint;
use Flarum\Api\Resource;
use Flarum\Api\Schema;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Event\Saving;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Extend;
use Flarum\Flags\Api\Controller\ListFlagsController;
use Flarum\Flags\Api\Resource\FlagResource;
use Flarum\Http\RequestUtil;
use Flarum\Post\Filter\PostSearcher;
use Flarum\Post\Post;
@@ -104,8 +103,13 @@ return [
return $endpoint->eagerLoad('discussion.tags');
}),
// (new Extend\ApiController(ListFlagsController::class))
// ->load('post.discussion.tags'),
(new Extend\Conditional())
->whenExtensionEnabled('flarum-flags', fn () => [
(new Extend\ApiResource(FlagResource::class))
->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) {
return $endpoint->eagerLoad(['post.discussion.tags']);
}),
]),
(new Extend\ApiResource(Resource\DiscussionResource::class))
->endpoint(

View File

@@ -11,9 +11,9 @@ namespace Flarum\Foundation;
use Flarum\Extension\Exception as ExtensionException;
use Flarum\Foundation\ErrorHandling as Handling;
use Flarum\Http\Exception\InvalidParameterException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException as IlluminateValidationException;
use Tobscure\JsonApi\Exception\InvalidParameterException;
use Tobyz\JsonApiServer\Exception as TobyzJsonApiServerException;
class ErrorServiceProvider extends AbstractServiceProvider

View File

@@ -7,15 +7,11 @@
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Flags\Command;
namespace Flarum\Http\Exception;
use Flarum\User\User;
use Exception;
class CreateFlag
class InvalidParameterException extends Exception
{
public function __construct(
public User $actor,
public array $data
) {
}
//
}