Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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