Completed
Push — update/build-to-prepare-for-wp... ( a960c5...31a28f )
by
unknown
156:22 queued 147:01
created

WPCOM_REST_API_V2_Endpoint_External_Media   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 486
rs 9.2
c 0
b 0
f 0
wmc 40
lcom 2
cbo 2

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B register_routes() 0 73 1
A permission_callback() 0 3 1
A create_item_permissions_check() 0 29 4
A sanitize_media() 0 5 1
A validate_media() 0 5 1
A prepare_media_param() 0 16 5
A copy_external_media() 0 30 4
B get_external_media() 0 57 8
A get_connection_details() 0 15 3
A delete_connection() 0 21 3
A tmp_name() 0 3 1
A get_download_url() 0 12 2
A sideload_media() 0 14 2
A update_attachment_meta() 0 15 1
A get_attachment_data() 0 21 2

How to fix   Complexity   

Complex Class

Complex classes like WPCOM_REST_API_V2_Endpoint_External_Media often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WPCOM_REST_API_V2_Endpoint_External_Media, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * REST API endpoint for the External Media.
4
 *
5
 * @package Jetpack
6
 * @since 8.7.0
7
 */
8
9
use Automattic\Jetpack\Connection\Client;
10
11
/**
12
 * External Media helper API.
13
 *
14
 * @since 8.7.0
15
 */
16
class WPCOM_REST_API_V2_Endpoint_External_Media extends WP_REST_Controller {
17
18
	/**
19
	 * Media argument schema for /copy endpoint.
20
	 *
21
	 * @var array
22
	 */
23
	public $media_schema = array(
24
		'type'       => 'object',
25
		'required'   => true,
26
		'properties' => array(
27
			'caption' => array(
28
				'type' => 'string',
29
			),
30
			'guid'    => array(
31
				'items' => array(
32
					'caption' => array(
33
						'type' => 'string',
34
					),
35
					'name'    => array(
36
						'type' => 'string',
37
					),
38
					'title'   => array(
39
						'type' => 'string',
40
					),
41
					'url'     => array(
42
						'format' => 'uri',
43
						'type'   => 'string',
44
					),
45
				),
46
				'type'  => 'array',
47
			),
48
			'title'   => array(
49
				'type' => 'string',
50
			),
51
		),
52
	);
53
54
	/**
55
	 * Service regex.
56
	 *
57
	 * @var string
58
	 */
59
	private static $services_regex = '(?P<service>google_photos|pexels)';
60
61
	/**
62
	 * Temporary filename.
63
	 *
64
	 * Needed to cope with Google's very long file names.
65
	 *
66
	 * @var string
67
	 */
68
	private $tmp_name;
69
70
	/**
71
	 * Constructor.
72
	 */
73
	public function __construct() {
74
		$this->namespace = 'wpcom/v2';
75
		$this->rest_base = 'external-media';
76
77
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
78
	}
79
80
	/**
81
	 * Registers the routes for external media.
82
	 */
83
	public function register_routes() {
84
		register_rest_route(
85
			$this->namespace,
86
			$this->rest_base . '/list/' . self::$services_regex,
87
			array(
88
				'methods'             => WP_REST_Server::READABLE,
89
				'callback'            => array( $this, 'get_external_media' ),
90
				'permission_callback' => array( $this, 'permission_callback' ),
91
				'args'                => array(
92
					'search'      => array(
93
						'description' => __( 'Media collection search term.', 'jetpack' ),
94
						'type'        => 'string',
95
					),
96
					'number'      => array(
97
						'description' => __( 'Number of media items in the request', 'jetpack' ),
98
						'type'        => 'number',
99
						'default'     => 20,
100
					),
101
					'path'        => array(
102
						'type' => 'string',
103
					),
104
					'page_handle' => array(
105
						'type' => 'string',
106
					),
107
				),
108
			)
109
		);
110
111
		register_rest_route(
112
			$this->namespace,
113
			$this->rest_base . '/copy/' . self::$services_regex,
114
			array(
115
				'methods'             => \WP_REST_Server::CREATABLE,
116
				'callback'            => array( $this, 'copy_external_media' ),
117
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
118
				'args'                => array(
119
					'media'   => array(
120
						'description'       => __( 'Media data to copy.', 'jetpack' ),
121
						'items'             => $this->media_schema,
122
						'required'          => true,
123
						'type'              => 'array',
124
						'sanitize_callback' => array( $this, 'sanitize_media' ),
125
						'validate_callback' => array( $this, 'validate_media' ),
126
					),
127
					'post_id' => array(
128
						'description' => __( 'The post ID to attach the upload to.', 'jetpack' ),
129
						'type'        => 'number',
130
						'minimum'     => 0,
131
					),
132
				),
133
			)
134
		);
135
136
		register_rest_route(
137
			$this->namespace,
138
			$this->rest_base . '/connection/(?P<service>google_photos)',
139
			array(
140
				'methods'             => \WP_REST_Server::READABLE,
141
				'callback'            => array( $this, 'get_connection_details' ),
142
				'permission_callback' => array( $this, 'permission_callback' ),
143
			)
144
		);
145
146
		register_rest_route(
147
			$this->namespace,
148
			$this->rest_base . '/connection/(?P<service>google_photos)',
149
			array(
150
				'methods'             => \WP_REST_Server::DELETABLE,
151
				'callback'            => array( $this, 'delete_connection' ),
152
				'permission_callback' => array( $this, 'permission_callback' ),
153
			)
154
		);
155
	}
156
157
	/**
158
	 * Checks if a given request has access to external media libraries.
159
	 */
160
	public function permission_callback() {
161
		return current_user_can( 'edit_posts' );
162
	}
163
164
	/**
165
	 * Checks if a given request has access to create an attachment.
166
	 *
167
	 * @param WP_REST_Request $request Full details about the request.
168
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
169
	 */
170
	public function create_item_permissions_check( $request ) {
171
		if ( ! empty( $request['id'] ) ) {
172
			return new WP_Error(
173
				'rest_post_exists',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_post_exists'.

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...
174
				__( 'Cannot create existing post.', 'jetpack' ),
175
				array( 'status' => 400 )
176
			);
177
		}
178
179
		$post_type = get_post_type_object( 'attachment' );
180
181
		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
182
			return new WP_Error(
183
				'rest_cannot_create',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_cannot_create'.

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...
184
				__( 'Sorry, you are not allowed to create posts as this user.', 'jetpack' ),
185
				array( 'status' => rest_authorization_required_code() )
186
			);
187
		}
188
189
		if ( ! current_user_can( 'upload_files' ) ) {
190
			return new WP_Error(
191
				'rest_cannot_create',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_cannot_create'.

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...
192
				__( 'Sorry, you are not allowed to upload media on this site.', 'jetpack' ),
193
				array( 'status' => 400 )
194
			);
195
		}
196
197
		return true;
198
	}
