Completed
Push — update/crm-integration-fixes ( a3fc2b )
by Jeremy
14:51 queued 06:26
created

class.wpcom-json-api-edit-media-v1-2-endpoint.php (9 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
jetpack_require_lib( 'class.media' );
4
5
define( 'REVISION_HISTORY_MAXIMUM_AMOUNT', 0 );
6
define( 'WP_ATTACHMENT_IMAGE_ALT', '_wp_attachment_image_alt' );
7
8
new WPCOM_JSON_API_Edit_Media_v1_2_Endpoint( array(
9
	'description' => 'Edit a media item.',
10
	'group'       => 'media',
11
	'stat'        => 'media:1:POST',
12
	'min_version' => '1',
13
	'max_version' => '1.2',
14
	'method'      => 'POST',
15
	'path'        => '/sites/%s/media/%d/edit',
16
	'path_labels' => array(
17
		'$site'    => '(int|string) Site ID or domain',
18
		'$media_ID' => '(int) The ID of the media item',
19
	),
20
21
	'request_format' => array(
22
		'parent_id'   => '(int) ID of the post this media is attached to',
23
		'title'       => '(string) The file name.',
24
		'caption'     => '(string) File caption.',
25
		'description' => '(HTML) Description of the file.',
26
		'alt'         => "(string) Alternative text for image files.",
27
		'artist'      => "(string) Audio Only. Artist metadata for the audio track.",
28
		'album'       => "(string) Audio Only. Album metadata for the audio track.",
29
		'media'       => "(object) An object file to attach to the post. To upload media, " .
30
						   "the entire request should be multipart/form-data encoded. " .
31
						   "Multiple media items will be displayed in a gallery. Accepts " .
32
						   "jpg, jpeg, png, gif, pdf, doc, ppt, odt, pptx, docx, pps, ppsx, xls, xlsx, key. " .
33
						   "Audio and Video may also be available. See <code>allowed_file_types</code> " .
34
						   "in the options response of the site endpoint. " .
35
						   "<br /><br /><strong>Example</strong>:<br />" .
36
						   "<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/posts/new'</code>",
37
		'attrs'       => "(object) An Object of attributes (`title`, `description` and `caption`) " .
38
						   "are supported to assign to the media uploaded via the `media` or `media_url`",
39
		'media_url'   => "(string) An URL of the image to attach to a post.",
40
	),
41
42
	'response_format' => array(
43
		'ID'               => '(int) The ID of the media item',
44
		'date'             => '(ISO 8601 datetime) The date the media was uploaded',
45
		'post_ID'          => '(int) ID of the post this media is attached to',
46
		'author_ID'        => '(int) ID of the user who uploaded the media',
47
		'URL'              => '(string) URL to the file',
48
		'guid'             => '(string) Unique identifier',
49
		'file'             => '(string) File name',
50
		'extension'        => '(string) File extension',
51
		'mime_type'        => '(string) File mime type',
52
		'title'            => '(string) File name',
53
		'caption'          => '(string) User provided caption of the file',
54
		'description'      => '(string) Description of the file',
55
		'alt'              => '(string)  Alternative text for image files.',
56
		'thumbnails'       => '(object) Media item thumbnail URL options',
57
		'height'           => '(int) (Image & video only) Height of the media item',
58
		'width'            => '(int) (Image & video only) Width of the media item',
59
		'length'           => '(int) (Video & audio only) Duration of the media item, in seconds',
60
		'exif'             => '(array) (Image & audio only) Exif (meta) information about the media item',
61
		'videopress_guid'  => '(string) (Video only) VideoPress GUID of the video when uploaded on a blog with VideoPress',
62
		'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.',
63
		'revision_history' => '(object) An object with `items` and `original` keys. ' .
64
								'`original` is an object with data about the original image. ' .
65
								'`items` is an array of snapshots of the previous images of this Media. ' .
66
								'Each item has the `URL`, `file, `extension`, `date`, and `mime_type` fields.'
67
	),
68
69
	'example_request'      => 'https://public-api.wordpress.com/rest/v1.2/sites/82974409/media/446',
70
	'example_request_data' =>  array(
71
		'headers' => array(
72
			'authorization' => 'Bearer YOUR_API_TOKEN'
73
		),
74
		'body' => array(
75
			'title' => 'Updated Title'
76
		)
77
	)
78
) );
79
80
class WPCOM_JSON_API_Edit_Media_v1_2_Endpoint extends WPCOM_JSON_API_Update_Media_v1_1_Endpoint {
81
	/**
82
	 * Return an array of mime_type items allowed when the media file is uploaded.
83
	 *
84
	 * @return {Array} mime_type array
85
	 */
86 View Code Duplication
	static function get_allowed_mime_types( $default_mime_types ) {
87
		return array_unique( array_merge( $default_mime_types, array(
88
			'application/msword',                                                         // .doc
89
			'application/vnd.ms-powerpoint',                                              // .ppt, .pps
90
			'application/vnd.ms-excel',                                                   // .xls
91
			'application/vnd.openxmlformats-officedocument.presentationml.presentation',  // .pptx
92
			'application/vnd.openxmlformats-officedocument.presentationml.slideshow',     // .ppsx
93
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',          // .xlsx
94
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',    // .docx
95
			'application/vnd.oasis.opendocument.text',                                    // .odt
96
			'application/pdf',                                                            // .pdf
97
		) ) );
98
	}
99
100
	/**
101
	 * Update the media post grabbing the post values from
102
	 * the `attrs` parameter
103
	 *
104
	 * @param  {Number} $media_id - post media ID
105
	 * @param  {Object} $attrs - `attrs` parameter sent from the client in the request body
106
	 * @return
107
	 */
108
	private function update_by_attrs_parameter( $media_id, $attrs ) {
109
		$insert = array();
110
111
		// Attributes: Title, Caption, Description
112
		if ( isset( $attrs['title'] ) ) {
113
			$insert['post_title'] = $attrs['title'];
114
		}
115
116
		if ( isset( $attrs['caption'] ) ) {
117
			$insert['post_excerpt'] = $attrs['caption'];
118
		}
119
120
		if ( isset( $attrs['description'] ) ) {
121
			$insert['post_content'] = $attrs['description'];
122
		}
123
124
		if ( ! empty( $insert ) ) {
125
			$insert['ID'] = $media_id;
126
			$update_action = wp_update_post( (object) $insert );
127
			if ( is_wp_error( $update_action ) ) {
128
				return $update_action;
129
			}
130
		}
131
132
		// Attributes: Alt
133
		if ( isset( $attrs['alt'] ) ) {
134
			$alt = wp_strip_all_tags( $attrs['alt'], true );
135
			$post_update_action = update_post_meta( $media_id, WP_ATTACHMENT_IMAGE_ALT, $alt );
136
137
			if ( is_wp_error( $post_update_action ) ) {
138
				return $post_update_action;
139
			}
140
		}
141
142
		// Attributes: Artist, Album
143
		$id3_meta = array();
144
145 View Code Duplication
		foreach ( array( 'artist', 'album' ) as $key ) {
146
			if ( isset( $attrs[ $key ] ) ) {
147
				$id3_meta[ $key ] = wp_strip_all_tags( $attrs[ $key ], true );
148
			}
149
		}
150
151
		if ( ! empty( $id3_meta ) ) {
152
			// Before updating metadata, ensure that the item is audio
153
			$item = $this->get_media_item_v1_1( $media_id );
154
			if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
155
				$update_action = wp_update_attachment_metadata( $media_id, $id3_meta );
156
				if ( is_wp_error( $update_action ) ) {
157
					return $update_action;
158
				}
159
			}
160
		}
161
162
		return $post_update_action;
163
	}
164
165
	/**
166
	 * Return an object to be used to store into the revision_history
167
	 *
168
	 * @param  {Object} $media_item - media post object
169
	 * @return {Object} the snapshot object
170
	 */
171 View Code Duplication
	private function get_snapshot( $media_item ) {
172
		$current_file = get_attached_file( $media_item->ID );
173
		$file_paths = pathinfo( $current_file );
174
175
		$snapshot = array(
176
			'date'             => (string) $this->format_date( $media_item->post_modified_gmt, $media_item->post_modified ),
177
			'URL'              => (string) wp_get_attachment_url( $media_item->ID ),
178
			'file'             => (string) $file_paths['basename'],
179
			'extension'        => (string) $file_paths['extension'],
180
			'mime_type'        => (string) $media_item->post_mime_type,
181
			'size'             => (int) filesize( $current_file )
182
		);
183
184
		return (object) $snapshot;
185
	}
186
187
	/**
188
	 * Try to remove the temporal file from the given file array.
189
	 *
190
	 * @param  {Array} $file_array - Array with data about the temporal file
191
	 * @return {Boolean} `true` if the file has been removed.
192
	 *                   `false` either the file doesn't exist or it couldn't be removed.
193
	 */
194
	private function remove_tmp_file( $file_array ) {
195
		if ( ! file_exists ( $file_array['tmp_name'] ) ) {
196
			return false;
197
		}
198
		return @unlink( $file_array['tmp_name'] );
199
	}
200
201
	/**
202
	 * Save the given temporal file in a local folder.
203
	 *
204
	 * @param  {Array} $file_array
205
	 * @param  {Number} $media_id
206
	 * @return {Array|WP_Error} An array with information about the new file saved or a WP_Error is something went wrong.
207
	 */
208
	private function save_temporary_file( $file_array, $media_id ) {
209
		$tmp_filename = $file_array['tmp_name'];
210
211
		if ( ! file_exists( $tmp_filename ) ) {
212
			return new WP_Error( 'invalid_input', 'No media provided in input.' );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
213
		}
214
215
		// add additional mime_types through of the `jetpack_supported_media_sideload_types` filter
216
		$mime_type_static_filter = array(
217
			'WPCOM_JSON_API_Edit_Media_v1_2_Endpoint',
218
			'get_allowed_mime_types'
219
		);
220
221
		add_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
222 View Code Duplication
		if (
223
			! $this->is_file_supported_for_sideloading( $tmp_filename ) &&
224
			! file_is_displayable_image( $tmp_filename )
225
		) {
226
			@unlink( $tmp_filename );
227
			return new WP_Error( 'invalid_input', 'Invalid file type.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_input'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
228
		}
229
		remove_filter( 'jetpack_supported_media_sideload_types', $mime_type_static_filter );
230
231
		// generate a new file name
232
		$tmp_new_filename = Jetpack_Media::generate_new_filename( $media_id, $file_array[ 'name' ] );
233
234
		// start to create the parameters to move the temporal file
235
		$overrides = array( 'test_form' => false );
236
237
		$time = $this->get_time_string_from_guid( $media_id );
238
239
		$file_array['name'] = $tmp_new_filename;
240
		$file = wp_handle_sideload( $file_array, $overrides, $time );
241
242
		$this->remove_tmp_file( $file_array );
243
244
		if ( isset( $file['error'] ) ) {
245
			return new WP_Error( 'upload_error', $file['error'] );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'upload_error'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
246
		}
247
248
		return $file;
249
	}
250
251
	/**
252
	 * File urls use the post date to generate a folder path.
253
	 * Post dates can change, so we use the original date used in the guid
254
	 * url so edits can remain in the same folder. In the following function
255
	 * we capture a string in the format of `YYYY/MM` from the guid.
256
	 *
257
	 * For example with a guid of
258
	 * "http://test.files.wordpress.com/2016/10/test.png" the resulting string
259
	 * would be: "2016/10"
260
	 *
261
	 * @param $media_id
262
	 *
263
	 * @return string
264
	 */
265 View Code Duplication
	private function get_time_string_from_guid( $media_id ) {
266
		$time = date( "Y/m", strtotime( current_time( 'mysql' ) ) );
267
		if ( $media = get_post( $media_id ) ) {
268
			$pattern = '/\/(\d{4}\/\d{2})\//';
269
			preg_match( $pattern, $media->guid, $matches );
270
			if ( count( $matches ) > 1 ) {
271
				$time = $matches[1];
272
			}
273
		}
274
		return $time;
275
	}
276
277
	/**
278
	 * Get the image from a remote url and then save it locally.
279
	 *
280
	 * @param  {Number} $media_id - media post ID
281
	 * @param  {String} $url - image URL to save locally
282
	 * @return {Array|WP_Error} An array with information about the new file saved or a WP_Error is something went wrong.
283
	 */
284
	private function build_file_array_from_url( $media_id, $url ) {
285
		if ( ! $url ) {
286
			return null;
287
		}
288
289
		// if we didn't get a URL, let's bail
290
		$parsed = wp_parse_url( $url );
291
		if ( empty( $parsed ) ) {
292
			return new WP_Error( 'invalid_url', 'No media provided in url.' );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'invalid_url'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
293
		}
294
295
		// save the remote image into a tmp file
296
		$tmp = download_url( wpcom_get_private_file( $url ) );
297
		if ( is_wp_error( $tmp ) ) {
298
			return $tmp;
299
		}
300
301
		return array(
302
			'name' => basename( $url ),
303
			'tmp_name' => $tmp
304
		);
305
	}
306
307
	/**
308
	 * Add a new item into revision_history array.
309
	 *
310
	 * @param  {Object} $media_item         - media post
311
	 * @param  {file} $file               - file recentrly added
312
	 * @param  {Boolean} $has_original_media - condition is the original media has been already added
313
	 * @return {Boolean} `true` if the item has been added. Otherwise `false`.
314
	 */
315
	private function register_revision( $media_item, $file, $has_original_media ) {
316
		if (
317
			is_wp_error( $file ) ||
318
			! $has_original_media
319
		) {
320
			return false;
321
		}
322
323
		add_post_meta( $media_item->ID, Jetpack_Media::$WP_REVISION_HISTORY, $this->get_snapshot( $media_item ) );
324
	}
325
326
	function callback( $path = '', $blog_id = 0, $media_id = 0 ) {
327
		$blog_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $blog_id ) );
328
		if ( is_wp_error( $blog_id ) ) {
329
			return $blog_id;
330
		}
331
332
		$media_item = get_post( $media_id );
333
334
		if ( ! $media_item || is_wp_error( $media_item ) ) {
335
			return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unknown_media'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
336
		}
337
338
		if ( is_wp_error( $media_item ) ) {
339
			return $media_item;
340
		}
341
342
		if ( ! current_user_can( 'upload_files', $media_id ) ) {
343
			return new WP_Error( 'unauthorized', 'User cannot view media', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
344
		}
345
346
		$input = $this->input( true );
347
348
		// Images.
349
		$media_file  = isset( $input['media'] ) ? (array) $input['media'] : null;
350
		$media_url   = isset( $input['media_url'] ) ? $input['media_url'] : null;
351
		$media_attrs = isset( $input['attrs'] ) ? (array) $input['attrs'] : null;
352
353
		if ( isset( $media_url ) || $media_file ) {
354
			$user_can_upload_files = current_user_can( 'upload_files' ) || $this->api->is_authorized_with_upload_token();
0 ignored issues
show
The method is_authorized_with_upload_token() does not seem to exist on object<WPCOM_JSON_API>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
355
356
			if ( ! $user_can_upload_files  ) {
357
				return new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
358
			}
359
360
			$has_original_media = Jetpack_Media::get_original_media( $media_id );
361
362 View Code Duplication
			if ( ! $has_original_media ) {
363
				// The first time that the media is updated
364
				// the original media is stored into the revision_history
365
				$snapshot = $this->get_snapshot( $media_item );
366
				add_post_meta( $media_id, Jetpack_Media::$WP_ORIGINAL_MEDIA, $snapshot, true );
367
			}
368
369
			// save the temporal file locally
370
			$temporal_file = $media_file ? $media_file : $this->build_file_array_from_url( $media_id, $media_url );
371
372
			if ( is_wp_error( $temporal_file ) ) {
373
				return $temporal_file;
374
			}
375
376
			$uploaded_file = $this->save_temporary_file( $temporal_file, $media_id );
377
378
			if ( is_wp_error( $uploaded_file ) ) {
379
				return $uploaded_file;
380
			}
381
382
			// revision_history control
383
			$this->register_revision( $media_item, $uploaded_file, $has_original_media );
384
385
			$uploaded_path = $uploaded_file['file'];
386
			$udpated_mime_type = $uploaded_file['type'];
387
			$was_updated = update_attached_file( $media_id, $uploaded_path );
388
389
			if ( $was_updated ) {
390
				$new_metadata = wp_generate_attachment_metadata( $media_id, $uploaded_path );
391
				wp_update_attachment_metadata( $media_id, $new_metadata );
392
393
				// check maximum amount of revision_history
394
				Jetpack_Media::limit_revision_history( $media_id, REVISION_HISTORY_MAXIMUM_AMOUNT );
395
396
				wp_update_post( (object) array(
397
					'ID'             => $media_id,
398
					'post_mime_type' => $udpated_mime_type
399
				) );
400
			}
401
402
			unset( $input['media'] );
403
			unset( $input['media_url'] );
404
			unset( $input['attrs'] );
405
		}
406
407
		// update media through of `attrs` value it it's defined
408
		if ( ( $media_file || isset( $media_url ) ) && $media_attrs ) {
409
			$was_updated = $this->update_by_attrs_parameter( $media_id, $media_attrs );
410
411
			if ( is_wp_error( $was_updated ) ) {
412
				return $was_updated;
413
			}
414
		}
415
416
		// call parent method
417
		$response = parent::callback( $path, $blog_id, $media_id );
0 ignored issues
show
It seems like $blog_id defined by $this->api->switch_to_bl...>get_blog_id($blog_id)) on line 327 can also be of type object<WP_Error>; however, WPCOM_JSON_API_Update_Me..._1_Endpoint::callback() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
418
419
		// expose `revision_history` object
420
		$response->revision_history = (object) array(
421
			'items'       => (array) Jetpack_Media::get_revision_history( $media_id ),
422
			'original'    => (object) Jetpack_Media::get_original_media( $media_id )
423
		);
424
425
		return $response;
426
	}
427
}
428