Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.16% covered (warning)
50.16%
158 / 315
30.77% covered (danger)
30.77%
4 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connections_Controller
50.16% covered (warning)
50.16%
157 / 313
30.77% covered (danger)
30.77%
4 / 13
227.31
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 register_routes
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
1 / 1
1
 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%
67 / 67
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
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 check_shared_param_permission
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 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 / 13
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                'description' => __( 'The connection status.', 'jetpack-publicize-pkg' ),
229                'oneOf'       => array(
230                    array(
231                        'type' => 'string',
232                        'enum' => array(
233                            'ok',
234                            'broken',
235                            'must_reauth',
236                        ),
237                    ),
238                    array(
239                        'type' => 'null',
240                    ),
241                ),
242            ),
243            'template'        => array(
244                'type'        => 'string',
245                'description' => __( 'Per-connection message template override. Empty string means fall back to the global template.', 'jetpack-publicize-pkg' ),
246                'default'     => '',
247                'maxLength'   => Settings::MESSAGE_TEMPLATE_MAX_LENGTH,
248                'arg_options' => array(
249                    'sanitize_callback' => array( Settings::class, 'sanitize_message_template' ),
250                ),
251            ),
252            'wpcom_user_id'   => array(
253                'type'        => 'integer',
254                'description' => __( 'wordpress.com ID of the user the connection belongs to.', 'jetpack-publicize-pkg' ),
255            ),
256        );
257    }
258
259    /**
260     * Verify that the request has access to connectoins list.
261     *
262     * @param WP_REST_Request $request Full details about the request.
263     * @return true|WP_Error
264     */
265    public function get_items_permissions_check( $request ) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
266        return $this->publicize_permissions_check();
267    }
268
269    /**
270     * Get list of connected Publicize connections.
271     *
272     * @param WP_REST_Request $request Full details about the request.
273     *
274     * @return WP_REST_Response suitable for 1-page collection
275     */
276    public function get_items( $request ) {
277        if ( Publicize_Utils::is_wpcom() ) {
278            $args = array(
279                'context'          => self::is_authorized_blog_request() ? 'blog' : 'user',
280                'test_connections' => $request->get_param( 'test_connections' ),
281            );
282
283            $connections = Connections::wpcom_get_connections( $args );
284        } else {
285            $connections = $this->proxy_request_to_wpcom_as_user( $request );
286        }
287
288        if ( is_wp_error( $connections ) ) {
289            return $connections;
290        }
291
292        $items = array();
293
294        foreach ( $connections as $item ) {
295            $data = $this->prepare_item_for_response( $item, $request );
296
297            $items[] = $this->prepare_response_for_collection( $data );
298        }
299
300        $response = rest_ensure_response( $items );
301        $response->header( 'X-WP-Total', (string) count( $items ) );
302        $response->header( 'X-WP-TotalPages', '1' );
303
304        return $response;
305    }
306
307    /**
308     * Checks if a given request has access to create a connection.
309     *
310     * @param WP_REST_Request $request Full details about the request.
311     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
312     */
313    public function create_item_permissions_check( $request ) {
314        $permissions = parent::publicize_permissions_check();
315
316        if ( is_wp_error( $permissions ) ) {
317            return $permissions;
318        }
319
320        $shared_permission = $this->check_shared_param_permission( $request );
321
322        if ( is_wp_error( $shared_permission ) ) {
323            return $shared_permission;
324        }
325
326        return current_user_can( 'publish_posts' );
327    }
328
329    /**
330     * Check whether the request is allowed to set the `shared` flag on a connection.
331     *
332     * Shared connections are usable by every author on the site, so only editors
333     * and above may set the flag. Used by both the create and the update permission
334     * check, so the rule cannot drift between the two.
335     *
336     * @param WP_REST_Request $request Full details about the request.
337     * @return true|WP_Error True if the request may proceed, WP_Error object otherwise.
338     */
339    protected function check_shared_param_permission( $request ) {
340        if ( ! $request->has_param( 'shared' ) ) {
341            return true;
342        }
343
344        if ( ! current_user_can( 'edit_others_posts' ) ) {
345            return new WP_Error(
346                'rest_cannot_share_connection',
347                __( 'Sorry, you are not allowed to share connections with other users.', 'jetpack-publicize-pkg' ),
348                array( 'status' => rest_authorization_required_code() )
349            );
350        }
351
352        return true;
353    }
354
355    /**
356     * Creates a new connection.
357     *
358     * @param WP_REST_Request $request Full details about the request.
359     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
360     */
361    public function create_item( $request ) {
362        if ( Publicize_Utils::is_wpcom() ) {
363
364            $input = array(
365                'keyring_connection_ID' => $request->get_param( 'keyring_connection_ID' ),
366                'shared'                => $request->get_param( 'shared' ),
367            );
368
369            $external_user_id = $request->get_param( 'external_user_ID' );
370            if ( ! empty( $external_user_id ) ) {
371                $input['external_user_ID'] = $external_user_id;
372            }
373
374            $result = Connections::wpcom_create_connection( $input );
375
376            if ( is_wp_error( $result ) ) {
377                return $result;
378            }
379
380            $connection = Connections::get_by_id( $result );
381
382            $response = $this->prepare_item_for_response( $connection, $request );
383            $response = rest_ensure_response( $response );
384
385            $response->set_status( 201 );
386
387            return $response;
388
389        }
390
391        $response = $this->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) );
392
393        if ( is_wp_error( $response ) ) {
394            return new WP_Error(
395                'jp_connection_update_failed',
396                __( 'Something went wrong while creating a connection.', 'jetpack-publicize-pkg' ),
397                $response->get_error_message()
398            );
399        }
400
401        $response = rest_ensure_response( $response );
402
403        $response->set_status( 201 );
404
405        return $response;
406    }
407
408    /**
409     * Checks if a given request has access to update a connection.
410     *
411     * @param WP_REST_Request $request Full details about the request.
412     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
413     */
414    public function update_item_permissions_check( $request ) {
415        $permissions = parent::publicize_permissions_check();
416
417        if ( is_wp_error( $permissions ) ) {
418            return $permissions;
419        }
420
421        // If the user cannot manage the connection, they can't update it either.
422        if ( ! $this->manage_connection_permission_check( $request ) ) {
423            return new WP_Error(
424                'rest_cannot_edit',
425                __( 'Sorry, you are not allowed to update this connection.', 'jetpack-publicize-pkg' ),
426                array( 'status' => rest_authorization_required_code() )
427            );
428        }
429
430        $shared_permission = $this->check_shared_param_permission( $request );
431
432        if ( is_wp_error( $shared_permission ) ) {
433            return $shared_permission;
434        }
435
436        return current_user_can( 'publish_posts' );
437    }
438
439    /**
440     * Update a connection.
441     *
442     * @param WP_REST_Request $request Full details about the request.
443     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
444     */
445    public function update_item( $request ) {
446        $connection_id = $request->get_param( 'connection_id' );
447
448        if ( Publicize_Utils::is_wpcom() ) {
449
450            $input = array(
451                'shared' => $request->get_param( 'shared' ),
452            );
453
454            if ( $request->has_param( 'template' ) ) {
455                require_lib( 'publicize/util/message-templates' );
456
457                $template_value = Settings::sanitize_message_template( $request->get_param( 'template' ) );
458
459                /**
460                 * Only gate non-empty values. Clearing an existing override
461                 * must be allowed regardless of plan — otherwise users who
462                 * downgrade can't remove a previously-set template.
463                 */
464                if ( '' !== $template_value && ! \Publicize\can_use_per_connection_templates() ) {
465                    return new WP_Error(
466                        'rest_forbidden_per_connection_template',
467                        __( 'Per-connection message templates require an upgraded plan.', 'jetpack-publicize-pkg' ),
468                        array( 'status' => rest_authorization_required_code() )
469                    );
470                }
471
472                $input['template'] = $template_value;
473            }
474
475            $result = Connections::wpcom_update_connection( $connection_id, $input );
476
477            if ( is_wp_error( $result ) ) {
478                return $result;
479            }
480
481            $connection = Connections::get_by_id( $connection_id );
482
483            $response = $this->prepare_item_for_response( $connection, $request );
484            $response = rest_ensure_response( $response );
485
486            $response->set_status( 201 );
487
488            return $response;
489        }
490
491        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
492
493        if ( is_wp_error( $response ) ) {
494            return new WP_Error(
495                'jp_connection_updation_failed',
496                __( 'Something went wrong while updating the connection.', 'jetpack-publicize-pkg' ),
497                $response->get_error_message()
498            );
499        }
500
501        $response = rest_ensure_response( $response );
502
503        $response->set_status( 201 );
504
505        return $response;
506    }
507
508    /**
509     * Checks if a given request has access to delete a connection.
510     *
511     * @param WP_REST_Request $request Full details about the request.
512     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
513     */
514    public function delete_item_permissions_check( $request ) {
515        $permissions = parent::publicize_permissions_check();
516
517        if ( is_wp_error( $permissions ) ) {
518            return $permissions;
519        }
520
521        return $this->manage_connection_permission_check( $request );
522    }
523
524    /**
525     * Delete a connection.
526     *
527     * @param WP_REST_Request $request Full details about the request.
528     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
529     */
530    public function delete_item( $request ) {
531        $connection_id = $request->get_param( 'connection_id' );
532
533        if ( Publicize_Utils::is_wpcom() ) {
534
535            $result = Connections::wpcom_delete_connection( $connection_id );
536
537            if ( is_wp_error( $result ) ) {
538                return $result;
539            }
540
541            $response = rest_ensure_response( $result );
542
543            $response->set_status( 201 );
544
545            return $response;
546        }
547
548        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
549
550        if ( is_wp_error( $response ) ) {
551            return new WP_Error(
552                'jp_connection_deletion_failed',
553                __( 'Something went wrong while deleting the connection.', 'jetpack-publicize-pkg' ),
554                $response->get_error_message()
555            );
556        }
557
558        $response = rest_ensure_response( $response );
559
560        $response->set_status( 201 );
561
562        return $response;
563    }
564}