mirror of
https://github.com/flarum/framework.git
synced 2025-05-03 18:14:03 +08:00
chore: Merge 1.6.3
into main
This commit is contained in:
commit
a4f4ee8e71
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# [v1.6.3](https://github.com/flarum/framework/compare/v1.6.2...v1.6.3)
|
||||||
|
### Fixed
|
||||||
|
* Post mentions can be used to read any post on the forum without access control (ab1c868b978e8b0d09a5d682c54665dae17d0985).
|
||||||
|
* Notifications can leak restricted content (d0a2b95dca57d3dae9a0d77b610b1cb1d0b1766a).
|
||||||
|
* Any user including unactivated can reply in public discussions whose first post was permanently deleted (12f14112a0ecd1484d97330b82beb2a145919015).
|
||||||
|
* (subscriptions) Post notifications not getting access checked (https://github.com/flarum/framework/commit/e5f05166a062a9a6eb7c12e28728bfd5db7270e3).
|
||||||
|
|
||||||
## [v1.6.2](https://github.com/flarum/framework/compare/v1.6.1...v1.6.2)
|
## [v1.6.2](https://github.com/flarum/framework/compare/v1.6.1...v1.6.2)
|
||||||
### Fixed
|
### Fixed
|
||||||
* XSS Vulnerability in core (https://github.com/flarum/framework/pull/3684).
|
* XSS Vulnerability in core (https://github.com/flarum/framework/pull/3684).
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"flarum/core": "^1.6"
|
"flarum/core": "^1.6.3"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -91,11 +91,9 @@ return [
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||||
->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy'])
|
|
||||||
->addOptionalInclude('mentionsGroups'),
|
->addOptionalInclude('mentionsGroups'),
|
||||||
|
|
||||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||||
->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy'])
|
|
||||||
->addOptionalInclude('mentionsGroups'),
|
->addOptionalInclude('mentionsGroups'),
|
||||||
|
|
||||||
(new Extend\ApiController(Controller\AbstractSerializeController::class))
|
(new Extend\ApiController(Controller\AbstractSerializeController::class))
|
||||||
|
@ -11,7 +11,7 @@ namespace Flarum\Mentions;
|
|||||||
|
|
||||||
use Flarum\Group\Group;
|
use Flarum\Group\Group;
|
||||||
use Flarum\Http\UrlGenerator;
|
use Flarum\Http\UrlGenerator;
|
||||||
use Flarum\Post\CommentPost;
|
use Flarum\Post\PostRepository;
|
||||||
use Flarum\Settings\SettingsRepositoryInterface;
|
use Flarum\Settings\SettingsRepositoryInterface;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -115,7 +115,8 @@ class ConfigureMentions
|
|||||||
|
|
||||||
$tag->filterChain
|
$tag->filterChain
|
||||||
->prepend([static::class, 'addPostId'])
|
->prepend([static::class, 'addPostId'])
|
||||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterPostMentions(tag); }');
|
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterPostMentions(tag); }')
|
||||||
|
->addParameterByName('actor');
|
||||||
|
|
||||||
$config->Preg->match('/\B@["|“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#p(?<id>[0-9]+)\b/', $tagName);
|
$config->Preg->match('/\B@["|“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#p(?<id>[0-9]+)\b/', $tagName);
|
||||||
}
|
}
|
||||||
@ -124,9 +125,11 @@ class ConfigureMentions
|
|||||||
* @param $tag
|
* @param $tag
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function addPostId($tag)
|
public static function addPostId($tag, User $actor)
|
||||||
{
|
{
|
||||||
$post = CommentPost::find($tag->getAttribute('id'));
|
$post = resolve(PostRepository::class)
|
||||||
|
->queryVisibleTo($actor)
|
||||||
|
->find($tag->getAttribute('id'));
|
||||||
|
|
||||||
if ($post) {
|
if ($post) {
|
||||||
$tag->setAttribute('discussionid', (int) $post->discussion_id);
|
$tag->setAttribute('discussionid', (int) $post->discussion_id);
|
||||||
|
@ -38,6 +38,7 @@ class PostMentionsTest extends TestCase
|
|||||||
],
|
],
|
||||||
'discussions' => [
|
'discussions' => [
|
||||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||||
|
['id' => 50, 'title' => __CLASS__, 'is_private' => true, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 1],
|
||||||
],
|
],
|
||||||
'posts' => [
|
'posts' => [
|
||||||
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
|
['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'],
|
||||||
@ -49,6 +50,9 @@ class PostMentionsTest extends TestCase
|
|||||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
||||||
['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 40, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 40, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
||||||
['id' => 12, 'number' => 13, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="deleted_user" id="11" number="12" discussionid="2">@"acme"#p11</POSTMENTION></r>'],
|
['id' => 12, 'number' => 13, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="deleted_user" id="11" number="12" discussionid="2">@"acme"#p11</POSTMENTION></r>'],
|
||||||
|
|
||||||
|
// Restricted access
|
||||||
|
['id' => 50, 'number' => 1, 'discussion_id' => 50, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '<r>no</r>'],
|
||||||
],
|
],
|
||||||
'post_mentions_post' => [
|
'post_mentions_post' => [
|
||||||
['post_id' => 4, 'mentions_post_id' => 5],
|
['post_id' => 4, 'mentions_post_id' => 5],
|
||||||
@ -128,6 +132,37 @@ class PostMentionsTest extends TestCase
|
|||||||
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(4));
|
$this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cannot_mention_a_post_without_access()
|
||||||
|
{
|
||||||
|
$response = $this->send(
|
||||||
|
$this->request('POST', '/api/posts', [
|
||||||
|
'authenticatedAs' => 1,
|
||||||
|
'json' => [
|
||||||
|
'data' => [
|
||||||
|
'attributes' => [
|
||||||
|
'content' => '@"potato"#p50',
|
||||||
|
],
|
||||||
|
'relationships' => [
|
||||||
|
'discussion' => ['data' => ['id' => 2]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response->getStatusCode());
|
||||||
|
|
||||||
|
$response = json_decode($response->getBody(), true);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('potato', $response['data']['attributes']['contentHtml']);
|
||||||
|
$this->assertEquals('@"potato"#p50', $response['data']['attributes']['content']);
|
||||||
|
$this->assertStringNotContainsString('PostMention', $response['data']['attributes']['contentHtml']);
|
||||||
|
$this->assertNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(50));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"flarum/core": "^1.6"
|
"flarum/core": "^1.6.3"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -21,6 +21,7 @@ use Flarum\Post\Event\Posted;
|
|||||||
use Flarum\Post\Event\Restored;
|
use Flarum\Post\Event\Restored;
|
||||||
use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage;
|
use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage;
|
||||||
use Flarum\Subscriptions\Listener;
|
use Flarum\Subscriptions\Listener;
|
||||||
|
use Flarum\Subscriptions\Notification\FilterVisiblePostsBeforeSending;
|
||||||
use Flarum\Subscriptions\Notification\NewPostBlueprint;
|
use Flarum\Subscriptions\Notification\NewPostBlueprint;
|
||||||
use Flarum\Subscriptions\Query\SubscriptionFilterGambit;
|
use Flarum\Subscriptions\Query\SubscriptionFilterGambit;
|
||||||
|
|
||||||
@ -36,7 +37,8 @@ return [
|
|||||||
->namespace('flarum-subscriptions', __DIR__.'/views'),
|
->namespace('flarum-subscriptions', __DIR__.'/views'),
|
||||||
|
|
||||||
(new Extend\Notification())
|
(new Extend\Notification())
|
||||||
->type(NewPostBlueprint::class, BasicDiscussionSerializer::class, ['alert', 'email']),
|
->type(NewPostBlueprint::class, BasicDiscussionSerializer::class, ['alert', 'email'])
|
||||||
|
->beforeSending(FilterVisiblePostsBeforeSending::class),
|
||||||
|
|
||||||
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
(new Extend\ApiSerializer(DiscussionSerializer::class))
|
||||||
->attribute('subscription', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
->attribute('subscription', function (DiscussionSerializer $serializer, Discussion $discussion) {
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<?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\Subscriptions\Notification;
|
||||||
|
|
||||||
|
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||||
|
|
||||||
|
class FilterVisiblePostsBeforeSending
|
||||||
|
{
|
||||||
|
public function __invoke(BlueprintInterface $blueprint, array $recipients): array
|
||||||
|
{
|
||||||
|
if ($blueprint instanceof NewPostBlueprint) {
|
||||||
|
$newRecipients = [];
|
||||||
|
|
||||||
|
// Flarum has built-in access control for the notification subject,
|
||||||
|
// but subscriptions post notifications has the discussion as the subject.
|
||||||
|
// We'll add a post visibility check so that users can't get access to hidden replies by subscribing.
|
||||||
|
foreach ($recipients as $recipient) {
|
||||||
|
if ($blueprint->post->isVisibleTo($recipient)) {
|
||||||
|
$newRecipients[] = $recipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newRecipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $recipients;
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,9 @@
|
|||||||
namespace Flarum\Subscriptions\tests\integration\api\discussions;
|
namespace Flarum\Subscriptions\tests\integration\api\discussions;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Flarum\Extend\ModelVisibility;
|
||||||
use Flarum\Group\Group;
|
use Flarum\Group\Group;
|
||||||
|
use Flarum\Post\Post;
|
||||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
use Flarum\Testing\integration\TestCase;
|
use Flarum\Testing\integration\TestCase;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
@ -278,4 +280,47 @@ class ReplyNotificationTest extends TestCase
|
|||||||
|
|
||||||
$this->assertEquals(1, $mainUser->getUnreadNotificationCount());
|
$this->assertEquals(1, $mainUser->getUnreadNotificationCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function replying_to_a_discussion_with_a_restricted_post_only_sends_notifications_to_allowed_users()
|
||||||
|
{
|
||||||
|
// Add visibility scoper to only allow admin
|
||||||
|
// to see expected new post with content containing 'restricted-test-post'.
|
||||||
|
$this->extend(
|
||||||
|
(new ModelVisibility(Post::class))
|
||||||
|
->scope(function (User $actor, $query) {
|
||||||
|
if (! $actor->isAdmin()) {
|
||||||
|
$query->where('content', 'not like', '%restricted-test-post%');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->app();
|
||||||
|
|
||||||
|
/** @var User $allowedUser */
|
||||||
|
$allowedUser = User::query()->find(1);
|
||||||
|
$normalUser = User::query()->find(2);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $allowedUser->getUnreadNotificationCount());
|
||||||
|
$this->assertEquals(0, $normalUser->getUnreadNotificationCount());
|
||||||
|
|
||||||
|
$this->send(
|
||||||
|
$this->request('POST', '/api/posts', [
|
||||||
|
'authenticatedAs' => 3,
|
||||||
|
'json' => [
|
||||||
|
'data' => [
|
||||||
|
'attributes' => [
|
||||||
|
'content' => 'restricted-test-post',
|
||||||
|
],
|
||||||
|
'relationships' => [
|
||||||
|
'discussion' => ['data' => ['id' => 1]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $allowedUser->getUnreadNotificationCount());
|
||||||
|
$this->assertEquals(0, $normalUser->getUnreadNotificationCount());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ class StartDiscussionHandler
|
|||||||
// We will do this by running the PostReply command.
|
// We will do this by running the PostReply command.
|
||||||
try {
|
try {
|
||||||
$post = $this->bus->dispatch(
|
$post = $this->bus->dispatch(
|
||||||
new PostReply($discussion->id, $actor, $data, $ipAddress)
|
new PostReply($discussion->id, $actor, $data, $ipAddress, true)
|
||||||
);
|
);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$discussion->delete();
|
$discussion->delete();
|
||||||
|
@ -91,6 +91,13 @@ class Formatter
|
|||||||
{
|
{
|
||||||
$parser = $this->getParser($context);
|
$parser = $this->getParser($context);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Can be injected in tag or attribute filters by calling:
|
||||||
|
* ->addParameterByName('actor') on the filter.
|
||||||
|
* See the mentions extension's ConfigureMentions.php for an example.
|
||||||
|
*/
|
||||||
|
$parser->registeredVars['actor'] = $user;
|
||||||
|
|
||||||
foreach ($this->parsingCallbacks as $callback) {
|
foreach ($this->parsingCallbacks as $callback) {
|
||||||
$text = $callback($parser, $context, $text, $user);
|
$text = $callback($parser, $context, $text, $user);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace Flarum\Notification;
|
namespace Flarum\Notification;
|
||||||
|
|
||||||
|
use Flarum\Database\AbstractModel;
|
||||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||||
use Flarum\Notification\Driver\NotificationDriverInterface;
|
use Flarum\Notification\Driver\NotificationDriverInterface;
|
||||||
use Flarum\User\User;
|
use Flarum\User\User;
|
||||||
@ -74,6 +75,12 @@ class NotificationSyncer
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To add access checking on notification subjects, we first attempt
|
||||||
|
// to load visible subjects to this user.
|
||||||
|
if (! $this->userCanSeeSubject($user, $blueprint->getSubject())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$existing = $toDelete->first(function ($notification) use ($user) {
|
$existing = $toDelete->first(function ($notification) use ($user) {
|
||||||
return $notification->user_id === $user->id;
|
return $notification->user_id === $user->id;
|
||||||
});
|
});
|
||||||
@ -161,6 +168,18 @@ class NotificationSyncer
|
|||||||
Notification::whereIn('id', $ids)->update(['is_deleted' => $isDeleted]);
|
Notification::whereIn('id', $ids)->update(['is_deleted' => $isDeleted]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check access to determine if the recipient is allowed to receive the notification.
|
||||||
|
*/
|
||||||
|
protected function userCanSeeSubject(User $user, ?AbstractModel $subject): bool
|
||||||
|
{
|
||||||
|
if ($subject && method_exists($subject, 'registerVisibilityScoper')) {
|
||||||
|
return (bool) $subject->newQuery()->whereVisibleTo($user)->find($subject->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a notification driver to the list.
|
* Adds a notification driver to the list.
|
||||||
*
|
*
|
||||||
|
@ -41,17 +41,23 @@ class PostReply
|
|||||||
*/
|
*/
|
||||||
public $ipAddress;
|
public $ipAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $isFirstPost;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $discussionId The ID of the discussion to post the reply to.
|
* @param int $discussionId The ID of the discussion to post the reply to.
|
||||||
* @param User $actor The user who is performing the action.
|
* @param User $actor The user who is performing the action.
|
||||||
* @param array $data The attributes to assign to the new post.
|
* @param array $data The attributes to assign to the new post.
|
||||||
* @param string $ipAddress The IP address of the actor.
|
* @param string $ipAddress The IP address of the actor.
|
||||||
*/
|
*/
|
||||||
public function __construct($discussionId, User $actor, array $data, $ipAddress = null)
|
public function __construct($discussionId, User $actor, array $data, $ipAddress = null, bool $isFirstPost = false)
|
||||||
{
|
{
|
||||||
$this->discussionId = $discussionId;
|
$this->discussionId = $discussionId;
|
||||||
$this->actor = $actor;
|
$this->actor = $actor;
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->ipAddress = $ipAddress;
|
$this->ipAddress = $ipAddress;
|
||||||
|
$this->isFirstPost = $isFirstPost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class PostReplyHandler
|
|||||||
|
|
||||||
// If this is the first post in the discussion, it's technically not a
|
// If this is the first post in the discussion, it's technically not a
|
||||||
// "reply", so we won't check for that permission.
|
// "reply", so we won't check for that permission.
|
||||||
if ($discussion->first_post_id !== null) {
|
if (! $command->isFirstPost) {
|
||||||
$actor->assertCan('reply', $discussion);
|
$actor->assertCan('reply', $discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class PostRepository
|
|||||||
* @param User|null $user
|
* @param User|null $user
|
||||||
* @return Builder<Post>
|
* @return Builder<Post>
|
||||||
*/
|
*/
|
||||||
protected function queryVisibleTo(User $user = null)
|
public function queryVisibleTo(User $user = null)
|
||||||
{
|
{
|
||||||
$query = $this->query();
|
$query = $this->query();
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
namespace Flarum\Tests\integration\api\posts;
|
namespace Flarum\Tests\integration\api\posts;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Flarum\Group\Group;
|
||||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
use Flarum\Testing\integration\TestCase;
|
use Flarum\Testing\integration\TestCase;
|
||||||
|
|
||||||
@ -26,36 +27,70 @@ class CreateTest extends TestCase
|
|||||||
|
|
||||||
$this->prepareDatabase([
|
$this->prepareDatabase([
|
||||||
'discussions' => [
|
'discussions' => [
|
||||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2],
|
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1],
|
||||||
|
// Discussion with deleted first post.
|
||||||
|
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => null],
|
||||||
|
],
|
||||||
|
'posts' => [
|
||||||
|
['id' => 1, 'discussion_id' => 1, 'number' => 1, 'created_at' => Carbon::now()->subDay()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t></t>'],
|
||||||
],
|
],
|
||||||
'users' => [
|
'users' => [
|
||||||
$this->normalUser(),
|
$this->normalUser(),
|
||||||
]
|
['id' => 3, 'username' => 'restricted', 'email' => 'restricted@machine.local', 'is_email_confirmed' => 1],
|
||||||
|
],
|
||||||
|
'groups' => [
|
||||||
|
['id' => 40, 'name_singular' => 'tess', 'name_plural' => 'tess'],
|
||||||
|
],
|
||||||
|
'group_user' => [
|
||||||
|
['group_id' => 40, 'user_id' => 3],
|
||||||
|
],
|
||||||
|
'group_permission' => [
|
||||||
|
['group_id' => 40, 'permission' => 'discussion.reply'],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @dataProvider discussionRepliesPrvider
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function can_create_reply()
|
public function can_create_reply_if_allowed(int $actorId, int $discussionId, int $responseStatus)
|
||||||
{
|
{
|
||||||
|
// Reset permissions for normal users group.
|
||||||
|
$this->database()
|
||||||
|
->table('group_permission')
|
||||||
|
->where('permission', 'discussion.reply')
|
||||||
|
->where('group_id', Group::MEMBER_ID)
|
||||||
|
->delete();
|
||||||
|
|
||||||
$response = $this->send(
|
$response = $this->send(
|
||||||
$this->request('POST', '/api/posts', [
|
$this->request('POST', '/api/posts', [
|
||||||
'authenticatedAs' => 2,
|
'authenticatedAs' => $actorId,
|
||||||
'json' => [
|
'json' => [
|
||||||
'data' => [
|
'data' => [
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
'content' => 'reply with predetermined content for automated testing - too-obscure',
|
||||||
],
|
],
|
||||||
'relationships' => [
|
'relationships' => [
|
||||||
'discussion' => ['data' => ['id' => 1]],
|
'discussion' => ['data' => ['id' => $discussionId]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals(201, $response->getStatusCode());
|
$this->assertEquals($responseStatus, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function discussionRepliesPrvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// [$actorId, $discussionId, $responseStatus]
|
||||||
|
'can_create_reply_with_ability' => [3, 1, 201],
|
||||||
|
'cannot_create_reply_without_ability' => [2, 1, 403],
|
||||||
|
'can_create_reply_with_ability_when_first_post_is_deleted' => [3, 2, 201],
|
||||||
|
'cannot_create_reply_without_ability_when_first_post_is_deleted' => [2, 2, 403],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
<?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\Tests\integration\notification;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Flarum\Api\Serializer\BasicDiscussionSerializer;
|
||||||
|
use Flarum\Api\Serializer\BasicPostSerializer;
|
||||||
|
use Flarum\Database\AbstractModel;
|
||||||
|
use Flarum\Discussion\Discussion;
|
||||||
|
use Flarum\Extend;
|
||||||
|
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||||
|
use Flarum\Notification\Notification;
|
||||||
|
use Flarum\Notification\NotificationSyncer;
|
||||||
|
use Flarum\Post\Post;
|
||||||
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
|
use Flarum\Testing\integration\TestCase;
|
||||||
|
use Flarum\User\User;
|
||||||
|
|
||||||
|
class NotificationSyncerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RetrievesAuthorizedUsers;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->prepareDatabase([
|
||||||
|
'users' => [
|
||||||
|
$this->normalUser(),
|
||||||
|
['id' => 3, 'username' => 'Receiver', 'email' => 'receiver@machine.local', 'is_email_confirmed' => 1],
|
||||||
|
],
|
||||||
|
'discussions' => [
|
||||||
|
['id' => 1, 'title' => 'Public discussion', 'created_at' => Carbon::parse('2021-11-01 13:00:00')->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 2, 'is_private' => 0, 'last_post_number' => 2],
|
||||||
|
|
||||||
|
['id' => 2, 'title' => 'Private discussion', 'created_at' => Carbon::parse('2021-11-01 13:00:00')->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 3, 'comment_count' => 2, 'is_private' => 1, 'last_post_number' => 2],
|
||||||
|
],
|
||||||
|
'posts' => [
|
||||||
|
['id' => 1, 'discussion_id' => 1, 'number' => 1, 'created_at' => Carbon::parse('2021-11-01 13:00:00')->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t></t>', 'is_private' => 0],
|
||||||
|
['id' => 2, 'discussion_id' => 1, 'number' => 2, 'created_at' => Carbon::parse('2021-11-01 13:00:03')->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t></t>', 'is_private' => 1],
|
||||||
|
|
||||||
|
['id' => 3, 'discussion_id' => 2, 'number' => 1, 'created_at' => Carbon::parse('2021-11-01 13:00:00')->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t></t>', 'is_private' => 0],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider visibleSubjectsProvider
|
||||||
|
* @param class-string<AbstractModel> $subjectClass
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function can_receive_notification_for_visible_subjects(string $subjectClass, int $subjectId, string $serializer)
|
||||||
|
{
|
||||||
|
$this->expect_notification_count_from_sending_notification_type_with_subject(
|
||||||
|
2,
|
||||||
|
$subjectClass,
|
||||||
|
$subjectId,
|
||||||
|
$serializer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider invisibleSubjectsProvider
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cannot_receive_notification_for_restricted_subjects(string $subjectClass, int $subjectId, string $serializer)
|
||||||
|
{
|
||||||
|
$this->expect_notification_count_from_sending_notification_type_with_subject(
|
||||||
|
0,
|
||||||
|
$subjectClass,
|
||||||
|
$subjectId,
|
||||||
|
$serializer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string<AbstractModel> $subjectClass
|
||||||
|
*/
|
||||||
|
protected function expect_notification_count_from_sending_notification_type_with_subject(int $count, string $subjectClass, int $subjectId, string $serializer)
|
||||||
|
{
|
||||||
|
CustomNotificationType::$subjectModel = $subjectClass;
|
||||||
|
|
||||||
|
$this->extend(
|
||||||
|
(new Extend\Notification())
|
||||||
|
->type(CustomNotificationType::class, $serializer, ['alert'])
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var NotificationSyncer $syncer */
|
||||||
|
$syncer = $this->app()->getContainer()->make(NotificationSyncer::class);
|
||||||
|
|
||||||
|
$subject = $subjectClass::query()->find($subjectId);
|
||||||
|
|
||||||
|
$syncer->sync(
|
||||||
|
$blueprint = new CustomNotificationType($subject),
|
||||||
|
User::query()
|
||||||
|
->whereIn('id', [1, 3])
|
||||||
|
->get()
|
||||||
|
->all()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$count,
|
||||||
|
Notification::query()
|
||||||
|
->matchingBlueprint($blueprint)
|
||||||
|
->whereSubject($subject)
|
||||||
|
->count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function visibleSubjectsProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[Post::class, 1, BasicPostSerializer::class],
|
||||||
|
[Discussion::class, 1, BasicDiscussionSerializer::class],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invisibleSubjectsProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[Post::class, 2, BasicPostSerializer::class],
|
||||||
|
[Discussion::class, 2, BasicDiscussionSerializer::class],
|
||||||
|
[Post::class, 3, BasicPostSerializer::class],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomNotificationType implements BlueprintInterface
|
||||||
|
{
|
||||||
|
protected $subject;
|
||||||
|
public static $subjectModel;
|
||||||
|
|
||||||
|
public function __construct($subject)
|
||||||
|
{
|
||||||
|
$this->subject = $subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFromUser()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubject()
|
||||||
|
{
|
||||||
|
return $this->subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getData()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getType()
|
||||||
|
{
|
||||||
|
return 'customNotificationType';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubjectModel()
|
||||||
|
{
|
||||||
|
return self::$subjectModel;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user