| 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 |
|