PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / DataSource.php

DataSource.php in WPGraphQL trunk, at src/Data/DataSource.php

984 lines 34.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Data;
4
5 use GraphQL\Error\UserError;
6 use GraphQL\Type\Definition\ResolveInfo;
7 use GraphQLRelay\Relay;
8 use WPGraphQL\AppContext;
9 use WPGraphQL\Data\Connection\CommentConnectionResolver;
10 use WPGraphQL\Data\Connection\PluginConnectionResolver;
11 use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
12 use WPGraphQL\Data\Connection\TermObjectConnectionResolver;
13 use WPGraphQL\Data\Connection\ThemeConnectionResolver;
14 use WPGraphQL\Data\Connection\UserConnectionResolver;
15 use WPGraphQL\Data\Connection\UserRoleConnectionResolver;
16 use WPGraphQL\Model\Avatar;
17 use WPGraphQL\Model\Comment;
18 use WPGraphQL\Model\CommentAuthor;
19 use WPGraphQL\Model\Menu;
20 use WPGraphQL\Model\Plugin;
21 use WPGraphQL\Model\Post;
22 use WPGraphQL\Model\PostType;
23 use WPGraphQL\Model\SettingGroup as SettingGroupModel;
24 use WPGraphQL\Model\Taxonomy;
25 use WPGraphQL\Model\Term;
26 use WPGraphQL\Model\Theme;
27 use WPGraphQL\Model\User;
28 use WPGraphQL\Model\UserRole;
29 use WPGraphQL\Registry\TypeRegistry;
30 use WPGraphQL\Type\ObjectType\SettingGroup;
31 use WPGraphQL\Utils\Utils;
32
33 /**
34 * Class DataSource
35 *
36 * This class serves as a factory for all the resolvers for queries and mutations. This layer of
37 * abstraction over the actual resolve functions allows easier, granular control over versioning as
38 * we can change big things behind the scenes if/when needed, and we just need to ensure the
39 * call to the DataSource method returns the expected data later on. This should make it easy
40 * down the road to version resolvers if/when changes to the WordPress API are rolled out.
41 *
42 * @package WPGraphQL\Data
43 * @since 0.0.4
44 */
45 class DataSource {
46
47 /**
48 * Stores an array of node definitions
49 *
50 * @var mixed[] $node_definition
51 * @since 0.0.4
52 */
53 protected static $node_definition;
54
55
56 /**
57 * Retrieves a WP_Comment object for the ID that gets passed
58 *
59 * @param int $comment_id The ID of the comment the comment author is associated with.
60 *
61 * @return \WPGraphQL\Model\CommentAuthor|null
62 * @throws \Exception Throws Exception.
63 */
64 public static function resolve_comment_author( int $comment_id ) {
65 $comment_author = get_comment( $comment_id );
66
67 return ! empty( $comment_author ) ? new CommentAuthor( $comment_author ) : null;
68 }
69
70 /**
71 * Wrapper for the CommentsConnectionResolver class
72 *
73 * @param mixed $source The object the connection is coming from
74 * @param array<string,mixed> $args Query args to pass to the connection resolver
75 * @param \WPGraphQL\AppContext $context The context of the query to pass along
76 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
77 *
78 * @return \GraphQL\Deferred
79 * @throws \Exception
80 * @since 0.0.5
81 */
82 public static function resolve_comments_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
83 $resolver = new CommentConnectionResolver( $source, $args, $context, $info );
84
85 return $resolver->get_connection();
86 }
87
88 /**
89 * Wrapper for PluginsConnectionResolver::resolve
90 *
91 * @param mixed $source The object the connection is coming from
92 * @param array<string,mixed> $args Array of arguments to pass to resolve method
93 * @param \WPGraphQL\AppContext $context AppContext object passed down
94 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
95 *
96 * @return \GraphQL\Deferred
97 * @throws \Exception
98 * @since 0.0.5
99 */
100 public static function resolve_plugins_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
101 $resolver = new PluginConnectionResolver( $source, $args, $context, $info );
102 return $resolver->get_connection();
103 }
104
105 /**
106 * Wrapper for PostObjectsConnectionResolver
107 *
108 * @param mixed $source The object the connection is coming from
109 * @param array<string,mixed> $args Arguments to pass to the resolve method
110 * @param \WPGraphQL\AppContext $context AppContext object to pass down
111 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
112 * @param mixed|string|string[] $post_type Post type of the post we are trying to resolve
113 *
114 * @return \GraphQL\Deferred
115 * @throws \Exception
116 * @since 0.0.5
117 */
118 public static function resolve_post_objects_connection( $source, array $args, AppContext $context, ResolveInfo $info, $post_type ) {
119 $resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, $post_type );
120
121 return $resolver->get_connection();
122 }
123
124 /**
125 * Retrieves the taxonomy object for the name of the taxonomy passed
126 *
127 * @param string $taxonomy Name of the taxonomy you want to retrieve the taxonomy object for
128 *
129 * @return \WPGraphQL\Model\Taxonomy object
130 * @throws \GraphQL\Error\UserError If no taxonomy is found with the name passed.
131 * @since 0.0.5
132 */
133 public static function resolve_taxonomy( $taxonomy ) {
134
135 /**
136 * Get the allowed_taxonomies.
137 */
138 $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies();
139
140 if ( ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
141 // translators: %s is the name of the taxonomy.
142 throw new UserError( esc_html( sprintf( __( 'No taxonomy was found with the name %s', 'wp-graphql' ), $taxonomy ) ) );
143 }
144
145 $tax_object = get_taxonomy( $taxonomy );
146
147 if ( ! $tax_object instanceof \WP_Taxonomy ) {
148 // translators: %s is the name of the taxonomy.
149 throw new UserError( esc_html( sprintf( __( 'No taxonomy was found with the name %s', 'wp-graphql' ), $taxonomy ) ) );
150 }
151
152 return new Taxonomy( $tax_object );
153 }
154
155 /**
156 * Wrapper for TermObjectConnectionResolver::resolve
157 *
158 * @param mixed $source The object the connection is coming from
159 * @param array<string,mixed> $args Array of args to be passed to the resolve method
160 * @param \WPGraphQL\AppContext $context The AppContext object to be passed down
161 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
162 * @param string $taxonomy The name of the taxonomy the term belongs to
163 *
164 * @return \GraphQL\Deferred
165 * @throws \Exception
166 * @since 0.0.5
167 */
168 public static function resolve_term_objects_connection( $source, array $args, AppContext $context, ResolveInfo $info, string $taxonomy ) {
169 $resolver = new TermObjectConnectionResolver( $source, $args, $context, $info, $taxonomy );
170
171 return $resolver->get_connection();
172 }
173
174 /**
175 * Retrieves the theme object for the theme you are looking for
176 *
177 * @param string $stylesheet Directory name for the theme.
178 *
179 * @return \WPGraphQL\Model\Theme object
180 * @throws \GraphQL\Error\UserError
181 * @since 0.0.5
182 */
183 public static function resolve_theme( $stylesheet ) {
184 $theme = wp_get_theme( $stylesheet );
185 if ( $theme->exists() ) {
186 return new Theme( $theme );
187 } else {
188 // translators: %s is the name of the theme stylesheet.
189 throw new UserError( esc_html( sprintf( __( 'No theme was found with the stylesheet: %s', 'wp-graphql' ), $stylesheet ) ) );
190 }
191 }
192
193 /**
194 * Wrapper for the ThemesConnectionResolver::resolve method
195 *
196 * @param mixed $source The object the connection is coming from
197 * @param array<string,mixed> $args Passes an array of arguments to the resolve method
198 * @param \WPGraphQL\AppContext $context The AppContext object to be passed down
199 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
200 *
201 * @return \GraphQL\Deferred
202 * @throws \Exception
203 * @since 0.0.5
204 */
205 public static function resolve_themes_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
206 $resolver = new ThemeConnectionResolver( $source, $args, $context, $info );
207 return $resolver->get_connection();
208 }
209
210 /**
211 * Wrapper for the UsersConnectionResolver::resolve method
212 *
213 * @param mixed $source The object the connection is coming from
214 * @param array<string,mixed> $args Array of args to be passed down to the resolve method
215 * @param \WPGraphQL\AppContext $context The AppContext object to be passed down
216 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
217 *
218 * @return \GraphQL\Deferred
219 * @throws \Exception
220 * @since 0.0.5
221 */
222 public static function resolve_users_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
223 $resolver = new UserConnectionResolver( $source, $args, $context, $info );
224
225 return $resolver->get_connection();
226 }
227
228 /**
229 * Returns an array of data about the user role you are requesting
230 *
231 * @param string $name Name of the user role you want info for
232 *
233 * @return \WPGraphQL\Model\UserRole
234 * @throws \GraphQL\Error\UserError If no user role is found with the name passed.
235 * @since 0.0.30
236 */
237 public static function resolve_user_role( $name ) {
238 $role = isset( wp_roles()->roles[ $name ] ) ? wp_roles()->roles[ $name ] : null;
239
240 if ( null === $role ) {
241 // translators: %s is the name of the user role.
242 throw new UserError( esc_html( sprintf( __( 'No user role was found with the name %s', 'wp-graphql' ), $name ) ) );
243 } else {
244 $role = (array) $role;
245 $role['id'] = $name;
246 $role['displayName'] = $role['name'];
247 $role['name'] = $name;
248
249 return new UserRole( $role );
250 }
251 }
252
253 /**
254 * Resolve the avatar for a user
255 *
256 * @param int $user_id ID of the user to get the avatar data for
257 * @param array<string,mixed> $args The args to pass to the get_avatar_data function
258 *
259 * @return \WPGraphQL\Model\Avatar|null
260 * @throws \Exception
261 */
262 public static function resolve_avatar( int $user_id, array $args ) {
263 $avatar = get_avatar_data( absint( $user_id ), $args );
264
265 // if there's no url returned, return null
266 if ( empty( $avatar['url'] ) ) {
267 return null;
268 }
269
270 $avatar = new Avatar( $avatar );
271
272 if ( 'private' === $avatar->get_visibility() ) {
273 return null;
274 }
275
276 return $avatar;
277 }
278
279 /**
280 * Resolve the connection data for user roles
281 *
282 * @param mixed[] $source The Query results
283 * @param array<string,mixed> $args The query arguments
284 * @param \WPGraphQL\AppContext $context The AppContext passed down to the query
285 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo object
286 *
287 * @return \GraphQL\Deferred
288 * @throws \Exception
289 */
290 public static function resolve_user_role_connection( $source, array $args, AppContext $context, ResolveInfo $info ) {
291 $resolver = new UserRoleConnectionResolver( $source, $args, $context, $info );
292
293 return $resolver->get_connection();
294 }
295
296 /**
297 * Format the setting group name to our standard.
298 *
299 * @param string $group
300 *
301 * @return string $group
302 */
303 public static function format_group_name( string $group ) {
304 $replaced_group = graphql_format_name( $group, ' ', '/[^a-zA-Z0-9 -]/' );
305
306 if ( ! empty( $replaced_group ) ) {
307 $group = $replaced_group;
308 }
309
310 $group = lcfirst( str_replace( '_', ' ', ucwords( $group, '_' ) ) );
311 $group = lcfirst( str_replace( '-', ' ', ucwords( $group, '_' ) ) );
312 $group = lcfirst( str_replace( ' ', '', ucwords( $group, ' ' ) ) );
313
314 return $group;
315 }
316
317 /**
318 * Get all of the allowed settings by group and return the
319 * settings group that matches the group param
320 *
321 * @param string $group
322 * @param \WPGraphQL\Registry\TypeRegistry $type_registry The WPGraphQL TypeRegistry
323 *
324 * @return array<string,mixed>
325 */
326 public static function get_setting_group_fields( string $group, TypeRegistry $type_registry ) {
327
328 /**
329 * Get all of the settings, sorted by group
330 */
331 $settings_groups = self::get_allowed_settings_by_group( $type_registry );
332
333 return ! empty( $settings_groups[ $group ] ) ? $settings_groups[ $group ] : [];
334 }
335
336 /**
337 * Build the canonical, normalized map of settings WPGraphQL can expose.
338 *
339 * The map is keyed by option name and each entry is the setting's registered
340 * args plus its `key`. Both read surfaces (the flat settings map and the
341 * grouped settings map) are derived from this single map so a setting cannot
342 * appear on one surface and not the other.
343 *
344 * The map is resolvable without a built schema. When a `TypeRegistry` is
345 * provided (the schema-build path) settings whose declared type has no
346 * corresponding GraphQL type are excluded, since they can't become fields.
347 * When resolved without one (e.g. cache invalidation reading the map outside a
348 * GraphQL request) that gate is skipped: the map is used to identify settings,
349 * not to register fields, so over-inclusion is harmless and no schema build is
350 * forced.
351 *
352 * @param \WPGraphQL\Registry\TypeRegistry|null $type_registry The WPGraphQL TypeRegistry, or null to resolve the map without a built schema.
353 *
354 * @return array<string,array<string,mixed>>
355 *
356 * @since 2.18.0
357 */
358 protected static function get_normalized_settings( ?TypeRegistry $type_registry = null ): array {
359
360 /**
361 * Get all registered settings
362 */
363 $registered_settings = get_registered_settings();
364
365 /**
366 * Loop through the $registered_settings and add the setting to the
367 * normalized map if it is allowed in GraphQL, or in REST when the
368 * setting doesn't declare `show_in_graphql`.
369 */
370 $normalized_settings = [];
371 foreach ( $registered_settings as $key => $setting ) {
372 $setting_key = (string) $key;
373
374 // Skip settings without a type, and, only when building the schema (a
375 // registry is provided), settings whose declared type has no
376 // corresponding GraphQL type. When the map is resolved without a
377 // registry the field-type gate doesn't apply, since the map is used to
378 // identify settings, not to register fields.
379 if ( ! isset( $setting['type'] ) || ( null !== $type_registry && ! $type_registry->get_type( $setting['type'] ) ) ) {
380 continue;
381 }
382
383 if ( ! isset( $setting['show_in_graphql'] ) ) {
384 if ( ! isset( $setting['show_in_rest'] ) || false === $setting['show_in_rest'] ) {
385 continue;
386 }
387 } elseif ( true !== $setting['show_in_graphql'] ) {
388 continue;
389 }
390
391 $setting['key'] = $setting_key;
392 $normalized_settings[ $setting_key ] = $setting;
393 }
394
395 /**
396 * Apply WPGraphQL-managed config to core settings that need behavior
397 * beyond what their registration args declare.
398 */
399 foreach ( self::get_core_setting_config() as $setting_key => $config ) {
400 if ( isset( $normalized_settings[ $setting_key ] ) ) {
401 $normalized_settings[ $setting_key ] = array_merge( $normalized_settings[ $setting_key ], $config );
402 }
403 }
404
405 /**
406 * Seed the in-memory shims for options WordPress doesn't register via
407 * register_setting() (e.g. `home`, the permalink options). Added after the
408 * registered-settings loop and before the filter so extensions can override
409 * them, and only when the option isn't already present in the map so a real
410 * registration always wins.
411 */
412 foreach ( self::get_core_shim_settings() as $setting_key => $shim ) {
413 if ( ! isset( $normalized_settings[ $setting_key ] ) ) {
414 $shim['key'] = (string) $setting_key;
415 $normalized_settings[ $setting_key ] = $shim;
416 }
417 }
418
419 /**
420 * Filter the normalized settings map before the read and mutation surfaces are derived from it.
421 *
422 * This is the seam for exposing options WordPress never registers via register_setting():
423 * seed an entry here, in memory, instead of mutating the global settings registry. Entries
424 * follow the register_setting() args shape (`group`, `type`, `description`, ...) plus a `key`
425 * holding the option name, and support WPGraphQL-specific config: `graphql_readonly` (bool)
426 * rejects updates to the setting through the updateSettings mutation, and `graphql_resolve`
427 * (callable) normalizes the setting's resolved value.
428 *
429 * @param array<string,array<string,mixed>> $normalized_settings The normalized settings map, keyed by option name.
430 * @param \WPGraphQL\Registry\TypeRegistry|null $type_registry The WPGraphQL TypeRegistry, or null when the map is resolved without a built schema.
431 *
432 * @hookGroup settings
433 * @since 2.18.0
434 */
435 $normalized_settings = apply_filters( 'graphql_normalized_settings', $normalized_settings, $type_registry );
436
437 /**
438 * Ensure every entry carries its option name as `key`, so filter-added
439 * entries don't need to duplicate it. Then precompute each entry's GraphQL
440 * field names once, so every read/write surface reads the same name instead
441 * of re-deriving it independently.
442 */
443 foreach ( $normalized_settings as $setting_key => $setting ) {
444 if ( ! isset( $setting['key'] ) ) {
445 $setting['key'] = (string) $setting_key;
446 $normalized_settings[ $setting_key ]['key'] = (string) $setting_key;
447 }
448
449 // The base/grouped field name (e.g. `homeUrl`, used on GeneralSettings).
450 $field_name = self::get_setting_field_name( $setting );
451
452 $normalized_settings[ $setting_key ]['graphql_field_name'] = $field_name;
453
454 // The flat field name (e.g. `generalSettingsHomeUrl`, used on the Settings type)
455 // only exists for entries that belong to a group.
456 if ( ! empty( $setting['group'] ) ) {
457 $normalized_settings[ $setting_key ]['graphql_settings_field_name'] = lcfirst( self::format_group_name( (string) $setting['group'] ) . 'Settings' . ucfirst( $field_name ) );
458 }
459 }
460
461 return $normalized_settings;
462 }
463
464 /**
465 * Derive the base (grouped) GraphQL field name for a normalized setting.
466 *
467 * `graphql_field_name`, when set, overrides the name otherwise derived from the
468 * REST name (`show_in_rest['name']`) or the option key. In every case the name
469 * is run through `Utils::format_field_name()`, the same canonical formatter the
470 * field registration applies, so the precomputed name matches the name that
471 * ends up in the Schema and stays consistent with field naming elsewhere.
472 *
473 * @param array<string,mixed> $setting A normalized settings map entry.
474 *
475 * @since 2.18.0
476 */
477 protected static function get_setting_field_name( array $setting ): string {
478 if ( ! empty( $setting['graphql_field_name'] ) ) {
479 $name = (string) $setting['graphql_field_name'];
480 } elseif ( ! empty( $setting['show_in_rest']['name'] ) ) {
481 $name = (string) $setting['show_in_rest']['name'];
482 } else {
483 $name = isset( $setting['key'] ) ? (string) $setting['key'] : '';
484 }
485
486 return Utils::format_field_name( $name );
487 }
488
489 /**
490 * WPGraphQL-managed config for core settings, applied on top of their
491 * registered args in the normalized settings map.
492 *
493 * @return array<string,array<string,mixed>>
494 *
495 * @since 2.18.0
496 */
497 protected static function get_core_setting_config(): array {
498 return [
499 // The administrator's email address is only readable by users who can
500 // manage the site's options.
501 'admin_email' => [
502 'graphql_capability' => 'manage_options',
503 ],
504 // The site URL must not be updatable through the API. The input field is
505 // kept (deprecated) rather than removed so existing schemas don't break.
506 'siteurl' => [
507 'graphql_readonly' => true,
508 'graphql_deprecated_input' => __( 'The site URL is read-only and cannot be changed through the API.', 'wp-graphql' ),
509 ],
510 // Derive the timezone from `gmt_offset` when `timezone_string` is empty.
511 'timezone_string' => [
512 'graphql_resolve' => [ SettingGroup::class, 'resolve_timezone_setting_value' ],
513 ],
514 ];
515 }
516
517 /**
518 * WPGraphQL-maintained shim settings for options WordPress does not register
519 * via register_setting() (e.g. the Site Address and the permalink options).
520 *
521 * These are seeded into the normalized settings map in memory so they surface
522 * in the Schema without mutating WordPress's global settings registry. Each
523 * entry follows the register_setting() args shape plus WPGraphQL per-entry
524 * config. They are seeded only when the option isn't already registered, so a
525 * real registration always wins.
526 *
527 * @return array<string,array<string,mixed>>
528 *
529 * @since 2.18.0
530 */
531 protected static function get_core_shim_settings(): array {
532 return [
533 // "siteurl" is registered by core on single-site but not on multisite.
534 // Seeding it here (only applied when it isn't already registered, i.e. on
535 // multisite) exposes `url` and the flat `generalSettingsUrl` through the
536 // settings machinery instead of a one-off polyfill field. On single-site
537 // the registered setting wins.
538 'siteurl' => [
539 'group' => 'general',
540 'type' => 'string',
541 'description' => __( 'The base URL where the site\'s application and content management backend are served. Can differ from the `homeUrl` field when the front end and backend are served from different addresses, such as on headless or decoupled installs.', 'wp-graphql' ),
542 'graphql_field_name' => 'url',
543 'graphql_readonly' => true,
544 // Multisite-aware: get_site_url() returns the current site's URL.
545 'graphql_resolve' => static function () {
546 return get_site_url();
547 },
548 ],
549 // "Site Address" (home) is not registered by core on any install.
550 'home' => [
551 'group' => 'general',
552 'type' => 'string',
553 'description' => __( 'The address at which visitors reach the site\'s front end. Can differ from the `url` field when the front end and the content management backend are served from different addresses, such as on headless or decoupled installs.', 'wp-graphql' ),
554 'graphql_field_name' => 'homeUrl',
555 'graphql_readonly' => true,
556 // Multisite-aware: get_home_url() returns the current site's home URL,
557 // not the raw option value.
558 'graphql_resolve' => static function () {
559 return get_home_url();
560 },
561 ],
562 // The permalink options drive the `uri` field on every content node and
563 // term, so a change to any of them has schema-wide impact rather than
564 // affecting only its own settings group. `graphql_purge_all` marks that
565 // breadth for cache-invalidation consumers (e.g. WPGraphQL Smart Cache).
566 'permalink_structure' => [
567 'group' => 'permalink',
568 'type' => 'string',
569 'description' => __( 'The structure used to build the URLs for content on the site.', 'wp-graphql' ),
570 'graphql_field_name' => 'structure',
571 'graphql_readonly' => true,
572 'graphql_purge_all' => true,
573 ],
574 'category_base' => [
575 'group' => 'permalink',
576 'type' => 'string',
577 'description' => __( 'The prefix used in the URLs of category archive pages.', 'wp-graphql' ),
578 'graphql_field_name' => 'categoryBase',
579 'graphql_readonly' => true,
580 'graphql_purge_all' => true,
581 ],
582 'tag_base' => [
583 'group' => 'permalink',
584 'type' => 'string',
585 'description' => __( 'The prefix used in the URLs of tag archive pages.', 'wp-graphql' ),
586 'graphql_field_name' => 'tagBase',
587 'graphql_readonly' => true,
588 'graphql_purge_all' => true,
589 ],
590 ];
591 }
592
593 /**
594 * Get all of the allowed settings by group
595 *
596 * @param \WPGraphQL\Registry\TypeRegistry|null $type_registry The WPGraphQL TypeRegistry, or null to resolve the grouped map without a built schema.
597 *
598 * @return array<string,array<string,mixed>> $allowed_settings_by_group
599 */
600 public static function get_allowed_settings_by_group( ?TypeRegistry $type_registry = null ) {
601
602 /**
603 * Group the normalized settings ( general, reading, discussion, writing, etc. ),
604 * skipping settings that don't have a group.
605 */
606 $allowed_settings_by_group = [];
607 foreach ( self::get_normalized_settings( $type_registry ) as $setting_key => $setting ) {
608 if ( ! isset( $setting['group'] ) || empty( $setting['group'] ) ) {
609 continue;
610 }
611
612 /** @var string $setting_group */
613 $setting_group = $setting['group'];
614 $group = self::format_group_name( $setting_group );
615
616 $allowed_settings_by_group[ $group ][ $setting_key ] = $setting;
617 }
618
619 /**
620 * Filter the $allowed_settings_by_group to allow enabling or disabling groups in the GraphQL Schema.
621 *
622 * @param array<string,array<string,mixed>> $allowed_settings_by_group The settings grouped by normalized setting group key.
623 *
624 * @hookGroup settings
625 * @since 0.0.1
626 */
627 return apply_filters( 'graphql_allowed_settings_by_group', $allowed_settings_by_group );
628 }
629
630 /**
631 * Get all of the $allowed_settings
632 *
633 * @param \WPGraphQL\Registry\TypeRegistry|null $type_registry The WPGraphQL TypeRegistry, or null to resolve the flat map without a built schema.
634 *
635 * @return array<string,array<string,mixed>> $allowed_settings
636 */
637 public static function get_allowed_settings( ?TypeRegistry $type_registry = null ) {
638
639 /**
640 * The flat view is the normalized map itself.
641 */
642 $allowed_settings = self::get_normalized_settings( $type_registry );
643
644 /**
645 * Filter the $allowed_settings to allow some to be enabled or disabled from showing in
646 * the GraphQL Schema.
647 *
648 * @param array<string,array<string,mixed>> $allowed_settings The settings that can be exposed in the GraphQL schema.
649 *
650 * @hookGroup settings
651 * @since 0.0.1
652 */
653 return apply_filters( 'graphql_allowed_setting_groups', $allowed_settings );
654 }
655
656 /**
657 * We get the node interface and field from the relay library.
658 *
659 * The first method is the way we resolve an ID to its object. The second is the way we resolve
660 * an object that implements node to its type.
661 *
662 * @return mixed[]
663 * @throws \GraphQL\Error\UserError
664 */
665 public static function get_node_definition() {
666 if ( null === self::$node_definition ) {
667 $node_definition = Relay::nodeDefinitions(
668 // The ID fetcher definition
669 static function ( $global_id, AppContext $context, ResolveInfo $info ) {
670 self::resolve_node( $global_id, $context, $info );
671 },
672 // Type resolver
673 static function ( $node ) {
674 self::resolve_node_type( $node );
675 }
676 );
677
678 self::$node_definition = $node_definition;
679 }
680
681 return self::$node_definition;
682 }
683
684 /**
685 * Given a node, returns the GraphQL Type
686 *
687 * @param mixed $node The node to resolve the type of
688 *
689 * @return string
690 * @throws \GraphQL\Error\UserError If no type is found for the node.
691 */
692 public static function resolve_node_type( $node ) {
693 $type = null;
694
695 if ( true === is_object( $node ) ) {
696 switch ( true ) {
697 case $node instanceof Post:
698 if ( $node->isRevision ) {
699 /** @var ?\WP_Post */
700 $parent_post = get_post( $node->parentDatabaseId );
701
702 if ( ! empty( $parent_post ) ) {
703 /** @var \WP_Post_Type $post_type_object */
704 $post_type_object = get_post_type_object( $parent_post->post_type );
705 $type = $post_type_object->graphql_single_name ?? null;
706
707 break;
708 }
709 }
710
711 /** @var \WP_Post_Type $post_type_object */
712 $post_type_object = isset( $node->post_type ) ? get_post_type_object( $node->post_type ) : null;
713 $type = $post_type_object->graphql_single_name ?? null;
714 break;
715 case $node instanceof Term:
716 /** @var \WP_Taxonomy $tax_object */
717 $tax_object = isset( $node->taxonomyName ) ? get_taxonomy( $node->taxonomyName ) : null;
718 $type = $tax_object->graphql_single_name;
719 break;
720 case $node instanceof Comment:
721 $type = 'Comment';
722 break;
723 case $node instanceof PostType:
724 $type = 'ContentType';
725 break;
726 case $node instanceof SettingGroupModel:
727 $type = SettingGroup::get_type_name( $node->get_group_key() );
728 break;
729 case $node instanceof Taxonomy:
730 $type = 'Taxonomy';
731 break;
732 case $node instanceof Theme:
733 $type = 'Theme';
734 break;
735 case $node instanceof User:
736 $type = 'User';
737 break;
738 case $node instanceof Plugin:
739 $type = 'Plugin';
740 break;
741 case $node instanceof CommentAuthor:
742 $type = 'CommentAuthor';
743 break;
744 case $node instanceof Menu:
745 $type = 'Menu';
746 break;
747 case $node instanceof \_WP_Dependency:
748 $type = isset( $node->type ) ? $node->type : null;
749 break;
750 default:
751 $type = null;
752 }
753 }
754
755 /**
756 * Add a filter to allow externally registered node types to return the proper type
757 * based on the node_object that's returned
758 *
759 * @param mixed|object|array $type The type definition the node should resolve to.
760 * @param mixed|object|array $node The $node that is being resolved
761 *
762 * @hookGroup request-lifecycle
763 * @since 0.0.6
764 */
765 $type = apply_filters( 'graphql_resolve_node_type', $type, $node );
766
767 /**
768 * If the $type is not properly resolved, throw an exception
769 *
770 * @since 0.0.6
771 */
772 if ( empty( $type ) ) {
773 throw new UserError( esc_html__( 'No type was found matching the node', 'wp-graphql' ) );
774 }
775
776 /**
777 * Return the resolved $type for the $node
778 *
779 * @since 0.0.5
780 */
781 return ucfirst( $type );
782 }
783
784 /**
785 * Given the ID of a node, this resolves the data
786 *
787 * @param string $global_id The Global ID of the node
788 * @param \WPGraphQL\AppContext $context The Context of the GraphQL Request
789 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo for the GraphQL Request
790 *
791 * @return ?\GraphQL\Deferred
792 * @throws \GraphQL\Error\UserError If no ID is passed.
793 */
794 public static function resolve_node( $global_id, AppContext $context, ResolveInfo $info ) {
795 if ( empty( $global_id ) ) {
796 throw new UserError( esc_html__( 'An ID needs to be provided to resolve a node.', 'wp-graphql' ) );
797 }
798
799 /**
800 * Convert the encoded ID into an array we can work with
801 *
802 * @since 0.0.4
803 */
804 $id_components = Relay::fromGlobalId( $global_id );
805
806 /**
807 * $id_components is an array with the id and type
808 *
809 * @since 0.0.5
810 */
811 if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) {
812 // translators: %s is the global ID.
813 throw new UserError( esc_html( sprintf( __( 'The global ID isn\'t recognized ID: %s', 'wp-graphql' ), $global_id ) ) );
814 }
815
816 /**
817 * Get the allowed_post_types and allowed_taxonomies
818 *
819 * @since 0.0.5
820 */
821
822 $loader = $context->get_loader( $id_components['type'] );
823
824 if ( $loader ) {
825 return $loader->load_deferred( $id_components['id'] );
826 }
827
828 return null;
829 }
830
831 /**
832 * Returns array of nav menu location names
833 *
834 * @return string[]
835 */
836 public static function get_registered_nav_menu_locations() {
837 global $_wp_registered_nav_menus;
838
839 return ! empty( $_wp_registered_nav_menus ) && is_array( $_wp_registered_nav_menus ) ? array_keys( $_wp_registered_nav_menus ) : [];
840 }
841
842 /**
843 * This resolves a resource, given a URI (the path / permalink to a resource)
844 *
845 * Based largely on the core parse_request function in wp-includes/class-wp.php
846 *
847 * @param string $uri The URI to fetch a resource from
848 * @param \WPGraphQL\AppContext $context The AppContext passed through the GraphQL Resolve Tree
849 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed through the GraphQL Resolve tree
850 *
851 * @return \GraphQL\Deferred
852 * @throws \Exception
853 */
854 public static function resolve_resource_by_uri( $uri, $context, $info ) {
855 $node_resolver = new NodeResolver( $context );
856
857 return $node_resolver->resolve_uri( $uri );
858 }
859
860 /**
861 * @todo remove in 3.0.0
862 * @deprecated Use the Loader passed in $context instead
863 * @codeCoverageIgnore
864 *
865 * @param int $id ID of the comment we want to get the object for.
866 * @param \WPGraphQL\AppContext $context The context of the request.
867 *
868 * @return \GraphQL\Deferred object
869 * @throws \GraphQL\Error\UserError Throws UserError.
870 * @throws \Exception Throws UserError.
871 */
872 public static function resolve_comment( $id, $context ) {
873 _doing_it_wrong(
874 __METHOD__,
875 sprintf(
876 /* translators: %s is the method name */
877 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
878 '$context->get_loader( \'comment\' )->load_deferred( $id )'
879 ),
880 '0.8.4'
881 );
882
883 return $context->get_loader( 'comment' )->load_deferred( $id );
884 }
885
886 /**
887 * @todo remove in 3.0.0
888 * @deprecated Use the Loader passed in $context instead
889 * @codeCoverageIgnore
890 *
891 * @param int $id ID of the post you are trying to retrieve
892 * @param \WPGraphQL\AppContext $context The context of the GraphQL Request
893 *
894 * @return \GraphQL\Deferred
895 *
896 * @throws \GraphQL\Error\UserError
897 * @throws \Exception
898 */
899 public static function resolve_post_object( int $id, AppContext $context ) {
900 _doing_it_wrong(
901 __METHOD__,
902 sprintf(
903 /* translators: %s is the method name */
904 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
905 '$context->get_loader( \'post\' )->load_deferred( $id )'
906 ),
907 '0.8.4'
908 );
909 return $context->get_loader( 'post' )->load_deferred( $id );
910 }
911
912 /**
913 * @todo remove in 3.0.0
914 * @deprecated Use the Loader passed in $context instead
915 * @codeCoverageIgnore
916 *
917 * @param int $id The ID of the menu item to load
918 * @param \WPGraphQL\AppContext $context The context of the GraphQL request
919 *
920 * @return \GraphQL\Deferred|null
921 * @throws \Exception
922 */
923 public static function resolve_menu_item( int $id, AppContext $context ) {
924 _doing_it_wrong(
925 __METHOD__,
926 sprintf(
927 /* translators: %s is the method name */
928 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
929 '$context->get_loader( \'menu_item\' )->load_deferred( $id )'
930 ),
931 '0.8.4'
932 );
933 return $context->get_loader( 'post' )->load_deferred( $id );
934 }
935
936 /**
937 * @todo remove in 3.0.0
938 * @deprecated Use the Loader passed in $context instead
939 * @codeCoverageIgnore
940 *
941 * @param int $id ID of the term you are trying to retrieve the object for
942 * @param \WPGraphQL\AppContext $context The context of the GraphQL Request
943 *
944 * @return \GraphQL\Deferred
945 * @throws \Exception
946 */
947 public static function resolve_term_object( $id, AppContext $context ) {
948 _doing_it_wrong(
949 __METHOD__,
950 sprintf(
951 /* translators: %s is the method name */
952 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
953 '$context->get_loader( \'term\' )->load_deferred( $id )'
954 ),
955 '0.8.4'
956 );
957 return $context->get_loader( 'term' )->load_deferred( $id );
958 }
959
960 /**
961 * @todo remove in 3.0.0
962 * @deprecated Use the Loader passed in $context instead
963 * @codeCoverageIgnore
964 *
965 * @param int $id ID of the user you want the object for
966 * @param \WPGraphQL\AppContext $context The AppContext
967 *
968 * @return \GraphQL\Deferred
969 * @throws \Exception
970 */
971 public static function resolve_user( $id, AppContext $context ) {
972 _doing_it_wrong(
973 __METHOD__,
974 sprintf(
975 /* translators: %s is the method name */
976 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
977 '$context->get_loader( \'user\' )->load_deferred( $id )'
978 ),
979 '0.8.4'
980 );
981 return $context->get_loader( 'user' )->load_deferred( $id );
982 }
983 }
984