PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-publicize/src/rest-api/class-connections-controller.php +631 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,631 @@
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 +}