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

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