summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/jsonapi
diff options
context:
space:
mode:
Diffstat (limited to 'core/modules/jsonapi')
-rw-r--r--core/modules/jsonapi/jsonapi.api.php17
-rw-r--r--core/modules/jsonapi/jsonapi.install51
-rw-r--r--core/modules/jsonapi/jsonapi.module16
-rw-r--r--core/modules/jsonapi/src/Access/TemporaryQueryGuard.php31
-rw-r--r--core/modules/jsonapi/src/Controller/EntityResource.php12
-rw-r--r--core/modules/jsonapi/src/Hook/JsonapiHooks.php61
-rw-r--r--core/modules/jsonapi/src/Hook/JsonapiRequirements.php73
-rw-r--r--core/modules/jsonapi/src/JsonApiFilter.php77
-rw-r--r--core/modules/jsonapi/src/Normalizer/Value/TemporaryArrayObjectThrowingExceptions.php2
-rw-r--r--core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml11
-rw-r--r--core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php241
-rw-r--r--core/modules/jsonapi/tests/src/Functional/FileUploadTest.php2
-rw-r--r--core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php157
-rw-r--r--core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php3
14 files changed, 642 insertions, 112 deletions
diff --git a/core/modules/jsonapi/jsonapi.api.php b/core/modules/jsonapi/jsonapi.api.php
index 5b2f2002d25c..ca8c5ae993fb 100644
--- a/core/modules/jsonapi/jsonapi.api.php
+++ b/core/modules/jsonapi/jsonapi.api.php
@@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Access\AccessResult;
+use Drupal\jsonapi\JsonApiFilter;
/**
* @defgroup jsonapi_architecture JSON:API Architecture
@@ -263,10 +264,10 @@ use Drupal\Core\Access\AccessResult;
* viewable.
* - AccessResult::neutral() if the implementation has no opinion.
* The supported subsets for which an access result may be returned are:
- * - JSONAPI_FILTER_AMONG_ALL: all entities of the given type.
- * - JSONAPI_FILTER_AMONG_PUBLISHED: all published entities of the given type.
- * - JSONAPI_FILTER_AMONG_ENABLED: all enabled entities of the given type.
- * - JSONAPI_FILTER_AMONG_OWN: all entities of the given type owned by the
+ * - JsonApiFilter::AMONG_ALL: all entities of the given type.
+ * - JsonApiFilter::AMONG_PUBLISHED: all published entities of the given type.
+ * - JsonApiFilter::AMONG_ENABLED: all enabled entities of the given type.
+ * - JsonApiFilter::AMONG_OWN: all entities of the given type owned by the
* user for whom access is being checked.
* See the documentation of the above constants for more information about
* each subset.
@@ -278,7 +279,7 @@ function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, Acc
// by all entities of that type to users with that permission.
if ($admin_permission = $entity_type->getAdminPermission()) {
return ([
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
]);
}
}
@@ -305,9 +306,9 @@ function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, Acc
*/
function hook_jsonapi_ENTITY_TYPE_filter_access(EntityTypeInterface $entity_type, AccountInterface $account): array {
return ([
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'),
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'),
- JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'),
+ JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'),
]);
}
diff --git a/core/modules/jsonapi/jsonapi.install b/core/modules/jsonapi/jsonapi.install
index c5a60c424fbe..47efd893ae0e 100644
--- a/core/modules/jsonapi/jsonapi.install
+++ b/core/modules/jsonapi/jsonapi.install
@@ -5,8 +5,6 @@
* Module install file.
*/
-use Drupal\Core\Url;
-
/**
* Implements hook_install().
*/
@@ -28,55 +26,6 @@ function jsonapi_install(): void {
}
/**
- * Implements hook_requirements().
- */
-function jsonapi_requirements($phase): array {
- $requirements = [];
- if ($phase === 'runtime') {
- $module_handler = \Drupal::moduleHandler();
- $potential_conflicts = [
- 'content_translation',
- 'config_translation',
- 'language',
- ];
- $should_warn = array_reduce($potential_conflicts, function ($should_warn, $module_name) use ($module_handler) {
- return $should_warn ?: $module_handler->moduleExists($module_name);
- }, FALSE);
- if ($should_warn) {
- $requirements['jsonapi_multilingual_support'] = [
- 'title' => t('JSON:API multilingual support'),
- 'value' => t('Limited'),
- 'severity' => REQUIREMENT_INFO,
- 'description' => t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [
- ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations',
- ]),
- ];
- }
- $requirements['jsonapi_revision_support'] = [
- 'title' => t('JSON:API revision support'),
- 'value' => t('Limited'),
- 'severity' => REQUIREMENT_INFO,
- 'description' => t('Revision support is currently read-only and only for the "Content" and "Media" entity types in JSON:API. See the <a href=":jsonapi-docs">JSON:API revision support documentation</a> for more information on the current status of revision support.', [
- ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/revisions',
- ]),
- ];
- $requirements['jsonapi_read_only_mode'] = [
- 'title' => t('JSON:API allowed operations'),
- 'value' => t('Read-only'),
- 'severity' => REQUIREMENT_INFO,
- ];
- if (!\Drupal::configFactory()->get('jsonapi.settings')->get('read_only')) {
- $requirements['jsonapi_read_only_mode']['value'] = t('All (create, read, update, delete)');
- $requirements['jsonapi_read_only_mode']['description'] = t('It is recommended to <a href=":configure-url">configure</a> JSON:API to only accept all operations if the site requires it. <a href=":docs">Learn more about securing your site with JSON:API.</a>', [
- ':docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/security-considerations',
- ':configure-url' => Url::fromRoute('jsonapi.settings')->toString(),
- ]);
- }
- }
- return $requirements;
-}
-
-/**
* Implements hook_update_last_removed().
*/
function jsonapi_update_last_removed(): int {
diff --git a/core/modules/jsonapi/jsonapi.module b/core/modules/jsonapi/jsonapi.module
index 69414af650b9..c512575305a4 100644
--- a/core/modules/jsonapi/jsonapi.module
+++ b/core/modules/jsonapi/jsonapi.module
@@ -11,6 +11,10 @@
* regardless of whether they are published or enabled, and regardless of
* their owner.
*
+ * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use
+ * \Drupal\jsonapi\JsonApiFilter::AMONG_ALL instead.
+ *
+ * @see https://www.drupal.org/node/3495601
* @see hook_jsonapi_entity_filter_access()
* @see hook_jsonapi_ENTITY_TYPE_filter_access()
*/
@@ -25,6 +29,10 @@ const JSONAPI_FILTER_AMONG_ALL = 'filter_among_all';
* This is used when an entity type has a "published" entity key and there's a
* query condition for the value of that equaling 1.
*
+ * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use
+ * \Drupal\jsonapi\JsonApiFilter::AMONG_PUBLISHED instead.
+ *
+ * @see https://www.drupal.org/node/3495601
* @see hook_jsonapi_entity_filter_access()
* @see hook_jsonapi_ENTITY_TYPE_filter_access()
*/
@@ -42,6 +50,10 @@ const JSONAPI_FILTER_AMONG_PUBLISHED = 'filter_among_published';
* For the User entity type, which does not have a "status" entity key, the
* "status" field is used.
*
+ * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use
+ * \Drupal\jsonapi\JsonApiFilter::AMONG_ENABLED instead.
+ *
+ * @see https://www.drupal.org/node/3495601
* @see hook_jsonapi_entity_filter_access()
* @see hook_jsonapi_ENTITY_TYPE_filter_access()
*/
@@ -64,6 +76,10 @@ const JSONAPI_FILTER_AMONG_ENABLED = 'filter_among_enabled';
* - The entity type has an "owner" entity key.
* - There's a filter/query condition for the value equal to the user's ID.
*
+ * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use
+ * \Drupal\jsonapi\JsonApiFilter::AMONG_OWN instead.
+ *
+ * @see https://www.drupal.org/node/3495601
* @see hook_jsonapi_entity_filter_access()
* @see hook_jsonapi_ENTITY_TYPE_filter_access()
*/
diff --git a/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php b/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php
index d59dca4ec03a..2888fbbec77c 100644
--- a/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php
+++ b/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php
@@ -12,6 +12,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
+use Drupal\jsonapi\JsonApiFilter;
use Drupal\jsonapi\Query\EntityCondition;
use Drupal\jsonapi\Query\EntityConditionGroup;
use Drupal\jsonapi\Query\Filter;
@@ -323,12 +324,12 @@ class TemporaryQueryGuard {
}
/**
- * Gets an access condition for the allowed JSONAPI_FILTER_AMONG_* subsets.
+ * Gets an access condition for the allowed JsonApiFilter::AMONG_* subsets.
*
- * If access is allowed for the JSONAPI_FILTER_AMONG_ALL subset, then no
+ * If access is allowed for the JsonApiFilter::AMONG_ALL subset, then no
* conditions are returned. Otherwise, if access is allowed for
- * JSONAPI_FILTER_AMONG_PUBLISHED, JSONAPI_FILTER_AMONG_ENABLED, or
- * JSONAPI_FILTER_AMONG_OWN, then a condition group is returned for the union
+ * JsonApiFilter::AMONG_PUBLISHED, JsonApiFilter::AMONG_ENABLED, or
+ * JsonApiFilter::AMONG_OWN, then a condition group is returned for the union
* of allowed subsets. If no subsets are allowed, then static::alwaysFalse()
* is returned.
*
@@ -344,12 +345,12 @@ class TemporaryQueryGuard {
* secure an entity query.
*/
protected static function getAccessConditionForKnownSubsets(EntityTypeInterface $entity_type, AccountInterface $account, CacheableMetadata $cacheability) {
- // Get the combined access results for each JSONAPI_FILTER_AMONG_* subset.
+ // Get the combined access results for each JsonApiFilter::AMONG_* subset.
$access_results = static::getAccessResultsFromEntityFilterHook($entity_type, $account);
// No conditions are needed if access is allowed for all entities.
- $cacheability->addCacheableDependency($access_results[JSONAPI_FILTER_AMONG_ALL]);
- if ($access_results[JSONAPI_FILTER_AMONG_ALL]->isAllowed()) {
+ $cacheability->addCacheableDependency($access_results[JsonApiFilter::AMONG_ALL]);
+ if ($access_results[JsonApiFilter::AMONG_ALL]->isAllowed()) {
return NULL;
}
@@ -363,7 +364,7 @@ class TemporaryQueryGuard {
// The "published" subset.
$published_field_name = $entity_type->getKey('published');
if ($published_field_name) {
- $access_result = $access_results[JSONAPI_FILTER_AMONG_PUBLISHED];
+ $access_result = $access_results[JsonApiFilter::AMONG_PUBLISHED];
$cacheability->addCacheableDependency($access_result);
if ($access_result->isAllowed()) {
$conditions[] = new EntityCondition($published_field_name, 1);
@@ -375,7 +376,7 @@ class TemporaryQueryGuard {
// @todo Remove ternary when the 'status' key is added to the User entity type.
$status_field_name = $entity_type->id() === 'user' ? 'status' : $entity_type->getKey('status');
if ($status_field_name) {
- $access_result = $access_results[JSONAPI_FILTER_AMONG_ENABLED];
+ $access_result = $access_results[JsonApiFilter::AMONG_ENABLED];
$cacheability->addCacheableDependency($access_result);
if ($access_result->isAllowed()) {
$conditions[] = new EntityCondition($status_field_name, 1);
@@ -387,7 +388,7 @@ class TemporaryQueryGuard {
// @todo Remove ternary when the 'uid' key is added to the User entity type.
$owner_field_name = $entity_type->id() === 'user' ? 'uid' : $entity_type->getKey('owner');
if ($owner_field_name) {
- $access_result = $access_results[JSONAPI_FILTER_AMONG_OWN];
+ $access_result = $access_results[JsonApiFilter::AMONG_OWN];
$cacheability->addCacheableDependency($access_result);
if ($access_result->isAllowed()) {
$cacheability->addCacheContexts(['user']);
@@ -415,7 +416,7 @@ class TemporaryQueryGuard {
}
/**
- * Gets the combined access result for each JSONAPI_FILTER_AMONG_* subset.
+ * Gets the combined access result for each JsonApiFilter::AMONG_* subset.
*
* This invokes hook_jsonapi_entity_filter_access() and
* hook_jsonapi_ENTITY_TYPE_filter_access() and combines the results from all
@@ -433,10 +434,10 @@ class TemporaryQueryGuard {
protected static function getAccessResultsFromEntityFilterHook(EntityTypeInterface $entity_type, AccountInterface $account) {
/** @var \Drupal\Core\Access\AccessResultInterface[] $combined_access_results */
$combined_access_results = [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::neutral(),
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::neutral(),
- JSONAPI_FILTER_AMONG_ENABLED => AccessResult::neutral(),
- JSONAPI_FILTER_AMONG_OWN => AccessResult::neutral(),
+ JsonApiFilter::AMONG_ALL => AccessResult::neutral(),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::neutral(),
+ JsonApiFilter::AMONG_ENABLED => AccessResult::neutral(),
+ JsonApiFilter::AMONG_OWN => AccessResult::neutral(),
];
// Invoke hook_jsonapi_entity_filter_access() and
diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php
index 6416d6cb27ac..53822cdf1f1f 100644
--- a/core/modules/jsonapi/src/Controller/EntityResource.php
+++ b/core/modules/jsonapi/src/Controller/EntityResource.php
@@ -672,9 +672,10 @@ class EntityResource {
return $this->getRelationship($resource_type, $entity, $related, $request, $status);
}
- $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName();
foreach ($new_resource_identifiers as $new_resource_identifier) {
- $new_field_value = [$main_property_name => $this->getEntityFromResourceIdentifier($new_resource_identifier)->id()];
+ // We assume all entity reference fields have an 'entity' computed
+ // property that can be used to assign the needed values.
+ $new_field_value = ['entity' => $this->getEntityFromResourceIdentifier($new_resource_identifier)];
// Remove `arity` from the received extra properties, otherwise this
// will fail field validation.
$new_field_value += array_diff_key($new_resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY]));
@@ -760,9 +761,10 @@ class EntityResource {
* The field definition of the entity field to be updated.
*/
protected function doPatchMultipleRelationship(EntityInterface $entity, array $resource_identifiers, FieldDefinitionInterface $field_definition) {
- $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName();
- $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) use ($main_property_name) {
- $field_properties = [$main_property_name => $this->getEntityFromResourceIdentifier($resource_identifier)->id()];
+ $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) {
+ // We assume all entity reference fields have an 'entity' computed
+ // property that can be used to assign the needed values.
+ $field_properties = ['entity' => $this->getEntityFromResourceIdentifier($resource_identifier)];
// Remove `arity` from the received extra properties, otherwise this
// will fail field validation.
$field_properties += array_diff_key($resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY]));
diff --git a/core/modules/jsonapi/src/Hook/JsonapiHooks.php b/core/modules/jsonapi/src/Hook/JsonapiHooks.php
index 7db75a3297be..d5f6d2540fcd 100644
--- a/core/modules/jsonapi/src/Hook/JsonapiHooks.php
+++ b/core/modules/jsonapi/src/Hook/JsonapiHooks.php
@@ -7,6 +7,7 @@ use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\jsonapi\JsonApiFilter;
use Drupal\jsonapi\Routing\Routes;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
@@ -107,7 +108,7 @@ class JsonapiHooks {
// AccessResult::forbidden() from its implementation of this hook.
if ($admin_permission = $entity_type->getAdminPermission()) {
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
];
}
return [];
@@ -122,8 +123,8 @@ class JsonapiHooks {
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (isReusable()), so this does not have to.
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'),
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed(),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowed(),
];
}
@@ -136,8 +137,8 @@ class JsonapiHooks {
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (access to the commented entity), so this does not have to.
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'),
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'),
];
}
@@ -148,7 +149,7 @@ class JsonapiHooks {
public function jsonapiEntityTestFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array {
// @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'),
];
}
@@ -161,7 +162,7 @@ class JsonapiHooks {
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (public OR owner), so this does not have to.
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'),
];
}
@@ -172,7 +173,7 @@ class JsonapiHooks {
public function jsonapiMediaFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
return [
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'),
];
}
@@ -184,30 +185,30 @@ class JsonapiHooks {
// @see \Drupal\node\NodeAccessControlHandler::access()
if ($account->hasPermission('bypass node access')) {
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowed()->cachePerPermissions(),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowed()->cachePerPermissions(),
];
}
if (!$account->hasPermission('access content')) {
$forbidden = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
return [
- JSONAPI_FILTER_AMONG_ALL => $forbidden,
- JSONAPI_FILTER_AMONG_OWN => $forbidden,
- JSONAPI_FILTER_AMONG_PUBLISHED => $forbidden,
+ JsonApiFilter::AMONG_ALL => $forbidden,
+ JsonApiFilter::AMONG_OWN => $forbidden,
+ JsonApiFilter::AMONG_PUBLISHED => $forbidden,
// For legacy reasons, the Node entity type has a "status" key, so
// forbid this subset as well, even though it has no semantic meaning.
- JSONAPI_FILTER_AMONG_ENABLED => $forbidden,
+ JsonApiFilter::AMONG_ENABLED => $forbidden,
];
}
return [
- // @see \Drupal\node\NodeAccessControlHandler::checkAccess()
- JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'),
- // @see \Drupal\node\NodeGrantDatabaseStorage::access()
- // Note that:
- // - This is just for the default grant. Other node access conditions
- // are added via the 'node_access' query tag.
- // - Permissions were checked earlier in this function, so we must
- // vary the cache by them.
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(),
+ // @see \Drupal\node\NodeAccessControlHandler::checkAccess()
+ JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'),
+ // @see \Drupal\node\NodeGrantDatabaseStorage::access()
+ // Note that:
+ // - This is just for the default grant. Other node access conditions
+ // are added via the 'node_access' query tag.
+ // - Permissions were checked earlier in this function, so we must
+ // vary the cache by them.
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(),
];
}
@@ -218,10 +219,10 @@ class JsonapiHooks {
public function jsonapiShortcutFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array {
// @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
- // (shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)),
+ // "shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)"
// so this does not have to.
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [
'access shortcuts',
'customize shortcut links',
])),
@@ -235,8 +236,8 @@ class JsonapiHooks {
public function jsonapiTaxonomyTermFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array {
// @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess()
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'),
- JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'),
+ JsonApiFilter::AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'),
];
}
@@ -249,8 +250,8 @@ class JsonapiHooks {
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (!isAnonymous()), so this does not have to.
return [
- JSONAPI_FILTER_AMONG_OWN => AccessResult::allowed(),
- JSONAPI_FILTER_AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'),
+ JsonApiFilter::AMONG_OWN => AccessResult::allowed(),
+ JsonApiFilter::AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'),
];
}
@@ -261,8 +262,8 @@ class JsonapiHooks {
public function jsonapiWorkspaceFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account): array {
// @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
return [
- JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
- JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),
+ JsonApiFilter::AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
+ JsonApiFilter::AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),
];
}
diff --git a/core/modules/jsonapi/src/Hook/JsonapiRequirements.php b/core/modules/jsonapi/src/Hook/JsonapiRequirements.php
new file mode 100644
index 000000000000..4903389fddfd
--- /dev/null
+++ b/core/modules/jsonapi/src/Hook/JsonapiRequirements.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi\Hook;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+
+/**
+ * Requirements for the JSON:API module.
+ */
+class JsonapiRequirements {
+
+ use StringTranslationTrait;
+
+ public function __construct(
+ protected readonly ConfigFactoryInterface $configFactory,
+ protected readonly ModuleHandlerInterface $moduleHandler,
+ ) {}
+
+ /**
+ * Implements hook_runtime_requirements().
+ */
+ #[Hook('runtime_requirements')]
+ public function runtime(): array {
+ $requirements = [];
+ $potential_conflicts = [
+ 'content_translation',
+ 'config_translation',
+ 'language',
+ ];
+ $should_warn = array_reduce($potential_conflicts, function ($should_warn, $module_name) {
+ return $should_warn ?: $this->moduleHandler->moduleExists($module_name);
+ }, FALSE);
+ if ($should_warn) {
+ $requirements['jsonapi_multilingual_support'] = [
+ 'title' => $this->t('JSON:API multilingual support'),
+ 'value' => $this->t('Limited'),
+ 'severity' => RequirementSeverity::Info,
+ 'description' => $this->t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [
+ ':jsonapi-docs' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/translations',
+ ]),
+ ];
+ }
+ $requirements['jsonapi_revision_support'] = [
+ 'title' => $this->t('JSON:API revision support'),
+ 'value' => $this->t('Limited'),
+ 'severity' => RequirementSeverity::Info,
+ 'description' => $this->t('Revision support is currently read-only and only for the "Content" and "Media" entity types in JSON:API. See the <a href=":jsonapi-docs">JSON:API revision support documentation</a> for more information on the current status of revision support.', [
+ ':jsonapi-docs' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/revisions',
+ ]),
+ ];
+ $requirements['jsonapi_read_only_mode'] = [
+ 'title' => $this->t('JSON:API allowed operations'),
+ 'value' => $this->t('Read-only'),
+ 'severity' => RequirementSeverity::Info,
+ ];
+ if (!$this->configFactory->get('jsonapi.settings')->get('read_only')) {
+ $requirements['jsonapi_read_only_mode']['value'] = $this->t('All (create, read, update, delete)');
+ $requirements['jsonapi_read_only_mode']['description'] = $this->t('It is recommended to <a href=":configure-url">configure</a> JSON:API to only accept all operations if the site requires it. <a href=":docs">Learn more about securing your site with JSON:API.</a>', [
+ ':docs' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/security-considerations',
+ ':configure-url' => Url::fromRoute('jsonapi.settings')->toString(),
+ ]);
+ }
+ return $requirements;
+ }
+
+}
diff --git a/core/modules/jsonapi/src/JsonApiFilter.php b/core/modules/jsonapi/src/JsonApiFilter.php
new file mode 100644
index 000000000000..c9ac90be7af7
--- /dev/null
+++ b/core/modules/jsonapi/src/JsonApiFilter.php
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi;
+
+/**
+ * JsonApi filter options.
+ */
+final class JsonApiFilter {
+
+ /**
+ * Array key for denoting type-based filtering access.
+ *
+ * Array key for denoting access to filter among all entities of a given type,
+ * regardless of whether they are published or enabled, and regardless of
+ * their owner.
+ *
+ * @see hook_jsonapi_entity_filter_access()
+ * @see hook_jsonapi_ENTITY_TYPE_filter_access()
+ */
+ const AMONG_ALL = 'filter_among_all';
+
+ /**
+ * Array key for denoting type-based published-only filtering access.
+ *
+ * Array key for denoting access to filter among all published entities of a
+ * given type, regardless of their owner.
+ *
+ * This is used when an entity type has a "published" entity key and there's a
+ * query condition for the value of that equaling 1.
+ *
+ * @see hook_jsonapi_entity_filter_access()
+ * @see hook_jsonapi_ENTITY_TYPE_filter_access()
+ */
+ const AMONG_PUBLISHED = 'filter_among_published';
+
+ /**
+ * Array key for denoting type-based enabled-only filtering access.
+ *
+ * Array key for denoting access to filter among all enabled entities of a
+ * given type, regardless of their owner.
+ *
+ * This is used when an entity type has a "status" entity key and there's a
+ * query condition for the value of that equaling 1.
+ *
+ * For the User entity type, which does not have a "status" entity key, the
+ * "status" field is used.
+ *
+ * @see hook_jsonapi_entity_filter_access()
+ * @see hook_jsonapi_ENTITY_TYPE_filter_access()
+ */
+ const AMONG_ENABLED = 'filter_among_enabled';
+
+ /**
+ * Array key for denoting type-based owned-only filtering access.
+ *
+ * Array key for denoting access to filter among all entities of a given type,
+ * regardless of whether they are published or enabled, so long as they are
+ * owned by the user for whom access is being checked.
+ *
+ * When filtering among User entities, this is used when access is being
+ * checked for an authenticated user and there's a query condition
+ * limiting the result set to just that user's entity object.
+ *
+ * When filtering among entities of another type, this is used when all of the
+ * following conditions are met:
+ * - Access is being checked for an authenticated user.
+ * - The entity type has an "owner" entity key.
+ * - There's a filter/query condition for the value equal to the user's ID.
+ *
+ * @see hook_jsonapi_entity_filter_access()
+ * @see hook_jsonapi_ENTITY_TYPE_filter_access()
+ */
+ const AMONG_OWN = 'filter_among_own';
+
+}
diff --git a/core/modules/jsonapi/src/Normalizer/Value/TemporaryArrayObjectThrowingExceptions.php b/core/modules/jsonapi/src/Normalizer/Value/TemporaryArrayObjectThrowingExceptions.php
index d6e766416b0b..0f3c74f1ef53 100644
--- a/core/modules/jsonapi/src/Normalizer/Value/TemporaryArrayObjectThrowingExceptions.php
+++ b/core/modules/jsonapi/src/Normalizer/Value/TemporaryArrayObjectThrowingExceptions.php
@@ -97,7 +97,7 @@ class TemporaryArrayObjectThrowingExceptions extends \ArrayObject {
}
/**
- * Gets the class name of the array iterator that is used by \ArrayObject::getIterator().
+ * Gets the class name of the iterator used by \ArrayObject::getIterator().
*
* @throws \Exception
* This class does not support this action but it must implement it, because
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml
new file mode 100644
index 000000000000..9d082c7649c3
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml
@@ -0,0 +1,11 @@
+field.storage_settings.jsonapi_test_field_type_entity_reference_uuid:
+ type: field.storage_settings.entity_reference
+ label: 'Entity reference field storage settings'
+
+field.field_settings.jsonapi_test_field_type_entity_reference_uuid:
+ type: field.field_settings.entity_reference
+ label: 'Entity reference field settings'
+
+field.value.jsonapi_test_field_type_entity_reference_uuid:
+ type: field.value.entity_reference
+ label: 'Default value'
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php b/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php
new file mode 100644
index 000000000000..f9aa3e77bbe9
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php
@@ -0,0 +1,241 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi_test_field_type\Plugin\Field\FieldType;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\TypedData\EntityDataDefinition;
+use Drupal\Core\Field\Attribute\FieldType;
+use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataReferenceDefinition;
+use Drupal\Core\TypedData\DataReferenceTargetDefinition;
+
+/**
+ * Defines the 'entity_reference_uuid' entity field type.
+ *
+ * Supported settings (below the definition's 'settings' key) are:
+ * - target_type: The entity type to reference. Required.
+ *
+ * @property string $target_uuid
+ */
+#[FieldType(
+ id: 'jsonapi_test_field_type_entity_reference_uuid',
+ label: new TranslatableMarkup('Entity reference UUID'),
+ description: new TranslatableMarkup('An entity field containing an entity reference by UUID.'),
+ category: 'reference',
+ default_widget: 'entity_reference_autocomplete',
+ default_formatter: 'entity_reference_label',
+ list_class: EntityReferenceFieldItemList::class,
+)]
+class EntityReferenceUuidItem extends EntityReferenceItem {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+ $settings = $field_definition->getSettings();
+ $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']);
+
+ $properties = parent::propertyDefinitions($field_definition);
+
+ $target_uuid_definition = DataReferenceTargetDefinition::create('string')
+ ->setLabel(new TranslatableMarkup('@label UUID', ['@label' => $target_type_info->getLabel()]));
+
+ $target_uuid_definition->setRequired(TRUE);
+ $properties['target_uuid'] = $target_uuid_definition;
+
+ $properties['entity'] = DataReferenceDefinition::create('entity')
+ ->setLabel($target_type_info->getLabel())
+ ->setDescription(new TranslatableMarkup('The referenced entity by UUID'))
+ // The entity object is computed out of the entity ID.
+ ->setComputed(TRUE)
+ ->setReadOnly(FALSE)
+ ->setTargetDefinition(EntityDataDefinition::create($settings['target_type']))
+ // We can add a constraint for the target entity type. The list of
+ // referenceable bundles is a field setting, so the corresponding
+ // constraint is added dynamically in ::getConstraints().
+ ->addConstraint('EntityType', $settings['target_type']);
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function mainPropertyName() {
+ return 'target_uuid';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function schema(FieldStorageDefinitionInterface $field_definition) {
+ $columns = [
+ 'target_uuid' => [
+ 'description' => 'The UUID of the target entity.',
+ 'type' => 'varchar_ascii',
+ 'length' => 128,
+ ],
+ ];
+
+ return [
+ 'columns' => $columns,
+ 'indexes' => [
+ 'target_uuid' => ['target_uuid'],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValue($values, $notify = TRUE): void {
+ if (isset($values) && !is_array($values)) {
+ // If either a scalar or an object was passed as the value for the item,
+ // assign it to the 'entity' or 'target_uuid' depending on values type.
+ if (is_object($values)) {
+ $this->set('entity', $values, $notify);
+ }
+ else {
+ $this->set('target_uuid', $values, $notify);
+ }
+ }
+ else {
+ parent::setValue($values, FALSE);
+ // Support setting the field item with only one property, but make sure
+ // values stay in sync if only property is passed.
+ // NULL is a valid value, so we use array_key_exists().
+ if (is_array($values) && array_key_exists('target_uuid', $values) && !isset($values['entity'])) {
+ $this->onChange('target_uuid', FALSE);
+ }
+ elseif (is_array($values) && !array_key_exists('target_uuid', $values) && isset($values['entity'])) {
+ $this->onChange('entity', FALSE);
+ }
+ elseif (is_array($values) && array_key_exists('target_uuid', $values) && isset($values['entity'])) {
+ // If both properties are passed, verify the passed values match. The
+ // only exception we allow is when we have a new entity: in this case
+ // its actual id and target_uuid will be different, due to the new
+ // entity marker.
+ $entity_uuid = $this->get('entity')->get('uuid');
+ // If the entity has been saved and we're trying to set both the
+ // target_uuid and the entity values with a non-null target UUID, then
+ // the value for target_uuid should match the UUID of the entity value.
+ if (!$this->entity->isNew() && $values['target_uuid'] !== NULL && ($entity_uuid !== $values['target_uuid'])) {
+ throw new \InvalidArgumentException('The target UUID and entity passed to the entity reference item do not match.');
+ }
+ }
+ // Notify the parent if necessary.
+ if ($notify && $this->parent) {
+ $this->parent->onChange($this->getName());
+ }
+ }
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onChange($property_name, $notify = TRUE): void {
+ // Make sure that the target UUID and the target property stay in sync.
+ if ($property_name === 'entity') {
+ $property = $this->get('entity');
+ if ($target_uuid = $property->isTargetNew() ? NULL : $property->getValue()->uuid()) {
+ $this->writePropertyValue('target_uuid', $target_uuid);
+ }
+ }
+ elseif ($property_name === 'target_uuid') {
+ $property = $this->get('entity');
+ $entity_type = $property->getDataDefinition()->getConstraint('EntityType');
+ $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadByProperties(['uuid' => $this->get('target_uuid')->getValue()]);
+ if ($entity = array_shift($entities)) {
+ assert($entity instanceof EntityInterface);
+ $this->writePropertyValue('target_uuid', $entity->uuid());
+ $this->writePropertyValue('entity', $entity);
+ }
+ }
+ parent::onChange($property_name, $notify);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty() {
+ // Avoid loading the entity by first checking the 'target_uuid'.
+ if ($this->target_uuid !== NULL) {
+ return FALSE;
+ }
+ if ($this->entity && $this->entity instanceof EntityInterface) {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(): void {
+ if ($this->hasNewEntity()) {
+ // Save the entity if it has not already been saved by some other code.
+ if ($this->entity->isNew()) {
+ $this->entity->save();
+ }
+ // Make sure the parent knows we are updating this property so it can
+ // react properly.
+ $this->target_uuid = $this->entity->uuid();
+ }
+ if (!$this->isEmpty() && $this->target_uuid === NULL) {
+ $this->target_uuid = $this->entity->uuid();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function generateSampleValue(FieldDefinitionInterface $field_definition): array {
+ $manager = \Drupal::service('plugin.manager.entity_reference_selection');
+
+ // Instead of calling $manager->getSelectionHandler($field_definition)
+ // replicate the behavior to be able to override the sorting settings.
+ $options = [
+ 'target_type' => $field_definition->getFieldStorageDefinition()->getSetting('target_type'),
+ 'handler' => $field_definition->getSetting('handler'),
+ 'handler_settings' => $field_definition->getSetting('handler_settings') ?: [],
+ 'entity' => NULL,
+ ];
+
+ $entity_type = \Drupal::entityTypeManager()->getDefinition($options['target_type']);
+ $options['handler_settings']['sort'] = [
+ 'field' => $entity_type->getKey('uuid'),
+ 'direction' => 'DESC',
+ ];
+ $selection_handler = $manager->getInstance($options);
+
+ // Select a random number of references between the last 50 referenceable
+ // entities created.
+ if ($referenceable = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 50)) {
+ $group = array_rand($referenceable);
+ return ['target_uuid' => array_rand($referenceable[$group])];
+ }
+ return [];
+ }
+
+ /**
+ * Determines whether the item holds an unsaved entity.
+ *
+ * This is notably used for "autocreate" widgets, and more generally to
+ * support referencing freshly created entities (they will get saved
+ * automatically as the hosting entity gets saved).
+ *
+ * @return bool
+ * TRUE if the item holds an unsaved entity.
+ */
+ public function hasNewEntity() {
+ return !$this->isEmpty() && $this->target_uuid === NULL && $this->entity->isNew();
+ }
+
+}
diff --git a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
index 22f8f7f57d1b..7539670e1555 100644
--- a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
@@ -265,7 +265,7 @@ class FileUploadTest extends ResourceTestBase {
->set('field_rest_file_test', ['target_id' => $existing_file->id()])
->save();
- $uri = Url::fromUri('base:' . '/jsonapi/entity_test/entity_test/' . $this->entity->uuid() . '/field_rest_file_test');
+ $uri = Url::fromUri('base:/jsonapi/entity_test/entity_test/' . $this->entity->uuid() . '/field_rest_file_test');
// DX: 405 when read-only mode is enabled.
$response = $this->fileRequest($uri, $this->testFileData);
diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php
new file mode 100644
index 000000000000..110e1a6840b3
--- /dev/null
+++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php
@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\Core\Url;
+use Drupal\entity_test\EntityTestHelper;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use GuzzleHttp\RequestOptions;
+
+/**
+ * JSON:API resource tests.
+ *
+ * @group jsonapi
+ *
+ * @internal
+ */
+class JsonApiRelationshipTest extends JsonApiFunctionalTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'basic_auth',
+ 'entity_test',
+ 'jsonapi_test_field_type',
+ ];
+
+ /**
+ * The entity type ID.
+ */
+ protected string $entityTypeId = 'entity_test';
+
+ /**
+ * The entity bundle.
+ */
+ protected string $bundle = 'entity_test';
+
+ /**
+ * The field name.
+ */
+ protected string $fieldName = 'field_child';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ EntityTestHelper::createBundle($this->bundle, 'Parent', $this->entityTypeId);
+
+ FieldStorageConfig::create([
+ 'field_name' => $this->fieldName,
+ 'type' => 'jsonapi_test_field_type_entity_reference_uuid',
+ 'entity_type' => $this->entityTypeId,
+ 'cardinality' => 1,
+ 'settings' => [
+ 'target_type' => $this->entityTypeId,
+ ],
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => $this->fieldName,
+ 'entity_type' => $this->entityTypeId,
+ 'bundle' => $this->bundle,
+ 'label' => $this->randomString(),
+ 'settings' => [
+ 'handler' => 'default',
+ 'handler_settings' => [],
+ ],
+ ])->save();
+
+ \Drupal::service('router.builder')->rebuild();
+ }
+
+ /**
+ * Test relationships without target_id as main property.
+ *
+ * @see https://www.drupal.org/project/drupal/issues/3476224
+ */
+ public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void {
+ $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
+ $user = $this->drupalCreateUser([
+ 'administer entity_test content',
+ 'view test entity',
+ ]);
+
+ // Create parent and child entities.
+ $storage = $this->container->get('entity_type.manager')
+ ->getStorage($this->entityTypeId);
+ $parentEntity = $storage
+ ->create([
+ 'type' => $this->bundle,
+ ]);
+ $parentEntity->save();
+ $childUuid = $this->container->get('uuid')->generate();
+ $childEntity = $storage
+ ->create([
+ 'type' => $this->bundle,
+ 'uuid' => $childUuid,
+ ]);
+ $childEntity->save();
+ $uuid = $childEntity->uuid();
+ $this->assertEquals($childUuid, $uuid);
+
+ // 1. Successful PATCH to the related endpoint.
+ $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldName));
+ $request_options = [
+ RequestOptions::HEADERS => [
+ 'Content-Type' => 'application/vnd.api+json',
+ 'Accept' => 'application/vnd.api+json',
+ ],
+ RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw],
+ RequestOptions::JSON => [
+ 'data' => [
+ 'id' => $childUuid,
+ 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
+ ],
+ ],
+ ];
+ $response = $this->request('PATCH', $url, $request_options);
+ $this->assertSame(204, $response->getStatusCode(), (string) $response->getBody());
+ $parentEntity = $storage->loadUnchanged($parentEntity->id());
+ $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid);
+
+ // Reset the relationship.
+ $parentEntity->set($this->fieldName, NULL)
+ ->save();
+ $parentEntity = $storage->loadUnchanged($parentEntity->id());
+ $this->assertTrue($parentEntity->get($this->fieldName)->isEmpty());
+
+ // 2. Successful PATCH to individual endpoint.
+ $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid()));
+ $request_options[RequestOptions::JSON] = [
+ 'data' => [
+ 'id' => $parentEntity->uuid(),
+ 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
+ 'relationships' => [
+ $this->fieldName => [
+ 'data' => [
+ [
+ 'id' => $childUuid,
+ 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle),
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ $response = $this->request('PATCH', $url, $request_options);
+ $this->assertSame(200, $response->getStatusCode(), (string) $response->getBody());
+ $parentEntity = $storage->loadUnchanged($parentEntity->id());
+ $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid);
+ }
+
+}
diff --git a/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php b/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php
index 97eec557d22d..48cbc20067a9 100644
--- a/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/WorkspaceTest.php
@@ -15,6 +15,7 @@ use Drupal\workspaces\Entity\Workspace;
* JSON:API integration test for the "Workspace" content entity type.
*
* @group jsonapi
+ * @group workspaces
*/
class WorkspaceTest extends ResourceTestBase {
@@ -142,7 +143,7 @@ class WorkspaceTest extends ResourceTestBase {
'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'label' => 'Campaign',
'drupal_internal__id' => 'campaign',
- 'drupal_internal__revision_id' => 2,
+ 'drupal_internal__revision_id' => 1,
],
'relationships' => [
'parent' => [