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