Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.52% covered (danger)
22.52%
68 / 302
8.33% covered (danger)
8.33%
1 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connections_Controller
22.33% covered (danger)
22.33%
67 / 300
8.33% covered (danger)
8.33%
1 / 12
608.91
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%
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
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                '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 ) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
314        $permissions = parent::publicize_permissions_check();
315
316        if ( is_wp_error( $permissions ) ) {
317            return $permissions;
318        }
319
320        return current_user_can( 'publish_posts' );
321    }
322
323    /**
324     * Creates a new connection.
325     *
326     * @param WP_REST_Request $request Full details about the request.
327     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
328     */
329    public function create_item( $request ) {
330        if ( Publicize_Utils::is_wpcom() ) {
331
332            $input = array(
333                'keyring_connection_ID' => $request->get_param( 'keyring_connection_ID' ),
334                'shared'                => $request->get_param( 'shared' ),
335            );
336
337            $external_user_id = $request->get_param( 'external_user_ID' );
338            if ( ! empty( $external_user_id ) ) {
339                $input['external_user_ID'] = $external_user_id;
340            }
341
342            $result = Connections::wpcom_create_connection( $input );
343
344            if ( is_wp_error( $result ) ) {
345                return $result;
346            }
347
348            $connection = Connections::get_by_id( $result );
349
350            $response = $this->prepare_item_for_response( $connection, $request );
351            $response = rest_ensure_response( $response );
352
353            $response->set_status( 201 );
354
355            return $response;
356
357        }
358
359        $response = $this->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) );
360
361        if ( is_wp_error( $response ) ) {
362            return new WP_Error(
363                'jp_connection_update_failed',
364                __( 'Something went wrong while creating a connection.', 'jetpack-publicize-pkg' ),
365                $response->get_error_message()
366            );
367        }
368
369        $response = rest_ensure_response( $response );
370
371        $response->set_status( 201 );
372
373        return $response;
374    }
375
376    /**
377     * Checks if a given request has access to update a connection.
378     *
379     * @param WP_REST_Request $request Full details about the request.
380     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
381     */
382    public function update_item_permissions_check( $request ) {
383        $permissions = parent::publicize_permissions_check();
384
385        if ( is_wp_error( $permissions ) ) {
386            return $permissions;
387        }
388
389        // If the user cannot manage the connection, they can't update it either.
390        if ( ! $this->manage_connection_permission_check( $request ) ) {
391            return new WP_Error(
392                'rest_cannot_edit',
393                __( 'Sorry, you are not allowed to update this connection.', 'jetpack-publicize-pkg' ),
394                array( 'status' => rest_authorization_required_code() )
395            );
396        }
397
398        // If the connection is being marked/unmarked as shared.
399        if ( $request->has_param( 'shared' ) ) {
400            // Only editors and above can mark a connection as shared.
401            return current_user_can( 'edit_others_posts' );
402        }
403
404        return current_user_can( 'publish_posts' );
405    }
406
407    /**
408     * Update a connection.
409     *
410     * @param WP_REST_Request $request Full details about the request.
411     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
412     */
413    public function update_item( $request ) {
414        $connection_id = $request->get_param( 'connection_id' );
415
416        if ( Publicize_Utils::is_wpcom() ) {
417
418            $input = array(
419                'shared' => $request->get_param( 'shared' ),
420            );
421
422            if ( $request->has_param( 'template' ) ) {
423                require_lib( 'publicize/util/message-templates' );
424
425                $template_value = Settings::sanitize_message_template( $request->get_param( 'template' ) );
426
427                /**
428                 * Only gate non-empty values. Clearing an existing override
429                 * must be allowed regardless of plan — otherwise users who
430                 * downgrade can't remove a previously-set template.
431                 */
432                if ( '' !== $template_value && ! \Publicize\can_use_per_connection_templates() ) {
433                    return new WP_Error(
434                        'rest_forbidden_per_connection_template',
435                        __( 'Per-connection message templates require an upgraded plan.', 'jetpack-publicize-pkg' ),
436                        array( 'status' => rest_authorization_required_code() )
437                    );
438                }
439
440                $input['template'] = $template_value;
441            }
442
443            $result = Connections::wpcom_update_connection( $connection_id, $input );
444
445            if ( is_wp_error( $result ) ) {
446                return $result;
447            }
448
449            $connection = Connections::get_by_id( $connection_id );
450
451            $response = $this->prepare_item_for_response( $connection, $request );
452            $response = rest_ensure_response( $response );
453
454            $response->set_status( 201 );
455
456            return $response;
457        }
458
459        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
460
461        if ( is_wp_error( $response ) ) {
462            return new WP_Error(
463                'jp_connection_updation_failed',
464                __( 'Something went wrong while updating the connection.', 'jetpack-publicize-pkg' ),
465                $response->get_error_message()
466            );
467        }
468
469        $response = rest_ensure_response( $response );
470
471        $response->set_status( 201 );
472
473        return $response;
474    }
475
476    /**
477     * Checks if a given request has access to delete a connection.
478     *
479     * @param WP_REST_Request $request Full details about the request.
480     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
481     */
482    public function delete_item_permissions_check( $request ) {
483        $permissions = parent::publicize_permissions_check();
484
485        if ( is_wp_error( $permissions ) ) {
486            return $permissions;
487        }
488
489        return $this->manage_connection_permission_check( $request );
490    }
491
492    /**
493     * Delete a connection.
494     *
495     * @param WP_REST_Request $request Full details about the request.
496     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
497     */
498    public function delete_item( $request ) {
499        $connection_id = $request->get_param( 'connection_id' );
500
501        if ( Publicize_Utils::is_wpcom() ) {
502
503            $result = Connections::wpcom_delete_connection( $connection_id );
504
505            if ( is_wp_error( $result ) ) {
506                return $result;
507            }
508
509            $response = rest_ensure_response( $result );
510
511            $response->set_status( 201 );
512
513            return $response;
514        }
515
516        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
517
518        if ( is_wp_error( $response ) ) {
519            return new WP_Error(
520                'jp_connection_deletion_failed',
521                __( 'Something went wrong while deleting the connection.', 'jetpack-publicize-pkg' ),
522                $response->get_error_message()
523            );
524        }
525
526        $response = rest_ensure_response( $response );
527
528        $response->set_status( 201 );
529
530        return $response;
531    }
532}