diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php index edb3029..574a4aa 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php @@ -514,6 +514,7 @@ public function testAutocreateValidation() { $file = File::create([ 'filename' => $filename, 'status' => 0, + 'uid' => 1, ]); $entity = EntityTest::create([ diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index 5deb382..991ca9b 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -238,6 +238,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['uid'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('User ID')) ->setDescription(t('The user ID of the file.')) + ->setDefaultValueCallback('Drupal\file\Entity\File::getCurrentUserId') ->setSetting('target_type', 'user'); $fields['filename'] = BaseFieldDefinition::create('string') @@ -278,4 +279,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { return $fields; } + /** + * Default value callback for 'uid' base field definition. + * + * @see \Drupal\file\Entity\File::baseFieldDefinitions::baseFieldDefinitions(). + * + * @return array + * An array of default values. + */ + public static function getCurrentUserId() { + return array(\Drupal::currentUser()->id()); + } + } diff --git a/core/modules/file/src/FileAccessControlHandler.php b/core/modules/file/src/FileAccessControlHandler.php index a82c460..1620461 100644 --- a/core/modules/file/src/FileAccessControlHandler.php +++ b/core/modules/file/src/FileAccessControlHandler.php @@ -55,6 +55,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter return AccessResult::forbidden(); } + if ($operation == 'delete' || $operation == 'update') { + $account = $this->prepareUser($account); + $file_uid = $entity->get('uid')->getValue(); + // Only admin users and the file owner can delete and update the file entity. + if ($account->hasPermission('administer nodes') || $account->id() == $file_uid[0]['target_id']) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::forbidden(); + } + // No opinion. return AccessResult::neutral(); } @@ -96,8 +106,6 @@ protected function checkCreateAccess(AccountInterface $account, array $context, // 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/hal/hal.services.yml b/core/modules/hal/hal.services.yml index e817fbf..80cce6d 100644 --- a/core/modules/hal/hal.services.yml +++ b/core/modules/hal/hal.services.yml @@ -16,7 +16,7 @@ services: class: Drupal\hal\Normalizer\FileEntityNormalizer tags: - { name: normalizer, priority: 20 } - arguments: ['@entity.manager', '@http_client', '@rest.link_manager', '@module_handler'] + arguments: ['@entity.manager', '@rest.link_manager', '@module_handler', '@file_system'] serializer.normalizer.entity.hal: class: Drupal\hal\Normalizer\ContentEntityNormalizer arguments: ['@rest.link_manager', '@entity.manager', '@module_handler'] diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php index fd8158b..9467c7f 100644 --- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php @@ -126,9 +126,10 @@ public function denormalize($data, $class, $format = NULL, array $context = arra throw new UnexpectedValueException('The type link relation must be specified.'); } - // Create the entity. + // Identify entity type and bundle. $typed_data_ids = $this->getTypedDataIds($data['_links']['type'], $context); $entity_type = $this->entityManager->getDefinition($typed_data_ids['entity_type']); + // Get definition of entity type and bundle (if any). $langcode_key = $entity_type->getKey('langcode'); $values = array(); @@ -146,6 +147,7 @@ public function denormalize($data, $class, $format = NULL, array $context = arra unset($data[$bundle_key]); } + // Create the entity. $entity = $this->entityManager->getStorage($typed_data_ids['entity_type'])->create($values); // Remove links from data array. diff --git a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php index 6c73256..d1fe05f 100644 --- a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php @@ -3,6 +3,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemInterface; use Drupal\rest\LinkManager\LinkManagerInterface; use Drupal\serialization\EntityResolver\EntityResolverInterface; use Drupal\serialization\EntityResolver\UuidReferenceInterface; @@ -53,12 +54,18 @@ public function normalize($field_item, $format = NULL, array $context = array()) /** @var $field_item \Drupal\Core\Field\FieldItemInterface */ $target_entity = $field_item->get('entity')->getValue(); + // Parent implementation simply copies the properties. + $properties = parent::normalize($field_item, $format, $context); // If this is not a content entity, let the parent implementation handle it, // only content entities are supported as embedded resources. if (!($target_entity instanceof FieldableEntityInterface)) { - return parent::normalize($field_item, $format, $context); + return $properties; } + // Discard the target_id property. + $field_name = $field_item->getParent()->getName(); + unset($properties[$field_name][0]['target_id']); + // If the parent entity passed in a langcode, unset it before normalizing // the target entity. Otherwise, untranslatable fields of the target entity // will include the langcode. @@ -74,13 +81,13 @@ public function normalize($field_item, $format = NULL, array $context = array()) if ($langcode) { $embedded['lang'] = $link['lang'] = $langcode; } + // Merge in the properties of the parent implementation. + $embedded += $properties[$field_name][0]; // The returned structure will be recursively merged into the normalized // entity so that the items are properly added to the _links and _embedded // objects. - $field_name = $field_item->getParent()->getName(); - $entity = $field_item->getEntity(); - $field_uri = $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name, $context); + $field_uri = $this->getFieldRelationUri($field_item); return array( '_links' => array( $field_uri => array($link), @@ -95,12 +102,19 @@ public function normalize($field_item, $format = NULL, array $context = array()) * {@inheritdoc} */ protected function constructValue($data, $context) { + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ $field_item = $context['target_instance']; $field_definition = $field_item->getFieldDefinition(); $target_type = $field_definition->getSetting('target_type'); $id = $this->entityResolver->resolve($this, $data, $target_type); if (isset($id)) { - return array('target_id' => $id); + $constructed = array('target_id' => $id); + foreach ($field_item->getProperties() as $property => $value) { + if ($property !== 'target_id' && isset($data[$property])) { + $constructed[$property] = $data[$property]; + } + } + return $constructed; } return NULL; } @@ -119,4 +133,21 @@ public function getUuid($data) { } } + /** + * Gets the relation URI of the field containing an item. + * + * The relation URI is used as a property key when building the HAL structure. + * + * @param \Drupal\Core\Field\FieldItemInterface $field_item + * The field item that is being normalized. + * + * @return string + * The relation URI of the field. + */ + protected function getFieldRelationUri(FieldItemInterface $field_item) { + $field_name = $field_item->getParent()->getName(); + $entity = $field_item->getEntity(); + return $this->linkManager->getRelationUri($entity->getEntityTypeId(), $entity->bundle(), $field_name); + } + } diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php index 46c6094..f72637d 100644 --- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php @@ -5,13 +5,19 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\rest\LinkManager\LinkManagerInterface; -use GuzzleHttp\ClientInterface; +use Drupal\Core\File\FileSystemInterface; +use Symfony\Component\Serializer\Exception\RuntimeException; /** * Converts the Drupal entity object structure to a HAL array structure. */ class FileEntityNormalizer extends ContentEntityNormalizer { + /** + * Key used to store actual file data. + */ + const FILE_DATA_KEY = 'data'; + /** * The interface or class that this Normalizer supports. * @@ -20,51 +26,55 @@ class FileEntityNormalizer extends ContentEntityNormalizer { protected $supportedInterfaceOrClass = 'Drupal\file\FileInterface'; /** - * The HTTP client. - * - * @var \GuzzleHttp\ClientInterface + * @var \Drupal\Core\File\FileSystemInterface */ - protected $httpClient; + protected $fileSystem; /** * Constructs a FileEntityNormalizer object. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. - * @param \GuzzleHttp\ClientInterface $http_client - * The HTTP Client. * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager * The hypermedia link manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\File\FileSystemInterface $file_system + * The file system. */ - public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler) { + public function __construct(EntityManagerInterface $entity_manager, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler, FileSystemInterface $file_system) { parent::__construct($link_manager, $entity_manager, $module_handler); - - $this->httpClient = $http_client; + $this->fileSystem = $file_system; } /** * {@inheritdoc} */ - public function normalize($entity, $format = NULL, array $context = array()) { - $data = parent::normalize($entity, $format, $context); - // Replace the file url with a full url for the file. - $data['uri'][0]['value'] = $this->getEntityUri($entity); + public function denormalize($data, $class, $format = NULL, array $context = array()) { + // File content can be passed base64 encoded in a special "data" property + // under the field data. + // That property is not a field, so we remove it before denormalizing the + // rest of the file entity. + $file_data = $data[self::FILE_DATA_KEY][0]['value']; + unset($data[self::FILE_DATA_KEY]); - return $data; - } + $entity = parent::denormalize($data, $class, $format, $context); - /** - * {@inheritdoc} - */ - public function denormalize($data, $class, $format = NULL, array $context = array()) { - $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody(); + // Decode and save to file if it's a new file. + if (!isset($context['request_method']) || $context['request_method'] != 'patch') { + $file_contents = base64_decode($file_data); + $dirname = $this->fileSystem->dirname($entity->getFileUri()); - $path = 'temporary://' . drupal_basename($data['uri'][0]['value']); - $data['uri'] = file_unmanaged_save_data($file_data, $path); + file_prepare_directory($dirname, FILE_CREATE_DIRECTORY); + if ($uri = file_unmanaged_save_data($file_contents, file_build_uri($this->fileSystem->basename($entity->getFilename())))) { + $entity->setFileUri($uri); + } + else { + throw new RuntimeException('Failed to write ' . $entity->getFilename()); + } + } - return $this->entityManager->getStorage('file')->create($data); + return $entity; } } diff --git a/core/modules/hal/src/Tests/FileDenormalizeTest.php b/core/modules/hal/src/Tests/FileDenormalizeTest.php index f160634..05bc45d 100644 --- a/core/modules/hal/src/Tests/FileDenormalizeTest.php +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php @@ -3,6 +3,7 @@ namespace Drupal\hal\Tests; use Drupal\file\Entity\File; +use Drupal\hal\Normalizer\FileEntityNormalizer; use Drupal\simpletest\WebTestBase; /** @@ -34,42 +35,23 @@ public function testFileDenormalize() { $file = File::create($file_params); file_put_contents($file->getFileUri(), 'hello world'); $file->save(); + $data = base64_encode(file_get_contents($file_params['uri'])); $serializer = \Drupal::service('serializer'); $normalized_data = $serializer->normalize($file, 'hal_json'); - $denormalized = $serializer->denormalize($normalized_data, 'Drupal\file\Entity\File', 'hal_json'); - + // Adding data to the entity. + $normalized_data[FileEntityNormalizer::FILE_DATA_KEY][0]['value'] = $data; + // Use 'patch' to avoid trying to recreate the file. + $denormalized = $serializer->denormalize($normalized_data, File::class, 'hal_json', array('request_method' => 'patch')); $this->assertTrue($denormalized instanceof File, 'A File instance was created.'); - $this->assertIdentical('temporary://' . $file->getFilename(), $denormalized->getFileUri(), 'The expected file URI was found.'); + $this->assertIdentical('public://' . $file->getFilename(), $denormalized->getFileUri()); $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 = array( - 'uri' => array( - array('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/hal/tests/src/Kernel/EntityNormalizeTest.php b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php index 88db403..1d17a7c 100644 --- a/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php @@ -4,14 +4,16 @@ use Drupal\comment\Tests\CommentTestTrait; use Drupal\comment\Entity\Comment; +use Drupal\hal\Normalizer\FileEntityNormalizer; use Drupal\node\Entity\Node; use Drupal\user\Entity\User; use Drupal\node\Entity\NodeType; use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; +use Drupal\file\Entity\File; /** - * Tests that nodes and terms are correctly normalized and denormalized. + * Tests that nodes, files and terms are correctly normalized and denormalized. * * @group hal */ @@ -24,7 +26,7 @@ class EntityNormalizeTest extends NormalizerTestBase { * * @var array */ - public static $modules = array('node', 'taxonomy', 'comment'); + public static $modules = array('node', 'taxonomy', 'comment', 'file'); /** * {@inheritdoc} @@ -37,6 +39,7 @@ protected function setUp() { $this->installSchema('comment', array('comment_entity_statistics')); $this->installEntitySchema('taxonomy_term'); $this->installConfig(['node', 'comment']); + $this->installEntitySchema('file'); } /** @@ -203,4 +206,40 @@ public function testComment() { $this->assertEqual($original_values, $denormalized_comment_values, 'The expected comment values are restored after normalizing and denormalizing.'); } + /** + * Tests the normalization of files. + */ + public function testFile() { + $user = User::create([ + 'name' => 'fileTestingUser', + ]); + $user->save(); + + $file_uri = 'public://normalization_test_file'; + $file_contents = 'hello world'; + $data = base64_encode($file_contents); + file_put_contents($file_uri, $file_contents); + + $file = File::create([ + 'uid' => $user->id(), + 'uri' => $file_uri, + 'status' => FILE_STATUS_PERMANENT, + ]); + + $file->save(); + + $original_values = $file->toArray(); + $normalized = $this->serializer->normalize($file, $this->format); + + // Adding data to the entity. + $normalized[FileEntityNormalizer::FILE_DATA_KEY][0]['value'] = $data; + + // Use PATCH to avoid trying to create new file on denormalize. + $denormalized_file = $this->serializer->denormalize($normalized, File::class, $this->format, ['request_method' => 'patch']); + // Loop over the remaining fields and verify that they are identical. + foreach ($original_values as $field_name => $field_values) { + $this->assertEquals($field_values, $denormalized_file->get($field_name)->getValue()); + } + } + } diff --git a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php index 2aeba89..69604ef 100644 --- a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php @@ -11,7 +11,7 @@ use Drupal\rest\LinkManager\RelationLinkManager; use Drupal\rest\LinkManager\TypeLinkManager; use Symfony\Component\Serializer\Serializer; - +use Drupal\hal\Normalizer\FieldNormalizer; /** * Tests that file entities can be normalized in HAL. @@ -39,8 +39,9 @@ protected function setUp() { // Set up the mock serializer. $normalizers = array( + new FieldNormalizer(), new FieldItemNormalizer(), - new FileEntityNormalizer($entity_manager, \Drupal::httpClient(), $link_manager, \Drupal::moduleHandler()), + new FileEntityNormalizer($entity_manager, $link_manager, \Drupal::moduleHandler(), $this->container->get('file_system')), ); $encoders = array( @@ -68,12 +69,18 @@ public function testNormalize() { $expected_array = array( 'uri' => array( array( - 'value' => file_create_url($file->getFileUri())), + 'value' => $file->getFileUri(), + ), + ), + 'data' => array( + array( + 'value' => base64_encode('hello world'), + ), ), ); $normalized = $this->serializer->normalize($file, $this->format); - $this->assertEqual($normalized['uri'], $expected_array['uri'], 'URI is normalized.'); + $this->assertEqual($normalized['uri'], $expected_array['uri']); } diff --git a/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php b/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php index d2899d0..6ae82c7 100644 --- a/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php +++ b/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php @@ -9,6 +9,7 @@ use Drupal\hal\Normalizer\EntityReferenceItemNormalizer; use Drupal\hal\Normalizer\FieldItemNormalizer; use Drupal\hal\Normalizer\FieldNormalizer; +use Drupal\hal\Normalizer\FileEntityNormalizer; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\rest\LinkManager\LinkManager; use Drupal\rest\LinkManager\RelationLinkManager; @@ -130,15 +131,14 @@ protected function setUp() { 'translatable' => TRUE, ])->save(); - $entity_manager = \Drupal::entityManager(); - $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new RelationLinkManager(new MemoryBackend('default'), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack'))); + $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend('default'), $this->container->get('module_handler'), \Drupal::service('config.factory'), \Drupal::service('request_stack')), new RelationLinkManager(new MemoryBackend('default'), $this->container->get('entity.manager'), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack'))); - $chain_resolver = new ChainEntityResolver(array(new UuidResolver($entity_manager), new TargetIdResolver())); + $chain_resolver = new ChainEntityResolver(array(new UuidResolver($this->container->get('entity.manager')), new TargetIdResolver())); // Set up the mock serializer. $normalizers = array( - new ContentEntityNormalizer($link_manager, $entity_manager, \Drupal::moduleHandler()), - new EntityReferenceItemNormalizer($link_manager, $chain_resolver), + new FileEntityNormalizer($this->container->get('entity.manager'), $link_manager, $this->container->get('module_handler'), $this->container->get('file_system')), + new ContentEntityNormalizer($link_manager, $this->container->get('entity.manager'), $this->container->get('module_handler')), new EntityReferenceItemNormalizer($link_manager, $chain_resolver), new FieldItemNormalizer(), new FieldNormalizer(), ); diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index dd2c6c5..60685f7 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -108,9 +108,15 @@ public function post(EntityInterface $entity = NULL) { $this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id())); // 201 Created responses return the newly created entity in the response - // body. - $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE); - $response = new ResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]); + // body. Verify if canonical path exists. + if ($entity->hasLinkTemplate('canonical')) { + $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE); + $response = new ResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]); + } + else { + $response = new ResourceResponse($entity, 201); + } + // Responses after creating an entity are not cacheable, so we add no // cacheability metadata here. return $response;