Skip to content

Commit

Permalink
feat: dispatch PSR-14 event when rate limit is exceeded
Browse files Browse the repository at this point in the history
resolves: #1
  • Loading branch information
brotkrueml committed Aug 7, 2023
1 parent 40bc8a7 commit 0f61d80
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- PSR-14 event is dispatched when rate limit is exceeded (#1)

## [1.1.0] - 2023-04-01

### Added
Expand Down
15 changes: 11 additions & 4 deletions Classes/Domain/Finishers/RateLimitFinisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
namespace Brotkrueml\FormRateLimit\Domain\Finishers;

use Brotkrueml\FormRateLimit\Domain\Dto\Options;
use Brotkrueml\FormRateLimit\Event\RateLimitExceededEvent;
use Brotkrueml\FormRateLimit\Guards\IntervalGuard;
use Brotkrueml\FormRateLimit\Guards\LimitGuard;
use Brotkrueml\FormRateLimit\Guards\PolicyGuard;
use Brotkrueml\FormRateLimit\Guards\RestrictionsGuard;
use Brotkrueml\FormRateLimit\RateLimiter\FormRateLimitFactory;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
Expand All @@ -25,6 +27,7 @@
final class RateLimitFinisher extends AbstractFinisher
{
private FormRateLimitFactory $rateLimitFactory;
private EventDispatcherInterface $eventDispatcher;
private IntervalGuard $intervalGuard;
private LimitGuard $limitGuard;
private PolicyGuard $policyGuard;
Expand All @@ -44,12 +47,10 @@ final class RateLimitFinisher extends AbstractFinisher
'template' => 'EXT:form_rate_limit/Resources/Private/Templates/RateLimitExceeded.html',
];

/**
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(FormRateLimitFactory $rateLimitFactory)
public function __construct(FormRateLimitFactory $rateLimitFactory, EventDispatcherInterface $eventDispatcher)
{
$this->rateLimitFactory = $rateLimitFactory;
$this->eventDispatcher = $eventDispatcher;
$this->intervalGuard = new IntervalGuard();
$this->limitGuard = new LimitGuard();
$this->policyGuard = new PolicyGuard();
Expand All @@ -73,6 +74,12 @@ protected function executeInternal(): ?string
$normalizedParams->getRemoteAddress()
);
if (! $limiter->consume()->isAccepted()) {
$this->eventDispatcher->dispatch(
new RateLimitExceededEvent(
$this->finisherContext->getFormRuntime()->getIdentifier(),
$options
)
);
$this->finisherContext->cancel();

return $this->renderExceededMessage($options);
Expand Down
48 changes: 48 additions & 0 deletions Classes/Event/RateLimitExceededEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "form_rate_limit" extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

namespace Brotkrueml\FormRateLimit\Event;

use Brotkrueml\FormRateLimit\Domain\Dto\Options;

final class RateLimitExceededEvent
{
private string $formIdentifier;
private Options $options;

public function __construct(
string $formIdentifier,
Options $options
) {
$this->formIdentifier = $formIdentifier;
$this->options = $options;
}

public function getFormIdentifier(): string
{
return $this->formIdentifier;
}

public function getInterval(): string
{
return $this->options->getInterval();
}

public function getLimit(): int
{
return $this->options->getLimit();
}

public function getPolicy(): string
{
return $this->options->getPolicy();
}
}
6 changes: 6 additions & 0 deletions Documentation/Changelog/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
`Unreleased <https://github.com/brotkrueml/typo3-form-rate-limit/compare/v1.1.0...HEAD>`_
---------------------------------------------------------------------------------------------

Added
^^^^^


* PSR-14 event is dispatched when rate limit is exceeded (#1)

`1.1.0 <https://github.com/brotkrueml/typo3-form-rate-limit/compare/v1.0.0...v1.1.0>`_ - 2023-04-01
-------------------------------------------------------------------------------------------------------

Expand Down
88 changes: 88 additions & 0 deletions Documentation/Events/Index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.. include:: /Includes.rst.txt

.. index:: Events

.. _events:

=============
PSR-14 events
=============

Target group: **Developers**

Have a look into the :ref:`event dispatcher documentation <t3coreapi:EventDispatcher>`,
if you are not familiar with PSR-14 events.

RateLimitExceededEvent
======================

.. versionadded:: 1.2.0

This event is dispatched when the rate limit for a form has been exceeded. This
way you can create an event listener which notifies you about the exceeded
limit: add a log entry, send an email, inform a third-party system, etc.

The event :php:`\Brotkrueml\FormRateLimit\Event\RateLimitExceededEvent`
provides the following methods:

:php:`->getFormIdentifier()`
Returns the form identifier.

:php:`->getInterval()`
Returns the configured :ref:`interval <options-interval>`.

:php:`->getLimit()`
Returns the configured :ref:`limit <options-limit>`.

:php:`->getPolicy()`
Returns the configured :ref:`policy <options-policy>`.

Example
-------

This example adds an entry to the TYPO3 log:

.. code-block:: php
:caption: EXT:your_extension/Classes/EventListener/FormRateLimitExceededLogger.php
<?php
declare(strict_types=1);
namespace YourVendor\YourExtension\EventListener;
use Brotkrueml\FormRateLimit\Event\RateLimitExceededEvent;
use Psr\Log\LoggerInterface;
final class FormRateLimitExceededLogger
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke(RateLimitExceededEvent $event): void
{
$this->logger->warning(
'The form with identifier "{formIdentifier}" was sent more than {limit} times within {interval}',
[
'formIdentifier' => $event->getFormIdentifier(),
'limit' => $event->getLimit(),
'interval' => $event->getInterval()
]
);
}
}
Registration of the event listener:

.. code-block:: yaml
:caption: EXT:your_extension/Configuration/Services.yaml
services:
YourVendor\YourExtension\EventListener\FormRateLimitExceededLogger:
tags:
- name: event.listener
identifier: 'yourFormRateLimitExceededLogger'
1 change: 1 addition & 0 deletions Documentation/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ sending a form.
Introduction/Index
Installation/Index
Finisher/Index
Events/Index
Command/Index
Troubleshooting/Index
Changelog/Index
Expand Down
1 change: 1 addition & 0 deletions Documentation/Settings.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ project_discussions =

[intersphinx_mapping]

t3coreapi = https://docs.typo3.org/m/typo3/reference-coreapi/12.4/en-us/
t3start = https://docs.typo3.org/m/typo3/tutorial-getting-started/12.4/en-us/

ext_form = https://docs.typo3.org/c/typo3/cms-form/12.4/en-us/
40 changes: 40 additions & 0 deletions Tests/Unit/Event/RateLimitExceededEventTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/*
* This file is part of the "form_rate_limit" extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

namespace Brotkrueml\FormRateLimit\Tests\Unit\Event;

use Brotkrueml\FormRateLimit\Domain\Dto\Options;
use Brotkrueml\FormRateLimit\Event\RateLimitExceededEvent;
use PHPUnit\Framework\TestCase;

final class RateLimitExceededEventTest extends TestCase
{
/**
* @test
*/
public function valuesFromGettersReturnedCorrectly(): void
{
$subject = new RateLimitExceededEvent(
'mypage-42',
new Options(
'1 hour',
3,
'sliding_window',
[]
)
);

self::assertSame('mypage-42', $subject->getFormIdentifier());
self::assertSame('1 hour', $subject->getInterval());
self::assertSame(3, $subject->getLimit());
self::assertSame('sliding_window', $subject->getPolicy());
}
}
2 changes: 1 addition & 1 deletion phpstan.baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ parameters:
ignoreErrors:
-
message: "#^Call to method getIdentifier\\(\\) on an unknown class TYPO3\\\\CMS\\\\Form\\\\Domain\\\\Runtime\\\\FormRuntime\\.$#"
count: 2
count: 3
path: Classes/Domain/Finishers/RateLimitFinisher.php

-
Expand Down

0 comments on commit 0f61d80

Please sign in to comment.