Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.90% covered (danger)
26.90%
230 / 855
3.45% covered (danger)
3.45%
1 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 4
jetpack_do_after_gravatar_hovercards_activation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_do_after_gravatar_hovercards_deactivation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_do_after_markdown_activation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Jetpack_Core_API_Module_Toggle_Endpoint
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 4
306
0.00% covered (danger)
0.00%
0 / 1
 process
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 activate_module
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 deactivate_module
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
56
 can_request
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Jetpack_Core_API_Module_List_Endpoint
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 4
380
0.00% covered (danger)
0.00%
0 / 1
 process
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_modules
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 activate_modules
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
110
 can_request
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
Jetpack_Core_API_Data
48.42% covered (danger)
48.42%
230 / 475
16.67% covered (danger)
16.67%
1 / 6
6800.21
0.00% covered (danger)
0.00%
0 / 1
 process
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get_module
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
 get_all_options
84.09% covered (warning)
84.09%
37 / 44
0.00% covered (danger)
0.00%
0 / 1
22.78
 update_data
45.55% covered (danger)
45.55%
174 / 382
0.00% covered (danger)
0.00%
0 / 1
4345.58
 process_onboarding
n/a
0 / 0
n/a
0 / 0
1
 handle_business_address
n/a
0 / 0
n/a
0 / 0
12
 has_business_address_widget
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 can_request
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
Jetpack_Core_API_Module_Data_Endpoint
0.00% covered (danger)
0.00%
0 / 241
0.00% covered (danger)
0.00%
0 / 12
3906
0.00% covered (danger)
0.00%
0 / 1
 process
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 key_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 get_protect_data
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_akismet_data
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 check_akismet_key
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 akismet_class_exists
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 akismet_is_active_and_registered
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 get_stats_data
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
42
 get_monitor_data
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 get_verification_tools_data
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
210
 get_vaultpress_data
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
56
 can_request
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Tools to interact with Jetpack modules via API requests.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\REST_Connector;
9use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
10use Automattic\Jetpack\Stats\WPCOM_Stats;
11use Automattic\Jetpack\Stats_Admin\Main as Stats_Admin_Main;
12use Automattic\Jetpack\Status;
13use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
14use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * This is the base class for every Core API endpoint Jetpack uses.
22 */
23class Jetpack_Core_API_Module_Toggle_Endpoint extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
24
25    /**
26     * Check if the module requires the site to be publicly accessible from WPCOM.
27     * If the site meets this requirement, the module is activated. Otherwise an error is returned.
28     *
29     * @since 4.3.0
30     *
31     * @param WP_REST_Request $request {
32     *     Array of parameters received by request.
33     *
34     *     @type string $slug Module slug.
35     *     @type bool   $active should module be activated.
36     * }
37     *
38     * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
39     */
40    public function process( $request ) {
41        if ( $request['active'] ) {
42            return $this->activate_module( $request );
43        } else {
44            return $this->deactivate_module( $request );
45        }
46    }
47
48    /**
49     * If it's a valid Jetpack module, activate it.
50     *
51     * @since 4.3.0
52     *
53     * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
54     *                                        and a string when called from Jetpack_Core_API_Data->update_data.
55     * {
56     *     Array of parameters received by request.
57     *
58     *     @type string $slug Module slug.
59     * }
60     *
61     * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
62     */
63    public function activate_module( $request ) {
64        $module_slug = '';
65
66        if (
67            (
68                is_array( $request )
69                || is_object( $request )
70            )
71            && isset( $request['slug'] )
72        ) {
73            $module_slug = $request['slug'];
74        } else {
75            $module_slug = $request;
76        }
77
78        if ( ! Jetpack::is_module( $module_slug ) ) {
79            return new WP_Error(
80                'not_found',
81                esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
82                array( 'status' => 404 )
83            );
84        }
85
86        if ( ! Jetpack_Plan::supports( $module_slug ) ) {
87            return new WP_Error(
88                'not_supported',
89                esc_html__( 'The requested Jetpack module is not supported by your plan.', 'jetpack' ),
90                array( 'status' => 424 )
91            );
92        }
93
94        if ( Jetpack::activate_module( $module_slug, false, false ) ) {
95            return rest_ensure_response(
96                array(
97                    'code'    => 'success',
98                    'message' => esc_html__( 'The requested Jetpack module was activated.', 'jetpack' ),
99                )
100            );
101        }
102
103        return new WP_Error(
104            'activation_failed',
105            esc_html__( 'The requested Jetpack module could not be activated.', 'jetpack' ),
106            array( 'status' => 424 )
107        );
108    }
109
110    /**
111     * If it's a valid Jetpack module, deactivate it.
112     *
113     * @since 4.3.0
114     *
115     * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
116     *                                        and a string when called from Jetpack_Core_API_Data->update_data.
117     * {
118     *     Array of parameters received by request.
119     *
120     *     @type string $slug Module slug.
121     * }
122     *
123     * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
124     */
125    public function deactivate_module( $request ) {
126        $module_slug = '';
127
128        if (
129            (
130                is_array( $request )
131                || is_object( $request )
132            )
133            && isset( $request['slug'] )
134        ) {
135            $module_slug = $request['slug'];
136        } else {
137            $module_slug = $request;
138        }
139
140        if ( ! Jetpack::is_module( $module_slug ) ) {
141            return new WP_Error(
142                'not_found',
143                esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
144                array( 'status' => 404 )
145            );
146        }
147
148        if ( ! Jetpack::is_module_active( $module_slug ) ) {
149            return new WP_Error(
150                'already_inactive',
151                esc_html__( 'The requested Jetpack module was already inactive.', 'jetpack' ),
152                array( 'status' => 409 )
153            );
154        }
155
156        if ( Jetpack::deactivate_module( $module_slug ) ) {
157            return rest_ensure_response(
158                array(
159                    'code'    => 'success',
160                    'message' => esc_html__( 'The requested Jetpack module was deactivated.', 'jetpack' ),
161                )
162            );
163        }
164        return new WP_Error(
165            'deactivation_failed',
166            esc_html__( 'The requested Jetpack module could not be deactivated.', 'jetpack' ),
167            array( 'status' => 400 )
168        );
169    }
170
171    /**
172     * Check that the current user has permissions to manage Jetpack modules.
173     *
174     * @since 4.3.0
175     *
176     * @return bool
177     */
178    public function can_request() {
179        return current_user_can( 'jetpack_manage_modules' );
180    }
181}
182
183/**
184 * Interact with multiple modules at once (list or activate).
185 *
186 * // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
187 */
188class Jetpack_Core_API_Module_List_Endpoint {
189    // phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound
190
191    /**
192     * A WordPress REST API callback method that accepts a request object and decides what to do with it.
193     *
194     * @param WP_REST_Request $request The request sent to the WP REST API.
195     *
196     * @since 4.3.0
197     *
198     * @return bool|Array|WP_Error a resulting value or object, or an error.
199     */
200    public function process( $request ) {
201        if ( 'GET' === $request->get_method() ) {
202            return $this->get_modules();
203        } else {
204            return static::activate_modules( $request );
205        }
206    }
207
208    /**
209     * Get a list of all Jetpack modules and their information.
210     *
211     * @since 4.3.0
212     *
213     * @return array Array of Jetpack modules.
214     */
215    public function get_modules() {
216        require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';
217
218        $modules = Jetpack_Admin::init()->get_modules();
219        foreach ( $modules as $slug => $properties ) {
220            $modules[ $slug ]['options'] =
221                Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $slug );
222            if (
223                isset( $modules[ $slug ]['requires_connection'] )
224                && $modules[ $slug ]['requires_connection']
225                && ( new Status() )->is_offline_mode()
226            ) {
227                $modules[ $slug ]['activated'] = false;
228            }
229        }
230
231        $modules = Jetpack::get_translated_modules( $modules );
232
233        return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $modules );
234    }
235
236    /**
237     * Activate a list of valid Jetpack modules.
238     *
239     * @since 4.3.0
240     *
241     * @param WP_REST_Request $request {
242     *     Array of parameters received by request.
243     *
244     *     @type string $slug Module slug.
245     * }
246     *
247     * @return bool|WP_Error True if modules were activated. Otherwise, a WP_Error instance with the corresponding error.
248     */
249    public static function activate_modules( $request ) {
250
251        if (
252            ! isset( $request['modules'] )
253            || ! is_array( $request['modules'] )
254        ) {
255            return new WP_Error(
256                'not_found',
257                esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
258                array( 'status' => 404 )
259            );
260        }
261
262        $activated = array();
263        $failed    = array();
264
265        foreach ( $request['modules'] as $module ) {
266            if ( Jetpack::activate_module( $module, false, false ) ) {
267                $activated[] = $module;
268            } else {
269                $failed[] = $module;
270            }
271        }
272
273        if ( empty( $failed ) ) {
274            return rest_ensure_response(
275                array(
276                    'code'    => 'success',
277                    'message' => esc_html__( 'All modules activated.', 'jetpack' ),
278                )
279            );
280        }
281
282        $error = '';
283
284        $activated_count = count( $activated );
285        if ( $activated_count > 0 ) {
286            $activated_last = array_pop( $activated );
287            $activated_text = $activated_count > 1 ? sprintf(
288                /* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
289                __( '%1$s and %2$s', 'jetpack' ),
290                implode( ', ', $activated ),
291                $activated_last
292            ) : $activated_last;
293
294            $error = sprintf(
295                /* Translators: the variable is a module name. */
296                _n( 'The module %s was activated.', 'The modules %s were activated.', $activated_count, 'jetpack' ),
297                $activated_text
298            ) . ' ';
299        }
300
301        $failed_count = count( $failed );
302        if ( count( $failed ) > 0 ) {
303            $failed_last = array_pop( $failed );
304            $failed_text = $failed_count > 1 ? sprintf(
305                /* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
306                __( '%1$s and %2$s', 'jetpack' ),
307                implode( ', ', $failed ),
308                $failed_last
309            ) : $failed_last;
310
311            $error = sprintf(
312                /* Translators: the variable is a module name. */
313                _n( 'The module %s failed to be activated.', 'The modules %s failed to be activated.', $failed_count, 'jetpack' ),
314                $failed_text
315            ) . ' ';
316        }
317
318        return new WP_Error(
319            'activation_failed',
320            esc_html( $error ),
321            array( 'status' => 424 )
322        );
323    }
324
325    /**
326     * A WordPress REST API permission callback method that accepts a request object and decides
327     * if the current user has enough privileges to act.
328     *
329     * @since 4.3.0
330     *
331     * @param WP_REST_Request $request The request sent to the WP REST API.
332     *
333     * @return bool does the current user have enough privilege.
334     */
335    public function can_request( $request ) {
336        if ( 'GET' === $request->get_method() ) {
337            return current_user_can( 'jetpack_admin_page' );
338        } else {
339            return current_user_can( 'jetpack_manage_modules' );
340        }
341    }
342}
343
344/**
345 * Class that manages updating of Jetpack module options and general Jetpack settings or retrieving module data.
346 * If no module is specified, all module settings are retrieved/updated.
347 *
348 * @since 4.3.0
349 * @since 4.4.0 Renamed Jetpack_Core_API_Module_Endpoint from to Jetpack_Core_API_Data.
350 *
351 * @author Automattic
352 *
353 * // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
354 */
355class Jetpack_Core_API_Data extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
356    // phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound
357
358    /**
359     * Process request by returning the module or updating it.
360     * If no module is specified, settings for all modules are assumed.
361     *
362     * @since 4.3.0
363     *
364     * @param WP_REST_Request $request WP API request.
365     *
366     * @return bool|mixed|void|WP_Error
367     */
368    public function process( $request ) {
369        if ( 'GET' === $request->get_method() ) {
370            if ( isset( $request['slug'] ) ) {
371                return $this->get_module( $request );
372            }
373
374            return $this->get_all_options();
375        } else {
376            return $this->update_data( $request );
377        }
378    }
379
380    /**
381     * Get information about a specific and valid Jetpack module.
382     *
383     * @since 4.3.0
384     *
385     * @param WP_REST_Request $request {
386     *     Array of parameters received by request.
387     *
388     *     @type string $slug Module slug.
389     * }
390     *
391     * @return mixed|void|WP_Error
392     */
393    public function get_module( $request ) {
394        if ( Jetpack::is_module( $request['slug'] ) ) {
395
396            $module = Jetpack::get_module( $request['slug'] );
397
398            $module['options'] = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $request['slug'] );
399
400            if (
401                isset( $module['requires_connection'] )
402                && $module['requires_connection']
403                && ( new Status() )->is_offline_mode()
404            ) {
405                $module['activated'] = false;
406            }
407
408            $i18n = jetpack_get_module_i18n( $request['slug'] );
409            if ( $i18n ) {
410                if ( isset( $module['name'] ) ) {
411                    $module['name'] = $i18n['name'];
412                }
413                if ( isset( $module['description'] ) ) {
414                    $module['description']       = $i18n['description'];
415                    $module['short_description'] = $i18n['description'];
416                }
417            }
418            if ( isset( $module['module_tags'] ) ) {
419                $module['module_tags'] = array_map( 'jetpack_get_module_i18n_tag', $module['module_tags'] );
420            }
421
422            return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $module );
423        }
424
425        return new WP_Error(
426            'not_found',
427            esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
428            array( 'status' => 404 )
429        );
430    }
431
432    /**
433     * Get information about all Jetpack module options and settings.
434     *
435     * @since 4.6.0
436     *
437     * @return WP_REST_Response $response
438     */
439    public function get_all_options() {
440        $response = array();
441
442        $modules = Jetpack::get_available_modules();
443        if ( is_array( $modules ) && ! empty( $modules ) ) {
444            foreach ( $modules as $module ) {
445                // Add all module options.
446                $options = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $module );
447                foreach ( $options as $option_name => $option ) {
448                    $response[ $option_name ] = $option['current_value'];
449                }
450
451                // Add the module activation state.
452                $response[ $module ] = Jetpack::is_module_active( $module );
453            }
454        }
455
456        $settings = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( 'settings' );
457
458        if ( ! function_exists( 'is_plugin_active' ) ) {
459            require_once ABSPATH . 'wp-admin/includes/plugin.php';
460        }
461
462        $response['categories'] = get_categories( array( 'get' => 'all' ) );
463
464        foreach ( $settings as $setting => $properties ) {
465            switch ( $setting ) {
466                case 'lang_id':
467                    if ( ! current_user_can( 'install_languages' ) ) {
468                        // The user doesn't have caps to install language packs, so warn the client.
469                        $response[ $setting ] = 'error_cap';
470                        break;
471                    }
472
473                    $value = get_option( 'WPLANG', '' );
474                    if ( empty( $value ) && defined( 'WPLANG' ) ) {
475                        $value = WPLANG;
476                    }
477                    $response[ $setting ] = empty( $value ) ? 'en_US' : $value;
478                    break;
479
480                case 'wordpress_api_key':
481                    // When field is clear, return empty. Otherwise it would return "false".
482                    if ( '' === get_option( 'wordpress_api_key', '' ) ) {
483                        $response[ $setting ] = '';
484                    } else {
485                        if ( ! class_exists( 'Akismet' ) ) {
486                            if ( is_readable( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
487                                require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
488                            }
489                        }
490                        $response[ $setting ] = class_exists( 'Akismet' ) ? Akismet::get_api_key() : '';
491                    }
492                    break;
493
494                case 'search_auto_config':
495                    // Only writable.
496                    $response[ $setting ] = 1;
497                    break;
498
499                default:
500                    $default              = $settings[ $setting ]['default'] ?? false;
501                    $response[ $setting ] = Jetpack_Core_Json_Api_Endpoints::cast_value( get_option( $setting, $default ), $settings[ $setting ] );
502                    break;
503            }
504        }
505
506        $response['akismet'] = is_plugin_active( 'akismet/akismet.php' );
507
508        require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
509        if ( class_exists( 'Jetpack_Memberships' ) ) {
510            $response['newsletter_has_active_plan'] = count( Jetpack_Memberships::get_all_newsletter_plan_ids( false ) ) > 0;
511        }
512
513        // Make sure we are returning a consistent type
514        if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
515            require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
516        }
517        $response['wpcom_newsletter_categories'] = Jetpack_Newsletter_Category_Helper::get_category_ids();
518
519        return rest_ensure_response( $response );
520    }
521
522    /**
523     * If it's a valid Jetpack module and configuration parameters have been sent, update it.
524     *
525     * @since 4.3.0
526     *
527     * @param WP_REST_Request $request {
528     *     Array of parameters received by request.
529     *
530     *     @type string $slug Module slug.
531     * }
532     *
533     * @return bool|WP_REST_Response|WP_Error True or a WP_REST_Response if module was updated. Otherwise, a WP_Error instance with the corresponding error.
534     */
535    public function update_data( $request ) {
536
537        // If it's null, we're trying to update many module options from different modules.
538        if ( $request['slug'] === null ) {
539
540            // Value admitted by Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list that will make it return all module options.
541            // It will not be passed. It's just checked in this method to pass that method a string or array.
542            $request['slug'] = 'any';
543        } else {
544            if ( ! Jetpack::is_module( $request['slug'] ) ) {
545                return new WP_Error( 'not_found', esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ), array( 'status' => 404 ) );
546            }
547
548            if ( ! Jetpack::is_module_active( $request['slug'] ) ) {
549                return new WP_Error( 'inactive', esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' ), array( 'status' => 409 ) );
550            }
551        }
552
553        /*
554         * Get parameters to update the module.
555         * We cannot simply use $request->get_params() because when we registered this route,
556         * we are adding the entire output of Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list()
557         * to the current request object's params. We are interested in body of the actual request.
558         * This may be JSON:
559         */
560        $params = $request->get_json_params();
561        if ( ! is_array( $params ) ) {
562            // Or it may be standard POST key-value pairs.
563            $params = $request->get_body_params();
564        }
565
566        // Exit if no parameters were passed.
567        if ( ! is_array( $params ) ) {
568            return new WP_Error( 'missing_options', esc_html__( 'Missing options.', 'jetpack' ), array( 'status' => 404 ) );
569        }
570
571        // If $params was set via `get_body_params()` there may be some additional variables in the request that can
572        // cause validation to fail. This method verifies that each param was in fact updated and will throw a `some_updated`
573        // error if unused variables are included in the request.
574        foreach ( array_keys( $params ) as $key ) {
575            if ( is_int( $key ) || 'slug' === $key || 'context' === $key ) {
576                unset( $params[ $key ] );
577            }
578        }
579
580        // Get available module options.
581        $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list(
582            'any' === $request['slug']
583            ? $params
584            : $request['slug']
585        );
586
587        // Prepare to toggle module if needed.
588        $toggle_module = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
589
590        // Options that are invalid or failed to update.
591        $invalid     = array_keys( array_diff_key( $params, $options ) );
592        $not_updated = array();
593
594        // Remove invalid options.
595        $params = array_intersect_key( $params, $options );
596
597        // Used if response is successful. The message can be overwritten and additional data can be added here.
598        $response = array(
599            'code'    => 'success',
600            'message' => esc_html__( 'The requested Jetpack data updates were successful.', 'jetpack' ),
601        );
602
603        // If there are modules to activate, activate them first so they're ready when their options are set.
604        foreach ( $params as $option => $value ) {
605            if ( 'modules' === $options[ $option ]['jp_group'] ) {
606
607                // Used if there was an error. Can be overwritten with specific error messages.
608                $error = '';
609
610                // Set to true if the module toggling was successful.
611                $updated = false;
612
613                // Check if user can toggle the module.
614                if ( $toggle_module->can_request() ) {
615
616                    // Activate or deactivate the module according to the value passed.
617                    $toggle_result = $value
618                        ? $toggle_module->activate_module( $option )
619                        : $toggle_module->deactivate_module( $option );
620
621                    if (
622                        is_wp_error( $toggle_result )
623                        && 'already_inactive' === $toggle_result->get_error_code()
624                    ) {
625
626                        // If the module is already inactive, we don't fail.
627                        $updated = true;
628                    } elseif ( is_wp_error( $toggle_result ) ) {
629                        $error = $toggle_result->get_error_message();
630                    } else {
631                        $updated = true;
632                    }
633                } else {
634                    $error = REST_Connector::get_user_permissions_error_msg();
635                }
636
637                // The module was not toggled.
638                if ( ! $updated ) {
639                    $not_updated[ $option ] = $error;
640                }
641
642                if ( $updated ) {
643                    // Return the module state.
644                    $response[ $option ] = $value;
645                }
646
647                // Remove module from list so we don't go through it again.
648                unset( $params[ $option ] );
649            }
650        }
651
652        if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
653            require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
654        }
655
656        foreach ( $params as $option => $value ) {
657
658            // Used if there was an error. Can be overwritten with specific error messages.
659            $error = '';
660
661            // Set to true if the option update was successful.
662            $updated = false;
663
664            // Get option attributes, including the group it belongs to.
665            $option_attrs = $options[ $option ];
666
667            // If this is a module option and the related module isn't active for any reason, continue with the next one.
668            if ( 'settings' !== $option_attrs['jp_group'] ) {
669                if ( ! Jetpack::is_module( $option_attrs['jp_group'] ) ) {
670                    $not_updated[ $option ] = esc_html__( 'The requested Jetpack module was not found.', 'jetpack' );
671                    continue;
672                }
673
674                if (
675                    'any' !== $request['slug']
676                    && ! Jetpack::is_module_active( $option_attrs['jp_group'] )
677                ) {
678
679                    // We only take note of skipped options when updating one module.
680                    $not_updated[ $option ] = esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' );
681                    continue;
682                }
683            }
684
685            // Properly cast value based on its type defined in endpoint accepted args.
686            $value = Jetpack_Core_Json_Api_Endpoints::cast_value( $value, $option_attrs );
687
688            switch ( $option ) {
689                case 'lang_id':
690                    if ( ! current_user_can( 'install_languages' ) ) {
691                        // We can't affect this setting.
692                        $updated = false;
693                        break;
694                    }
695
696                    if ( 'en_US' === $value || empty( $value ) ) {
697                        return delete_option( 'WPLANG' );
698                    }
699
700                    if ( ! function_exists( 'request_filesystem_credentials' ) ) {
701                        require_once ABSPATH . 'wp-admin/includes/file.php';
702                    }
703
704                    if ( ! function_exists( 'wp_download_language_pack' ) ) {
705                        require_once ABSPATH . 'wp-admin/includes/translation-install.php';
706                    }
707
708                    // `wp_download_language_pack` only tries to download packs if they're not already available.
709                    $language = wp_download_language_pack( $value );
710                    if ( false === $language ) {
711                        // The language pack download failed.
712                        $updated = false;
713                        break;
714                    }
715                    $updated = get_option( 'WPLANG' ) === $language ? true : update_option( 'WPLANG', $language );
716                    break;
717
718                case 'monitor_receive_notifications':
719                    if ( ! class_exists( 'Jetpack_Monitor' ) ) {
720                        $updated = false;
721                        break;
722                    }
723
724                    $monitor = new Jetpack_Monitor();
725
726                    // If we got true as response, consider it done.
727                    $updated = true === $monitor->update_option_receive_jetpack_monitor_notification( $value );
728                    break;
729
730                case 'post_by_email_address':
731                    if ( ! class_exists( 'Jetpack_Post_By_Email' ) ) {
732                        $updated = false;
733                        break;
734                    }
735
736                    $result = Jetpack_Post_By_Email::init()->process_api_request( $value );
737
738                    // If we got an email address (create or regenerate) or 1 (delete), consider it done.
739                    if ( is_string( $result ) && preg_match( '/[a-z0-9]+@post.wordpress.com/', $result ) ) {
740                        $response[ $option ] = $result;
741                        $updated             = true;
742                    } elseif ( 1 == $result ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
743                        $updated = true;
744                    } elseif ( is_array( $result ) && isset( $result['message'] ) ) {
745                        $error = $result['message'];
746                    }
747                    break;
748
749                case 'jetpack_protect_key':
750                    $brute_force_protection = Brute_Force_Protection::instance();
751                    if ( 'create' === $value ) {
752                        $result = $brute_force_protection->get_protect_key();
753                    } else {
754                        $result = false;
755                    }
756
757                    // If we got one of Protect keys, consider it done.
758                    if ( is_string( $result ) && preg_match( '/[a-z0-9]{40,}/i', $result ) ) {
759                        $response[ $option ] = $result;
760                        $updated             = true;
761                    }
762                    break;
763
764                case 'jetpack_protect_global_whitelist':
765                    $updated = Brute_Force_Protection_Shared_Functions::save_allow_list( explode( PHP_EOL, str_replace( array( ' ', ',' ), array( '', "\n" ), $value ) ) );
766
767                    if ( is_wp_error( $updated ) ) {
768                        $error = $updated->get_error_message();
769                    }
770                    break;
771
772                case 'show_headline':
773                case 'show_thumbnails':
774                    $grouped_options_current    = (array) Jetpack_Options::get_option( 'relatedposts' );
775                    $grouped_options            = $grouped_options_current;
776                    $grouped_options[ $option ] = $value;
777
778                    // If option value was the same, consider it done.
779                    $updated = $grouped_options_current !== $grouped_options ? Jetpack_Options::update_option( 'relatedposts', $grouped_options ) : true;
780                    break;
781
782                case 'search_auto_config':
783                    if ( ! $value ) {
784                        // Skip execution if no value is specified.
785                        $updated = true;
786                    } else {
787                        $plan = new Automattic\Jetpack\Search\Plan();
788                        if ( ! $plan->supports_instant_search() ) {
789                            $updated = new WP_Error( 'instant_search_not_supported', 'Instant Search is not supported by this site', array( 'status' => 400 ) );
790                            $error   = $updated->get_error_message();
791                        } elseif ( ! Automattic\Jetpack\Search\Options::is_instant_enabled() ) {
792                            $updated = new WP_Error( 'instant_search_disabled', 'Instant Search is disabled', array( 'status' => 400 ) );
793                            $error   = $updated->get_error_message();
794                        } else {
795                            $blog_id  = Automattic\Jetpack\Search\Helper::get_wpcom_site_id();
796                            $instance = Automattic\Jetpack\Search\Instant_Search::instance( $blog_id );
797                            $instance->auto_config_search();
798                            $updated = true;
799                        }
800                    }
801                    break;
802
803                case 'google':
804                case 'bing':
805                case 'pinterest':
806                case 'yandex':
807                case 'facebook':
808                    $grouped_options_current = (array) get_option( 'verification_services_codes' );
809                    $grouped_options         = $grouped_options_current;
810
811                    // Extracts the content attribute from the HTML meta tag if needed.
812                    if ( preg_match( '#.*<meta name="(?:[^"]+)" content="([^"]+)" />.*#i', $value, $matches ) ) {
813                        $grouped_options[ $option ] = $matches[1];
814                    } else {
815                        $grouped_options[ $option ] = $value;
816                    }
817
818                    // If option value was the same, consider it done.
819                    $updated = $grouped_options_current !== $grouped_options
820                        ? update_option( 'verification_services_codes', $grouped_options )
821                        : true;
822                    break;
823
824                case 'sharing_services':
825                    if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
826                        break;
827                    }
828
829                    $sharer = new Sharing_Service();
830
831                    // If option value was the same, consider it done.
832                    $updated = $value !== $sharer->get_blog_services()
833                        ? $sharer->set_blog_services( $value['visible'], $value['hidden'] )
834                        : true;
835                    break;
836
837                case 'button_style':
838                case 'sharing_label':
839                case 'show':
840                    if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
841                        break;
842                    }
843
844                    $sharer                     = new Sharing_Service();
845                    $grouped_options            = $sharer->get_global_options();
846                    $grouped_options[ $option ] = $value;
847                    $updated                    = $sharer->set_global_options( $grouped_options );
848                    break;
849
850                case 'custom':
851                    if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
852                        break;
853                    }
854
855                    $sharer  = new Sharing_Service();
856                    $updated = $sharer->new_service( stripslashes( $value['sharing_name'] ), stripslashes( $value['sharing_url'] ), stripslashes( $value['sharing_icon'] ) );
857
858                    // Return new custom service.
859                    $response[ $option ] = $updated;
860                    break;
861
862                case 'sharing_delete_service':
863                    if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
864                        break;
865                    }
866
867                    $sharer  = new Sharing_Service();
868                    $updated = $sharer->delete_service( $value );
869                    break;
870
871                case 'jetpack-twitter-cards-site-tag':
872                    $value   = trim( ltrim( wp_strip_all_tags( $value ), '@' ) );
873                    $updated = get_option( $option ) !== $value ? update_option( $option, $value ) : true;
874                    break;
875
876                case 'admin_bar':
877                case 'roles':
878                case 'count_roles':
879                case 'blog_id':
880                case 'do_not_track':
881                case 'version':
882                case 'collapse_nudges':
883                    $grouped_options_current    = (array) get_option( 'stats_options' );
884                    $grouped_options            = $grouped_options_current;
885                    $grouped_options[ $option ] = $value;
886
887                    // If option value was the same, consider it done.
888                    $updated = $grouped_options_current !== $grouped_options
889                        ? update_option( 'stats_options', $grouped_options )
890                        : true;
891                    break;
892
893                case 'enable_odyssey_stats':
894                    $updated = Stats_Admin_Main::update_new_stats_status( $value );
895
896                    break;
897
898                case 'akismet_show_user_comments_approved':
899                    // Save Akismet option '1' or '0' like it's done in akismet/class.akismet-admin.php.
900                    $updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
901                        ? update_option( $option, $value ? '1' : '0' )
902                        : true;
903                    break;
904
905                case 'wordpress_api_key':
906                    if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
907                        $error   = esc_html__( 'Please install Akismet.', 'jetpack' );
908                        $updated = false;
909                        break;
910                    }
911
912                    if ( ! defined( 'AKISMET_VERSION' ) ) {
913                        $error   = esc_html__( 'Please activate Akismet.', 'jetpack' );
914                        $updated = false;
915                        break;
916                    }
917
918                    // Allow to clear the API key field.
919                    if ( '' === $value ) {
920                        $updated = get_option( $option ) !== $value
921                            ? update_option( $option, $value )
922                            : true;
923                        break;
924                    }
925
926                    require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
927                    require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
928
929                    if ( class_exists( 'Akismet_Admin' ) && method_exists( 'Akismet_Admin', 'save_key' ) ) {
930                        if ( Akismet::verify_key( $value ) === 'valid' ) {
931                            $akismet_user = Akismet_Admin::get_akismet_user( $value );
932                            if ( $akismet_user ) {
933                                if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ), true ) ) {
934                                    $updated = get_option( $option ) !== $value
935                                        ? update_option( $option, $value )
936                                        : true;
937                                    break;
938                                } else {
939                                    $error = esc_html__( "Akismet user status doesn't allow to update the key", 'jetpack' );
940                                }
941                            } else {
942                                $error = esc_html__( 'Invalid Akismet user', 'jetpack' );
943                            }
944                        } else {
945                            $error = esc_html__( 'Invalid Akismet key', 'jetpack' );
946                        }
947                    } else {
948                        $error = esc_html__( 'Akismet is not installed or active', 'jetpack' );
949                    }
950                    $updated = false;
951                    break;
952
953                case 'google_analytics_tracking_id':
954                    $grouped_options_current = (array) get_option( 'jetpack_wga' );
955                    $grouped_options         = $grouped_options_current;
956                    $grouped_options['code'] = $value;
957
958                    // If option value was the same, consider it done.
959                    $updated = $grouped_options_current !== $grouped_options
960                        ? update_option( 'jetpack_wga', $grouped_options )
961                        : true;
962                    break;
963
964                case 'dismiss_empty_stats_card':
965                case 'dismiss_dash_backup_getting_started':
966                case 'dismiss_dash_agencies_learn_more':
967                    // If option value was the same, consider it done.
968                    $updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support bools or strings saved by update_option.
969                        ? update_option( $option, (bool) $value )
970                        : true;
971                    break;
972
973                case 'jetpack_subscriptions_reply_to':
974                    // If option value was the same, consider it done.
975                    require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
976                    $sub_value = Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value )
977                        ? $value
978                        : Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to;
979
980                        $updated = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
981                    break;
982                case 'jetpack_subscriptions_from_name':
983                    // If option value was the same, consider it done.
984                    $sub_value = sanitize_text_field( $value );
985                    $updated   = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
986                    break;
987
988                case 'stb_enabled':
989                case 'stc_enabled':
990                case 'sm_enabled':
991                case 'jetpack_subscribe_overlay_enabled':
992                case 'jetpack_subscribe_floating_button_enabled':
993                case 'wpcom_newsletter_categories_enabled':
994                case 'wpcom_featured_image_in_email':
995                case 'jetpack_gravatar_in_email':
996                case 'jetpack_author_in_email':
997                case 'jetpack_post_date_in_email':
998                case 'wpcom_subscription_emails_use_excerpt':
999                case 'jetpack_subscriptions_subscribe_post_end_enabled':
1000                case 'jetpack_subscriptions_login_navigation_enabled':
1001                case 'jetpack_subscriptions_subscribe_navigation_enabled':
1002                    // Convert the false value to 0. This allows the option to be updated if it doesn't exist yet.
1003                    $sub_value = $value ? $value : 0;
1004                    $updated   = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
1005                    break;
1006
1007                case 'jetpack_blocks_disabled':
1008                    $updated = (bool) get_option( $option ) !== (bool) $value ? update_option( $option, (bool) $value ) : true;
1009                    break;
1010
1011                case 'subscription_options':
1012                    if ( ! is_array( $value ) ) {
1013                        break;
1014                    }
1015
1016                    $allowed_keys   = array( 'invitation', 'comment_follow', 'welcome', 'subscribe_modal_heading', 'free_tier_description', 'hide_free_tier' );
1017                    $filtered_value = array_filter(
1018                        $value,
1019                        function ( $key ) use ( $allowed_keys ) {
1020                            return in_array( $key, $allowed_keys, true );
1021                        },
1022                        ARRAY_FILTER_USE_KEY
1023                    );
1024
1025                    if ( empty( $filtered_value ) ) {
1026                        break;
1027                    }
1028
1029                    // `hide_free_tier` is a boolean flag, so pull it out before the HTML
1030                    // sanitization below (which expects strings). Sanitize it with
1031                    // rest_sanitize_boolean() so stringy booleans (e.g. "false", "0")
1032                    // are interpreted correctly rather than being treated as truthy by a
1033                    // plain `! empty()`.
1034                    $has_hide_free_tier = array_key_exists( 'hide_free_tier', $filtered_value );
1035                    $hide_free_tier     = $has_hide_free_tier && rest_sanitize_boolean( $filtered_value['hide_free_tier'] );
1036                    unset( $filtered_value['hide_free_tier'] );
1037
1038                    array_walk_recursive(
1039                        $filtered_value,
1040                        function ( &$value ) {
1041                            $value = wp_kses(
1042                                $value,
1043                                array(
1044                                    'ul'     => array(),
1045                                    'li'     => array(),
1046                                    'p'      => array(),
1047                                    'strong' => array(),
1048                                    'ol'     => array(),
1049                                    'em'     => array(),
1050                                    'a'      => array(
1051                                        'href' => array(),
1052                                    ),
1053                                )
1054                            );
1055                        }
1056                    );
1057
1058                    // Normalize whitespace-only `subscribe_modal_heading` input to empty so
1059                    // the modal template's `empty()` fallback fires. PHP's `empty()` treats
1060                    // `"   "` as non-empty, which would otherwise render a blank heading.
1061                    if ( isset( $filtered_value['subscribe_modal_heading'] ) ) {
1062                        $filtered_value['subscribe_modal_heading'] = trim( $filtered_value['subscribe_modal_heading'] );
1063                    }
1064
1065                    // The free tier description is stored as plain markdown source, so strip
1066                    // all HTML and cap its length to match the paid-tier description field.
1067                    // WordPress core guarantees mb_substr() (polyfilled in wp-includes/compat.php
1068                    // when the mbstring extension is unavailable), so it's safe to use directly.
1069                    // A JSON payload could supply a non-scalar (array/object) for this field,
1070                    // which would fatal in wp_kses()/mb_substr() on PHP 8+, so drop invalid values.
1071                    if ( isset( $filtered_value['free_tier_description'] ) ) {
1072                        if ( is_scalar( $filtered_value['free_tier_description'] ) ) {
1073                            $filtered_value['free_tier_description'] = mb_substr( wp_kses( (string) $filtered_value['free_tier_description'], array() ), 0, 500 );
1074                        } else {
1075                            unset( $filtered_value['free_tier_description'] );
1076                        }
1077                    }
1078
1079                    if ( $has_hide_free_tier ) {
1080                        $filtered_value['hide_free_tier'] = $hide_free_tier;
1081                    }
1082
1083                    $old_subscription_options = get_option( 'subscription_options' );
1084                    if ( ! is_array( $old_subscription_options ) ) {
1085                        $old_subscription_options = array();
1086                    }
1087                    $new_subscription_options = array_merge( $old_subscription_options, $filtered_value );
1088                    $updated                  = true;
1089
1090                    if ( serialize( $old_subscription_options ) === serialize( $new_subscription_options ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1091                        break; // This prevents the option update to fail when the values are the same.
1092                    }
1093
1094                    if ( ! update_option( $option, $new_subscription_options ) ) {
1095                        $updated = false;
1096                        $error   = esc_html__( 'Subscription Options failed to process.', 'jetpack' );
1097                    }
1098                    break;
1099
1100                case Jetpack_Newsletter_Category_Helper::NEWSLETTER_CATEGORIES_OPTION:
1101                    if ( ! is_array( $value ) || empty( $value ) ) {
1102                        $updated = true;
1103                        break;
1104                    }
1105
1106                    // If we are already current, do nothing
1107                    $current_value = Jetpack_Newsletter_Category_Helper::get_category_ids();
1108                    if ( $value === $current_value ) {
1109                        $updated = true;
1110                        break;
1111                    }
1112
1113                    if ( Jetpack_Newsletter_Category_Helper::save_category_ids( $value ) ) {
1114                        $updated = true;
1115                    } else {
1116                        $updated = false;
1117                        $error   = esc_html__( 'Newsletter category did not update.', 'jetpack' );
1118                    }
1119
1120                    break;
1121
1122                default:
1123                    // Boolean values are stored as 1 or 0.
1124                    if ( isset( $options[ $option ]['type'] ) && 'boolean' === $options[ $option ]['type'] ) {
1125                        $value = (int) $value;
1126                    }
1127
1128                    // If option value was the same as it's current value, or it's default, consider it done.
1129                    $default = $options[ $option ]['default'] ?? false;
1130                    $updated = get_option( $option, $default ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support scalars or strings saved by update_option.
1131                        ? update_option( $option, $value )
1132                        : true;
1133                    break;
1134            }
1135
1136            // The option was not updated.
1137            if ( ! $updated ) {
1138                $not_updated[ $option ] = $error;
1139            }
1140        }
1141
1142        if ( empty( $invalid ) && empty( $not_updated ) ) {
1143            // The option was updated.
1144            return rest_ensure_response( $response );
1145        } else {
1146            $invalid_count     = count( $invalid );
1147            $not_updated_count = count( $not_updated );
1148            $error             = '';
1149            if ( $invalid_count > 0 ) {
1150                $error = sprintf(
1151                /* Translators: the plural variable is a comma-separated list. Example: dog, cat, bird. */
1152                    _n( 'Invalid option: %s.', 'Invalid options: %s.', $invalid_count, 'jetpack' ),
1153                    implode( ', ', $invalid )
1154                );
1155            }
1156            if ( $not_updated_count > 0 ) {
1157                $not_updated_messages = array();
1158                foreach ( $not_updated as $not_updated_option => $not_updated_message ) {
1159                    if ( ! empty( $not_updated_message ) ) {
1160                        $not_updated_messages[] = sprintf(
1161                            /* Translators: the first variable is a module option or slug, or setting. The second is the error message . */
1162                            __( '%1$s: %2$s', 'jetpack' ),
1163                            $not_updated_option,
1164                            $not_updated_message
1165                        );
1166                    }
1167                }
1168                if ( ! empty( $error ) ) {
1169                    $error .= ' ';
1170                }
1171                if ( ! empty( $not_updated_messages ) ) {
1172                    $error .= ' ' . implode( '. ', $not_updated_messages );
1173                }
1174            }
1175            // There was an error because some options were updated but others were invalid or failed to update.
1176            return new WP_Error( 'some_updated', esc_html( $error ), array( 'status' => 400 ) );
1177        }
1178    }
1179
1180    /**
1181     * Perform tasks in the site based on onboarding choices.
1182     *
1183     * @since 5.4.0
1184     *
1185     * @deprecated since 13.9
1186     *
1187     * @param array $data Onboarding choices made by user.
1188     *
1189     * @return string Result of onboarding processing and, if there is one, an error message.
1190     */
1191    private function process_onboarding( $data ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1192        _deprecated_function( __METHOD__, '13.9' );
1193        return '';
1194    }
1195
1196    /**
1197     * Add or update Business Address widget.
1198     *
1199     * @deprecated since 13.9
1200     *
1201     * @param array $address Array of business address fields.
1202     *
1203     * @return WP_Error|true True if the data was saved correctly.
1204     */
1205    private static function handle_business_address( $address ) {
1206        _deprecated_function( __METHOD__, '13.9' );
1207        $first_sidebar = Jetpack_Widgets::get_first_sidebar();
1208
1209        $widgets_module_active = Jetpack::is_module_active( 'widgets' );
1210        if ( ! $widgets_module_active ) {
1211            $widgets_module_active = Jetpack::activate_module( 'widgets', false, false );
1212        }
1213        if ( ! $widgets_module_active ) {
1214            return new WP_Error( 'module_activation_failed', 'Failed to activate the widgets module.', 400 );
1215        }
1216
1217        if ( $first_sidebar ) {
1218            $title   = isset( $address['name'] ) ? sanitize_text_field( $address['name'] ) : '';
1219            $street  = isset( $address['street'] ) ? sanitize_text_field( $address['street'] ) : '';
1220            $city    = isset( $address['city'] ) ? sanitize_text_field( $address['city'] ) : '';
1221            $state   = isset( $address['state'] ) ? sanitize_text_field( $address['state'] ) : '';
1222            $zip     = isset( $address['zip'] ) ? sanitize_text_field( $address['zip'] ) : '';
1223            $country = isset( $address['country'] ) ? sanitize_text_field( $address['country'] ) : '';
1224
1225            $full_address = implode( ' ', array_filter( array( $street, $city, $state, $zip, $country ) ) );
1226
1227            $widget_options = array(
1228                'title'   => $title,
1229                'address' => $full_address,
1230                'phone'   => '',
1231                'hours'   => '',
1232                'showmap' => false,
1233                'email'   => '',
1234            );
1235
1236            $widget_updated = '';
1237            if ( ! self::has_business_address_widget( $first_sidebar ) ) {
1238                $widget_updated = Jetpack_Widgets::insert_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
1239            } else {
1240                $widget_updated = Jetpack_Widgets::update_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
1241            }
1242            if ( is_wp_error( $widget_updated ) ) {
1243                return new WP_Error( 'widget_update_failed', 'Widget could not be updated.', 400 );
1244            }
1245
1246            $address_save = array(
1247                'name'    => $title,
1248                'street'  => $street,
1249                'city'    => $city,
1250                'state'   => $state,
1251                'zip'     => $zip,
1252                'country' => $country,
1253            );
1254            update_option( 'jpo_business_address', $address_save );
1255            return true;
1256        }
1257
1258        // No sidebar to place the widget.
1259        return new WP_Error( 'sidebar_not_found', 'No sidebar.', 400 );
1260    }
1261
1262    /**
1263     * Check whether "Contact Info & Map" widget is present in a given sidebar.
1264     *
1265     * @param string $sidebar ID of the sidebar to which the widget will be added.
1266     *
1267     * @return bool Whether the widget is present in a given sidebar.
1268     */
1269    private static function has_business_address_widget( $sidebar ) {
1270        $sidebars_widgets = get_option( 'sidebars_widgets', array() );
1271        if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
1272            return false;
1273        }
1274        foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
1275            if ( str_contains( $widget, 'widget_contact_info' ) ) {
1276                return true;
1277            }
1278        }
1279        return false;
1280    }
1281
1282    /**
1283     * Check if user is allowed to perform the update.
1284     *
1285     * @since 4.3.0
1286     *
1287     * @param WP_REST_Request $request The request sent to the WP REST API.
1288     *
1289     * @return bool
1290     */
1291    public function can_request( $request ) {
1292        if ( 'GET' === $request->get_method() ) {
1293            return current_user_can( 'jetpack_admin_page' );
1294        } else {
1295            $module = Jetpack_Core_Json_Api_Endpoints::get_module_requested();
1296            if ( empty( $module ) ) {
1297                $params = $request->get_json_params();
1298                if ( ! is_array( $params ) ) {
1299                    $params = $request->get_body_params();
1300                }
1301                $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( $params );
1302                foreach ( $options as $option => $definition ) {
1303                    if ( in_array( $options[ $option ]['jp_group'], array( 'post-by-email' ), true ) ) {
1304                        $module = $options[ $option ]['jp_group'];
1305                        break;
1306                    }
1307                }
1308            }
1309            // User is trying to create, regenerate or delete its PbE.
1310            if ( 'post-by-email' === $module ) {
1311                return current_user_can( 'edit_posts' ) && current_user_can( 'jetpack_admin_page' );
1312            }
1313            return current_user_can( 'jetpack_configure_modules' );
1314        }
1315    }
1316}
1317
1318/**
1319 * Get detailed data from a specific module.
1320 *
1321 * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
1322 */
1323class Jetpack_Core_API_Module_Data_Endpoint {
1324
1325    /**
1326     * Process request and return different data based on the module we are interested in.
1327     *
1328     * @param WP_REST_Request $request WP API request.
1329     *
1330     * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
1331     */
1332    public function process( $request ) {
1333        switch ( $request['slug'] ) {
1334            case 'protect':
1335                return $this->get_protect_data();
1336            case 'stats':
1337                return $this->get_stats_data( $request );
1338            case 'akismet':
1339                return $this->get_akismet_data();
1340            case 'monitor':
1341                return $this->get_monitor_data();
1342            case 'verification-tools':
1343                return $this->get_verification_tools_data();
1344            case 'vaultpress':
1345                return $this->get_vaultpress_data();
1346        }
1347    }
1348
1349    /**
1350     * Decide against which service to check the key.
1351     *
1352     * @since 4.8.0
1353     *
1354     * @param WP_REST_Request $request WP API request.
1355     *
1356     * @return bool
1357     */
1358    public function key_check( $request ) {
1359        switch ( $request['service'] ) {
1360            case 'akismet':
1361                $params = $request->get_json_params();
1362                if ( isset( $params['api_key'] ) && ! empty( $params['api_key'] ) ) {
1363                    return $this->check_akismet_key( $params['api_key'] );
1364                }
1365                return $this->check_akismet_key();
1366        }
1367        return false;
1368    }
1369
1370    /**
1371     * Get number of blocked intrusion attempts.
1372     *
1373     * @since 4.3.0
1374     *
1375     * @return mixed|WP_Error Number of blocked attempts if protection is enabled. Otherwise, a WP_Error instance with the corresponding error.
1376     */
1377    public function get_protect_data() {
1378        if ( Jetpack::is_module_active( 'protect' ) ) {
1379            return (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 );
1380        }
1381
1382        return new WP_Error(
1383            'not_active',
1384            esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1385            array( 'status' => 404 )
1386        );
1387    }
1388
1389    /**
1390     * Get number of spam messages blocked by Akismet.
1391     *
1392     * @since 4.3.0
1393     *
1394     * @return int|string Number of spam blocked by Akismet. Otherwise, an error message.
1395     */
1396    public function get_akismet_data() {
1397        $akismet_status = $this->akismet_is_active_and_registered();
1398        if ( ! is_wp_error( $akismet_status ) ) {
1399            return (int) get_option( 'akismet_spam_count', 0 );
1400        } else {
1401            return $akismet_status->get_error_code();
1402        }
1403    }
1404
1405    /**
1406     * Verify the Akismet API key.
1407     *
1408     * @since 4.8.0
1409     *
1410     * @param string $api_key Optional API key to check.
1411     *
1412     * @return array Information about the key. 'validKey' is true if key is valid, false otherwise.
1413     */
1414    public function check_akismet_key( $api_key = '' ) {
1415        $akismet_status = $this->akismet_class_exists();
1416        if ( is_wp_error( $akismet_status ) ) {
1417            return rest_ensure_response(
1418                array(
1419                    'validKey'          => false,
1420                    'invalidKeyCode'    => $akismet_status->get_error_code(),
1421                    'invalidKeyMessage' => $akismet_status->get_error_message(),
1422                )
1423            );
1424        }
1425
1426        $key_status = Akismet::check_key_status( empty( $api_key ) ? Akismet::get_api_key() : $api_key );
1427
1428        if ( ! $key_status || 'invalid' === $key_status || 'failed' === $key_status ) {
1429            return rest_ensure_response(
1430                array(
1431                    'validKey'          => false,
1432                    'invalidKeyCode'    => 'invalid_key',
1433                    'invalidKeyMessage' => esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ),
1434                )
1435            );
1436        }
1437
1438        return rest_ensure_response(
1439            array(
1440                'validKey' => isset( $key_status[1] ) && 'valid' === $key_status[1],
1441            )
1442        );
1443    }
1444
1445    /**
1446     * Check if Akismet class file exists and if class is loaded.
1447     *
1448     * @since 4.8.0
1449     *
1450     * @return bool|WP_Error Returns true if class file exists and class is loaded, WP_Error otherwise.
1451     */
1452    private function akismet_class_exists() {
1453        if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
1454            return new WP_Error( 'not_installed', esc_html__( 'Please install Akismet.', 'jetpack' ), array( 'status' => 400 ) );
1455        }
1456
1457        if ( ! class_exists( 'Akismet' ) ) {
1458            return new WP_Error( 'not_active', esc_html__( 'Please activate Akismet.', 'jetpack' ), array( 'status' => 400 ) );
1459        }
1460
1461        return true;
1462    }
1463
1464    /**
1465     * Is Akismet registered and active?
1466     *
1467     * @since 4.3.0
1468     *
1469     * @return bool|WP_Error True if Akismet is active and registered. Otherwise, a WP_Error instance with the corresponding error.
1470     */
1471    private function akismet_is_active_and_registered() {
1472        $akismet_exists = $this->akismet_class_exists();
1473        if ( is_wp_error( $akismet_exists ) ) {
1474            return $akismet_exists;
1475        }
1476
1477        // What about if Akismet is put in a sub-directory or maybe in mu-plugins?
1478        require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
1479        require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
1480        $akismet_key = Akismet::verify_key( Akismet::get_api_key() );
1481
1482        if ( ! $akismet_key || 'invalid' === $akismet_key || 'failed' === $akismet_key ) {
1483            return new WP_Error( 'invalid_key', esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ), array( 'status' => 400 ) );
1484        }
1485
1486        return true;
1487    }
1488
1489    /**
1490     * Get stats data for this site
1491     *
1492     * @since 4.1.0
1493     *
1494     * @param WP_REST_Request $request {
1495     *     Array of parameters received by request.
1496     *
1497     *     @type string $date Date range to restrict results to.
1498     * }
1499     *
1500     * @return WP_Error|WP_HTTP_Response|WP_REST_Response Stats information relayed from WordPress.com.
1501     */
1502    public function get_stats_data( WP_REST_Request $request ) {
1503        // Get parameters to fetch Stats data.
1504        $range = $request->get_param( 'range' );
1505
1506        // If no parameters were passed.
1507        if (
1508            empty( $range )
1509            || ! in_array( $range, array( 'day', 'week', 'month' ), true )
1510        ) {
1511            $range = 'day';
1512        }
1513
1514        $wpcom_stats = new WPCOM_Stats();
1515        switch ( $range ) {
1516
1517            // This is always called first on page load.
1518            case 'day':
1519                $initial_stats = $wpcom_stats->convert_stats_array_to_object( $wpcom_stats->get_stats() );
1520                return rest_ensure_response(
1521                    array(
1522                        'general' => $initial_stats,
1523
1524                        // Build data for 'day' as if it was $wpcom_stats ->get_visits( array( 'unit' => 'day, 'quantity' => 30).
1525                        'day'     => $initial_stats->visits ?? array(),
1526                    )
1527                );
1528            case 'week':
1529                return rest_ensure_response(
1530                    array(
1531                        'week' => $wpcom_stats->convert_stats_array_to_object(
1532                            $wpcom_stats->get_visits(
1533                                array(
1534                                    'unit'     => 'week',
1535                                    'quantity' => 14,
1536                                )
1537                            )
1538                        ),
1539                    )
1540                );
1541            case 'month':
1542                return rest_ensure_response(
1543                    array(
1544                        'month' => $wpcom_stats->convert_stats_array_to_object(
1545                            $wpcom_stats->get_visits(
1546                                array(
1547                                    'unit'     => 'month',
1548                                    'quantity' => 12,
1549                                )
1550                            )
1551                        ),
1552                    )
1553                );
1554        }
1555    }
1556
1557    /**
1558     * Get date of last downtime.
1559     *
1560     * @since 4.3.0
1561     *
1562     * @return mixed|WP_Error Number of days since last downtime. Otherwise, a WP_Error instance with the corresponding error.
1563     */
1564    public function get_monitor_data() {
1565        if ( ! Jetpack::is_module_active( 'monitor' ) ) {
1566            return new WP_Error(
1567                'not_active',
1568                esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1569                array( 'status' => 404 )
1570            );
1571        }
1572
1573        $monitor       = new Jetpack_Monitor();
1574        $last_downtime = $monitor->monitor_get_last_downtime();
1575        if ( is_wp_error( $last_downtime ) ) {
1576            return $last_downtime;
1577        } elseif ( false === strtotime( $last_downtime ) ) {
1578            return rest_ensure_response(
1579                array(
1580                    'code' => 'success',
1581                    'date' => null,
1582                )
1583            );
1584        } else {
1585            return rest_ensure_response(
1586                array(
1587                    'code' => 'success',
1588                    'date' => human_time_diff( strtotime( $last_downtime ), strtotime( 'now' ) ),
1589                )
1590            );
1591        }
1592    }
1593
1594    /**
1595     * Get services that this site is verified with.
1596     *
1597     * @since 4.3.0
1598     *
1599     * @return mixed|WP_Error List of services that verified this site. Otherwise, a WP_Error instance with the corresponding error.
1600     */
1601    public function get_verification_tools_data() {
1602        if ( ! Jetpack::is_module_active( 'verification-tools' ) ) {
1603            return new WP_Error(
1604                'not_active',
1605                esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1606                array( 'status' => 404 )
1607            );
1608        }
1609
1610        $verification_services_codes = get_option( 'verification_services_codes' );
1611        if (
1612            ! is_array( $verification_services_codes )
1613            || empty( $verification_services_codes )
1614        ) {
1615            return new WP_Error(
1616                'empty',
1617                esc_html__( 'Site not verified with any service.', 'jetpack' ),
1618                array( 'status' => 404 )
1619            );
1620        }
1621
1622        $services = array();
1623        foreach ( jetpack_verification_services() as $name => $service ) {
1624            if ( is_array( $service ) && ! empty( $verification_services_codes[ $name ] ) ) {
1625                switch ( $name ) {
1626                    case 'google':
1627                        $services[] = 'Google';
1628                        break;
1629                    case 'bing':
1630                        $services[] = 'Bing';
1631                        break;
1632                    case 'pinterest':
1633                        $services[] = 'Pinterest';
1634                        break;
1635                    case 'yandex':
1636                        $services[] = 'Yandex';
1637                        break;
1638                    case 'facebook':
1639                        $services[] = 'Facebook';
1640                        break;
1641                }
1642            }
1643        }
1644
1645        if ( empty( $services ) ) {
1646            return new WP_Error(
1647                'empty',
1648                esc_html__( 'Site not verified with any service.', 'jetpack' ),
1649                array( 'status' => 404 )
1650            );
1651        }
1652
1653        if ( 2 > count( $services ) ) {
1654            $message = esc_html(
1655                sprintf(
1656                    /* translators: %s is a service name like Google, Bing, Pinterest, etc. */
1657                    __( 'Your site is verified with %s.', 'jetpack' ),
1658                    $services[0]
1659                )
1660            );
1661        } else {
1662            $copy_services = $services;
1663            $last          = count( $copy_services ) - 1;
1664            $last_service  = $copy_services[ $last ];
1665            unset( $copy_services[ $last ] );
1666            $message = esc_html(
1667                sprintf(
1668                    /* translators: %1$s is a comma-separated list of services, and %2$s is a single service name like Google, Bing, Pinterest, etc. */
1669                    __( 'Your site is verified with %1$s and %2$s.', 'jetpack' ),
1670                    implode( ', ', $copy_services ),
1671                    $last_service
1672                )
1673            );
1674        }
1675
1676        return rest_ensure_response(
1677            array(
1678                'code'     => 'success',
1679                'message'  => $message,
1680                'services' => $services,
1681            )
1682        );
1683    }
1684
1685    /**
1686     * Get VaultPress site data including, among other things, the date of the last backup if it was completed.
1687     *
1688     * @since 4.3.0
1689     *
1690     * @return mixed|WP_Error VaultPress site data. Otherwise, a WP_Error instance with the corresponding error.
1691     */
1692    public function get_vaultpress_data() {
1693        if ( ! class_exists( 'VaultPress' ) ) {
1694            return new WP_Error(
1695                'not_active',
1696                esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1697                array( 'status' => 404 )
1698            );
1699        }
1700
1701        $vaultpress = new VaultPress();
1702        if ( ! $vaultpress->is_registered() ) {
1703            return rest_ensure_response(
1704                array(
1705                    'code'    => 'not_registered',
1706                    'message' => esc_html__( 'You need to register for VaultPress.', 'jetpack' ),
1707                )
1708            );
1709        }
1710
1711        $data = json_decode( base64_decode( $vaultpress->contact_service( 'plugin_data' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1712        if ( false === $data ) {
1713            return rest_ensure_response(
1714                array(
1715                    'code'    => 'not_registered',
1716                    'message' => esc_html__( 'Could not connect to VaultPress.', 'jetpack' ),
1717                )
1718            );
1719        } elseif ( is_wp_error( $data ) || ! isset( $data->backups->last_backup ) ) {
1720            return $data;
1721        } elseif ( empty( $data->backups->last_backup ) ) {
1722            return rest_ensure_response(
1723                array(
1724                    'code'    => 'success',
1725                    'message' => esc_html__( 'VaultPress is active and will back up your site soon.', 'jetpack' ),
1726                    'data'    => $data,
1727                )
1728            );
1729        } else {
1730            return rest_ensure_response(
1731                array(
1732                    'code'    => 'success',
1733                    'message' => esc_html(
1734                        sprintf(
1735                            /* translators: placeholder is a unit of time (1 hour, 5 days, ...) */
1736                            esc_html__( 'Your site was successfully backed up %s ago.', 'jetpack' ),
1737                            human_time_diff(
1738                                $data->backups->last_backup,
1739                                current_time( 'timestamp' ) // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested -- We cannot switch to time() or another "unix" timestamp option as long as $data->backups->last_backup uses WP timestamps.
1740                            )
1741                        )
1742                    ),
1743                    'data'    => $data,
1744                )
1745            );
1746        }
1747    }
1748
1749    /**
1750     * A WordPress REST API permission callback method that accepts a request object and
1751     * decides if the current user has enough privileges to act.
1752     *
1753     * @since 4.3.0
1754     *
1755     * @return bool does a current user have enough privileges.
1756     */
1757    public function can_request() {
1758        return current_user_can( 'jetpack_admin_page' );
1759    }
1760}
1761
1762// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
1763
1764/**
1765 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
1766 *
1767 * @since 4.3.1
1768 */
1769function jetpack_do_after_gravatar_hovercards_activation() {
1770
1771    // When Gravatar Hovercards is activated, enable them automatically.
1772    update_option( 'gravatar_disable_hovercards', 'enabled' );
1773}
1774add_action( 'jetpack_activate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_activation' );
1775
1776/**
1777 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
1778 *
1779 * @since 4.3.1
1780 */
1781function jetpack_do_after_gravatar_hovercards_deactivation() {
1782
1783    // When Gravatar Hovercards is deactivated, disable them automatically.
1784    update_option( 'gravatar_disable_hovercards', 'disabled' );
1785}
1786add_action( 'jetpack_deactivate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_deactivation' );
1787
1788/**
1789 * Actions performed only when Markdown is activated through the endpoint call.
1790 *
1791 * @since 4.7.0
1792 */
1793function jetpack_do_after_markdown_activation() {
1794
1795    // When Markdown is activated, enable support for post editing automatically.
1796    update_option( 'wpcom_publish_posts_with_markdown', true );
1797}
1798add_action( 'jetpack_activate_module_markdown', 'jetpack_do_after_markdown_activation' );