PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | json-endpoints/class.wpcom-json-api-edit-media-v1-2-endpoint.php +503 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,503 @@
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +
3 +if ( ! defined( 'ABSPATH' ) ) {
4 + exit( 0 );
5 +}
6 +
7 +require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media.php';
8 +
9 +define( 'REVISION_HISTORY_MAXIMUM_AMOUNT', 5 );
10 +define( 'WP_ATTACHMENT_IMAGE_ALT', '_wp_attachment_image_alt' );
11 +
12 +new WPCOM_JSON_API_Edit_Media_v1_2_Endpoint(
13 + array(
14 + 'description' => 'Edit a media item.',
15 + 'group' => 'media',
16 + 'stat' => 'media:1:POST',
17 + 'min_version' => '1',
18 + 'max_version' => '1.2',
19 + 'method' => 'POST',
20 + 'path' => '/sites/%s/media/%d/edit',
21 + 'path_labels' => array(
22 + '$site' => '(int|string) Site ID or domain',
23 + '$media_ID' => '(int) The ID of the media item',
24 + ),
25 +
26 + 'request_format' => array(
27 + 'parent_id' => '(int) ID of the post this media is attached to',
28 + 'title' => '(string) The file name.',
29 + 'caption' => '(string) File caption.',
30 + 'description' => '(HTML) Description of the file.',
31 + 'alt' => '(string) Alternative text for image files.',
32 + 'artist' => '(string) Audio Only. Artist metadata for the audio track.',
33 + 'album' => '(string) Audio Only. Album metadata for the audio track.',
34 + 'media' => '(media) An object file to attach to the post. To upload media, ' .
35 + 'the entire request should be multipart/form-data encoded. ' .
36 + 'Multiple media items will be displayed in a gallery. Accepts ' .
37 + 'jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. ' .
38 + 'Audio and Video may also be available. See <code>allowed_file_types</code> ' .
39 + 'in the options response of the site endpoint. ' .
40 + '<br /><br /><strong>Example</strong>:<br />' .
41 + "<code>curl \<br />--form 'title=Image' \<br />--form 'media=@/path/to/file.jpg' \<br />-H 'Authorization: BEARER your-token' \<br />'https://public-api.wordpress.com/rest/v1/sites/123/media/new'</code>",
42 + 'attrs' => '(object) An Object of attributes (`title`, `description` and `caption`) ' .
43 + 'are supported to assign to the media uploaded via the `media` or `media_url`',
44 + 'media_url' => '(string) An URL of the image to attach to a post.',
45 + ),
46 +
47 + 'response_format' => array(
48 + 'ID' => '(int) The ID of the media item',
49 + 'date' => '(ISO 8601 datetime) The date the media was uploaded',
50 + 'post_ID' => '(int) ID of the post this media is attached to',
51 + 'author_ID' => '(int) ID of the user who uploaded the media',
52 + 'URL' => '(string) URL to the file',
53 + 'guid' => '(string) Unique identifier',
54 + 'file' => '(string) File name',
55 + 'extension' => '(string) File extension',
56 + 'mime_type' => '(string) File mime type',
57 + 'title' => '(string) File name',
58 + 'caption' => '(string) User provided caption of the file',
59 + 'description' => '(string) Description of the file',
60 + 'alt' => '(string) Alternative text for image files.',
61 + 'thumbnails' => '(object) Media item thumbnail URL options',
62 + 'height' => '(int) (Image & video only) Height of the media item',
63 + 'width' => '(int) (Image & video only) Width of the media item',
64 + 'length' => '(int) (Video & audio only) Duration of the media item, in seconds',
65 + 'exif' => '(array) (Image & audio only) Exif (meta) information about the media item',
66 + 'videopress_guid' => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
67 + 'videopress_processing_done' => '(bool) (Video only) If the video is uploaded on a blog with VideoPress, this will return the status of processing on the video.',
68 + 'revision_history' => '(object) An object with `items` and `original` keys. ' .
69 + '`original` is an object with data about the original image. ' .
70 + '`items` is an array of snapshots of the previous images of this Media. ' .
71 + 'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.',
72 + ),
73 +
74 + 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/446',
75 + 'example_request_data' => array(
76 + 'headers' => array(
77 + 'authorization' => 'Bearer YOUR_API_TOKEN',
78 + ),
79 + 'body' => array(
80 + 'title' => 'Updated Title',
81 + ),
82 + ),
83 + )
84 +);
85 +
86 +/**
87 + * Edit media v1_2 endpoint class.
88 + *
89 + * @phan-constructor-used-for-side-effects
90 + */
91 +class WPCOM_JSON_API_Edit_Media_v1_2_Endpoint extends WPCOM_JSON_API_Update_Media_v1_1_Endpoint { //phpcs:ignore
92 + /**
93 + * Return an array of mime_type items allowed when the media file is uploaded.
94 + *
95 + * @param array $default_mime_types - array of default mime types.
96 + *
97 + * @return array mime_type array
98 + */
99 + public static function get_allowed_mime_types( $default_mime_types ) {
100 + return array_unique(
101 + array_merge(
102 + $default_mime_types,
103 + array(
104 + 'application/msword', // .doc
105 + 'application/vnd.ms-powerpoint', // .ppt, .pps
106 + 'application/vnd.ms-excel', // .xls
107 + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
108 + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', // .ppsx
109 + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
110 + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
111 + 'application/vnd.oasis.opendocument.text', // .odt
112 + 'application/pdf', // .pdf
113 + )
114 + )
115 + );
116 + }
117 +
118 + /**
119 + * Update the media post grabbing the post values from
120 + * the `attrs` parameter
121 + *
122 + * @param int $media_id - post media ID.
123 + * @param array $attrs - `attrs` parameter sent from the client in the request body.
124 + */
125 + private function update_by_attrs_parameter( $media_id, $attrs ) {
126 + $post_update_action = null;
127 + $insert = array();
128 +
129 + // Attributes: Title, Caption, Description.
130 + if ( isset( $attrs['title'] ) ) {
131 + $insert['post_title'] = $attrs['title'];
132 + }
133 +
134 + if ( isset( $attrs['caption'] ) ) {
135 + $insert['post_excerpt'] = $attrs['caption'];
136 + }
137 +
138 + if ( isset( $attrs['description'] ) ) {
139 + $insert['post_content'] = $attrs['description'];
140 + }
141 +
142 + if ( ! empty( $insert ) ) {
143 + $insert['ID'] = $media_id;
144 + $update_action = wp_update_post( (object) $insert );
145 + if ( is_wp_error( $update_action ) ) {
146 + return $update_action;
147 + }
148 + }
149 +
150 + // Attributes: Alt.
151 + if ( isset( $attrs['alt'] ) ) {
152 + $alt = wp_strip_all_tags( $attrs['alt'], true );
153 + $post_update_action = update_post_meta( $media_id, WP_ATTACHMENT_IMAGE_ALT, $alt );
154 +
155 + if ( is_wp_error( $post_update_action ) ) {
156 + return $post_update_action;
157 + }
158 + }
159 +
160 + // Attributes: Artist, Album.
161 + $id3_meta = array();
162 +
163 + foreach ( array( 'artist', 'album' ) as $key ) {
164 + if ( isset( $attrs[ $key ] ) ) {
165 + $id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
166 + }
167 + }
168 +
169 + if ( ! empty( $id3_meta ) ) {
170 + // Before updating metadata, ensure that the item is audio.
171 + $item = $this->get_media_item_v1_1( $media_id );
172 + if ( str_starts_with( $item->mime_type, 'audio/' ) ) {
173 + $update_action = wp_update_attachment_metadata( $media_id, $id3_meta );
174 + if ( is_wp_error( $update_action ) ) {
175 + return $update_action;
176 + }
177 + }
178 + }
179 +
180 + return $post_update_action;
181 + }
182 +
183 + /**
184 + * Return an object to be used to store into the revision_history
185 + *
186 + * @param object $media_item - media post object.
187 + * @return object the snapshot object
188 + */
189 + private function get_snapshot( $media_item ) {
190 + $current_file = get_attached_file( $media_item->ID );
191 + $file_paths = pathinfo( $current_file );
192 +
193 + $snapshot = array(
194 + 'date' => (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
195 + 'URL' => (string) wp_get_attachment_url( $media_item->ID ),
196 + 'file' => (string) $file_paths['basename'],
197 + 'extension' => (string) $file_paths['extension'],
198 + 'mime_type' => (string) $media_item->post_mime_type,
199 + 'size' => (int) filesize( $current_file ),
200 + );
201 +
202 + return (object) $snapshot;
203 + }
204 +
205 + /**
206 + * Try to remove the temporal file from the given file array.
207 + *
208 + * @param array $file_array - Array with data about the temporal file.
209 + */
210 + private function remove_tmp_file( $file_array ) {
211 + if ( file_exists( $file_array['tmp_name'] ) ) {
212 + wp_delete_file( $file_array['tmp_name'] );
213 + }
214 + }
215 +
216 + /**
217 + * Save the given temporal file in a local folder.
218 + *
219 + * @param array $file_array - array containing file data.
220 + * @param int $media_id - the media id.
221 + * @param bool $is_upload - True if `$file_array` derives from an upload in `$_FILES`, false if this is a sideload.
222 + * @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong.
223 + */
224 + private function save_temporary_file( $file_array, $media_id, $is_upload ) {
225 + $tmp_filename = $file_array['tmp_name'];
226 +
227 + $is_ok = $is_upload ? is_uploaded_file( $tmp_filename ) : file_exists( $tmp_filename );
228 + if ( ! $is_ok ) {
229 + return new WP_Error( 'invalid_input', 'No media provided in input.' );
230 + }
231 +
232 + // add additional mime_types through of the `jetpack_supported_media_sideload_types` filter.
233 + $mime_type_static_filter = array(
234 + 'WPCOM_JSON_API_Edit_Media_v1_2_Endpoint',
235 + 'get_allowed_mime_types',
236 + );
237 +
238 + add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
239 + if (
240 + ! $this->is_file_supported_for_sideloading( $tmp_filename ) &&
241 + ! file_is_displayable_image( $tmp_filename )
242 + ) {
243 + if ( ! $is_upload ) {
244 + wp_delete_file( $tmp_filename );
245 + }
246 + return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
247 + }
248 + remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
249 +
250 + // generate a new file name.
251 + $tmp_new_filename = Jetpack_Media::generate_new_filename( $media_id, $file_array['name'] );
252 +
253 + // start to create the parameters to move the temporal file.
254 + $overrides = array( 'test_form' => false );
255 +
256 + $time = $this->get_time_string_from_guid( $media_id );
257 +
258 + $file_array['name'] = $tmp_new_filename;
259 + if ( $is_upload ) {
260 + $file = wp_handle_upload( $file_array, $overrides, $time );
261 + } else {
262 + $file = wp_handle_sideload( $file_array, $overrides, $time );
263 + $this->remove_tmp_file( $file_array );
264 + }
265 +
266 + if ( isset( $file['error'] ) ) {
267 + return new WP_Error( 'upload_error', $file['error'] );
268 + }
269 +
270 + return $file;
271 + }
272 +
273 + /**
274 + * File urls use the post date to generate a folder path.
275 + * Post dates can change, so we use the original date used in the guid
276 + * url so edits can remain in the same folder. In the following function
277 + * we capture a string in the format of `YYYY/MM` from the guid.
278 + *
279 + * For example with a guid of
280 + * "http://test.files.wordpress.com/2016/10/test.png" the resulting string
281 + * would be: "2016/10"
282 + *
283 + * @param int $media_id - the media id.
284 + *
285 + * @return string
286 + */
287 + private function get_time_string_from_guid( $media_id ) {
288 + // @todo: investigate if we can replace date with gmdate()
289 + // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
290 + $time = date( 'Y/m', strtotime( current_time( 'mysql' ) ) );
291 + $media = get_post( $media_id );
292 + if ( $media ) {
293 + $pattern = '/\/(\d{4}\/\d{2})\//';
294 + preg_match( $pattern, $media->guid, $matches );
295 + if ( count( $matches ) > 1 ) {
296 + $time = $matches[1];
297 + }
298 + }
299 + return $time;
300 + }
301 +
302 + /**
303 + * Get the image from a remote url and then save it locally.
304 + *
305 + * @param int $media_id - media post ID.
306 + * @param string $url - image URL to save locally.
307 + * @return array|WP_Error An array with information about the new file saved or a WP_Error is something went wrong.
308 + */
309 + private function build_file_array_from_url( $media_id, $url ) {
310 + if ( ! $url ) {
311 + return null;
312 + }
313 +
314 + // if we didn't get a URL, let's bail.
315 + $parsed = wp_parse_url( $url );
316 + if ( empty( $parsed ) ) {
317 + return new WP_Error( 'invalid_url', 'No media provided in url.' );
318 + }
319 +
320 + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
321 + $url = wpcom_get_private_file( $url );
322 + }
323 +
324 + // save the remote image into a tmp file.
325 + $tmp = download_url( $url );
326 + if ( is_wp_error( $tmp ) ) {
327 + return $tmp;
328 + }
329 +
330 + return array(
331 + 'name' => basename( $url ),
332 + 'tmp_name' => $tmp,
333 + );
334 + }
335 +
336 + /**
337 + * Add a new item into revision_history array.
338 + *
339 + * @param object $media_item - media post.
340 + * @param array|WP_Error $file - File data, or WP_Error on error.
341 + * @param bool $has_original_media - condition is the original media has been already added.
342 + * @return bool `true` if the item has been added. Otherwise `false`.
343 + */
344 + private function register_revision( $media_item, $file, $has_original_media ) {
345 + if (
346 + is_wp_error( $file ) ||
347 + ! $has_original_media
348 + ) {
349 + return false;
350 + }
351 +
352 + add_post_meta( $media_item->ID, Jetpack_Media::WP_REVISION_HISTORY, $this->get_snapshot( $media_item ) );
353 + }
354 +
355 + /**
356 + * Restore the original media file.
357 + *
358 + * @param int $media_id - media post ID.
359 + * @param object $original_media - orginal media data.
360 + * @return array - restore media info.
361 + */
362 + private function restore_original( $media_id, $original_media ) {
363 + $revisions = (array) Jetpack_Media::get_revision_history( $media_id );
364 + $revisions = array_filter(
365 + $revisions,
366 + function ( $revision ) use ( $original_media ) {
367 + return $revision->file !== $original_media->file;
368 + }
369 + );
370 + $criteria = array(
371 + 'from' => 0,
372 + 'to' => REVISION_HISTORY_MAXIMUM_AMOUNT,
373 + );
374 +
375 + Jetpack_Media::remove_items_from_revision_history( $media_id, $criteria, $revisions );
376 + $file = get_attached_file( $media_id );
377 + $file_parts = pathinfo( $file );
378 + $orginal_file = path_join( $file_parts['dirname'], $original_media->file );
379 + $restored_media = array(
380 + 'file' => $orginal_file,
381 + 'type' => $original_media->mime_type,
382 + );
383 +
384 + return $restored_media;
385 + }
386 +
387 + /**
388 + * API callback.
389 + *
390 + * @param string $path - the path.
391 + * @param int $blog_id - the blog ID.
392 + * @param int $media_id - the media ID.
393 + */
394 + public function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
395 + $blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
396 + if ( is_wp_error( $blog_id ) ) {
397 + return $blog_id;
398 + }
399 +
400 + $media_item = get_post( $media_id );
401 +
402 + if ( ! $media_item || is_wp_error( $media_item ) ) {
403 + return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
404 + }
405 +
406 + if ( is_wp_error( $media_item ) ) {
407 + return $media_item;
408 + }
409 +
410 + if ( ! $this->current_user_can_edit_media_item( $media_id ) ) {
411 + return new WP_Error( 'unauthorized', 'User cannot edit media', 403 );
412 + }
413 +
414 + $input = $this->input( true );
415 +
416 + // Images.
417 + $media_file = isset( $input['media'] ) ? (array) $input['media'] : null;
418 + $media_url = $input['media_url'] ?? null;
419 + $media_attrs = isset( $input['attrs'] ) ? (array) $input['attrs'] : null;
420 +
421 + if ( isset( $media_url ) || $media_file ) {
422 + $user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
423 +
424 + if ( ! $user_can_upload_files ) {
425 + return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
426 + }
427 +
428 + $has_original_media = Jetpack_Media::get_original_media( $media_id );
429 +
430 + if ( ! $has_original_media ) {
431 + // The first time that the media is updated
432 + // the original media is stored into the revision_history.
433 + $snapshot = $this->get_snapshot( $media_item );
434 + add_post_meta( $media_id, Jetpack_Media::WP_ORIGINAL_MEDIA, $snapshot, true );
435 + }
436 +
437 + // save the temporal file locally.
438 + $is_upload = (bool) $media_file;
439 + $temporal_file = $media_file ? $media_file : $this->build_file_array_from_url( $media_id, $media_url );
440 +
441 + if ( is_wp_error( $temporal_file ) ) {
442 + return $temporal_file;
443 + }
444 +
445 + // edited media is sent as $media_file and restored media is sent as $media_url
446 + $should_restore = isset( $media_url ) && ! isset( $media_file ) && $has_original_media;
447 +
448 + $uploaded_file = $should_restore
449 + ? $this->restore_original( $media_id, $has_original_media )
450 + : $this->save_temporary_file( $temporal_file, $media_id, $is_upload );
451 +
452 + if ( is_wp_error( $uploaded_file ) ) {
453 + return $uploaded_file;
454 + }
455 +
456 + // revision_history control.
457 + $this->register_revision( $media_item, $uploaded_file, $has_original_media );
458 +
459 + $uploaded_path = $uploaded_file['file'];
460 + $udpated_mime_type = $uploaded_file['type'];
461 + $was_updated = update_attached_file( $media_id, $uploaded_path );
462 +
463 + if ( $was_updated ) {
464 + $new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
465 + wp_update_attachment_metadata( $media_id, $new_metadata );
466 +
467 + // check maximum amount of revision_history.
468 + Jetpack_Media::limit_revision_history( $media_id, REVISION_HISTORY_MAXIMUM_AMOUNT );
469 +
470 + wp_update_post(
471 + (object) array(
472 + 'ID' => $media_id,
473 + 'post_mime_type' => $udpated_mime_type,
474 + )
475 + );
476 + }
477 +
478 + unset( $input['media'] );
479 + unset( $input['media_url'] );
480 + unset( $input['attrs'] );
481 + }
482 +
483 + // update media through of `attrs` value it it's defined.
484 + if ( ( $media_file || isset( $media_url ) ) && $media_attrs ) {
485 + $was_updated = $this->update_by_attrs_parameter( $media_id, $media_attrs );
486 +
487 + if ( is_wp_error( $was_updated ) ) {
488 + return $was_updated;
489 + }
490 + }
491 +
492 + // call parent method.
493 + $response = parent::callback( $path, $blog_id, $media_id );
494 +
495 + // expose `revision_history` object.
496 + $response->revision_history = (object) array(
497 + 'items' => (array) Jetpack_Media::get_revision_history( $media_id ),
498 + 'original' => (object) Jetpack_Media::get_original_media( $media_id ),
499 + );
500 +
501 + return $response;
502 + }
503 +}