Completed
Push — update/story-block-loading-med... ( e6e62b...d81748 )
by
unknown
28:46 queued 18:45
created

WPCOM_REST_API_V2_Endpoint_External_Media   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 454
Duplicated Lines 2.64 %

Coupling/Cohesion

Components 2
Dependencies 2

Importance

Changes 0
Metric Value
dl 12
loc 454
rs 9.28
c 0
b 0
f 0
wmc 39
lcom 2
cbo 2

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B register_routes() 0 63 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() 6 21 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   

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:

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
147
	/**
148
	 * Checks if a given request has access to external media libraries.
149
	 */
150
	public function permission_callback() {
151
		return current_user_can( 'edit_posts' );
152
	}
153
154
	/**
155
	 * Checks if a given request has access to create an attachment.
156
	 *
157
	 * @param WP_REST_Request $request Full details about the request.
158
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
159
	 */
160
	public function create_item_permissions_check( $request ) {
161
		if ( ! empty( $request['id'] ) ) {
162
			return new WP_Error(
163
				'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...
164
				__( 'Cannot create existing post.', 'jetpack' ),
165
				array( 'status' => 400 )
166
			);
167
		}
168
169
		$post_type = get_post_type_object( 'attachment' );
170
171
		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
172
			return new WP_Error(
173
				'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...
174
				__( 'Sorry, you are not allowed to create posts as this user.', 'jetpack' ),
175
				array( 'status' => rest_authorization_required_code() )
176
			);
177
		}
178
179
		if ( ! current_user_can( 'upload_files' ) ) {
180
			return new WP_Error(
181
				'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...
182
				__( 'Sorry, you are not allowed to upload media on this site.', 'jetpack' ),
183
				array( 'status' => 400 )
184
			);
185
		}
186
187
		return true;
188
	}
189
190
	/**
191
	 * Sanitization callback for media parameter.
192
	 *
193
	 * @param array $param Media parameter.
194
	 * @return true|\WP_Error
195
	 */
196
	public function sanitize_media( $param ) {
197
		$param = $this->prepare_media_param( $param );
198
199
		return rest_sanitize_value_from_schema( $param, $this->media_schema );
200
	}
201
202
	/**
203
	 * Validation callback for media parameter.
204
	 *
205
	 * @param array $param Media parameter.
206
	 * @return true|\WP_Error
207
	 */
208
	public function validate_media( $param ) {
209
		$param = $this->prepare_media_param( $param );
210
211
		return rest_validate_value_from_schema( $param, $this->media_schema, 'media' );
212
	}
213
214
	/**
215
	 * Decodes guid json and sets parameter defaults.
216
	 *
217
	 * @param array $param Media parameter.
218
	 * @return array
219
	 */
220
	private function prepare_media_param( $param ) {
221
		foreach ( $param as $key => $item ) {
222
			if ( ! empty( $item['guid'] ) ) {
223
				$param[ $key ]['guid'] = json_decode( $item['guid'], true );
224
			}
225
226
			if ( empty( $param[ $key ]['caption'] ) ) {
227
				$param[ $key ]['caption'] = '';
228
			}
229
			if ( empty( $param[ $key ]['title'] ) ) {
230
				$param[ $key ]['title'] = '';
231
			}
232
		}
233
234
		return $param;
235
	}
236
237
	/**
238
	 * Retrieves media items from external libraries.
239
	 *
240
	 * @param \WP_REST_Request $request Full details about the request.
241
	 * @return array|\WP_Error|mixed
242
	 */
243
	public function get_external_media( \WP_REST_Request $request ) {
244
		$params     = $request->get_params();
245
		$wpcom_path = sprintf( '/meta/external-media/%s', rawurlencode( $params['service'] ) );
246
247 View Code Duplication
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
248
			$request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
249
			$request->set_query_params( $params );
250
251
			return rest_do_request( $request );
252
		}
253
254
		// Build query string to pass to wpcom endpoint.
255
		$service_args = array_filter(
256
			$params,
257
			function( $key ) {
258
				return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter' ), true );
259
			},
260
			ARRAY_FILTER_USE_KEY
261
		);
262
		if ( ! empty( $service_args ) ) {
263
			$wpcom_path .= '?' . http_build_query( $service_args );
264
		}
265
266
		$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
267
268
		switch ( wp_remote_retrieve_response_code( $response ) ) {
269
			case 200:
270
				$response = json_decode( wp_remote_retrieve_body( $response ) );
271
				break;
272
273
			case 401:
274
				$response = new WP_Error(
275
					'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...
276
					__( 'You are not connected to that service.', 'jetpack' ),
277
					array( 'status' => 403 )
278
				);
279
				break;
280
281
			case 403:
282
				$error    = json_decode( wp_remote_retrieve_body( $response ) );
283
				$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...
284
				break;
285
286
			default:
287
				if ( is_wp_error( $response ) ) {
288
					$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...
289
					break;
290
				}
291
				$response = new WP_Error(
292
					'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...
293
					__( 'An unknown error has occurred. Please try again later.', 'jetpack' ),
294
					array( 'status' => wp_remote_retrieve_response_code( $response ) )
295
				);
296
		}
297
298
		return $response;
299
	}
300
301
	/**
302
	 * Saves an external media item to the media library.
303
	 *
304
	 * @param \WP_REST_Request $request Full details about the request.
305
	 * @return array|\WP_Error|mixed
306
	 */
