diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 2b32b5aafb05d..fb5ca66f17573 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5777,6 +5777,8 @@ function sanitize_trackback_urls( $to_ping ) { * * @param string|array $value String or array of data to slash. * @return string|array Slashed `$value`, in the same type as supplied. + * + * @phpstan-return ( $value is string ? string : array ) */ function wp_slash( $value ) { if ( is_array( $value ) ) { diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 88deb1090fc5c..a598adecbcf14 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -1145,6 +1145,14 @@ function get_extended( $post ) { * or 'display'. Default 'raw'. * @return WP_Post|array|null Type corresponding to $output on success or null on failure. * When $output is OBJECT, a `WP_Post` instance is returned. + * + * @phpstan-return ( + * $output is 'ARRAY_A' ? array|null : ( + * $output is 'ARRAY_N' ? array|null : ( + * WP_Post|null + * ) + * ) + * ) */ function get_post( $post = null, $output = OBJECT, $filter = 'raw' ) { if ( empty( $post ) && isset( $GLOBALS['post'] ) ) { diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 922f8968c5b21..af5ece695adee 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -684,6 +684,8 @@ function rest_ensure_request( $request ) { * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response * is already an instance, WP_REST_Response, otherwise * returns a new WP_REST_Response instance. + * + * @phpstan-return ( $response is WP_Error ? WP_Error : WP_REST_Response ) */ function rest_ensure_response( $response ) { if ( is_wp_error( $response ) ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 7b207938412f8..b52843d5024b0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -327,7 +327,7 @@ protected function insert_attachment( $request ) { $name = wp_basename( $file['file'] ); $name_parts = pathinfo( $name ); - $name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) ); + $name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ?? '' ) ) ) ); $url = $file['url']; $type = $file['type']; @@ -355,6 +355,9 @@ protected function insert_attachment( $request ) { } $attachment = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $attachment ) ) { + return $attachment; + } $attachment->post_mime_type = $type; $attachment->guid = $url; @@ -362,10 +365,12 @@ protected function insert_attachment( $request ) { // If the title was not set, use the original filename. if ( empty( $attachment->post_title ) && ! empty( $files['file']['name'] ) ) { // Remove the file extension (after the last `.`) - $tmp_title = substr( $files['file']['name'], 0, strrpos( $files['file']['name'], '.' ) ); - - if ( ! empty( $tmp_title ) ) { - $attachment->post_title = $tmp_title; + $last_dot_location = strrpos( $files['file']['name'], '.' ); + if ( false !== $last_dot_location ) { + $tmp_title = substr( $files['file']['name'], 0, $last_dot_location ); + if ( ! empty( $tmp_title ) ) { + $attachment->post_title = $tmp_title; + } } } @@ -377,10 +382,6 @@ protected function insert_attachment( $request ) { // $post_parent is inherited from $attachment['post_parent']. $id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false ); - if ( trim( $alt ) ) { - update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) ); - } - if ( is_wp_error( $id ) ) { if ( 'db_update_error' === $id->get_error_code() ) { $id->add_data( array( 'status' => 500 ) ); @@ -391,6 +392,10 @@ protected function insert_attachment( $request ) { return $id; } + if ( trim( $alt ) ) { + update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) ); + } + $attachment = get_post( $id ); /** @@ -659,6 +664,13 @@ public function edit_media_item( $request ) { if ( ! file_exists( $image_file_to_edit ) ) { $image_file_to_edit = _load_image_to_edit_path( $attachment_id ); } + if ( false === $image_file_to_edit ) { + return new WP_Error( + 'rest_cannot_get_image_file_to_edit', + __( 'Unable to get image file.' ), + array( 'status' => 404 ) + ); + } $image_editor = wp_get_image_editor( $image_file_to_edit ); @@ -766,7 +778,11 @@ public function edit_media_item( $request ) { $original_attachment_post = get_post( $attachment_id ); // Check request fields and assign default values. - $new_attachment_post = $this->prepare_item_for_database( $request ); + $new_attachment_post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $new_attachment_post ) ) { + return $new_attachment_post; + } + $new_attachment_post->post_mime_type = $saved['mime-type']; $new_attachment_post->guid = $uploads['url'] . "/$filename"; @@ -852,7 +868,15 @@ public function edit_media_item( $request ) { wp_update_attachment_metadata( $new_attachment_id, $new_image_meta ); - $response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request ); + $new_attachment_post = get_post( $new_attachment_id ); + if ( ! $new_attachment_post ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } + $response = $this->prepare_item_for_response( $new_attachment_post, $request ); $response->set_status( 201 ); $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) ); @@ -869,6 +893,9 @@ public function edit_media_item( $request ) { */ protected function prepare_item_for_database( $request ) { $prepared_attachment = parent::prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_attachment ) ) { + return $prepared_attachment; + } // Attachment caption (post_excerpt internally). if ( isset( $request['caption'] ) ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index f0cec04f191f8..f4564af41b061 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -14,6 +14,8 @@ * * @see WP_REST_Revisions_Controller * @see WP_REST_Controller + * + * @phpstan-import-type PreparedPost from WP_REST_Posts_Controller */ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller { @@ -220,7 +222,14 @@ public function create_item( $request ) { return $post; } - $prepared_post = $this->parent_controller->prepare_item_for_database( $request ); + $prepared_post = $this->parent_controller->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + /** + * @var PreparedPost $prepared_post + */ $prepared_post->ID = $post->ID; $user_id = get_current_user_id(); @@ -272,6 +281,13 @@ public function create_item( $request ) { } $autosave = get_post( $autosave_id ); + if ( ! $autosave ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } $request->set_param( 'context', 'edit' ); $response = $this->prepare_item_for_response( $autosave, $request ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php index dd72bc1c15210..07dc24c50d17b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php @@ -196,6 +196,13 @@ public function create_item( $request ) { do_action( 'rest_after_insert_nav_menu_item', $nav_menu_item, $request, true ); $post = get_post( $nav_menu_item_id ); + if ( ! $post ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } wp_after_insert_post( $post, false, null ); $response = $this->prepare_item_for_response( $post, $request ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 0ab54a3a0d384..ac1b18d93b625 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -13,6 +13,26 @@ * @since 4.7.0 * * @see WP_REST_Controller + * + * @phpstan-type PreparedPost object{ + * ID?: int, + * post_title?: string, + * post_content?: string, + * post_excerpt?: string, + * post_type: string, + * post_status?: string, + * post_date?: string|null, + * post_date_gmt?: string|null, + * edit_date?: true, + * post_name?: string, + * post_author?: int, + * post_parent?: int, + * menu_order?: int, + * comment_status?: string, + * ping_status?: string, + * page_template?: null, + * meta_input?: array, + * }&stdClass */ class WP_REST_Posts_Controller extends WP_REST_Controller { /** @@ -694,6 +714,13 @@ public function create_item_permissions_check( $request ) { } $post_type = get_post_type_object( $this->post_type ); + if ( ! $post_type ) { + return new WP_Error( + 'rest_post_invalid_type', + __( 'Invalid post type.' ), + array( 'status' => 400 ) + ); + } if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( @@ -753,6 +780,10 @@ public function create_item( $request ) { return $prepared_post; } + /* + * This is also set in the ::prepare_item_for_database() method above, but since the return value is filterable, + * there is no guarantee. + */ $prepared_post->post_type = $this->post_type; if ( ! empty( $prepared_post->post_name ) @@ -763,13 +794,16 @@ public function create_item( $request ) { * `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts. * * To ensure that a unique slug is generated, pass the post data with the 'publish' status. + * + * Note that neither ID nor post_parent are guaranteed to be set in ::prepare_item_for_database(), so this + * is the reason for the null coalescing operator. */ $prepared_post->post_name = wp_unique_post_slug( $prepared_post->post_name, - $prepared_post->id, + $prepared_post->ID ?? 0, 'publish', $prepared_post->post_type, - $prepared_post->post_parent + $prepared_post->post_parent ?? 0 ); } @@ -787,6 +821,13 @@ public function create_item( $request ) { } $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } /** * Fires after a single post is created or updated via the REST API. @@ -843,7 +884,6 @@ public function create_item( $request ) { } } - $post = get_post( $post_id ); $fields_update = $this->update_additional_fields_for_object( $post, $request ); if ( is_wp_error( $fields_update ) ) { @@ -874,6 +914,10 @@ public function create_item( $request ) { wp_after_insert_post( $post, false, null ); $response = $this->prepare_item_for_response( $post, $request ); + if ( is_wp_error( $response ) ) { + return $response; + } + $response = rest_ensure_response( $response ); $response->set_status( 201 ); @@ -942,14 +986,12 @@ public function update_item_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_item( $request ) { - $valid_check = $this->get_post( $request['id'] ); - if ( is_wp_error( $valid_check ) ) { - return $valid_check; + $post_before = $this->get_post( $request['id'] ); + if ( is_wp_error( $post_before ) ) { + return $post_before; } - $post_before = get_post( $request['id'] ); - $post = $this->prepare_item_for_database( $request ); - + $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } @@ -964,15 +1006,17 @@ public function update_item( $request ) { * `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts. * * To ensure that a unique slug is generated, pass the post data with the 'publish' status. + * + * Note that neither ID nor post_parent are guaranteed to be set in ::prepare_item_for_database(), so this + * is the reason for the null coalescing operator. */ if ( ! empty( $post->post_name ) && in_array( $post_status, array( 'draft', 'pending' ), true ) ) { - $post_parent = ! empty( $post->post_parent ) ? $post->post_parent : 0; $post->post_name = wp_unique_post_slug( $post->post_name, - $post->ID, + $post->ID ?? 0, 'publish', $post->post_type, - $post_parent + $post->post_parent ?? 0 ); } @@ -989,6 +1033,13 @@ public function update_item( $request ) { } $post = get_post( $post_id ); + if ( ! $post ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ do_action( "rest_insert_{$this->post_type}", $post, $request, false ); @@ -1029,7 +1080,6 @@ public function update_item( $request ) { } } - $post = get_post( $post_id ); $fields_update = $this->update_additional_fields_for_object( $post, $request ); if ( is_wp_error( $fields_update ) ) { @@ -1283,13 +1333,15 @@ protected function prepare_date_response( $date_gmt, $date = null ) { * @since 4.7.0 * * @param WP_REST_Request $request Request object. - * @return stdClass|WP_Error Post object or WP_Error. + * @return object|WP_Error Post object or WP_Error. + * @phpstan-return PreparedPost|WP_Error */ protected function prepare_item_for_database( $request ) { $prepared_post = new stdClass(); $current_status = ''; // Post ID. + $existing_post = null; if ( isset( $request['id'] ) ) { $existing_post = $this->get_post( $request['id'] ); if ( is_wp_error( $existing_post ) ) { @@ -1330,15 +1382,22 @@ protected function prepare_item_for_database( $request ) { } // Post type. - if ( empty( $request['id'] ) ) { + if ( ! $existing_post ) { // Creating new post, use default type for the controller. $prepared_post->post_type = $this->post_type; } else { // Updating a post, use previous type. - $prepared_post->post_type = get_post_type( $request['id'] ); + $prepared_post->post_type = $existing_post->post_type; } $post_type = get_post_type_object( $prepared_post->post_type ); + if ( ! $post_type ) { + return new WP_Error( + 'rest_post_invalid_type', + __( 'Invalid post type.' ), + array( 'status' => 400 ) + ); + } // Post status. if ( @@ -1357,7 +1416,7 @@ protected function prepare_item_for_database( $request ) { // Post date. if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) { - $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false; + $current_date = $existing_post ? $existing_post->post_date : false; $date_data = rest_get_date_with_gmt( $request['date'] ); if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) { @@ -1365,7 +1424,7 @@ protected function prepare_item_for_database( $request ) { $prepared_post->edit_date = true; } } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) { - $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false; + $current_date = $existing_post ? $existing_post->post_date_gmt : false; $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) { @@ -1423,7 +1482,7 @@ protected function prepare_item_for_database( $request ) { ); } - if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) { + if ( $existing_post && is_sticky( $existing_post->ID ) ) { return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), @@ -1434,7 +1493,7 @@ protected function prepare_item_for_database( $request ) { } if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { - if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) { + if ( $existing_post && post_password_required( $prepared_post->ID ) ) { return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ),