annotate core/modules/jsonapi/jsonapi.api.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 /**
Chris@18 4 * @file
Chris@18 5 * Documentation related to JSON:API.
Chris@18 6 */
Chris@18 7
Chris@18 8 use Drupal\Core\Access\AccessResult;
Chris@18 9
Chris@18 10 /**
Chris@18 11 * @defgroup jsonapi_architecture JSON:API Architecture
Chris@18 12 * @{
Chris@18 13 *
Chris@18 14 * @section overview Overview
Chris@18 15 * The JSON:API module is a Drupal-centric implementation of the JSON:API
Chris@18 16 * specification. By its own definition, the JSON:API specification "is a
Chris@18 17 * specification for how a client should request that resources be fetched or
Chris@18 18 * modified, and how a server should respond to those requests. [It] is designed
Chris@18 19 * to minimize both the number of requests and the amount of data transmitted
Chris@18 20 * between clients and servers. This efficiency is achieved without compromising
Chris@18 21 * readability, flexibility, or discoverability."
Chris@18 22 *
Chris@18 23 * While "Drupal-centric", the JSON:API module is committed to strict compliance
Chris@18 24 * with the specification. Wherever possible, the module attempts to implement
Chris@18 25 * the specification in a way which is compatible and familiar with the patterns
Chris@18 26 * and concepts inherent to Drupal. However, when "Drupalisms" cannot be
Chris@18 27 * reconciled with the specification, the module will always choose the
Chris@18 28 * implementation most faithful to the specification.
Chris@18 29 *
Chris@18 30 * @see http://jsonapi.org/
Chris@18 31 *
Chris@18 32 * @section resources Resources
Chris@18 33 * Every unit of data in the specification is a "resource". The specification
Chris@18 34 * defines how a client should interact with a server to fetch and manipulate
Chris@18 35 * these resources.
Chris@18 36 *
Chris@18 37 * The JSON:API module maps every entity type + bundle to a resource type.
Chris@18 38 * Since the specification does not have a concept of resource type inheritance
Chris@18 39 * or composition, the JSON:API module implements different bundles of the same
Chris@18 40 * entity type as *distinct* resource types.
Chris@18 41 *
Chris@18 42 * While it is theoretically possible to expose arbitrary data as resources, the
Chris@18 43 * JSON:API module only exposes resources from (config and content) entities.
Chris@18 44 * This eliminates the need for another abstraction layer in order implement
Chris@18 45 * certain features of the specification.
Chris@18 46 *
Chris@18 47 * @section relationships Relationships
Chris@18 48 * The specification defines semantics for the "relationships" between
Chris@18 49 * resources. Since the JSON:API module defines every entity type + bundle as a
Chris@18 50 * resource type and does not allow non-entity resources, it is able to use
Chris@18 51 * entity references to automatically define and represent the relationships
Chris@18 52 * between all resources.
Chris@18 53 *
Chris@18 54 * @section revisions Resource versioning
Chris@18 55 * The JSON:API module exposes entity revisions in a manner inspired by RFC5829:
Chris@18 56 * Link Relation Types for Simple Version Navigation between Web Resources.
Chris@18 57 *
Chris@18 58 * Revision support is not an official part of the JSON:API specification.
Chris@18 59 * However, a number of "profiles" are being developed (also not officially part
Chris@18 60 * in the spec, but already committed to JSON:API v1.1) to standardize any
Chris@18 61 * custom behaviors that the JSON:API module has developed (all of which are
Chris@18 62 * still specification-compliant).
Chris@18 63 *
Chris@18 64 * @see https://github.com/json-api/json-api/pull/1268
Chris@18 65 * @see https://github.com/json-api/json-api/pull/1311
Chris@18 66 * @see https://www.drupal.org/project/jsonapi/issues/2955020
Chris@18 67 *
Chris@18 68 * By implementing revision support as a profile, the JSON:API module should be
Chris@18 69 * maximally compatible with other systems.
Chris@18 70 *
Chris@18 71 * A "version" in the JSON:API module is any revision that was previously, or is
Chris@18 72 * currently, a default revision. Not all revisions are considered to be a
Chris@18 73 * "version". Revisions that are not marked as a "default" revision are
Chris@18 74 * considered "working copies" since they are not usually publicly available
Chris@18 75 * and are the revisions to which most new work is applied.
Chris@18 76 *
Chris@18 77 * When the Content Moderation module is installed, it is possible that the
Chris@18 78 * most recent default revision is *not* the latest revision.
Chris@18 79 *
Chris@18 80 * Requesting a resource version is done via a URL query parameter. It has the
Chris@18 81 * following form:
Chris@18 82 *
Chris@18 83 * @code
Chris@18 84 * version-identifier
Chris@18 85 * __|__
Chris@18 86 * / \
Chris@18 87 * ?resource_version=foo:bar
Chris@18 88 * \_/ \_/
Chris@18 89 * | |
Chris@18 90 * version-negotiator |
Chris@18 91 * version-argument
Chris@18 92 * @endcode
Chris@18 93 *
Chris@18 94 * A version identifier is a string with enough information to load a
Chris@18 95 * particular revision. The version negotiator component names the negotiation
Chris@18 96 * mechanism for loading a revision. Currently, this can be either `id` or
Chris@18 97 * `rel`. The `id` negotiator takes a version argument which is the desired
Chris@18 98 * revision ID. The `rel` negotiator takes a version argument which is either
Chris@18 99 * the string `latest-version` or the string `working-copy`.
Chris@18 100 *
Chris@18 101 * In the future, other negotiatiors may be developed, such as negotiatiors that
Chris@18 102 * are UUID-, timestamp-, or workspace-based.
Chris@18 103 *
Chris@18 104 * To illustrate how a particular entity revision is requested, imagine a node
Chris@18 105 * that has a "Published" revision and a subsequent "Draft" revision.
Chris@18 106 *
Chris@18 107 * Using JSON:API, one could request the "Published" node by requesting
Chris@18 108 * `/jsonapi/node/page/{{uuid}}?resource_version=rel:latest-version`.
Chris@18 109 *
Chris@18 110 * To preview an entity that is still a work-in-progress (i.e. the "Draft"
Chris@18 111 * revision) one could request
Chris@18 112 * `/jsonapi/node/page/{{uuid}}?resource_version=rel:working-copy`.
Chris@18 113 *
Chris@18 114 * To request a specific revision ID, one can request
Chris@18 115 * `/jsonapi/node/page/{{uuid}}?resource_version=id:{{revision_id}}`.
Chris@18 116 *
Chris@18 117 * It is not yet possible to request a collection of revisions. This is still
Chris@18 118 * under development in issue [#3009588].
Chris@18 119 *
Chris@18 120 * @see https://www.drupal.org/project/jsonapi/issues/3009588.
Chris@18 121 * @see https://tools.ietf.org/html/rfc5829
Chris@18 122 * @see https://www.drupal.org/docs/8/modules/jsonapi/revisions
Chris@18 123 *
Chris@18 124 * @section translations Resource translations
Chris@18 125 *
Chris@18 126 * Some multilingual features currently do not work well with JSON:API. See
Chris@18 127 * JSON:API modules's multilingual support documentation online for more
Chris@18 128 * information on the current status of multilingual support.
Chris@18 129 *
Chris@18 130 * @see https://www.drupal.org/docs/8/modules/jsonapi/translations
Chris@18 131 *
Chris@18 132 * @section api API
Chris@18 133 * The JSON:API module provides an HTTP API that adheres to the JSON:API
Chris@18 134 * specification.
Chris@18 135 *
Chris@18 136 * The JSON:API module provides *no PHP API to modify its behavior.* It is
Chris@18 137 * designed to have zero configuration.
Chris@18 138 *
Chris@18 139 * - Adding new resources/resource types is unsupported: all entities/entity
Chris@18 140 * types are exposed automatically. If you want to expose more data via the
Chris@18 141 * JSON:API module, the data must be defined as entity. See the "Resources"
Chris@18 142 * section.
Chris@18 143 * - Custom field type normalization is not supported because the JSON:API
Chris@18 144 * specification requires specific representations for resources (entities),
Chris@18 145 * attributes on resources (non-entity reference fields) and relationships
Chris@18 146 * between those resources (entity reference fields). A field contains
Chris@18 147 * properties, and properties are of a certain data type. All non-internal
Chris@18 148 * properties on a field are normalized.
Chris@18 149 * - The same data type normalizers as those used by core's Serialization and
Chris@18 150 * REST modules are also used by the JSON:API module.
Chris@18 151 * - All available authentication mechanisms are allowed.
Chris@18 152 *
Chris@18 153 * @section tests Test Coverage
Chris@18 154 * The JSON:API module comes with extensive unit and kernel tests. But most
Chris@18 155 * importantly for end users, it also has comprehensive integration tests. These
Chris@18 156 * integration tests are designed to:
Chris@18 157 *
Chris@18 158 * - ensure a great DX (Developer Experience)
Chris@18 159 * - detect regressions and normalization changes before shipping a release
Chris@18 160 * - guarantee 100% of Drupal core's entity types work as expected
Chris@18 161 *
Chris@18 162 * The integration tests test the same common cases and edge cases using
Chris@18 163 * \Drupal\Tests\jsonapi\Functional\ResourceTestBase, which is a base class
Chris@18 164 * subclassed for every entity type that Drupal core ships with. It is ensured
Chris@18 165 * that 100% of Drupal core's entity types are tested thanks to
Chris@18 166 * \Drupal\Tests\jsonapi\Functional\TestCoverageTest.
Chris@18 167 *
Chris@18 168 * Custom entity type developers can get the same assurances by subclassing it
Chris@18 169 * for their entity types.
Chris@18 170 *
Chris@18 171 * @section bc Backwards Compatibility
Chris@18 172 * PHP API: there is no PHP API except for three security-related hooks. This
Chris@18 173 * means that this module's implementation details are entirely free to
Chris@18 174 * change at any time.
Chris@18 175 *
Chris@18 176 * Note that *normalizers are internal implementation details.* While
Chris@18 177 * normalizers are services, they are *not* to be used directly. This is due to
Chris@18 178 * the design of the Symfony Serialization component, not because the JSON:API
Chris@18 179 * module wanted to publicly expose services.
Chris@18 180 *
Chris@18 181 * HTTP API: URLs and JSON response structures are considered part of this
Chris@18 182 * module's public API. However, inconsistencies with the JSON:API specification
Chris@18 183 * will be considered bugs. Fixes which bring the module into compliance with
Chris@18 184 * the specification are *not* guaranteed to be backwards-compatible. When
Chris@18 185 * compliance bugs are found, clients are expected to be made compatible with
Chris@18 186 * both the pre-fix and post-fix representations.
Chris@18 187 *
Chris@18 188 * What this means for developing consumers of the HTTP API is that *clients
Chris@18 189 * should be implemented from the specification first and foremost.* This should
Chris@18 190 * mitigate implicit dependencies on implementation details or inconsistencies
Chris@18 191 * with the specification that are specific to this module.
Chris@18 192 *
Chris@18 193 * To help develop compatible clients, every response indicates the version of
Chris@18 194 * the JSON:API specification used under its "jsonapi" key. Future releases
Chris@18 195 * *may* increment the minor version number if the module implements features of
Chris@18 196 * a later specification. Remember that the specification stipulates that future
Chris@18 197 * versions *will* remain backwards-compatible as only additions may be
Chris@18 198 * released.
Chris@18 199 *
Chris@18 200 * @see http://jsonapi.org/faq/#what-is-the-meaning-of-json-apis-version
Chris@18 201 *
Chris@18 202 * Tests: subclasses of base test classes may contain BC breaks between minor
Chris@18 203 * releases, to allow minor releases to A) comply better with the JSON:API spec,
Chris@18 204 * B) guarantee that all resource types (and therefore entity types) function as
Chris@18 205 * expected, C) update to future versions of the JSON:API spec.
Chris@18 206 *
Chris@18 207 * @}
Chris@18 208 */
Chris@18 209
Chris@18 210 /**
Chris@18 211 * @addtogroup hooks
Chris@18 212 * @{
Chris@18 213 */
Chris@18 214
Chris@18 215 /**
Chris@18 216 * Controls access when filtering by entity data via JSON:API.
Chris@18 217 *
Chris@18 218 * This module supports filtering by resource object attributes referenced by
Chris@18 219 * relationship fields. For example, a site may add a "Favorite Animal" field
Chris@18 220 * to user entities, which would permit the following filtered query:
Chris@18 221 * @code
Chris@18 222 * /jsonapi/node/article?filter[uid.field_favorite_animal]=llama
Chris@18 223 * @endcode
Chris@18 224 * This query would return articles authored by users whose favorite animal is a
Chris@18 225 * llama. However, the information about a user's favorite animal should not be
Chris@18 226 * available to users without the "access user profiles" permission. The same
Chris@18 227 * must hold true even if that user is referenced as an article's author.
Chris@18 228 * Therefore, access to filter by this data must be restricted so that access
Chris@18 229 * cannot be bypassed via a JSON:API filtered query.
Chris@18 230 *
Chris@18 231 * As a rule, clients should only be able to filter by data that they can
Chris@18 232 * view.
Chris@18 233 *
Chris@18 234 * Conventionally, `$entity->access('view')` is how entity access is checked.
Chris@18 235 * This call invokes the corresponding hooks. However, these access checks
Chris@18 236 * require an `$entity` object. This means that they cannot be called prior to
Chris@18 237 * executing a database query.
Chris@18 238 *
Chris@18 239 * In order to safely enable filtering across a relationship, modules
Chris@18 240 * responsible for entity access must do two things:
Chris@18 241 * - Implement this hook (or hook_jsonapi_ENTITY_TYPE_filter_access()) and
Chris@18 242 * return an array of AccessResults keyed by the named entity subsets below.
Chris@18 243 * - If the AccessResult::allowed() returned by the above hook does not provide
Chris@18 244 * enough granularity (for example, if access depends on a bundle field value
Chris@18 245 * of the entity being queried), then hook_query_TAG_alter() must be
Chris@18 246 * implemented using the 'entity_access' or 'ENTITY_TYPE_access' query tag.
Chris@18 247 * See node_query_node_access_alter() for an example.
Chris@18 248 *
Chris@18 249 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
Chris@18 250 * The entity type of the entity to be filtered upon.
Chris@18 251 * @param \Drupal\Core\Session\AccountInterface $account
Chris@18 252 * The account for which to check access.
Chris@18 253 *
Chris@18 254 * @return \Drupal\Core\Access\AccessResultInterface[]
Chris@18 255 * An array keyed by a constant which identifies a subset of entities. For
Chris@18 256 * each subset, the value is one of the following access results:
Chris@18 257 * - AccessResult::allowed() if all entities within the subset (potentially
Chris@18 258 * narrowed by hook_query_TAG_alter() implementations) are viewable.
Chris@18 259 * - AccessResult::forbidden() if any entity within the subset is not
Chris@18 260 * viewable.
Chris@18 261 * - AccessResult::neutral() if the implementation has no opinion.
Chris@18 262 * The supported subsets for which an access result may be returned are:
Chris@18 263 * - JSONAPI_FILTER_AMONG_ALL: all entities of the given type.
Chris@18 264 * - JSONAPI_FILTER_AMONG_PUBLISHED: all published entities of the given type.
Chris@18 265 * - JSONAPI_FILTER_AMONG_ENABLED: all enabled entities of the given type.
Chris@18 266 * - JSONAPI_FILTER_AMONG_OWN: all entities of the given type owned by the
Chris@18 267 * user for whom access is being checked.
Chris@18 268 * See the documentation of the above constants for more information about
Chris@18 269 * each subset.
Chris@18 270 *
Chris@18 271 * @see hook_jsonapi_ENTITY_TYPE_filter_access()
Chris@18 272 */
Chris@18 273 function hook_jsonapi_entity_filter_access(\Drupal\Core\Entity\EntityTypeInterface $entity_type, \Drupal\Core\Session\AccountInterface $account) {
Chris@18 274 // For every entity type that has an admin permission, allow access to filter
Chris@18 275 // by all entities of that type to users with that permission.
Chris@18 276 if ($admin_permission = $entity_type->getAdminPermission()) {
Chris@18 277 return ([
Chris@18 278 JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
Chris@18 279 ]);
Chris@18 280 }
Chris@18 281 }
Chris@18 282
Chris@18 283 /**
Chris@18 284 * Controls access to filtering by entity data via JSON:API.
Chris@18 285 *
Chris@18 286 * This is the entity-type-specific variant of
Chris@18 287 * hook_jsonapi_entity_filter_access(). For implementations with logic that is
Chris@18 288 * specific to a single entity type, it is recommended to implement this hook
Chris@18 289 * rather than the generic hook_jsonapi_entity_filter_access() hook, which is
Chris@18 290 * called for every entity type.
Chris@18 291 *
Chris@18 292 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
Chris@18 293 * The entity type of the entities to be filtered upon.
Chris@18 294 * @param \Drupal\Core\Session\AccountInterface $account
Chris@18 295 * The account for which to check access.
Chris@18 296 *
Chris@18 297 * @return \Drupal\Core\Access\AccessResultInterface[]
Chris@18 298 * The array of access results, keyed by subset. See
Chris@18 299 * hook_jsonapi_entity_filter_access() for details.
Chris@18 300 *
Chris@18 301 * @see hook_jsonapi_entity_filter_access()
Chris@18 302 */
Chris@18 303 function hook_jsonapi_ENTITY_TYPE_filter_access(\Drupal\Core\Entity\EntityTypeInterface $entity_type, \Drupal\Core\Session\AccountInterface $account) {
Chris@18 304 return ([
Chris@18 305 JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'),
Chris@18 306 JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'),
Chris@18 307 JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermissions($account, ['view own published llamas', 'view own unpublished llamas'], 'AND'),
Chris@18 308 ]);
Chris@18 309 }
Chris@18 310
Chris@18 311 /**
Chris@18 312 * Restricts filtering access to the given field.
Chris@18 313 *
Chris@18 314 * Some fields may contain sensitive information. In these cases, modules are
Chris@18 315 * supposed to implement hook_entity_field_access(). However, this hook receives
Chris@18 316 * an optional `$items` argument and often must return AccessResult::neutral()
Chris@18 317 * when `$items === NULL`. This is because access may or may not be allowed
Chris@18 318 * based on the field items or based on the entity on which the field is
Chris@18 319 * attached (if the user is the entity owner, for example).
Chris@18 320 *
Chris@18 321 * Since JSON:API must check field access prior to having a field item list
Chris@18 322 * instance available (access must be checked before a database query is made),
Chris@18 323 * it is not sufficiently secure to check field 'view' access alone.
Chris@18 324 *
Chris@18 325 * This hook exists so that modules which cannot return
Chris@18 326 * AccessResult::forbidden() from hook_entity_field_access() can still secure
Chris@18 327 * JSON:API requests where necessary.
Chris@18 328 *
Chris@18 329 * If a corresponding implementation of hook_entity_field_access() *can* be
Chris@18 330 * forbidden for one or more values of the `$items` argument, this hook *MUST*
Chris@18 331 * return AccessResult::forbidden().
Chris@18 332 *
Chris@18 333 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
Chris@18 334 * The field definition of the field to be filtered upon.
Chris@18 335 * @param \Drupal\Core\Session\AccountInterface $account
Chris@18 336 * The account for which to check access.
Chris@18 337 *
Chris@18 338 * @return \Drupal\Core\Access\AccessResultInterface
Chris@18 339 * The access result.
Chris@18 340 */
Chris@18 341 function hook_jsonapi_entity_field_filter_access(\Drupal\Core\Field\FieldDefinitionInterface $field_definition, \Drupal\Core\Session\AccountInterface $account) {
Chris@18 342 if ($field_definition->getTargetEntityTypeId() === 'node' && $field_definition->getName() === 'field_sensitive_data') {
Chris@18 343 $has_sufficient_access = FALSE;
Chris@18 344 foreach (['administer nodes', 'view all sensitive field data'] as $permission) {
Chris@18 345 $has_sufficient_access = $has_sufficient_access ?: $account->hasPermission($permission);
Chris@18 346 }
Chris@18 347 return AccessResult::forbiddenIf(!$has_sufficient_access)->cachePerPermissions();
Chris@18 348 }
Chris@18 349 return AccessResult::neutral();
Chris@18 350 }
Chris@18 351
Chris@18 352 /**
Chris@18 353 * @} End of "addtogroup hooks".
Chris@18 354 */