Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.02% covered (warning)
86.02%
240 / 279
57.14% covered (warning)
57.14%
12 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
86.28% covered (warning)
86.28%
239 / 277
57.14% covered (warning)
57.14%
12 / 21
124.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 register_rest_routes
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 register_common_rest_routes
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
1
 register_jetpack_only_rest_routes
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
1
 register_wpcom_only_rest_routes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 require_admin_privilege_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 require_valid_blog_token_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_forbidden_error
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_search_plan
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update_settings
74.55% covered (warning)
74.55%
41 / 55
0.00% covered (danger)
0.00%
0 / 1
37.15
 validate_search_settings
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
31
 get_settings
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 is_reader_chat_setting_registered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_stats
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_search_results
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 activate_plan
92.50% covered (success)
92.50%
37 / 40
0.00% covered (danger)
0.00%
0 / 1
13.07
 deactivate_plan
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_local_stats
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 product_pricing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 make_proper_response
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
6.60
 get_blog_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * The Search Rest Controller class.
4 * Registers the REST routes for Search.
5 *
6 * @package automattic/jetpack-search
7 */
8
9namespace Automattic\Jetpack\Search;
10
11use Automattic\Jetpack\Connection\Client;
12use Automattic\Jetpack\Connection\Rest_Authentication;
13use Automattic\Jetpack\My_Jetpack\Products\Search as Search_Product;
14use Automattic\Jetpack\My_Jetpack\Products\Search_Stats as Search_Product_Stats;
15use Jetpack_Options;
16use WP_Error;
17use WP_REST_Request;
18use WP_REST_Server;
19
20if ( ! defined( 'ABSPATH' ) ) {
21    exit( 0 );
22}
23
24/**
25 * Registers the REST routes for Search.
26 */
27class REST_Controller {
28    /**
29     * Namespace for the REST API.
30     *
31     * This is overriden with value `wpcom-orgin/jetpack/v4` for WPCOM.
32     *
33     * @var string
34     */
35    public static $namespace = 'jetpack/v4';
36    /**
37     * Whether it's run on WPCOM.
38     *
39     * @var bool
40     */
41    protected $is_wpcom;
42
43    /**
44     * Module Control object.
45     *
46     * @var Module_Control
47     */
48    protected $search_module;
49
50    /**
51     * Plan object.
52     *
53     * @var Plan
54     */
55    public $plan;
56
57    /**
58     * Constructor
59     *
60     * @param bool                $is_wpcom - Whether it's run on WPCOM.
61     * @param Module_Control|null $module_control - Module_Control object if any.
62     * @param Plan|null           $plan - Plan object if any.
63     */
64    public function __construct( $is_wpcom = false, $module_control = null, $plan = null ) {
65        $this->is_wpcom      = $is_wpcom;
66        $this->search_module = $module_control === null ? new Module_Control() : $module_control;
67        $this->plan          = $plan === null ? new Plan() : $plan;
68    }
69
70    /**
71     * Registers the REST routes for Search.
72     *
73     * @access public
74     * @static
75     */
76    public function register_rest_routes() {
77        $this->register_common_rest_routes();
78        if ( ! Helper::is_wpcom() ) {
79            $this->register_jetpack_only_rest_routes();
80        } else {
81            $this->register_wpcom_only_rest_routes();
82        }
83    }
84
85    /**
86     * Routes both existing in Jetpack and WPCOM simple sites.
87     */
88    protected function register_common_rest_routes() {
89        register_rest_route(
90            static::$namespace,
91            '/search/plan',
92            array(
93                'methods'             => WP_REST_Server::READABLE,
94                'callback'            => array( $this, 'get_search_plan' ),
95                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
96            )
97        );
98        register_rest_route(
99            static::$namespace,
100            '/search/settings',
101            array(
102                'methods'             => WP_REST_Server::EDITABLE,
103                'callback'            => array( $this, 'update_settings' ),
104                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
105            )
106        );
107        register_rest_route(
108            static::$namespace,
109            '/search/settings',
110            array(
111                'methods'             => WP_REST_Server::READABLE,
112                'callback'            => array( $this, 'get_settings' ),
113                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
114            )
115        );
116        register_rest_route(
117            static::$namespace,
118            '/search/stats',
119            array(
120                'methods'             => WP_REST_Server::READABLE,
121                'callback'            => array( $this, 'get_stats' ),
122                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
123            )
124        );
125        register_rest_route(
126            static::$namespace,
127            '/search/pricing',
128            array(
129                'methods'             => WP_REST_Server::READABLE,
130                'callback'            => array( $this, 'product_pricing' ),
131                'permission_callback' => 'is_user_logged_in',
132            )
133        );
134    }
135
136    /**
137     * Routes only existing in Jetpack.
138     */
139    protected function register_jetpack_only_rest_routes() {
140        register_rest_route(
141            static::$namespace,
142            '/search/plan/activate',
143            array(
144                'methods'             => WP_REST_Server::EDITABLE,
145                'callback'            => array( $this, 'activate_plan' ),
146                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
147            )
148        );
149        register_rest_route(
150            static::$namespace,
151            '/search/plan/deactivate',
152            array(
153                'methods'             => WP_REST_Server::EDITABLE,
154                'callback'            => array( $this, 'deactivate_plan' ),
155                'permission_callback' => array( $this, 'require_admin_privilege_callback' ),
156            )
157        );
158        register_rest_route(
159            static::$namespace,
160            '/search',
161            array(
162                'methods'             => WP_REST_Server::READABLE,
163                'callback'            => array( $this, 'get_search_results' ),
164                'permission_callback' => 'is_user_logged_in',
165            )
166        );
167        register_rest_route(
168            static::$namespace,
169            '/search/local-stats',
170            array(
171                'methods'             => WP_REST_Server::READABLE,
172                'callback'            => array( $this, 'get_local_stats' ),
173                'permission_callback' => array( $this, 'require_valid_blog_token_callback' ),
174            )
175        );
176    }
177
178    /**
179     * Routes only existing in WPCOM.
180     *
181     * We currently don't have any.
182     */
183    protected function register_wpcom_only_rest_routes() {
184        return true;
185    }
186
187    /**
188     * Only administrators can access the API.
189     *
190     * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
191     */
192    public function require_admin_privilege_callback() {
193        if ( current_user_can( 'manage_options' ) ) {
194            return true;
195        }
196
197        return $this->get_forbidden_error();
198    }
199
200    /**
201     * The corresponding endpoints can only be accessible from WPCOM.
202     *
203     * @access public
204     * @static
205     *
206     * @return bool|WP_Error True if a blog token was used to sign the request, WP_Error otherwise.
207     */
208    public function require_valid_blog_token_callback() {
209        if ( Rest_Authentication::is_signed_with_blog_token() ) {
210            return true;
211        }
212
213        return $this->get_forbidden_error();
214    }
215
216    /**
217     * Return a WP_Error object with a forbidden error.
218     */
219    protected function get_forbidden_error() {
220        $error_msg = esc_html__(
221            'You are not allowed to perform this action.',
222            'jetpack-search-pkg'
223        );
224
225        return new WP_Error( 'rest_forbidden', $error_msg, array( 'status' => rest_authorization_required_code() ) );
226    }
227
228    /**
229     * Proxy the request to WPCOM and return the response.
230     *
231     * GET `jetpack/v4/search/plan`
232     */
233    public function get_search_plan() {
234        $response = ( new Plan() )->get_plan_info_from_wpcom();
235        return $this->make_proper_response( $response );
236    }
237
238    /**
239     * POST `jetpack/v4/search/settings`
240     *
241     * @param WP_REST_Request $request - REST request.
242     */
243    public function update_settings( $request ) {
244        $request_body = $request->get_json_params();
245        if ( ! is_array( $request_body ) ) {
246            $request_body = array();
247        }
248
249        $module_active                 = isset( $request_body['module_active'] ) ? (bool) $request_body['module_active'] : null;
250        $instant_search_enabled        = isset( $request_body['instant_search_enabled'] ) ? (bool) $request_body['instant_search_enabled'] : null;
251        $swap_classic_to_inline_search = isset( $request_body['swap_classic_to_inline_search'] ) ? (bool) $request_body['swap_classic_to_inline_search'] : null;
252        $experience                    = isset( $request_body['experience'] ) && is_string( $request_body['experience'] )
253            ? sanitize_text_field( $request_body['experience'] )
254            : null;
255        $reader_chat                   = array_key_exists( 'reader_chat', $request_body ) ? (bool) $request_body['reader_chat'] : null;
256        $ai_answers_enabled            = isset( $request_body['ai_answers_enabled'] ) ? (bool) $request_body['ai_answers_enabled'] : null;
257
258        $search_suggestions_enabled = isset( $request_body['search_suggestions_enabled'] ) ? (bool) $request_body['search_suggestions_enabled'] : null;
259
260        $override_woocommerce_search_template = isset( $request_body['override_woocommerce_search_template'] ) ? (bool) $request_body['override_woocommerce_search_template'] : null;
261
262        $error = $this->validate_search_settings( $module_active, $instant_search_enabled, $swap_classic_to_inline_search, $experience, $reader_chat, $ai_answers_enabled, $search_suggestions_enabled, $override_woocommerce_search_template );
263
264        if ( is_wp_error( $error ) ) {
265            return $error;
266        }
267
268        // If an experience value was provided, delegate to Module_Control::update_experience(),
269        // which encapsulates the storage shape (off → module deactivate, inline → delete option,
270        // embedded/overlay → write affirmative value) and keeps the legacy booleans in lockstep.
271        if ( $experience !== null ) {
272            $result = $this->search_module->update_experience( $experience );
273            if ( is_wp_error( $result ) ) {
274                return $result;
275            }
276            return rest_ensure_response( $this->get_settings() );
277        }
278
279        // Enabling instant search should enable the module too.
280        if ( true === $instant_search_enabled && true !== $module_active ) {
281            $module_active = true;
282        }
283
284        $errors = array();
285        if ( $module_active !== null ) {
286            $module_active_updated = $this->search_module->update_status( $module_active );
287            if ( is_wp_error( $module_active_updated ) ) {
288                $errors['module_active'] = $module_active_updated;
289            }
290        }
291
292        if ( $instant_search_enabled !== null ) {
293            $instant_search_enabled_updated = $this->search_module->update_instant_search_status( $instant_search_enabled );
294            if ( is_wp_error( $instant_search_enabled_updated ) ) {
295                $errors['instant_search_enabled'] = $instant_search_enabled_updated;
296            }
297        }
298
299        if ( $swap_classic_to_inline_search !== null ) {
300            $this->search_module->update_swap_classic_to_inline_search( $swap_classic_to_inline_search );
301        }
302
303        if ( $reader_chat !== null ) {
304            update_option( 'reader_chat', $reader_chat );
305        }
306
307        if ( $ai_answers_enabled !== null ) {
308            update_option( 'jetpack_search_ai_answers_enabled', $ai_answers_enabled );
309        }
310        if ( $search_suggestions_enabled !== null ) {
311            update_option( 'jetpack_search_suggestions_enabled', $search_suggestions_enabled );
312        }
313        if ( $override_woocommerce_search_template !== null ) {
314            update_option( 'jetpack_search_override_woocommerce_search_template', $override_woocommerce_search_template );
315        }
316
317        if ( ! empty( $errors ) ) {
318            return new WP_Error(
319                'some_updated',
320                sprintf(
321                    /* translators: %s are the setting name that not updated. */
322                    __( 'Some settings ( %s ) not updated.', 'jetpack-search-pkg' ),
323                    implode(
324                        ',',
325                        array_keys( $errors )
326                    )
327                ),
328                array( 'status' => 400 )
329            );
330        }
331
332        return rest_ensure_response( $this->get_settings() );
333    }
334
335    /**
336     * Validate $module_active and $instant_search_enabled. Returns an WP_Error instance if invalid.
337     *
338     * @param boolean     $module_active - Module status.
339     * @param boolean     $instant_search_enabled - Instant Search status.
340     * @param boolean     $swap_classic_to_inline_search - New inline search status.
341     * @param string|null $experience - Experience value.
342     * @param bool|null   $reader_chat - Reader Chat status.
343     * @param bool|null   $ai_answers_enabled - Whether Jetpack Search AI answers is enabled.
344     * @param bool|null   $search_suggestions_enabled - New search suggestions status.
345     * @param bool|null   $override_woocommerce_search_template - New WooCommerce search-template override status.
346     */
347    protected function validate_search_settings( $module_active, $instant_search_enabled, $swap_classic_to_inline_search, $experience = null, $reader_chat = null, $ai_answers_enabled = null, $search_suggestions_enabled = null, $override_woocommerce_search_template = null ) {
348        if ( $reader_chat !== null && ! $this->is_reader_chat_setting_registered() ) {
349            return new WP_Error(
350                'rest_invalid_arguments',
351                esc_html__( 'The arguments passed in are invalid.', 'jetpack-search-pkg' ),
352                array( 'status' => 400 )
353            );
354        }
355
356        // `experience` is the canonical source of truth and writes the legacy booleans in lockstep.
357        // Reject requests that mix it with any other settings field so callers don't silently
358        // lose those fields — the `experience` branch in update_settings() early-returns and
359        // would otherwise drop them.
360        if ( $experience !== null ) {
361            if ( $module_active !== null || $instant_search_enabled !== null || $swap_classic_to_inline_search !== null || $reader_chat !== null || $ai_answers_enabled !== null || $search_suggestions_enabled !== null || $override_woocommerce_search_template !== null ) {
362                return new WP_Error(
363                    'rest_invalid_arguments',
364                    esc_html__( 'The `experience` field cannot be combined with `module_active`, `instant_search_enabled`, `swap_classic_to_inline_search`, `reader_chat`, `ai_answers_enabled`, `search_suggestions_enabled`, or `override_woocommerce_search_template`.', 'jetpack-search-pkg' ),
365                    array( 'status' => 400 )
366                );
367            }
368            return true;
369        }
370        if (
371            $module_active === null &&
372            $instant_search_enabled === null &&
373            ( $swap_classic_to_inline_search !== null || $reader_chat !== null )
374        ) {
375            // Allow updating auxiliary settings without updating/validating the module settings.
376            return true;
377        }
378        if ( $module_active === null && $instant_search_enabled === null && $swap_classic_to_inline_search === null && $ai_answers_enabled !== null ) {
379            // allow updating 'ai_answers_enabled' without updating/validating other settings.
380            return true;
381        }
382        if ( $module_active === null && $instant_search_enabled === null && $swap_classic_to_inline_search === null && $search_suggestions_enabled !== null ) {
383            // allow updating 'search_suggestions_enabled' without updating/validating other settings.
384            return true;
385        }
386        if ( $module_active === null && $instant_search_enabled === null && $swap_classic_to_inline_search === null && $override_woocommerce_search_template !== null ) {
387            // allow updating 'override_woocommerce_search_template' without updating/validating other settings.
388            return true;
389        }
390        if ( ( true === $instant_search_enabled && false === $module_active ) || ( $module_active === null && $instant_search_enabled === null ) ) {
391            return new WP_Error(
392                'rest_invalid_arguments',
393                esc_html__( 'The arguments passed in are invalid.', 'jetpack-search-pkg' ),
394                array( 'status' => 400 )
395            );
396        }
397        return true;
398    }
399
400        /**
401         *     GET `jetpack/v4/search/settings`
402         */
403    public function get_settings() {
404        $settings = array(
405            'module_active'                        => $this->search_module->is_active(),
406            'instant_search_enabled'               => $this->search_module->is_instant_search_enabled(),
407            'swap_classic_to_inline_search'        => $this->search_module->is_swap_classic_to_inline_search(),
408            'experience'                           => $this->search_module->get_experience(),
409            'ai_answers_enabled'                   => AI_Answers::is_enabled(),
410            'search_suggestions_enabled'           => (bool) get_option( 'jetpack_search_suggestions_enabled', false ),
411            'override_woocommerce_search_template' => Search_Blocks::woocommerce_search_template_override_enabled(),
412        );
413
414        if ( $this->is_reader_chat_setting_registered() ) {
415            $settings['reader_chat'] = (bool) get_option( 'reader_chat', false );
416        }
417
418        return rest_ensure_response( $settings );
419    }
420
421    /**
422     * Check whether Reader Chat is available through REST settings in this request.
423     *
424     * Reader Chat registers `reader_chat` only for proxied rollout contexts, so the
425     * Search dashboard should expose the toggle only when that setting exists.
426     *
427     * @return bool True when reader_chat is registered.
428     */
429    protected function is_reader_chat_setting_registered() {
430        return array_key_exists( 'reader_chat', get_registered_settings() );
431    }
432
433    /**
434     * Proxy the request to WPCOM and return the response.
435     *
436     * GET `jetpack/v4/search/stats`
437     */
438    public function get_stats() {
439        $response = ( new Stats() )->get_stats_from_wpcom();
440        return $this->make_proper_response( $response );
441    }
442
443    /**
444     * Search Endpoint for private sites.
445     *
446     * GET `jetpack/v4/search`
447     *
448     * @param WP_REST_Request $request - REST request.
449     */
450    public function get_search_results( $request ) {
451        $blog_id  = $this->get_blog_id();
452        $path     = sprintf( '/sites/%d/search', absint( $blog_id ) );
453        $path     = add_query_arg(
454            $request->get_query_params(),
455            sprintf( '/sites/%d/search', absint( $blog_id ) )
456        );
457        $response = Client::wpcom_json_api_request_as_blog( $path, '1.3', array(), null, 'rest' );
458        return rest_ensure_response( $this->make_proper_response( $response ) );
459    }
460
461    /**
462     * Activate plan: activate the search module, instant search and do initial configuration.
463     * Typically called from WPCOM.
464     *
465     * POST `jetpack/v4/search/plan/activate`
466     *
467     * @param WP_REST_Request $request - REST request.
468     */
469    public function activate_plan( $request ) {
470        $default_options = array(
471            'search_plan_info'      => null,
472            'enable_search'         => true,
473            'enable_instant_search' => true,
474            'search_experience'     => null,
475            'auto_config_search'    => true,
476        );
477        $payload         = $request->get_json_params();
478        $payload         = wp_parse_args( $payload, $default_options );
479
480        // Update plan data, plan info is in the request body.
481        // We do this to avoid another call to WPCOM and reduce latency.
482        if ( $payload['search_plan_info'] === null || ! $this->plan->set_plan_options( $payload['search_plan_info'] ) ) {
483            $this->plan->get_plan_info_from_wpcom();
484        }
485
486        // Enable search module by default, unless `enable_search` is explicitly set to boolean `false`.
487        if ( false !== $payload['enable_search'] ) {
488            $ret = $this->search_module->activate();
489            if ( is_wp_error( $ret ) ) {
490                return $ret;
491            }
492        }
493
494        if ( $payload['search_experience'] !== null ) {
495            // Canonical path. Restrict to activate-able experiences — `off`
496            // belongs on `/plan/deactivate`, and a non-string payload would
497            // blow up `update_experience(string $experience)`.
498            $valid_experiences = array(
499                Module_Control::EXPERIENCE_OVERLAY,
500                Module_Control::EXPERIENCE_INLINE,
501                Module_Control::EXPERIENCE_EMBEDDED,
502            );
503            if ( ! is_string( $payload['search_experience'] )
504                || ! in_array( $payload['search_experience'], $valid_experiences, true )
505            ) {
506                return new WP_Error(
507                    'invalid_experience',
508                    __( 'Invalid experience value.', 'jetpack-search-pkg' ),
509                    array( 'status' => 400 )
510                );
511            }
512            $ret = $this->search_module->update_experience( sanitize_text_field( $payload['search_experience'] ) );
513            if ( is_wp_error( $ret ) ) {
514                return $ret;
515            }
516        }
517
518        if ( $payload['search_experience'] === null && false !== $payload['enable_instant_search'] ) {
519            // Legacy path: old WPCOM callers send `enable_instant_search`
520            // instead of `search_experience`. Gated on the canonical value
521            // being absent so it doesn't overwrite a non-overlay experience
522            // the caller just set.
523            // Error handling intentionally skipped — this is the legacy fallback.
524            $ret = $this->search_module->enable_instant_search();
525        }
526
527        // `auto_config_search` wires up Overlay sidebar widgets — only meaningful
528        // when Overlay is the resulting experience. For Inline / Embedded, the
529        // caller would otherwise get widget side effects they didn't ask for.
530        if ( false !== $payload['auto_config_search'] && $this->search_module->is_instant_search_enabled() ) {
531            Instant_Search::instance( $this->get_blog_id() )->auto_config_search();
532        }
533
534        return rest_ensure_response(
535            array(
536                'code' => 'success',
537            )
538        );
539    }
540
541    /**
542     * Deactivate plan: turn off search module and instant search.
543     * If the plan is still valid then the function would simply deactivate the search module.
544     * Typically called from WPCOM.
545     *
546     * POST `jetpack/v4/search/plan/deactivate`
547     */
548    public function deactivate_plan() {
549        // Instant Search would be disabled along with search module.
550        $this->search_module->deactivate();
551        return rest_ensure_response(
552            array(
553                'code' => 'success',
554            )
555        );
556    }
557
558    /**
559     * Return post type breakdown for the site.
560     */
561    public function get_local_stats() {
562        return array(
563            'post_count'          => Search_Product_Stats::estimate_count(),
564            'post_type_breakdown' => Search_Product_Stats::get_post_type_breakdown(),
565        );
566    }
567
568    /**
569     * Pricing for record count of the site
570     */
571    public function product_pricing() {
572        $tier_pricing = Search_Product::get_pricing_for_ui();
573        // we can force the plugin to use the new pricing by appending `new_pricing_202208=1` to URL.
574        if ( Helper::is_forced_new_pricing_202208() ) {
575            $tier_pricing['pricing_version'] = Plan::JETPACK_SEARCH_NEW_PRICING_VERSION;
576        }
577        return rest_ensure_response( $tier_pricing );
578    }
579
580    /**
581     * Forward remote response to client with error handling.
582     *
583     * @param array|WP_Error $response - Response from WPCOM.
584     */
585    protected function make_proper_response( $response ) {
586        if ( is_wp_error( $response ) ) {
587            return $response;
588        }
589
590        $body        = json_decode( wp_remote_retrieve_body( $response ), true );
591        $status_code = wp_remote_retrieve_response_code( $response );
592
593        if ( 200 === $status_code ) {
594            return $body;
595        }
596
597        return new WP_Error(
598            isset( $body['error'] ) ? 'remote-error-' . $body['error'] : 'remote-error',
599            $body['message'] ?? 'unknown remote error',
600            array( 'status' => $status_code )
601        );
602    }
603
604    /**
605     * Get blog id
606     */
607    protected function get_blog_id() {
608        return $this->is_wpcom ? get_current_blog_id() : Jetpack_Options::get_option( 'id' );
609    }
610}