Completed
Push — update/base-styles-210 ( 2e278b...ad767b )
by Jeremy
22:25 queued 13:15
created

WPCOM_REST_API_V2_Endpoint_External_Media   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 498
Duplicated Lines 5.22 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
dl 26
loc 498
rs 8.8798
c 0
b 0
f 0
wmc 44
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
B get_external_media() 6 57 8
A copy_external_media() 0 30 4
A get_connection_details() 10 21 5
A delete_connection() 10 27 5
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   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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 View Code Duplication
		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 ) );
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 View Code Duplication
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
359
			$request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
360
			$request->set_query_params( $request->get_params() );
361
362
			return rest_do_request( $request );
363
		}
364
365
		$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
366
		$response = json_decode( wp_remote_retrieve_body( $response ) );
367
368 View Code Duplication
		if ( isset( $response->code, $response->message, $response->data ) ) {
369
			$response->data = empty( $response->data->status ) ? array( 'status' => $response->data ) : $response->data;
370
			$response       = new WP_Error( $response->code, $response->message, $response->data );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $response->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...
371
		}
372
373
		return $response;
374
	}
375
376
	/**
377
	 * Deletes a Google Photos connection.
378
	 *
379
	 * @param WP_REST_Request $request Full details about the request.
380
	 * @return array|WP_Error|WP_REST_Response
381
	 */
382
	public function delete_connection( WP_REST_Request $request ) {
383
		$service    = rawurlencode( $request->get_param( 'service' ) );
384
		$wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
385
386 View Code Duplication
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
387
			$request = new WP_REST_Request( REQUESTS::DELETE, '/' . $this->namespace . $wpcom_path );
388
			$request->set_query_params( $request->get_params() );
389
390
			return rest_do_request( $request );
391
		}
392
393
		$response = Client::wpcom_json_api_request_as_user(
394
			$wpcom_path,
395
			'2',
396
			array(
397
				'method' => REQUESTS::DELETE,
398
			)
399
		);
400
		$response = json_decode( wp_remote_retrieve_body( $response ) );
401
402 View Code Duplication
		if ( isset( $response->code, $response->message, $response->data ) ) {
403
			$response->data = empty( $response->data->status ) ? array( 'status' => $response->data ) : $response->data;
404
			$response       = new WP_Error( $response->code, $response->message, $response->data );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $response->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...
405
		}
406
407
		return $response;
408
	}
409
410
	/**
411
	 * Filter callback to provide a shorter file name for google images.
412
	 *
413
	 * @return string
414
	 */
415
	public function tmp_name() {
416
		return $this->tmp_name;
417
	}
418
419
	/**
420
	 * Returns a download URL, dealing with Google's long file names.
421
	 *
422
	 * @param array $guid Media information.
423
	 * @return string|\WP_Error
424
	 */
425
	public function get_download_url( $guid ) {
426
		$this->tmp_name = $guid['name'];
427
		add_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
428
		$download_url = download_url( $guid['url'] );
429
		remove_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
430
431
		if ( is_wp_error( $download_url ) ) {
432
			$download_url->add_data( array( 'status' => 400 ) );
433
		}
434
435
		return $download_url;
436
	}
437
438
	/**
439
	 * Uploads media file and creates attachment object.
440
	 *
441
	 * @param string $file_name    Name of media file.
442
	 * @param string $download_url Download URL.
443
	 * @param int    $post_id      The ID of the post to attach the image to.
444
	 *
445
	 * @return int|\WP_Error
446
	 */
447
	public function sideload_media( $file_name, $download_url, $post_id = 0 ) {
448
		$file = array(
449
			'name'     => wp_basename( $file_name ),
450
			'tmp_name' => $download_url,
451
		);
452
453
		$id = media_handle_sideload( $file, $post_id, null );
454
		if ( is_wp_error( $id ) ) {
455
			@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...
456
			$id->add_data( array( 'status' => 400 ) );
457
		}
458
459
		return $id;
460
	}
461
462
	/**
463
	 * Updates attachment meta data for media item.
464
	 *
465
	 * @param int   $id   Attachment ID.
466
	 * @param array $item Media item.
467
	 */
468
	public function update_attachment_meta( $id, $item ) {
469
		$meta                          = wp_get_attachment_metadata( $id );
470
		$meta['image_meta']['title']   = $item['title'];
471
		$meta['image_meta']['caption'] = $item['caption'];
472
473
		wp_update_attachment_metadata( $id, $meta );
474
475
		update_post_meta( $id, '_wp_attachment_image_alt', $item['title'] );
476
		wp_update_post(
477
			array(
478
				'ID'           => $id,
479
				'post_excerpt' => $item['caption'],
480
			)
481
		);
482
	}
483
484
	/**
485
	 * Retrieves attachment data for media item.
486
	 *
487
	 * @param int   $id   Attachment ID.
488
	 * @param array $item Media item.
489
	 *
490
	 * @return array|\WP_REST_Response Attachment data on success, WP_Error on failure.
491
	 */
492
	public function get_attachment_data( $id, $item ) {
493
		$image_src = wp_get_attachment_image_src( $id, 'full' );
494
495
		if ( empty( $image_src[0] ) ) {
496
			$response = new WP_Error(
497
				'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...
498
				__( 'Could not retrieve source URL.', 'jetpack' ),
499
				array( 'status' => 400 )
500
			);
501
		} else {
502
			$response = array(
503
				'id'      => $id,
504
				'caption' => $item['caption'],
505
				'alt'     => $item['title'],
506
				'type'    => 'image',
507
				'url'     => $image_src[0],
508
			);
509
		}
510
511
		return $response;
512
	}
513
}
514
515
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' );
516