PluginProbe
WPGraphQL / 2.22.0
WPGraphQL v2.22.0
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 / PostObjectMutation.php

PostObjectMutation.php in WPGraphQL 2.22.0, at src/Data/PostObjectMutation.php

520 lines 19.9 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\Type\Definition\ResolveInfo;
6 use GraphQLRelay\Relay;
7 use WPGraphQL\AppContext;
8 use WPGraphQL\Utils\Utils;
9 use WP_Post_Type;
10
11 /**
12 * Class PostObjectMutation
13 *
14 * @package WPGraphQL\Type\PostObject
15 */
16 class PostObjectMutation {
17
18 /**
19 * This handles inserting the post object
20 *
21 * @param array<string,mixed> $input The input for the mutation
22 * @param \WP_Post_Type $post_type_object The post_type_object for the type of post being mutated
23 * @param string $mutation_name The name of the mutation being performed
24 *
25 * @return array<string,mixed>
26 * @throws \Exception
27 */
28 public static function prepare_post_object( $input, $post_type_object, $mutation_name ) {
29 $insert_post_args = [];
30
31 /**
32 * Set the post_type for the insert
33 */
34 $insert_post_args['post_type'] = $post_type_object->name;
35
36 /**
37 * Prepare the data for inserting the post
38 * NOTE: These are organized in the same order as: https://developer.wordpress.org/reference/functions/wp_insert_post/
39 */
40 if ( ! empty( $input['authorId'] ) ) {
41 $insert_post_args['post_author'] = Utils::get_database_id_from_id( $input['authorId'] );
42 }
43
44 if ( ! empty( $input['date'] ) && false !== strtotime( $input['date'] ) ) {
45 $insert_post_args['post_date'] = gmdate( 'Y-m-d H:i:s', strtotime( $input['date'] ) );
46 }
47
48 if ( ! empty( $input['content'] ) ) {
49 $insert_post_args['post_content'] = $input['content'];
50 }
51
52 if ( ! empty( $input['title'] ) ) {
53 $insert_post_args['post_title'] = $input['title'];
54 }
55
56 if ( ! empty( $input['excerpt'] ) ) {
57 $insert_post_args['post_excerpt'] = $input['excerpt'];
58 }
59
60 if ( ! empty( $input['status'] ) ) {
61 $insert_post_args['post_status'] = $input['status'];
62 }
63
64 if ( ! empty( $input['commentStatus'] ) ) {
65 $insert_post_args['comment_status'] = $input['commentStatus'];
66 }
67
68 if ( ! empty( $input['pingStatus'] ) ) {
69 $insert_post_args['ping_status'] = $input['pingStatus'];
70 }
71
72 if ( ! empty( $input['password'] ) ) {
73 $insert_post_args['post_password'] = $input['password'];
74 }
75
76 if ( ! empty( $input['slug'] ) ) {
77 $insert_post_args['post_name'] = $input['slug'];
78 }
79
80 if ( ! empty( $input['toPing'] ) ) {
81 $insert_post_args['to_ping'] = $input['toPing'];
82 }
83
84 if ( ! empty( $input['pinged'] ) ) {
85 $insert_post_args['pinged'] = $input['pinged'];
86 }
87
88 if ( ! empty( $input['parentId'] ) ) {
89 $insert_post_args['post_parent'] = Utils::get_database_id_from_id( $input['parentId'] );
90 }
91
92 if ( ! empty( $input['menuOrder'] ) ) {
93 $insert_post_args['menu_order'] = $input['menuOrder'];
94 }
95
96 if ( ! empty( $input['mimeType'] ) ) {
97 $insert_post_args['post_mime_type'] = $input['mimeType'];
98 }
99
100 if ( ! empty( $input['commentCount'] ) ) {
101 $insert_post_args['comment_count'] = $input['commentCount'];
102 }
103
104 /**
105 * Filter the $insert_post_args
106 *
107 * @param array<string,mixed> $insert_post_args The array of $input_post_args that will be passed to wp_insert_post
108 * @param array<string,mixed> $input The data that was entered as input for the mutation
109 * @param \WP_Post_Type $post_type_object The post_type_object that the mutation is affecting
110 * @param string $mutation_type The type of mutation being performed (create, edit, etc)
111 *
112 * @hookGroup models
113 * @since 0.0.5
114 */
115 $insert_post_args = apply_filters( 'graphql_post_object_insert_post_args', $insert_post_args, $input, $post_type_object, $mutation_name );
116
117 /**
118 * Return the $args
119 */
120 return $insert_post_args;
121 }
122
123 /**
124 * This updates additional data related to a post object, such as postmeta, term relationships,
125 * etc.
126 *
127 * @param int $post_id The ID of the postObject being mutated
128 * @param array<string,mixed> $input The input for the mutation
129 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
130 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
131 * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
132 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
133 * @param string $default_post_status The default status posts should use if an intended status wasn't set
134 * @param string $intended_post_status The intended post_status the post should have according to the mutation input
135 *
136 * @return void
137 */
138 public static function update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, AppContext $context, ResolveInfo $info, $default_post_status = null, $intended_post_status = null ) {
139
140 /**
141 * Sets the post lock
142 *
143 * @param bool $is_locked Whether the post is locked
144 * @param int $post_id The ID of the postObject being mutated
145 * @param array<string,mixed> $input The input for the mutation
146 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
147 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
148 * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
149 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
150 * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input
151 * @param ?string $default_post_status The default status posts should use if an intended status wasn't set
152 */
153 if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) {
154 /**
155 * Set the post_lock for the $new_post_id
156 */
157 self::set_edit_lock( $post_id );
158 }
159
160 /**
161 * Update the _edit_last field
162 */
163 update_post_meta( $post_id, '_edit_last', get_current_user_id() );
164
165 /**
166 * Update the postmeta fields
167 */
168 if ( ! empty( $input['desiredSlug'] ) ) {
169 update_post_meta( $post_id, '_wp_desired_post_slug', $input['desiredSlug'] );
170 }
171
172 /**
173 * Set the object terms
174 *
175 * @param int $post_id The ID of the postObject being mutated
176 * @param array<string,mixed> $input The input for the mutation
177 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
178 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
179 */
180 self::set_object_terms( $post_id, $input, $post_type_object, $mutation_name );
181
182 /**
183 * Run an action after the additional data has been updated. This is a great spot to hook into to
184 * update additional data related to postObjects, such as setting relationships, updating additional postmeta,
185 * or sending emails to Kevin. . .whatever you need to do with the postObject.
186 *
187 * @param int $post_id The ID of the postObject being mutated
188 * @param array<string,mixed> $input The input for the mutation
189 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
190 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
191 * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
192 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
193 * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input
194 * @param ?string $default_post_status The default status posts should use if an intended status wasn't set
195 *
196 * @hookGroup models
197 * @since 0.0.5
198 */
199 do_action( 'graphql_post_object_mutation_update_additional_data', $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );
200
201 /**
202 * Sets the post lock
203 *
204 * @param bool $is_locked Whether the post is locked.
205 * @param int $post_id The ID of the postObject being mutated
206 * @param array<string,mixed> $input The input for the mutation
207 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
208 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
209 * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
210 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
211 * @param ?string $intended_post_status The intended post_status the post should have according to the mutation input
212 * @param ?string $default_post_status The default status posts should use if an intended status wasn't set
213 *
214 * @return bool
215 */
216 if ( true === apply_filters( 'graphql_post_object_mutation_set_edit_lock', true, $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status ) ) {
217 /**
218 * Set the post_lock for the $new_post_id
219 */
220 self::remove_edit_lock( $post_id );
221 }
222 }
223
224 /**
225 * Given a $post_id and $input from the mutation, check to see if any term associations are
226 * being made, and properly set the relationships
227 *
228 * @param int $post_id The ID of the postObject being mutated
229 * @param array<string,mixed> $input The input for the mutation
230 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
231 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
232 *
233 * @return void
234 */
235 protected static function set_object_terms( int $post_id, array $input, WP_Post_Type $post_type_object, string $mutation_name ) {
236
237 /**
238 * Fire an action before setting object terms during a GraphQL Post Object Mutation.
239 *
240 * One example use for this hook would be to create terms from the input that may not exist yet, so that they can be set as a relation below.
241 *
242 * @param int $post_id The ID of the postObject being mutated
243 * @param array<string,mixed> $input The input for the mutation
244 * @param \WP_Post_Type $post_type_object The Post Type Object for the type of post being mutated
245 * @param string $mutation_name The name of the mutation (ex: create, update, delete)
246 *
247 * @hookGroup models
248 * @since 0.0.5
249 */
250 do_action( 'graphql_post_object_mutation_set_object_terms', $post_id, $input, $post_type_object, $mutation_name );
251
252 /**
253 * Get the allowed taxonomies and iterate through them to find the term inputs to use for setting relationships.
254 */
255 $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' );
256
257 foreach ( $allowed_taxonomies as $tax_object ) {
258
259 /**
260 * If the taxonomy is in the array of taxonomies registered to the post_type
261 */
262 if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
263
264 /**
265 * If there is input for the taxonomy, process it
266 */
267 if ( isset( $input[ lcfirst( $tax_object->graphql_plural_name ) ] ) ) {
268 $term_input = $input[ lcfirst( $tax_object->graphql_plural_name ) ];
269
270 /**
271 * Default append to true, but allow input to set it to false.
272 */
273 $append = ! isset( $term_input['append'] ) || false !== $term_input['append'];
274
275 /**
276 * Start an array of terms to connect
277 */
278 $terms_to_connect = [];
279
280 /**
281 * Filter whether to allow terms to be created during a post mutation.
282 *
283 * If a post mutation includes term input for a term that does not already exist,
284 * this will allow terms to be created in order to connect the term to the post object,
285 * but if filtered to false, this will prevent the term that doesn't already exist
286 * from being created during the mutation of the post.
287 *
288 * @param bool $allow_term_creation Whether new terms should be created during the post object mutation
289 * @param \WP_Taxonomy $tax_object The Taxonomy object for the term being added to the Post Object
290 *
291 * @hookGroup models
292 * @since 0.0.5
293 */
294 $allow_term_creation = apply_filters( 'graphql_post_object_mutations_allow_term_creation', true, $tax_object );
295
296 /**
297 * If there are nodes in the term_input
298 */
299 if ( ! empty( $term_input['nodes'] ) && is_array( $term_input['nodes'] ) ) {
300 foreach ( $term_input['nodes'] as $node ) {
301 $term_exists = false;
302
303 /**
304 * Handle the input for ID first.
305 */
306 if ( ! empty( $node['id'] ) ) {
307 if ( ! absint( $node['id'] ) ) {
308 $id_parts = Relay::fromGlobalId( $node['id'] );
309
310 if ( ! empty( $id_parts['id'] ) ) {
311 $term_exists = get_term_by( 'id', absint( $id_parts['id'] ), $tax_object->name );
312 if ( isset( $term_exists->term_id ) ) {
313 $terms_to_connect[] = $term_exists->term_id;
314 }
315 }
316 } else {
317 $term_exists = get_term_by( 'id', absint( $node['id'] ), $tax_object->name );
318 if ( isset( $term_exists->term_id ) ) {
319 $terms_to_connect[] = $term_exists->term_id;
320 }
321 }
322
323 /**
324 * Next, handle the input for slug if there wasn't an ID input
325 */
326 } elseif ( ! empty( $node['slug'] ) ) {
327 $sanitized_slug = sanitize_text_field( $node['slug'] );
328 $term_exists = get_term_by( 'slug', $sanitized_slug, $tax_object->name );
329 if ( isset( $term_exists->term_id ) ) {
330 $terms_to_connect[] = $term_exists->term_id;
331 }
332
333 /**
334 * Finally, handle the input for name if there wasn't an ID or slug input.
335 *
336 * Matching by name lets users assign existing terms (e.g. a standard
337 * post format like "video") without needing the `edit_terms` capability
338 * that creating a new term requires.
339 */
340 } elseif ( ! empty( $node['name'] ) ) {
341 $sanitized_name = sanitize_text_field( $node['name'] );
342 $term_exists = get_term_by( 'name', $sanitized_name, $tax_object->name );
343 if ( isset( $term_exists->term_id ) ) {
344 $terms_to_connect[] = $term_exists->term_id;
345 }
346 }
347
348 /**
349 * If no term exists so far, and terms are set to be allowed to be created
350 * during a post object mutation, create the term to connect based on the
351 * input
352 */
353 if ( ! $term_exists && true === $allow_term_creation ) {
354
355 /**
356 * If the current user cannot edit terms, don't create a term to connect.
357 *
358 * Skip just this node rather than aborting the whole taxonomy, so that
359 * any existing terms the user matched (and is allowed to assign) are still
360 * connected below.
361 */
362 if ( ! isset( $tax_object->cap->edit_terms ) || ! current_user_can( $tax_object->cap->edit_terms ) ) {
363 continue;
364 }
365
366 $created_term = self::create_term_to_connect( $node, $tax_object->name );
367
368 if ( ! empty( $created_term ) ) {
369 $terms_to_connect[] = $created_term;
370 }
371 }
372 }
373 }
374
375 /**
376 * If the current user cannot assign terms of this taxonomy, skip it
377 * without connecting any of its terms, but keep processing the
378 * remaining taxonomies rather than aborting the whole mutation's
379 * term assignment.
380 */
381 if ( ! isset( $tax_object->cap->assign_terms ) || ! current_user_can( $tax_object->cap->assign_terms ) ) {
382 continue;
383 }
384
385 if ( $append && 'category' === $tax_object->name ) {
386 $default_category_id = absint( get_option( 'default_category' ) );
387 if ( ! in_array( $default_category_id, $terms_to_connect, true ) ) {
388 wp_remove_object_terms( $post_id, $default_category_id, 'category' );
389 }
390 }
391
392 wp_set_object_terms( $post_id, $terms_to_connect, $tax_object->name, $append );
393 }
394 }
395 }
396 }
397
398 /**
399 * Given an array of Term properties (slug, name, description, etc), create the term and return
400 * a term_id
401 *
402 * @param array<string,mixed> $node The node input for the term
403 * @param string $taxonomy The taxonomy the term input is for
404 *
405 * @return int $term_id The ID of the created term. 0 if no term was created.
406 */
407 protected static function create_term_to_connect( $node, $taxonomy ) {
408 $created_term = [];
409 $term_to_create = [];
410 $term_args = [];
411
412 if ( ! empty( $node['name'] ) ) {
413 $term_to_create['name'] = sanitize_text_field( $node['name'] );
414 } elseif ( ! empty( $node['slug'] ) ) {
415 $term_to_create['name'] = sanitize_text_field( $node['slug'] );
416 }
417
418 if ( ! empty( $node['slug'] ) ) {
419 $term_args['slug'] = sanitize_text_field( $node['slug'] );
420 }
421
422 if ( ! empty( $node['description'] ) ) {
423 $term_args['description'] = sanitize_text_field( $node['description'] );
424 }
425
426 /**
427 * @todo: consider supporting "parent" input in $term_args
428 */
429
430 if ( isset( $term_to_create['name'] ) && ! empty( $term_to_create['name'] ) ) {
431 $created_term = wp_insert_term( $term_to_create['name'], $taxonomy, $term_args );
432 }
433
434 if ( is_wp_error( $created_term ) ) {
435 if ( isset( $created_term->error_data['term_exists'] ) ) {
436 return $created_term->error_data['term_exists'];
437 }
438
439 return 0;
440 }
441
442 /**
443 * Return the created term, or 0
444 */
445 return isset( $created_term['term_id'] ) ? absint( $created_term['term_id'] ) : 0;
446 }
447
448 /**
449 * This is a copy of the wp_set_post_lock function that exists in WordPress core, but is not
450 * accessible because that part of WordPress is never loaded for WPGraphQL executions
451 *
452 * Mark the post as currently being edited by the current user
453 *
454 * @param int $post_id ID of the post being edited.
455 *
456 * @return int[]|false Array of the lock time and user ID. False if the post does not exist, or
457 * there is no current user.
458 */
459 public static function set_edit_lock( $post_id ) {
460 $post = get_post( $post_id );
461 $user_id = get_current_user_id();
462
463 if ( empty( $post ) ) {
464 return false;
465 }
466
467 if ( 0 === $user_id ) {
468 return false;
469 }
470
471 $now = time();
472 $lock = "$now:$user_id";
473 update_post_meta( $post->ID, '_edit_lock', $lock );
474
475 return [ $now, $user_id ];
476 }
477
478 /**
479 * Remove the edit lock for a post
480 *
481 * @param int $post_id ID of the post to delete the lock for
482 *
483 * @return bool
484 */
485 public static function remove_edit_lock( int $post_id ) {
486 $post = get_post( $post_id );
487
488 if ( empty( $post ) ) {
489 return false;
490 }
491
492 return delete_post_meta( $post->ID, '_edit_lock' );
493 }
494
495 /**
496 * Check the edit lock for a post
497 *
498 * @param false|int $post_id ID of the post to delete the lock for
499 * @param array<string,mixed> $input The input for the mutation
500 *
501 * @return false|int Return false if no lock or the user_id of the owner of the lock
502 */
503 public static function check_edit_lock( $post_id, array $input ) {
504 if ( false === $post_id ) {
505 return false;
506 }
507
508 // If override the edit lock is set, return early
509 if ( isset( $input['ignoreEditLock'] ) && true === $input['ignoreEditLock'] ) {
510 return false;
511 }
512
513 if ( ! function_exists( 'wp_check_post_lock' ) ) {
514 require_once ABSPATH . 'wp-admin/includes/post.php';
515 }
516
517 return wp_check_post_lock( $post_id );
518 }
519 }
520