|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Drupal\graphql\GraphQL\Utility; |
| 4 | + |
| 5 | +use Drupal\Component\Utility\Bytes; |
| 6 | +use Drupal\Component\Utility\Environment; |
| 7 | +use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 8 | +use Drupal\Core\File\FileSystem; |
| 9 | +use Drupal\Core\File\FileSystemInterface; |
| 10 | +use Drupal\Core\Logger\LoggerChannelInterface; |
| 11 | +use Drupal\Core\Session\AccountProxyInterface; |
| 12 | +use Drupal\Core\StringTranslation\StringTranslationTrait; |
| 13 | +use Drupal\graphql\Wrappers\FileUploadResponse; |
| 14 | +use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; |
| 15 | +use Symfony\Component\HttpFoundation\File\UploadedFile; |
| 16 | + |
| 17 | +/** |
| 18 | + * Service to manage file uploads within GraphQL mutations. |
| 19 | + */ |
| 20 | +class FileUpload { |
| 21 | + |
| 22 | + use StringTranslationTrait; |
| 23 | + |
| 24 | + /** |
| 25 | + * The entity type manager. |
| 26 | + * |
| 27 | + * @var \Drupal\Core\Entity\EntityTypeManagerInterface |
| 28 | + */ |
| 29 | + protected $entityTypeManager; |
| 30 | + |
| 31 | + /** |
| 32 | + * The current user. |
| 33 | + * |
| 34 | + * @var \Drupal\Core\Session\AccountProxyInterface |
| 35 | + */ |
| 36 | + protected $currentUser; |
| 37 | + |
| 38 | + /** |
| 39 | + * The mime type guesser service. |
| 40 | + * |
| 41 | + * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface |
| 42 | + */ |
| 43 | + protected $mimeTypeGuesser; |
| 44 | + |
| 45 | + /** |
| 46 | + * The file system service. |
| 47 | + * |
| 48 | + * @var \Drupal\Core\File\FileSystemInterface |
| 49 | + */ |
| 50 | + protected $fileSystem; |
| 51 | + |
| 52 | + /** |
| 53 | + * GraphQL logger channel. |
| 54 | + * |
| 55 | + * @var \Drupal\Core\Logger\LoggerChannelInterface |
| 56 | + */ |
| 57 | + protected $logger; |
| 58 | + |
| 59 | + /** |
| 60 | + * {@inheritdoc} |
| 61 | + */ |
| 62 | + public function __construct( |
| 63 | + EntityTypeManagerInterface $entityTypeManager, |
| 64 | + AccountProxyInterface $currentUser, |
| 65 | + MimeTypeGuesserInterface $mimeTypeGuesser, |
| 66 | + FileSystemInterface $fileSystem, |
| 67 | + LoggerChannelInterface $logger |
| 68 | + ) { |
| 69 | + $this->entityTypeManager = $entityTypeManager; |
| 70 | + $this->currentUser = $currentUser; |
| 71 | + $this->mimeTypeGuesser = $mimeTypeGuesser; |
| 72 | + $this->fileSystem = $fileSystem; |
| 73 | + $this->logger = $logger; |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * Gets max upload size. |
| 78 | + * |
| 79 | + * @param array $settings |
| 80 | + * The file field settings. |
| 81 | + * |
| 82 | + * @return int |
| 83 | + * Max upload size. |
| 84 | + */ |
| 85 | + protected function getMaxUploadSize(array $settings) { |
| 86 | + // Cap the upload size according to the PHP limit. |
| 87 | + $max_filesize = Bytes::toInt(Environment::getUploadMaxSize()); |
| 88 | + if (!empty($settings['max_filesize'])) { |
| 89 | + $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize'])); |
| 90 | + } |
| 91 | + return $max_filesize; |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Retrieves the upload validators for a file field. |
| 96 | + * |
| 97 | + * @param array $settings |
| 98 | + * The file field settings. |
| 99 | + * |
| 100 | + * @return array |
| 101 | + * List of file validators. |
| 102 | + */ |
| 103 | + protected function getUploadValidators(array $settings) { |
| 104 | + // Validate name length. |
| 105 | + $validators = [ |
| 106 | + 'file_validate_name_length' => [], |
| 107 | + ]; |
| 108 | + |
| 109 | + // There is always a file size limit due to the PHP server limit. |
| 110 | + $validators['file_validate_size'] = [$this->getMaxUploadSize($settings)]; |
| 111 | + |
| 112 | + // Add the extension check if necessary. |
| 113 | + if (!empty($settings['file_extensions'])) { |
| 114 | + $validators['file_validate_extensions'] = [$settings['file_extensions']]; |
| 115 | + } |
| 116 | + |
| 117 | + return $validators; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Create a temporary file and send back the newly created entity. |
| 122 | + * |
| 123 | + * Based on several file upload handlers, see |
| 124 | + * _file_save_upload_single() |
| 125 | + * \Drupal\file\Plugin\Field\FieldType\FileItem |
| 126 | + * \Drupal\file\Plugin\rest\resource\FileUploadResource. |
| 127 | + * |
| 128 | + * @param \Symfony\Component\HttpFoundation\File\UploadedFile $file |
| 129 | + * The file entity to upload. |
| 130 | + * @param array $settings |
| 131 | + * File settings as specified in regular file field config. Contains keys: |
| 132 | + * - file_directory: Where to upload the file |
| 133 | + * - uri_scheme: Uri scheme to upload the file to (eg public://, private://) |
| 134 | + * - file_extensions: List of valid file extensions (eg [xml, pdf]) |
| 135 | + * - max_filesize: Maximum allowed size of uploaded file. |
| 136 | + * |
| 137 | + * @return \Drupal\graphql\Wrappers\FileUploadResponse |
| 138 | + * The file upload response containing file entity or list of violations. |
| 139 | + * |
| 140 | + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException |
| 141 | + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException |
| 142 | + */ |
| 143 | + public function createTemporaryFileUpload(UploadedFile $file, array $settings) { |
| 144 | + $response = new FileUploadResponse(); |
| 145 | + |
| 146 | + // Check for file upload errors and return FALSE for this file if a lower |
| 147 | + // level system error occurred. |
| 148 | + // @see http://php.net/manual/features.file-upload.errors.php. |
| 149 | + switch ($file->getError()) { |
| 150 | + case UPLOAD_ERR_INI_SIZE: |
| 151 | + case UPLOAD_ERR_FORM_SIZE: |
| 152 | + $maxUploadSize = format_size($this->getMaxUploadSize($settings)); |
| 153 | + $response->setViolation($this->t('The file @file could not be saved because it exceeds @maxsize, the maximum allowed size for uploads.', ['@file' => $file->getClientOriginalName(), '@maxsize' => $maxUploadSize])); |
| 154 | + $this->logger->error('The file @file could not be saved because it exceeds @maxsize, the maximum allowed size for uploads.', ['@file' => $file->getFilename(), '@maxsize' => $maxUploadSize]); |
| 155 | + return $response; |
| 156 | + |
| 157 | + case UPLOAD_ERR_PARTIAL: |
| 158 | + case UPLOAD_ERR_NO_FILE: |
| 159 | + $response->setViolation($this->t('The file "@file" could not be saved because the upload did not complete.', ['@file' => $file->getClientOriginalName()])); |
| 160 | + $this->logger->error('The file "@file" could not be saved because the upload did not complete.', ['@file' => $file->getFilename()]); |
| 161 | + return $response; |
| 162 | + |
| 163 | + case UPLOAD_ERR_OK: |
| 164 | + // Final check that this is a valid upload, if it isn't, use the |
| 165 | + // default error handler. |
| 166 | + if (is_uploaded_file($file->getRealPath())) { |
| 167 | + break; |
| 168 | + } |
| 169 | + |
| 170 | + default: |
| 171 | + $response->setViolation($this->t('Unknown error while uploading the file "@file".', ['@file' => $file->getClientOriginalName()])); |
| 172 | + $this->logger->error('Error while uploading the file "@file" with an error code "@code".', ['@file' => $file->getFilename(), '@code' => $file->getError()]); |
| 173 | + return $response; |
| 174 | + } |
| 175 | + |
| 176 | + // Make sure the destination directory exists. |
| 177 | + $uploadDir = $settings['uri_scheme'] . '://' . trim($settings['file_directory'], '/'); |
| 178 | + if (!$this->fileSystem->prepareDirectory($uploadDir, FileSystem::CREATE_DIRECTORY)) { |
| 179 | + $response->setViolation($this->t('Unknown error while uploading the file "@file".', ['@file' => $file->getClientOriginalName()])); |
| 180 | + $this->logger->error('Could not create directory "@upload_directory".', ["@upload_directory" => $uploadDir]); |
| 181 | + return $response; |
| 182 | + } |
| 183 | + $name = $file->getClientOriginalName(); |
| 184 | + $mime = $this->mimeTypeGuesser->guess($name); |
| 185 | + $destination = $this->fileSystem->getDestinationFilename("{$uploadDir}/{$name}", $this->fileSystem::EXISTS_RENAME); |
| 186 | + |
| 187 | + // Begin building file entity. |
| 188 | + $values = [ |
| 189 | + 'uid' => $this->currentUser->id(), |
| 190 | + 'status' => 0, |
| 191 | + 'filename' => $name, |
| 192 | + 'uri' => $destination, |
| 193 | + 'filesize' => $file->getSize(), |
| 194 | + 'filemime' => $mime, |
| 195 | + ]; |
| 196 | + $storage = $this->entityTypeManager->getStorage('file'); |
| 197 | + /** @var \Drupal\file\FileInterface $fileEntity */ |
| 198 | + $fileEntity = $storage->create($values); |
| 199 | + |
| 200 | + // Validate the entity values. |
| 201 | + if (($violations = $fileEntity->validate()) && $violations->count()) { |
| 202 | + $response->setViolations($violations); |
| 203 | + return $response; |
| 204 | + } |
| 205 | + |
| 206 | + // Validate the file name length. |
| 207 | + if ($violations = file_validate($fileEntity, $this->getUploadValidators($settings))) { |
| 208 | + $response->setViolations($violations); |
| 209 | + return $response; |
| 210 | + } |
| 211 | + |
| 212 | + // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary |
| 213 | + // directory. This overcomes open_basedir restrictions for future file |
| 214 | + // operations. |
| 215 | + if (!$this->fileSystem->moveUploadedFile($file->getRealPath(), $fileEntity->getFileUri())) { |
| 216 | + $response->setViolation($this->t('Unknown error while uploading the file "@file".', ['@file' => $file->getClientOriginalName()])); |
| 217 | + $this->logger->error('Unable to move file from "@file" to "@destination".', ['@file' => $file->getRealPath(), '@destination' => $fileEntity->getFileUri()]); |
| 218 | + return $response; |
| 219 | + } |
| 220 | + |
| 221 | + // Adjust permissions. |
| 222 | + if (!$this->fileSystem->chmod($fileEntity->getFileUri())) { |
| 223 | + $response->setViolation($this->t('Unknown error while uploading the file "@file".', ['@file' => $file->getClientOriginalName()])); |
| 224 | + $this->logger->error('Unable to set file permission for file "@file".', ['@file' => $fileEntity->getFileUri()]); |
| 225 | + return $response; |
| 226 | + } |
| 227 | + |
| 228 | + $response->setFileEntity($fileEntity); |
| 229 | + return $response; |
| 230 | + } |
| 231 | + |
| 232 | +} |
0 commit comments