Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.95% covered (danger)
20.95%
62 / 296
8.33% covered (danger)
8.33%
1 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connections_Controller
20.75% covered (danger)
20.75%
61 / 294
8.33% covered (danger)
8.33%
1 / 12
644.76
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
2
 get_item_schema
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
6
 get_the_item_schema
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
1 / 1
1
 get_items_permissions_check
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_items
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 create_item_permissions_check
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 create_item
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 update_item_permissions_check
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 update_item
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
 delete_item_permissions_check
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 delete_item
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * The Publicize Connections Controller class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize\REST_API;
9
10use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
11use Automattic\Jetpack\Publicize\Connections;
12use Automattic\Jetpack\Publicize\Jetpack_Social_Settings\Settings;
13use Automattic\Jetpack\Publicize\Publicize_Utils;
14use WP_Error;
15use WP_REST_Request;
16use WP_REST_Response;
17use WP_REST_Server;
18
19if ( ! defined( 'ABSPATH' ) ) {
20    exit( 0 );
21}
22
23/**
24 * Connections Controller class.
25 *
26 * @phan-constructor-used-for-side-effects
27 */
28class Connections_Controller extends Base_Controller {
29
30    use WPCOM_REST_API_Proxy_Request;
31
32    /**
33     * Constructor.
34     */
35    public function __construct() {
36        parent::__construct();
37
38        $this->base_api_path = 'wpcom';
39        $this->version       = 'v2';
40
41        $this->namespace = "{$this->base_api_path}/{$this->version}";
42        $this->rest_base = 'publicize/connections';
43
44        $this->allow_requests_as_blog = true;
45
46        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
47    }
48
49    /**
50     * Register the routes.
51     */
52    public function register_routes() {
53        register_rest_route(
54            $this->namespace,
55            '/' . $this->rest_base,
56            array(
57                array(
58                    'methods'             => WP_REST_Server::READABLE,
59                    'callback'            => array( $this, 'get_items' ),
60                    'permission_callback' => array( $this, 'get_items_permissions_check' ),
61                    'args'                => array(
62                        'test_connections' => array(
63                            'type'        => 'boolean',
64                            'description' => __( 'Whether to test connections.', 'jetpack-publicize-pkg' ),
65                        ),
66                    ),
67                ),
68                array(
69                    'methods'             => WP_REST_Server::CREATABLE,
70                    'callback'            => array( $this, 'create_item' ),
71                    'permission_callback' => array( $this, 'create_item_permissions_check' ),
72                    'args'                => array(
73                        'keyring_connection_ID' => array(
74                            'description' => __( 'Keyring connection ID.', 'jetpack-publicize-pkg' ),
75                            'type'        => 'integer',
76                            'required'    => true,
77                        ),
78                        'external_user_ID'      => array(
79                            'description' => __( 'External User Id - in case of services like Facebook.', 'jetpack-publicize-pkg' ),
80                            'type'        => 'string',
81                        ),
82                        'shared'                => array(
83                            'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
84                            'type'        => 'boolean',
85                        ),
86                    ),
87                ),
88                'schema' => array( $this, 'get_public_item_schema' ),
89            )
90        );
91
92        register_rest_route(
93            $this->namespace,
94            '/' . $this->rest_base . '/(?P<connection_id>[0-9]+)',
95            array(
96                'args'   => array(
97                    'connection_id' => array(
98                        'description' => __( 'Unique identifier for the connection.', 'jetpack-publicize-pkg' ),
99                        'type'        => 'string',
100                        'required'    => true,
101                    ),
102                ),
103                array(
104                    'methods'             => WP_REST_Server::EDITABLE,
105                    'callback'            => array( $this, 'update_item' ),
106                    'permission_callback' => array( $this, 'update_item_permissions_check' ),
107                    'args'                => array(
108                        'shared' => array(
109                            'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
110                            'type'        => 'boolean',
111                        ),
112                    ),
113                ),
114                array(
115                    'methods'             => WP_REST_Server::DELETABLE,
116                    'callback'            => array( $this, 'delete_item' ),
117                    'permission_callback' => array( $this, 'delete_item_permissions_check' ),
118
119                ),
120                'schema' => array( $this, 'get_public_item_schema' ),
121            )
122        );
123    }
124
125    /**
126     * Schema for the endpoint.
127     *
128     * @return array
129     */
130    public function get_item_schema() {
131        if ( $this->schema ) {
132            return $this->add_additional_fields_schema( $this->schema );
133        }
134        $deprecated_fields = array(
135            'id'                   => array(
136                'type'        => 'string',
137                'description' => __( 'Unique identifier for the Jetpack Social connection.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
138                    /* translators: %s is the new field name */
139                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
140                    'connection_id'
141                ),
142            ),
143            'username'             => array(
144                'type'        => 'string',
145                'description' => __( 'Username of the connected account.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
146                    /* translators: %s is the new field name */
147                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
148                    'external_handle'
149                ),
150            ),
151            'profile_display_name' => array(
152                'type'        => 'string',
153                'description' => __( 'The name to display in the profile of the connected account.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
154                    /* translators: %s is the new field name */
155                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
156                    'display_name'
157                ),
158            ),
159            'global'               => array(
160                'type'        => 'boolean',
161                'description' => __( 'Is this connection available to all users?', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
162                    /* translators: %s is the new field name */
163                    __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
164                    'shared'
165                ),
166            ),
167        );
168
169        $schema = array(
170            '$schema'    => 'http://json-schema.org/draft-04/schema#',
171            'title'      => 'jetpack-publicize-connection',
172            'type'       => 'object',
173            'properties' => array_merge(
174                $deprecated_fields,
175                self::get_the_item_schema()
176            ),
177        );
178
179        $this->schema = $schema;
180
181        return $this->add_additional_fields_schema( $schema );
182    }
183
184    /**
185     * Get the schema for the connection item.
186     *
187     * @return array
188     */
189    public static function get_the_item_schema() {
190        return array(
191            'connection_id'   => array(
192                'type'        => 'string',
193                'description' => __( 'Connection ID of the connected account.', 'jetpack-publicize-pkg' ),
194            ),
195            'display_name'    => array(
196                'type'        => 'string',
197                'description' => __( 'Display name of the connected account.', 'jetpack-publicize-pkg' ),
198            ),
199            'external_handle' => array(
200                'type'        => array( 'string', 'null' ),
201                'description' => __( 'The external handle or username of the connected account.', 'jetpack-publicize-pkg' ),
202            ),
203            'external_id'     => array(
204                'type'        => 'string',
205                'description' => __( 'The external ID of the connected account.', 'jetpack-publicize-pkg' ),
206            ),
207            'profile_link'    => array(
208                'type'        => 'string',
209                'description' => __( 'Profile link of the connected account.', 'jetpack-publicize-pkg' ),
210            ),
211            'profile_picture' => array(
212                'type'        => 'string',
213                'description' => __( 'URL of the profile picture of the connected account.', 'jetpack-publicize-pkg' ),
214            ),
215            'service_label'   => array(
216                'type'        => 'string',
217                'description' => __( 'Human-readable label for the Jetpack Social service.', 'jetpack-publicize-pkg' ),
218            ),
219            'service_name'    => array(
220                'type'        => 'string',
221                'description' => __( 'Alphanumeric identifier for the Jetpack Social service.', 'jetpack-publicize-pkg' ),
222            ),
223            'shared'          => array(
224                'type'        => 'boolean',
225                'description' => __( 'Whether the connection is shared with other users.', 'jetpack-publicize-pkg' ),
226            ),
227            'status'          => array(
228                'type'        => array( 'string', 'null' ),
229                'description' => __( 'The connection status.', 'jetpack-publicize-pkg' ),
230                'enum'        => array(
231                    'ok',
232                    'broken',
233                    'must_reauth',
234                    null,
235                ),
236            ),
237            'template'        => array(
238                'type'        => 'string',
239                'description' => __( 'Per-connection message template override. Empty string means fall back to the global template.', 'jetpack-publicize-pkg' ),
240                'default'     => '',
241                'maxLength'   => Settings::MESSAGE_TEMPLATE_MAX_LENGTH,
242                'arg_options' => array(
243                    'sanitize_callback' => array( Settings::class, 'sanitize_message_template' ),
244                ),
245            ),
246            'wpcom_user_id'   => array(
247                'type'        => 'integer',
248                'description' => __( 'wordpress.com ID of the user the connection belongs to.', 'jetpack-publicize-pkg' ),
249            ),
250        );
251    }
252
253    /**
254     * Verify that the request has access to connectoins list.
255     *
256     * @param WP_REST_Request $request Full details about the request.
257     * @return true|WP_Error
258     */
259    public function get_items_permissions_check( $request ) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
260        return $this->publicize_permissions_check();
261    }
262
263    /**
264     * Get list of connected Publicize connections.
265     *
266     * @param WP_REST_Request $request Full details about the request.
267     *
268     * @return WP_REST_Response suitable for 1-page collection
269     */
270    public function get_items( $request ) {
271        if ( Publicize_Utils::is_wpcom() ) {
272            $args = array(
273                'context'          => self::is_authorized_blog_request() ? 'blog' : 'user',
274                'test_connections' => $request->get_param( 'test_connections' ),
275            );
276
277            $connections = Connections::wpcom_get_connections( $args );
278        } else {
279            $connections = $this->proxy_request_to_wpcom_as_user( $request );
280        }
281
282        if ( is_wp_error( $connections ) ) {
283            return $connections;
284        }
285
286        $items = array();
287
288        foreach ( $connections as $item ) {
289            $data = $this->prepare_item_for_response( $item, $request );
290
291            $items[] = $this->prepare_response_for_collection( $data );
292        }
293
294        $response = rest_ensure_response( $items );
295        $response->header( 'X-WP-Total', (string) count( $items ) );
296        $response->header( 'X-WP-TotalPages', '1' );
297
298        return $response;
299    }
300
301    /**
302     * Checks if a given request has access to create a connection.
303     *
304     * @param WP_REST_Request $request Full details about the request.
305     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
306     */
307    public function create_item_permissions_check( $request ) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
308        $permissions = parent::publicize_permissions_check();
309
310        if ( is_wp_error( $permissions ) ) {
311            return $permissions;
312        }
313
314        return current_user_can( 'publish_posts' );
315    }
316
317    /**
318     * Creates a new connection.
319     *
320     * @param WP_REST_Request $request Full details about the request.
321     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
322     */
323    public function create_item( $request ) {
324        if ( Publicize_Utils::is_wpcom() ) {
325
326            $input = array(
327                'keyring_connection_ID' => $request->get_param( 'keyring_connection_ID' ),
328                'shared'                => $request->get_param( 'shared' ),
329            );
330
331            $external_user_id = $request->get_param( 'external_user_ID' );
332            if ( ! empty( $external_user_id ) ) {
333                $input['external_user_ID'] = $external_user_id;
334            }
335
336            $result = Connections::wpcom_create_connection( $input );
337
338            if ( is_wp_error( $result ) ) {
339                return $result;
340            }
341
342            $connection = Connections::get_by_id( $result );
343
344            $response = $this->prepare_item_for_response( $connection, $request );
345            $response = rest_ensure_response( $response );
346
347            $response->set_status( 201 );
348
349            return $response;
350
351        }
352
353        $response = $this->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) );
354
355        if ( is_wp_error( $response ) ) {
356            return new WP_Error(
357                'jp_connection_update_failed',
358                __( 'Something went wrong while creating a connection.', 'jetpack-publicize-pkg' ),
359                $response->get_error_message()
360            );
361        }
362
363        $response = rest_ensure_response( $response );
364
365        $response->set_status( 201 );
366
367        return $response;
368    }
369
370    /**
371     * Checks if a given request has access to update a connection.
372     *
373     * @param WP_REST_Request $request Full details about the request.
374     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
375     */
376    public function update_item_permissions_check( $request ) {
377        $permissions = parent::publicize_permissions_check();
378
379        if ( is_wp_error( $permissions ) ) {
380            return $permissions;
381        }
382
383        // If the user cannot manage the connection, they can't update it either.
384        if ( ! $this->manage_connection_permission_check( $request ) ) {
385            return new WP_Error(
386                'rest_cannot_edit',
387                __( 'Sorry, you are not allowed to update this connection.', 'jetpack-publicize-pkg' ),
388                array( 'status' => rest_authorization_required_code() )
389            );
390        }
391
392        // If the connection is being marked/unmarked as shared.
393        if ( $request->has_param( 'shared' ) ) {
394            // Only editors and above can mark a connection as shared.
395            return current_user_can( 'edit_others_posts' );
396        }
397
398        return current_user_can( 'publish_posts' );
399    }
400
401    /**
402     * Update a connection.
403     *
404     * @param WP_REST_Request $request Full details about the request.
405     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
406     */
407    public function update_item( $request ) {
408        $connection_id = $request->get_param( 'connection_id' );
409
410        if ( Publicize_Utils::is_wpcom() ) {
411
412            $input = array(
413                'shared' => $request->get_param( 'shared' ),
414            );
415
416            if ( $request->has_param( 'template' ) ) {
417                require_lib( 'publicize/util/message-templates' );
418
419                $template_value = Settings::sanitize_message_template( $request->get_param( 'template' ) );
420
421                /**
422                 * Only gate non-empty values. Clearing an existing override
423                 * must be allowed regardless of plan — otherwise users who
424                 * downgrade can't remove a previously-set template.
425                 */
426                if ( '' !== $template_value && ! \Publicize\can_use_per_connection_templates() ) {
427                    return new WP_Error(
428                        'rest_forbidden_per_connection_template',
429                        __( 'Per-connection message templates require an upgraded plan.', 'jetpack-publicize-pkg' ),
430                        array( 'status' => rest_authorization_required_code() )
431                    );
432                }
433
434                $input['template'] = $template_value;
435            }
436
437            $result = Connections::wpcom_update_connection( $connection_id, $input );
438
439            if ( is_wp_error( $result ) ) {
440                return $result;
441            }
442
443            $connection = Connections::get_by_id( $connection_id );
444
445            $response = $this->prepare_item_for_response( $connection, $request );
446            $response = rest_ensure_response( $response );
447
448            $response->set_status( 201 );
449
450            return $response;
451        }
452
453        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
454
455        if ( is_wp_error( $response ) ) {
456            return new WP_Error(
457                'jp_connection_updation_failed',
458                __( 'Something went wrong while updating the 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 delete 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 delete_item_permissions_check( $request ) {
477        $permissions = parent::publicize_permissions_check();
478
479        if ( is_wp_error( $permissions ) ) {
480            return $permissions;
481        }
482
483        return $this->manage_connection_permission_check( $request );
484    }
485
486    /**
487     * Delete a connection.
488     *
489     * @param WP_REST_Request $request Full details about the request.
490     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
491     */
492    public function delete_item( $request ) {
493        $connection_id = $request->get_param( 'connection_id' );
494
495        if ( Publicize_Utils::is_wpcom() ) {
496
497            $result = Connections::wpcom_delete_connection( $connection_id );
498
499            if ( is_wp_error( $result ) ) {
500                return $result;
501            }
502
503            $response = rest_ensure_response( $result );
504
505            $response->set_status( 201 );
506
507            return $response;
508        }
509
510        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
511
512        if ( is_wp_error( $response ) ) {
513            return new WP_Error(
514                'jp_connection_deletion_failed',
515                __( 'Something went wrong while deleting the connection.', 'jetpack-publicize-pkg' ),
516                $response->get_error_message()
517            );
518        }
519
520        $response = rest_ensure_response( $response );
521
522        $response->set_status( 201 );
523
524        return $response;
525    }
526}