Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.56% covered (warning)
56.56%
125 / 221
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
56.56% covered (warning)
56.56%
125 / 221
16.67% covered (danger)
16.67%
2 / 12
149.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_rest_routes
93.18% covered (success)
93.18%
123 / 132
0.00% covered (danger)
0.00%
0 / 1
2.00
 manage_connection_permission_check
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 update_connection_permission_check
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 require_admin_privilege_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 require_author_privilege_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_jetpack_social_connections_schema
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 get_jetpack_social_connections_update_schema
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 get_publicize_connection_test_results
n/a
0 / 0
n/a
0 / 0
1
 get_publicize_connections
n/a
0 / 0
n/a
0 / 0
2
 create_publicize_connection
n/a
0 / 0
n/a
0 / 0
1
 update_publicize_connection
n/a
0 / 0
n/a
0 / 0
1
 delete_publicize_connection
n/a
0 / 0
n/a
0 / 0
1
 get_social_product_info
n/a
0 / 0
n/a
0 / 0
2
 share_post
n/a
0 / 0
n/a
0 / 0
1
 make_proper_response
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 get_blog_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 update_post_shares
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 get_post_share_status
n/a
0 / 0
n/a
0 / 0
1
1<?php
2/**
3 * The Publicize Rest Controller class.
4 * Registers the REST routes for Publicize.
5 *
6 * @package automattic/jetpack-publicize
7 */
8
9namespace Automattic\Jetpack\Publicize;
10
11use Automattic\Jetpack\Connection\Rest_Authentication;
12use Automattic\Jetpack\Publicize\REST_API\Proxy_Requests;
13use Jetpack_Options;
14use WP_Error;
15use WP_REST_Request;
16use WP_REST_Response;
17use WP_REST_Server;
18
19/**
20 * Registers the REST routes for Search.
21 */
22class REST_Controller {
23    /**
24     * Whether it's run on WPCOM.
25     *
26     * @var bool
27     */
28    protected $is_wpcom;
29
30    /**
31     * Social Product Slugs
32     *
33     * @var string
34     */
35    const JETPACK_SOCIAL_V1_YEARLY = 'jetpack_social_v1_yearly';
36
37    /**
38     * Constructor
39     *
40     * @param bool $is_wpcom - Whether it's run on WPCOM.
41     */
42    public function __construct( $is_wpcom = false ) {
43        $this->is_wpcom = $is_wpcom;
44    }
45
46    /**
47     * Registers the REST routes on the `rest_api_init` hook.
48     *
49     * Instantiated here, rather than eagerly, so the controller class only loads
50     * on requests that reach `rest_api_init`. Static so the callback can be
51     * unregistered.
52     *
53     * @access public
54     */
55    public static function register() {
56        ( new self() )->register_rest_routes();
57    }
58
59    /**
60     * Registers the REST routes for Social.
61     *
62     * @access public
63     * @static
64     */
65    public function register_rest_routes() {
66        register_rest_route(
67            'jetpack/v4',
68            '/publicize/connection-test-results',
69            array(
70                'methods'             => WP_REST_Server::READABLE,
71                'callback'            => array( $this, 'get_publicize_connection_test_results' ),
72                'permission_callback' => array( $this, 'require_author_privilege_callback' ),
73            )
74        );
75
76        register_rest_route(
77            'jetpack/v4',
78            '/publicize/connections',
79            array(
80                'methods'             => WP_REST_Server::READABLE,
81                'callback'            => array( $this, 'get_publicize_connections' ),
82                'permission_callback' => array( $this, 'require_author_privilege_callback' ),
83            )
84        );
85
86        // Get current social product from the product's endpoint.
87        register_rest_route(
88            'jetpack/v4',
89            '/social-product-info',
90            array(
91                'methods'             => WP_REST_Server::READABLE,
92                'callback'            => array( $this, 'get_social_product_info' ),
93                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
94            )
95        );
96
97        register_rest_route(
98            'jetpack/v4',
99            '/publicize/(?P<postId>\d+)',
100            array(
101                'methods'             => WP_REST_Server::CREATABLE,
102                'callback'            => array( $this, 'share_post' ),
103                'permission_callback' => array( $this, 'require_author_privilege_callback' ),
104                'args'                => array(
105                    'message'             => array(
106                        'description'       => __( 'The message to share.', 'jetpack-publicize-pkg' ),
107                        'type'              => 'string',
108                        'required'          => true,
109                        'validate_callback' => function ( $param ) {
110                            return is_string( $param );
111                        },
112                        'sanitize_callback' => 'sanitize_textarea_field',
113                    ),
114                    'skipped_connections' => array(
115                        'description'       => __( 'Array of external connection IDs to skip sharing.', 'jetpack-publicize-pkg' ),
116                        'type'              => 'array',
117                        'required'          => false,
118                        'validate_callback' => function ( $param ) {
119                            return is_array( $param );
120                        },
121                        'sanitize_callback' => function ( $param ) {
122                            if ( ! is_array( $param ) ) {
123                                return new WP_Error(
124                                    'rest_invalid_param',
125                                    esc_html__( 'The skipped_connections argument must be an array of connection IDs.', 'jetpack-publicize-pkg' ),
126                                    array( 'status' => 400 )
127                                );
128                            }
129                            return array_map( 'absint', $param );
130                        },
131                    ),
132                    'async'               => array(
133                        'description' => __( 'Whether to share the post asynchronously.', 'jetpack-publicize-pkg' ),
134                        'type'        => 'boolean',
135                        'default'     => false,
136                    ),
137                ),
138            )
139        );
140
141        // Create a Jetpack Social connection.
142        register_rest_route(
143            'jetpack/v4',
144            '/social/connections',
145            array(
146                'methods'             => WP_REST_Server::CREATABLE,
147                'callback'            => array( $this, 'create_publicize_connection' ),
148                'permission_callback' => array( $this, 'require_author_privilege_callback' ),
149                'schema'              => array( $this, 'get_jetpack_social_connections_schema' ),
150            )
151        );
152
153        // Update a Jetpack Social connection.
154        register_rest_route(
155            'jetpack/v4',
156            '/social/connections/(?P<connection_id>\d+)',
157            array(
158                'methods'             => WP_REST_Server::EDITABLE,
159                'callback'            => array( $this, 'update_publicize_connection' ),
160                'permission_callback' => array( $this, 'update_connection_permission_check' ),
161                'schema'              => array( $this, 'get_jetpack_social_connections_update_schema' ),
162            )
163        );
164
165        // Delete a Jetpack Social connection.
166        register_rest_route(
167            'jetpack/v4',
168            '/social/connections/(?P<connection_id>\d+)',
169            array(
170                'methods'             => WP_REST_Server::DELETABLE,
171                'callback'            => array( $this, 'delete_publicize_connection' ),
172                'permission_callback' => array( $this, 'manage_connection_permission_check' ),
173            )
174        );
175
176        register_rest_route(
177            'jetpack/v4',
178            '/social/sync-shares/post/(?P<id>\d+)',
179            array(
180                array(
181                    'methods'             => WP_REST_Server::EDITABLE,
182                    'callback'            => array( $this, 'update_post_shares' ),
183                    'permission_callback' => array( Rest_Authentication::class, 'is_signed_with_blog_token' ),
184                    'args'                => array(
185                        'meta' => array(
186                            'type'       => 'object',
187                            'required'   => true,
188                            'properties' => array(
189                                '_publicize_shares' => array(
190                                    'type'     => 'array',
191                                    'required' => true,
192                                ),
193                            ),
194                        ),
195                    ),
196                ),
197            )
198        );
199
200        register_rest_route(
201            'jetpack/v4',
202            '/social/share-status/(?P<post_id>\d+)',
203            array(
204                array(
205                    'methods'             => WP_REST_Server::READABLE,
206                    'callback'            => array( $this, 'get_post_share_status' ),
207                    'permission_callback' => array( $this, 'require_author_privilege_callback' ),
208                ),
209            )
210        );
211    }
212
213    /**
214     * Manage connection permission check
215     *
216     * @param WP_REST_Request $request The request object, which includes the parameters.
217     *
218     * @return bool True if the user can manage the connection, false otherwise.
219     */
220    public function manage_connection_permission_check( WP_REST_Request $request ) {
221
222        if ( current_user_can( 'edit_others_posts' ) ) {
223            return true;
224        }
225
226        /**
227         * Publicize instance.
228         *
229         * @var Publicize $publicize Publicize instance.
230         */
231        global $publicize;
232
233        $connection = $publicize->get_connection_for_user( $request->get_param( 'connection_id' ) );
234
235        $owns_connection = isset( $connection['user_id'] ) && get_current_user_id() === (int) $connection['user_id'];
236
237        return $owns_connection;
238    }
239
240    /**
241     * Update connection permission check.
242     *
243     * @param WP_REST_Request $request The request object, which includes the parameters.
244     *
245     * @return bool True if the user can update the connection, false otherwise.
246     */
247    public function update_connection_permission_check( WP_REST_Request $request ) {
248
249        // If the user cannot manage the connection, they can't update it either.
250        if ( ! $this->manage_connection_permission_check( $request ) ) {
251            return false;
252        }
253
254        // If the connection is being marked/unmarked as shared.
255        if ( $request->has_param( 'shared' ) ) {
256            // Only editors and above can mark a connection as shared.
257            return current_user_can( 'edit_others_posts' );
258        }
259
260        return $this->require_author_privilege_callback();
261    }
262
263    /**
264     * Only administrators can access the API.
265     *
266     * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
267     */
268    public function require_admin_privilege_callback() {
269        return current_user_can( 'manage_options' );
270    }
271
272    /**
273     * Only Authors can access the API.
274     *
275     * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
276     */
277    public function require_author_privilege_callback() {
278        return current_user_can( 'publish_posts' );
279    }
280
281    /**
282     * Retrieves the JSON schema for creating a jetpack social connection.
283     *
284     * @return array Schema data.
285     */
286    public function get_jetpack_social_connections_schema() {
287        $schema = array(
288            '$schema'    => 'http://json-schema.org/draft-04/schema#',
289            'title'      => 'jetpack-social-connection',
290            'type'       => 'object',
291            'properties' => array(
292                'keyring_connection_ID' => array(
293                    'description' => __( 'Keyring connection ID', 'jetpack-publicize-pkg' ),
294                    'type'        => 'integer',
295                    'required'    => true,
296                ),
297                'external_user_ID'      => array(
298                    'description' => __( 'External User Id - in case of services like Facebook', 'jetpack-publicize-pkg' ),
299                    'type'        => 'string',
300                ),
301                'shared'                => array(
302                    'description' => __( 'Whether the connection is shared with other users', 'jetpack-publicize-pkg' ),
303                    'type'        => 'boolean',
304                ),
305            ),
306        );
307
308        return rest_default_additional_properties_to_false( $schema );
309    }
310
311    /**
312     * Retrieves the JSON schema for updating a jetpack social connection.
313     *
314     * @return array Schema data.
315     */
316    public function get_jetpack_social_connections_update_schema() {
317        $schema = array(
318            '$schema'    => 'http://json-schema.org/draft-04/schema#',
319            'title'      => 'jetpack-social-connection',
320            'type'       => 'object',
321            'properties' => array(
322                'external_user_ID' => array(
323                    'description' => __( 'External User Id - in case of services like Facebook', 'jetpack-publicize-pkg' ),
324                    'type'        => 'string',
325                ),
326                'shared'           => array(
327                    'description' => __( 'Whether the connection is shared with other users', 'jetpack-publicize-pkg' ),
328                    'type'        => 'boolean',
329                ),
330            ),
331        );
332
333        return rest_default_additional_properties_to_false( $schema );
334    }
335
336    /**
337     * Gets the current Publicize connections, with the resolt of testing them, for the site.
338     *
339     * GET `jetpack/v4/publicize/connection-test-results`
340     *
341     * @deprecated 0.61.1
342     */
343    public function get_publicize_connection_test_results() {
344
345        Publicize_Utils::endpoint_deprecated_warning(
346            __METHOD__,
347            'jetpack-14.4, jetpack-social-6.2.0',
348            'jetpack/v4/publicize/connection-test-results',
349            'wpcom/v2/publicize/connections?test_connections=1'
350        );
351
352        $proxy = new Proxy_Requests( 'publicize/connections' );
353
354        $request = new WP_REST_Request( 'GET' );
355
356        $request->set_param( 'test_connections', '1' );
357
358        return rest_ensure_response( $proxy->proxy_request_to_wpcom_as_user( $request ) );
359    }
360
361    /**
362     * Gets the current Publicize connections for the site.
363     *
364     * GET `jetpack/v4/publicize/connections`
365     *
366     * @deprecated 0.61.1
367     *
368     * @param WP_REST_Request $request The request object, which includes the parameters.
369     */
370    public function get_publicize_connections( $request ) {
371
372        Publicize_Utils::endpoint_deprecated_warning(
373            __METHOD__,
374            'jetpack-14.4, jetpack-social-6.2.0',
375            'jetpack/v4/publicize/connections',
376            'wpcom/v2/publicize/connections?test_connections=1'
377        );
378
379        if ( $request->get_param( 'test_connections' ) ) {
380
381            $proxy = new Proxy_Requests( 'publicize/connections' );
382
383            return rest_ensure_response( $proxy->proxy_request_to_wpcom_as_user( $request ) );
384        }
385
386        return rest_ensure_response( Connections::get_all_for_user() );
387    }
388
389    /**
390     * Create a publicize connection
391     *
392     * @deprecated 0.61.1
393     *
394     * @param WP_REST_Request $request The request object, which includes the parameters.
395     * @return WP_REST_Response|WP_Error True if the request was successful, or a WP_Error otherwise.
396     */
397    public function create_publicize_connection( $request ) {
398
399        Publicize_Utils::endpoint_deprecated_warning(
400            __METHOD__,
401            'jetpack-14.4, jetpack-social-6.2.0',
402            'jetpack/v4/social/connections',
403            'wpcom/v2/publicize/connections'
404        );
405
406        $proxy = new Proxy_Requests( 'publicize/connections' );
407
408        return rest_ensure_response(
409            $proxy->proxy_request_to_wpcom_as_user( $request, '', array( 'timeout' => 120 ) )
410        );
411    }
412
413    /**
414     * Calls the WPCOM endpoint to update the publicize connection.
415     *
416     * POST jetpack/v4/social/connections/{connection_id}
417     *
418     * @deprecated 0.61.1
419     *
420     * @param WP_REST_Request $request The request object, which includes the parameters.
421     */
422    public function update_publicize_connection( $request ) {
423
424        Publicize_Utils::endpoint_deprecated_warning(
425            __METHOD__,
426            'jetpack-14.4, jetpack-social-6.2.0',
427            'jetpack/v4/social/connections/:connection_id',
428            'wpcom/v2/publicize/connections/:connection_id'
429        );
430
431        $proxy = new Proxy_Requests( 'publicize/connections' );
432
433        $path = $request->get_param( 'connection_id' );
434
435        return rest_ensure_response(
436            $proxy->proxy_request_to_wpcom_as_user( $request, $path, array( 'timeout' => 120 ) )
437        );
438    }
439
440    /**
441     * Calls the WPCOM endpoint to delete the publicize connection.
442     *
443     * DELETE jetpack/v4/social/connections/{connection_id}
444     *
445     * @deprecated 0.61.1
446     *
447     * @param WP_REST_Request $request The request object, which includes the parameters.
448     */
449    public function delete_publicize_connection( $request ) {
450
451        Publicize_Utils::endpoint_deprecated_warning(
452            __METHOD__,
453            'jetpack-14.4, jetpack-social-6.2.0',
454            'jetpack/v4/social/connections/:connection_id',
455            'wpcom/v2/publicize/connections/:connection_id'
456        );
457
458        $proxy = new Proxy_Requests( 'publicize/connections' );
459
460        $path = $request->get_param( 'connection_id' );
461
462        return rest_ensure_response(
463            $proxy->proxy_request_to_wpcom_as_user( $request, $path, array( 'timeout' => 120 ) )
464        );
465    }
466
467    /**
468     * Gets information about the current social product plans.
469     *
470     * @deprecated 0.63.0 Swapped to using the /my-jetpack/v1/site/products endpoint instead.
471     *
472     * @return string|WP_Error A JSON object of the current social product being if the request was successful, or a WP_Error otherwise.
473     */
474    public static function get_social_product_info() {
475        Publicize_Utils::endpoint_deprecated_warning(
476            __METHOD__,
477            'jetpack-14.6, jetpack-social-6.4.0',
478            'jetpack/v4/social-product-info',
479            'my-jetpack/v1/site/products?products=social'
480        );
481
482        $request_url   = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
483        $wpcom_request = wp_remote_get( esc_url_raw( $request_url ) );
484        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
485
486        if ( 200 !== $response_code ) {
487            // Something went wrong so we'll just return the response without caching.
488            return new WP_Error(
489                'failed_to_fetch_data',
490                esc_html__( 'Unable to fetch the requested data.', 'jetpack-publicize-pkg' ),
491                array(
492                    'status'  => $response_code,
493                    'request' => $wpcom_request,
494                )
495            );
496        }
497
498        $products = json_decode( wp_remote_retrieve_body( $wpcom_request ) );
499        return array(
500            'v1' => $products->{self::JETPACK_SOCIAL_V1_YEARLY},
501        );
502    }
503
504    /**
505     * Calls the WPCOM endpoint to reshare the post.
506     *
507     * POST jetpack/v4/publicize/(?P<postId>\d+)
508     *
509     * @deprecated 0.61.2
510     *
511     * @param WP_REST_Request $request The request object, which includes the parameters.
512     */
513    public function share_post( $request ) {
514        $post_id = $request->get_param( 'postId' );
515
516        Publicize_Utils::endpoint_deprecated_warning(
517            __METHOD__,
518            'jetpack-14.4.1, jetpack-social-6.2.0',
519            'jetpack/v4/publicize/:postId',
520            'wpcom/v2/publicize/share-post/:postId'
521        );
522
523        $proxy = new Proxy_Requests( 'publicize/share-post' );
524
525        return rest_ensure_response(
526            $proxy->proxy_request_to_wpcom_as_user( $request, $post_id )
527        );
528    }
529
530    /**
531     * Forward remote response to client with error handling.
532     *
533     * @param array|WP_Error $response - Response from WPCOM.
534     */
535    public function make_proper_response( $response ) {
536        if ( is_wp_error( $response ) ) {
537            return $response;
538        }
539
540        $body        = json_decode( wp_remote_retrieve_body( $response ), true );
541        $status_code = wp_remote_retrieve_response_code( $response );
542
543        if ( 200 === $status_code ) {
544            return $body;
545        }
546
547        return new WP_Error(
548            isset( $body['error'] ) ? 'remote-error-' . $body['error'] : 'remote-error',
549            $body['message'] ?? 'unknown remote error',
550            array( 'status' => $status_code )
551        );
552    }
553
554    /**
555     * Get blog id
556     */
557    protected function get_blog_id() {
558        return $this->is_wpcom ? get_current_blog_id() : Jetpack_Options::get_option( 'id' );
559    }
560
561    /**
562     * Update the post with information about shares.
563     *
564     * @param WP_REST_Request $request Full details about the request.
565     */
566    public function update_post_shares( $request ) {
567
568        Publicize_Utils::endpoint_deprecated_warning(
569            __METHOD__,
570            'jetpack-14.6, jetpack-social-6.4.0',
571            'jetpack/v4/social/sync-shares/post/:id',
572            'wpcom/v2/publicize/share-status/sync'
573        );
574
575        $request_body = $request->get_json_params();
576
577        $post_id   = $request->get_param( 'id' );
578        $post_meta = $request_body['meta'];
579        $post      = get_post( $post_id );
580
581        if ( $post && 'publish' === $post->post_status && isset( $post_meta[ Share_Status::SHARES_META_KEY ] ) ) {
582            update_post_meta( $post_id, Share_Status::SHARES_META_KEY, $post_meta[ Share_Status::SHARES_META_KEY ] );
583            $urls = array();
584            foreach ( $post_meta[ Share_Status::SHARES_META_KEY ] as $share ) {
585                if ( isset( $share['status'] ) && 'success' === $share['status'] ) {
586                    $urls[] = array(
587                        'url'     => $share['message'],
588                        'service' => $share['service'],
589                    );
590                }
591            }
592            /**
593             * Fires after Publicize Shares post meta has been saved.
594             *
595             * @param array $urls {
596             *     An array of social media shares.
597             *     @type array $url URL to the social media post.
598             *     @type string $service Social media service shared to.
599             * }
600             */
601            do_action( 'jetpack_publicize_share_urls_saved', $urls );
602            return rest_ensure_response( new WP_REST_Response() );
603        }
604
605        return new WP_Error(
606            'rest_cannot_edit',
607            __( 'Failed to update the post meta', 'jetpack-publicize-pkg' ),
608            array( 'status' => 500 )
609        );
610    }
611
612    /**
613     * Gets the share status for a post.
614     *
615     * GET `jetpack/v4/social/share-status/<post_id>`
616     *
617     * @deprecated 0.63.0
618     *
619     * @param WP_REST_Request $request The request object.
620     */
621    public function get_post_share_status( WP_REST_Request $request ) {
622        $post_id = $request->get_param( 'post_id' );
623
624        Publicize_Utils::endpoint_deprecated_warning(
625            __METHOD__,
626            'jetpack-14.6, jetpack-social-6.4.0',
627            'jetpack/v4/social/share-status/:postId',
628            'wpcom/v2/publicize/share-status'
629        );
630
631        return rest_ensure_response( Share_Status::get_post_share_status( $post_id ) );
632    }
633}