diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 60a15d6190..fc3ce622c7 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -48,6 +48,17 @@ function file_help($route_name, RouteMatchInterface $route_match) { } } +/** + * Implements hook_field_widget_info_alter(). + */ +function file_field_widget_info_alter(array &$info) { + // This allows setting a valid default widget in the FileUriItem annotation. + // It's not strictly needed, but it's a required value on the annotation so + // should be a valid value. + // @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 0000000000..2eb012edc6 --- /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 a9ade9e7d6..4060b773ee 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 055e32dc8b..0facb7e033 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 0000000000..84f09406f3 --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldType/FileUriItem.php @@ -0,0 +1,40 @@ +setLabel(t('Root-relative file URL')) + ->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive')) + ->setComputed(TRUE) + ->setInternal(FALSE) + ->setClass(ComputedFileUrl::class); + + return $properties; + } + +} 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 0000000000..d67eba0707 --- /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/file/tests/src/Unit/ComputedFileUrlTest.php b/core/modules/file/tests/src/Unit/ComputedFileUrlTest.php new file mode 100644 index 0000000000..8e2a4a469e --- /dev/null +++ b/core/modules/file/tests/src/Unit/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/hal/config/install/hal.settings.yml b/core/modules/hal/config/install/hal.settings.yml index 67107af00d..1c895c1718 100644 --- a/core/modules/hal/config/install/hal.settings.yml +++ b/core/modules/hal/config/install/hal.settings.yml @@ -1,3 +1,4 @@ # Set the domain for HAL type and relation links. # If left blank, the site's domain will be used. link_domain: ~ +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 3192d67580..b70f47eaff 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 file URI values as a full URL.' diff --git a/core/modules/hal/hal.install b/core/modules/hal/hal.install index 78810e37f5..0dc3fd566d 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('The REST API will no longer return the full URL for file entity URI values for HAL+JSON responses. It will return the URI value itself, with an additional URL property providing a root relative file path. If your site depends on these value being strings, read the change record to learn how to enable the BC mode.'); +} diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml index b2c898fc56..a877163c08 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 ec870e9e14..d3b1797a57 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.x, to be removed before Drupal 9.0.x. */ class FileEntityNormalizer extends ContentEntityNormalizer { @@ -26,6 +29,13 @@ class FileEntityNormalizer extends ContentEntityNormalizer { */ protected $httpClient; + /** + * The hal settings config. + * + * @var \Drupal\Core\Config\Config + */ + 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/Media/MediaHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php index c71b54e6e6..66e1cc08f3 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/Media/MediaHalJsonAnonTest.php @@ -86,11 +86,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $file->url(), - ], - ], 'uuid' => [ [ 'value' => $file->uuid(), @@ -126,11 +121,6 @@ protected function getExpectedNormalizedEntity() { ], ], 'lang' => 'en', - 'uri' => [ - [ - 'value' => $thumbnail->url(), - ], - ], 'uuid' => [ [ 'value' => $thumbnail->uuid(), diff --git a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php index 05ee234893..dbb708f8ca 100644 --- a/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php +++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php @@ -20,6 +20,17 @@ class FileDenormalizeTest extends BrowserTestBase { */ public static $modules = ['hal', 'file', 'node']; + protected function setUp() { + parent::setUp(); + + // Override the default configuration to the hal BC setting is enabled, to + // return the full URL value as the file URI 'value'. + \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 c4b67f54e3..355c09d1a6 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/tests/Drupal/Tests/Listeners/DeprecationListener.php b/core/tests/Drupal/Tests/Listeners/DeprecationListener.php index 3673376d61..352819d98d 100644 --- a/core/tests/Drupal/Tests/Listeners/DeprecationListener.php +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListener.php @@ -111,6 +111,7 @@ public static function getSkippedDeprecations() { 'The Drupal\migrate_drupal\Plugin\migrate\source\d6\i18nVariable is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Instead, use Drupal\migrate_drupal\Plugin\migrate\source\d6\VariableTranslation', 'Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.5.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling. See https://www.drupal.org/node/2918937', 'Automatically creating the first item for computed fields is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', + 'The "serializer.normalizer.file_entity.hal" normalizer service is deprecated: it is obsolete, it only remains available for backwards compatibility.', ]; }