summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorxjm2018-04-03 12:44:42 (GMT)
committerxjm2018-04-03 12:44:42 (GMT)
commita33fe9c59a9a09db5cc680b24ef26952935070ce (patch)
treefa67752357cf6200f5f9818fb72c963486142535
parentbd9008cf6d24bb24c50ab252a8650ba8be666180 (diff)
Revert "Issue #1927648 by damiankloip, Wim Leers, marthinal, tedbow, Arla, alexpott, juampynr, garphy, bc, ibustos, eiriksm, larowlan, dawehner, gcardinal, vivekvpandya, kylebrowning, Sam152, neclimdul, pnagornyak, drnikki, gaurav.goyal, queenvictoria, kim.pepper, Berdir, clemens.tolboom, blainelang, moshe weitzman, linclark, webchick, Dave Reid, dabito, skyredwang, klausi, dagmar, gabesullice, pwolanin, amateescu, slashrsm, andypost, catch, aheimlich: Allow creation of file entities from binary data via REST requests"
This reverts commit fd8233c78f652a3f1412dd0d1ac84f1a14784b5f.
-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);
- }
- }
- }
-
}