diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 88bc2990fcd999e9b5276854870d1393c697b09c..013201119a71a43b66a34f5ddaeb8a20978bb5a2 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -48,6 +48,16 @@ function file_help($route_name, RouteMatchInterface $route_match) { } } +/** + * Implements hook_field_widget_info_alter(). + */ +function file_field_widget_info_alter(array &$info) { + // Allows using the 'uri' widget for the 'file_uri' field type, which uses it + // as the default widget. + // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem + $info['uri']['field_types'][] = 'file_uri'; +} + /** * Loads file entities from the database. * diff --git a/core/modules/file/src/ComputedFileUrl.php b/core/modules/file/src/ComputedFileUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..2eb012edc659aa895e5cf73f0c3f1d43e25ea11f --- /dev/null +++ b/core/modules/file/src/ComputedFileUrl.php @@ -0,0 +1,47 @@ +url !== NULL) { + return $this->url; + } + + assert($this->getParent()->getEntity() instanceof FileInterface); + + $uri = $this->getParent()->getEntity()->getFileUri(); + $this->url = file_url_transform_relative(file_create_url($uri)); + + return $this->url; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->url = $value; + + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + +} diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php index a9ade9e7d6ee33cbfcc156756c426b797f9e1bc3..4060b773eed13db4ec5bb687f2160033c0551129 100644 --- a/core/modules/file/src/Entity/File.php +++ b/core/modules/file/src/Entity/File.php @@ -243,7 +243,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Filename')) ->setDescription(t('Name of the file with no path components.')); - $fields['uri'] = BaseFieldDefinition::create('uri') + $fields['uri'] = BaseFieldDefinition::create('file_uri') ->setLabel(t('URI')) ->setDescription(t('The URI to access the file (either local or remote).')) ->setSetting('max_length', 255) diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php index 055e32dc8b12ab7bef429293f7757664b63c456d..0facb7e033885818c8f96921c09f012b9618f0e3 100644 --- a/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileUriFormatter.php @@ -13,7 +13,8 @@ * id = "file_uri", * label = @Translation("File URI"), * field_types = { - * "uri" + * "uri", + * "file_uri", * } * ) */ diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php new file mode 100644 index 0000000000000000000000000000000000000000..33bc15ef03f120059cb006e7bccad661f8cad681 --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php @@ -0,0 +1,39 @@ +setLabel(t('Root-relative file URL')) + ->setComputed(TRUE) + ->setInternal(FALSE) + ->setClass(ComputedFileUrl::class); + + return $properties; + } + +} diff --git a/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php b/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d0f1e616ecc306266b807f35d98136da44df8420 --- /dev/null +++ b/core/modules/file/tests/src/Kernel/ComputedFileUrlTest.php @@ -0,0 +1,91 @@ +prophesize(FileInterface::class); + $entity->getFileUri() + ->willReturn($this->testUrl); + + $parent = $this->prophesize(FieldItemInterface::class); + $parent->getEntity() + ->shouldBeCalledTimes(2) + ->willReturn($entity->reveal()); + + $definition = $this->prophesize(DataDefinitionInterface::class); + + $typed_data = new ComputedFileUrl($definition->reveal(), $this->randomMachineName(), $parent->reveal()); + + $expected = base_path() . $this->siteDirectory . '/files/druplicon.txt'; + + $this->assertSame($expected, $typed_data->getValue()); + // Do this a second time to confirm the same value is returned but the value + // isn't retrieved from the parent entity again. + $this->assertSame($expected, $typed_data->getValue()); + } + + /** + * @covers ::setValue + */ + public function testSetValue() { + $name = $this->randomMachineName(); + $parent = $this->prophesize(FieldItemInterface::class); + $parent->onChange($name) + ->shouldBeCalled(); + + $definition = $this->prophesize(DataDefinitionInterface::class); + $typed_data = new ComputedFileUrl($definition->reveal(), $name, $parent->reveal()); + + // Setting the value explicitly should mean the parent entity is never + // called into. + $typed_data->setValue($this->testUrl); + + $this->assertSame($this->testUrl, $typed_data->getValue()); + // Do this a second time to confirm the same value is returned but the value + // isn't retrieved from the parent entity again. + $this->assertSame($this->testUrl, $typed_data->getValue()); + } + + /** + * @covers ::setValue + */ + public function testSetValueNoNotify() { + $name = $this->randomMachineName(); + $parent = $this->prophesize(FieldItemInterface::class); + $parent->onChange($name) + ->shouldNotBeCalled(); + + $definition = $this->prophesize(DataDefinitionInterface::class); + $typed_data = new ComputedFileUrl($definition->reveal(), $name, $parent->reveal()); + + // Setting the value should explicitly should mean the parent entity is + // never called into. + $typed_data->setValue($this->testUrl, FALSE); + + $this->assertSame($this->testUrl, $typed_data->getValue()); + } + +} diff --git a/core/modules/file/tests/src/Kernel/FileUriItemTest.php b/core/modules/file/tests/src/Kernel/FileUriItemTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d67eba07070cb047526cd530c8fbb1a685e42747 --- /dev/null +++ b/core/modules/file/tests/src/Kernel/FileUriItemTest.php @@ -0,0 +1,40 @@ + 1, + 'filename' => 'druplicon.txt', + 'uri' => $uri, + 'filemime' => 'text/plain', + 'status' => FILE_STATUS_PERMANENT, + ]); + file_put_contents($file->getFileUri(), 'hello world'); + + $file->save(); + + $this->assertSame($uri, $file->uri->value); + $expected_url = base_path() . $this->siteDirectory . '/files/druplicon.txt'; + $this->assertSame($expected_url, $file->uri->url); + } + +} diff --git a/core/modules/hal/config/install/hal.settings.yml b/core/modules/hal/config/install/hal.settings.yml index 67107af00d34bdddac5e35ad25b0ad6c4cea682a..c62cb6a04819ec264d5cf55542276f89d5541c6b 100644 --- a/core/modules/hal/config/install/hal.settings.yml +++ b/core/modules/hal/config/install/hal.settings.yml @@ -1,3 +1,11 @@ # Set the domain for HAL type and relation links. # If left blank, the site's domain will be used. link_domain: ~ +# Before Drupal 8.5, the File entity 'uri' field value was overridden to return +# the absolute file URL instead of the actual (stream wrapper) URI. The default +# for new sites is now to return the actual URI as well as a root-relative file +# URL. Enable this setting to use the previous behavior. For existing sites, +# the previous behavior is kept by default. +# @see hal_update_8501() +# @see https://www.drupal.org/node/2925783 +bc_file_uri_as_url_normalizer: false diff --git a/core/modules/hal/config/schema/hal.schema.yml b/core/modules/hal/config/schema/hal.schema.yml index 3192d6758052417bb4bfe895a464b88c260dfeeb..cad1bdb722b813dc45b80c540c1ed19f83edcb92 100644 --- a/core/modules/hal/config/schema/hal.schema.yml +++ b/core/modules/hal/config/schema/hal.schema.yml @@ -6,3 +6,6 @@ hal.settings: link_domain: type: string label: 'Domain of the relation' + bc_file_uri_as_url_normalizer: + type: boolean + label: 'Whether to retain pre Drupal 8.5 behavior of normalizing the File entity "uri" field value to an absolute URL.' diff --git a/core/modules/hal/hal.install b/core/modules/hal/hal.install index 78810e37f5341bd546ef48395383f3fb9265f3ca..87944844325eab5a327db3ccd4c533992faa5be3 100644 --- a/core/modules/hal/hal.install +++ b/core/modules/hal/hal.install @@ -31,3 +31,15 @@ function hal_update_8301() { $hal_settings->set('link_domain', $link_domain); $hal_settings->save(TRUE); } + +/** + * Add hal.settings::bc_file_uri_as_url_normalizer configuration. + */ +function hal_update_8501() { + $config_factory = \Drupal::configFactory(); + $config_factory->getEditable('hal.settings') + ->set('bc_file_uri_as_url_normalizer', TRUE) + ->save(TRUE); + + return t('Backwards compatibility mode has been enabled for File entities\' HAL normalization of the "uri" field. Like before, it will continue to return only the absolute file URL. If you want the new behavior, which returns both the stored URI and a root-relative file URL, read the change record to learn how to opt in.'); +} diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml index b2c898fc5608c809f76046b269cc8cd82e7894fd..a877163c08de63ddc9286b8fd770fc738a9187f3 100644 --- a/core/modules/hal/hal.services.yml +++ b/core/modules/hal/hal.services.yml @@ -14,9 +14,10 @@ services: - { name: normalizer, priority: 10 } 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', '@http_client', '@hal.link_manager', '@module_handler', '@config.factory'] tags: - { name: normalizer, priority: 20 } - arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler'] serializer.normalizer.timestamp_item.hal: class: Drupal\hal\Normalizer\TimestampItemNormalizer tags: diff --git a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php index ec870e9e14c6df6c306e62a73216ca468a24ab3d..5186f8b2559440ae037eeeb82bedb429de19e59b 100644 --- a/core/modules/hal/src/Normalizer/FileEntityNormalizer.php +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php @@ -2,6 +2,7 @@ namespace Drupal\hal\Normalizer; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\hal\LinkManager\LinkManagerInterface; @@ -9,6 +10,8 @@ /** * Converts the Drupal entity object structure to a HAL array structure. + * + * @deprecated in Drupal 8.5.0, to be removed before Drupal 9.0.0. */ class FileEntityNormalizer extends ContentEntityNormalizer { @@ -26,6 +29,13 @@ class FileEntityNormalizer extends ContentEntityNormalizer { */ protected $httpClient; + /** + * The HAL settings config. + * + * @var \Drupal\Core\Config\ImmutableConfig + */ + protected $halSettings; + /** * Constructs a FileEntityNormalizer object. * @@ -37,11 +47,14 @@ class FileEntityNormalizer extends ContentEntityNormalizer { * The hypermedia link manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. */ - public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler) { + 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'); } /** @@ -49,8 +62,13 @@ public function __construct(EntityManagerInterface $entity_manager, ClientInterf */ public function normalize($entity, $format = NULL, array $context = []) { $data = parent::normalize($entity, $format, $context); - // Replace the file url with a full url for the file. - $data['uri'][0]['value'] = $this->getEntityUri($entity); + + $this->addCacheableDependency($context, $this->halSettings); + + if ($this->halSettings->get('bc_file_uri_as_url_normalizer')) { + // Replace the file url with a full url for the file. + $data['uri'][0]['value'] = $this->getEntityUri($entity); + } return $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 ff89f745903fb617cb31f7ecd4e5c5f53d9082bc..f3036a9de8680885e80e0f63d50cea723a0d1461 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileHalJsonAnonTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\hal\Functional\EntityResource\File; +use Drupal\Core\Cache\Cache; use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; use Drupal\Tests\rest\Functional\EntityResource\File\FileResourceTestBase; @@ -38,7 +39,11 @@ protected function getExpectedNormalizedEntity() { $normalization = $this->applyHalFieldNormalization($default_normalization); $url = file_create_url($this->entity->getFileUri()); - $normalization['uri'][0]['value'] = $url; + // @see \Drupal\Tests\hal\Functional\EntityResource\File\FileHalJsonAnonTest::testGetBcUriField() + if ($this->config('hal.settings')->get('bc_file_uri_as_url_normalizer')) { + $normalization['uri'][0]['value'] = $url; + } + $uid = $this->author->id(); return $normalization + [ @@ -90,6 +95,13 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']); + } + /** * {@inheritdoc} */ @@ -100,6 +112,30 @@ protected function getExpectedCacheContexts() { ]; } + /** + * @see hal_update_8501() + */ + public function testGetBcUriField() { + $this->config('hal.settings')->set('bc_file_uri_as_url_normalizer', TRUE)->save(TRUE); + + $this->initAuthentication(); + $url = $this->getEntityResourceUrl(); + $url->setOption('query', ['_format' => static::$format]); + $request_options = $this->getAuthenticationRequestOptions('GET'); + $this->provisionEntityResource(); + $this->setUpAuthorization('GET'); + $response = $this->request('GET', $url, $request_options); + $expected = $this->getExpectedNormalizedEntity(); + static::recursiveKSort($expected); + $actual = $this->serializer->decode((string) $response->getBody(), static::$format); + static::recursiveKSort($actual); + $this->assertSame($expected, $actual); + + // Explicitly assert that $file->uri->value is an absolute file URL, unlike + // the default normalization. + $this->assertSame($this->baseUrl . '/' . $this->siteDirectory . '/files/drupal.txt', $actual['uri'][0]['value']); + } + /** * {@inheritdoc} */ 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 c71b54e6e6c8dd109789bf74ce97316f40413cad..c9ee7733cfa36820b1b03d6343f9590d9857cb2b 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\hal\Functional\EntityResource\Media; +use Drupal\Core\Cache\Cache; use Drupal\file\Entity\File; use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; @@ -86,11 +87,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $file->url(), - ], - ], 'uuid' => [ [ 'value' => $file->uuid(), @@ -126,11 +122,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $thumbnail->url(), - ], - ], 'uuid' => [ [ 'value' => $thumbnail->uuid(), @@ -173,4 +164,11 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']); + } + } diff --git a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php index 05ee23489394832d3457ad7c664a6d704bd905bd..9c00902988d4f8a5f1111cb5f4af56d7aaf5ec85 100644 --- a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php +++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php @@ -20,6 +20,20 @@ class FileDenormalizeTest extends BrowserTestBase { */ 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. */ diff --git a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php index c4b67f54e345b2237cbab1f44b6b815c16909f3b..355c09d1a67e7c523cd766123149689ed1465c89 100644 --- a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php @@ -44,7 +44,10 @@ public function testNormalize() { $expected_array = [ 'uri' => [ - ['value' => file_create_url($file->getFileUri())], + [ + 'value' => $file->getFileUri(), + 'url' => file_url_transform_relative(file_create_url($file->getFileUri())), + ], ], ]; diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 2962ef380ab2c827a20ad8cd071ba5180525ed31..6d0d0bd180708973d2217a8de8137aa99b6fb41a 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -461,8 +461,13 @@ public function testGet() { // 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()) { 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 c63853e0bf430db106be1b94836610eb76b7402c..1fa5a3856bb78c602203789e63000c89f372286c 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/File/FileResourceTestBase.php @@ -154,6 +154,7 @@ protected function getExpectedNormalizedEntity() { ], 'uri' => [ [ + 'url' => base_path() . $this->siteDirectory . '/files/drupal.txt', 'value' => 'public://drupal.txt', ], ], diff --git a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php index 094fcf10174d01cd6dc16fb17d65a1254d74134f..b7445fb899653a811cf6d00f6d797c753ae3a012 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php @@ -122,6 +122,7 @@ public static function getSkippedDeprecations() { 'drupal_set_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. See https://www.drupal.org/node/2774931', 'drupal_get_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::all() or \Drupal\Core\Messenger\MessengerInterface::messagesByType() instead. See https://www.drupal.org/node/2774931', 'Adding or retrieving messages prior to the container being initialized was deprecated in Drupal 8.5.0 and this functionality will be removed before Drupal 9.0.0. Please report this usage at https://www.drupal.org/node/2928994.', + 'The "serializer.normalizer.file_entity.hal" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.', ]; }