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 */
|