Skip to content

Commit

Permalink
Transformation limit event listener (#501)
Browse files Browse the repository at this point in the history
* Add ImageTransformationLimiter Event Listener
* Use a parameter array for settings instead of constructor arg
* Update tests to use param argument
* Update docs with Image transformation limiter section
  • Loading branch information
matslindh authored and christeredvartsen committed Nov 10, 2016
1 parent 449fa94 commit 44efd87
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
29 changes: 29 additions & 0 deletions docs/installation/event_listeners.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,35 @@ and is enabled like this:
The above command will delete all files in ``/path/to/cache`` older than 7 days and can be used with for instance `crontab <http://en.wikipedia.org/wiki/Cron>`_.


Image transformation limiter
++++++++++++++++++++++++++++

Allows you to limit the number of transformations a user can apply to a single resource in a request. Read more about image transformations in the :ref:`image-transformations` section.

The listener accepts a single parameter, the number of transformations a user can apply:

``(int) limit``
The number of transformations to allow (count > limit will be denied with a 403 response code).

.. code-block:: php
<?php
return [
// ...
'eventListeners' => [
'imageTransformationLimiter' => [
'listener' => 'Imbo\EventListener\ImageTransformationLimiter',
'params' => [
'limit' => 2,
],
],
],
// ...
];
.. _image-variations-listener:

Image variations
Expand Down
92 changes: 92 additions & 0 deletions src/EventListener/ImageTransformationLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
/**
* This file is part of the Imbo package
*
* (c) Christer Edvartsen <cogo@starzinger.net>
*
* For the full copyright and license information, please view the LICENSE file that was
* distributed with this source code.
*/

namespace Imbo\EventListener;

use Imbo\EventManager\EventInterface,
Imbo\EventListener\ListenerInterface,
Imbo\Exception\ResourceException;

/**
* Limit the number of transformations that can be applied in a request.
*
* @author Mats Lindh <mats@lindh.no>
* @package Event\Listeners
*/
class ImageTransformationLimiter implements ListenerInterface {
/**
* Number of transformations to allow
*
* @var int
*/
private $transformationLimit;

/**
* Class constructor
*
* @param array $params Parameters for the limit listener. `limit` (int) is required, and is the max number of
* transformations to allow. 0 will disable the check, but allow the listener to remain
* active.
* @throws InvalidArgumentException Throws an exception if the "limit" element is missing in params
*/
public function __construct(array $params) {
if (!isset($params['limit'])) {
throw new InvalidArgumentException(
'The image transformation limiter needs the "limit" argument to be configured.',
500
);
}

$this->setTransformationLimit($params['limit']);
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
'image.get' => ['checkTransformationCount' => 20],
];
}

/**
* Check the number of transformations in a request and generate a 403 if there's an excessive number of
* transformations.
*
* @param EventInterface $event The triggered event
* @throws ResourceException Throws an exception if the transformation count exceeds the allowed value.
*/
public function checkTransformationCount(EventInterface $event) {
$transformations = $event->getRequest()->getTransformations();

if ($this->transformationLimit && (count($transformations) > $this->transformationLimit)) {
throw new ResourceException('Too many transformations applied to resource. The limit is ' .
$this->transformationLimit . ' transformations.', 403);
}
}

/**
* Get the current transformation limit applied
*
* @return int
*/
public function getTransformationLimit() {
return $this->transformationLimit;
}

/**
* Set the current transformation limit. Set value to 0 to disable the check without removing the listener.
*
* @param int $transformationLimit
*/
public function setTransformationLimit($transformationLimit) {
$this->transformationLimit = (int) $transformationLimit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php
/**
* This file is part of the Imbo package
*
* (c) Christer Edvartsen <cogo@starzinger.net>
*
* For the full copyright and license information, please view the LICENSE file that was
* distributed with this source code.
*/

namespace ImboIntegrationTest\EventListener;

use Imbo\EventListener\ImageTransformationLimiter;

/**
* @covers Imbo\EventListener\ExifMetadata
* @group integration
* @group listeners
*/
class ImageTransformationLimiterTest extends \PHPUnit_Framework_TestCase {
/**
* @covers Imbo\EventListener\ImageTransformationLimiter::__construct
* @covers Imbo\EventListener\ImageTransformationLimiter::checkTransformationCount
* @covers Imbo\EventListener\ImageTransformationLimiter::setTransformationCount
*/
public function testLimitsTransformationCount() {
$listener = new ImageTransformationLimiter(['limit' => 2]);

$request = $this->getMock('Imbo\Http\Request\Request');

// content of array isn't important, the check is done on the count of the array
$request->expects($this->any())->method('getTransformations')->will($this->returnValue([1, 2, 3, 4, 5]));

$event = $this->getMock('Imbo\EventManager\Event');
$event->expects($this->any())->method('getRequest')->will($this->returnValue($request));

$this->setExpectedException('Imbo\Exception\ResourceException', '', 403);
$listener->checkTransformationCount($event);
}

/**
* @covers Imbo\EventListener\ImageTransformationLimiter::__construct
* @covers Imbo\EventListener\ImageTransformationLimiter::checkTransformationCount
* @covers Imbo\EventListener\ImageTransformationLimiter::setTransformationCount
*/
public function testAllowsTransformationCount() {
$listener = new ImageTransformationLimiter(['limit' => 2]);

$request = $this->getMock('Imbo\Http\Request\Request');

// content of array isn't important, the check is done on the count of the array
$request->expects($this->any())->method('getTransformations')->will($this->returnValue([1, 2]));

$event = $this->getMock('Imbo\EventManager\Event');
$event->expects($this->any())->method('getRequest')->will($this->returnValue($request));

$listener->checkTransformationCount($event);
}

/**
* @covers Imbo\EventListener\ImageTransformationLimiter::__construct
* @covers Imbo\EventListener\ImageTransformationLimiter::checkTransformationCount
* @covers Imbo\EventListener\ImageTransformationLimiter::setTransformationCount
*/
public function testAllowsAnyTransformationCount() {
$listener = new ImageTransformationLimiter(['limit' => 0]);

$request = $this->getMock('Imbo\Http\Request\Request');

// content of array isn't important, the check is done on the count of the array
$request->expects($this->any())->method('getTransformations')->will($this->returnValue([1, 2, 3, 4, 5, 6, 7, 8, 9]));

$event = $this->getMock('Imbo\EventManager\Event');
$event->expects($this->any())->method('getRequest')->will($this->returnValue($request));

$listener->checkTransformationCount($event);
}

/**
* @covers Imbo\EventListener\ImageTransformationLimiter::__construct
* @covers Imbo\EventListener\ImageTransformationLimiter::getTransformationCount
* @covers Imbo\EventListener\ImageTransformationLimiter::setTransformationCount
*/
public function testGetSetLimitCountTransformationCount() {
$listener = new ImageTransformationLimiter(['limit' => 42]);
$this->assertSame(42, $listener->getTransformationLimit());

$listener->setTransformationLimit(10);
$this->assertSame(10, $listener->getTransformationLimit());
}
}

0 comments on commit 44efd87

Please sign in to comment.