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

wp-graphql / wp-graphql / 13316763745

13 Feb 2025 08:45PM UTC coverage: 82.712% (-0.3%) from 83.023%
13316763745

push

github

web-flow
Merge pull request #3307 from wp-graphql/release/v2.0.0

release: v2.0.0

195 of 270 new or added lines in 20 files covered. (72.22%)

180 existing lines in 42 files now uncovered.

13836 of 16728 relevant lines covered (82.71%)

299.8 hits per line

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

44.53
/src/Data/UserMutation.php
1
<?php
2

3
namespace WPGraphQL\Data;
4

5
use Exception;
6
use GraphQL\Error\UserError;
7
use GraphQL\Type\Definition\ResolveInfo;
8
use WPGraphQL\AppContext;
9

10
/**
11
 * Class UserMutation
12
 *
13
 * @package WPGraphQL\Type\User\Mutation
14
 */
15
class UserMutation {
16

17
        /**
18
         * Stores the input fields static definition
19
         *
20
         * @var array<string,array<string,mixed>>
21
         */
22
        private static $input_fields = [];
23

24
        /**
25
         * Defines the accepted input arguments
26
         *
27
         * @return array<string,array<string,mixed>>|null
28
         */
UNCOV
29
        public static function input_fields() {
×
30
                if ( empty( self::$input_fields ) ) {
×
31
                        $input_fields = [
×
32
                                'password'    => [
×
33
                                        'type'        => 'String',
×
34
                                        'description' => __( 'A string that contains the plain text password for the user.', 'wp-graphql' ),
×
35
                                ],
×
36
                                'nicename'    => [
×
37
                                        'type'        => 'String',
×
38
                                        'description' => __( 'A string that contains a URL-friendly name for the user. The default is the user\'s username.', 'wp-graphql' ),
×
39
                                ],
×
40
                                'websiteUrl'  => [
×
41
                                        'type'        => 'String',
×
42
                                        'description' => __( 'A string containing the user\'s URL for the user\'s web site.', 'wp-graphql' ),
×
43
                                ],
×
44
                                'email'       => [
×
45
                                        'type'        => 'String',
×
46
                                        'description' => __( 'A string containing the user\'s email address.', 'wp-graphql' ),
×
47
                                ],
×
48
                                'displayName' => [
×
49
                                        'type'        => 'String',
×
50
                                        'description' => __( 'A string that will be shown on the site. Defaults to user\'s username. It is likely that you will want to change this, for both appearance and security through obscurity (that is if you dont use and delete the default admin user).', 'wp-graphql' ),
×
51
                                ],
×
52
                                'nickname'    => [
×
53
                                        'type'        => 'String',
×
54
                                        'description' => __( 'The user\'s nickname, defaults to the user\'s username.', 'wp-graphql' ),
×
55
                                ],
×
56
                                'firstName'   => [
×
57
                                        'type'        => 'String',
×
58
                                        'description' => __( '        The user\'s first name.', 'wp-graphql' ),
×
59
                                ],
×
60
                                'lastName'    => [
×
61
                                        'type'        => 'String',
×
62
                                        'description' => __( 'The user\'s last name.', 'wp-graphql' ),
×
63
                                ],
×
64
                                'description' => [
×
65
                                        'type'        => 'String',
×
66
                                        'description' => __( 'A string containing content about the user.', 'wp-graphql' ),
×
67
                                ],
×
68
                                'richEditing' => [
×
69
                                        'type'        => 'String',
×
70
                                        'description' => __( 'A string for whether to enable the rich editor or not. False if not empty.', 'wp-graphql' ),
×
71
                                ],
×
72
                                'registered'  => [
×
73
                                        'type'        => 'String',
×
74
                                        'description' => __( 'The date the user registered. Format is Y-m-d H:i:s.', 'wp-graphql' ),
×
75
                                ],
×
76
                                'roles'       => [
×
77
                                        'type'        => [ 'list_of' => 'String' ],
×
78
                                        'description' => __( 'An array of roles to be assigned to the user.', 'wp-graphql' ),
×
79
                                ],
×
80
                                'jabber'      => [
×
81
                                        'type'        => 'String',
×
82
                                        'description' => __( 'User\'s Jabber account.', 'wp-graphql' ),
×
83
                                ],
×
84
                                'aim'         => [
×
85
                                        'type'        => 'String',
×
86
                                        'description' => __( 'User\'s AOL IM account.', 'wp-graphql' ),
×
87
                                ],
×
88
                                'yim'         => [
×
89
                                        'type'        => 'String',
×
90
                                        'description' => __( 'User\'s Yahoo IM account.', 'wp-graphql' ),
×
91
                                ],
×
92
                                'locale'      => [
×
93
                                        'type'        => 'String',
×
94
                                        'description' => __( 'User\'s locale.', 'wp-graphql' ),
×
95
                                ],
×
96
                        ];
×
97

98
                        /**
99
                         * Filters all of the fields available for input
100
                         *
101
                         * @param array<string,array<string,mixed>> $input_fields
102
                         */
103
                        self::$input_fields = apply_filters( 'graphql_user_mutation_input_fields', $input_fields );
×
104
                }
105

106
                return ( ! empty( self::$input_fields ) ) ? self::$input_fields : null;
×
107
        }
108

109
        /**
110
         * Maps the GraphQL input to a format that the WordPress functions can use
111
         *
112
         * @param array<string,mixed> $input         Data coming from the GraphQL mutation query input
113
         * @param string              $mutation_name Name of the mutation being performed
114
         *
115
         * @return array<string,mixed>
116
         * @throws \GraphQL\Error\UserError If the passed email address is invalid.
117
         */
118
        public static function prepare_user_object( $input, $mutation_name ) {
9✔
119
                $insert_user_args = [];
9✔
120

121
                /**
122
                 * Optional fields
123
                 */
124
                if ( isset( $input['nicename'] ) ) {
9✔
125
                        $insert_user_args['user_nicename'] = $input['nicename'];
1✔
126
                }
127

128
                if ( isset( $input['websiteUrl'] ) ) {
9✔
129
                        $insert_user_args['user_url'] = esc_url( $input['websiteUrl'] );
1✔
130
                }
131

132
                if ( isset( $input['displayName'] ) ) {
9✔
133
                        $insert_user_args['display_name'] = $input['displayName'];
1✔
134
                }
135

136
                if ( isset( $input['nickname'] ) ) {
9✔
137
                        $insert_user_args['nickname'] = $input['nickname'];
1✔
138
                }
139

140
                if ( isset( $input['firstName'] ) ) {
9✔
141
                        $insert_user_args['first_name'] = $input['firstName'];
7✔
142
                }
143

144
                if ( isset( $input['lastName'] ) ) {
9✔
145
                        $insert_user_args['last_name'] = $input['lastName'];
7✔
146
                }
147

148
                if ( isset( $input['description'] ) ) {
9✔
149
                        $insert_user_args['description'] = $input['description'];
1✔
150
                }
151

152
                if ( isset( $input['richEditing'] ) ) {
9✔
153
                        $insert_user_args['rich_editing'] = $input['richEditing'];
×
154
                }
155

156
                if ( isset( $input['registered'] ) ) {
9✔
157
                        $insert_user_args['user_registered'] = $input['registered'];
1✔
158
                }
159

160
                if ( isset( $input['locale'] ) ) {
9✔
161
                        $insert_user_args['locale'] = $input['locale'];
1✔
162
                }
163

164
                /**
165
                 * Required fields
166
                 */
167
                if ( ! empty( $input['email'] ) ) {
9✔
168
                        if ( false === is_email( apply_filters( 'pre_user_email', $input['email'] ) ) ) {
7✔
169
                                throw new UserError( esc_html__( 'The email address you are trying to use is invalid', 'wp-graphql' ) );
1✔
170
                        }
171
                        $insert_user_args['user_email'] = $input['email'];
6✔
172
                }
173

174
                if ( ! empty( $input['password'] ) ) {
8✔
175
                        $insert_user_args['user_pass'] = $input['password'];
1✔
176
                } else {
177
                        $insert_user_args['user_pass'] = null;
7✔
178
                }
179

180
                if ( ! empty( $input['username'] ) ) {
8✔
181
                        $insert_user_args['user_login'] = $input['username'];
6✔
182
                }
183

184
                if ( ! empty( $input['roles'] ) ) {
8✔
185
                        /**
186
                         * Pluck the first role out of the array since the insert and update functions only
187
                         * allow one role to be set at a time. We will add all of the roles passed to the
188
                         * mutation later on after the initial object has been created or updated.
189
                         */
190
                        $insert_user_args['role'] = $input['roles'][0];
6✔
191
                }
192

193
                /**
194
                 * Filters the mappings for input to arguments
195
                 *
196
                 * @param array<string,mixed> $insert_user_args The arguments to ultimately be passed to the WordPress function
197
                 * @param array<string,mixed> $input            Input data from the GraphQL mutation
198
                 * @param string              $mutation_name    What user mutation is being performed for context
199
                 */
200
                $insert_user_args = apply_filters( 'graphql_user_insert_post_args', $insert_user_args, $input, $mutation_name );
8✔
201

202
                return $insert_user_args;
8✔
203
        }
204

205
        /**
206
         * This updates additional data related to the user object after the initial mutation has
207
         * happened
208
         *
209
         * @param int                                  $user_id       The ID of the user being mutated
210
         * @param array<string,mixed>                  $input         The input data from the GraphQL query
211
         * @param string                               $mutation_name Name of the mutation currently being run
212
         * @param \WPGraphQL\AppContext                $context The AppContext passed down the resolve tree
213
         * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the Resolve Tree
214
         *
215
         * @return void
216
         * @throws \Exception
217
         */
218
        public static function update_additional_user_object_data( $user_id, $input, $mutation_name, AppContext $context, ResolveInfo $info ) {
6✔
219
                $roles = ! empty( $input['roles'] ) ? $input['roles'] : [];
6✔
220
                self::add_user_roles( $user_id, $roles );
6✔
221

222
                /**
223
                 * Run an action after the additional data has been updated. This is a great spot to hook into to
224
                 * update additional data related to users, such as setting relationships, updating additional usermeta,
225
                 * or sending emails to Kevin... whatever you need to do with the userObject.
226
                 *
227
                 * @param int                                  $user_id       The ID of the user being mutated
228
                 * @param array<string,mixed>                  $input         The input for the mutation
229
                 * @param string                               $mutation_name The name of the mutation (ex: create, update, delete)
230
                 * @param \WPGraphQL\AppContext                $context       The AppContext passed down the resolve tree
231
                 * @param \GraphQL\Type\Definition\ResolveInfo $info          The ResolveInfo passed down the Resolve Tree
232
                 */
233
                do_action( 'graphql_user_object_mutation_update_additional_data', $user_id, $input, $mutation_name, $context, $info );
5✔
234
        }
235

236
        /**
237
         * Method to add user roles to a user object
238
         *
239
         * @param int      $user_id The ID of the user
240
         * @param string[] $roles   List of roles that need to get added to the user
241
         *
242
         * @return void
243
         * @throws \Exception
244
         */
245
        private static function add_user_roles( $user_id, $roles ) {
6✔
246
                if ( empty( $roles ) || ! is_array( $roles ) || ! current_user_can( 'edit_user', $user_id ) ) {
6✔
247
                        return;
3✔
248
                }
249

250
                $user = get_user_by( 'ID', $user_id );
4✔
251

252
                if ( false !== $user ) {
4✔
253
                        foreach ( $roles as $role ) {
4✔
254
                                $verified = self::verify_user_role( $role, $user_id );
4✔
255

256
                                if ( true === $verified ) {
4✔
257
                                        $user->add_role( $role );
3✔
258
                                } elseif ( is_wp_error( $verified ) ) {
1✔
259
                                        throw new Exception( esc_html( $verified->get_error_message() ) );
1✔
260
                                } elseif ( false === $verified ) {
×
261
                                        // Translators: The placeholder is the name of the user role
262
                                        throw new Exception( esc_html( sprintf( __( 'The %s role cannot be added to this user', 'wp-graphql' ), $role ) ) );
×
263
                                }
264
                        }
265
                }
266
        }
267

268
        /**
269
         * Method to check if the user role is valid, and if the current user has permission to add, or
270
         * remove it from a user.
271
         *
272
         * @param string $role    Name of the role trying to get added to a user object
273
         * @param int    $user_id The ID of the user being mutated
274
         *
275
         * @return bool|\WP_Error
276
         */
277
        private static function verify_user_role( $role, $user_id ) {
4✔
278
                global $wp_roles;
4✔
279

280
                $potential_role = isset( $wp_roles->role_objects[ $role ] ) ? $wp_roles->role_objects[ $role ] : '';
4✔
281

282
                if ( empty( $wp_roles->role_objects[ $role ] ) ) {
4✔
283
                        // Translators: The placeholder is the name of the user role
284
                        return new \WP_Error( 'wpgraphql_user_invalid_role', sprintf( __( 'The role %s does not exist', 'wp-graphql' ), $role ) );
1✔
285
                }
286

287
                /*
288
                 * Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
289
                 * Multisite super admins can freely edit their blog roles -- they possess all caps.
290
                 */
291
                if (
292
                        ! ( is_multisite() && current_user_can( 'manage_sites' ) ) &&
3✔
293
                        get_current_user_id() === $user_id &&
3✔
294
                        ! $potential_role->has_cap( 'edit_users' )
3✔
295
                ) {
296
                        return new \WP_Error( 'wpgraphql_user_invalid_role', __( 'Sorry, you cannot remove user editing permissions for your own account.', 'wp-graphql' ) );
×
297
                }
298

299
                /**
300
                 * The function for this is only loaded on admin pages. See note: https://codex.wordpress.org/Function_Reference/get_editable_roles#Notes
301
                 */
302
                if ( ! function_exists( 'get_editable_roles' ) ) {
3✔
303
                        // @phpstan-ignore requireOnce.fileNotFound
UNCOV
304
                        require_once ABSPATH . 'wp-admin/includes/admin.php';
×
305
                }
306

307
                $editable_roles = get_editable_roles();
3✔
308

309
                if ( empty( $editable_roles[ $role ] ) ) {
3✔
310
                        // Translators: %s is the name of the role that can't be added to the user.
311
                        return new \WP_Error( 'wpgraphql_user_invalid_role', sprintf( __( 'Sorry, you are not allowed to give this the following role: %s.', 'wp-graphql' ), $role ) );
×
312
                } else {
313
                        return true;
3✔
314
                }
315
        }
316
}
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