• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / #887

16 Jan 2025 10:08PM UTC coverage: 83.189% (-0.8%) from 83.968%
#887

push

php-coveralls

web-flow
Merge pull request #3272 from wp-graphql/release/v1.30.0

release: v1.30.0

473 of 718 new or added lines in 23 files covered. (65.88%)

2 existing lines in 2 files now uncovered.

12995 of 15621 relevant lines covered (83.19%)

298.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.57
/src/Utils/InstrumentSchema.php
1
<?php
2

3
namespace WPGraphQL\Utils;
4

5
use GraphQL\Error\UserError;
6
use GraphQL\Executor\Executor;
7
use GraphQL\Type\Definition\FieldDefinition;
8
use GraphQL\Type\Definition\ResolveInfo;
9
use GraphQL\Type\Definition\Type;
10
use WPGraphQL\AppContext;
11

12
/**
13
 * Class InstrumentSchema
14
 *
15
 * @package WPGraphQL\Data
16
 */
17
class InstrumentSchema {
18

19
        /**
20
         * @param \GraphQL\Type\Definition\Type $type Instance of the Schema.
21
         * @param string                        $type_name Name of the Type
22
         */
23
        public static function instrument_resolvers( Type $type, string $type_name ): Type {
24
                if ( ! method_exists( $type, 'getFields' ) ) {
599✔
25
                        return $type;
591✔
26
                }
27

28
                $fields = $type->getFields();
598✔
29

30
                $fields                 = ! empty( $fields ) ? self::wrap_fields( $fields, $type->name ) : [];
598✔
31
                $type->name             = ucfirst( esc_html( $type->name ) );
598✔
32
                $type->description      = ! empty( $type->description ) ? esc_html( $type->description ) : '';
598✔
33
                $type->config['fields'] = $fields;
598✔
34

35
                return $type;
598✔
36
        }
37

38
        /**
39
         * Wrap Fields
40
         *
41
         * This wraps fields to provide sanitization on fields output by introspection queries
42
         * (description/deprecation reason) and provides hooks to resolvers.
43
         *
44
         * @param mixed[] $fields    The fields configured for a Type
45
         * @param string  $type_name The Type name
46
         *
47
         * @return mixed[]
48
         */
49
        protected static function wrap_fields( array $fields, string $type_name ) {
50
                if ( empty( $fields ) ) {
598✔
51
                        return $fields;
×
52
                }
53

54
                foreach ( $fields as $field_key => $field ) {
598✔
55

56
                        /**
57
                         * Filter the field definition
58
                         *
59
                         * @param \GraphQL\Type\Definition\FieldDefinition $field The field definition
60
                         * @param string          $type_name The name of the Type the field belongs to
61
                         */
62
                        $field = apply_filters( 'graphql_field_definition', $field, $type_name );
598✔
63

64
                        if ( ! $field instanceof FieldDefinition ) {
598✔
65
                                return $field;
418✔
66
                        }
67

68
                        /**
69
                         * Get the fields resolve function
70
                         *
71
                         * @since 0.0.1
72
                         */
73
                        $field_resolver = is_callable( $field->resolveFn ) ? $field->resolveFn : null;
597✔
74

75
                        /**
76
                         * Sanitize the description and deprecation reason
77
                         */
78
                        $field->description       = ! empty( $field->description ) && is_string( $field->description ) ? esc_html( $field->description ) : '';
597✔
79
                        $field->deprecationReason = ! empty( $field->deprecationReason ) && is_string( $field->description ) ? esc_html( $field->deprecationReason ) : null;
597✔
80

81
                        /**
82
                         * Replace the existing field resolve method with a new function that captures data about
83
                         * the resolver to be stored in the resolver_report
84
                         *
85
                         * @param mixed                                $source  The source passed down the Resolve Tree
86
                         * @param array<string,mixed>                  $args    The args for the field
87
                         * @param \WPGraphQL\AppContext                $context The AppContext passed down the ResolveTree
88
                         * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the ResolveTree
89
                         *
90
                         * @return mixed
91
                         * @throws \Exception
92
                         * @since 0.0.1
93
                         */
94
                        $field->resolveFn = static function ( $source, array $args, AppContext $context, ResolveInfo $info ) use ( $field_resolver, $type_name, $field_key, $field ) {
745✔
95

96
                                /**
97
                                 * Fire an action BEFORE the field resolves
98
                                 *
99
                                 * @param mixed                                    $source         The source passed down the Resolve Tree
100
                                 * @param array<string,mixed>                      $args           The args for the field
101
                                 * @param \WPGraphQL\AppContext                    $context        The AppContext passed down the ResolveTree
102
                                 * @param \GraphQL\Type\Definition\ResolveInfo     $info           The ResolveInfo passed down the ResolveTree
103
                                 * @param ?callable                                $field_resolver The Resolve function for the field
104
                                 * @param string                                   $type_name      The name of the type the fields belong to
105
                                 * @param string                                   $field_key      The name of the field
106
                                 * @param \GraphQL\Type\Definition\FieldDefinition $field          The Field Definition for the resolving field
107
                                 */
108
                                do_action( 'graphql_before_resolve_field', $source, $args, $context, $info, $field_resolver, $type_name, $field_key, $field );
663✔
109

110
                                /**
111
                                 * Create unique custom "nil" value which is different from the build-in PHP null, false etc.
112
                                 * When this custom "nil" is returned we can know that the filter did not try to preresolve
113
                                 * the field because it does not equal with anything but itself.
114
                                 */
115
                                $nil = new \stdClass();
663✔
116

117
                                /**
118
                                 * When this filter return anything other than the $nil it will be used as the resolved value
119
                                 * and the execution of the actual resolved is skipped. This filter can be used to implement
120
                                 * field level caches or for efficiently hiding data by returning null.
121
                                 *
122
                                 * @param mixed                                    $nil            Unique nil value
123
                                 * @param mixed                                    $source         The source passed down the Resolve Tree
124
                                 * @param array<string,mixed>                      $args           The args for the field
125
                                 * @param \WPGraphQL\AppContext                    $context        The AppContext passed down the ResolveTree
126
                                 * @param \GraphQL\Type\Definition\ResolveInfo     $info           The ResolveInfo passed down the ResolveTree
127
                                 * @param string                                   $type_name      The name of the type the fields belong to
128
                                 * @param string                                   $field_key      The name of the field
129
                                 * @param \GraphQL\Type\Definition\FieldDefinition $field          The Field Definition for the resolving field
130
                                 * @param ?callable                                $field_resolver The default field resolver
131
                                 */
132
                                $result = apply_filters( 'graphql_pre_resolve_field', $nil, $source, $args, $context, $info, $type_name, $field_key, $field, $field_resolver );
663✔
133

134
                                /**
135
                                 * Check if the field pre-resolved
136
                                 */
137
                                if ( $nil === $result ) {
663✔
138
                                        /**
139
                                         * If the current field doesn't have a resolve function, use the defaultFieldResolver,
140
                                         * otherwise use the $field_resolver
141
                                         */
142
                                        if ( null === $field_resolver ) {
662✔
143
                                                $result = Executor::defaultFieldResolver( $source, $args, $context, $info );
554✔
144
                                        } else {
145
                                                $result = $field_resolver( $source, $args, $context, $info );
657✔
146
                                        }
147
                                }
148

149
                                /**
150
                                 * Fire an action before the field resolves
151
                                 *
152
                                 * @param mixed                                    $result          The result of the field resolution
153
                                 * @param mixed                                    $source          The source passed down the Resolve Tree
154
                                 * @param array<string,mixed>                      $args            The args for the field
155
                                 * @param \WPGraphQL\AppContext                    $context         The AppContext passed down the ResolveTree
156
                                 * @param \GraphQL\Type\Definition\ResolveInfo     $info            The ResolveInfo passed down the ResolveTree
157
                                 * @param string                                   $type_name       The name of the type the fields belong to
158
                                 * @param string                                   $field_key       The name of the field
159
                                 * @param \GraphQL\Type\Definition\FieldDefinition $field The Field Definition for the resolving field
160
                                 * @param ?callable                                $field_resolver  The default field resolver
161
                                 */
162
                                $result = apply_filters( 'graphql_resolve_field', $result, $source, $args, $context, $info, $type_name, $field_key, $field, $field_resolver );
612✔
163

164
                                /**
165
                                 * Fire an action AFTER the field resolves
166
                                 *
167
                                 * @param mixed                                    $source         The source passed down the Resolve Tree
168
                                 * @param array<string,mixed>                      $args           The args for the field
169
                                 * @param \WPGraphQL\AppContext                    $context        The AppContext passed down the ResolveTree
170
                                 * @param \GraphQL\Type\Definition\ResolveInfo     $info           The ResolveInfo passed down the ResolveTree
171
                                 * @param ?callable                                $field_resolver The Resolve function for the field
172
                                 * @param string                                   $type_name      The name of the type the fields belong to
173
                                 * @param string                                   $field_key      The name of the field
174
                                 * @param \GraphQL\Type\Definition\FieldDefinition $field          The Field Definition for the resolving field
175
                                 * @param mixed                                    $result         The result of the field resolver
176
                                 */
177
                                do_action( 'graphql_after_resolve_field', $source, $args, $context, $info, $field_resolver, $type_name, $field_key, $field, $result );
611✔
178

179
                                return $result;
611✔
180
                        };
745✔
181
                }
182

183
                /**
184
                 * Return the fields
185
                 */
186
                return $fields;
597✔
187
        }
188

189
        /**
190
         * Check field permissions when resolving. If the check fails, an error will be thrown.
191
         *
192
         * This takes into account auth params defined in the Schema
193
         *
194
         * @param mixed                                    $source         The source passed down the Resolve Tree
195
         * @param array<string,mixed>                      $args           The args for the field
196
         * @param \WPGraphQL\AppContext                    $context        The AppContext passed down the ResolveTree
197
         * @param \GraphQL\Type\Definition\ResolveInfo     $info           The ResolveInfo passed down the ResolveTree
198
         * @param mixed|callable|string                    $field_resolver The Resolve function for the field
199
         * @param string                                   $type_name      The name of the type the fields belong to
200
         * @param string                                   $field_key      The name of the field
201
         * @param \GraphQL\Type\Definition\FieldDefinition $field          The Field Definition for the resolving field
202
         *
203
         * @return void
204
         *
205
         * @throws \GraphQL\Error\UserError
206
         */
207
        public static function check_field_permissions( $source, array $args, AppContext $context, ResolveInfo $info, $field_resolver, string $type_name, string $field_key, FieldDefinition $field ) {
208
                if ( ( ! isset( $field->config['auth'] ) || ! is_array( $field->config['auth'] ) ) && ! isset( $field->config['isPrivate'] ) ) {
663✔
209
                        return;
612✔
210
                }
211

212
                /**
213
                 * Set the default auth error message
214
                 */
215
                $default_auth_error_message = __( 'You do not have permission to view this', 'wp-graphql' );
485✔
216
                $default_auth_error_message = apply_filters( 'graphql_field_resolver_auth_error_message', $default_auth_error_message, $field );
485✔
217

218
                /**
219
                 * Retrieve permissions error message.
220
                 */
221
                $auth_error = isset( $field->config['auth']['errorMessage'] ) && ! empty( $field->config['auth']['errorMessage'] )
485✔
222
                        ? $field->config['auth']['errorMessage']
1✔
223
                        : $default_auth_error_message;
484✔
224

225
                /**
226
                 * If the user is authenticated, and the field has a custom auth callback configured
227
                 * execute the callback before continuing resolution
228
                 */
229
                if ( isset( $field->config['auth']['callback'] ) && is_callable( $field->config['auth']['callback'] ) ) {
485✔
230
                        $authorized = call_user_func( $field->config['auth']['callback'], $field, $field_key, $source, $args, $context, $info, $field_resolver );
3✔
231

232
                        // If callback returns explicit false throw.
233
                        if ( false === $authorized ) {
3✔
234
                                throw new UserError( esc_html( $auth_error ) );
3✔
235
                        }
236

237
                        return;
3✔
238
                }
239

240
                /**
241
                 * If the schema for the field is configured to "isPrivate" or has "auth" configured,
242
                 * make sure the user is authenticated before resolving the field
243
                 */
244
                if ( isset( $field->config['isPrivate'] ) && true === $field->config['isPrivate'] && empty( get_current_user_id() ) ) {
483✔
245
                        throw new UserError( esc_html( $auth_error ) );
2✔
246
                }
247

248
                /**
249
                 * If the user is authenticated and the field has "allowedCaps" configured,
250
                 * ensure the user has at least one of the allowedCaps before resolving
251
                 */
252
                if ( isset( $field->config['auth']['allowedCaps'] ) && is_array( $field->config['auth']['allowedCaps'] ) ) {
483✔
253
                        $caps = ! empty( wp_get_current_user()->allcaps ) ? wp_get_current_user()->allcaps : [];
1✔
254
                        if ( empty( array_intersect( array_keys( $caps ), array_values( $field->config['auth']['allowedCaps'] ) ) ) ) {
1✔
255
                                throw new UserError( esc_html( $auth_error ) );
1✔
256
                        }
257
                }
258

259
                /**
260
                 * If the user is authenticated and the field has "allowedRoles" configured,
261
                 * ensure the user has at least one of the allowedRoles before resolving
262
                 */
263
                if ( isset( $field->config['auth']['allowedRoles'] ) && is_array( $field->config['auth']['allowedRoles'] ) ) {
483✔
264
                        $roles         = ! empty( wp_get_current_user()->roles ) ? wp_get_current_user()->roles : [];
×
265
                        $allowed_roles = array_values( $field->config['auth']['allowedRoles'] );
×
NEW
266
                        if ( empty( array_intersect( array_values( $roles ), $allowed_roles ) ) ) {
×
267
                                throw new UserError( esc_html( $auth_error ) );
×
268
                        }
269
                }
270
        }
271
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc