summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/modules/file/file.module7
-rw-r--r--core/modules/file/src/FileAccessControlHandler.php2
-rw-r--r--core/modules/file/src/FileServiceProvider.php23
-rw-r--r--core/modules/file/src/Plugin/rest/resource/FileUploadResource.php586
-rw-r--r--core/modules/file/tests/src/Functional/FileUploadJsonBasicAuthTest.php35
-rw-r--r--core/modules/file/tests/src/Functional/FileUploadJsonCookieTest.php30
-rw-r--r--core/modules/hal/hal.services.yml2
-rw-r--r--core/modules/hal/src/Normalizer/FileEntityNormalizer.php18
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php8
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonBasicAuthTest.php24
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonCookieTest.php19
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php97
-rw-r--r--core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php54
-rw-r--r--core/modules/hal/tests/src/Functional/FileDenormalizeTest.php89
-rw-r--r--core/modules/rest/src/RequestHandler.php46
-rw-r--r--core/modules/rest/src/Routing/ResourceRoutes.php8
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php39
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php7
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php160
-rw-r--r--core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php725
-rw-r--r--core/modules/rest/tests/src/Functional/ResourceTestBase.php25
21 files changed, 161 insertions, 1843 deletions
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 8fd6823..c9e290c 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -19,11 +19,6 @@ use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Template\Attribute;
-/**
- * The regex pattern used when checking for insecure file types.
- */
-define('FILE_INSECURE_EXTENSION_REGEX', '/\.(php|pl|py|cgi|asp|js)(\.|$)/i');
-
// Load all Field module hooks for File.
require_once __DIR__ . '/file.field.inc';
@@ -959,7 +954,7 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
// rename filename.php.foo and filename.php to filename.php.foo.txt and
// filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
// evaluates to TRUE.
- if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
+ if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
$file->setMimeType('text/plain');
// The destination filename will also later be used to create the URI.
$file->setFilename($file->getFilename() . '.txt');
diff --git a/core/modules/file/src/FileAccessControlHandler.php b/core/modules/file/src/FileAccessControlHandler.php
index 07f9cec..e378b64 100644
--- a/core/modules/file/src/FileAccessControlHandler.php
+++ b/core/modules/file/src/FileAccessControlHandler.php
@@ -127,6 +127,8 @@ class FileAccessControlHandler extends EntityAccessControlHandler {
// create file entities that are referenced from another entity
// (e.g. an image for a article). A contributed module is free to alter
// this to allow file entities to be created directly.
+ // @todo Update comment to mention REST module when
+ // https://www.drupal.org/node/1927648 is fixed.
return AccessResult::neutral();
}
diff --git a/core/modules/file/src/FileServiceProvider.php b/core/modules/file/src/FileServiceProvider.php
deleted file mode 100644
index a22750a..0000000
--- a/core/modules/file/src/FileServiceProvider.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-namespace Drupal\file;
-
-use Drupal\Core\DependencyInjection\ContainerBuilder;
-use Drupal\Core\DependencyInjection\ServiceModifierInterface;
-use Drupal\Core\StackMiddleware\NegotiationMiddleware;
-
-/**
- * Adds 'application/octet-stream' as a known (bin) format.
- */
-class FileServiceProvider implements ServiceModifierInterface {
-
- /**
- * {@inheritdoc}
- */
- public function alter(ContainerBuilder $container) {
- if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
- $container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]);
- }
- }
-
-}
diff --git a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
deleted file mode 100644
index e9cbb57..0000000
--- a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
+++ /dev/null
@@ -1,586 +0,0 @@
-<?php
-
-namespace Drupal\file\Plugin\rest\resource;
-
-use Drupal\Component\Utility\Bytes;
-use Drupal\Component\Utility\Crypt;
-use Drupal\Core\Config\Config;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Lock\LockBackendInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Utility\Token;
-use Drupal\file\FileInterface;
-use Drupal\rest\ModifiedResourceResponse;
-use Drupal\rest\Plugin\ResourceBase;
-use Drupal\Component\Render\PlainTextOutput;
-use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\Core\File\FileSystem;
-use Drupal\file\Entity\File;
-use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
-use Drupal\rest\RequestHandler;
-use Psr\Log\LoggerInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
-use Symfony\Component\Routing\Route;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpKernel\Exception\HttpException;
-
-/**
- * File upload resource.
- *
- * This is implemented as a field-level resource for the following reasons:
- * - Validation for uploaded files is tied to fields (allowed extensions, max
- * size, etc..).
- * - The actual files do not need to be stored in another temporary location,
- * to be later moved when they are referenced from a file field.
- * - Permission to upload a file can be determined by a users field level
- * create access to the file field.
- *
- * @RestResource(
- * id = "file:upload",
- * label = @Translation("File Upload"),
- * serialization_class = "Drupal\file\Entity\File",
- * uri_paths = {
- * "https://www.drupal.org/link-relations/create" = "/file/upload/{entity_type_id}/{bundle}/{field_name}"
- * }
- * )
- */
-class FileUploadResource extends ResourceBase {
-
- use EntityResourceValidationTrait {
- validate as resourceValidate;
- }
-
- /**
- * The regex used to extract the filename from the content disposition header.
- *
- * @var string
- */
- const REQUEST_HEADER_FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
-
- /**
- * The amount of bytes to read in each iteration when streaming file data.
- *
- * @var int
- */
- const BYTES_TO_READ = 8192;
-
- /**
- * The file system service.
- *
- * @var \Drupal\Core\File\FileSystem
- */
- protected $fileSystem;
-
- /**
- * The entity type manager.
- *
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
- */
- protected $entityTypeManager;
-
- /**
- * The entity field manager.
- *
- * @var \Drupal\Core\Entity\EntityFieldManagerInterface
- */
- protected $entityFieldManager;
-
- /**
- * The currently authenticated user.
- *
- * @var \Drupal\Core\Session\AccountInterface
- */
- protected $currentUser;
-
- /**
- * The MIME type guesser.
- *
- * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
- */
- protected $mimeTypeGuesser;
-
- /**
- * The token replacement instance.
- *
- * @var \Drupal\Core\Utility\Token
- */
- protected $token;
-
- /**
- * The lock service.
- *
- * @var \Drupal\Core\Lock\LockBackendInterface
- */
- protected $lock;
-
- /**
- * @var \Drupal\Core\Config\ImmutableConfig
- */
- protected $systemFileConfig;
-
- /**
- * Constructs a FileUploadResource instance.
- *
- * @param array $configuration
- * A configuration array containing information about the plugin instance.
- * @param string $plugin_id
- * The plugin_id for the plugin instance.
- * @param mixed $plugin_definition
- * The plugin implementation definition.
- * @param array $serializer_formats
- * The available serialization formats.
- * @param \Psr\Log\LoggerInterface $logger
- * A logger instance.
- * @param \Drupal\Core\File\FileSystem $file_system
- * The file system service.
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity type manager.
- * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
- * The entity field manager.
- * @param \Drupal\Core\Session\AccountInterface $current_user
- * The currently authenticated user.
- * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser
- * The MIME type guesser.
- * @param \Drupal\Core\Utility\Token $token
- * The token replacement instance.
- * @param \Drupal\Core\Lock\LockBackendInterface $lock
- * The lock service.
- * @param \Drupal\Core\Config\Config $system_file_config
- * The system file configuration.
- */
- public function __construct(array $configuration, $plugin_id, $plugin_definition, $serializer_formats, LoggerInterface $logger, FileSystem $file_system, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AccountInterface $current_user, MimeTypeGuesserInterface $mime_type_guesser, Token $token, LockBackendInterface $lock, Config $system_file_config) {
- parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
- $this->fileSystem = $file_system;
- $this->entityTypeManager = $entity_type_manager;
- $this->entityFieldManager = $entity_field_manager;
- $this->currentUser = $current_user;
- $this->mimeTypeGuesser = $mime_type_guesser;
- $this->token = $token;
- $this->lock = $lock;
- $this->systemFileConfig = $system_file_config;
- }
-
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $configuration,
- $plugin_id,
- $plugin_definition,
- $container->getParameter('serializer.formats'),
- $container->get('logger.factory')->get('rest'),
- $container->get('file_system'),
- $container->get('entity_type.manager'),
- $container->get('entity_field.manager'),
- $container->get('current_user'),
- $container->get('file.mime_type.guesser'),
- $container->get('token'),
- $container->get('lock'),
- $container->get('config.factory')->get('system.file')
- );
- }
-
- /**
- * {@inheritdoc}
- */
- public function permissions() {
- // Access to this resource depends on field-level access so no explicit
- // permissions are required.
- // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validateAndLoadFieldDefinition()
- // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
- return [];
- }
-
- /**
- * Creates a file from an endpoint.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The current request.
- * @param string $entity_type_id
- * The entity type ID.
- * @param string $bundle
- * The entity bundle. This will be the same as $entity_type_id for entity
- * types that don't support bundles.
- * @param string $field_name
- * The field name.
- *
- * @return \Drupal\rest\ModifiedResourceResponse
- * A 201 response, on success.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\HttpException
- * Thrown when temporary files cannot be written, a lock cannot be acquired,
- * or when temporary files cannot be moved to their new location.
- */
- public function post(Request $request, $entity_type_id, $bundle, $field_name) {
- $filename = $this->validateAndParseContentDispositionHeader($request);
-
- $field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
-
- $destination = $this->getUploadLocation($field_definition->getSettings());
-
- // Check the destination file path is writable.
- if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
- throw new HttpException(500, 'Destination file path is not writable');
- }
-
- $validators = $this->getUploadValidators($field_definition);
-
- $prepared_filename = $this->prepareFilename($filename, $validators);
-
- // Create the file.
- $file_uri = "{$destination}/{$prepared_filename}";
-
- $temp_file_path = $this->streamUploadData();
-
- // This will take care of altering $file_uri if a file already exists.
- file_unmanaged_prepare($temp_file_path, $file_uri);
-
- // Lock based on the prepared file URI.
- $lock_id = $this->generateLockIdFromFileUri($file_uri);
-
- if (!$this->lock->acquire($lock_id)) {
- throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
- }
-
- // Begin building file entity.
- $file = File::create([]);
- $file->setOwnerId($this->currentUser->id());
- $file->setFilename($prepared_filename);
- $file->setMimeType($this->mimeTypeGuesser->guess($prepared_filename));
- $file->setFileUri($file_uri);
- // Set the size. This is done in File::preSave() but we validate the file
- // before it is saved.
- $file->setSize(@filesize($temp_file_path));
-
- // Validate the file entity against entity-level validation and field-level
- // validators.
- $this->validate($file, $validators);
-
- // Move the file to the correct location after validation. Use
- // FILE_EXISTS_ERROR as the file location has already been determined above
- // in file_unmanaged_prepare().
- if (!file_unmanaged_move($temp_file_path, $file_uri, FILE_EXISTS_ERROR)) {
- throw new HttpException(500, 'Temporary file could not be moved to file location');
- }
-
- $file->save();
-
- $this->lock->release($lock_id);
-
- // 201 Created responses return the newly created entity in the response
- // body. These responses are not cacheable, so we add no cacheability
- // metadata here.
- return new ModifiedResourceResponse($file, 201);
- }
-
- /**
- * Streams file upload data to temporary file and moves to file destination.
- *
- * @return string
- * The temp file path.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\HttpException
- * Thrown when input data cannot be read, the temporary file cannot be
- * opened, or the temporary file cannot be written.
- */
- protected function streamUploadData() {
- // 'rb' is needed so reading works correctly on Windows environments too.
- $file_data = fopen('php://input', 'rb');
-
- $temp_file_path = $this->fileSystem->tempnam('temporary://', 'file');
- $temp_file = fopen($temp_file_path, 'wb');
-
- if ($temp_file) {
- while (!feof($file_data)) {
- $read = fread($file_data, static::BYTES_TO_READ);
-
- if ($read === FALSE) {
- // Close the file streams.
- fclose($temp_file);
- fclose($file_data);
- $this->logger->error('Input data could not be read');
- throw new HttpException(500, 'Input file data could not be read');
- }
-
- if (fwrite($temp_file, $read) === FALSE) {
- // Close the file streams.
- fclose($temp_file);
- fclose($file_data);
- $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
- throw new HttpException(500, 'Temporary file data could not be written');
- }
- }
-
- // Close the temp file stream.
- fclose($temp_file);
- }
- else {
- // Close the file streams.
- fclose($temp_file);
- fclose($file_data);
- $this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
- throw new HttpException(500, 'Temporary file could not be opened');
- }
-
- // Close the input stream.
- fclose($file_data);
-
- return $temp_file_path;
- }
-
- /**
- * Validates and extracts the filename from the Content-Disposition header.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The request object.
- *
- * @return string
- * The filename extracted from the header.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
- * Thrown when the 'Content-Disposition' request header is invalid.
- */
- protected function validateAndParseContentDispositionHeader(Request $request) {
- // Firstly, check the header exists.
- if (!$request->headers->has('content-disposition')) {
- throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided');
- }
-
- $content_disposition = $request->headers->get('content-disposition');
-
- // Parse the header value. This regex does not allow an empty filename.
- // i.e. 'filename=""'. This also matches on a word boundary so other keys
- // like 'not_a_filename' don't work.
- if (!preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $content_disposition, $matches)) {
- throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided');
- }
-
- // Check for the "filename*" format. This is currently unsupported.
- if (!empty($matches['star'])) {
- throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition" header');
- }
-
- // Don't validate the actual filename here, that will be done by the upload
- // validators in validate().
- // @see \Drupal\file\Plugin\rest\resource\FileUploadResource::validate()
- $filename = $matches['filename'];
-
- // Make sure only the filename component is returned. Path information is
- // stripped as per https://tools.ietf.org/html/rfc6266#section-4.3.
- return basename($filename);
- }
-
- /**
- * Validates and loads a field definition instance.
- *
- * @param string $entity_type_id
- * The entity type ID the field is attached to.
- * @param string $bundle
- * The bundle the field is attached to.
- * @param string $field_name
- * The field name.
- *
- * @return \Drupal\Core\Field\FieldDefinitionInterface
- * The field definition.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
- * Thrown when the field does not exist.
- * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
- * Thrown when the target type of the field is not a file, or the current
- * user does not have 'edit' access for the field.
- */
- protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name) {
- $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
- if (!isset($field_definitions[$field_name])) {
- throw new NotFoundHttpException(sprintf('Field "%s" does not exist', $field_name));
- }
-
- /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
- $field_definition = $field_definitions[$field_name];
- if ($field_definition->getSetting('target_type') !== 'file') {
- throw new AccessDeniedHttpException(sprintf('"%s" is not a file field', $field_name));
- }
-
- $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
- $bundle = $this->entityTypeManager->getDefinition($entity_type_id)->hasKey('bundle') ? $bundle : NULL;
- $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
- ->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
- if (!$access_result->isAllowed()) {
- throw new AccessDeniedHttpException($access_result->getReason());
- }
-
- return $field_definition;
- }
-
- /**
- * Validates the file.
- *
- * @param \Drupal\file\FileInterface $file
- * The file entity to validate.
- * @param array $validators
- * An array of upload validators to pass to file_validate().
- *
- * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
- * Thrown when there are file validation errors.
- */
- protected function validate(FileInterface $file, array $validators) {
- $this->resourceValidate($file);
-
- // Validate the file based on the field definition configuration.
- $errors = file_validate($file, $validators);
-
- if (!empty($errors)) {
- $message = "Unprocessable Entity: file validation failed.\n";
- $message .= implode("\n", array_map(function ($error) {
- return PlainTextOutput::renderFromHtml($error);
- }, $errors));
-
- throw new UnprocessableEntityHttpException($message);
- }
- }
-
- /**
- * Prepares the filename to strip out any malicious extensions.
- *
- * @param string $filename
- * The file name.
- * @param array $validators
- * The array of upload validators.
- *
- * @return string
- * The prepared/munged filename.
- */
- protected function prepareFilename($filename, array &$validators) {
- if (!empty($validators['file_validate_extensions'][0])) {
- // If there is a file_validate_extensions validator and a list of
- // valid extensions, munge the filename to protect against possible
- // malicious extension hiding within an unknown file type. For example,
- // "filename.html.foo".
- $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
- }
-
- // Rename potentially executable files, to help prevent exploits (i.e. will
- // rename filename.php.foo and filename.php to filename.php.foo.txt and
- // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
- // evaluates to TRUE.
- if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
- // The destination filename will also later be used to create the URI.
- $filename .= '.txt';
-
- // The .txt extension may not be in the allowed list of extensions. We
- // have to add it here or else the file upload will fail.
- if (!empty($validators['file_validate_extensions'][0])) {
- $validators['file_validate_extensions'][0] .= ' txt';
- }
- }
-
- return $filename;
- }
-
- /**
- * Determines the URI for a file field.
- *
- * @param array $settings
- * The array of field settings.
- *
- * @return string
- * An un-sanitized file directory URI with tokens replaced. The result of
- * the token replacement is then converted to plain text and returned.
- */
- protected function getUploadLocation(array $settings) {
- $destination = trim($settings['file_directory'], '/');
-
- // Replace tokens. As the tokens might contain HTML we convert it to plain
- // text.
- $destination = PlainTextOutput::renderFromHtml($this->token->replace($destination, []));
- return $settings['uri_scheme'] . '://' . $destination;
- }
-
- /**
- * Retrieves the upload validators for a field definition.
- *
- * This is copied from \Drupal\file\Plugin\Field\FieldType\FileItem as there
- * is no entity instance available here that that a FileItem would exist for.
- *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The field definition for which to get validators.
- *
- * @return array
- * An array suitable for passing to file_save_upload() or the file field
- * element's '#upload_validators' property.
- */
- protected function getUploadValidators(FieldDefinitionInterface $field_definition) {
- $validators = [
- // Add in our check of the file name length.
- 'file_validate_name_length' => [],
- ];
- $settings = $field_definition->getSettings();
-
- // Cap the upload size according to the PHP limit.
- $max_filesize = Bytes::toInt(file_upload_max_size());
- if (!empty($settings['max_filesize'])) {
- $max_filesize = min($max_filesize, Bytes::toInt($settings['max_filesize']));
- }
-
- // There is always a file size limit due to the PHP server limit.
- $validators['file_validate_size'] = [$max_filesize];
-
- // Add the extension check if necessary.
- if (!empty($settings['file_extensions'])) {
- $validators['file_validate_extensions'] = [$settings['file_extensions']];
- }
-
- return $validators;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function getBaseRoute($canonical_path, $method) {
- return new Route($canonical_path, [
- '_controller' => RequestHandler::class . '::handleRaw',
- ],
- $this->getBaseRouteRequirements($method),
- [],
- '',
- [],
- // The HTTP method is a requirement for this route.
- [$method]
- );
- }
-
- /**
- * {@inheritdoc}
- */
- protected function getBaseRouteRequirements($method) {
- $requirements = parent::getBaseRouteRequirements($method);
-
- // Add the content type format access check. This will enforce that all
- // incoming requests can only use the 'application/octet-stream'
- // Content-Type header.
- $requirements['_content_type_format'] = 'bin';
-
- return $requirements;
- }
-
- /**
- * Generates a lock ID based on the file URI.
- *
- * @param $file_uri
- * The file URI.
- *
- * @return string
- * The generated lock ID.
- */
- protected static function generateLockIdFromFileUri($file_uri) {
- return 'file:rest:' . Crypt::hashBase64($file_uri);
- }
-
-}
diff --git a/core/modules/file/tests/src/Functional/FileUploadJsonBasicAuthTest.php b/core/modules/file/tests/src/Functional/FileUploadJsonBasicAuthTest.php
deleted file mode 100644
index cef59f8..0000000
--- a/core/modules/file/tests/src/Functional/FileUploadJsonBasicAuthTest.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Drupal\Tests\file\Functional;
-
-use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
-use Drupal\Tests\rest\Functional\FileUploadResourceTestBase;
-
-/**
- * @group file
- */
-class FileUploadJsonBasicAuthTest extends FileUploadResourceTestBase {
-
- use BasicAuthResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- public static $modules = ['basic_auth'];
-
- /**
- * {@inheritdoc}
- */
- protected static $format = 'json';
-
- /**
- * {@inheritdoc}
- */
- protected static $mimeType = 'application/json';
-
- /**
- * {@inheritdoc}
- */
- protected static $auth = 'basic_auth';
-
-}
diff --git a/core/modules/file/tests/src/Functional/FileUploadJsonCookieTest.php b/core/modules/file/tests/src/Functional/FileUploadJsonCookieTest.php
deleted file mode 100644
index 8f7495a..0000000
--- a/core/modules/file/tests/src/Functional/FileUploadJsonCookieTest.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace Drupal\Tests\file\Functional;
-
-use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
-use Drupal\Tests\rest\Functional\FileUploadResourceTestBase;
-
-/**
- * @group file
- */
-class FileUploadJsonCookieTest extends FileUploadResourceTestBase {
-
- use CookieResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- protected static $format = 'json';
-
- /**
- * {@inheritdoc}
- */
- protected static $mimeType = 'application/json';
-
- /**
- * {@inheritdoc}
- */
- protected static $auth = 'cookie';
-
-}
diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml
index 342836a..1330180 100644
--- a/core/modules/hal/hal.services.yml
+++ b/core/modules/hal/hal.services.yml
@@ -15,7 +15,7 @@ services:
serializer.normalizer.file_entity.hal:
class: Drupal\hal\Normalizer\FileEntityNormalizer
deprecated: 'The "%service_id%" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.'
- arguments: ['@entity.manager', '@hal.link_manager', '@module_handler', '@config.factory']
+ arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler', '@config.factory']
tags:
- { name: normalizer, priority: 20 }
serializer.normalizer.timestamp_item.hal:
diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
index 5aaed3c..5186f8b 100644
--- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
@@ -6,6 +6,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\hal\LinkManager\LinkManagerInterface;
+use GuzzleHttp\ClientInterface;
/**
* Converts the Drupal entity object structure to a HAL array structure.
@@ -40,6 +41,8 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
+ * @param \GuzzleHttp\ClientInterface $http_client
+ * The HTTP Client.
* @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
* The hypermedia link manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
@@ -47,9 +50,10 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
- public function __construct(EntityManagerInterface $entity_manager, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) {
+ public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory) {
parent::__construct($link_manager, $entity_manager, $module_handler);
+ $this->httpClient = $http_client;
$this->halSettings = $config_factory->get('hal.settings');
}
@@ -69,4 +73,16 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
return $data;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function denormalize($data, $class, $format = NULL, array $context = []) {
+ $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
+
+ $path = 'temporary://' . drupal_basename($data['uri'][0]['value']);
+ $data['uri'] = file_unmanaged_save_data($file_data, $path);
+
+ return $this->entityManager->getStorage('file')->create($data);
+ }
+
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php
index ffd64ac..f3036a9 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php
@@ -136,4 +136,12 @@ class FileHalJsonAnonTest extends FileResourceTestBase {
$this->assertSame($this->baseUrl . '/' . $this->siteDirectory . '/files/drupal.txt', $actual['uri'][0]['value']);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function testPatch() {
+ // @todo https://www.drupal.org/node/1927648
+ $this->markTestSkipped();
+ }
+
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonBasicAuthTest.php
deleted file mode 100644
index 9d2348b..0000000
--- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonBasicAuthTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-namespace Drupal\Tests\hal\Functional\EntityResource\File;
-
-use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
-
-/**
- * @group hal
- */
-class FileUploadHalJsonBasicAuthTest extends FileUploadHalJsonTestBase {
-
- use BasicAuthResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- public static $modules = ['basic_auth'];
-
- /**
- * {@inheritdoc}
- */
- protected static $auth = 'basic_auth';
-
-}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonCookieTest.php b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonCookieTest.php
deleted file mode 100644
index dc2dde4..0000000
--- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonCookieTest.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-namespace Drupal\Tests\hal\Functional\EntityResource\File;
-
-use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
-
-/**
- * @group hal
- */
-class FileUploadHalJsonCookieTest extends FileUploadHalJsonTestBase {
-
- use CookieResourceTestTrait;
-
- /**
- * {@inheritdoc}
- */
- protected static $auth = 'cookie';
-
-}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
deleted file mode 100644
index 58df16b..0000000
--- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
+++ /dev/null
@@ -1,97 +0,0 @@
-<?php
-
-namespace Drupal\Tests\hal\Functional\EntityResource\File;
-
-use Drupal\Tests\rest\Functional\FileUploadResourceTestBase;
-use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
-
-/**
- * Tests binary data file upload route for HAL JSON.
- */
-abstract class FileUploadHalJsonTestBase extends FileUploadResourceTestBase {
-
- use HalEntityNormalizationTrait;
-
- /**
- * {@inheritdoc}
- */
- public static $modules = ['hal'];
-
- /**
- * {@inheritdoc}
- */
- protected static $format = 'hal_json';
-
- /**
- * {@inheritdoc}
- */
- protected static $mimeType = 'application/hal+json';
-
- /**
- * {@inheritdoc}
- */
- protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE) {
- $normalization = parent::getExpectedNormalizedEntity($fid, $expected_filename, $expected_as_filename);
-
- // Cannot use applyHalFieldNormalization() as it uses the $entity property
- // from the test class, which in the case of file upload tests, is the
- // parent entity test entity for the file that's created.
-
- // The HAL normalization adds entity reference fields to '_links' and
- // '_embedded'.
- unset($normalization['uid']);
-
- return $normalization + [
- '_links' => [
- 'self' => [
- // @todo This can use a proper link once
- // https://www.drupal.org/project/drupal/issues/2907402 is complete.
- // This link matches what is generated from from File::url(), a
- // resource URL is currently not available.
- 'href' => file_create_url($normalization['uri'][0]['value']),
- ],
- 'type' => [
- 'href' => $this->baseUrl . '/rest/type/file/file',
- ],
- $this->baseUrl . '/rest/relation/file/file/uid' => [
- ['href' => $this->baseUrl . '/user/' . $this->account->id() . '?_format=hal_json']
- ],
- ],
- '_embedded' => [
- $this->baseUrl . '/rest/relation/file/file/uid' => [
- [
- '_links' => [
- 'self' => [
- 'href' => $this->baseUrl . '/user/' . $this->account->id() . '?_format=hal_json',
- ],
- 'type' => [
- 'href' => $this->baseUrl . '/rest/type/user/user',
- ],
- ],
- 'uuid' => [
- [
- 'value' => $this->account->uuid(),
- ],
- ],
- ],
- ],
- ],
- ];
- }
-
- /**
- * {@inheritdoc}
- *
- * @see \Drupal\Tests\hal\Functional\EntityResource\EntityTest\EntityTestHalJsonAnonTest::getNormalizedPostEntity()
- */
- protected function getNormalizedPostEntity() {
- return parent::getNormalizedPostEntity() + [
- '_links' => [
- 'type' => [
- 'href' => $this->baseUrl . '/rest/type/entity_test/entity_test',
- ],
- ],
- ];
- }
-
-}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
index c5ca04a..c9ee773 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php
@@ -154,60 +154,6 @@ class MediaHalJsonAnonTest extends MediaResourceTestBase {
/**
* {@inheritdoc}
*/
- protected function getExpectedNormalizedFileEntity() {
- $normalization = parent::getExpectedNormalizedFileEntity();
-
- $owner = static::$auth ? $this->account : User::load(0);
-
- // Cannot use applyHalFieldNormalization() as it uses the $entity property
- // from the test class, which in the case of file upload tests, is the
- // parent entity test entity for the file that's created.
-
- // The HAL normalization adds entity reference fields to '_links' and
- // '_embedded'.
- unset($normalization['uid']);
-
- return $normalization + [
- '_links' => [
- 'self' => [
- // @todo This can use a proper link once
- // https://www.drupal.org/project/drupal/issues/2907402 is complete.
- // This link matches what is generated from from File::url(), a
- // resource URL is currently not available.
- 'href' => file_create_url($normalization['uri'][0]['value']),
- ],
- 'type' => [
- 'href' => $this->baseUrl . '/rest/type/file/file',
- ],
- $this->baseUrl . '/rest/relation/file/file/uid' => [
- ['href' => $this->baseUrl . '/user/' . $owner->id() . '?_format=hal_json']
- ],
- ],
- '_embedded' => [
- $this->baseUrl . '/rest/relation/file/file/uid' => [
- [
- '_links' => [
- 'self' => [
- 'href' => $this->baseUrl . '/user/' . $owner->id() . '?_format=hal_json',
- ],
- 'type' => [
- 'href' => $this->baseUrl . '/rest/type/user/user',
- ],
- ],
- 'uuid' => [
- [
- 'value' => $owner->uuid(),
- ],
- ],
- ],
- ],
- ],
- ];
- }
-
- /**
- * {@inheritdoc}
- */
protected function getNormalizedPostEntity() {
return parent::getNormalizedPostEntity() + [
'_links' => [
diff --git a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php
new file mode 100644
index 0000000..9c00902
--- /dev/null
+++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\hal\Functional;
+
+use Drupal\file\Entity\File;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests that file entities can be denormalized in HAL.
+ *
+ * @group hal
+ * @see \Drupal\hal\Normalizer\FileEntityNormalizer
+ */
+class FileDenormalizeTest extends BrowserTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['hal', 'file', 'node'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // @todo Remove this work-around in https://www.drupal.org/node/1927648.
+ // @see hal_update_8501()
+ \Drupal::configFactory()
+ ->getEditable('hal.settings')
+ ->set('bc_file_uri_as_url_normalizer', TRUE)
+ ->save(TRUE);
+ }
+
+ /**
+ * Tests file entity denormalization.
+ */
+ public function testFileDenormalize() {
+ $file_params = [
+ 'filename' => 'test_1.txt',
+ 'uri' => 'public://test_1.txt',
+ 'filemime' => 'text/plain',
+ 'status' => FILE_STATUS_PERMANENT,
+ ];
+ // Create a new file entity.
+ $file = File::create($file_params);
+ file_put_contents($file->getFileUri(), 'hello world');
+ $file->save();
+
+ $serializer = \Drupal::service('serializer');
+ $normalized_data = $serializer->normalize($file, 'hal_json');
+ $denormalized = $serializer->denormalize($normalized_data, 'Drupal\file\Entity\File', 'hal_json');
+
+ $this->assertTrue($denormalized instanceof File, 'A File instance was created.');
+
+ $this->assertIdentical('temporary://' . $file->getFilename(), $denormalized->getFileUri(), 'The expected file URI was found.');
+ $this->assertTrue(file_exists($denormalized->getFileUri()), 'The temporary file was found.');
+
+ $this->assertIdentical($file->uuid(), $denormalized->uuid(), 'The expected UUID was found');
+ $this->assertIdentical($file->getMimeType(), $denormalized->getMimeType(), 'The expected MIME type was found.');
+ $this->assertIdentical($file->getFilename(), $denormalized->getFilename(), 'The expected filename was found.');
+ $this->assertTrue($denormalized->isPermanent(), 'The file has a permanent status.');
+
+ // Try to denormalize with the file uri only.
+ $file_name = 'test_2.txt';
+ $file_path = 'public://' . $file_name;
+
+ file_put_contents($file_path, 'hello world');
+ $file_uri = file_create_url($file_path);
+
+ $data = [
+ 'uri' => [
+ ['value' => $file_uri],
+ ],
+ ];
+
+ $denormalized = $serializer->denormalize($data, 'Drupal\file\Entity\File', 'hal_json');
+
+ $this->assertIdentical('temporary://' . $file_name, $denormalized->getFileUri(), 'The expected file URI was found.');
+ $this->assertTrue(file_exists($denormalized->getFileUri()), 'The temporary file was found.');
+
+ $this->assertIdentical('text/plain', $denormalized->getMimeType(), 'The expected MIME type was found.');
+ $this->assertIdentical($file_name, $denormalized->getFilename(), 'The expected filename was found.');
+ $this->assertFalse($denormalized->isPermanent(), 'The file has a permanent status.');
+ }
+
+}
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index 8180b70..92dd505 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -69,51 +69,16 @@ class RequestHandler implements ContainerInjectionInterface {
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
- * The REST resource config entity.
+ * REST resource config entity ID.
*
* @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
* The REST resource response.
*/
public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
- $resource = $_rest_resource_config->getResourcePlugin();
- $unserialized = $this->deserialize($route_match, $request, $resource);
- $response = $this->delegateToRestResourcePlugin($route_match, $request, $unserialized, $resource);
- return $this->prepareResponse($response, $_rest_resource_config);
- }
+ $response = $this->delegateToRestResourcePlugin($route_match, $request, $_rest_resource_config->getResourcePlugin());
- /**
- * Handles a REST API request without deserializing the request body.
- *
- * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
- * The route match.
- * @param \Symfony\Component\HttpFoundation\Request $request
- * The HTTP request object.
- * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
- * The REST resource config entity.
- *
- * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
- * The REST resource response.
- */
- public function handleRaw(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
- $resource = $_rest_resource_config->getResourcePlugin();
- $response = $this->delegateToRestResourcePlugin($route_match, $request, NULL, $resource);
- return $this->prepareResponse($response, $_rest_resource_config);
- }
-
- /**
- * Prepares the REST resource response.
- *
- * @param \Drupal\rest\ResourceResponseInterface $response
- * The REST resource response.
- * @param \Drupal\rest\RestResourceConfigInterface $resource_config
- * The REST resource config entity.
- *
- * @return \Drupal\rest\ResourceResponseInterface
- * The prepared REST resource response.
- */
- protected function prepareResponse($response, RestResourceConfigInterface $resource_config) {
if ($response instanceof CacheableResponseInterface) {
- $response->addCacheableDependency($resource_config);
+ $response->addCacheableDependency($_rest_resource_config);
// Add global rest settings config's cache tag, for BC flags.
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
// @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
@@ -216,15 +181,14 @@ class RequestHandler implements ContainerInjectionInterface {
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
- * @param mixed|null $unserialized
- * The unserialized request body, if any.
* @param \Drupal\rest\Plugin\ResourceInterface $resource
* The REST resource plugin.
*
* @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
* The REST resource response.
*/
- protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, $unserialized, ResourceInterface $resource) {
+ protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
+ $unserialized = $this->deserialize($route_match, $request, $resource);
$method = static::getNormalizedRequestMethod($route_match);
// Determine the request parameters that should be passed to the resource
diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php
index 27da48a..81bf789 100644
--- a/core/modules/rest/src/Routing/ResourceRoutes.php
+++ b/core/modules/rest/src/Routing/ResourceRoutes.php
@@ -121,14 +121,14 @@ class ResourceRoutes implements EventSubscriberInterface {
// The configuration has been validated, so we update the route to:
// - set the allowed response body content types/formats for methods
- // that may send response bodies (unless hardcoded by the plugin)
+ // that may send response bodies
// - set the allowed request body content types/formats for methods that
- // allow request bodies to be sent (unless hardcoded by the plugin)
+ // allow request bodies to be sent
// - set the allowed authentication providers
- if (in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'PATCH'], TRUE) && !$route->hasRequirement('_format')) {
+ if (in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'PATCH'], TRUE)) {
$route->addRequirements(['_format' => implode('|', $rest_resource_config->getFormats($method))]);
}
- if (in_array($method, ['POST', 'PATCH', 'PUT'], TRUE) && !$route->hasRequirement('_content_type_format')) {
+ if (in_array($method, ['POST', 'PATCH', 'PUT'], TRUE)) {
$route->addRequirements(['_content_type_format' => implode('|', $rest_resource_config->getFormats($method))]);
}
$route->setOption('_auth', $rest_resource_config->getAuthenticationProviders($method));
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 2ab4527..669382d 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -537,9 +537,13 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// Note: deserialization of the XML format is not supported, so only test
// this for other formats.
if (static::$format !== 'xml') {
- $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
- $this->assertSame($unserialized->uuid(), $this->entity->uuid());
-
+ // @todo Work-around for HAL's FileEntityNormalizer::denormalize() being
+ // broken, being fixed in https://www.drupal.org/node/1927648, where this
+ // if-test should be removed.
+ if (!(static::$entityTypeId === 'file' && static::$format === 'hal_json')) {
+ $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
+ $this->assertSame($unserialized->uuid(), $this->entity->uuid());
+ }
}
// Finally, assert that the expected 'Link' headers are present.
if ($this->entity->getEntityType()->getLinkTemplates()) {
@@ -742,6 +746,27 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
/**
+ * Recursively sorts an array by key.
+ *
+ * @param array $array
+ * An array to sort.
+ *
+ * @return array
+ * The sorted array.
+ */
+ protected static function recursiveKSort(array &$array) {
+ // First, sort the main array.
+ ksort($array);
+
+ // Then check for child arrays.
+ foreach ($array as $key => &$value) {
+ if (is_array($value)) {
+ static::recursiveKSort($value);
+ }
+ }
+ }
+
+ /**
* Tests a POST request for an entity, plus edge cases to ensure good DX.
*/
public function testPost() {
@@ -824,13 +849,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
- // @todo Remove this if-test in https://www.drupal.org/project/drupal/issues/2820364
- if (static::$entityTypeId === 'media' && !static::$auth) {
- $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nname: Name: this field cannot hold more than 1 values.\nfield_media_file.0: You do not have access to the referenced entity (file: 3).\n", $response);
- }
- else {
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
- }
+ $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->setUpAuthorization('POST');
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php
index 267f453..1fa5a38 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php
@@ -204,12 +204,7 @@ abstract class FileResourceTestBase extends EntityResourceTestBase {
* {@inheritdoc}
*/
public function testPost() {
- // Drupal does not allow creating file entities independently. It allows you
- // to create file entities that are referenced from another entity (e.g. an
- // image for a node's image field).
- // For that purpose, there is the "file_upload" REST resource plugin.
- // @see \Drupal\file\FileAccessControlHandler::checkCreateAccess()
- // @see \Drupal\file\Plugin\rest\resource\FileUploadResource
+ // @todo https://www.drupal.org/node/1927648
$this->markTestSkipped();
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
index 85548ad..40f7997 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
@@ -2,18 +2,12 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Media;
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use Drupal\media\Entity\MediaType;
-use Drupal\rest\RestResourceConfigInterface;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
-use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
-use Drupal\user\RoleInterface;
-use GuzzleHttp\RequestOptions;
abstract class MediaResourceTestBase extends EntityResourceTestBase {
@@ -51,7 +45,7 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
break;
case 'POST':
- $this->grantPermissionsToTestedRole(['create camelids media', 'access content']);
+ $this->grantPermissionsToTestedRole(['create camelids media']);
break;
case 'PATCH':
@@ -236,26 +230,12 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
'value' => 'Dramallama',
],
],
- 'field_media_file' => [
- [
- 'description' => NULL,
- 'display' => NULL,
- 'target_id' => 3,
- ],
- ],
];
}
/**
* {@inheritdoc}
*/
- protected function getNormalizedPatchEntity() {
- return array_diff_key($this->getNormalizedPostEntity(), ['field_media_file' => TRUE]);
- }
-
- /**
- * {@inheritdoc}
- */
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
@@ -265,9 +245,6 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
case 'GET';
return "The 'view media' permission is required and the media item must be published.";
- case 'POST':
- return "The following permissions are required: 'administer media' OR 'create media' OR 'create camelids media'.";
-
case 'PATCH':
return 'You are not authorized to update this media entity of bundle camelids.';
@@ -283,140 +260,7 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase {
* {@inheritdoc}
*/
public function testPost() {
- $file_storage = $this->container->get('entity_type.manager')->getStorage('file');
-
- // Step 1: upload file, results in File entity marked temporary.
- $this->uploadFile();
- $file = $file_storage->loadUnchanged(3);
- $this->assertTrue($file->isTemporary());
- $this->assertFalse($file->isPermanent());
-
- // Step 2: create Media entity using the File, makes File entity permanent.
- parent::testPost();
- $file = $file_storage->loadUnchanged(3);
- $this->assertFalse($file->isTemporary());
- $this->assertTrue($file->isPermanent());
- }
-
- /**
- * This duplicates some of the 'file_upload' REST resource plugin test
- * coverage, to be able to test it on a concrete use case.
- */
- protected function uploadFile() {
- // Enable the 'file_upload' REST resource for the current format + auth.
- $this->resourceConfigStorage->create([
- 'id' => 'file.upload',
- 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
- 'configuration' => [
- 'methods' => ['POST'],
- 'formats' => [static::$format],
- 'authentication' => isset(static::$auth) ? [static::$auth] : [],
- ],
- 'status' => TRUE,
- ])->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $this->initAuthentication();
-
- // POST to create a File entity.
- $url = Url::fromUri('base:file/upload/media/camelids/field_media_file');
- $url->setOption('query', ['_format' => static::$format]);
- $request_options = [];
- $request_options[RequestOptions::HEADERS] = [
- // Set the required (and only accepted) content type for the request.
- 'Content-Type' => 'application/octet-stream',
- // Set the required Content-Disposition header for the file name.
- 'Content-Disposition' => 'file; filename="drupal rocks 🤘.txt"',
- ];
- $request_options[RequestOptions::BODY] = 'Drupal is the best!';
- $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
- $response = $this->request('POST', $url, $request_options);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
-
- // Grant necessary permission, retry.
- $this->grantPermissionsToTestedRole(['create camelids media']);
- $response = $this->request('POST', $url, $request_options);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedFileEntity();
- static::recursiveKSort($expected);
- $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- static::recursiveKSort($actual);
- $this->assertSame($expected, $actual);
-
- // To still run the complete test coverage for POSTing a Media entity, we
- // must revoke the additional permissions that we granted.
- $role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::AUTHENTICATED_ID);
- $role->revokePermission('create camelids media');
- $role->trustData()->save();
- }
-
- /**
- * Gets the expected file entity.
- *
- * @return array
- * The expected normalized data array.
- */
- protected function getExpectedNormalizedFileEntity() {
- $file = File::load(3);
- $owner = static::$auth ? $this->account : User::load(0);
-
- return [
- 'fid' => [
- [
- 'value' => 3,
- ],
- ],
- 'uuid' => [
- [
- 'value' => $file->uuid(),
- ],
- ],
- 'langcode' => [
- [
- 'value' => 'en',
- ],
- ],
- 'uid' => [
- [
- 'target_id' => (int) $owner->id(),
- 'target_type' => 'user',
- 'target_uuid' => $owner->uuid(),
- 'url' => base_path() . 'user/' . $owner->id(),
- ],
- ],
- 'filename' => [
- [
- 'value' => 'drupal rocks 🤘.txt',
- ],
- ],
- 'uri' => [
- [
- 'value' => 'public://' . date('Y-m') . '/drupal rocks 🤘.txt',
- 'url' => base_path() . $this->siteDirectory . '/files/' . date('Y-m') . '/drupal%20rocks%20%F0%9F%A4%98.txt',
- ],
- ],
- 'filemime' => [
- [
- 'value' => 'text/plain',
- ],
- ],
- 'filesize' => [
- [
- 'value' => 19,
- ],
- ],
- 'status' => [
- [
- 'value' => FALSE,
- ],
- ],
- 'created' => [
- $this->formatExpectedTimestampItemValues($file->getCreatedTime()),
- ],
- 'changed' => [
- $this->formatExpectedTimestampItemValues($file->getChangedTime()),
- ],
- ];
+ $this->markTestSkipped('POSTing File Media items is not supported until https://www.drupal.org/node/1927648 is solved.');
}
/**
diff --git a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
deleted file mode 100644
index af423f5..0000000
--- a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
+++ /dev/null
@@ -1,725 +0,0 @@
-<?php
-
-namespace Drupal\Tests\rest\Functional;
-
-use Drupal\Component\Render\PlainTextOutput;
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\Core\Url;
-use Drupal\entity_test\Entity\EntityTest;
-use Drupal\field\Entity\FieldConfig;
-use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\file\Entity\File;
-use Drupal\rest\RestResourceConfigInterface;
-use Drupal\Tests\RandomGeneratorTrait;
-use Drupal\user\Entity\User;
-use GuzzleHttp\RequestOptions;
-use Psr\Http\Message\ResponseInterface;
-
-/**
- * Tests binary data file upload route.
- */
-abstract class FileUploadResourceTestBase extends ResourceTestBase {
-
- use RandomGeneratorTrait, BcTimestampNormalizerUnixTestTrait;
-
- /**
- * {@inheritdoc}
- */
- public static $modules = ['rest_test', 'entity_test', 'file'];
-
- /**
- * {@inheritdoc}
- */
- protected static $resourceConfigId = 'file.upload';
-
- /**
- * The POST URI.
- *
- * @var string
- */
- protected static $postUri = 'file/upload/entity_test/entity_test/field_rest_file_test';
-
- /**
- * Test file data.
- *
- * @var string
- */
- protected $testFileData = 'Hares sit on chairs, and mules sit on stools.';
-
- /**
- * The test field storage config.
- *
- * @var \Drupal\field\Entity\FieldStorageConfig
- */
- protected $fieldStorage;
-
- /**
- * The field config.
- *
- * @var \Drupal\field\Entity\FieldConfig
- */
- protected $field;
-
- /**
- * The parent entity.
- *
- * @var \Drupal\Core\Entity\EntityInterface
- */
- protected $entity;
-
- /**
- * Created file entity.
- *
- * @var \Drupal\file\Entity\File
- */
- protected $file;
-
- /**
- * An authenticated user.
- *
- * @var \Drupal\user\UserInterface
- */
- protected $user;
-
- /**
- * The entity storage for the 'file' entity type.
- *
- * @var \Drupal\Core\Entity\EntityStorageInterface
- */
- protected $fileStorage;
-
- /**
- * {@inheritdoc}
- */
- public function setUp() {
- parent::setUp();
-
- $this->fileStorage = $this->container->get('entity_type.manager')
- ->getStorage('file');
-
- // Add a file field.
- $this->fieldStorage = FieldStorageConfig::create([
- 'entity_type' => 'entity_test',
- 'field_name' => 'field_rest_file_test',
- 'type' => 'file',
- 'settings' => [
- 'uri_scheme' => 'public',
- ],
- ])
- ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
- $this->fieldStorage->save();
-
- $this->field = FieldConfig::create([
- 'entity_type' => 'entity_test',
- 'field_name' => 'field_rest_file_test',
- 'bundle' => 'entity_test',
- 'settings' => [
- 'file_directory' => 'foobar',
- 'file_extensions' => 'txt',
- 'max_filesize' => '',
- ],
- ])
- ->setLabel('Test file field')
- ->setTranslatable(FALSE);
- $this->field->save();
-
- // Create an entity that a file can be attached to.
- $this->entity = EntityTest::create([
- 'name' => 'Llama',
- 'type' => 'entity_test',
- ]);
- $this->entity->setOwnerId(isset($this->account) ? $this->account->id() : 0);
- $this->entity->save();
-
- // Provision entity_test resource.
- $this->resourceConfigStorage->create([
- 'id' => 'entity.entity_test',
- 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
- 'configuration' => [
- 'methods' => ['POST'],
- 'formats' => [static::$format],
- 'authentication' => [static::$auth],
- ],
- 'status' => TRUE,
- ])->save();
-
- $this->refreshTestStateAfterRestConfigChange();
- }
-
- /**
- * Tests using the file upload POST route.
- */
- public function testPostFileUpload() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // DX: 403 when unauthorized.
- $response = $this->fileRequest($uri, $this->testFileData);
- $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
-
- $this->setUpAuthorization('POST');
-
- // 404 when the field name is invalid.
- $invalid_uri = Url::fromUri('base:file/upload/entity_test/entity_test/field_rest_file_test_invalid');
- $response = $this->fileRequest($invalid_uri, $this->testFileData);
- $this->assertResourceErrorResponse(404, 'Field "field_rest_file_test_invalid" does not exist', $response);
-
- // This request will have the default 'application/octet-stream' content
- // type header.
- $response = $this->fileRequest($uri, $this->testFileData);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity();
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
-
- // Test the file again but using 'filename' in the Content-Disposition
- // header with no 'file' prefix.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt');
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
- $this->assertTrue($this->fileStorage->loadUnchanged(1)->isTemporary());
-
- // Verify that we can create an entity that references the uploaded file.
- $entity_test_post_url = Url::fromRoute('rest.entity.entity_test.POST')
- ->setOption('query', ['_format' => static::$format]);
- $request_options = [];
- $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
- $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
-
- $request_options[RequestOptions::BODY] = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
- $response = $this->request('POST', $entity_test_post_url, $request_options);
- $this->assertResourceResponse(201, FALSE, $response);
- $this->assertTrue($this->fileStorage->loadUnchanged(1)->isPermanent());
- // @todo Remove this early return in https://www.drupal.org/project/drupal/issues/2935738.
- if (static::$format === 'hal_json') {
- return;
- }
- $this->assertSame([
- [
- 'target_id' => '1',
- 'display' => NULL,
- 'description' => "The most fascinating file ever!",
- ],
- ], EntityTest::load(2)->get('field_rest_file_test')->getValue());
- }
-
- /**
- * Returns the normalized POST entity referencing the uploaded file.
- *
- * @return array
- *
- * @see ::testPostFileUpload()
- * @see \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase::getNormalizedPostEntity()
- */
- protected function getNormalizedPostEntity() {
- return [
- 'type' => [
- [
- 'value' => 'entity_test',
- ],
- ],
- 'name' => [
- [
- 'value' => 'Dramallama',
- ],
- ],
- 'field_rest_file_test' => [
- [
- 'target_id' => 1,
- 'description' => 'The most fascinating file ever!',
- ],
- ],
- ];
- }
-
- /**
- * Tests using the file upload POST route with invalid headers.
- */
- public function testPostFileUploadInvalidHeaders() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // The wrong content type header should return a 415 code.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Type' => static::$mimeType]);
- $this->assertResourceErrorResponse(415, sprintf('No route found that matches "Content-Type: %s"', static::$mimeType), $response);
-
- // An empty Content-Disposition header should return a 400.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => '']);
- $this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided', $response);
-
- // An empty filename with a context in the Content-Disposition header should
- // return a 400.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
- $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
-
- // An empty filename without a context in the Content-Disposition header
- // should return a 400.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
- $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
-
- // An invalid key-value pair in the Content-Disposition header should return
- // a 400.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']);
- $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response);
-
- // Using filename* extended format is not currently supported.
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']);
- $this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header', $response);
- }
-
- /**
- * Tests using the file upload POST route with a duplicate file name.
- *
- * A new file should be created with a suffixed name.
- */
- public function testPostFileUploadDuplicateFile() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // This request will have the default 'application/octet-stream' content
- // type header.
- $response = $this->fileRequest($uri, $this->testFileData);
-
- $this->assertSame(201, $response->getStatusCode());
-
- // Make the same request again. The file should be saved as a new file
- // entity that has the same file name but a suffixed file URI.
- $response = $this->fileRequest($uri, $this->testFileData);
- $this->assertSame(201, $response->getStatusCode());
-
- // Loading expected normalized data for file 2, the duplicate file.
- $expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt');
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt'));
- }
-
- /**
- * Tests using the file upload route with any path prefixes being stripped.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives
- */
- public function testFileUploadStrippedFilePath() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity();
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data. It should have been written to the configured
- // directory, not /foobar/directory/example.txt.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
-
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="../../example_2.txt"']);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity(2, 'example_2.txt', TRUE);
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data. It should have been written to the configured
- // directory, not /foobar/directory/example.txt.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_2.txt'));
- $this->assertFalse(file_exists('../../example_2.txt'));
-
- // Check a path from the root. Extensions have to be empty to allow a file
- // with no extension to pass validation.
- $this->field->setSetting('file_extensions', '')
- ->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="/etc/passwd"']);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity(3, 'passwd', TRUE);
- // This mime will be guessed as there is no extension.
- $expected['filemime'][0]['value'] = 'application/octet-stream';
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data. It should have been written to the configured
- // directory, not /foobar/directory/example.txt.
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/passwd'));
- }
-
- /**
- * Tests using the file upload route with a unicode file name.
- */
- public function testFileUploadUnicodeFilename() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="example-✓.txt"']);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity(1, 'example-✓.txt', TRUE);
- $this->assertResponseData($expected, $response);
- $this->assertSame($this->testFileData, file_get_contents('public://foobar/example-✓.txt'));
- }
-
- /**
- * Tests using the file upload route with a zero byte file.
- */
- public function testFileUploadZeroByteFile() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // Test with a zero byte file.
- $response = $this->fileRequest($uri, NULL);
- $this->assertSame(201, $response->getStatusCode());
- $expected = $this->getExpectedNormalizedEntity();
- // Modify the default expected data to account for the 0 byte file.
- $expected['filesize'][0]['value'] = 0;
- $this->assertResponseData($expected, $response);
-
- // Check the actual file data.
- $this->assertSame('', file_get_contents('public://foobar/example.txt'));
- }
-
- /**
- * Tests using the file upload route with an invalid file type.
- */
- public function testFileUploadInvalidFileType() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // Test with a JSON file.
- $response = $this->fileRequest($uri, '{"test":123}', ['Content-Disposition' => 'filename="example.json"']);
- $this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nOnly files with the following extensions are allowed: <em class=\"placeholder\">txt</em>."), $response);
-
- // Make sure that no file was saved.
- $this->assertEmpty(File::load(1));
- $this->assertFalse(file_exists('public://foobar/example.txt'));
- }
-
- /**
- * Tests using the file upload route with a file size larger than allowed.
- */
- public function testFileUploadLargerFileSize() {
- // Set a limit of 50 bytes.
- $this->field->setSetting('max_filesize', 50)
- ->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- // Generate a string larger than the 50 byte limit set.
- $response = $this->fileRequest($uri, $this->randomString(100));
- $this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file is <em class=\"placeholder\">100 bytes</em> exceeding the maximum file size of <em class=\"placeholder\">50 bytes</em>."), $response);
-
- // Make sure that no file was saved.
- $this->assertEmpty(File::load(1));
- $this->assertFalse(file_exists('public://foobar/example.txt'));
- }
-
- /**
- * Tests using the file upload POST route with malicious extensions.
- */
- public function testFileUploadMaliciousExtension() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
- // Allow all file uploads but system.file::allow_insecure_uploads is set to
- // FALSE.
- $this->field->setSetting('file_extensions', '')->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- $php_string = '<?php print "Drupal"; ?>';
-
- // Test using a masked exploit file.
- $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
- // The filename is not munged because .txt is added and it is a known
- // extension to apache.
- $expected = $this->getExpectedNormalizedEntity(1, 'example.php.txt', TRUE);
- // Override the expected filesize.
- $expected['filesize'][0]['value'] = strlen($php_string);
- $this->assertResponseData($expected, $response);
- $this->assertTrue(file_exists('public://foobar/example.php.txt'));
-
- // Add php as an allowed format. Allow insecure uploads still being FALSE
- // should still not allow this. So it should still have a .txt extension
- // appended even though it is not in the list of allowed extensions.
- $this->field->setSetting('file_extensions', 'php')
- ->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
- $expected = $this->getExpectedNormalizedEntity(2, 'example_2.php.txt', TRUE);
- // Override the expected filesize.
- $expected['filesize'][0]['value'] = strlen($php_string);
- $this->assertResponseData($expected, $response);
- $this->assertTrue(file_exists('public://foobar/example_2.php.txt'));
- $this->assertFalse(file_exists('public://foobar/example_2.php'));
-
- // Allow .doc file uploads and ensure even a mis-configured apache will not
- // fallback to php because the filename will be munged.
- $this->field->setSetting('file_extensions', 'doc')->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- // Test using a masked exploit file.
- $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_3.php.doc"']);
- // The filename is munged.
- $expected = $this->getExpectedNormalizedEntity(3, 'example_3.php_.doc', TRUE);
- // Override the expected filesize.
- $expected['filesize'][0]['value'] = strlen($php_string);
- // The file mime should be 'application/msword'.
- $expected['filemime'][0]['value'] = 'application/msword';
- $this->assertResponseData($expected, $response);
- $this->assertTrue(file_exists('public://foobar/example_3.php_.doc'));
- $this->assertFalse(file_exists('public://foobar/example_3.php.doc'));
-
- // Now allow insecure uploads.
- \Drupal::configFactory()
- ->getEditable('system.file')
- ->set('allow_insecure_uploads', TRUE)
- ->save();
- // Allow all file uploads. This is very insecure.
- $this->field->setSetting('file_extensions', '')->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']);
- $expected = $this->getExpectedNormalizedEntity(4, 'example_4.php', TRUE);
- // Override the expected filesize.
- $expected['filesize'][0]['value'] = strlen($php_string);
- // The file mime should also now be PHP.
- $expected['filemime'][0]['value'] = 'application/x-httpd-php';
- $this->assertResponseData($expected, $response);
- $this->assertTrue(file_exists('public://foobar/example_4.php'));
- }
-
- /**
- * Tests using the file upload POST route no extension configured.
- */
- public function testFileUploadNoExtensionSetting() {
- $this->initAuthentication();
-
- $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
-
- $this->setUpAuthorization('POST');
-
- $uri = Url::fromUri('base:' . static::$postUri);
-
- $this->field->setSetting('file_extensions', '')
- ->save();
- $this->refreshTestStateAfterRestConfigChange();
-
- $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
- $expected = $this->getExpectedNormalizedEntity(1, 'example.txt', TRUE);
-
- $this->assertResponseData($expected, $response);
- $this->assertTrue(file_exists('public://foobar/example.txt'));
- }
-
- /**
- * {@inheritdoc}
- */
- protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
- // The file upload resource only accepts binary data, so there are no
- // normalization edge cases to test, as there are no normalized entity
- // representations incoming.
- }
-
- /**
- * {@inheritdoc}
- */
- protected function getExpectedUnauthorizedAccessMessage($method) {
- return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
- }
-
- /**
- * Gets the expected file entity.
- *
- * @param int $fid
- * The file ID to load and create normalized data for.
- * @param string $expected_filename
- * The expected filename for the stored file.
- * @param bool $expected_as_filename
- * Whether the expected filename should be the filename property too.
- *
- * @return array
- * The expected normalized data array.
- */
- protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE) {
- $author = User::load(static::$auth ? $this->account->id() : 0);
- $file = File::load($fid);
-
- $expected_normalization = [
- 'fid' => [
- [
- 'value' => (int) $file->id(),
- ],
- ],
- 'uuid' => [
- [
- 'value' => $file->uuid(),
- ],
- ],
- 'langcode' => [
- [
- 'value' => 'en',
- ],
- ],
- 'uid' => [
- [
- 'target_id' => (int) $author->id(),
- 'target_type' => 'user',
- 'target_uuid' => $author->uuid(),
- 'url' => base_path() . 'user/' . $author->id(),
- ],
- ],
- 'filename' => [
- [
- 'value' => $expected_as_filename ? $expected_filename : 'example.txt',
- ],
- ],
- 'uri' => [
- [
- 'value' => 'public://foobar/' . $expected_filename,
- 'url' => base_path() . $this->siteDirectory . '/files/foobar/' . rawurlencode($expected_filename),
- ],
- ],
- 'filemime' => [
- [
- 'value' => 'text/plain',
- ],
- ],
- 'filesize' => [
- [
- 'value' => strlen($this->testFileData),
- ],
- ],
- 'status' => [
- [
- 'value' => FALSE,
- ],
- ],
- 'created' => [
- $this->formatExpectedTimestampItemValues($file->getCreatedTime()),
- ],
- 'changed' => [
- $this->formatExpectedTimestampItemValues($file->getChangedTime()),
- ],
- ];
-
- return $expected_normalization;
- }
-
- /**
- * Performs a file upload request. Wraps the Guzzle HTTP client.
- *
- * @see \GuzzleHttp\ClientInterface::request()
- *
- * @param \Drupal\Core\Url $url
- * URL to request.
- * @param string $file_contents
- * The file contents to send as the request body.
- * @param array $headers
- * Additional headers to send with the request. Defaults will be added for
- * Content-Type and Content-Disposition.
- *
- * @return \Psr\Http\Message\ResponseInterface
- */
- protected function fileRequest(Url $url, $file_contents, array $headers = []) {
- // Set the format for the response.
- $url->setOption('query', ['_format' => static::$format]);
-
- $request_options = [];
- $request_options[RequestOptions::HEADERS] = $headers + [
- // Set the required (and only accepted) content type for the request.
- 'Content-Type' => 'application/octet-stream',
- // Set the required Content-Disposition header for the file name.
- 'Content-Disposition' => 'file; filename="example.txt"',
- ];
- $request_options[RequestOptions::BODY] = $file_contents;
- $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
-
- return $this->request('POST', $url, $request_options);
- }
-
- /**
- * {@inheritdoc}
- */
- protected function setUpAuthorization($method) {
- switch ($method) {
- case 'GET':
- $this->grantPermissionsToTestedRole(['view test entity']);
- break;
- case 'POST':
- $this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities', 'access content']);
- break;
- }
- }
-
- /**
- * Asserts expected normalized data matches response data.
- *
- * @param array $expected
- * The expected data.
- * @param \Psr\Http\Message\ResponseInterface $response
- * The file upload response.
- */
- protected function assertResponseData(array $expected, ResponseInterface $response) {
- static::recursiveKSort($expected);
- $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
- static::recursiveKSort($actual);
-
- $this->assertSame($expected, $actual);
- }
-
- /**
- * {@inheritdoc}
- */
- protected function getExpectedUnauthorizedAccessCacheability() {
- // There is cacheability metadata to check as file uploads only allows POST
- // requests, which will not return cacheable responses.
- }
-
-}
diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
index a23a567..8283bef 100644
--- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
@@ -144,12 +144,12 @@ abstract class ResourceTestBase extends BrowserTestBase {
* @param string[] $authentication
* The allowed authentication providers for this resource.
*/
- protected function provisionResource($formats = [], $authentication = [], array $methods = ['GET', 'POST', 'PATCH', 'DELETE']) {
+ protected function provisionResource($formats = [], $authentication = []) {
$this->resourceConfigStorage->create([
'id' => static::$resourceConfigId,
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
- 'methods' => $methods,
+ 'methods' => ['GET', 'POST', 'PATCH', 'DELETE'],
'formats' => $formats,
'authentication' => $authentication,
],
@@ -481,25 +481,4 @@ abstract class ResourceTestBase extends BrowserTestBase {
return $request_options;
}
- /**
- * Recursively sorts an array by key.
- *
- * @param array $array
- * An array to sort.
- *
- * @return array
- * The sorted array.
- */
- protected static function recursiveKSort(array &$array) {
- // First, sort the main array.
- ksort($array);
-
- // Then check for child arrays.
- foreach ($array as $key => &$value) {
- if (is_array($value)) {
- static::recursiveKSort($value);
- }
- }
- }
-
}