PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 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 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / rest-api / class-connections-controller.php

class-connections-controller.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-publicize/src/rest-api/class-connections-controller.php

632 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The Publicize Connections Controller class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8 namespace Automattic\Jetpack\Publicize\REST_API;
9
10 use Automattic\Jetpack\Connection\Rest_Authentication;
11 use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
12 use Automattic\Jetpack\Publicize\Connections;
13 use Automattic\Jetpack\Publicize\Jetpack_Social_Settings\Settings;
14 use Automattic\Jetpack\Publicize\Publicize_Utils;
15 use WP_Error;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_REST_Server;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit( 0 );
22 }
23
24 /**
25 * Connections Controller class.
26 *
27 * @phan-constructor-used-for-side-effects
28 */
29 class Connections_Controller extends Base_Controller {
30
31 use WPCOM_REST_API_Proxy_Request;
32
33 /**
34 * Constructor.
35 */
36 public function __construct() {
37 parent::__construct();
38
39 $this->base_api_path = 'wpcom';
40 $this->version = 'v2';
41
42 $this->namespace = "{$this->base_api_path}/{$this->version}";
43 $this->rest_base = 'publicize/connections';
44
45 $this->allow_requests_as_blog = true;
46
47 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
48 }
49
50 /**
51 * Register the routes.
52 */
53 public function register_routes() {
54 register_rest_route(
55 $this->namespace,
56 '/' . $this->rest_base,
57 array(
58 array(
59 'methods' => WP_REST_Server::READABLE,
60 'callback' => array( $this, 'get_items' ),
61 'permission_callback' => array( $this, 'get_items_permissions_check' ),
62 'args' => array(
63 'test_connections' => array(
64 'type' => 'boolean',
65 'description' => __( 'Whether to test connections.', 'jetpack-publicize-pkg' ),
66 ),
67 ),
68 ),
69 array(
70 'methods' => WP_REST_Server::CREATABLE,
71 'callback' => array( $this, 'create_item' ),
72 'permission_callback' => array( $this, 'create_item_permissions_check' ),
73 'args' => array(
74 'keyring_connection_ID' => array(
75 'description' => __( 'Keyring connection ID.', 'jetpack-publicize-pkg' ),
76 'type' => 'integer',
77 'required' => true,
78 ),
79 'external_user_ID' => array(
80 'description' => __( 'External User Id - in case of services like Facebook.', 'jetpack-publicize-pkg' ),
81 'type' => 'string',
82 ),
83 'shared' => array(
84 'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
85 'type' => 'boolean',
86 ),
87 ),
88 ),
89 'schema' => array( $this, 'get_public_item_schema' ),
90 )
91 );
92
93 register_rest_route(
94 $this->namespace,
95 '/' . $this->rest_base . '/(?P<connection_id>[0-9]+)',
96 array(
97 'args' => array(
98 'connection_id' => array(
99 'description' => __( 'Unique identifier for the connection.', 'jetpack-publicize-pkg' ),
100 'type' => 'string',
101 'required' => true,
102 ),
103 ),
104 array(
105 'methods' => WP_REST_Server::EDITABLE,
106 'callback' => array( $this, 'update_item' ),
107 'permission_callback' => array( $this, 'update_item_permissions_check' ),
108 'args' => array(
109 'external_user_ID' => array(
110 'description' => __( 'External User Id - in case of services like Facebook.', 'jetpack-publicize-pkg' ),
111 'type' => 'string',
112 ),
113 'shared' => array(
114 'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
115 'type' => 'boolean',
116 ),
117 ),
118 ),
119 array(
120 'methods' => WP_REST_Server::DELETABLE,
121 'callback' => array( $this, 'delete_item' ),
122 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
123
124 ),
125 'schema' => array( $this, 'get_public_item_schema' ),
126 )
127 );
128
129 // This route receives pushes from WPCOM, so it is registered under the
130 // site-local jetpack/v4 namespace and never on WPCOM itself.
131 if ( ! Publicize_Utils::is_wpcom() ) {
132 register_rest_route(
133 'jetpack/v4',
134 '/publicize/connections/sync',
135 array(
136 array(
137 'methods' => WP_REST_Server::CREATABLE,
138 'callback' => array( $this, 'receive_updated_connections' ),
139 'permission_callback' => array( Rest_Authentication::class, 'is_signed_with_user_token' ),
140 'args' => array(
141 // An empty value is accepted on purpose: a site with no connections left
142 // syncs an empty payload, which arrives here as an empty object.
143 'connections' => array(
144 'type' => 'object',
145 'required' => true,
146 'description' => __( 'The updated Publicize connections, keyed by service name.', 'jetpack-publicize-pkg' ),
147 ),
148 ),
149 ),
150 )
151 );
152 }
153 }
154
155 /**
156 * Receive updated Publicize connections from WPCOM.
157 *
158 * REST replacement for the jetpack.updatePublicizeConnections XML-RPC method.
159 *
160 * Unusable connections are dropped rather than rejected: an error response would send
161 * WPCOM down its XML-RPC fallback, which stores the same payload without the check.
162 *
163 * @param WP_REST_Request $request Full details about the request.
164 * @return WP_REST_Response
165 */
166 public function receive_updated_connections( $request ) {
167 /**
168 * The route only registers on Jetpack sites, where the global is this package's Publicize.
169 *
170 * @var \Automattic\Jetpack\Publicize\Publicize $publicize
171 */
172 global $publicize;
173
174 return rest_ensure_response(
175 $publicize->receive_updated_publicize_connections( $request->get_param( 'connections' ) )
176 );
177 }
178
179 /**
180 * Schema for the endpoint.
181 *
182 * @return array
183 */
184 public function get_item_schema() {
185 if ( $this->schema ) {
186 return $this->add_additional_fields_schema( $this->schema );
187 }
188 $deprecated_fields = array(
189 'id' => array(
190 'type' => 'string',
191 'description' => __( 'Unique identifier for the Jetpack Social connection.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
192 /* translators: %s is the new field name */
193 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
194 'connection_id'
195 ),
196 ),
197 'username' => array(
198 'type' => 'string',
199 'description' => __( 'Username of the connected account.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
200 /* translators: %s is the new field name */
201 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
202 'external_handle'
203 ),
204 ),
205 'profile_display_name' => array(
206 'type' => 'string',
207 'description' => __( 'The name to display in the profile of the connected account.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
208 /* translators: %s is the new field name */
209 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
210 'display_name'
211 ),
212 ),
213 'global' => array(
214 'type' => 'boolean',
215 'description' => __( 'Is this connection available to all users?', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
216 /* translators: %s is the new field name */
217 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
218 'shared'
219 ),
220 ),
221 );
222
223 $schema = array(
224 '$schema' => 'http://json-schema.org/draft-04/schema#',
225 'title' => 'jetpack-publicize-connection',
226 'type' => 'object',
227 'properties' => array_merge(
228 $deprecated_fields,
229 self::get_the_item_schema()
230 ),
231 );
232
233 $this->schema = $schema;
234
235 return $this->add_additional_fields_schema( $schema );
236 }
237
238 /**
239 * Get the schema for the connection item.
240 *
241 * @return array
242 */
243 public static function get_the_item_schema() {
244 return array(
245 'connection_id' => array(
246 'type' => 'string',
247 'description' => __( 'Connection ID of the connected account.', 'jetpack-publicize-pkg' ),
248 ),
249 'display_name' => array(
250 'type' => 'string',
251 'description' => __( 'Display name of the connected account.', 'jetpack-publicize-pkg' ),
252 ),
253 'external_handle' => array(
254 'type' => array( 'string', 'null' ),
255 'description' => __( 'The external handle or username of the connected account.', 'jetpack-publicize-pkg' ),
256 ),
257 'external_id' => array(
258 'type' => 'string',
259 'description' => __( 'The external ID of the connected account.', 'jetpack-publicize-pkg' ),
260 ),
261 'profile_link' => array(
262 'type' => 'string',
263 'description' => __( 'Profile link of the connected account.', 'jetpack-publicize-pkg' ),
264 ),
265 'profile_picture' => array(
266 'type' => 'string',
267 'description' => __( 'URL of the profile picture of the connected account.', 'jetpack-publicize-pkg' ),
268 ),
269 'service_label' => array(
270 'type' => 'string',
271 'description' => __( 'Human-readable label for the Jetpack Social service.', 'jetpack-publicize-pkg' ),
272 ),
273 'service_name' => array(
274 'type' => 'string',
275 'description' => __( 'Alphanumeric identifier for the Jetpack Social service.', 'jetpack-publicize-pkg' ),
276 ),
277 'shared' => array(
278 'type' => 'boolean',
279 'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
280 ),
281 'status' => array(
282 'description' => __( 'The connection status.', 'jetpack-publicize-pkg' ),
283 'oneOf' => array(
284 array(
285 'type' => 'string',
286 'enum' => array(
287 'ok',
288 'broken',
289 'must_reauth',
290 ),
291 ),
292 array(
293 'type' => 'null',
294 ),
295 ),
296 ),
297 'template' => array(
298 'type' => 'string',
299 'description' => __( 'Per-connection message template override. Empty string means fall back to the global template.', 'jetpack-publicize-pkg' ),
300 'default' => '',
301 'maxLength' => Settings::MESSAGE_TEMPLATE_MAX_LENGTH,
302 'arg_options' => array(
303 'sanitize_callback' => array( Settings::class, 'sanitize_message_template' ),
304 ),
305 ),
306 'wpcom_user_id' => array(
307 'type' => 'integer',
308 'description' => __( 'wordpress.com ID of the user the connection belongs to.', 'jetpack-publicize-pkg' ),
309 ),
310 );
311 }
312
313 /**
314 * Verify that the request has access to connectoins list.
315 *
316 * @param WP_REST_Request $request Full details about the request.
317 * @return true|WP_Error
318 */
319 public function get_items_permissions_check( $request ) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
320 return $this->publicize_permissions_check();
321 }
322
323 /**
324 * Get list of connected Publicize connections.
325 *
326 * @param WP_REST_Request $request Full details about the request.
327 *
328 * @return WP_REST_Response suitable for 1-page collection
329 */
330 public function get_items( $request ) {
331 if ( Publicize_Utils::is_wpcom() ) {
332 $args = array(
333 'context' => self::is_authorized_blog_request() ? 'blog' : 'user',
334 'test_connections' => $request->get_param( 'test_connections' ),
335 );
336
337 $connections = Connections::wpcom_get_connections( $args );
338 } else {
339 $connections = $this->proxy_request_to_wpcom_as_user( $request );
340 }
341
342 if ( is_wp_error( $connections ) ) {
343 return $connections;
344 }
345
346 /*
347 * The Jetpack site path proxies to WPCOM instead of going through Connections::get_all(),
348 * so the filter is applied here too to keep both paths consistent.
349 *
350 * This filter is documented in projects/packages/publicize/src/class-connections.php
351 */
352 $connections = (array) apply_filters( 'jetpack_publicize_connections', $connections );
353
354 $items = array();
355
356 foreach ( $connections as $item ) {
357 $data = $this->prepare_item_for_response( $item, $request );
358
359 $items[] = $this->prepare_response_for_collection( $data );
360 }
361
362 $response = rest_ensure_response( $items );
363 $response->header( 'X-WP-Total', (string) count( $items ) );
364 $response->header( 'X-WP-TotalPages', '1' );
365
366 return $response;
367 }
368
369 /**
370 * Checks if a given request has access to create a connection.
371 *
372 * @param WP_REST_Request $request Full details about the request.
373 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
374 */
375 public function create_item_permissions_check( $request ) {
376 $permissions = parent::publicize_permissions_check();
377
378 if ( is_wp_error( $permissions ) ) {
379 return $permissions;
380 }
381
382 $shared_permission = $this->check_shared_param_permission( $request );
383
384 if ( is_wp_error( $shared_permission ) ) {
385 return $shared_permission;
386 }
387
388 return current_user_can( 'publish_posts' );
389 }
390
391 /**
392 * Check whether the request is allowed to set the `shared` flag on a connection.
393 *
394 * Shared connections are usable by every author on the site, so only editors
395 * and above may set the flag. Used by both the create and the update permission
396 * check, so the rule cannot drift between the two.
397 *
398 * @param WP_REST_Request $request Full details about the request.
399 * @return true|WP_Error True if the request may proceed, WP_Error object otherwise.
400 */
401 protected function check_shared_param_permission( $request ) {
402 if ( ! $request->has_param( 'shared' ) ) {
403 return true;
404 }
405
406 if ( ! current_user_can( 'edit_others_posts' ) ) {
407 return new WP_Error(
408 'rest_cannot_share_connection',
409 __( 'Sorry, you are not allowed to share connections with other users.', 'jetpack-publicize-pkg' ),
410 array( 'status' => rest_authorization_required_code() )
411 );
412 }
413
414 return true;
415 }
416
417 /**
418 * Creates a new connection.
419 *
420 * @param WP_REST_Request $request Full details about the request.
421 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
422 */
423 public function create_item( $request ) {
424 if ( Publicize_Utils::is_wpcom() ) {
425
426 $input = array(
427 'keyring_connection_ID' => $request->get_param( 'keyring_connection_ID' ),
428 'shared' => $request->get_param( 'shared' ),
429 );
430
431 $external_user_id = $request->get_param( 'external_user_ID' );
432 if ( ! empty( $external_user_id ) ) {
433 $input['external_user_ID'] = $external_user_id;
434 }
435
436 $result = Connections::wpcom_create_connection( $input );
437
438 if ( is_wp_error( $result ) ) {
439 return $result;
440 }
441
442 $connection = Connections::get_by_id( $result );
443
444 $response = $this->prepare_item_for_response( $connection, $request );
445 $response = rest_ensure_response( $response );
446
447 $response->set_status( 201 );
448
449 return $response;
450
451 }
452
453 $response = $this->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) );
454
455 if ( is_wp_error( $response ) ) {
456 return new WP_Error(
457 'jp_connection_update_failed',
458 __( 'Something went wrong while creating a connection.', 'jetpack-publicize-pkg' ),
459 $response->get_error_message()
460 );
461 }
462
463 $response = rest_ensure_response( $response );
464
465 $response->set_status( 201 );
466
467 return $response;
468 }
469
470 /**
471 * Checks if a given request has access to update a connection.
472 *
473 * @param WP_REST_Request $request Full details about the request.
474 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
475 */
476 public function update_item_permissions_check( $request ) {
477 $permissions = parent::publicize_permissions_check();
478
479 if ( is_wp_error( $permissions ) ) {
480 return $permissions;
481 }
482
483 // If the user cannot manage the connection, they can't update it either.
484 if ( ! $this->manage_connection_permission_check( $request ) ) {
485 return new WP_Error(
486 'rest_cannot_edit',
487 __( 'Sorry, you are not allowed to update this connection.', 'jetpack-publicize-pkg' ),
488 array( 'status' => rest_authorization_required_code() )
489 );
490 }
491
492 $shared_permission = $this->check_shared_param_permission( $request );
493
494 if ( is_wp_error( $shared_permission ) ) {
495 return $shared_permission;
496 }
497
498 return current_user_can( 'publish_posts' );
499 }
500
501 /**
502 * Update a connection.
503 *
504 * @param WP_REST_Request $request Full details about the request.
505 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
506 */
507 public function update_item( $request ) {
508 $connection_id = $request->get_param( 'connection_id' );
509
510 if ( Publicize_Utils::is_wpcom() ) {
511
512 $input = array(
513 'shared' => $request->get_param( 'shared' ),
514 );
515
516 $external_user_id = $request->get_param( 'external_user_ID' );
517 if ( ! empty( $external_user_id ) ) {
518 $input['external_user_ID'] = $external_user_id;
519 }
520
521 if ( $request->has_param( 'template' ) ) {
522 require_lib( 'publicize/util/message-templates' );
523
524 $template_value = Settings::sanitize_message_template( $request->get_param( 'template' ) );
525
526 /**
527 * Only gate non-empty values. Clearing an existing override
528 * must be allowed regardless of plan — otherwise users who
529 * downgrade can't remove a previously-set template.
530 */
531 if ( '' !== $template_value && ! \Publicize\can_use_per_connection_templates() ) {
532 return new WP_Error(
533 'rest_forbidden_per_connection_template',
534 __( 'Per-connection message templates require an upgraded plan.', 'jetpack-publicize-pkg' ),
535 array( 'status' => rest_authorization_required_code() )
536 );
537 }
538
539 $input['template'] = $template_value;
540 }
541
542 $result = Connections::wpcom_update_connection( $connection_id, $input );
543
544 if ( is_wp_error( $result ) ) {
545 return $result;
546 }
547
548 $connection = Connections::get_by_id( $connection_id );
549
550 $response = $this->prepare_item_for_response( $connection, $request );
551 $response = rest_ensure_response( $response );
552
553 $response->set_status( 201 );
554
555 return $response;
556 }
557
558 $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
559
560 if ( is_wp_error( $response ) ) {
561 return new WP_Error(
562 'jp_connection_updation_failed',
563 __( 'Something went wrong while updating the connection.', 'jetpack-publicize-pkg' ),
564 $response->get_error_message()
565 );
566 }
567
568 $response = rest_ensure_response( $response );
569
570 $response->set_status( 201 );
571
572 return $response;
573 }
574
575 /**
576 * Checks if a given request has access to delete a connection.
577 *
578 * @param WP_REST_Request $request Full details about the request.
579 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
580 */
581 public function delete_item_permissions_check( $request ) {
582 $permissions = parent::publicize_permissions_check();
583
584 if ( is_wp_error( $permissions ) ) {
585 return $permissions;
586 }
587
588 return $this->manage_connection_permission_check( $request );
589 }
590
591 /**
592 * Delete a connection.
593 *
594 * @param WP_REST_Request $request Full details about the request.
595 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
596 */
597 public function delete_item( $request ) {
598 $connection_id = $request->get_param( 'connection_id' );
599
600 if ( Publicize_Utils::is_wpcom() ) {
601
602 $result = Connections::wpcom_delete_connection( $connection_id );
603
604 if ( is_wp_error( $result ) ) {
605 return $result;
606 }
607
608 $response = rest_ensure_response( $result );
609
610 $response->set_status( 201 );
611
612 return $response;
613 }
614
615 $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
616
617 if ( is_wp_error( $response ) ) {
618 return new WP_Error(
619 'jp_connection_deletion_failed',
620 __( 'Something went wrong while deleting the connection.', 'jetpack-publicize-pkg' ),
621 $response->get_error_message()
622 );
623 }
624
625 $response = rest_ensure_response( $response );
626
627 $response->set_status( 201 );
628
629 return $response;
630 }
631 }
632