Loading core/modules/jsonapi/jsonapi.services.yml +1 −12 Original line number Diff line number Diff line Loading @@ -199,11 +199,7 @@ services: Drupal\jsonapi\Controller\EntityResource: '@jsonapi.entity_resource' jsonapi.file_upload: class: Drupal\jsonapi\Controller\FileUpload arguments: - '@current_user' - '@entity_field.manager' - '@jsonapi.file.uploader.field' - '@http_kernel' autowire: true Drupal\jsonapi\Controller\FileUpload: '@jsonapi.file_upload' # Event subscribers. Loading Loading @@ -249,10 +245,3 @@ services: - '@jsonapi.version_negotiator' tags: - { name: route_enhancer } # @todo Remove once https://www.drupal.org/project/drupal/issues/2940383 lands. jsonapi.file.uploader.field: class: Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader public: false arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory', '@event_dispatcher', '@file.validator', '@file.input_stream_file_writer'] Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader: '@jsonapi.file.uploader.field' core/modules/jsonapi/src/Controller/FileUpload.php +138 −79 Original line number Diff line number Diff line Loading @@ -6,11 +6,23 @@ use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\Exception\FileExistsException; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Lock\LockAcquiringException; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\file\Upload\ContentDispositionFilenameParser; use Drupal\file\Upload\FileUploadHandler; use Drupal\file\Upload\FileUploadLocationTrait; use Drupal\file\Upload\FileUploadResult; use Drupal\file\Upload\InputStreamFileWriterInterface; use Drupal\file\Upload\InputStreamUploadedFile; use Drupal\file\Validation\FileValidatorSettingsTrait; use Drupal\jsonapi\Entity\EntityValidationTrait; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\JsonApiResource\Link; Loading @@ -21,13 +33,17 @@ use Drupal\jsonapi\ResourceResponse; use Drupal\jsonapi\ResourceType\ResourceType; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; use Symfony\Component\HttpFoundation\File\Exception\NoFileException; use Symfony\Component\HttpFoundation\File\Exception\UploadException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; /** * Handles file upload requests. Loading @@ -41,52 +57,17 @@ class FileUpload { use EntityValidationTrait; use FileUploadLocationTrait; use FileValidatorSettingsTrait; /** * The current user making the request. * * @var \Drupal\Core\Session\AccountInterface */ protected $currentUser; /** * The field manager. * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $fieldManager; /** * The file uploader. * * @var \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader */ protected $fileUploader; /** * An HTTP kernel for making subrequests. * * @var \Symfony\Component\HttpKernel\HttpKernelInterface */ protected $httpKernel; /** * Creates a new FileUpload instance. * * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager * The entity field manager. * @param \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader $file_uploader * The file uploader. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel * An HTTP kernel for making subrequests. */ public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, TemporaryJsonapiFileFieldUploader $file_uploader, HttpKernelInterface $http_kernel) { $this->currentUser = $current_user; $this->fieldManager = $field_manager; $this->fileUploader = $file_uploader; $this->httpKernel = $http_kernel; public function __construct( protected AccountInterface $currentUser, protected EntityFieldManagerInterface $fieldManager, protected FileUploadHandler $fileUploadHandler, protected HttpKernelInterface $httpKernel, protected InputStreamFileWriterInterface $inputStreamFileWriter, protected FileSystemInterface $fileSystem, ) { } /** Loading @@ -112,23 +93,9 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn * Thrown if an exception occurs during a subrequest to fetch the newly * created file entity. */ public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) { $file_field_name = $resource_type->getInternalName($file_field_name); $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name); static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity); $filename = ContentDispositionFilenameParser::parseFilename($request); $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser); if ($file instanceof ConstraintViolationListInterface) { $violations = $file; $message = "Unprocessable Entity: file validation failed.\n"; $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) { return PlainTextOutput::renderFromHtml($violation->getMessage()); }, (array) $violations->getIterator())); throw new UnprocessableEntityHttpException($message); } public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, string $file_field_name, FieldableEntityInterface $entity): Response { $result = $this->handleFileUploadForResource($request, $resource_type, $file_field_name, $entity); $file = $result->getFile(); if ($resource_type->getFieldByInternalName($file_field_name)->hasOne()) { $entity->{$file_field_name} = $file; Loading Loading @@ -162,33 +129,125 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException * Thrown when there are validation errors. */ public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) { public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, string $file_field_name): ResourceResponse { $result = $this->handleFileUploadForResource($request, $resource_type, $file_field_name); $file = $result->getFile(); // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463. $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), 'self'); /* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */ $links = new LinkCollection(['self' => $self_link]); $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name)); $file_resource_type = reset($relatable_resource_types); $resource_object = ResourceObject::createFromEntity($file_resource_type, $file); return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []); } /** * Handles JSON:API file upload requests. * * @param \Symfony\Component\HttpFoundation\Request $request * The HTTP request object. * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The JSON:API resource type for the current request. * @param string $file_field_name * The file field for which the file is to be uploaded. * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity * (optional) The entity for which the file is to be uploaded. * * @return \Drupal\file\Upload\FileUploadResult * The file upload result. */ protected function handleFileUploadForResource(Request $request, ResourceType $resource_type, string $file_field_name, ?FieldableEntityInterface $entity = NULL): FileUploadResult { $file_field_name = $resource_type->getInternalName($file_field_name); $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name); static::ensureFileUploadAccess($this->currentUser, $field_definition); static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity); $filename = ContentDispositionFilenameParser::parseFilename($request); $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser); $tempPath = $this->inputStreamFileWriter->writeStreamToFile(); $uploadedFile = new InputStreamUploadedFile($filename, $filename, $tempPath, @filesize($tempPath)); $settings = $field_definition->getSettings(); $validators = $this->getFileUploadValidators($settings); if (!array_key_exists('FileExtension', $validators) && $settings['file_extensions'] === '') { // An empty string means 'all file extensions' but the FileUploadHandler // needs the FileExtension entry to be present and empty in order for this // to be respected. An empty array means 'all file extensions'. // @see \Drupal\file\Upload\FileUploadHandler::handleExtensionValidation $validators['FileExtension'] = []; } $destination = $this->getUploadLocation($field_definition); if ($file instanceof ConstraintViolationListInterface) { $violations = $file; // Check the destination file path is writable. if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) { throw new HttpException(500, 'Destination file path is not writable'); } try { $result = $this->fileUploadHandler->handleFileUpload($uploadedFile, $validators, $destination, FileExists::Rename, FALSE); } catch (LockAcquiringException $e) { throw new HttpException(503, $e->getMessage(), NULL, ['Retry-After' => 1]); } catch (UploadException $e) { throw new HttpException(500, 'Input file data could not be read', $e); } catch (CannotWriteFileException $e) { throw new HttpException(500, 'Temporary file data could not be written', $e); } catch (NoFileException $e) { throw new HttpException(500, 'Temporary file could not be opened', $e); } catch (FileExistsException $e) { throw new HttpException(500, $e->getMessage(), $e); } catch (FileException $e) { throw new HttpException(500, 'Temporary file could not be moved to file location'); } if ($result->hasViolations()) { $message = "Unprocessable Entity: file validation failed.\n"; $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) { return PlainTextOutput::renderFromHtml($violation->getMessage()); }, iterator_to_array($violations))); }, (array) $result->getViolations()->getIterator())); throw new UnprocessableEntityHttpException($message); } // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463. $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), 'self'); /* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */ $links = new LinkCollection(['self' => $self_link]); return $result; } $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name)); $file_resource_type = reset($relatable_resource_types); $resource_object = ResourceObject::createFromEntity($file_resource_type, $file); return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []); /** * Checks if the current user has access to upload the file. * * @param \Drupal\Core\Session\AccountInterface $account * The account for which file upload access should be checked. * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field definition for which to get validators. * @param \Drupal\Core\Entity\EntityInterface $entity * (optional) The entity to which the file is to be uploaded, if it exists. * If the entity does not exist and it is not given, create access to the * entity the file is attached to will be checked. * * @return \Drupal\Core\Access\AccessResultInterface * The file upload access result. */ public static function checkFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, EntityInterface $entity = NULL) { assert(is_null($entity) || $field_definition->getTargetEntityTypeId() === $entity->getEntityTypeId() && // Base fields do not have target bundles. (is_null($field_definition->getTargetBundle()) || $field_definition->getTargetBundle() === $entity->bundle()) ); $entity_type_manager = \Drupal::entityTypeManager(); $entity_access_control_handler = $entity_type_manager->getAccessControlHandler($field_definition->getTargetEntityTypeId()); $bundle = $entity_type_manager->getDefinition($field_definition->getTargetEntityTypeId())->hasKey('bundle') ? $field_definition->getTargetBundle() : NULL; $entity_access_result = $entity ? $entity_access_control_handler->access($entity, 'update', $account, TRUE) : $entity_access_control_handler->createAccess($bundle, $account, [], TRUE); $field_access_result = $entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE); return $entity_access_result->andIf($field_access_result); } /** Loading @@ -203,8 +262,8 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r */ protected static function ensureFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, FieldableEntityInterface $entity = NULL) { $access_result = $entity ? TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition, $entity) : TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition); ? static::checkFileUploadAccess($account, $field_definition, $entity) : static::checkFileUploadAccess($account, $field_definition); if (!$access_result->isAllowed()) { $reason = 'The current user is not permitted to upload a file for this field.'; if ($access_result instanceof AccessResultReasonInterface) { Loading core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.phpdeleted 100644 → 0 +0 −284 File deleted.Preview size limit exceeded, changes collapsed. Show changes core/modules/jsonapi/tests/src/Functional/FileUploadTest.php +1 −0 Original line number Diff line number Diff line Loading @@ -785,6 +785,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) { protected function getExpectedDocument($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE, $expected_status = FALSE) { $author = User::load($this->account->id()); $file = File::load($fid); $this->assertInstanceOf(File::class, $file); $self_url = Url::fromUri('base:/jsonapi/file/file/' . $file->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl(); return [ Loading core/modules/jsonapi/tests/src/Kernel/Controller/TemporaryJsonapiFileFieldUploaderTest.php→core/modules/jsonapi/tests/src/Kernel/Controller/FileUploadTest.php +19 −19 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ namespace Drupal\Tests\jsonapi\Kernel\Controller; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader; use Drupal\jsonapi\Controller\FileUpload; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase; Loading @@ -13,10 +13,10 @@ use Drupal\user\Entity\User; /** * @coversDefaultClass \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader * @coversDefaultClass \Drupal\jsonapi\Controller\FileUpload * @group jsonapi */ class TemporaryJsonapiFileFieldUploaderTest extends JsonapiKernelTestBase { class FileUploadTest extends JsonapiKernelTestBase { /** * {@inheritdoc} Loading Loading @@ -134,49 +134,49 @@ public function testCheckFileUploadAccessWithBaseField() { // Tests the expected access result for each user. // The $article_editor account can edit any article. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition, $node); $this->assertTrue($result->isAllowed()); // The article editor cannot create a node of undetermined type. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition); $this->assertFalse($result->isAllowed()); // The article editor can edit any article. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition, $node); $this->assertTrue($result->isAllowed()); // The article editor can create an article. The type can be determined // because the field is a bundle field. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition); $this->assertTrue($result->isAllowed()); // The $editor account has the bypass node access permissions and can edit // and create all node types. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($editor, $base_field_definition, $node); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($editor, $base_field_definition); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition, $node); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition); $this->assertTrue($result->isAllowed()); // The $page_editor account can only edit and create pages therefore has no // access. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition); $this->assertFalse($result->isAllowed()); // The $no_access_user account has no access at all. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $base_field_definition); $result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition); $this->assertFalse($result->isAllowed()); } Loading Loading
core/modules/jsonapi/jsonapi.services.yml +1 −12 Original line number Diff line number Diff line Loading @@ -199,11 +199,7 @@ services: Drupal\jsonapi\Controller\EntityResource: '@jsonapi.entity_resource' jsonapi.file_upload: class: Drupal\jsonapi\Controller\FileUpload arguments: - '@current_user' - '@entity_field.manager' - '@jsonapi.file.uploader.field' - '@http_kernel' autowire: true Drupal\jsonapi\Controller\FileUpload: '@jsonapi.file_upload' # Event subscribers. Loading Loading @@ -249,10 +245,3 @@ services: - '@jsonapi.version_negotiator' tags: - { name: route_enhancer } # @todo Remove once https://www.drupal.org/project/drupal/issues/2940383 lands. jsonapi.file.uploader.field: class: Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader public: false arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory', '@event_dispatcher', '@file.validator', '@file.input_stream_file_writer'] Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader: '@jsonapi.file.uploader.field'
core/modules/jsonapi/src/Controller/FileUpload.php +138 −79 Original line number Diff line number Diff line Loading @@ -6,11 +6,23 @@ use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\Exception\FileExistsException; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Lock\LockAcquiringException; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\file\Upload\ContentDispositionFilenameParser; use Drupal\file\Upload\FileUploadHandler; use Drupal\file\Upload\FileUploadLocationTrait; use Drupal\file\Upload\FileUploadResult; use Drupal\file\Upload\InputStreamFileWriterInterface; use Drupal\file\Upload\InputStreamUploadedFile; use Drupal\file\Validation\FileValidatorSettingsTrait; use Drupal\jsonapi\Entity\EntityValidationTrait; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\JsonApiResource\Link; Loading @@ -21,13 +33,17 @@ use Drupal\jsonapi\ResourceResponse; use Drupal\jsonapi\ResourceType\ResourceType; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; use Symfony\Component\HttpFoundation\File\Exception\NoFileException; use Symfony\Component\HttpFoundation\File\Exception\UploadException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; /** * Handles file upload requests. Loading @@ -41,52 +57,17 @@ class FileUpload { use EntityValidationTrait; use FileUploadLocationTrait; use FileValidatorSettingsTrait; /** * The current user making the request. * * @var \Drupal\Core\Session\AccountInterface */ protected $currentUser; /** * The field manager. * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $fieldManager; /** * The file uploader. * * @var \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader */ protected $fileUploader; /** * An HTTP kernel for making subrequests. * * @var \Symfony\Component\HttpKernel\HttpKernelInterface */ protected $httpKernel; /** * Creates a new FileUpload instance. * * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager * The entity field manager. * @param \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader $file_uploader * The file uploader. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel * An HTTP kernel for making subrequests. */ public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, TemporaryJsonapiFileFieldUploader $file_uploader, HttpKernelInterface $http_kernel) { $this->currentUser = $current_user; $this->fieldManager = $field_manager; $this->fileUploader = $file_uploader; $this->httpKernel = $http_kernel; public function __construct( protected AccountInterface $currentUser, protected EntityFieldManagerInterface $fieldManager, protected FileUploadHandler $fileUploadHandler, protected HttpKernelInterface $httpKernel, protected InputStreamFileWriterInterface $inputStreamFileWriter, protected FileSystemInterface $fileSystem, ) { } /** Loading @@ -112,23 +93,9 @@ public function __construct(AccountInterface $current_user, EntityFieldManagerIn * Thrown if an exception occurs during a subrequest to fetch the newly * created file entity. */ public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, $file_field_name, FieldableEntityInterface $entity) { $file_field_name = $resource_type->getInternalName($file_field_name); $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name); static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity); $filename = ContentDispositionFilenameParser::parseFilename($request); $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser); if ($file instanceof ConstraintViolationListInterface) { $violations = $file; $message = "Unprocessable Entity: file validation failed.\n"; $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) { return PlainTextOutput::renderFromHtml($violation->getMessage()); }, (array) $violations->getIterator())); throw new UnprocessableEntityHttpException($message); } public function handleFileUploadForExistingResource(Request $request, ResourceType $resource_type, string $file_field_name, FieldableEntityInterface $entity): Response { $result = $this->handleFileUploadForResource($request, $resource_type, $file_field_name, $entity); $file = $result->getFile(); if ($resource_type->getFieldByInternalName($file_field_name)->hasOne()) { $entity->{$file_field_name} = $file; Loading Loading @@ -162,33 +129,125 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException * Thrown when there are validation errors. */ public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, $file_field_name) { public function handleFileUploadForNewResource(Request $request, ResourceType $resource_type, string $file_field_name): ResourceResponse { $result = $this->handleFileUploadForResource($request, $resource_type, $file_field_name); $file = $result->getFile(); // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463. $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), 'self'); /* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */ $links = new LinkCollection(['self' => $self_link]); $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name)); $file_resource_type = reset($relatable_resource_types); $resource_object = ResourceObject::createFromEntity($file_resource_type, $file); return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []); } /** * Handles JSON:API file upload requests. * * @param \Symfony\Component\HttpFoundation\Request $request * The HTTP request object. * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The JSON:API resource type for the current request. * @param string $file_field_name * The file field for which the file is to be uploaded. * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity * (optional) The entity for which the file is to be uploaded. * * @return \Drupal\file\Upload\FileUploadResult * The file upload result. */ protected function handleFileUploadForResource(Request $request, ResourceType $resource_type, string $file_field_name, ?FieldableEntityInterface $entity = NULL): FileUploadResult { $file_field_name = $resource_type->getInternalName($file_field_name); $field_definition = $this->validateAndLoadFieldDefinition($resource_type->getEntityTypeId(), $resource_type->getBundle(), $file_field_name); static::ensureFileUploadAccess($this->currentUser, $field_definition); static::ensureFileUploadAccess($this->currentUser, $field_definition, $entity); $filename = ContentDispositionFilenameParser::parseFilename($request); $file = $this->fileUploader->handleFileUploadForField($field_definition, $filename, $this->currentUser); $tempPath = $this->inputStreamFileWriter->writeStreamToFile(); $uploadedFile = new InputStreamUploadedFile($filename, $filename, $tempPath, @filesize($tempPath)); $settings = $field_definition->getSettings(); $validators = $this->getFileUploadValidators($settings); if (!array_key_exists('FileExtension', $validators) && $settings['file_extensions'] === '') { // An empty string means 'all file extensions' but the FileUploadHandler // needs the FileExtension entry to be present and empty in order for this // to be respected. An empty array means 'all file extensions'. // @see \Drupal\file\Upload\FileUploadHandler::handleExtensionValidation $validators['FileExtension'] = []; } $destination = $this->getUploadLocation($field_definition); if ($file instanceof ConstraintViolationListInterface) { $violations = $file; // Check the destination file path is writable. if (!$this->fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY)) { throw new HttpException(500, 'Destination file path is not writable'); } try { $result = $this->fileUploadHandler->handleFileUpload($uploadedFile, $validators, $destination, FileExists::Rename, FALSE); } catch (LockAcquiringException $e) { throw new HttpException(503, $e->getMessage(), NULL, ['Retry-After' => 1]); } catch (UploadException $e) { throw new HttpException(500, 'Input file data could not be read', $e); } catch (CannotWriteFileException $e) { throw new HttpException(500, 'Temporary file data could not be written', $e); } catch (NoFileException $e) { throw new HttpException(500, 'Temporary file could not be opened', $e); } catch (FileExistsException $e) { throw new HttpException(500, $e->getMessage(), $e); } catch (FileException $e) { throw new HttpException(500, 'Temporary file could not be moved to file location'); } if ($result->hasViolations()) { $message = "Unprocessable Entity: file validation failed.\n"; $message .= implode("\n", array_map(function (ConstraintViolationInterface $violation) { return PlainTextOutput::renderFromHtml($violation->getMessage()); }, iterator_to_array($violations))); }, (array) $result->getViolations()->getIterator())); throw new UnprocessableEntityHttpException($message); } // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463. $self_link = new Link(new CacheableMetadata(), Url::fromRoute('jsonapi.file--file.individual', ['entity' => $file->uuid()]), 'self'); /* $self_link = new Link(new CacheableMetadata(), $this->entity->toUrl('jsonapi'), ['self']); */ $links = new LinkCollection(['self' => $self_link]); return $result; } $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($resource_type->getPublicName($file_field_name)); $file_resource_type = reset($relatable_resource_types); $resource_object = ResourceObject::createFromEntity($file_resource_type, $file); return new ResourceResponse(new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), $links), 201, []); /** * Checks if the current user has access to upload the file. * * @param \Drupal\Core\Session\AccountInterface $account * The account for which file upload access should be checked. * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field definition for which to get validators. * @param \Drupal\Core\Entity\EntityInterface $entity * (optional) The entity to which the file is to be uploaded, if it exists. * If the entity does not exist and it is not given, create access to the * entity the file is attached to will be checked. * * @return \Drupal\Core\Access\AccessResultInterface * The file upload access result. */ public static function checkFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, EntityInterface $entity = NULL) { assert(is_null($entity) || $field_definition->getTargetEntityTypeId() === $entity->getEntityTypeId() && // Base fields do not have target bundles. (is_null($field_definition->getTargetBundle()) || $field_definition->getTargetBundle() === $entity->bundle()) ); $entity_type_manager = \Drupal::entityTypeManager(); $entity_access_control_handler = $entity_type_manager->getAccessControlHandler($field_definition->getTargetEntityTypeId()); $bundle = $entity_type_manager->getDefinition($field_definition->getTargetEntityTypeId())->hasKey('bundle') ? $field_definition->getTargetBundle() : NULL; $entity_access_result = $entity ? $entity_access_control_handler->access($entity, 'update', $account, TRUE) : $entity_access_control_handler->createAccess($bundle, $account, [], TRUE); $field_access_result = $entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE); return $entity_access_result->andIf($field_access_result); } /** Loading @@ -203,8 +262,8 @@ public function handleFileUploadForNewResource(Request $request, ResourceType $r */ protected static function ensureFileUploadAccess(AccountInterface $account, FieldDefinitionInterface $field_definition, FieldableEntityInterface $entity = NULL) { $access_result = $entity ? TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition, $entity) : TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($account, $field_definition); ? static::checkFileUploadAccess($account, $field_definition, $entity) : static::checkFileUploadAccess($account, $field_definition); if (!$access_result->isAllowed()) { $reason = 'The current user is not permitted to upload a file for this field.'; if ($access_result instanceof AccessResultReasonInterface) { Loading
core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.phpdeleted 100644 → 0 +0 −284 File deleted.Preview size limit exceeded, changes collapsed. Show changes
core/modules/jsonapi/tests/src/Functional/FileUploadTest.php +1 −0 Original line number Diff line number Diff line Loading @@ -785,6 +785,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) { protected function getExpectedDocument($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE, $expected_status = FALSE) { $author = User::load($this->account->id()); $file = File::load($fid); $this->assertInstanceOf(File::class, $file); $self_url = Url::fromUri('base:/jsonapi/file/file/' . $file->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl(); return [ Loading
core/modules/jsonapi/tests/src/Kernel/Controller/TemporaryJsonapiFileFieldUploaderTest.php→core/modules/jsonapi/tests/src/Kernel/Controller/FileUploadTest.php +19 −19 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ namespace Drupal\Tests\jsonapi\Kernel\Controller; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader; use Drupal\jsonapi\Controller\FileUpload; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase; Loading @@ -13,10 +13,10 @@ use Drupal\user\Entity\User; /** * @coversDefaultClass \Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader * @coversDefaultClass \Drupal\jsonapi\Controller\FileUpload * @group jsonapi */ class TemporaryJsonapiFileFieldUploaderTest extends JsonapiKernelTestBase { class FileUploadTest extends JsonapiKernelTestBase { /** * {@inheritdoc} Loading Loading @@ -134,49 +134,49 @@ public function testCheckFileUploadAccessWithBaseField() { // Tests the expected access result for each user. // The $article_editor account can edit any article. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition, $node); $this->assertTrue($result->isAllowed()); // The article editor cannot create a node of undetermined type. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($article_editor, $base_field_definition); $this->assertFalse($result->isAllowed()); // The article editor can edit any article. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition, $node); $this->assertTrue($result->isAllowed()); // The article editor can create an article. The type can be determined // because the field is a bundle field. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($article_editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($article_editor, $bundle_field_definition); $this->assertTrue($result->isAllowed()); // The $editor account has the bypass node access permissions and can edit // and create all node types. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($editor, $base_field_definition, $node); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($editor, $base_field_definition); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition, $node); $this->assertTrue($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($editor, $bundle_field_definition); $this->assertTrue($result->isAllowed()); // The $page_editor account can only edit and create pages therefore has no // access. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $base_field_definition); $result = FileUpload::checkFileUploadAccess($page_editor, $base_field_definition); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($page_editor, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($page_editor, $bundle_field_definition); $this->assertFalse($result->isAllowed()); // The $no_access_user account has no access at all. $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $base_field_definition, $node); $result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $base_field_definition); $result = FileUpload::checkFileUploadAccess($no_access_user, $base_field_definition); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $bundle_field_definition, $node); $result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition, $node); $this->assertFalse($result->isAllowed()); $result = TemporaryJsonapiFileFieldUploader::checkFileUploadAccess($no_access_user, $bundle_field_definition); $result = FileUpload::checkFileUploadAccess($no_access_user, $bundle_field_definition); $this->assertFalse($result->isAllowed()); } Loading