diff --git a/graphql.services.yml b/graphql.services.yml
index b86695715..39a5eed99 100644
--- a/graphql.services.yml
+++ b/graphql.services.yml
@@ -183,6 +183,7 @@ services:
- '@config.factory'
- '@renderer'
- '@event_dispatcher'
+ - '@image.factory'
plugin.manager.graphql.persisted_query:
class: Drupal\graphql\Plugin\PersistedQueryPluginManager
diff --git a/src/GraphQL/Utility/FileUpload.php b/src/GraphQL/Utility/FileUpload.php
index 1fbb3fadc..bd22f7e0a 100644
--- a/src/GraphQL/Utility/FileUpload.php
+++ b/src/GraphQL/Utility/FileUpload.php
@@ -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;
@@ -103,6 +104,13 @@ class FileUpload {
*/
protected $eventDispatcher;
+ /**
+ * The image factory service.
+ *
+ * @var \Drupal\Core\Image\ImageFactory
+ */
+ protected $imageFactory;
+
/**
* Constructor.
*/
@@ -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');
@@ -130,6 +139,7 @@ public function __construct(
$this->systemFileConfig = $config_factory->get('system.file');
$this->renderer = $renderer;
$this->eventDispatcher = $eventDispatcher;
+ $this->imageFactory = $image_factory;
}
/**
@@ -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);
@@ -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.
*
diff --git a/tests/files/image/10x10.png b/tests/files/image/10x10.png
new file mode 100644
index 000000000..f59f2d5e1
Binary files /dev/null and b/tests/files/image/10x10.png differ
diff --git a/tests/src/Kernel/Framework/UploadFileServiceTest.php b/tests/src/Kernel/Framework/UploadFileServiceTest.php
index c3f6d8d9c..ce668c835 100644
--- a/tests/src/Kernel/Framework/UploadFileServiceTest.php
+++ b/tests/src/Kernel/Framework/UploadFileServiceTest.php
@@ -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 15x15 pixels and the image size is 10x10 pixels.',
+ $violations[0]['message']
+ );
+ }
+
/**
* Tests that the uploaded file extension is renamed to txt.
*/
@@ -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.