PluginProbe
WPGraphQL / 2.22.1
WPGraphQL v2.22.1
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 / UserMutation.php

UserMutation.php in WPGraphQL 2.22.1, at src/Data/UserMutation.php

382 lines 13.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 WPGraphQL\AppContext;
8
9 /**
10 * Class UserMutation
11 *
12 * @package WPGraphQL\Type\User\Mutation
13 */
14 class UserMutation {
15
16 /**
17 * Stores the input fields static definition
18 *
19 * @var array<string,array<string,mixed>>
20 */
21 private static $input_fields = [];
22
23 /**
24 * Defines the accepted input arguments
25 *
26 * @return array<string,array<string,mixed>>|null
27 */
28 public static function input_fields() {
29 if ( empty( self::$input_fields ) ) {
30 $input_fields = [
31 'password' => [
32 'type' => 'String',
33 'description' => static function () {
34 return __( 'A string that contains the plain text password for the user.', 'wp-graphql' );
35 },
36 ],
37 'nicename' => [
38 'type' => 'String',
39 'description' => static function () {
40 return __( 'A string that contains a URL-friendly name for the user. The default is the user\'s username.', 'wp-graphql' );
41 },
42 ],
43 'websiteUrl' => [
44 'type' => 'String',
45 'description' => static function () {
46 return __( 'A string containing the user\'s URL for the user\'s web site.', 'wp-graphql' );
47 },
48 ],
49 'email' => [
50 'type' => 'String',
51 'description' => static function () {
52 return __( 'A string containing the user\'s email address.', 'wp-graphql' );
53 },
54 ],
55 'displayName' => [
56 'type' => 'String',
57 'description' => static function () {
58 return __( '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' );
59 },
60 ],
61 'nickname' => [
62 'type' => 'String',
63 'description' => static function () {
64 return __( 'The user\'s nickname, defaults to the user\'s username.', 'wp-graphql' );
65 },
66 ],
67 'firstName' => [
68 'type' => 'String',
69 'description' => static function () {
70 return __( 'The user\'s first name.', 'wp-graphql' );
71 },
72 ],
73 'lastName' => [
74 'type' => 'String',
75 'description' => static function () {
76 return __( 'The user\'s last name.', 'wp-graphql' );
77 },
78 ],
79 'description' => [
80 'type' => 'String',
81 'description' => static function () {
82 return __( 'A string containing content about the user.', 'wp-graphql' );
83 },
84 ],
85 'richEditing' => [
86 'type' => 'String',
87 'description' => static function () {
88 return __( 'A string for whether to enable the rich editor or not. False if not empty.', 'wp-graphql' );
89 },
90 ],
91 'registered' => [
92 'type' => 'String',
93 'description' => static function () {
94 return __( 'The date the user registered. Format is Y-m-d H:i:s.', 'wp-graphql' );
95 },
96 ],
97 'roles' => [
98 'type' => [ 'list_of' => 'String' ],
99 'description' => static function () {
100 return __( 'An array of roles to be assigned to the user.', 'wp-graphql' );
101 },
102 ],
103 'jabber' => [
104 'type' => 'String',
105 'description' => static function () {
106 return __( 'User\'s Jabber account.', 'wp-graphql' );
107 },
108 ],
109 'aim' => [
110 'type' => 'String',
111 'description' => static function () {
112 return __( 'User\'s AOL IM account.', 'wp-graphql' );
113 },
114 ],
115 'yim' => [
116 'type' => 'String',
117 'description' => static function () {
118 return __( 'User\'s Yahoo IM account.', 'wp-graphql' );
119 },
120 ],
121 'locale' => [
122 'type' => 'String',
123 'description' => static function () {
124 return __( 'User\'s locale.', 'wp-graphql' );
125 },
126 ],
127 ];
128
129 /**
130 * Filters all of the fields available for input
131 *
132 * @param array<string,array<string,mixed>> $input_fields The fields available as user mutation input.
133 *
134 * @hookGroup models
135 * @since 0.0.5
136 */
137 self::$input_fields = apply_filters( 'graphql_user_mutation_input_fields', $input_fields );
138 }
139
140 return ( ! empty( self::$input_fields ) ) ? self::$input_fields : null;
141 }
142
143 /**
144 * Maps the GraphQL input to a format that the WordPress functions can use
145 *
146 * @param array<string,mixed> $input Data coming from the GraphQL mutation query input
147 * @param string $mutation_name Name of the mutation being performed
148 * @param int $user_id The ID of the user being updated, or 0 when creating a new user. Used to validate role assignments against the current user's capabilities.
149 *
150 * @return array<string,mixed>
151 * @throws \GraphQL\Error\UserError If the passed email address is invalid, or the current user is not allowed to assign one of the requested roles.
152 */
153 public static function prepare_user_object( $input, $mutation_name, $user_id = 0 ) {
154 $insert_user_args = [];
155
156 /**
157 * Optional fields
158 */
159 if ( isset( $input['nicename'] ) ) {
160 $insert_user_args['user_nicename'] = $input['nicename'];
161 }
162
163 if ( isset( $input['websiteUrl'] ) ) {
164 $insert_user_args['user_url'] = esc_url( $input['websiteUrl'] );
165 }
166
167 if ( isset( $input['displayName'] ) ) {
168 $insert_user_args['display_name'] = $input['displayName'];
169 }
170
171 if ( isset( $input['nickname'] ) ) {
172 $insert_user_args['nickname'] = $input['nickname'];
173 }
174
175 if ( isset( $input['firstName'] ) ) {
176 $insert_user_args['first_name'] = $input['firstName'];
177 }
178
179 if ( isset( $input['lastName'] ) ) {
180 $insert_user_args['last_name'] = $input['lastName'];
181 }
182
183 if ( isset( $input['description'] ) ) {
184 $insert_user_args['description'] = $input['description'];
185 }
186
187 if ( isset( $input['richEditing'] ) ) {
188 $insert_user_args['rich_editing'] = $input['richEditing'];
189 }
190
191 if ( isset( $input['registered'] ) ) {
192 $insert_user_args['user_registered'] = $input['registered'];
193 }
194
195 if ( isset( $input['locale'] ) ) {
196 $insert_user_args['locale'] = $input['locale'];
197 }
198
199 if ( ! empty( $input['email'] ) ) {
200 if ( false === is_email( apply_filters( 'pre_user_email', $input['email'] ) ) ) {
201 throw new UserError( esc_html__( 'The email address you are trying to use is invalid', 'wp-graphql' ) );
202 }
203 $insert_user_args['user_email'] = $input['email'];
204 }
205
206 if ( ! empty( $input['password'] ) ) {
207 $insert_user_args['user_pass'] = $input['password'];
208 } elseif ( ! empty( $input['is_create'] ) ) {
209 // WP 6.9+ requires a password when creating users.
210 // Generate a random one if not provided - user can reset it later.
211 // Only generate password for create operations, not updates.
212 // The $input['is_create'] flag is set by the calling mutation to indicate
213 // whether this is a create (true) or update (false/not set) operation.
214 $insert_user_args['user_pass'] = wp_generate_password();
215 }
216
217 if ( ! empty( $input['username'] ) ) {
218 $insert_user_args['user_login'] = $input['username'];
219 }
220
221 if ( ! empty( $input['roles'] ) ) {
222 $roles = is_array( $input['roles'] ) ? $input['roles'] : [ $input['roles'] ];
223
224 // Validate every requested role before any of them is written to the database.
225 //
226 // wp_insert_user()/wp_update_user() apply the `role` argument immediately and
227 // unconditionally, without consulting the current user's editable roles. If we
228 // only validated afterward (in add_user_roles()), a privileged role placed first
229 // in the array would already be persisted even though the mutation then errors.
230 // Validating here ensures an unauthorized role never reaches the database.
231 foreach ( $roles as $role ) {
232 $verified = self::verify_user_role( $role, $user_id );
233
234 if ( is_wp_error( $verified ) ) {
235 throw new UserError( esc_html( $verified->get_error_message() ) );
236 } elseif ( true !== $verified ) {
237 // Translators: The placeholder is the name of the user role.
238 throw new UserError( esc_html( sprintf( __( 'The %s role cannot be added to this user', 'wp-graphql' ), $role ) ) );
239 }
240 }
241
242 /**
243 * Pluck the first role out of the array since the insert and update functions only
244 * allow one role to be set at a time. We will add all of the roles passed to the
245 * mutation later on after the initial object has been created or updated.
246 */
247 $insert_user_args['role'] = $roles[0];
248 }
249
250 /**
251 * Filters the mappings for input to arguments.
252 *
253 * This is a trusted server-side extension point and runs after the role
254 * validation above. A callback that sets `role` bypasses that validation by
255 * design; it is not reachable from the GraphQL input surface.
256 *
257 * @param array<string,mixed> $insert_user_args The arguments to ultimately be passed to the WordPress function
258 * @param array<string,mixed> $input Input data from the GraphQL mutation
259 * @param string $mutation_name What user mutation is being performed for context
260 *
261 * @hookGroup models
262 * @since 0.0.5
263 */
264 $insert_user_args = apply_filters( 'graphql_user_insert_post_args', $insert_user_args, $input, $mutation_name );
265
266 return $insert_user_args;
267 }
268
269 /**
270 * This updates additional data related to the user object after the initial mutation has
271 * happened
272 *
273 * @param int $user_id The ID of the user being mutated
274 * @param array<string,mixed> $input The input data from the GraphQL query
275 * @param string $mutation_name Name of the mutation currently being run
276 * @param \WPGraphQL\AppContext $context The AppContext passed down the resolve tree
277 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the Resolve Tree
278 *
279 * @return void
280 * @throws \Exception
281 */
282 public static function update_additional_user_object_data( $user_id, $input, $mutation_name, AppContext $context, ResolveInfo $info ) {
283 $roles = ! empty( $input['roles'] ) ? $input['roles'] : [];
284 self::add_user_roles( $user_id, $roles );
285
286 /**
287 * Run an action after the additional data has been updated. This is a great spot to hook into to
288 * update additional data related to users, such as setting relationships, updating additional usermeta,
289 * or sending emails to Kevin... whatever you need to do with the userObject.
290 *
291 * @param int $user_id The ID of the user being mutated
292 * @param array<string,mixed> $input The input for the mutation
293 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
294 * @param \WPGraphQL\AppContext $context The AppContext passed down the resolve tree
295 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the Resolve Tree
296 *
297 * @hookGroup models
298 * @since 0.0.5
299 */
300 do_action( 'graphql_user_object_mutation_update_additional_data', $user_id, $input, $mutation_name, $context, $info );
301 }
302
303 /**
304 * Method to add user roles to a user object
305 *
306 * @param int $user_id The ID of the user
307 * @param string[] $roles List of roles that need to get added to the user
308 *
309 * @throws \GraphQL\Error\UserError
310 */
311 private static function add_user_roles( $user_id, $roles ): void {
312 if ( empty( $roles ) || ! is_array( $roles ) || ! current_user_can( 'edit_user', $user_id ) ) {
313 return;
314 }
315
316 $user = get_user_by( 'ID', $user_id );
317
318 if ( false !== $user ) {
319 foreach ( $roles as $role ) {
320 $verified = self::verify_user_role( $role, $user_id );
321
322 if ( true === $verified ) {
323 $user->add_role( $role );
324 } elseif ( is_wp_error( $verified ) ) {
325 throw new UserError( esc_html( $verified->get_error_message() ) );
326 } elseif ( false === $verified ) {
327 // Translators: The placeholder is the name of the user role
328 throw new UserError( esc_html( sprintf( __( 'The %s role cannot be added to this user', 'wp-graphql' ), $role ) ) );
329 }
330 }
331 }
332 }
333
334 /**
335 * Method to check if the user role is valid, and if the current user has permission to add, or
336 * remove it from a user.
337 *
338 * @param string $role Name of the role trying to get added to a user object
339 * @param int $user_id The ID of the user being mutated
340 *
341 * @return true|\WP_Error
342 */
343 private static function verify_user_role( $role, $user_id ) {
344 global $wp_roles;
345
346 $potential_role = isset( $wp_roles->role_objects[ $role ] ) ? $wp_roles->role_objects[ $role ] : '';
347
348 if ( empty( $wp_roles->role_objects[ $role ] ) ) {
349 // Translators: The placeholder is the name of the user role
350 return new \WP_Error( 'wpgraphql_user_invalid_role', sprintf( __( 'The role %s does not exist', 'wp-graphql' ), $role ) );
351 }
352
353 /*
354 * Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
355 * Multisite super admins can freely edit their blog roles -- they possess all caps.
356 */
357 if (
358 ! ( is_multisite() && current_user_can( 'manage_sites' ) ) &&
359 get_current_user_id() === $user_id &&
360 ! $potential_role->has_cap( 'edit_users' )
361 ) {
362 return new \WP_Error( 'wpgraphql_user_invalid_role', __( 'Sorry, you cannot remove user editing permissions for your own account.', 'wp-graphql' ) );
363 }
364
365 /**
366 * The function for this is only loaded on admin pages. See note: https://codex.wordpress.org/Function_Reference/get_editable_roles#Notes
367 */
368 if ( ! function_exists( 'get_editable_roles' ) ) {
369 require_once ABSPATH . 'wp-admin/includes/admin.php';
370 }
371
372 $editable_roles = get_editable_roles();
373
374 if ( empty( $editable_roles[ $role ] ) ) {
375 // Translators: %s is the name of the role that can't be added to the user.
376 return new \WP_Error( 'wpgraphql_user_invalid_role', sprintf( __( 'Sorry, you are not allowed to give this the following role: %s.', 'wp-graphql' ), $role ) );
377 }
378
379 return true;
380 }
381 }
382