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

wp-graphql / wp-graphql / 18790791685

24 Oct 2025 08:03PM UTC coverage: 83.207% (-1.4%) from 84.575%
18790791685

push

github

actions-user
release: merge develop into master for v2.5.0

2 of 4 new or added lines in 2 files covered. (50.0%)

189 existing lines in 10 files now uncovered.

16143 of 19401 relevant lines covered (83.21%)

257.79 hits per line

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

83.97
/src/Data/PostObjectMutation.php
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 ) {
18✔
29
                $insert_post_args = [];
18✔
30

31
                /**
32
                 * Set the post_type for the insert
33
                 */
34
                $insert_post_args['post_type'] = $post_type_object->name;
18✔
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'] ) ) {
18✔
41
                        $insert_post_args['post_author'] = Utils::get_database_id_from_id( $input['authorId'] );
1✔
42
                }
43

44
                if ( ! empty( $input['date'] ) && false !== strtotime( $input['date'] ) ) {
18✔
45
                        $insert_post_args['post_date'] = gmdate( 'Y-m-d H:i:s', strtotime( $input['date'] ) );
3✔
46
                }
47

48
                if ( ! empty( $input['content'] ) ) {
18✔
49
                        $insert_post_args['post_content'] = $input['content'];
5✔
50
                }
51

52
                if ( ! empty( $input['title'] ) ) {
18✔
53
                        $insert_post_args['post_title'] = $input['title'];
18✔
54
                }
55

56
                if ( ! empty( $input['excerpt'] ) ) {
18✔
57
                        $insert_post_args['post_excerpt'] = $input['excerpt'];
×
58
                }
59

60
                if ( ! empty( $input['status'] ) ) {
18✔
61
                        $insert_post_args['post_status'] = $input['status'];
11✔
62
                }
63

64
                if ( ! empty( $input['commentStatus'] ) ) {
18✔
65
                        $insert_post_args['comment_status'] = $input['commentStatus'];
×
66
                }
67

68
                if ( ! empty( $input['pingStatus'] ) ) {
18✔
69
                        $insert_post_args['ping_status'] = $input['pingStatus'];
×
70
                }
71

72
                if ( ! empty( $input['password'] ) ) {
18✔
73
                        $insert_post_args['post_password'] = $input['password'];
×
74
                }
75

76
                if ( ! empty( $input['slug'] ) ) {
18✔
77
                        $insert_post_args['post_name'] = $input['slug'];
×
78
                }
79

80
                if ( ! empty( $input['toPing'] ) ) {
18✔
81
                        $insert_post_args['to_ping'] = $input['toPing'];
×
82
                }
83

84
                if ( ! empty( $input['pinged'] ) ) {
18✔
85
                        $insert_post_args['pinged'] = $input['pinged'];
×
86
                }
87

88
                if ( ! empty( $input['parentId'] ) ) {
18✔
89
                        $insert_post_args['post_parent'] = Utils::get_database_id_from_id( $input['parentId'] );
1✔
90
                }
91

92
                if ( ! empty( $input['menuOrder'] ) ) {
18✔
93
                        $insert_post_args['menu_order'] = $input['menuOrder'];
×
94
                }
95

96
                if ( ! empty( $input['mimeType'] ) ) {
18✔
97
                        $insert_post_args['post_mime_type'] = $input['mimeType'];
×
98
                }
99

100
                if ( ! empty( $input['commentCount'] ) ) {
18✔
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
                $insert_post_args = apply_filters( 'graphql_post_object_insert_post_args', $insert_post_args, $input, $post_type_object, $mutation_name );
18✔
113

114
                /**
115
                 * Return the $args
116
                 */
117
                return $insert_post_args;
18✔
118
        }
119

120
        /**
121
         * This updates additional data related to a post object, such as postmeta, term relationships,
122
         * etc.
123
         *
124
         * @param int                                  $post_id              The ID of the postObject being mutated
125
         * @param array<string,mixed>                  $input                The input for the mutation
126
         * @param \WP_Post_Type                        $post_type_object     The Post Type Object for the type of post being mutated
127
         * @param string                               $mutation_name        The name of the mutation (ex: create, update, delete)
128
         * @param \WPGraphQL\AppContext                $context              The AppContext passed down to all resolvers
129
         * @param \GraphQL\Type\Definition\ResolveInfo $info                 The ResolveInfo passed down to all resolvers
130
         * @param string                               $default_post_status  The default status posts should use if an intended status wasn't set
131
         * @param string                               $intended_post_status The intended post_status the post should have according to the mutation input
132
         *
133
         * @return void
134
         */
135
        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 ) {
18✔
136

137
                /**
138
                 * Sets the post lock
139
                 *
140
                 * @param bool                                 $is_locked            Whether the post is locked
141
                 * @param int                                  $post_id              The ID of the postObject being mutated
142
                 * @param array<string,mixed>                  $input                The input for the mutation
143
                 * @param \WP_Post_Type                        $post_type_object The Post Type Object for the type of post being mutated
144
                 * @param string                               $mutation_name        The name of the mutation (ex: create, update, delete)
145
                 * @param \WPGraphQL\AppContext                $context The AppContext passed down to all resolvers
146
                 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
147
                 * @param ?string                              $intended_post_status The intended post_status the post should have according to the mutation input
148
                 * @param ?string                              $default_post_status  The default status posts should use if an intended status wasn't set
149
                 */
150
                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 ) ) {
18✔
151
                        /**
152
                         * Set the post_lock for the $new_post_id
153
                         */
154
                        self::set_edit_lock( $post_id );
18✔
155
                }
156

157
                /**
158
                 * Update the _edit_last field
159
                 */
160
                update_post_meta( $post_id, '_edit_last', get_current_user_id() );
18✔
161

162
                /**
163
                 * Update the postmeta fields
164
                 */
165
                if ( ! empty( $input['desiredSlug'] ) ) {
18✔
166
                        update_post_meta( $post_id, '_wp_desired_post_slug', $input['desiredSlug'] );
×
167
                }
168

169
                /**
170
                 * Set the object terms
171
                 *
172
                 * @param int                 $post_id          The ID of the postObject being mutated
173
                 * @param array<string,mixed> $input            The input for the mutation
174
                 * @param \WP_Post_Type       $post_type_object The Post Type Object for the type of post being mutated
175
                 * @param string              $mutation_name    The name of the mutation (ex: create, update, delete)
176
                 */
177
                self::set_object_terms( $post_id, $input, $post_type_object, $mutation_name );
18✔
178

179
                /**
180
                 * Run an action after the additional data has been updated. This is a great spot to hook into to
181
                 * update additional data related to postObjects, such as setting relationships, updating additional postmeta,
182
                 * or sending emails to Kevin. . .whatever you need to do with the postObject.
183
                 *
184
                 * @param int                                  $post_id              The ID of the postObject being mutated
185
                 * @param array<string,mixed>                  $input                The input for the mutation
186
                 * @param \WP_Post_Type                        $post_type_object     The Post Type Object for the type of post being mutated
187
                 * @param string                               $mutation_name        The name of the mutation (ex: create, update, delete)
188
                 * @param \WPGraphQL\AppContext                $context              The AppContext passed down to all resolvers
189
                 * @param \GraphQL\Type\Definition\ResolveInfo $info                 The ResolveInfo passed down to all resolvers
190
                 * @param ?string                              $intended_post_status The intended post_status the post should have according to the mutation input
191
                 * @param ?string                              $default_post_status  The default status posts should use if an intended status wasn't set
192
                 */
193
                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 );
18✔
194

195
                /**
196
                 * Sets the post lock
197
                 *
198
                 * @param bool                                 $is_locked            Whether the post is locked.
199
                 * @param int                                  $post_id              The ID of the postObject being mutated
200
                 * @param array<string,mixed>                  $input                The input for the mutation
201
                 * @param \WP_Post_Type                        $post_type_object     The Post Type Object for the type of post being mutated
202
                 * @param string                               $mutation_name        The name of the mutation (ex: create, update, delete)
203
                 * @param \WPGraphQL\AppContext                $context              The AppContext passed down to all resolvers
204
                 * @param \GraphQL\Type\Definition\ResolveInfo $info                 The ResolveInfo passed down to all resolvers
205
                 * @param ?string                              $intended_post_status The intended post_status the post should have according to the mutation input
206
                 * @param ?string                              $default_post_status  The default status posts should use if an intended status wasn't set
207
                 *
208
                 * @return bool
209
                 */
