Skip to content

Commit

Permalink
feat(fileupload): Add image dimension validation (#1342)
Browse files Browse the repository at this point in the history
  • Loading branch information
aurelianzaha authored May 17, 2023
1 parent 871a1b4 commit bf9dea3
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 1 deletion.
1 change: 1 addition & 0 deletions graphql.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ services:
- '@config.factory'
- '@renderer'
- '@event_dispatcher'
- '@image.factory'

plugin.manager.graphql.persisted_query:
class: Drupal\graphql\Plugin\PersistedQueryPluginManager
Expand Down
95 changes: 94 additions & 1 deletion src/GraphQL/Utility/FileUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Render\RenderContext;
Expand Down Expand Up @@ -103,6 +104,13 @@ class FileUpload {
*/
protected $eventDispatcher;

/**
* The image factory service.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;

/**
* Constructor.
*/
Expand All @@ -116,7 +124,8 @@ public function __construct(
LockBackendInterface $lock,
ConfigFactoryInterface $config_factory,
RendererInterface $renderer,
EventDispatcherInterface $eventDispatcher
EventDispatcherInterface $eventDispatcher,
ImageFactory $image_factory
) {
/** @var \Drupal\file\FileStorageInterface $file_storage */
$file_storage = $entityTypeManager->getStorage('file');
Expand All @@ -130,6 +139,7 @@ public function __construct(
$this->systemFileConfig = $config_factory->get('system.file');
$this->renderer = $renderer;
$this->eventDispatcher = $eventDispatcher;
$this->imageFactory = $image_factory;
}

/**
Expand Down Expand Up @@ -259,6 +269,11 @@ public function saveFileUpload(UploadedFile $uploaded_file, array $settings): Fi

// Validate against file_validate() first with the temporary path.
$errors = file_validate($file, $validators);
$maxResolution = $settings['max_resolution'] ?? 0;
$minResolution = $settings['min_resolution'] ?? 0;
if (!empty($maxResolution) || !empty($minResolution)) {
$errors += $this->validateFileImageResolution($file, $maxResolution, $minResolution);
}

if (!empty($errors)) {
$response->addViolations($errors);
Expand Down Expand Up @@ -370,6 +385,84 @@ protected function validate(FileInterface $file, array $validators, FileUploadRe
return TRUE;
}

/**
* Copy of file_validate_image_resolution() without creating messages.
*
* Verifies that image dimensions are within the specified maximum and
* minimum.
*
* Non-image files will be ignored. If an image toolkit is available the image
* will be scaled to fit within the desired maximum dimensions.
*
* @param \Drupal\file\FileInterface $file
* A file entity. This function may resize the file affecting its size.
* @param string|int $maximum_dimensions
* (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
* '85x85'. If an image toolkit is installed, the image will be resized down
* to these dimensions. A value of zero (the default) indicates no
* restriction on size, so no resizing will be attempted.
* @param string|int $minimum_dimensions
* (optional) A string in the form WIDTHxHEIGHT. This will check that the
* image meets a minimum size. A value of zero (the default) indicates that
* there is no restriction on size.
*
* @return array
* An empty array if the file meets the specified dimensions, was resized
* successfully to meet those requirements or is not an image. If the image
* does not meet the requirements or an attempt to resize it fails, an array
* containing the error message will be returned.
*/
protected function validateFileImageResolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0): array {
$errors = [];

// Check first that the file is an image.
/** @var \Drupal\Core\Image\ImageInterface $image */
$image = $this->imageFactory->get($file->getFileUri());

if ($image->isValid()) {
$scaling = FALSE;
if ($maximum_dimensions) {
// Check that it is smaller than the given dimensions.
[$width, $height] = explode('x', $maximum_dimensions);
if ($image->getWidth() > $width || $image->getHeight() > $height) {
// Try to resize the image to fit the dimensions.
if ($image->scale((int) $width, (int) $height)) {
$scaling = TRUE;
$image->save();
}
else {
$errors[] = $this->t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
}
}
}

if ($minimum_dimensions) {
// Check that it is larger than the given dimensions.
[$width, $height] = explode('x', $minimum_dimensions);
if ($image->getWidth() < $width || $image->getHeight() < $height) {
if ($scaling) {
$errors[] = $this->t('The resized image is too small. The minimum dimensions are %dimensions pixels and after resizing, the image size will be %widthx%height pixels.',
[
'%dimensions' => $minimum_dimensions,
'%width' => $image->getWidth(),
'%height' => $image->getHeight(),
]);
}
else {
$errors[] = $this->t('The image is too small. The minimum dimensions are %dimensions pixels and the image size is %widthx%height pixels.',
[
'%dimensions' => $minimum_dimensions,
'%width' => $image->getWidth(),
'%height' => $image->getHeight(),
]);
}
}
}
}

return $errors;
}

/**
* Prepares the filename to strip out any malicious extensions.
*
Expand Down
Binary file added tests/files/image/10x10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions tests/src/Kernel/Framework/UploadFileServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,56 @@ public function testSizeValidation(): void {
);
}

/**
* Tests that a larger image is resized to maximum dimensions.
*/
public function testDimensionTooLargeValidation(): void {
// Create a Symfony dummy uploaded file in test mode.
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);

$image = file_get_contents(\Drupal::service('extension.list.module')->getPath('graphql') . '/tests/files/image/10x10.png');

// Create a file with 4 bytes.
file_put_contents($uploadFile->getRealPath(), $image);

$file_upload_response = $this->uploadService->saveFileUpload($uploadFile, [
'uri_scheme' => 'public',
'file_directory' => 'test',
// Only allow maximum 5x5 dimension.
'max_resolution' => '5x5',
]);
$file_entity = $file_upload_response->getFileEntity();
$image = \Drupal::service('image.factory')->get($file_entity->getFileUri());
$this->assertEquals(5, $image->getWidth());
$this->assertEquals(5, $image->getHeight());
}

/**
* Tests that a image that is too small returns a violation.
*/
public function testDimensionTooSmallValidation(): void {
// Create a Symfony dummy uploaded file in test mode.
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);

$image = file_get_contents(\Drupal::service('extension.list.module')->getPath('graphql') . '/tests/files/image/10x10.png');

// Create a file with 4 bytes.
file_put_contents($uploadFile->getRealPath(), $image);

$file_upload_response = $this->uploadService->saveFileUpload($uploadFile, [
'uri_scheme' => 'public',
'file_directory' => 'test',
// Only allow minimum dimension 15x15.
'min_resolution' => '15x15',
]);
$violations = $file_upload_response->getViolations();

$this->assertStringMatchesFormat(
'The image is too small. The minimum dimensions are <em class="placeholder">15x15</em> pixels and the image size is <em class="placeholder">10</em>x<em class="placeholder">10</em> pixels.',
$violations[0]['message']
);
}

/**
* Tests that the uploaded file extension is renamed to txt.
*/
Expand Down Expand Up @@ -205,6 +255,7 @@ public function testLockReleased(): void {
\Drupal::service('config.factory'),
\Drupal::service('renderer'),
\Drupal::service('event_dispatcher'),
\Drupal::service('image.factory'),
);

// Create a Symfony dummy uploaded file in test mode.
Expand Down

0 comments on commit bf9dea3

Please sign in to comment.