diff --git a/framework/core/js/lib/App.js b/framework/core/js/lib/App.js index caf4b667f..01360f61f 100644 --- a/framework/core/js/lib/App.js +++ b/framework/core/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/framework/core/src/Core/Command/StartDiscussionHandler.php b/framework/core/src/Core/Command/StartDiscussionHandler.php index bfd2f2f5c..0dad73fcd 100644 --- a/framework/core/src/Core/Command/StartDiscussionHandler.php +++ b/framework/core/src/Core/Command/StartDiscussionHandler.php @@ -60,7 +60,7 @@ class StartDiscussionHandler $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/framework/core/src/Core/CoreServiceProvider.php b/framework/core/src/Core/CoreServiceProvider.php index f735aa770..127bd99b1 100644 --- a/framework/core/src/Core/CoreServiceProvider.php +++ b/framework/core/src/Core/CoreServiceProvider.php @@ -91,6 +91,7 @@ class CoreServiceProvider extends AbstractServiceProvider $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/framework/core/src/Core/Exception/FloodingException.php b/framework/core/src/Core/Exception/FloodingException.php new file mode 100644 index 000000000..8aaf92437 --- /dev/null +++ b/framework/core/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/framework/core/src/Core/Listener/FloodController.php b/framework/core/src/Core/Listener/FloodController.php new file mode 100755 index 000000000..68d2ed939 --- /dev/null +++ b/framework/core/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(); + } +}