Changeset 62081 for trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
- Timestamp:
- 03/20/2026 05:09:14 PM (3 months ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
r61982 r62081 64 64 ) 65 65 ); 66 67 if ( wp_is_client_side_media_processing_enabled() ) {68 $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );69 // Special case to set 'original_image' in attachment metadata.70 $valid_image_sizes[] = 'original';71 // Used for PDF thumbnails.72 $valid_image_sizes[] = 'full';73 // Client-side big image threshold: sideload the scaled version.74 $valid_image_sizes[] = 'scaled';75 76 register_rest_route(77 $this->namespace,78 '/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',79 array(80 array(81 'methods' => WP_REST_Server::CREATABLE,82 'callback' => array( $this, 'sideload_item' ),83 'permission_callback' => array( $this, 'sideload_item_permissions_check' ),84 'args' => array(85 'id' => array(86 'description' => __( 'Unique identifier for the attachment.' ),87 'type' => 'integer',88 ),89 'image_size' => array(90 'description' => __( 'Image size.' ),91 'type' => 'string',92 'enum' => $valid_image_sizes,93 'required' => true,94 ),95 'convert_format' => array(96 'type' => 'boolean',97 'default' => true,98 'description' => __( 'Whether to convert image formats.' ),99 ),100 ),101 ),102 'allow_batch' => $this->allow_batch,103 'schema' => array( $this, 'get_public_item_schema' ),104 )105 );106 107 register_rest_route(108 $this->namespace,109 '/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',110 array(111 array(112 'methods' => WP_REST_Server::CREATABLE,113 'callback' => array( $this, 'finalize_item' ),114 'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),115 'args' => array(116 'id' => array(117 'description' => __( 'Unique identifier for the attachment.' ),118 'type' => 'integer',119 ),120 ),121 ),122 'allow_batch' => $this->allow_batch,123 'schema' => array( $this, 'get_public_item_schema' ),124 )125 );126 }127 }128 129 /**130 * Retrieves the query params for the attachments collection.131 *132 * @since 7.0.0133 *134 * @param string $method Optional. HTTP method of the request.135 * The arguments for `CREATABLE` requests are136 * checked for required values and may fall-back to a given default.137 * Default WP_REST_Server::CREATABLE.138 * @return array<string, array<string, mixed>> Endpoint arguments.139 */140 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {141 $args = parent::get_endpoint_args_for_item_schema( $method );142 143 if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) {144 $args['generate_sub_sizes'] = array(145 'type' => 'boolean',146 'default' => true,147 'description' => __( 'Whether to generate image sub sizes.' ),148 );149 $args['convert_format'] = array(150 'type' => 'boolean',151 'default' => true,152 'description' => __( 'Whether to convert image formats.' ),153 );154 }155 156 return $args;157 66 } 158 67 … … 253 162 $prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, $files['file']['type'] ?? null ); 254 163 255 // When the client handles image processing (generate_sub_sizes is false),256 // skip the server-side image editor support check.257 if ( false === $request['generate_sub_sizes'] ) {258 $prevent_unsupported_uploads = false;259 }260 261 164 // If the upload is an image, check if the server can handle the mime type. 262 165 if ( … … 290 193 * 291 194 * @since 4.7.0 292 * @since 7.0.0 Added `generate_sub_sizes` and `convert_format` parameters.293 195 * 294 196 * @param WP_REST_Request $request Full details about the request. … … 304 206 } 305 207 306 // Handle generate_sub_sizes parameter.307 if ( false === $request['generate_sub_sizes'] ) {308 add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );309 add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );310 // Disable server-side EXIF rotation so the client can handle it.311 // This preserves the original orientation value in the metadata.312 add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );313 }314 315 // Handle convert_format parameter.316 if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {317 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );318 }319 320 208 $insert = $this->insert_attachment( $request ); 321 209 322 210 if ( is_wp_error( $insert ) ) { 323 $this->remove_client_side_media_processing_filters();324 211 return $insert; 325 212 } … … 339 226 340 227 if ( is_wp_error( $thumbnail_update ) ) { 341 $this->remove_client_side_media_processing_filters();342 228 return $thumbnail_update; 343 229 } … … 348 234 349 235 if ( is_wp_error( $meta_update ) ) { 350 $this->remove_client_side_media_processing_filters();351 236 return $meta_update; 352 237 } … … 357 242 358 243 if ( is_wp_error( $fields_update ) ) { 359 $this->remove_client_side_media_processing_filters();360 244 return $fields_update; 361 245 } … … 364 248 365 249 if ( is_wp_error( $terms_update ) ) { 366 $this->remove_client_side_media_processing_filters();367 250 return $terms_update; 368 251 } … … 401 284 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); 402 285 403 $this->remove_client_side_media_processing_filters();404 405 286 $response = $this->prepare_item_for_response( $attachment, $request ); 406 287 $response = rest_ensure_response( $response ); … … 409 290 410 291 return $response; 411 }412 413 /**414 * Removes filters added for client-side media processing.415 *416 * @since 7.0.0417 */418 private function remove_client_side_media_processing_filters() {419 remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );420 remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );421 remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );422 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );423 292 } 424 293 … … 1988 1857 return null; 1989 1858 } 1990 1991 /**1992 * Checks if a given request has access to sideload a file.1993 *1994 * Sideloading a file for an existing attachment1995 * requires both update and create permissions.1996 *1997 * @since 7.0.01998 *1999 * @param WP_REST_Request $request Full details about the request.2000 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.2001 */2002 public function sideload_item_permissions_check( $request ) {2003 return $this->edit_media_item_permissions_check( $request );2004 }2005 2006 /**2007 * Side-loads a media file without creating a new attachment.2008 *2009 * @since 7.0.02010 *2011 * @param WP_REST_Request $request Full details about the request.2012 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.2013 */2014 public function sideload_item( WP_REST_Request $request ) {2015 $attachment_id = $request['id'];2016 2017 $post = $this->get_post( $attachment_id );2018 2019 if ( is_wp_error( $post ) ) {2020 return $post;2021 }2022 2023 if (2024 ! wp_attachment_is_image( $post ) &&2025 ! wp_attachment_is( 'pdf', $post )2026 ) {2027 return new WP_Error(2028 'rest_post_invalid_id',2029 __( 'Invalid post ID. Only images and PDFs can be sideloaded.' ),2030 array( 'status' => 400 )2031 );2032 }2033 2034 if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {2035 // Prevent image conversion as that is done client-side.2036 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );2037 }2038 2039 // Get the file via $_FILES or raw data.2040 $files = $request->get_file_params();2041 $headers = $request->get_headers();2042 2043 /*2044 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.2045 * See /wp-includes/functions.php.2046 * With the following filter we can work around this safeguard.2047 */2048 $attachment_filename = get_attached_file( $attachment_id, true );2049 $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null;2050 2051 $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {2052 return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename );2053 };2054 2055 add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );2056 2057 $parent_post = get_post_parent( $attachment_id );2058 2059 $time = null;2060 2061 // Matches logic in media_handle_upload().2062 // The post date doesn't usually matter for pages, so don't backdate this upload.2063 if ( $parent_post && 'page' !== $parent_post->post_type && ! str_starts_with( $parent_post->post_date, '0000-00-00' ) ) {2064 $time = $parent_post->post_date;2065 }2066 2067 if ( ! empty( $files ) ) {2068 $file = $this->upload_from_file( $files, $headers, $time );2069 } else {2070 $file = $this->upload_from_data( $request->get_body(), $headers, $time );2071 }2072 2073 remove_filter( 'wp_unique_filename', $filter_filename );2074 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );2075 2076 if ( is_wp_error( $file ) ) {2077 return $file;2078 }2079 2080 $type = $file['type'];2081 $path = $file['file'];2082 2083 $image_size = $request['image_size'];2084 2085 $metadata = wp_get_attachment_metadata( $attachment_id, true );2086 2087 if ( ! $metadata ) {2088 $metadata = array();2089 }2090 2091 if ( 'original' === $image_size ) {2092 $metadata['original_image'] = wp_basename( $path );2093 } elseif ( 'scaled' === $image_size ) {2094 // The current attached file is the original; record it as original_image.2095 $current_file = get_attached_file( $attachment_id, true );2096 2097 if ( ! $current_file ) {2098 return new WP_Error(2099 'rest_sideload_no_attached_file',2100 __( 'Unable to retrieve the attached file for this attachment.' ),2101 array( 'status' => 404 )2102 );2103 }2104 2105 $metadata['original_image'] = wp_basename( $current_file );2106 2107 // Validate the scaled image before updating the attached file.2108 $size = wp_getimagesize( $path );2109 $filesize = wp_filesize( $path );2110 2111 if ( ! $size || ! $filesize ) {2112 return new WP_Error(2113 'rest_sideload_invalid_image',2114 __( 'Unable to read the scaled image file.' ),2115 array( 'status' => 500 )2116 );2117 }2118 2119 // Update the attached file to point to the scaled version.2120 if (2121 get_attached_file( $attachment_id, true ) !== $path &&2122 ! update_attached_file( $attachment_id, $path )2123 ) {2124 return new WP_Error(2125 'rest_sideload_update_attached_file_failed',2126 __( 'Unable to update the attached file for this attachment.' ),2127 array( 'status' => 500 )2128 );2129 }2130 2131 $metadata['width'] = $size[0];2132 $metadata['height'] = $size[1];2133 $metadata['filesize'] = $filesize;2134 $metadata['file'] = _wp_relative_upload_path( $path );2135 } else {2136 $metadata['sizes'] = $metadata['sizes'] ?? array();2137 2138 $size = wp_getimagesize( $path );2139 2140 $metadata['sizes'][ $image_size ] = array(2141 'width' => $size ? $size[0] : 0,2142 'height' => $size ? $size[1] : 0,2143 'file' => wp_basename( $path ),2144 'mime-type' => $type,2145 'filesize' => wp_filesize( $path ),2146 );2147 }2148 2149 wp_update_attachment_metadata( $attachment_id, $metadata );2150 2151 $response_request = new WP_REST_Request(2152 WP_REST_Server::READABLE,2153 rest_get_route_for_post( $attachment_id )2154 );2155 2156 $response_request['context'] = 'edit';2157 2158 if ( isset( $request['_fields'] ) ) {2159 $response_request['_fields'] = $request['_fields'];2160 }2161 2162 $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );2163 2164 $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );2165 2166 return $response;2167 }2168 2169 /**2170 * Filters wp_unique_filename during sideloads.2171 *2172 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.2173 * Adding this closure to the filter helps work around this safeguard.2174 *2175 * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg,2176 * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg2177 * However, here it is desired not to add the suffix in order to maintain the same2178 * naming convention as if the file was uploaded regularly.2179 *2180 * @since 7.0.02181 *2182 * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L25822183 *2184 * @param string $filename Unique file name.2185 * @param string $dir Directory path.2186 * @param int|string $number The highest number that was used to make the file name unique2187 * or an empty string if unused.2188 * @param string|null $attachment_filename Original attachment file name.2189 * @return string Filtered file name.2190 */2191 private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) {2192 if ( ! is_int( $number ) || ! $attachment_filename ) {2193 return $filename;2194 }2195 2196 $ext = pathinfo( $filename, PATHINFO_EXTENSION );2197 $name = pathinfo( $filename, PATHINFO_FILENAME );2198 $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME );2199 2200 if ( ! $ext || ! $name ) {2201 return $filename;2202 }2203 2204 $matches = array();2205 if ( preg_match( '/(.*)-(\d+x\d+|scaled)-' . $number . '$/', $name, $matches ) ) {2206 $filename_without_suffix = $matches[1] . '-' . $matches[2] . ".$ext";2207 if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {2208 return $filename_without_suffix;2209 }2210 }2211 2212 return $filename;2213 }2214 2215 /**2216 * Finalizes an attachment after client-side media processing.2217 *2218 * Triggers the 'wp_generate_attachment_metadata' filter so that2219 * server-side plugins can process the attachment after all client-side2220 * operations (upload, thumbnail generation, sideloads) are complete.2221 *2222 * @since 7.0.02223 *2224 * @param WP_REST_Request $request Full details about the request.2225 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.2226 */2227 public function finalize_item( WP_REST_Request $request ) {2228 $attachment_id = $request['id'];2229 2230 $post = $this->get_post( $attachment_id );2231 if ( is_wp_error( $post ) ) {2232 return $post;2233 }2234 2235 $metadata = wp_get_attachment_metadata( $attachment_id );2236 if ( ! is_array( $metadata ) ) {2237 $metadata = array();2238 }2239 2240 /** This filter is documented in wp-admin/includes/image.php */2241 $metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );2242 2243 wp_update_attachment_metadata( $attachment_id, $metadata );2244 2245 $response_request = new WP_REST_Request(2246 WP_REST_Server::READABLE,2247 rest_get_route_for_post( $attachment_id )2248 );2249 2250 $response_request['context'] = 'edit';2251 2252 if ( isset( $request['_fields'] ) ) {2253 $response_request['_fields'] = $request['_fields'];2254 }2255 2256 return $this->prepare_item_for_response( $post, $response_request );2257 }2258 1859 }
Note: See TracChangeset
for help on using the changeset viewer.