210
                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 ) ) {
18✔
211
                        /**
212
                         * Set the post_lock for the $new_post_id
213
                         */
214
                        self::remove_edit_lock( $post_id );
18✔
215
                }
216
        }
217

218
        /**
219
         * Given a $post_id and $input from the mutation, check to see if any term associations are
220
         * being made, and properly set the relationships
221
         *
222
         * @param int                 $post_id           The ID of the postObject being mutated
223
         * @param array<string,mixed> $input             The input for the mutation
224
         * @param \WP_Post_Type       $post_type_object The Post Type Object for the type of post being mutated
225
         * @param string              $mutation_name     The name of the mutation (ex: create, update, delete)
226
         *
227
         * @return void
228
         */
229
        protected static function set_object_terms( int $post_id, array $input, WP_Post_Type $post_type_object, string $mutation_name ) {
18✔
230

231
                /**
232
                 * Fire an action before setting object terms during a GraphQL Post Object Mutation.
233
                 *
234
                 * 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.
235
                 *
236
                 * @param int                 $post_id          The ID of the postObject being mutated
237
                 * @param array<string,mixed> $input            The input for the mutation
238
                 * @param \WP_Post_Type       $post_type_object The Post Type Object for the type of post being mutated
239
                 * @param string              $mutation_name    The name of the mutation (ex: create, update, delete)
240
                 */
241
                do_action( 'graphql_post_object_mutation_set_object_terms', $post_id, $input, $post_type_object, $mutation_name );
18✔
242

243
                /**
244
                 * Get the allowed taxonomies and iterate through them to find the term inputs to use for setting relationships.
245
                 */
246
                $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' );
18✔
247

248
                foreach ( $allowed_taxonomies as $tax_object ) {
18✔
249

250
                        /**
251
                         * If the taxonomy is in the array of taxonomies registered to the post_type
252
                         */
253
                        if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
18✔
254

255
                                /**
256
                                 * If there is input for the taxonomy, process it
257
                                 */
258
                                if ( isset( $input[ lcfirst( $tax_object->graphql_plural_name ) ] ) ) {
15✔
259
                                        $term_input = $input[ lcfirst( $tax_object->graphql_plural_name ) ];
6✔
260

261
                                        /**
262
                                         * Default append to true, but allow input to set it to false.
263
                                         */
264
                                        $append = ! isset( $term_input['append'] ) || false !== $term_input['append'];
6✔
265

266
                                        /**
267
                                         * Start an array of terms to connect
268
                                         */
269
                                        $terms_to_connect = [];
6✔
270

271
                                        /**
272
                                         * Filter whether to allow terms to be created during a post mutation.
273
                                         *
274
                                         * If a post mutation includes term input for a term that does not already exist,
275
                                         * this will allow terms to be created in order to connect the term to the post object,
276
                                         * but if filtered to false, this will prevent the term that doesn't already exist
277
                                         * from being created during the mutation of the post.
278
                                         *
279
                                         * @param bool         $allow_term_creation Whether new terms should be created during the post object mutation
280
                                         * @param \WP_Taxonomy $tax_object          The Taxonomy object for the term being added to the Post Object
281
                                         */
282
                                        $allow_term_creation = apply_filters( 'graphql_post_object_mutations_allow_term_creation', true, $tax_object );
6✔
283

284
                                        /**
285
                                         * If there are nodes in the term_input
286
                                         */
287
                                        if ( ! empty( $term_input['nodes'] ) && is_array( $term_input['nodes'] ) ) {
6✔
288
                                                foreach ( $term_input['nodes'] as $node ) {
6✔
289
                                                        $term_exists = false;
6✔
290

291
                                                        /**
292
                                                         * Handle the input for ID first.
293
                                                         */
294
                                                        if ( ! empty( $node['id'] ) ) {
6✔
295
                                                                if ( ! absint( $node['id'] ) ) {
3✔
296
                                                                        $id_parts = Relay::fromGlobalId( $node['id'] );
2✔
297

298
                                                                        if ( ! empty( $id_parts['id'] ) ) {
2✔
299
                                                                                $term_exists = get_term_by( 'id', absint( $id_parts['id'] ), $tax_object->name );
2✔
300
                                                                                if ( isset( $term_exists->term_id ) ) {
2✔
301
                                                                                        $terms_to_connect[] = $term_exists->term_id;
1✔
302
                                                                                }
303
                                                                        }
304
                                                                } else {
305
                                                                        $term_exists = get_term_by( 'id', absint( $node['id'] ), $tax_object->name );
1✔
306
                                                                        if ( isset( $term_exists->term_id ) ) {
1✔
307
                                                                                $terms_to_connect[] = $term_exists->term_id;
1✔
308
                                                                        }
309
                                                                }
310

311
                                                                /**
312
                                                                 * Next, handle the input for slug if there wasn't an ID input
313
                                                                 */
314
                                                        } elseif ( ! empty( $node['slug'] ) ) {
3✔
315
                                                                $sanitized_slug = sanitize_text_field( $node['slug'] );
3✔
316
                                                                $term_exists    = get_term_by( 'slug', $sanitized_slug, $tax_object->name );
3✔
317
                                                                if ( isset( $term_exists->term_id ) ) {
3✔
318
                                                                        $terms_to_connect[] = $term_exists->term_id;
2✔
319
                                                                }
320
                                                                /**
321
                                                                 * If the input for the term isn't an existing term, check to make sure
322
                                                                 * we're allowed to create new terms during a Post Object mutation
323
                                                                 */
324
                                                        }
325

326
                                                        /**
327
                                                         * If no term exists so far, and terms are set to be allowed to be created
328
                                                         * during a post object mutation, create the term to connect based on the
329
                                                         * input
330
                                                         */
331
                                                        if ( ! $term_exists && true === $allow_term_creation ) {
6✔
332

333
                                                                /**
334
                                                                 * If the current user cannot edit terms, don't create terms to connect
335
                                                                 */
336
                                                                if ( ! isset( $tax_object->cap->edit_terms ) || ! current_user_can( $tax_object->cap->edit_terms ) ) {
2✔
337
                                                                        return;
×
338
                                                                }
339

340
                                                                $created_term = self::create_term_to_connect( $node, $tax_object->name );
2✔
341

342
                                                                if ( ! empty( $created_term ) ) {
2✔
343
                                                                        $terms_to_connect[] = $created_term;
1✔
344
                                                                }
345
                                                        }
346
                                                }
347
                                        }
348

349
                                        /**
350
                                         * If the current user cannot edit terms, don't create terms to connect
351
                                         */
352
                                        if ( ! isset( $tax_object->cap->assign_terms ) || ! current_user_can( $tax_object->cap->assign_terms ) ) {
6✔
353
                                                return;
×
354
                                        }
355

356
                                        if ( $append && 'category' === $tax_object->name ) {
6✔
357
                                                $default_category_id = absint( get_option( 'default_category' ) );
1✔
358
                                                if ( ! in_array( $default_category_id, $terms_to_connect, true ) ) {
1✔
359
                                                        wp_remove_object_terms( $post_id, $default_category_id, 'category' );
1✔
360
                                                }
361
                                        }
362

363
                                        wp_set_object_terms( $post_id, $terms_to_connect, $tax_object->name, $append );
6✔
364
                                }
365
                        }
366
                }
