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 / PostObjectUpdate.php

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

260 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPGraphQL\Mutation;
3
4 use GraphQL\Error\UserError;
5 use GraphQL\Type\Definition\ResolveInfo;
6 use WPGraphQL\AppContext;
7 use WPGraphQL\Data\PostObjectMutation;
8 use WPGraphQL\Utils\Utils;
9 use WP_Post_Type;
10
11 class PostObjectUpdate {
12 /**
13 * Registers the PostObjectUpdate mutation.
14 *
15 * @param \WP_Post_Type $post_type_object The post type of the mutation.
16 *
17 * @return void
18 * @throws \Exception
19 */
20 public static function register_mutation( WP_Post_Type $post_type_object ) {
21 $mutation_name = 'update' . ucwords( $post_type_object->graphql_single_name );
22
23 register_graphql_mutation(
24 $mutation_name,
25 [
26 'inputFields' => self::get_input_fields( $post_type_object ),
27 'outputFields' => self::get_output_fields( $post_type_object ),
28 'mutateAndGetPayload' => self::mutate_and_get_payload( $post_type_object, $mutation_name ),
29 ]
30 );
31 }
32
33 /**
34 * Defines the mutation input field configuration.
35 *
36 * @param \WP_Post_Type $post_type_object The post type of the mutation.
37 *
38 * @return array<string,array<string,mixed>>
39 */
40 public static function get_input_fields( $post_type_object ) {
41 return array_merge(
42 PostObjectCreate::get_input_fields( $post_type_object ),
43 [
44 'id' => [
45 'type' => [
46 'non_null' => 'ID',
47 ],
48 'description' => static function () use ( $post_type_object ) {
49 // translators: the placeholder is the name of the type of post object being updated
50 return sprintf( __( 'The ID of the %1$s object', 'wp-graphql' ), $post_type_object->graphql_single_name );
51 },
52 ],
53 'ignoreEditLock' => [
54 'type' => 'Boolean',
55 'description' => static function () {
56 return __( 'Override the edit lock when another user is editing the post', 'wp-graphql' );
57 },
58 ],
59 ]
60 );
61 }
62
63 /**
64 * Defines the mutation output field configuration.
65 *
66 * @param \WP_Post_Type $post_type_object The post type of the mutation.
67 *
68 * @return array<string,array<string,mixed>>
69 */
70 public static function get_output_fields( $post_type_object ) {
71 return PostObjectCreate::get_output_fields( $post_type_object );
72 }
73
74 /**
75 * Defines the mutation data modification closure.
76 *
77 * @param \WP_Post_Type $post_type_object The post type of the mutation.
78 * @param string $mutation_name The mutation name.
79 *
80 * @return callable(array<string,mixed>$input,\WPGraphQL\AppContext $context,\GraphQL\Type\Definition\ResolveInfo $info):array<string,mixed>
81 */
82 public static function mutate_and_get_payload( $post_type_object, $mutation_name ) {
83 return static function ( $input, AppContext $context, ResolveInfo $info ) use ( $post_type_object, $mutation_name ) {
84 // Get the database ID for the comment.
85 $post_id = Utils::get_database_id_from_id( $input['id'] );
86 $existing_post = ! empty( $post_id ) ? get_post( $post_id ) : null;
87
88 /**
89 * If there's no existing post, throw an exception
90 */
91 if ( null === $existing_post ) {
92 // translators: the placeholder is the name of the type of post being updated
93 throw new UserError( esc_html( sprintf( __( 'No %1$s could be found to update', 'wp-graphql' ), $post_type_object->graphql_single_name ) ) );
94 }
95
96 if ( $post_type_object->name !== $existing_post->post_type ) {
97 // translators: The first placeholder is an ID and the second placeholder is the name of the post type being edited
98 throw new UserError( esc_html( sprintf( __( 'The id %1$d is not of the type "%2$s"', 'wp-graphql' ), $post_id, $post_type_object->name ) ) );
99 }
100
101 /**
102 * Stop now if a user isn't allowed to edit posts
103 */
104 if ( ! isset( $post_type_object->cap->edit_posts ) || ! current_user_can( $post_type_object->cap->edit_posts ) ) {
105 // translators: the $post_type_object->graphql_single_name placeholder is the name of the object being mutated
106 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to update a %1$s', 'wp-graphql' ), $post_type_object->graphql_single_name ) ) );
107 }
108
109 /**
110 * If the existing post was authored by another author, ensure the requesting user has permission to edit it
111 */
112 if ( get_current_user_id() !== (int) $existing_post->post_author && ( ! isset( $post_type_object->cap->edit_others_posts ) || true !== current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
113 // translators: the $post_type_object->graphql_single_name placeholder is the name of the object being mutated
114 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to update another author\'s %1$s', 'wp-graphql' ), $post_type_object->graphql_single_name ) ) );
115 }
116
117 /**
118 * Enforce the object-level edit capability for this specific post. WordPress maps
119 * the `edit_post` meta capability to `edit_published_posts` once a post is
120 * published, so a user who can create and edit drafts (e.g. a Contributor) cannot
121 * edit a post after it has been published. This mirrors the WordPress REST API,
122 * which returns `rest_cannot_edit` for the same request.
123 */
124 if ( ! isset( $post_type_object->cap->edit_post ) || ! current_user_can( $post_type_object->cap->edit_post, $post_id ) ) {
125 // translators: the placeholder is the singular name of the post type being mutated
126 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to update this %1$s', 'wp-graphql' ), $post_type_object->graphql_single_name ) ) );
127 }
128
129 $author_id = absint( $existing_post->post_author );
130
131 /**
132 * If the mutation is setting the author to be someone other than the user making the request
133 * make sure they have permission to edit others posts
134 */
135 if ( ! empty( $input['authorId'] ) ) {
136 // Ensure authorId is a valid databaseId.
137 $input['authorId'] = Utils::get_database_id_from_id( $input['authorId'] );
138 // Use the new author for checks.
139 $author_id = $input['authorId'];
140 }
141
142 /**
143 * Check to see if the existing_media_item author matches the current user,
144 * if not they need to be able to edit others posts to proceed
145 */
146 if ( get_current_user_id() !== $author_id && ( ! isset( $post_type_object->cap->edit_others_posts ) || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
147 // translators: the $post_type_object->graphql_single_name placeholder is the name of the object being mutated
148 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to update %1$s as this user.', 'wp-graphql' ), $post_type_object->graphql_plural_name ) ) );
149 }
150
151 // If post is locked and the override is not specified, do not allow the edit
152 $locked_user_id = PostObjectMutation::check_edit_lock( $post_id, $input );
153 if ( false !== $locked_user_id ) {
154 $user = get_userdata( (int) $locked_user_id );
155 $display_name = isset( $user->display_name ) ? $user->display_name : 'unknown';
156 /* translators: %s: User's display name. */
157 throw new UserError( esc_html( sprintf( __( 'You cannot update this item. %s is currently editing.', 'wp-graphql' ), $display_name ) ) );
158 }
159
160 /**
161 * @todo: when we add support for making posts sticky, we should check permissions to make sure users can make posts sticky
162 * @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L640-L642
163 */
164
165 /**
166 * @todo: when we add support for assigning terms to posts, we should check permissions to make sure they can assign terms
167 * @see : https://github.com/WordPress/WordPress/blob/e357195ce303017d517aff944644a7a1232926f7/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L644-L646
168 */
169
170 /**
171 * Insert the post object and get the ID
172 */
173 $post_args = PostObjectMutation::prepare_post_object( $input, $post_type_object, $mutation_name );
174 $post_args['ID'] = $post_id;
175
176 /**
177 * If the update requests a status that requires publishing capability (anything
178 * other than draft or pending) and the current user cannot publish, reject the
179 * request rather than silently changing the post's visibility. This mirrors the
180 * WordPress REST API, which returns `rest_cannot_publish` for the same request.
181 */
182 if (
183 isset( $post_args['post_status'] ) &&
184 ! in_array( $post_args['post_status'], [ 'draft', 'pending' ], true ) &&
185 ( ! isset( $post_type_object->cap->publish_posts ) || ! current_user_can( $post_type_object->cap->publish_posts ) )
186 ) {
187 // translators: the placeholder is the singular name of the post type being mutated
188 throw new UserError( esc_html( sprintf( __( 'Sorry, you are not allowed to publish this %1$s', 'wp-graphql' ), $post_type_object->graphql_single_name ) ) );
189 }
190
191 $clean_args = wp_slash( (array) $post_args );
192
193 if ( ! is_array( $clean_args ) || empty( $clean_args ) ) {
194 throw new UserError( esc_html__( 'The object failed to update.', 'wp-graphql' ) );
195 }
196
197 /**
198 * Insert the post and retrieve the ID
199 */
200 $updated_post_id = wp_update_post( $clean_args, true );
201
202 /**
203 * Throw an exception if the post failed to update
204 */
205 if ( is_wp_error( $updated_post_id ) ) {
206 $error_message = $updated_post_id->get_error_message();
207 if ( ! empty( $error_message ) ) {
208 throw new UserError( esc_html( $error_message ) );
209 }
210
211 throw new UserError( esc_html__( 'The object failed to update but no error was provided', 'wp-graphql' ) );
212 }
213
214 /**
215 * Fires after a single term is created or updated via a GraphQL mutation
216 *
217 * The dynamic portion of the hook name, `$taxonomy->name` refers to the taxonomy of the term being mutated
218 *
219 * @param int $post_id Inserted post ID
220 * @param \WP_Post_Type $post_type_object The Post Type object for the post being mutated
221 * @param array<string,mixed> $args The args used to insert the term
222 * @param string $mutation_name The name of the mutation being performed
223 *
224 * @hookGroup models
225 * @since 0.0.5
226 */
227 do_action( 'graphql_insert_post_object', absint( $post_id ), $post_type_object, $post_args, $mutation_name );
228
229 /**
230 * Fires after a single term is created or updated via a GraphQL mutation
231 *
232 * The dynamic portion of the hook name, `$taxonomy->name` refers to the taxonomy of the term being mutated
233 *
234 * @param int $post_id Inserted post ID
235 * @param array<string,mixed> $args The args used to insert the term
236 * @param string $mutation_name The name of the mutation being performed
237 *
238 * @hookGroup models
239 * @since 0.0.5
240 */
241 do_action( "graphql_insert_{$post_type_object->name}", absint( $post_id ), $post_args, $mutation_name );
242
243 /**
244 * This updates additional data not part of the posts table (postmeta, terms, other relations, etc)
245 *
246 * The input for the postObjectMutation will be passed, along with the $new_post_id for the
247 * postObject that was updated so that relations can be set, meta can be updated, etc.
248 */
249 PostObjectMutation::update_additional_post_object_data( (int) $post_id, $input, $post_type_object, $mutation_name, $context, $info );
250
251 /**
252 * Return the payload
253 */
254 return [
255 'postObjectId' => $post_id,
256 ];
257 };
258 }
259 }
260