199
200
	/**
201
	 * Sanitization callback for media parameter.
202
	 *
203
	 * @param array $param Media parameter.
204
	 * @return true|\WP_Error
205
	 */
206
	public function sanitize_media( $param ) {
207
		$param = $this->prepare_media_param( $param );
208
209
		return rest_sanitize_value_from_schema( $param, $this->media_schema );
210
	}
211
212
	/**
213
	 * Validation callback for media parameter.
214
	 *
215
	 * @param array $param Media parameter.
216
	 * @return true|\WP_Error
217
	 */
218
	public function validate_media( $param ) {
219
		$param = $this->prepare_media_param( $param );
220
221
		return rest_validate_value_from_schema( $param, $this->media_schema, 'media' );
222
	}
223
224
	/**
225
	 * Decodes guid json and sets parameter defaults.
226
	 *
227
	 * @param array $param Media parameter.
228
	 * @return array
229
	 */
230
	private function prepare_media_param( $param ) {
231
		foreach ( $param as $key => $item ) {
232
			if ( ! empty( $item['guid'] ) ) {
233
				$param[ $key ]['guid'] = json_decode( $item['guid'], true );
234
			}
235
236
			if ( empty( $param[ $key ]['caption'] ) ) {
237
				$param[ $key ]['caption'] = '';
238
			}
239
			if ( empty( $param[ $key ]['title'] ) ) {
240
				$param[ $key ]['title'] = '';
241
			}
242
		}
243
244
		return $param;
245
	}
