diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index 9b5b67dac18..0f0a77bb2b9 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -30,6 +30,13 @@ class PathItem extends FieldItemBase { */ protected $isLoaded = FALSE; + /** + * Whether the alias is currently being set. + * + * @var bool + */ + protected $isLoading = FALSE; + /** * {@inheritdoc} */ @@ -119,6 +126,17 @@ class PathItem extends FieldItemBase { return parent::get($property_name); } + /** + * {@inheritdoc} + */ + public function setValue($values, $notify = TRUE) { + // Also ensure that existing values are loaded when setting a value, this + // ensures that it is possible to set a new value immediately after loading + // an entity. + $this->ensureLoaded(); + return parent::setValue($values, $notify); + } + /** * {@inheritdoc} */ @@ -170,7 +188,9 @@ class PathItem extends FieldItemBase { * https://www.drupal.org/node/2392845. */ protected function ensureLoaded() { - if (!$this->isLoaded) { + // Make sure to avoid a infinite loop if setValue() has be called from this + // block which calls ensureLoaded(). + if (!$this->isLoaded && !$this->isLoading) { $entity = $this->getEntity(); if (!$entity->isNew()) { // @todo Support loading languge neutral aliases in @@ -180,7 +200,9 @@ class PathItem extends FieldItemBase { 'langcode' => $this->getLangcode(), ]); if ($alias) { + $this->isLoading = TRUE; $this->setValue($alias); + $this->isLoading = FALSE; } else { // If there is no existing alias, default the langcode to the current diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php index b4fc553f082..492ff642e42 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php @@ -7,6 +7,7 @@ use Drupal\node\Entity\NodeType; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; use Drupal\user\Entity\User; +use GuzzleHttp\RequestOptions; abstract class NodeResourceTestBase extends EntityResourceTestBase { @@ -215,4 +216,60 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase { return parent::getExpectedUnauthorizedAccessMessage($method); } + /** + * Tests PATCHing a node's path with and without 'create url aliases'. + * + * For a positive test, see the similar test coverage for Term. + * + * @see \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath() + */ + public function testPatchPath() { + $this->initAuthentication(); + $this->provisionEntityResource(); + $this->setUpAuthorization('GET'); + $this->setUpAuthorization('PATCH'); + + $url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]); + + // GET node's current normalization. + $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET')); + $normalization = $this->serializer->decode((string) $response->getBody(), static::$format); + + // @todo In https://www.drupal.org/node/2824851, we will be able to stop + // unsetting these fields from the normalization, because + // EntityResource::patch() will ignore any fields that are sent that + // match the current value (and obviously we're sending the current + // value). + $normalization = $this->removeFieldsFromNormalization($normalization, [ + 'revision_timestamp', + 'revision_uid', + 'created', + 'changed', + 'promote', + 'sticky', + ]); + + // Change node's path alias. + $normalization['path'][0]['alias'] .= 's-rule-the-world'; + + // Create node PATCH request. + $request_options = []; + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + // PATCH request: 403 when creating URL aliases unauthorized. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceErrorResponse(403, "Access denied on updating field 'path'.", $response); + + // Grant permission to create URL aliases. + $this->grantPermissionsToTestedRole(['create url aliases']); + + // Repeat PATCH request: 200. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + $updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format); + $this->assertSame($normalization['path'], $updated_normalization['path']); + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 97599774b32..e0a15813a82 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -6,6 +6,7 @@ use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; +use GuzzleHttp\RequestOptions; abstract class TermResourceTestBase extends EntityResourceTestBase { @@ -185,4 +186,48 @@ abstract class TermResourceTestBase extends EntityResourceTestBase { } } + /** + * Tests PATCHing a term's path. + * + * For a negative test, see the similar test coverage for Node. + * + * @see \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase::testPatchPath() + */ + public function testPatchPath() { + $this->initAuthentication(); + $this->provisionEntityResource(); + $this->setUpAuthorization('GET'); + $this->setUpAuthorization('PATCH'); + + $url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]); + + // GET term's current normalization. + $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET')); + $normalization = $this->serializer->decode((string) $response->getBody(), static::$format); + + // @todo In https://www.drupal.org/node/2824851, we will be able to stop + // unsetting these fields from the normalization, because + // EntityResource::patch() will ignore any fields that are sent that + // match the current value (and obviously we're sending the current + // value). + $normalization = $this->removeFieldsFromNormalization($normalization, [ + 'changed', + ]); + + // Change term's path alias. + $normalization['path'][0]['alias'] .= 's-rule-the-world'; + + // Create term PATCH request. + $request_options = []; + $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; + $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH')); + $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format); + + // PATCH request: 200. + $response = $this->request('PATCH', $url, $request_options); + $this->assertResourceResponse(200, FALSE, $response); + $updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format); + $this->assertSame($normalization['path'], $updated_normalization['path']); + } + }