307
	public function copy_external_media( \WP_REST_Request $request ) {
308
		require_once ABSPATH . 'wp-admin/includes/file.php';
309
		require_once ABSPATH . 'wp-admin/includes/media.php';
310
		require_once ABSPATH . 'wp-admin/includes/image.php';
311
312
		$post_id = $request->get_param( 'post_id' );
313
314
		$responses = array();
315
		foreach ( $request->get_param( 'media' ) as $item ) {
316
			// Download file to temp dir.
317
			$download_url = $this->get_download_url( $item['guid'] );
318
			if ( is_wp_error( $download_url ) ) {
319
				$responses[] = $download_url;
320
				continue;
321
			}
322
323
			$id = $this->sideload_media( $item['guid']['name'], $download_url, $post_id );
324
			if ( is_wp_error( $id ) ) {
325
				$responses[] = $id;
326
				continue;
327
			}
328
329
			$this->update_attachment_meta( $id, $item );
330
331
			// Add attachment data or WP_Error.
332
			$responses[] = $this->get_attachment_data( $id, $item );
333
		}
334
335
		return $responses;
336
	}
337
338
	/**
339
	 * Gets connection authorization details.
340
	 *
341
	 * @param \WP_REST_Request $request Full details about the request.
342
	 * @return array|\WP_Error|mixed
343
	 */
344
	public function get_connection_details( \WP_REST_Request $request ) {
345
		$service    = rawurlencode( $request->get_param( 'service' ) );
346
		$wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
347
348 View Code Duplication
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
349
			$request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
350
			$request->set_query_params( $request->get_params() );
351
352
			return rest_do_request( $request );
353
		}
354
355
		$response = Client::wpcom_json_api_request_as_user( $wpcom_path );
356
		$response = json_decode( wp_remote_retrieve_body( $response ) );
357
358
		if ( isset( $response->code, $response->message, $response->data ) ) {
359
			$response->data = empty( $response->data->status ) ? array( 'status' => $response->data ) : $response->data;
360
			$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...
361
		}
362
363
		return $response;
364
	}
365
366
	/**
367
	 * Filter callback to provide a shorter file name for google images.
368
	 *
369
	 * @return string
370
	 */
371
	public function tmp_name() {
372
		return $this->tmp_name;
373
	}
374
375
	/**
376
	 * Returns a download URL, dealing with Google's long file names.
377
	 *
378
	 * @param array $guid Media information.
379
	 * @return string|\WP_Error
380
	 */
381
	public function get_download_url( $guid ) {
382
		$this->tmp_name = $guid['name'];
383
		add_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
384
		$download_url = download_url( $guid['url'] );
385
		remove_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) );
386
387
		if ( is_wp_error( $download_url ) ) {
388
			$download_url->add_data( array( 'status' => 400 ) );
389
		}
390
391
		return $download_url;
392
	}
393
394
	/**
395
	 * Uploads media file and creates attachment object.
396
	 *
397
	 * @param string $file_name    Name of media file.
398
	 * @param string $download_url Download URL.
399
	 * @param int    $post_id      The ID of the post to attach the image to.
400
	 *
401
	 * @return int|\WP_Error
402
	 */
403
	public function sideload_media( $file_name, $download_url, $post_id = 0 ) {
404
		$file = array(
405
			'name'     => wp_basename( $file_name ),
406
			'tmp_name' => $download_url,
407
		);
408
409
		$id = media_handle_sideload( $file, $post_id, null );
410
		if ( is_wp_error( $id ) ) {
411
			@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...
412
			$id->add_data( array( 'status' => 400 ) );
413
		}
414
415
		return $id;
416
	}
417
418
	/**
419
	 * Updates attachment meta data for media item.
420
	 *
421
	 * @param int   $id   Attachment ID.
422
	 * @param array $item Media item.
423
	 */
424
	public function update_attachment_meta( $id, $item ) {
425
		$meta                          = wp_get_attachment_metadata( $id );
426
		$meta['image_meta']['title']   = $item['title'];
427
		$meta['image_meta']['caption'] = $item['caption'];
428
429
		wp_update_attachment_metadata( $id, $meta );
430
431
		update_post_meta( $id, '_wp_attachment_image_alt', $item['title'] );
432
		wp_update_post(
433
			array(
434
				'ID'           => $id,
435
				'post_excerpt' => $item['caption'],
436
			)
437
		);
438
	}
439
440
	/**
441
	 * Retrieves attachment data for media item.
442
	 *
443
	 * @param int   $id   Attachment ID.
444
	 * @param array $item Media item.
445
	 *
446
	 * @return array|\WP_REST_Response Attachment data on success, WP_Error on failure.
447
	 */
448
	public function get_attachment_data( $id, $item ) {
449
		$image_src = wp_get_attachment_image_src( $id, 'full' );
450
451
		if ( empty( $image_src[0] ) ) {
452
			$response = new WP_Error(
453
				'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...
454
				__( 'Could not retrieve source URL.', 'jetpack' ),
455
				array( 'status' => 400 )
456
			);
457
		} else {
458
			$response = array(
459
				'id'      => $id,
460
				'caption' => $item['caption'],
461
				'alt'     => $item['title'],
462
				'type'    => 'image',
463
				'url'     => $image_src[0],
464
			);
465
		}
466
467
		return $response;
468
	}
469
}
470
471
wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' );
472