246
247
	/**
248
	 * Retrieves media items from external libraries.
249
	 *
250
	 * @param \WP_REST_Request $request Full details about the request.
251
	 * @return array|\WP_Error|mixed
252
	 */
253
	public function get_external_media( \WP_REST_Request $request ) {
254
		$params     = $request->get_params();
255
		$wpcom_path = sprintf( '/meta/external-media/%s', rawurlencode( $params['service'] ) );
256
257
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
258
			$request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
259
			$request->set_query_params( $params );
260
261
			return rest_do_request( $request );
262
		}
263
264
		// Build query string to pass to wpcom endpoint.
265
		$service_args = array_filter(
266
			$params,
267
			function ( $key ) {
268
				return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter' ), true );
269
			},
270
			ARRAY_FILTER_USE_KEY
271
		);
272
		if ( ! empty( $service_args ) ) {
273
			$wpcom_path .= '?' . http_build_query( $service_args );
274
		}
275
276
		$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
277
278
		switch ( wp_remote_retrieve_response_code( $response ) ) {
279
			case 200:
280
				$response = json_decode( wp_remote_retrieve_body( $response ), true );
281
				break;
282
283
			case 401:
284
				$response = new WP_Error(
285
					'authorization_required',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'authorization_required'.

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...
286
					__( 'You are not connected to that service.', 'jetpack' ),
287
					array( 'status' => 403 )
288
				);
289
				break;
290
291
			case 403:
292
				$error    = json_decode( wp_remote_retrieve_body( $response ) );
293
				$response = new WP_Error( $error->code, $error->message, $error->data );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $error->code.

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...
294
				break;
295
296
			default:
297
				if ( is_wp_error( $response ) ) {
298
					$response->add_data( array( 'status' => 400 ) );
0 ignored issues
show
Bug introduced by
The method add_data() does not seem to exist on object<WP_Error>.

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...
299
					break;
300
				}
301
				$response = new WP_Error(
302
					'rest_request_error',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_request_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...
303
					__( 'An unknown error has occurred. Please try again later.', 'jetpack' ),
304
					array( 'status' => wp_remote_retrieve_response_code( $response ) )
305
				);
306
		}
307
308
		return $response;
309
	}
310
311
	/**
312
	 * Saves an external media item to the media library.
313
	 *
314
	 * @param \WP_REST_Request $request Full details about the request.
315
	 * @return array|\WP_Error|mixed
316
	 */
317
	public function copy_external_media( \WP_REST_Request $request ) {
318
		require_once ABSPATH . 'wp-admin/includes/file.php';
319
		require_once ABSPATH . 'wp-admin/includes/media.php';
320
		require_once ABSPATH . 'wp-admin/includes/image.php';
321
322
		$post_id = $request->get_param( 'post_id' );
323
324
		$responses = array();
325
		foreach ( $request->get_param( 'media' ) as $item ) {
326
			// Download file to temp dir.
327
			$download_url = $this->get_download_url( $item['guid'] );
328
			if ( is_wp_error( $download_url ) ) {
329
				$responses[] = $download_url;
330
				continue;
331
			}
332
333
			$id = $this->sideload_media( $item['guid']['name'], $download_url, $post_id );
334
			if ( is_wp_error( $id ) ) {
335
				$responses[] = $id;
336
				continue;
337
			}
338
339
			$this->update_attachment_meta( $id, $item );
340
341
			// Add attachment data or WP_Error.
342
			$responses[] = $this->get_attachment_data( $id, $item );
343
		}
344
345
		return $responses;
346
	}
347
348
	/**
349
	 * Gets connection authorization details.
350
	 *
351
	 * @param \WP_REST_Request $request Full details about the request.
352
	 * @return array|\WP_Error|mixed
353
	 */
354
	public function get_connection_details( \WP_REST_Request $request ) {
355
		$service    = rawurlencode( $request->get_param( 'service' ) );
356
		$wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
357
358
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
359
			$internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
360
			$internal_request->set_query_params( $request->get_params() );
361
362
			return rest_do_request( $internal_request );
363
		}
364
365
		$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
366
367
		return json_decode( wp_remote_retrieve_body( $response ), true );
368
	}
