PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / _inc / lib / core-api / wpcom-endpoints / class-wpcom-rest-api-v2-endpoint-external-media.php

class-wpcom-rest-api-v2-endpoint-external-media.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at _inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-external-media.php

888 lines 24.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API endpoint for the External Media.
4 *
5 * @package automattic/jetpack
6 * @since 8.7.0
7 */
8
9 use Automattic\Jetpack\Connection\Client;
10 use Automattic\Jetpack\Connection\Manager;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit( 0 );
14 }
15
16 /**
17 * External Media helper API.
18 *
19 * @since 8.7.0
20 */
21 class WPCOM_REST_API_V2_Endpoint_External_Media extends WP_REST_Controller {
22
23 /**
24 * Media argument schema for /copy endpoint.
25 *
26 * @var array
27 */
28 public $media_schema = array(
29 'type' => 'array',
30 'items' => array(
31 'type' => 'object',
32 'required' => true,
33 'properties' => array(
34 'caption' => array(
35 'type' => 'string',
36 ),
37 'guid' => array(
38 'type' => 'object',
39 'properties' => array(
40 'caption' => array(
41 'type' => 'string',
42 ),
43 'name' => array(
44 'type' => 'string',
45 ),
46 'title' => array(
47 'type' => 'string',
48 ),
49 'url' => array(
50 'format' => 'uri',
51 'type' => 'string',
52 ),
53 ),
54 ),
55 'title' => array(
56 'type' => 'string',
57 ),
58 'meta' => array(
59 'type' => 'object',
60 'additionalProperties' => false,
61 'properties' => array(
62 'vertical_id' => array(
63 'type' => 'string',
64 'format' => 'text-field',
65 ),
66 'pexels_object' => array(
67 'type' => 'object',
68 ),
69 ),
70 ),
71 ),
72 ),
73 );
74
75 /**
76 * Service regex.
77 *
78 * @var string
79 */
80 private static $services_regex = '(?P<service>google_photos|openverse|pexels)';
81
82 /**
83 * Constructor.
84 */
85 public function __construct() {
86 $this->namespace = 'wpcom/v2';
87 $this->rest_base = 'external-media';
88
89 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
90 }
91
92 /**
93 * Registers the routes for external media.
94 */
95 public function register_routes() {
96 register_rest_route(
97 $this->namespace,
98 $this->rest_base . '/list/' . self::$services_regex,
99 array(
100 'methods' => WP_REST_Server::READABLE,
101 'callback' => array( $this, 'get_external_media' ),
102 'permission_callback' => array( $this, 'permission_callback' ),
103 'args' => array(
104 'search' => array(
105 'description' => __( 'Media collection search term.', 'jetpack' ),
106 'type' => 'string',
107 ),
108 'number' => array(
109 'description' => __( 'Number of media items in the request', 'jetpack' ),
110 'type' => 'number',
111 'default' => 20,
112 ),
113 'path' => array(
114 'type' => 'string',
115 ),
116 'page_handle' => array(
117 'type' => 'string',
118 ),
119 'session_id' => array(
120 'description' => __( 'Session id of a service, currently only Google Photos Picker', 'jetpack' ),
121 'type' => 'string',
122 ),
123 ),
124 )
125 );
126
127 register_rest_route(
128 $this->namespace,
129 $this->rest_base . '/copy/' . self::$services_regex,
130 array(
131 'methods' => \WP_REST_Server::CREATABLE,
132 'callback' => array( $this, 'copy_external_media' ),
133 'permission_callback' => array( $this, 'create_item_permissions_check' ),
134 'args' => array(
135 'media' => array(
136 'description' => __( 'Media data to copy.', 'jetpack' ),
137 'items' => $this->media_schema,
138 'required' => true,
139 'type' => 'array',
140 'sanitize_callback' => array( $this, 'sanitize_media' ),
141 'validate_callback' => array( $this, 'validate_media' ),
142 ),
143 'post_id' => array(
144 'description' => __( 'The post ID to attach the upload to.', 'jetpack' ),
145 'type' => 'number',
146 'minimum' => 0,
147 ),
148 'should_proxy' => array(
149 'description' => __( 'Whether to proxy the media request.', 'jetpack' ),
150 'type' => 'boolean',
151 'default' => false,
152 ),
153 ),
154 )
155 );
156
157 register_rest_route(
158 $this->namespace,
159 $this->rest_base . '/connection/(?P<service>google_photos)',
160 array(
161 'methods' => \WP_REST_Server::READABLE,
162 'callback' => array( $this, 'get_connection_details' ),
163 'permission_callback' => array( $this, 'permission_callback' ),
164 )
165 );
166
167 register_rest_route(
168 $this->namespace,
169 $this->rest_base . '/connection/(?P<service>google_photos)',
170 array(
171 'methods' => \WP_REST_Server::DELETABLE,
172 'callback' => array( $this, 'delete_connection' ),
173 'permission_callback' => array( $this, 'permission_callback' ),
174 )
175 );
176
177 register_rest_route(
178 $this->namespace,
179 $this->rest_base . '/connection/(?P<service>google_photos)/picker_status',
180 array(
181 'methods' => \WP_REST_Server::READABLE,
182 'callback' => array( $this, 'get_picker_status' ),
183 'permission_callback' => array( $this, 'permission_callback' ),
184 )
185 );
186
187 // Add new session route, currently for Google Photos Picker only
188 register_rest_route(
189 $this->namespace,
190 $this->rest_base . '/session/(?P<service>google_photos)',
191 array(
192 'methods' => \WP_REST_Server::CREATABLE,
193 'callback' => array( $this, 'create_session' ),
194 'permission_callback' => array( $this, 'permission_callback' ),
195 )
196 );
197
198 // Get new session route, currently for Google Photos Picker only
199 register_rest_route(
200 $this->namespace,
201 $this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)',
202 array(
203 'methods' => \WP_REST_Server::READABLE,
204 'callback' => array( $this, 'get_session' ),
205 'permission_callback' => array( $this, 'permission_callback' ),
206 )
207 );
208
209 // Delete session route, currently for Google Photos Picker only
210 register_rest_route(
211 $this->namespace,
212 $this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)',
213 array(
214 'methods' => \WP_REST_Server::DELETABLE,
215 'callback' => array( $this, 'delete_session' ),
216 'permission_callback' => array( $this, 'permission_callback' ),
217 )
218 );
219
220 // Add new proxy route for media files
221 register_rest_route(
222 $this->namespace,
223 $this->rest_base . '/proxy/(?P<service>google_photos)',
224 array(
225 'methods' => WP_REST_Server::CREATABLE,
226 'callback' => array( $this, 'proxy_media_request' ),
227 'permission_callback' => array( $this, 'permission_callback' ),
228 'args' => array(
229 'url' => array(
230 'required' => true,
231 'type' => 'string',
232 ),
233 ),
234 )
235 );
236 }
237
238 /**
239 * Checks if a given request has access to external media libraries.
240 */
241 public function permission_callback() {
242 return current_user_can( 'upload_files' );
243 }
244
245 /**
246 * Checks if a given request has access to create an attachment.
247 *
248 * @param WP_REST_Request $request Full details about the request.
249 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
250 */
251 public function create_item_permissions_check( $request ) {
252 if ( ! empty( $request['id'] ) ) {
253 return new WP_Error(
254 'rest_post_exists',
255 __( 'Cannot create existing post.', 'jetpack' ),
256 array( 'status' => 400 )
257 );
258 }
259
260 $post_type = get_post_type_object( 'attachment' );
261
262 if ( ! current_user_can( $post_type->cap->create_posts ) ) {
263 return new WP_Error(
264 'rest_cannot_create',
265 __( 'Sorry, you are not allowed to create posts as this user.', 'jetpack' ),
266 array( 'status' => rest_authorization_required_code() )
267 );
268 }
269
270 if ( ! current_user_can( 'upload_files' ) ) {
271 return new WP_Error(
272 'rest_cannot_create',
273 __( 'Sorry, you are not allowed to upload media on this site.', 'jetpack' ),
274 array( 'status' => 400 )
275 );
276 }
277
278 // Attaching media to a post requires the ability to edit that post, mirroring
279 // WP_REST_Attachments_Controller::create_item_permissions_check(). Without this
280 // check any user with upload_files could parent an attachment to a post they
281 // cannot edit.
282 $post_id = (int) $request->get_param( 'post_id' );
283 if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
284 return new WP_Error(
285 'rest_cannot_edit',
286 __( 'Sorry, you are not allowed to upload media to this post.', 'jetpack' ),
287 array( 'status' => rest_authorization_required_code() )
288 );
289 }
290
291 return true;
292 }
293
294 /**
295 * Sanitization callback for media parameter.
296 *
297 * @param array $param Media parameter.
298 * @return true|\WP_Error
299 */
300 public function sanitize_media( $param ) {
301 $param = $this->prepare_media_param( $param );
302
303 return rest_sanitize_value_from_schema( $param, $this->media_schema );
304 }
305
306 /**
307 * Validation callback for media parameter.
308 *
309 * @param array $param Media parameter.
310 * @return true|\WP_Error
311 */
312 public function validate_media( $param ) {
313 $param = $this->prepare_media_param( $param );
314
315 return rest_validate_value_from_schema( $param, $this->media_schema, 'media' );
316 }
317
318 /**
319 * Decodes guid json and sets parameter defaults.
320 *
321 * @param array $param Media parameter.
322 * @return array
323 */
324 private function prepare_media_param( $param ) {
325 foreach ( $param as $key => $item ) {
326 if ( ! empty( $item['guid'] ) ) {
327 $param[ $key ]['guid'] = json_decode( $item['guid'], true );
328 }
329
330 if ( empty( $param[ $key ]['caption'] ) ) {
331 $param[ $key ]['caption'] = '';
332 }
333 if ( empty( $param[ $key ]['title'] ) ) {
334 $param[ $key ]['title'] = '';
335 }
336 }
337
338 return $param;
339 }
340
341 /**
342 * Retrieves media items from external libraries.
343 *
344 * @param \WP_REST_Request $request Full details about the request.
345 * @return array|\WP_Error|mixed
346 */
347 public function get_external_media( \WP_REST_Request $request ) {
348 $params = $request->get_params();
349 $wpcom_path = sprintf( '/meta/external-media/%s', rawurlencode( $params['service'] ) );
350
351 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
352 $request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
353 $request->set_query_params( $params );
354
355 return rest_do_request( $request );
356 }
357
358 // Build query string to pass to wpcom endpoint.
359 $service_args = array_filter(
360 $params,
361 function ( $key ) {
362 return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter', 'session_id' ), true );
363 },
364 ARRAY_FILTER_USE_KEY
365 );
366 if ( ! empty( $service_args ) ) {
367 $wpcom_path .= '?' . http_build_query( $service_args );
368 }
369
370 $response = Client::wpcom_json_api_request_as_user( $wpcom_path );
371
372 switch ( wp_remote_retrieve_response_code( $response ) ) {
373 case 200:
374 $response = json_decode( wp_remote_retrieve_body( $response ), true );
375 break;
376
377 case 401:
378 $response = new WP_Error(
379 'authorization_required',
380 __( 'You are not connected to that service.', 'jetpack' ),
381 array( 'status' => 403 )
382 );
383 break;
384
385 case 403:
386 $error = json_decode( wp_remote_retrieve_body( $response ) );
387 $response = new WP_Error( $error->code, $error->message, $error->data );
388 break;
389
390 default:
391 if ( is_wp_error( $response ) ) {
392 $response->add_data( array( 'status' => 400 ) );
393 break;
394 }
395 $response = new WP_Error(
396 'rest_request_error',
397 __( 'An unknown error has occurred. Please try again later.', 'jetpack' ),
398 array( 'status' => wp_remote_retrieve_response_code( $response ) )
399 );
400 }
401
402 return $response;
403 }
404
405 /**
406 * Saves an external media item to the media library.
407 *
408 * @param \WP_REST_Request $request Full details about the request.
409 * @return array|\WP_Error|mixed
410 **/
411 public function copy_external_media( \WP_REST_Request $request ) {
412 require_once ABSPATH . 'wp-admin/includes/file.php';
413 require_once ABSPATH . 'wp-admin/includes/media.php';
414 require_once ABSPATH . 'wp-admin/includes/image.php';
415
416 $post_id = (int) $request->get_param( 'post_id' );
417 $should_proxy = $request->get_param( 'should_proxy' );
418 $service = rawurlencode( $request->get_param( 'service' ) );
419
420 // Fail closed: never parent an attachment to a post the caller cannot edit,
421 // even if a future change lets an unauthorized request reach this handler.
422 // The permission callback already rejects such requests with a 403.
423 if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
424 $post_id = 0;
425 }
426
427 $responses = array();
428
429 foreach ( $request->get_param( 'media' ) as $item ) {
430 // Download file to temp dir.
431 if ( $should_proxy ) {
432 $wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service );
433 $wpcom_path .= '?url=' . rawurlencode( $item['guid']['url'] );
434 $download_url = wp_tempnam();
435 $response = Client::wpcom_json_api_request_as_user(
436 $wpcom_path,
437 '2',
438 array(
439 'method' => 'POST',
440 )
441 );
442
443 if ( is_wp_error( $response ) ) {
444 $responses[] = $response;
445 continue;
446 }
447 $wp_filesystem = $this->get_wp_filesystem();
448 $written = $wp_filesystem->put_contents( $download_url, wp_remote_retrieve_body( $response ) );
449
450 if ( false === $written ) {
451 $responses[] = new WP_Error(
452 'rest_upload_error',
453 __( 'Could not download media file.', 'jetpack' ),
454 array( 'status' => 400 )
455 );
456 continue;
457 }
458 } else {
459 $download_url = $this->get_download_url( $item['guid'] );
460 }
461
462 if ( is_wp_error( $download_url ) ) {
463 $responses[] = $download_url;
464 continue;
465 }
466
467 $id = $this->sideload_media( $item['guid']['name'], $download_url, $post_id );
468 if ( is_wp_error( $id ) ) {
469 $responses[] = $id;
470 continue;
471 }
472
473 $this->update_attachment_meta( $id, $item );
474
475 // Add attachment data or WP_Error.
476 $responses[] = $this->get_attachment_data( $id, $item );
477 }
478
479 return $responses;
480 }
481
482 /**
483 * Gets connection authorization details.
484 *
485 * @param \WP_REST_Request $request Full details about the request.
486 * @return array|\WP_Error|mixed
487 */
488 public function get_connection_details( \WP_REST_Request $request ) {
489 $service = $request->get_param( 'service' );
490
491 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
492 $wpcom_path = sprintf( '/meta/external-media/connection/%s', rawurlencode( $service ) );
493 $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
494 $internal_request->set_query_params( $request->get_params() );
495
496 return rest_do_request( $internal_request );
497 }
498
499 $site_id = Manager::get_site_id();
500 if ( is_wp_error( $site_id ) ) {
501 return $site_id;
502 }
503
504 $path = sprintf( '/sites/%d/external-services', $site_id );
505 $response = Client::wpcom_json_api_request_as_user( $path );
506 if ( is_wp_error( $response ) ) {
507 return $response;
508 }
509
510 $body = json_decode( wp_remote_retrieve_body( $response ) );
511 if ( ! property_exists( $body, 'services' ) || ! property_exists( $body->services, $service ) ) {
512 return new WP_Error(
513 'bad_request',
514 __( 'An error occurred. Please try again later.', 'jetpack' ),
515 array( 'status' => 400 )
516 );
517 }
518
519 return $body->services->{ $service };
520 }
521
522 /**
523 * Deletes a Google Photos connection.
524 *
525 * @param WP_REST_Request $request Full details about the request.
526 * @return array|WP_Error|WP_REST_Response
527 */
528 public function delete_connection( WP_REST_Request $request ) {
529 $service = rawurlencode( $request->get_param( 'service' ) );
530 $wpcom_path = sprintf( '/meta/external-media/connection/%s', $service );
531
532 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
533 $internal_request = new WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path );
534 $internal_request->set_query_params( $request->get_params() );
535
536 return rest_do_request( $internal_request );
537 }
538
539 $response = Client::wpcom_json_api_request_as_user(
540 $wpcom_path,
541 '2',
542 array(
543 'method' => 'DELETE',
544 )
545 );
546
547 return json_decode( wp_remote_retrieve_body( $response ), true );
548 }
549
550 /**
551 * Gets Google Photos Picker enabled Status.
552 *
553 * @param \WP_REST_Request $request Full details about the request.
554 * @return array|\WP_Error|mixed
555 */
556 public function get_picker_status( \WP_REST_Request $request ) {
557 $service = $request->get_param( 'service' );
558 $wpcom_path = sprintf( '/meta/external-media/connection/%s/picker_status', rawurlencode( $service ) );
559
560 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
561 $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
562 $internal_request->set_query_params( $request->get_params() );
563
564 return rest_do_request( $internal_request );
565 }
566
567 $response = Client::wpcom_json_api_request_as_user(
568 $wpcom_path,
569 '2',
570 array(
571 'method' => 'GET',
572 )
573 );
574
575 return json_decode( wp_remote_retrieve_body( $response ), true );
576 }
577
578 /**
579 * Creates a new session for a service.
580 *
581 * @param \WP_REST_Request $request Full details about the request.
582 * @return array|\WP_Error|mixed
583 */
584 public function create_session( \WP_REST_Request $request ) {
585 $service = $request->get_param( 'service' );
586 $wpcom_path = sprintf( '/meta/external-media/session/%s', rawurlencode( $service ) );
587
588 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
589 $internal_request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path );
590 $internal_request->set_query_params( $request->get_params() );
591
592 return rest_do_request( $internal_request );
593 }
594
595 $response = Client::wpcom_json_api_request_as_user(
596 $wpcom_path,
597 '2',
598 array(
599 'method' => 'POST',
600 )
601 );
602
603 return json_decode( wp_remote_retrieve_body( $response ), true );
604 }
605
606 /**
607 * Gets a session for a service.
608 *
609 * @param \WP_REST_Request $request Full details about the request.
610 * @return array|\WP_Error|mixed
611 */
612 public function get_session( \WP_REST_Request $request ) {
613 $service = $request->get_param( 'service' );
614 $session_id = $request->get_param( 'session_id' );
615 $wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) );
616
617 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
618 $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path );
619 $internal_request->set_query_params( $request->get_params() );
620
621 return rest_do_request( $internal_request );
622 }
623
624 $response = Client::wpcom_json_api_request_as_user(
625 $wpcom_path,
626 '2',
627 array(
628 'method' => 'GET',
629 )
630 );
631
632 return json_decode( wp_remote_retrieve_body( $response ), true );
633 }
634
635 /**
636 * Deletes a session for a service.
637 *
638 * @param \WP_REST_Request $request Full details about the request.
639 * @return array|\WP_Error|mixed
640 */
641 public function delete_session( \WP_REST_Request $request ) {
642 $service = $request->get_param( 'service' );
643 $session_id = $request->get_param( 'session_id' );
644 $wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) );
645
646 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
647 $internal_request = new \WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path );
648 $internal_request->set_query_params( $request->get_params() );
649
650 return rest_do_request( $internal_request );
651 }
652
653 $response = Client::wpcom_json_api_request_as_user(
654 $wpcom_path,
655 '2',
656 array(
657 'method' => 'DELETE',
658 )
659 );
660
661 return json_decode( wp_remote_retrieve_body( $response ), true );
662 }
663
664 /**
665 * Proxies media requests with proper authorization headers
666 *
667 * @param WP_REST_Request $request Full details about the request.
668 * @return WP_REST_Response|WP_Error|array Response object or WP_Error.
669 */
670 public function proxy_media_request( $request ) {
671 $params = $request->get_params();
672 $service = rawurlencode( $request->get_param( 'service' ) );
673 $wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service );
674
675 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
676 $request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path );
677 $request->set_query_params( $params );
678
679 return rest_do_request( $request );
680
681 } else {
682 // Build query string to pass to wpcom endpoint.
683 $service_args = array_filter(
684 $params,
685 function ( $key ) {
686 return in_array( $key, array( 'url' ), true );
687 },
688 ARRAY_FILTER_USE_KEY
689 );
690
691 if ( ! empty( $service_args ) ) {
692 $wpcom_path .= '?' . http_build_query( $service_args );
693 }
694
695 $response = Client::wpcom_json_api_request_as_user(
696 $wpcom_path,
697 '2',
698 array(
699 'method' => 'POST',
700 )
701 );
702
703 $status_code = wp_remote_retrieve_response_code( $response );
704 $headers = wp_remote_retrieve_headers( $response );
705 $body = wp_remote_retrieve_body( $response );
706
707 // For non-200 responses, parse and return JSON error
708 if ( $status_code !== 200 ) {
709 $error_data = json_decode( $body, true );
710 return new \WP_REST_Response( $error_data, $status_code );
711 }
712 }
713
714 // Return binary content directly
715 $valid_headers = array(
716 'content-type',
717 'content-length',
718 'content-disposition',
719 );
720 // Set content headers
721 foreach ( $valid_headers as $header ) {
722 if ( ! empty( $headers[ $header ] ) ) {
723 header( ucwords( $header, '-' ) . ': ' . $headers[ $header ] );
724 }
725 }
726
727 // Set cache headers
728 header( 'Cache-Control: no-cache, no-store, must-revalidate' );
729 header( 'Pragma: no-cache' );
730 header( 'Expires: 0' );
731 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Media binary data
732 echo $body;
733 exit( 0 );
734 }
735
736 /**
737 * Downloads a remote media file into a temporary file for sideloading.
738 *
739 * The remote file is streamed into a randomly-named temporary file created by
740 * wp_tempnam(). The caller-supplied name is never used for the temporary file
741 * itself; it is only applied — and validated by WordPress — later, when the
742 * completed download is handed to media_handle_sideload(). This prevents a
743 * crafted name from controlling the physical path or extension of the file
744 * written to disk.
745 *
746 * @param array $guid Media information.
747 * @return string|\WP_Error Path to the downloaded temporary file, or WP_Error on failure.
748 */
749 public function get_download_url( $guid ) {
750 require_once ABSPATH . 'wp-admin/includes/file.php';
751
752 $tmp_name = wp_tempnam();
753 if ( ! $tmp_name ) {
754 return new WP_Error(
755 'rest_upload_error',
756 __( 'Could not create a temporary file.', 'jetpack' ),
757 array( 'status' => 500 )
758 );
759 }
760
761 $response = wp_safe_remote_get(
762 $guid['url'],
763 array(
764 'timeout' => 300,
765 'stream' => true,
766 'filename' => $tmp_name,
767 )
768 );
769
770 if ( is_wp_error( $response ) ) {
771 wp_delete_file( $tmp_name );
772 $response->add_data( array( 'status' => 400 ) );
773 return $response;
774 }
775
776 if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
777 wp_delete_file( $tmp_name );
778 return new WP_Error(
779 'rest_upload_error',
780 __( 'Could not download the media file.', 'jetpack' ),
781 array( 'status' => 400 )
782 );
783 }
784
785 return $tmp_name;
786 }
787
788 /**
789 * Uploads media file and creates attachment object.
790 *
791 * @param string $file_name Name of media file.
792 * @param string $download_url Download URL.
793 * @param int $post_id The ID of the post to attach the image to.
794 *
795 * @return int|\WP_Error
796 */
797 public function sideload_media( $file_name, $download_url, $post_id = 0 ) {
798 $file = array(
799 'name' => sanitize_file_name( wp_basename( $file_name ) ),
800 'tmp_name' => $download_url,
801 );
802
803 $id = media_handle_sideload( $file, $post_id, null );
804 if ( is_wp_error( $id ) ) {
805 wp_delete_file( $file['tmp_name'] );
806 $id->add_data( array( 'status' => 400 ) );
807 }
808
809 return $id;
810 }
811
812 /**
813 * Updates attachment meta data for media item.
814 *
815 * @param int $id Attachment ID.
816 * @param array $item Media item.
817 */
818 public function update_attachment_meta( $id, $item ) {
819 $meta = wp_get_attachment_metadata( $id );
820 $meta['image_meta']['title'] = $item['title'];
821 $meta['image_meta']['caption'] = $item['caption'];
822
823 wp_update_attachment_metadata( $id, $meta );
824
825 update_post_meta( $id, '_wp_attachment_image_alt', $item['title'] );
826 wp_update_post(
827 array(
828 'ID' => $id,
829 'post_excerpt' => $item['caption'],
830 )
831 );
832
833 if ( ! empty( $item['meta'] ) ) {
834 foreach ( $item['meta'] as $meta_key => $meta_value ) {
835 update_post_meta( $id, $meta_key, $meta_value );
836 }
837 }
838 }
839
840 /**
841 * Retrieves attachment data for media item.
842 *
843 * @param int $id Attachment ID.
844 * @param array $item Media item.
845 *
846 * @return array|\WP_REST_Response Attachment data on success, WP_Error on failure.
847 */
848 public function get_attachment_data( $id, $item ) {
849 $image_src = wp_get_attachment_image_src( $id, 'full' );
850
851 if ( empty( $image_src[0] ) ) {
852 $response = new WP_Error(
853 'rest_upload_error',
854 __( 'Could not retrieve source URL.', 'jetpack' ),
855 array( 'status' => 400 )
856 );
857 } else {
858 $response = array(
859 'id' => $id,
860 'caption' => $item['caption'],
861 'alt' => $item['title'],
862 'type' => 'image',
863 'url' => $image_src[0],
864 );
865 }
866
867 return $response;
868 }
869
870 /**
871 * Get the wp filesystem.
872 *
873 * @return \WP_Filesystem_Base|null
874 */
875 private function get_wp_filesystem() {
876 global $wp_filesystem;
877
878 if ( ! isset( $wp_filesystem ) ) {
879 require_once ABSPATH . '/wp-admin/includes/file.php';
880 WP_Filesystem();
881 }
882
883 return $wp_filesystem;
884 }
885 }
886
887 wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' );
888