jsonapi.module 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. /**
  3. * @file
  4. * Module implementation file.
  5. */
  6. use Drupal\Core\Access\AccessResult;
  7. use Drupal\Core\Routing\RouteMatchInterface;
  8. use Drupal\Core\Entity\EntityInterface;
  9. use Drupal\Core\Entity\EntityTypeInterface;
  10. use Drupal\Core\Session\AccountInterface;
  11. use Drupal\jsonapi\Routing\Routes as JsonApiRoutes;
  12. /**
  13. * Array key for denoting type-based filtering access.
  14. *
  15. * Array key for denoting access to filter among all entities of a given type,
  16. * regardless of whether they are published or enabled, and regardless of
  17. * their owner.
  18. *
  19. * @see hook_jsonapi_entity_filter_access()
  20. * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  21. */
  22. const JSONAPI_FILTER_AMONG_ALL = 'filter_among_all';
  23. /**
  24. * Array key for denoting type-based published-only filtering access.
  25. *
  26. * Array key for denoting access to filter among all published entities of a
  27. * given type, regardless of their owner.
  28. *
  29. * This is used when an entity type has a "published" entity key and there's a
  30. * query condition for the value of that equaling 1.
  31. *
  32. * @see hook_jsonapi_entity_filter_access()
  33. * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  34. */
  35. const JSONAPI_FILTER_AMONG_PUBLISHED = 'filter_among_published';
  36. /**
  37. * Array key for denoting type-based enabled-only filtering access.
  38. *
  39. * Array key for denoting access to filter among all enabled entities of a
  40. * given type, regardless of their owner.
  41. *
  42. * This is used when an entity type has a "status" entity key and there's a
  43. * query condition for the value of that equaling 1.
  44. *
  45. * For the User entity type, which does not have a "status" entity key, the
  46. * "status" field is used.
  47. *
  48. * @see hook_jsonapi_entity_filter_access()
  49. * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  50. */
  51. const JSONAPI_FILTER_AMONG_ENABLED = 'filter_among_enabled';
  52. /**
  53. * Array key for denoting type-based owned-only filtering access.
  54. *
  55. * Array key for denoting access to filter among all entities of a given type,
  56. * regardless of whether they are published or enabled, so long as they are
  57. * owned by the user for whom access is being checked.
  58. *
  59. * When filtering among User entities, this is used when access is being
  60. * checked for an authenticated user and there's a query condition
  61. * limiting the result set to just that user's entity object.
  62. *
  63. * When filtering among entities of another type, this is used when all of the
  64. * following conditions are met:
  65. * - Access is being checked for an authenticated user.
  66. * - The entity type has an "owner" entity key.
  67. * - There's a filter/query condition for the value equal to the user's ID.
  68. *
  69. * @see hook_jsonapi_entity_filter_access()
  70. * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  71. */
  72. const JSONAPI_FILTER_AMONG_OWN = 'filter_among_own';
  73. /**
  74. * Implements hook_help().
  75. */
  76. function jsonapi_help($route_name, RouteMatchInterface $route_match) {
  77. switch ($route_name) {
  78. case 'help.page.jsonapi':
  79. $output = '<h3>' . t('About') . '</h3>';
  80. $output .= '<p>' . t('The JSON:API module is a fully compliant implementation of the <a href=":spec">JSON:API Specification</a>. By following shared conventions, you can increase productivity, take advantage of generalized tooling, and focus on what matters: your application. Clients built around JSON:API are able to take advantage of features like efficient response caching, which can sometimes eliminate network requests entirely. For more information, see the <a href=":docs">online documentation for the JSON:API module</a>.', [
  81. ':spec' => 'https://jsonapi.org',
  82. ':docs' => 'https://www.drupal.org/docs/8/modules/json-api',
  83. ]) . '</p>';
  84. $output .= '<dl>';
  85. $output .= '<dt>' . t('General') . '</dt>';
  86. $output .= '<dd>' . t('JSON:API is a particular implementation of REST that provides conventions for resource relationships, collections, filters, pagination, and sorting. These conventions help developers build clients faster and encourages reuse of code.') . '</dd>';
  87. $output .= '<dd>' . t('The <a href=":jsonapi-docs">JSON:API</a> and <a href=":rest-docs">RESTful Web Services</a> modules serve similar purposes. <a href=":comparison">Read the comparison of the RESTFul Web Services and JSON:API modules</a> to determine the best choice for your site.', [
  88. ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/json-api',
  89. ':rest-docs' => 'https://www.drupal.org/docs/8/core/modules/rest',
  90. ':comparison' => 'https://www.drupal.org/docs/8/modules/jsonapi/jsonapi-vs-cores-rest-module',
  91. ]) . '</dd>';
  92. $output .= '<dd>' . 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.', [
  93. ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations',
  94. ]) . '</dd>';
  95. $output .= '<dd>' . 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.', [
  96. ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/revisions',
  97. ]) . '</dd>';
  98. $output .= '</dl>';
  99. return $output;
  100. }
  101. return NULL;
  102. }
  103. /**
  104. * Implements hook_modules_installed().
  105. */
  106. function jsonapi_modules_installed($modules) {
  107. $potential_conflicts = [
  108. 'content_translation',
  109. 'config_translation',
  110. 'language',
  111. ];
  112. if (!empty(array_intersect($modules, $potential_conflicts))) {
  113. \Drupal::messenger()->addWarning(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.', [
  114. ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations',
  115. ]));
  116. }
  117. }
  118. /**
  119. * Implements hook_entity_bundle_create().
  120. */
  121. function jsonapi_entity_bundle_create() {
  122. JsonApiRoutes::rebuild();
  123. }
  124. /**
  125. * Implements hook_entity_bundle_delete().
  126. */
  127. function jsonapi_entity_bundle_delete() {
  128. JsonApiRoutes::rebuild();
  129. }
  130. /**
  131. * Implements hook_entity_create().
  132. */
  133. function jsonapi_entity_create(EntityInterface $entity) {
  134. if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
  135. // @todo: only do this when relationship fields are updated, not just any field.
  136. JsonApiRoutes::rebuild();
  137. }
  138. }
  139. /**
  140. * Implements hook_entity_delete().
  141. */
  142. function jsonapi_entity_delete(EntityInterface $entity) {
  143. if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
  144. // @todo: only do this when relationship fields are updated, not just any field.
  145. JsonApiRoutes::rebuild();
  146. }
  147. }
  148. /**
  149. * Implements hook_jsonapi_entity_filter_access().
  150. */
  151. function jsonapi_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  152. // All core entity types and most or all contrib entity types allow users
  153. // with the entity type's administrative permission to view all of the
  154. // entities, so enable similarly permissive filtering to those users as well.
  155. // A contrib module may override this decision by returning
  156. // AccessResult::forbidden() from its implementation of this hook.
  157. if ($admin_permission = $entity_type->getAdminPermission()) {
  158. return ([
  159. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
  160. ]);
  161. }
  162. }
  163. /**
  164. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'aggregator_feed'.
  165. */
  166. function jsonapi_jsonapi_aggregator_feed_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  167. // @see \Drupal\aggregator\FeedAccessControlHandler::checkAccess()
  168. return ([
  169. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access news feeds'),
  170. ]);
  171. }
  172. /**
  173. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'block_content'.
  174. */
  175. function jsonapi_jsonapi_block_content_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  176. // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
  177. // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
  178. // (isReusable()), so this does not have to.
  179. return ([
  180. JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed(),
  181. ]);
  182. }
  183. /**
  184. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'comment'.
  185. */
  186. function jsonapi_jsonapi_comment_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  187. // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
  188. // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
  189. // (access to the commented entity), so this does not have to.
  190. return ([
  191. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'),
  192. JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'),
  193. ]);
  194. }
  195. /**
  196. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'entity_test'.
  197. */
  198. function jsonapi_jsonapi_entity_test_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  199. // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
  200. return ([
  201. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'),
  202. ]);
  203. }
  204. /**
  205. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'file'.
  206. */
  207. function jsonapi_jsonapi_file_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  208. // @see \Drupal\file\FileAccessControlHandler::checkAccess()
  209. // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
  210. // (public OR owner), so this does not have to.
  211. return ([
  212. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'),
  213. ]);
  214. }
  215. /**
  216. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'media'.
  217. */
  218. function jsonapi_jsonapi_media_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  219. // @see \Drupal\media\MediaAccessControlHandler::checkAccess()
  220. return ([
  221. JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'),
  222. ]);
  223. }
  224. /**
  225. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'node'.
  226. */
  227. function jsonapi_jsonapi_node_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  228. // @see \Drupal\node\NodeAccessControlHandler::access()
  229. if ($account->hasPermission('bypass node access')) {
  230. return ([
  231. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowed()->cachePerPermissions(),
  232. ]);
  233. }
  234. if (!$account->hasPermission('access content')) {
  235. $forbidden = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
  236. return ([
  237. JSONAPI_FILTER_AMONG_ALL => $forbidden,
  238. JSONAPI_FILTER_AMONG_OWN => $forbidden,
  239. JSONAPI_FILTER_AMONG_PUBLISHED => $forbidden,
  240. // For legacy reasons, the Node entity type has a "status" key, so forbid
  241. // this subset as well, even though it has no semantic meaning.
  242. JSONAPI_FILTER_AMONG_ENABLED => $forbidden,
  243. ]);
  244. }
  245. return ([
  246. // @see \Drupal\node\NodeAccessControlHandler::checkAccess()
  247. JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'),
  248. // @see \Drupal\node\NodeGrantDatabaseStorage::access()
  249. // Note that:
  250. // - This is just for the default grant. Other node access conditions are
  251. // added via the 'node_access' query tag.
  252. // - Permissions were checked earlier in this function, so we must vary the
  253. // cache by them.
  254. JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(),
  255. ]);
  256. }
  257. /**
  258. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'shortcut'.
  259. */
  260. function jsonapi_jsonapi_shortcut_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  261. // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
  262. // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
  263. // (shortcut_set = shortcut_current_displayed_set()), so this does not have
  264. // to.
  265. return ([
  266. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')
  267. ->orIf(AccessResult::allowedIfHasPermissions($account, ['access shortcuts', 'customize shortcut links'])),
  268. ]);
  269. }
  270. /**
  271. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'taxonomy_term'.
  272. */
  273. function jsonapi_jsonapi_taxonomy_term_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  274. // @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess()
  275. return ([
  276. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'),
  277. JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'),
  278. ]);
  279. }
  280. /**
  281. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'user'.
  282. */
  283. function jsonapi_jsonapi_user_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
  284. // @see \Drupal\user\UserAccessControlHandler::checkAccess()
  285. // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
  286. // (!isAnonymous()), so this does not have to.
  287. return ([
  288. JSONAPI_FILTER_AMONG_OWN => AccessResult::allowed(),
  289. JSONAPI_FILTER_AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'),
  290. ]);
  291. }
  292. /**
  293. * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'workspace'.
  294. */
  295. function jsonapi_jsonapi_workspace_filter_access(EntityTypeInterface $entity_type, $published, $owner, AccountInterface $account) {
  296. // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
  297. return ([
  298. JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
  299. JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),
  300. ]);
  301. }