From 415b68f84ffbf38e812f8648a40d48e484768da3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 22 Oct 2015 16:57:48 +1030 Subject: [PATCH] Add flood control closes #271 --- js/lib/App.js | 4 ++ src/Core/Command/StartDiscussionHandler.php | 2 +- src/Core/CoreServiceProvider.php | 1 + src/Core/Exception/FloodingException.php | 33 +++++++++ src/Core/Listener/FloodController.php | 75 +++++++++++++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/Core/Exception/FloodingException.php create mode 100755 src/Core/Listener/FloodController.php diff --git a/js/lib/App.js b/js/lib/App.js index caf4b667fa..01360f61f7 100644 --- a/js/lib/App.js +++ b/js/lib/App.js @@ -263,6 +263,10 @@ export default class App { children = app.translator.trans('core.lib.error.not_found_message'); break; + case 429: + children = app.translator.trans('core.lib.error.rate_limit_exceeded_message'); + break; + default: children = app.translator.trans('core.lib.error.generic_message'); } diff --git a/src/Core/Command/StartDiscussionHandler.php b/src/Core/Command/StartDiscussionHandler.php index bfd2f2f5c4..0dad73fcd9 100644 --- a/src/Core/Command/StartDiscussionHandler.php +++ b/src/Core/Command/StartDiscussionHandler.php @@ -60,7 +60,7 @@ public function handle(StartDiscussion $command) $this->assertCan($actor, 'startDiscussion'); // Create a new Discussion entity, persist it, and dispatch domain - // events. Before persistance, though, fire an event to give plugins + // events. Before persistence, though, fire an event to give plugins // an opportunity to alter the discussion entity based on data in the // command they may have passed through in the controller. $discussion = Discussion::start( diff --git a/src/Core/CoreServiceProvider.php b/src/Core/CoreServiceProvider.php index f735aa7702..127bd99b1f 100644 --- a/src/Core/CoreServiceProvider.php +++ b/src/Core/CoreServiceProvider.php @@ -91,6 +91,7 @@ public function boot() $events->subscribe('Flarum\Core\Listener\UserMetadataUpdater'); $events->subscribe('Flarum\Core\Listener\EmailConfirmationMailer'); $events->subscribe('Flarum\Core\Listener\DiscussionRenamedNotifier'); + $events->subscribe('Flarum\Core\Listener\FloodController'); $events->subscribe('Flarum\Core\Access\DiscussionPolicy'); $events->subscribe('Flarum\Core\Access\GroupPolicy'); diff --git a/src/Core/Exception/FloodingException.php b/src/Core/Exception/FloodingException.php new file mode 100644 index 0000000000..8aaf92437f --- /dev/null +++ b/src/Core/Exception/FloodingException.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Core\Exception; + +use Exception; +use Tobscure\JsonApi\Exception\JsonApiSerializableInterface; + +class FloodingException extends Exception implements JsonApiSerializableInterface +{ + /** + * {@inheritdoc} + */ + public function getStatusCode() + { + return 429; + } + + /** + * {@inheritdoc} + */ + public function getErrors() + { + return []; + } +} diff --git a/src/Core/Listener/FloodController.php b/src/Core/Listener/FloodController.php new file mode 100755 index 0000000000..68d2ed939f --- /dev/null +++ b/src/Core/Listener/FloodController.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Core\Listener; + +use DateTime; +use Flarum\Core\Exception\FloodingException; +use Flarum\Core\Post; +use Flarum\Core\User; +use Flarum\Event\DiscussionWillBeSaved; +use Flarum\Event\PostWillBeSaved; +use Illuminate\Contracts\Events\Dispatcher; + +class FloodController +{ + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + $events->listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']); + $events->listen(PostWillBeSaved::class, [$this, 'whenPostWillBeSaved']); + } + + /** + * @param DiscussionWillBeSaved $event + */ + public function whenDiscussionWillBeSaved(DiscussionWillBeSaved $event) + { + if ($event->discussion->exists) { + return; + } + + $this->assertNotFlooding($event->actor); + } + + /** + * @param PostWillBeSaved $event + */ + public function whenPostWillBeSaved(PostWillBeSaved $event) + { + if ($event->post->exists) { + return; + } + + $this->assertNotFlooding($event->actor); + } + + /** + * @param User $actor + * @throws FloodingException + */ + protected function assertNotFlooding(User $actor) + { + if ($this->isFlooding($actor)) { + throw new FloodingException; + } + } + + /** + * @param User $actor + * @return bool + */ + protected function isFlooding(User $actor) + { + return Post::where('user_id', $actor->id)->where('time', '>=', new DateTime('-10 seconds'))->exists(); + } +}