367
        }
368

369
        /**
370
         * Given an array of Term properties (slug, name, description, etc), create the term and return
371
         * a term_id
372
         *
373
         * @param array<string,mixed> $node     The node input for the term
374
         * @param string              $taxonomy The taxonomy the term input is for
375
         *
376
         * @return int $term_id The ID of the created term. 0 if no term was created.
377
         */
378
        protected static function create_term_to_connect( $node, $taxonomy ) {
2✔
379
                $created_term   = [];
2✔
380
                $term_to_create = [];
2✔
381
                $term_args      = [];
2✔
382

383
                if ( ! empty( $node['name'] ) ) {
2✔
384
                        $term_to_create['name'] = sanitize_text_field( $node['name'] );
1✔
385
                } elseif ( ! empty( $node['slug'] ) ) {
2✔
386
                        $term_to_create['name'] = sanitize_text_field( $node['slug'] );
1✔
387
                }
388

389
                if ( ! empty( $node['slug'] ) ) {
2✔
390
                        $term_args['slug'] = sanitize_text_field( $node['slug'] );
1✔
391
                }
392

393
                if ( ! empty( $node['description'] ) ) {
2✔
394
                        $term_args['description'] = sanitize_text_field( $node['description'] );
1✔
395
                }
396

397
                /**
398
                 * @todo: consider supporting "parent" input in $term_args
399
                 */
400

401
                if ( isset( $term_to_create['name'] ) && ! empty( $term_to_create['name'] ) ) {
2✔
402
                        $created_term = wp_insert_term( $term_to_create['name'], $taxonomy, $term_args );
1✔
403
                }
404

405
                if ( is_wp_error( $created_term ) ) {
2✔
406
                        if ( isset( $created_term->error_data['term_exists'] ) ) {
×
407
                                return $created_term->error_data['term_exists'];
×
408
                        }
409

410
                        return 0;
×
411
                }
412

413
                /**
414
                 * Return the created term, or 0
415
                 */
416
                return isset( $created_term['term_id'] ) ? absint( $created_term['term_id'] ) : 0;
2✔
417
        }
