PluginProbe
Gutenberg / 9.6.2
Gutenberg v9.6.2
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / class-wp-rest-image-editor-controller.php

class-wp-rest-image-editor-controller.php in Gutenberg 9.6.2, at lib/class-wp-rest-image-editor-controller.php

363 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Start: Include for phase 2
4 * REST API: WP_REST_Image_Editor_Controller class
5 *
6 * @package WordPress
7 * @subpackage REST_API
8 */
9
10 /**
11 * Controller which provides REST API endpoints for image editing.
12 *
13 * @since 7.x ?
14 *
15 * @see WP_REST_Controller
16 */
17 class WP_REST_Image_Editor_Controller extends WP_REST_Controller {
18
19 /**
20 * Constructs the controller.
21 *
22 * @since 7.x ?
23 * @access public
24 */
25 public function __construct() {
26 $this->namespace = 'wp/v2';
27 $this->rest_base = 'media';
28 }
29
30 /**
31 * Registers the necessary REST API routes.
32 *
33 * @since 7.x ?
34 * @access public
35 */
36 public function register_routes() {
37 register_rest_route(
38 $this->namespace,
39 '/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
40 array(
41 array(
42 'methods' => WP_REST_Server::EDITABLE,
43 'callback' => array( $this, 'apply_edits' ),
44 'permission_callback' => array( $this, 'permission_callback' ),
45 'args' => array(
46 'rotation' => array(
47 'type' => 'integer',
48 ),
49
50 // Src is required to check for correct $image_meta.
51 'src' => array(
52 'type' => 'string',
53 'required' => true,
54 ),
55
56 // Crop values are in percents.
57 'x' => array(
58 'type' => 'number',
59 'minimum' => 0,
60 'maximum' => 100,
61 ),
62 'y' => array(
63 'type' => 'number',
64 'minimum' => 0,
65 'maximum' => 100,
66 ),
67 'width' => array(
68 'type' => 'number',
69 'minimum' => 0,
70 'maximum' => 100,
71 ),
72 'height' => array(
73 'type' => 'number',
74 'minimum' => 0,
75 'maximum' => 100,
76 ),
77 ),
78 ),
79 )
80 );
81 }
82
83 /**
84 * Checks if the user has permissions to make the request.
85 *
86 * @since 7.x ?
87 * @access public
88 *
89 * @param WP_REST_Request $request Full details about the request.
90 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
91 */
92 public function permission_callback( $request ) {
93 if ( ! current_user_can( 'edit_post', $request['id'] ) ) {
94 $error = __( 'Sorry, you are not allowed to edit images.', 'gutenberg' );
95 return new WP_Error( 'rest_cannot_edit_image', $error, array( 'status' => rest_authorization_required_code() ) );
96 }
97
98 if ( ! current_user_can( 'upload_files' ) ) {
99 return new WP_Error( 'rest_cannot_edit_image', __( 'Sorry, you are not allowed to upload media on this site.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
100 }
101
102 return true;
103 }
104
105 /**
106 * Applies all edits in one go.
107 *
108 * @since 7.x ?
109 * @access public
110 *
111 * @param WP_REST_Request $request Full details about the request.
112 * @return WP_REST_Response|WP_Error If successful image JSON for the modified image, otherwise a WP_Error.
113 */
114 public function apply_edits( $request ) {
115 require_once ABSPATH . 'wp-admin/includes/image.php';
116
117 $attachment_id = $request['id'];
118
119 // This also confirms the attachment is an image.
120 $image_file = wp_get_original_image_path( $attachment_id );
121 $image_meta = wp_get_attachment_metadata( $attachment_id );
122
123 if ( function_exists( 'wp_image_file_matches_image_meta' ) ) {
124 if (
125 ! $image_meta ||
126 ! $image_file ||
127 ! wp_image_file_matches_image_meta( $request['src'], $image_meta )
128 ) {
129 return new WP_Error(
130 'rest_unknown_attachment',
131 __( 'Unable to get meta information for file.', 'gutenberg' ),
132 array( 'status' => 404 )
133 );
134 }
135 } else {
136 // Back-compat for WP versions < 5.5.
137 if ( ! $image_meta || ! $image_file ) {
138 return new WP_Error(
139 'rest_unknown_attachment',
140 __( 'Unable to get meta information for file.', 'gutenberg' ),
141 array( 'status' => 404 )
142 );
143 } else {
144 $match = false;
145 $image_src = $request['src'];
146
147 if ( isset( $image_meta['file'] ) && strlen( $image_meta['file'] ) > 4 ) {
148 // Remove quiery args.
149 list( $image_src ) = explode( '?', $image_src );
150
151 // Check if the relative image path from the image meta is at the end of $image_src.
152 if ( strrpos( $image_src, $image_meta['file'] ) === strlen( $image_src ) - strlen( $image_meta['file'] ) ) {
153 $match = true;
154 }
155
156 if ( ! empty( $image_meta['sizes'] ) ) {
157 // Retrieve the uploads sub-directory from the full size image.
158 $dirname = _wp_get_attachment_relative_path( $image_meta['file'] );
159
160 if ( $dirname ) {
161 $dirname = trailingslashit( $dirname );
162 }
163
164 foreach ( $image_meta['sizes'] as $image_size_data ) {
165 $relative_path = $dirname . $image_size_data['file'];
166
167 if ( strrpos( $image_src, $relative_path ) === strlen( $image_src ) - strlen( $relative_path ) ) {
168 $match = true;
169 break;
170 }
171 }
172 }
173 }
174
175 if ( ! $match ) {
176 return new WP_Error(
177 'rest_unknown_attachment',
178 __( 'Unable to get meta information for file.', 'gutenberg' ),
179 array( 'status' => 404 )
180 );
181 }
182 }
183 }
184
185 $supported_types = array( 'image/jpeg', 'image/png', 'image/gif' );
186 $mime_type = get_post_mime_type( $attachment_id );
187 if ( ! in_array( $mime_type, $supported_types, true ) ) {
188 return new WP_Error(
189 'rest_cannot_edit_file_type',
190 __( 'This type of file cannot be edited.', 'gutenberg' ),
191 array( 'status' => 400 )
192 );
193 }
194
195 // Check if we need to do anything.
196 $rotate = 0;
197 $crop = false;
198
199 if ( ! empty( $request['rotation'] ) ) {
200 // Rotation direction: clockwise vs. counter clockwise.
201 $rotate = 0 - (int) $request['rotation'];
202 }
203
204 if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) {
205 $crop = true;
206 }
207
208 if ( ! $rotate && ! $crop ) {
209 $error = __( 'The image was not edited. Edit the image before applying the changes.', 'gutenberg' );
210 return new WP_Error( 'rest_image_not_edited', $error, array( 'status' => 400 ) );
211 }
212
213 // If the file doesn't exist, attempt a URL fopen on the src link.
214 // This can occur with certain file replication plugins.
215 // Keep the original file path to get a modified name later.
216 $image_file_to_edit = $image_file;
217 if ( ! file_exists( $image_file_to_edit ) ) {
218 $image_file_to_edit = _load_image_to_edit_path( $attachment_id );
219 }
220
221 $image_editor = wp_get_image_editor( $image_file_to_edit );
222
223 if ( is_wp_error( $image_editor ) ) {
224 // This image cannot be edited.
225 $error = __( 'Unable to edit this image.', 'gutenberg' );
226 return new WP_Error( 'rest_unknown_image_file_type', $error, array( 'status' => 500 ) );
227 }
228
229 if ( 0 !== $rotate ) {
230 $result = $image_editor->rotate( $rotate );
231
232 if ( is_wp_error( $result ) ) {
233 $error = __( 'Unable to rotate this image.', 'gutenberg' );
234 return new WP_Error( 'rest_image_rotation_failed', $error, array( 'status' => 500 ) );
235 }
236 }
237
238 if ( $crop ) {
239 $size = $image_editor->get_size();
240
241 $crop_x = round( ( $size['width'] * floatval( $request['x'] ) ) / 100.0 );
242 $crop_y = round( ( $size['height'] * floatval( $request['y'] ) ) / 100.0 );
243 $width = round( ( $size['width'] * floatval( $request['width'] ) ) / 100.0 );
244 $height = round( ( $size['height'] * floatval( $request['height'] ) ) / 100.0 );
245
246 $result = $image_editor->crop( $crop_x, $crop_y, $width, $height );
247
248 if ( is_wp_error( $result ) ) {
249 $error = __( 'Unable to crop this image.', 'gutenberg' );
250 return new WP_Error( 'rest_image_crop_failed', $error, array( 'status' => 500 ) );
251 }
252 }
253
254 // Calculate the file name.
255 $image_ext = pathinfo( $image_file, PATHINFO_EXTENSION );
256 $image_name = wp_basename( $image_file, ".{$image_ext}" );
257
258 // Do not append multiple `-edited` to the file name.
259 // The user may be editing a previously edited image.
260 if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) {
261 // Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number.
262 $image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name );
263 } else {
264 // Append `-edited` before the extension.
265 $image_name .= '-edited';
266 }
267
268 $filename = "{$image_name}.{$image_ext}";
269
270 // Create the uploads sub-directory if needed.
271 $uploads = wp_upload_dir();
272
273 // Make the file name unique in the (new) upload directory.
274 $filename = wp_unique_filename( $uploads['path'], $filename );
275
276 // Save to disk.
277 $saved = $image_editor->save( $uploads['path'] . "/$filename" );
278
279 if ( is_wp_error( $saved ) ) {
280 return $saved;
281 }
282
283 // Create new attachment post.
284 $attachment_post = array(
285 'post_mime_type' => $saved['mime-type'],
286 'guid' => $uploads['url'] . "/$filename",
287 'post_title' => $filename,
288 'post_content' => '',
289 );
290
291 $new_attachment_id = wp_insert_attachment( wp_slash( $attachment_post ), $saved['path'], 0, true );
292
293 if ( is_wp_error( $new_attachment_id ) ) {
294 if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
295 $new_attachment_id->add_data( array( 'status' => 500 ) );
296 } else {
297 $new_attachment_id->add_data( array( 'status' => 400 ) );
298 }
299
300 return $new_attachment_id;
301 }
302
303 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
304 // Set a custom header with the attachment_id.
305 // Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
306 header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id );
307 }
308
309 // Generate image sub-sizes and meta.
310 $new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] );
311
312 // Copy the EXIF metadata from the original attachment if not generated for the edited image.
313 if ( ! empty( $image_meta['image_meta'] ) ) {
314 $empty_image_meta = true;
315
316 if ( isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) {
317 $empty_image_meta = empty( array_filter( array_values( $new_image_meta['image_meta'] ) ) );
318 }
319
320 if ( $empty_image_meta ) {
321 $new_image_meta['image_meta'] = $image_meta['image_meta'];
322 }
323 }
324
325 // Reset orientation. At this point the image is edited and orientation is correct.
326 if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) {
327 $new_image_meta['image_meta']['orientation'] = 1;
328 }
329
330 // The attachment_id may change if the site is exported and imported.
331 $new_image_meta['parent_image'] = array(
332 'attachment_id' => $attachment_id,
333 // Path to the originally uploaded image file relative to the uploads directory.
334 'file' => _wp_relative_upload_path( $image_file ),
335 );
336
337 /**
338 * Filters the updated attachment meta data.
339 *
340 * @since 5.5.0
341 *
342 * @param array $data Array of updated attachment meta data.
343 * @param int $new_attachment_id Attachment post ID.
344 * @param int $attachment_id Original Attachment post ID.
345 */
346 $new_image_meta = apply_filters( 'wp_edited_attachment_metadata', $new_image_meta, $new_attachment_id, $attachment_id );
347
348 wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );
349
350 $path = '/wp/v2/media/' . $new_attachment_id;
351 $new_request = new WP_REST_Request( 'GET', $path );
352 $new_request->set_query_params( array( 'context' => 'edit' ) );
353 $response = rest_do_request( $new_request );
354
355 if ( ! $response->is_error() ) {
356 $response->set_status( 201 );
357 $response->header( 'Location', rest_url( $path ) );
358 }
359
360 return $response;
361 }
362 }
363