Skip to content

Commit bf9dea3

Browse files
authored
feat(fileupload): Add image dimension validation (#1342)
1 parent 871a1b4 commit bf9dea3

File tree

4 files changed

+146
-1
lines changed

4 files changed

+146
-1
lines changed

graphql.services.yml

+1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ services:
183183
- '@config.factory'
184184
- '@renderer'
185185
- '@event_dispatcher'
186+
- '@image.factory'
186187

187188
plugin.manager.graphql.persisted_query:
188189
class: Drupal\graphql\Plugin\PersistedQueryPluginManager

src/GraphQL/Utility/FileUpload.php

+94-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Drupal\Core\Entity\EntityTypeManagerInterface;
1111
use Drupal\Core\File\Exception\FileException;
1212
use Drupal\Core\File\FileSystemInterface;
13+
use Drupal\Core\Image\ImageFactory;
1314
use Drupal\Core\Lock\LockBackendInterface;
1415
use Drupal\Core\Logger\LoggerChannelInterface;
1516
use Drupal\Core\Render\RenderContext;
@@ -103,6 +104,13 @@ class FileUpload {
103104
*/
104105
protected $eventDispatcher;
105106

107+
/**
108+
* The image factory service.
109+
*
110+
* @var \Drupal\Core\Image\ImageFactory
111+
*/
112+
protected $imageFactory;
113+
106114
/**
107115
* Constructor.
108116
*/
@@ -116,7 +124,8 @@ public function __construct(
116124
LockBackendInterface $lock,
117125
ConfigFactoryInterface $config_factory,
118126
RendererInterface $renderer,
119-
EventDispatcherInterface $eventDispatcher
127+
EventDispatcherInterface $eventDispatcher,
128+
ImageFactory $image_factory
120129
) {
121130
/** @var \Drupal\file\FileStorageInterface $file_storage */
122131
$file_storage = $entityTypeManager->getStorage('file');
@@ -130,6 +139,7 @@ public function __construct(
130139
$this->systemFileConfig = $config_factory->get('system.file');
131140
$this->renderer = $renderer;
132141
$this->eventDispatcher = $eventDispatcher;
142+
$this->imageFactory = $image_factory;
133143
}
134144

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

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

263278
if (!empty($errors)) {
264279
$response->addViolations($errors);
@@ -370,6 +385,84 @@ protected function validate(FileInterface $file, array $validators, FileUploadRe
370385
return TRUE;
371386
}
372387

388+
/**
389+
* Copy of file_validate_image_resolution() without creating messages.
390+
*
391+
* Verifies that image dimensions are within the specified maximum and
392+
* minimum.
393+
*
394+
* Non-image files will be ignored. If an image toolkit is available the image
395+
* will be scaled to fit within the desired maximum dimensions.
396+
*
397+
* @param \Drupal\file\FileInterface $file
398+
* A file entity. This function may resize the file affecting its size.
399+
* @param string|int $maximum_dimensions
400+
* (optional) A string in the form WIDTHxHEIGHT; for example, '640x480' or
401+
* '85x85'. If an image toolkit is installed, the image will be resized down
402+
* to these dimensions. A value of zero (the default) indicates no
403+
* restriction on size, so no resizing will be attempted.
404+
* @param string|int $minimum_dimensions
405+
* (optional) A string in the form WIDTHxHEIGHT. This will check that the
406+
* image meets a minimum size. A value of zero (the default) indicates that
407+
* there is no restriction on size.
408+
*
409+
* @return array
410+
* An empty array if the file meets the specified dimensions, was resized
411+
* successfully to meet those requirements or is not an image. If the image
412+
* does not meet the requirements or an attempt to resize it fails, an array
413+
* containing the error message will be returned.
414+
*/
415+
protected function validateFileImageResolution(FileInterface $file, $maximum_dimensions = 0, $minimum_dimensions = 0): array {
416+
$errors = [];
417+
418+
// Check first that the file is an image.
419+
/** @var \Drupal\Core\Image\ImageInterface $image */
420+
$image = $this->imageFactory->get($file->getFileUri());
421+
422+
if ($image->isValid()) {
423+
$scaling = FALSE;
424+
if ($maximum_dimensions) {
425+
// Check that it is smaller than the given dimensions.
426+
[$width, $height] = explode('x', $maximum_dimensions);
427+
if ($image->getWidth() > $width || $image->getHeight() > $height) {
428+
// Try to resize the image to fit the dimensions.
429+
if ($image->scale((int) $width, (int) $height)) {
430+
$scaling = TRUE;
431+
$image->save();
432+
}
433+
else {
434+
$errors[] = $this->t('The image exceeds the maximum allowed dimensions and an attempt to resize it failed.');
435+
}
436+
}
437+
}
438+
439+
if ($minimum_dimensions) {
440+
// Check that it is larger than the given dimensions.
441+
[$width, $height] = explode('x', $minimum_dimensions);
442+
if ($image->getWidth() < $width || $image->getHeight() < $height) {
443+
if ($scaling) {
444+
$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.',
445+
[
446+
'%dimensions' => $minimum_dimensions,
447+
'%width' => $image->getWidth(),
448+
'%height' => $image->getHeight(),
449+
]);
450+
}
451+
else {
452+
$errors[] = $this->t('The image is too small. The minimum dimensions are %dimensions pixels and the image size is %widthx%height pixels.',
453+
[
454+
'%dimensions' => $minimum_dimensions,
455+
'%width' => $image->getWidth(),
456+
'%height' => $image->getHeight(),
457+
]);
458+
}
459+
}
460+
}
461+
}
462+
463+
return $errors;
464+
}
465+
373466
/**
374467
* Prepares the filename to strip out any malicious extensions.
375468
*

tests/files/image/10x10.png

1.04 KB
Loading

tests/src/Kernel/Framework/UploadFileServiceTest.php

+51
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,56 @@ public function testSizeValidation(): void {
146146
);
147147
}
148148

149+
/**
150+
* Tests that a larger image is resized to maximum dimensions.
151+
*/
152+
public function testDimensionTooLargeValidation(): void {
153+
// Create a Symfony dummy uploaded file in test mode.
154+
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);
155+
156+
$image = file_get_contents(\Drupal::service('extension.list.module')->getPath('graphql') . '/tests/files/image/10x10.png');
157+
158+
// Create a file with 4 bytes.
159+
file_put_contents($uploadFile->getRealPath(), $image);
160+
161+
$file_upload_response = $this->uploadService->saveFileUpload($uploadFile, [
162+
'uri_scheme' => 'public',
163+
'file_directory' => 'test',
164+
// Only allow maximum 5x5 dimension.
165+
'max_resolution' => '5x5',
166+
]);
167+
$file_entity = $file_upload_response->getFileEntity();
168+
$image = \Drupal::service('image.factory')->get($file_entity->getFileUri());
169+
$this->assertEquals(5, $image->getWidth());
170+
$this->assertEquals(5, $image->getHeight());
171+
}
172+
173+
/**
174+
* Tests that a image that is too small returns a violation.
175+
*/
176+
public function testDimensionTooSmallValidation(): void {
177+
// Create a Symfony dummy uploaded file in test mode.
178+
$uploadFile = $this->getUploadedFile(UPLOAD_ERR_OK, 4);
179+
180+
$image = file_get_contents(\Drupal::service('extension.list.module')->getPath('graphql') . '/tests/files/image/10x10.png');
181+
182+
// Create a file with 4 bytes.
183+
file_put_contents($uploadFile->getRealPath(), $image);
184+
185+
$file_upload_response = $this->uploadService->saveFileUpload($uploadFile, [
186+
'uri_scheme' => 'public',
187+
'file_directory' => 'test',
188+
// Only allow minimum dimension 15x15.
189+
'min_resolution' => '15x15',
190+
]);
191+
$violations = $file_upload_response->getViolations();
192+
193+
$this->assertStringMatchesFormat(
194+
'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.',
195+
$violations[0]['message']
196+
);
197+
}
198+
149199
/**
150200
* Tests that the uploaded file extension is renamed to txt.
151201
*/
@@ -205,6 +255,7 @@ public function testLockReleased(): void {
205255
\Drupal::service('config.factory'),
206256
\Drupal::service('renderer'),
207257
\Drupal::service('event_dispatcher'),
258+
\Drupal::service('image.factory'),
208259
);
209260

210261
// Create a Symfony dummy uploaded file in test mode.

0 commit comments

Comments
 (0)