369
370
	/**
371
	 * Deletes a Google Photos connection.
372
	 *
373
	 * @param WP_REST_Request $request Full details about the request.
374
	 * @return array|WP_Error|WP_REST_Response
375
	 */
376
	public function delete_connection( WP_REST_Request $request ) {
377
		$service    = rawurlencode( $request->get_param( 'service' ) );
378
		$wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
379
380
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
381
			$internal_request = new WP_REST_Request( REQUESTS::DELETE, '/' . $this->namespace . $wpcom_path );
382
			$internal_request->set_query_params( $request->get_params() );
383
384
			return rest_do_request( $internal_request );
385
		}
386
387
		$response = Client::wpcom_json_api_request_as_user(
388
			$wpcom_path,
389
			'2',
390
			array(
391
				'method' => REQUESTS::DELETE,
392
			)
393
		);
394
395
		return json_decode( wp_remote_retrieve_body( $response ), true );
396
	}
397
398
	/**
399
	 * Filter callback to provide a shorter file name for google images.
400
	 *
401
	 * @return string
402
	 */
403
	public function tmp_name() {
404
		return $this->tmp_name;
405
	}
406
407
	/**
408
	 * Returns a download URL, dealing with Google's long file names.
409
	 *
410
	 * @param array $guid Media information.
411
	 * @return string|\WP_Error
412
	 */
413
	public function get_download_url( $guid ) {
414
		$this->tmp_name = $guid['name'];
415
		add_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
416
		$download_url = download_url( $guid['url'] );
417
		remove_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
418
419
		if ( is_wp_error( $download_url ) ) {
420
			$download_url->add_data( array( 'status' => 400 ) );
421
		}
422
423
		return $download_url;
424
	}
425
426
	/**
427
	 * Uploads media file and creates attachment object.
428
	 *
429
	 * @param string $file_name    Name of media file.
430
	 * @param string $download_url Download URL.
431
	 * @param int    $post_id      The ID of the post to attach the image to.
432
	 *
433
	 * @return int|\WP_Error
434
	 */
435
	public function sideload_media( $file_name, $download_url, $post_id = 0 ) {
436
		$file = array(
437
			'name'     => wp_basename( $file_name ),
438
			'tmp_name' => $download_url,
439
		);
440
441
		$id = media_handle_sideload( $file, $post_id, null );
442
		if ( is_wp_error( $id ) ) {
443
			@unlink( $file['tmp_name'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
444
			$id->add_data( array( 'status' => 400 ) );
445
		}
446
447
		return $id;
448
	}
449
450
	/**
451
	 * Updates attachment meta data for media item.
452
	 *
453
	 * @param int   $id   Attachment ID.
454
	 * @param array $item Media item.
455
	 */
456
	public function update_attachment_meta( $id, $item ) {
457
		$meta                          = wp_get_attachment_metadata( $id );
458
		$meta['image_meta']['title']   = $item['title'];
459
		$meta['image_meta']['caption'] = $item['caption'];
460
461
		wp_update_attachment_metadata( $id, $meta );
462
463
		update_post_meta( $id, '_wp_attachment_image_alt', $item['title'] );
464
		wp_update_post(
465
			array(
466
				'ID'           => $id,
467
				'post_excerpt' => $item['caption'],
468
			)
469
		);
470
	}
471
472
	/**
473
	 * Retrieves attachment data for media item.
474
	 *
475
	 * @param int   $id   Attachment ID.
476
	 * @param array $item Media item.
477
	 *
478
	 * @return array|\WP_REST_Response Attachment data on success, WP_Error on failure.
479
	 */
480
	public function get_attachment_data( $id, $item ) {
481
		$image_src = wp_get_attachment_image_src( $id, 'full' );
482
483
		if ( empty( $image_src[0] ) ) {
484
			$response = new WP_Error(
485
				'rest_upload_error',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'rest_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...
486
				__( 'Could not retrieve source URL.', 'jetpack' ),
487
				array( 'status' => 400 )
488
			);
489
		} else {
490
			$response = array(
491
				'id'      => $id,
492
				'caption' => $item['caption'],
493
				'alt'     => $item['title'],
494
				'type'    => 'image',
495
				'url'     => $image_src[0],
496
			);
497
		}
498
499
		return $response;
500
	}
501
}
502
503
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' );
504