418

419
        /**
420
         * This is a copy of the wp_set_post_lock function that exists in WordPress core, but is not
421
         * accessible because that part of WordPress is never loaded for WPGraphQL executions
422
         *
423
         * Mark the post as currently being edited by the current user
424
         *
425
         * @param int $post_id ID of the post being edited.
426
         *
427
         * @return int[]|false Array of the lock time and user ID. False if the post does not exist, or
428
         *                     there is no current user.
429
         */
430
        public static function set_edit_lock( $post_id ) {
18✔
431
                $post    = get_post( $post_id );
18✔
432
                $user_id = get_current_user_id();
18✔
433

434
                if ( empty( $post ) ) {
18✔
435
                        return false;
×
436
                }
437

438
                if ( 0 === $user_id ) {
18✔
439
                        return false;
×
440
                }
441

442
                $now  = time();
18✔
443
                $lock = "$now:$user_id";
18✔
444
                update_post_meta( $post->ID, '_edit_lock', $lock );
18✔
445

446
                return [ $now, $user_id ];
18✔
447
        }
448

449
        /**
450
         * Remove the edit lock for a post
451
         *
452
         * @param int $post_id ID of the post to delete the lock for
453
         *
454
         * @return bool
455
         */
456
        public static function remove_edit_lock( int $post_id ) {
18✔
457
                $post = get_post( $post_id );
18✔
458

459
                if ( empty( $post ) ) {
18✔
460
                        return false;
×
461
                }
462

463
                return delete_post_meta( $post->ID, '_edit_lock' );
18✔
464
        }
465

466
        /**
467
         * Check the edit lock for a post
468
         *
469
         * @param false|int           $post_id ID of the post to delete the lock for
470
         * @param array<string,mixed> $input             The input for the mutation
471
         *
472
         * @return false|int Return false if no lock or the user_id of the owner of the lock
473
         */
474
        public static function check_edit_lock( $post_id, array $input ) {
5✔
475
                if ( false === $post_id ) {
5✔
476
                        return false;
×
477
                }
478

479
                // If override the edit lock is set, return early
480
                if ( isset( $input['ignoreEditLock'] ) && true === $input['ignoreEditLock'] ) {
5✔
481
                        return false;
2✔
482
                }
483

484
                if ( ! function_exists( 'wp_check_post_lock' ) ) {
5✔
UNCOV
485
                        require_once ABSPATH . 'wp-admin/includes/post.php';
×
486
                }
487

488
                return wp_check_post_lock( $post_id );
5✔
489
        }
490
}
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

© 2025 Coveralls, Inc