PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
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 / Mutation / PostObjectCreate.php

PostObjectCreate.php in WPGraphQL trunk, at src/Mutation/PostObjectCreate.php

419 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Mutation;
4
5 use GraphQL\Error\UserError;
6 use GraphQL\Type\Definition\ResolveInfo;
7 use WPGraphQL\AppContext;
8 use WPGraphQL\Data\PostObjectMutation;
9 use WPGraphQL\Utils\Utils;
10 use WP_Post_Type;
11
12 /**
13 * Class PostObjectCreate
14 *
15 * @package WPGraphQL\Mutation
16 */
17 class PostObjectCreate {
18 /**
19 * Registers the PostObjectCreate mutation.
20 *
21 * @param \WP_Post_Type $post_type_object The post type of the mutation.
22 *
23 * @return void
24 */
25 public static function register_mutation( WP_Post_Type $post_type_object ) {
26 $mutation_name = 'create' . ucwords( $post_type_object->graphql_single_name );
27
28 register_graphql_mutation(
29 $mutation_name,
30 [
31 'inputFields' => self::get_input_fields( $post_type_object ),
32 'outputFields' => self::get_output_fields( $post_type_object ),
33 'mutateAndGetPayload' => self::mutate_and_get_payload( $post_type_object, $mutation_name ),
34 ]
35 );
36 }
37
38 /**
39 * Defines the mutation input field configuration.
40 *
41 * @param \WP_Post_Type $post_type_object The post type of the mutation.
42 *
43 * @return array<string,array<string,mixed>>
44 */
45 public static function get_input_fields( $post_type_object ) {
46 $fields = [
47 'date' => [
48 'type' => 'String',
49 'description' => static function () {
50 return __( 'The date of the object. Preferable to enter as year/month/day (e.g. 01/31/2017) as it will rearrange date as fit if it is not specified. Incomplete dates may have unintended results for example, "2017" as the input will use current date with timestamp 20:17 ', 'wp-graphql' );
51 },
52 ],
53 'menuOrder' => [
54 'type' => 'Int',
55 'description' => static function () {
56 return __( 'A field used for ordering posts. This is typically used with nav menu items or for special ordering of hierarchical content types.', 'wp-graphql' );
57 },
58 ],
59 'password' => [
60 'type' => 'String',
61 'description' => static function () {
62 return __( 'The password used to protect the content of the object', 'wp-graphql' );
63 },
64 ],
65 'slug' => [
66 'type' => 'String',
67 'description' => static function () {
68 return __( 'The slug of the object', 'wp-graphql' );
69 },
70 ],
71 'status' => [
72 'type' => 'PostStatusEnum',
73 'description' => static function () {
74 return __( 'The status of the object', 'wp-graphql' );
75 },
76 ],
77 ];
78
79 if ( post_type_supports( $post_type_object->name, 'author' ) ) {
80 $fields['authorId'] = [
81 'type' => 'ID',
82 'description' => static function () {
83 return __( 'The userId to assign as the author of the object', 'wp-graphql' );
84 },
85 ];
86 }
87
88 if ( post_type_supports( $post_type_object->name, 'comments' ) ) {
89 $fields['commentStatus'] = [
90 'type' => 'String',
91 'description' => static function () {
92 return __( 'The comment status for the object', 'wp-graphql' );
93 },
94 ];
95 }
96
97 if ( post_type_supports( $post_type_object->name, 'editor' ) ) {
98 $fields['content'] = [
99 'type' => 'String',
100 'description' => static function () {
101 return __( 'The content of the object', 'wp-graphql' );
102 },
103 ];
104 }
105
106 if ( post_type_supports( $post_type_object->name, 'excerpt' ) ) {
107 $fields['excerpt'] = [
108 'type' => 'String',
109 'description' => static function () {
110 return __( 'The excerpt of the object', 'wp-graphql' );
111 },
112 ];
113 }
114
115 if ( post_type_supports( $post_type_object->name, 'title' ) ) {
116 $fields['title'] = [
117 'type' => 'String',
118 'description' => static function () {
119 return __( 'The title of the object', 'wp-graphql' );
120 },
121 ];
122 }
123
124 if ( post_type_supports( $post_type_object->name, 'trackbacks' ) ) {
125 $fields['pinged'] = [
126 'type' => [
127 'list_of' => 'String',
128 ],
129 'description' => static function () {
130 return __( 'URLs that have been pinged.', 'wp-graphql' );
131 },
132 ];
133
134 $fields['pingStatus'] = [
135 'type' => 'String',
136 'description' => static function () {
137 return __( 'The ping status for the object', 'wp-graphql' );
138 },
139 ];
140
141 $fields['toPing'] = [
142 'type' => [
143 'list_of' => 'String',
144 ],
145 'description' => static function () {
146 return __( 'URLs queued to be pinged.', 'wp-graphql' );
147 },
148 ];
149 }
150
151 if ( $post_type_object->hierarchical || in_array(
152 $post_type_object->name,
153 [
154 'attachment',
155 'revision',
156 ],
157 true
158 ) ) {
159 $fields['parentId'] = [
160 'type' => 'ID',
161 'description' => static function () {
162 return __( 'The ID of the parent object', 'wp-graphql' );
163 },
164 ];
165 }
166
167 if ( 'attachment' === $post_type_object->name ) {
168 $fields['mimeType'] = [
169 'type' => 'MimeTypeEnum',
170 'description' => static function () {
171 return __( 'The MIME type of the file for the media item (for example, image/jpeg or application/pdf).', 'wp-graphql' );
172 },
173 ];
174 }
175
176 $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies( 'objects' );
177
178 foreach ( $allowed_taxonomies as $tax_object ) {
179 // If the taxonomy is in the array of taxonomies registered to the post_type
180 if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
181 $fields[ $tax_object->graphql_plural_name ] = [
182 'description' => static function () use ( $post_type_object, $tax_object ) {
183 return sprintf(
184 // translators: %1$s is the post type GraphQL name, %2$s is the taxonomy GraphQL name.
185 __( 'Set connections between the %1$s and %2$s', 'wp-graphql' ),
186 $post_type_object->graphql_single_name,
187 $tax_object->graphql_plural_name
188 );
189 },
190 'type' => ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
191 ];
192 }
193 }
194
195 return $fields;
196 }
197
198 /**
199 * Defines the mutation output field configuration.
200 *
201 * @param \WP_Post_Type $post_type_object The post type of the mutation.
202 *
203 * @return array<string,array<string,mixed>>
204 */
205 public static function get_output_fields( WP_Post_Type $post_type_object ) {
206 return [
207 $post_type_object->graphql_single_name => [
208 'type' => $post_type_object->graphql_single_name,
209 'description' => static function () {
210 return __( 'The Post object mutation type.', 'wp-graphql' );
211 },
212 'resolve' => static function ( $payload, $_args, AppContext $context ) {
213 if ( empty( $payload['postObjectId'] ) || ! absint( $payload['postObjectId'] ) ) {
214 return null;
215 }
216
217 return $context->get_loader( 'post' )->load_deferred( $payload['postObjectId'] );
218 },
219 ],
220 ];
221 }
222
223 /**
224 * Defines the mutation data modification closure.
225 *
226 * @param \WP_Post_Type $post_type_object The post type of the mutation.
227 * @param string $mutation_name The mutation name.
228 *
229 * @return callable(array<string,mixed>$input,\WPGraphQL\AppContext $context,\GraphQL\Type\Definition\ResolveInfo $info):array<string,mixed>
230 */
231 public static function mutate_and_get_payload( $post_type_object, $mutation_name ) {
232 return static function ( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
233
234 /**
235 * Throw an exception if there's no input
236 */
237 if ( ( empty( $post_type_object->name ) ) || ( empty( $input ) || ! is_array( $input ) ) ) {
238 throw new UserError( esc_html__( 'Mutation not processed. There was no input for the mutation or the post_type_object was invalid', 'wp-graphql' ) );
239 }
240
241 /**
242 * Stop now if a user isn't allowed to create a post
243 */
244 if ( ! isset( $post_type_object->cap->create_posts ) || ! current_user_can( $post_type_object->cap->create_posts ) ) {
245 // translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
246 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to create %1$s', 'wp-graphql' ), $post_type_object->graphql_plural_name ) ) );
247 }
248
249 /**
250 * If the post being created is being assigned to another user that's not the current user, make sure
251 * the current user has permission to edit others posts for this post_type
252 */
253 if ( ! empty( $input['authorId'] ) ) {
254 // Ensure authorId is a valid databaseId.
255 $input['authorId'] = Utils::get_database_id_from_id( $input['authorId'] );
256
257 $author = ! empty( $input['authorId'] ) ? get_user_by( 'ID', $input['authorId'] ) : false;
258
259 if ( false === $author ) {
260 throw new UserError( esc_html__( 'The provided `authorId` is not a valid user', 'wp-graphql' ) );
261 }
262
263 if ( get_current_user_id() !== $input['authorId'] && ( ! isset( $post_type_object->cap->edit_others_posts ) || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
264 // translators: the $post_type_object->graphql_plural_name placeholder is the name of the object being mutated
265 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to create %1$s as this user', 'wp-graphql' ), $post_type_object->graphql_plural_name ) ) );
266 }
267 }
268
269 /**
270 * @todo: When we support assigning terms and setting posts as "sticky" we need to check permissions
271 * @see :https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L504-L506
272 * @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L496-L498
273 */
274
275 /**
276 * Insert the post object and get the ID
277 */
278 $post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );
279
280 /**
281 * Filter the default post status to use when the post is initially created. Pass through a filter to
282 * allow other plugins to override the default (for example, Edit Flow, which provides control over
283 * customizing stati or various E-commerce plugins that make heavy use of custom stati)
284 *
285 * @param string $default_status The default status to be used when the post is initially inserted
286 * @param \WP_Post_Type $post_type_object The Post Type that is being inserted
287 * @param string $mutation_name The name of the mutation currently in progress
288 *
289 * @hookGroup models
290 * @since 0.0.5
291 */
292 $default_post_status = apply_filters( 'graphql_post_object_create_default_post_status', 'draft', $post_type_object, $mutation_name );
293
294 /**
295 * We want to cache the "post_status" and set the status later. We will set the initial status
296 * of the inserted post as the default status for the site, allow side effects to process with the
297 * inserted post (set term object connections, set meta input, sideload images if necessary, etc)
298 * Then will follow up with setting the status as what it was declared to be later
299 */
300 $intended_post_status = ! empty( $post_args['post_status'] ) ? $post_args['post_status'] : $default_post_status;
301
302 /**
303 * If the current user cannot publish posts but their intent was to publish,
304 * default the status to pending.
305 */
306 if ( ( ! isset( $post_type_object->cap->publish_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) ) && ! in_array(
307 $intended_post_status,
308 [
309 'draft',
310 'pending',
311 ],
312 true
313 ) ) {
314 $intended_post_status = 'pending';
315 }
316
317 /**
318 * Set the post_status as the default for the initial insert. The intended $post_status will be set after
319 * side effects are complete.
320 */
321 $post_args['post_status'] = $default_post_status;
322
323 $clean_args = wp_slash( (array) $post_args );
324
325 if ( ! is_array( $clean_args ) || empty( $clean_args ) ) {
326 throw new UserError( esc_html__( 'The object failed to create', 'wp-graphql' ) );
327 }
328
329 /**
330 * Insert the post and retrieve the ID
331 */
332 $post_id = wp_insert_post( $clean_args, true );
333
334 /**
335 * Throw an exception if the post failed to create
336 */
337 if ( is_wp_error( $post_id ) ) {
338 $error_message = $post_id->get_error_message();
339 if ( ! empty( $error_message ) ) {
340 throw new UserError( esc_html( $error_message ) );
341 }
342
343 throw new UserError( esc_html__( 'The object failed to create but no error was provided', 'wp-graphql' ) );
344 }
345
346 /**
347 * This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
348 *
349 * The input for the postObjectMutation will be passed, along with the $new_post_id for the
350 * postObject that was created so that relations can be set, meta can be updated, etc.
351 */
352 PostObjectMutation::update_additional_post_object_data( $post_id, $input, $post_type_object, $mutation_name, $context, $info, $default_post_status, $intended_post_status );
353
354 /**
355 * Determine whether the intended status should be set or not.
356 *
357 * By filtering to false, the $intended_post_status will not be set at the completion of the mutation.
358 *
359 * This allows for side-effect actions to set the status later. For example, if a post
360 * was being created via a GraphQL Mutation, the post had additional required assets, such as images
361 * that needed to be sideloaded or some other semi-time-consuming side effect, those actions could
362 * be deferred (cron or whatever), and when those actions complete they could come back and set
363 * the $intended_status.
364 *
365 * @param bool $should_set_intended_status Whether to set the intended post_status or not. Default true.
366 * @param \WP_Post_Type $post_type_object The Post Type Object for the post being mutated
367 * @param string $mutation_name The name of the mutation currently in progress
368 * @param \WPGraphQL\AppContext $context The AppContext passed down to all resolvers
369 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down to all resolvers
370 * @param string $intended_post_status The intended post_status the post should have according to the mutation input
371 * @param string $default_post_status The default status posts should use if an intended status wasn't set
372 *
373 * @hookGroup models
374 * @since 0.0.5
375 */
376 $should_set_intended_status = apply_filters( 'graphql_post_object_create_should_set_intended_post_status', true, $post_type_object, $mutation_name, $context, $info, $intended_post_status, $default_post_status );
377
378 /**
379 * If the intended post status and the default post status are not the same,
380 * update the post with the intended status now that side effects are complete.
381 */
382 if ( $intended_post_status !== $default_post_status && true === $should_set_intended_status ) {
383
384 /**
385 * If the post was deleted by a side effect action before getting here,
386 * don't proceed.
387 */
388 $new_post = get_post( $post_id );
389 if ( empty( $new_post ) ) {
390 throw new UserError( esc_html__( 'The status of the post could not be set', 'wp-graphql' ) );
391 }
392
393 /**
394 * If the $intended_post_status is different than the current status of the post
395 * proceed and update the status.
396 */
397 if ( $intended_post_status !== $new_post->post_status ) {
398 $update_args = [
399 'ID' => $post_id,
400 'post_status' => $intended_post_status,
401 // Prevent the post_date from being reset if the date was included in the create post $args
402 // see: https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/post.php#L3637
403 'edit_date' => ! empty( $post_args['post_date'] ) ? $post_args['post_date'] : false,
404 ];
405
406 wp_update_post( $update_args );
407 }
408 }
409
410 /**
411 * Return the post object
412 */
413 return [
414 'postObjectId' => $post_id,
415 ];
416 };
417 }
418 }
419