Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
158 / 316
30.77% covered (danger)
30.77%
4 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connections_Controller
50.00% covered (danger)
50.00%
157 / 314
30.77% covered (danger)
30.77%
4 / 13
229.12
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 / 18
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        /*
293         * The Jetpack site path proxies to WPCOM instead of going through Connections::get_all(),
294         * so the filter is applied here too to keep both paths consistent.
295         *
296         * This filter is documented in projects/packages/publicize/src/class-connections.php
297         */
298        $connections = (array) apply_filters( 'jetpack_publicize_connections', $connections );
299
300        $items = array();
301
302        foreach ( $connections as $item ) {
303            $data = $this->prepare_item_for_response( $item, $request );
304
305            $items[] = $this->prepare_response_for_collection( $data );
306        }
307
308        $response = rest_ensure_response( $items );
309        $response->header( 'X-WP-Total', (string) count( $items ) );
310        $response->header( 'X-WP-TotalPages', '1' );
311
312        return $response;
313    }
314
315    /**
316     * Checks if a given request has access to create a connection.
317     *
318     * @param WP_REST_Request $request Full details about the request.
319     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
320     */
321    public function create_item_permissions_check( $request ) {
322        $permissions = parent::publicize_permissions_check();
323
324        if ( is_wp_error( $permissions ) ) {
325            return $permissions;
326        }
327
328        $shared_permission = $this->check_shared_param_permission( $request );
329
330        if ( is_wp_error( $shared_permission ) ) {
331            return $shared_permission;
332        }
333
334        return current_user_can( 'publish_posts' );
335    }
336
337    /**
338     * Check whether the request is allowed to set the `shared` flag on a connection.
339     *
340     * Shared connections are usable by every author on the site, so only editors
341     * and above may set the flag. Used by both the create and the update permission
342     * check, so the rule cannot drift between the two.
343     *
344     * @param WP_REST_Request $request Full details about the request.
345     * @return true|WP_Error True if the request may proceed, WP_Error object otherwise.
346     */
347    protected function check_shared_param_permission( $request ) {
348        if ( ! $request->has_param( 'shared' ) ) {
349            return true;
350        }
351
352        if ( ! current_user_can( 'edit_others_posts' ) ) {
353            return new WP_Error(
354                'rest_cannot_share_connection',
355                __( 'Sorry, you are not allowed to share connections with other users.', 'jetpack-publicize-pkg' ),
356                array( 'status' => rest_authorization_required_code() )
357            );
358        }
359
360        return true;
361    }
362
363    /**
364     * Creates a new connection.
365     *
366     * @param WP_REST_Request $request Full details about the request.
367     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
368     */
369    public function create_item( $request ) {
370        if ( Publicize_Utils::is_wpcom() ) {
371
372            $input = array(
373                'keyring_connection_ID' => $request->get_param( 'keyring_connection_ID' ),
374                'shared'                => $request->get_param( 'shared' ),
375            );
376
377            $external_user_id = $request->get_param( 'external_user_ID' );
378            if ( ! empty( $external_user_id ) ) {
379                $input['external_user_ID'] = $external_user_id;
380            }
381
382            $result = Connections::wpcom_create_connection( $input );
383
384            if ( is_wp_error( $result ) ) {
385                return $result;
386            }
387
388            $connection = Connections::get_by_id( $result );
389
390            $response = $this->prepare_item_for_response( $connection, $request );
391            $response = rest_ensure_response( $response );
392
393            $response->set_status( 201 );
394
395            return $response;
396
397        }
398
399        $response = $this->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) );
400
401        if ( is_wp_error( $response ) ) {
402            return new WP_Error(
403                'jp_connection_update_failed',
404                __( 'Something went wrong while creating a connection.', 'jetpack-publicize-pkg' ),
405                $response->get_error_message()
406            );
407        }
408
409        $response = rest_ensure_response( $response );
410
411        $response->set_status( 201 );
412
413        return $response;
414    }
415
416    /**
417     * Checks if a given request has access to update a connection.
418     *
419     * @param WP_REST_Request $request Full details about the request.
420     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
421     */
422    public function update_item_permissions_check( $request ) {
423        $permissions = parent::publicize_permissions_check();
424
425        if ( is_wp_error( $permissions ) ) {
426            return $permissions;
427        }
428
429        // If the user cannot manage the connection, they can't update it either.
430        if ( ! $this->manage_connection_permission_check( $request ) ) {
431            return new WP_Error(
432                'rest_cannot_edit',
433                __( 'Sorry, you are not allowed to update this connection.', 'jetpack-publicize-pkg' ),
434                array( 'status' => rest_authorization_required_code() )
435            );
436        }
437
438        $shared_permission = $this->check_shared_param_permission( $request );
439
440        if ( is_wp_error( $shared_permission ) ) {
441            return $shared_permission;
442        }
443
444        return current_user_can( 'publish_posts' );
445    }
446
447    /**
448     * Update a connection.
449     *
450     * @param WP_REST_Request $request Full details about the request.
451     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
452     */
453    public function update_item( $request ) {
454        $connection_id = $request->get_param( 'connection_id' );
455
456        if ( Publicize_Utils::is_wpcom() ) {
457
458            $input = array(
459                'shared' => $request->get_param( 'shared' ),
460            );
461
462            if ( $request->has_param( 'template' ) ) {
463                require_lib( 'publicize/util/message-templates' );
464
465                $template_value = Settings::sanitize_message_template( $request->get_param( 'template' ) );
466
467                /**
468                 * Only gate non-empty values. Clearing an existing override
469                 * must be allowed regardless of plan — otherwise users who
470                 * downgrade can't remove a previously-set template.
471                 */
472                if ( '' !== $template_value && ! \Publicize\can_use_per_connection_templates() ) {
473                    return new WP_Error(
474                        'rest_forbidden_per_connection_template',
475                        __( 'Per-connection message templates require an upgraded plan.', 'jetpack-publicize-pkg' ),
476                        array( 'status' => rest_authorization_required_code() )
477                    );
478                }
479
480                $input['template'] = $template_value;
481            }
482
483            $result = Connections::wpcom_update_connection( $connection_id, $input );
484
485            if ( is_wp_error( $result ) ) {
486                return $result;
487            }
488
489            $connection = Connections::get_by_id( $connection_id );
490
491            $response = $this->prepare_item_for_response( $connection, $request );
492            $response = rest_ensure_response( $response );
493
494            $response->set_status( 201 );
495
496            return $response;
497        }
498
499        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
500
501        if ( is_wp_error( $response ) ) {
502            return new WP_Error(
503                'jp_connection_updation_failed',
504                __( 'Something went wrong while updating the connection.', 'jetpack-publicize-pkg' ),
505                $response->get_error_message()
506            );
507        }
508
509        $response = rest_ensure_response( $response );
510
511        $response->set_status( 201 );
512
513        return $response;
514    }
515
516    /**
517     * Checks if a given request has access to delete a connection.
518     *
519     * @param WP_REST_Request $request Full details about the request.
520     * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
521     */
522    public function delete_item_permissions_check( $request ) {
523        $permissions = parent::publicize_permissions_check();
524
525        if ( is_wp_error( $permissions ) ) {
526            return $permissions;
527        }
528
529        return $this->manage_connection_permission_check( $request );
530    }
531
532    /**
533     * Delete a connection.
534     *
535     * @param WP_REST_Request $request Full details about the request.
536     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
537     */
538    public function delete_item( $request ) {
539        $connection_id = $request->get_param( 'connection_id' );
540
541        if ( Publicize_Utils::is_wpcom() ) {
542
543            $result = Connections::wpcom_delete_connection( $connection_id );
544
545            if ( is_wp_error( $result ) ) {
546                return $result;
547            }
548
549            $response = rest_ensure_response( $result );
550
551            $response->set_status( 201 );
552
553            return $response;
554        }
555
556        $response = $this->proxy_request_to_wpcom_as_user( $request, $connection_id, array( 'timeout' => 120 ) );
557
558        if ( is_wp_error( $response ) ) {
559            return new WP_Error(
560                'jp_connection_deletion_failed',
561                __( 'Something went wrong while deleting the connection.', 'jetpack-publicize-pkg' ),
562                $response->get_error_message()
563            );
564        }
565
566        $response = rest_ensure_response( $response );
567
568        $response->set_status( 201 );
569
570        return $response;
571    }
572}