PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / _inc / lib / core-api / class.jetpack-core-api-module-endpoints.php

class.jetpack-core-api-module-endpoints.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at _inc/lib/core-api/class.jetpack-core-api-module-endpoints.php

1,816 lines 57.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
8 use Automattic\Jetpack\Connection\REST_Connector;
9 use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
10 use Automattic\Jetpack\Stats\WPCOM_Stats;
11 use Automattic\Jetpack\Stats_Admin\Main as Stats_Admin_Main;
12 use Automattic\Jetpack\Status;
13 use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
14 use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit( 0 );
18 }
19
20 /**
21 * This is the base class for every Core API endpoint Jetpack uses.
22 */
23 class 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 */
188 class 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 */
355 class 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::filter_options_for_response(
457 Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( 'settings' )
458 );
459
460 if ( ! function_exists( 'is_plugin_active' ) ) {
461 require_once ABSPATH . 'wp-admin/includes/plugin.php';
462 }
463
464 $response['categories'] = get_categories( array( 'get' => 'all' ) );
465
466 foreach ( $settings as $setting => $properties ) {
467 switch ( $setting ) {
468 case 'lang_id':
469 if ( ! current_user_can( 'install_languages' ) ) {
470 // The user doesn't have caps to install language packs, so warn the client.
471 $response[ $setting ] = 'error_cap';
472 break;
473 }
474
475 $value = get_option( 'WPLANG', '' );
476 if ( empty( $value ) && defined( 'WPLANG' ) ) {
477 $value = WPLANG;
478 }
479 $response[ $setting ] = empty( $value ) ? 'en_US' : $value;
480 break;
481
482 case 'wordpress_api_key':
483 // When field is clear, return empty. Otherwise it would return "false".
484 if ( '' === get_option( 'wordpress_api_key', '' ) ) {
485 $response[ $setting ] = '';
486 } else {
487 if ( ! class_exists( 'Akismet' ) ) {
488 if ( is_readable( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
489 require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
490 }
491 }
492 $response[ $setting ] = class_exists( 'Akismet' ) ? Akismet::get_api_key() : '';
493 }
494 break;
495
496 case 'search_auto_config':
497 // Only writable.
498 $response[ $setting ] = 1;
499 break;
500
501 default:
502 $default = $settings[ $setting ]['default'] ?? false;
503 $response[ $setting ] = Jetpack_Core_Json_Api_Endpoints::cast_value( get_option( $setting, $default ), $settings[ $setting ] );
504 break;
505 }
506 }
507
508 $response['akismet'] = is_plugin_active( 'akismet/akismet.php' );
509
510 require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
511 if ( class_exists( 'Jetpack_Memberships' ) ) {
512 $response['newsletter_has_active_plan'] = count( Jetpack_Memberships::get_all_newsletter_plan_ids( false ) ) > 0;
513 }
514
515 // Make sure we are returning a consistent type
516 if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
517 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
518 }
519 $response['wpcom_newsletter_categories'] = Jetpack_Newsletter_Category_Helper::get_category_ids();
520
521 return rest_ensure_response( $response );
522 }
523
524 /**
525 * If it's a valid Jetpack module and configuration parameters have been sent, update it.
526 *
527 * @since 4.3.0
528 *
529 * @param WP_REST_Request $request {
530 * Array of parameters received by request.
531 *
532 * @type string $slug Module slug.
533 * }
534 *
535 * @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.
536 */
537 public function update_data( $request ) {
538
539 // If it's null, we're trying to update many module options from different modules.
540 if ( $request['slug'] === null ) {
541
542 // Value admitted by Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list that will make it return all module options.
543 // It will not be passed. It's just checked in this method to pass that method a string or array.
544 $request['slug'] = 'any';
545 } else {
546 if ( ! Jetpack::is_module( $request['slug'] ) ) {
547 return new WP_Error( 'not_found', esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ), array( 'status' => 404 ) );
548 }
549
550 if ( ! Jetpack::is_module_active( $request['slug'] ) ) {
551 return new WP_Error( 'inactive', esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' ), array( 'status' => 409 ) );
552 }
553 }
554
555 /*
556 * Get parameters to update the module.
557 * We cannot simply use $request->get_params() because when we registered this route,
558 * we are adding the entire output of Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list()
559 * to the current request object's params. We are interested in body of the actual request.
560 * This may be JSON:
561 */
562 $params = $request->get_json_params();
563 if ( ! is_array( $params ) ) {
564 // Or it may be standard POST key-value pairs.
565 $params = $request->get_body_params();
566 }
567
568 // Exit if no parameters were passed.
569 if ( ! is_array( $params ) ) {
570 return new WP_Error( 'missing_options', esc_html__( 'Missing options.', 'jetpack' ), array( 'status' => 404 ) );
571 }
572
573 // If $params was set via `get_body_params()` there may be some additional variables in the request that can
574 // cause validation to fail. This method verifies that each param was in fact updated and will throw a `some_updated`
575 // error if unused variables are included in the request.
576 foreach ( array_keys( $params ) as $key ) {
577 if ( is_int( $key ) || 'slug' === $key || 'context' === $key ) {
578 unset( $params[ $key ] );
579 }
580 }
581
582 // Get available module options.
583 $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list(
584 'any' === $request['slug']
585 ? $params
586 : $request['slug']
587 );
588
589 // Prepare to toggle module if needed.
590 $toggle_module = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );
591
592 // Options that are invalid or failed to update.
593 $invalid = array_keys( array_diff_key( $params, $options ) );
594 $not_updated = array();
595
596 // Remove invalid options.
597 $params = array_intersect_key( $params, $options );
598
599 // Used if response is successful. The message can be overwritten and additional data can be added here.
600 $response = array(
601 'code' => 'success',
602 'message' => esc_html__( 'The requested Jetpack data updates were successful.', 'jetpack' ),
603 );
604
605 // If there are modules to activate, activate them first so they're ready when their options are set.
606 foreach ( $params as $option => $value ) {
607 if ( 'modules' === $options[ $option ]['jp_group'] ) {
608
609 // Used if there was an error. Can be overwritten with specific error messages.
610 $error = '';
611
612 // Set to true if the module toggling was successful.
613 $updated = false;
614
615 // Check if user can toggle the module.
616 if ( $toggle_module->can_request() ) {
617
618 // Activate or deactivate the module according to the value passed.
619 $toggle_result = $value
620 ? $toggle_module->activate_module( $option )
621 : $toggle_module->deactivate_module( $option );
622
623 if (
624 is_wp_error( $toggle_result )
625 && 'already_inactive' === $toggle_result->get_error_code()
626 ) {
627
628 // If the module is already inactive, we don't fail.
629 $updated = true;
630 } elseif ( is_wp_error( $toggle_result ) ) {
631 $error = $toggle_result->get_error_message();
632 } else {
633 $updated = true;
634 }
635 } else {
636 $error = REST_Connector::get_user_permissions_error_msg();
637 }
638
639 // The module was not toggled.
640 if ( ! $updated ) {
641 $not_updated[ $option ] = $error;
642 }
643
644 if ( $updated ) {
645 // Return the module state.
646 $response[ $option ] = $value;
647 }
648
649 // Remove module from list so we don't go through it again.
650 unset( $params[ $option ] );
651 }
652 }
653
654 if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
655 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
656 }
657
658 foreach ( $params as $option => $value ) {
659
660 // Used if there was an error. Can be overwritten with specific error messages.
661 $error = '';
662
663 // Set to true if the option update was successful.
664 $updated = false;
665
666 // Get option attributes, including the group it belongs to.
667 $option_attrs = $options[ $option ];
668
669 // Everything outside the Post by Email group requires the admin capability.
670 if ( 'post-by-email' !== $option_attrs['jp_group'] && ! current_user_can( 'jetpack_configure_modules' ) ) {
671 $not_updated[ $option ] = REST_Connector::get_user_permissions_error_msg();
672 continue;
673 }
674
675 // If this is a module option and the related module isn't active for any reason, continue with the next one.
676 if ( 'settings' !== $option_attrs['jp_group'] ) {
677 if ( ! Jetpack::is_module( $option_attrs['jp_group'] ) ) {
678 $not_updated[ $option ] = esc_html__( 'The requested Jetpack module was not found.', 'jetpack' );
679 continue;
680 }
681
682 if (
683 'any' !== $request['slug']
684 && ! Jetpack::is_module_active( $option_attrs['jp_group'] )
685 ) {
686
687 // We only take note of skipped options when updating one module.
688 $not_updated[ $option ] = esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' );
689 continue;
690 }
691 }
692
693 // Properly cast value based on its type defined in endpoint accepted args.
694 $value = Jetpack_Core_Json_Api_Endpoints::cast_value( $value, $option_attrs );
695
696 switch ( $option ) {
697 case 'lang_id':
698 if ( ! current_user_can( 'install_languages' ) ) {
699 // We can't affect this setting.
700 $updated = false;
701 break;
702 }
703
704 if ( 'en_US' === $value || empty( $value ) ) {
705 return delete_option( 'WPLANG' );
706 }
707
708 if ( ! function_exists( 'request_filesystem_credentials' ) ) {
709 require_once ABSPATH . 'wp-admin/includes/file.php';
710 }
711
712 if ( ! function_exists( 'wp_download_language_pack' ) ) {
713 require_once ABSPATH . 'wp-admin/includes/translation-install.php';
714 }
715
716 // `wp_download_language_pack` only tries to download packs if they're not already available.
717 $language = wp_download_language_pack( $value );
718 if ( false === $language ) {
719 // The language pack download failed.
720 $updated = false;
721 break;
722 }
723 $updated = get_option( 'WPLANG' ) === $language ? true : update_option( 'WPLANG', $language );
724 break;
725
726 case 'monitor_receive_notifications':
727 if ( ! class_exists( 'Jetpack_Monitor' ) ) {
728 $updated = false;
729 break;
730 }
731
732 $monitor = new Jetpack_Monitor();
733
734 // If we got true as response, consider it done.
735 $updated = true === $monitor->update_option_receive_jetpack_monitor_notification( $value );
736 break;
737
738 case 'post_by_email_address':
739 if ( ! class_exists( 'Jetpack_Post_By_Email' ) ) {
740 $updated = false;
741 break;
742 }
743
744 $result = Jetpack_Post_By_Email::init()->process_api_request( $value );
745
746 // If we got an email address (create or regenerate) or 1 (delete), consider it done.
747 if ( is_string( $result ) && preg_match( '/[a-z0-9]+@post.wordpress.com/', $result ) ) {
748 $response[ $option ] = $result;
749 $updated = true;
750 } elseif ( 1 == $result ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
751 $updated = true;
752 } elseif ( is_array( $result ) && isset( $result['message'] ) ) {
753 $error = $result['message'];
754 }
755 break;
756
757 case 'jetpack_protect_key':
758 $brute_force_protection = Brute_Force_Protection::instance();
759 if ( 'create' === $value ) {
760 $result = $brute_force_protection->get_protect_key();
761 } else {
762 $result = false;
763 }
764
765 // If we got one of Protect keys, consider it done.
766 if ( is_string( $result ) && preg_match( '/[a-z0-9]{40,}/i', $result ) ) {
767 $response[ $option ] = $result;
768 $updated = true;
769 }
770 break;
771
772 case 'jetpack_protect_global_whitelist':
773 $updated = Brute_Force_Protection_Shared_Functions::save_allow_list( explode( PHP_EOL, str_replace( array( ' ', ',' ), array( '', "\n" ), $value ) ) );
774
775 if ( is_wp_error( $updated ) ) {
776 $error = $updated->get_error_message();
777 }
778 break;
779
780 case 'show_headline':
781 case 'show_thumbnails':
782 $grouped_options_current = (array) Jetpack_Options::get_option( 'relatedposts' );
783 $grouped_options = $grouped_options_current;
784 $grouped_options[ $option ] = $value;
785
786 // If option value was the same, consider it done.
787 $updated = $grouped_options_current !== $grouped_options ? Jetpack_Options::update_option( 'relatedposts', $grouped_options ) : true;
788 break;
789
790 case 'search_auto_config':
791 if ( ! $value ) {
792 // Skip execution if no value is specified.
793 $updated = true;
794 } else {
795 $plan = new Automattic\Jetpack\Search\Plan();
796 if ( ! $plan->supports_instant_search() ) {
797 $updated = new WP_Error( 'instant_search_not_supported', 'Instant Search is not supported by this site', array( 'status' => 400 ) );
798 $error = $updated->get_error_message();
799 } elseif ( ! Automattic\Jetpack\Search\Options::is_instant_enabled() ) {
800 $updated = new WP_Error( 'instant_search_disabled', 'Instant Search is disabled', array( 'status' => 400 ) );
801 $error = $updated->get_error_message();
802 } else {
803 $blog_id = Automattic\Jetpack\Search\Helper::get_wpcom_site_id();
804 $instance = Automattic\Jetpack\Search\Instant_Search::instance( $blog_id );
805 $instance->auto_config_search();
806 $updated = true;
807 }
808 }
809 break;
810
811 case 'google':
812 case 'bing':
813 case 'pinterest':
814 case 'yandex':
815 case 'facebook':
816 $grouped_options_current = (array) get_option( 'verification_services_codes' );
817 $grouped_options = $grouped_options_current;
818
819 // Extracts the content attribute from the HTML meta tag if needed.
820 if ( preg_match( '#.*<meta name="(?:[^"]+)" content="([^"]+)" />.*#i', $value, $matches ) ) {
821 $grouped_options[ $option ] = $matches[1];
822 } else {
823 $grouped_options[ $option ] = $value;
824 }
825
826 // If option value was the same, consider it done.
827 $updated = $grouped_options_current !== $grouped_options
828 ? update_option( 'verification_services_codes', $grouped_options )
829 : true;
830 break;
831
832 case Jetpack_SEO_Utils::FRONT_PAGE_META_OPTION:
833 Jetpack_SEO_Utils::update_front_page_meta_description( $value );
834 $response[ $option ] = Jetpack_SEO_Utils::get_front_page_meta_description();
835 // The helper returns an empty string for a successful clear or
836 // same-value write, so use its authoritative getter for the response
837 // and treat every valid request reaching this switch as handled.
838 $updated = true;
839 break;
840
841 case 'sharing_services':
842 if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
843 break;
844 }
845
846 $sharer = new Sharing_Service();
847
848 // If option value was the same, consider it done.
849 $updated = $value !== $sharer->get_blog_services()
850 ? $sharer->set_blog_services( $value['visible'], $value['hidden'] )
851 : true;
852 break;
853
854 case 'button_style':
855 case 'sharing_label':
856 case 'show':
857 if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
858 break;
859 }
860
861 $sharer = new Sharing_Service();
862 $grouped_options = $sharer->get_global_options();
863 $grouped_options[ $option ] = $value;
864 $updated = $sharer->set_global_options( $grouped_options );
865 break;
866
867 case 'custom':
868 if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
869 break;
870 }
871
872 $sharer = new Sharing_Service();
873 $updated = $sharer->new_service( stripslashes( $value['sharing_name'] ), stripslashes( $value['sharing_url'] ), stripslashes( $value['sharing_icon'] ) );
874
875 // Return new custom service.
876 $response[ $option ] = $updated;
877 break;
878
879 case 'sharing_delete_service':
880 if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
881 break;
882 }
883
884 $sharer = new Sharing_Service();
885 $updated = $sharer->delete_service( $value );
886 break;
887
888 case 'jetpack-twitter-cards-site-tag':
889 $value = trim( ltrim( wp_strip_all_tags( $value ), '@' ) );
890 $updated = get_option( $option ) !== $value ? update_option( $option, $value ) : true;
891 break;
892
893 case 'admin_bar':
894 case 'roles':
895 case 'count_roles':
896 case 'blog_id':
897 case 'do_not_track':
898 case 'version':
899 case 'collapse_nudges':
900 $grouped_options_current = (array) get_option( 'stats_options' );
901 $grouped_options = $grouped_options_current;
902 $grouped_options[ $option ] = $value;
903
904 // If option value was the same, consider it done.
905 $updated = $grouped_options_current !== $grouped_options
906 ? update_option( 'stats_options', $grouped_options )
907 : true;
908 break;
909
910 case 'enable_odyssey_stats':
911 $updated = Stats_Admin_Main::update_new_stats_status( $value );
912
913 break;
914
915 case 'akismet_show_user_comments_approved':
916 // Save Akismet option '1' or '0' like it's done in akismet/class.akismet-admin.php.
917 $updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
918 ? update_option( $option, $value ? '1' : '0' )
919 : true;
920 break;
921
922 case 'wordpress_api_key':
923 if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
924 $error = esc_html__( 'Please install Akismet.', 'jetpack' );
925 $updated = false;
926 break;
927 }
928
929 if ( ! defined( 'AKISMET_VERSION' ) ) {
930 $error = esc_html__( 'Please activate Akismet.', 'jetpack' );
931 $updated = false;
932 break;
933 }
934
935 // Allow to clear the API key field.
936 if ( '' === $value ) {
937 $updated = get_option( $option ) !== $value
938 ? update_option( $option, $value )
939 : true;
940 break;
941 }
942
943 require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
944 require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
945
946 if ( class_exists( 'Akismet_Admin' ) && method_exists( 'Akismet_Admin', 'save_key' ) ) {
947 if ( Akismet::verify_key( $value ) === 'valid' ) {
948 $akismet_user = Akismet_Admin::get_akismet_user( $value );
949 if ( $akismet_user ) {
950 if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ), true ) ) {
951 $updated = get_option( $option ) !== $value
952 ? update_option( $option, $value )
953 : true;
954 break;
955 } else {
956 $error = esc_html__( "Akismet user status doesn't allow to update the key", 'jetpack' );
957 }
958 } else {
959 $error = esc_html__( 'Invalid Akismet user', 'jetpack' );
960 }
961 } else {
962 $error = esc_html__( 'Invalid Akismet key', 'jetpack' );
963 }
964 } else {
965 $error = esc_html__( 'Akismet is not installed or active', 'jetpack' );
966 }
967 $updated = false;
968 break;
969
970 case 'google_analytics_tracking_id':
971 $grouped_options_current = (array) get_option( 'jetpack_wga' );
972 $grouped_options = $grouped_options_current;
973 $grouped_options['code'] = $value;
974
975 // If option value was the same, consider it done.
976 $updated = $grouped_options_current !== $grouped_options
977 ? update_option( 'jetpack_wga', $grouped_options )
978 : true;
979 break;
980
981 case 'dismiss_empty_stats_card':
982 case 'dismiss_dash_backup_getting_started':
983 case 'dismiss_dash_agencies_learn_more':
984 // If option value was the same, consider it done.
985 $updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support bools or strings saved by update_option.
986 ? update_option( $option, (bool) $value )
987 : true;
988 break;
989
990 case 'jetpack_subscriptions_reply_to':
991 // If option value was the same, consider it done.
992 require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
993 $sub_value = Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value )
994 ? $value
995 : Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to;
996
997 $updated = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
998 break;
999 case 'jetpack_subscriptions_from_name':
1000 // If option value was the same, consider it done.
1001 $sub_value = sanitize_text_field( $value );
1002 $updated = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
1003 break;
1004
1005 case 'stb_enabled':
1006 case 'stc_enabled':
1007 case 'sm_enabled':
1008 case 'jetpack_subscribe_overlay_enabled':
1009 case 'jetpack_subscribe_floating_button_enabled':
1010 case 'wpcom_newsletter_categories_enabled':
1011 case 'wpcom_featured_image_in_email':
1012 case 'jetpack_gravatar_in_email':
1013 case 'jetpack_author_in_email':
1014 case 'jetpack_post_date_in_email':
1015 case 'wpcom_subscription_emails_use_excerpt':
1016 case 'jetpack_subscriptions_subscribe_post_end_enabled':
1017 case 'jetpack_subscriptions_login_navigation_enabled':
1018 case 'jetpack_subscriptions_subscribe_navigation_enabled':
1019 // Convert the false value to 0. This allows the option to be updated if it doesn't exist yet.
1020 $sub_value = $value ? $value : 0;
1021 $updated = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
1022 break;
1023
1024 case 'jetpack_blocks_disabled':
1025 $updated = (bool) get_option( $option ) !== (bool) $value ? update_option( $option, (bool) $value ) : true;
1026 break;
1027
1028 case 'subscription_options':
1029 if ( ! is_array( $value ) ) {
1030 break;
1031 }
1032
1033 $allowed_keys = array( 'invitation', 'comment_follow', 'welcome', 'subscribe_modal_heading', 'free_tier_description', 'hide_free_tier' );
1034 $filtered_value = array_filter(
1035 $value,
1036 function ( $key ) use ( $allowed_keys ) {
1037 return in_array( $key, $allowed_keys, true );
1038 },
1039 ARRAY_FILTER_USE_KEY
1040 );
1041
1042 if ( empty( $filtered_value ) ) {
1043 break;
1044 }
1045
1046 // `hide_free_tier` is a boolean flag, so pull it out before the HTML
1047 // sanitization below (which expects strings). Sanitize it with
1048 // rest_sanitize_boolean() so stringy booleans (e.g. "false", "0")
1049 // are interpreted correctly rather than being treated as truthy by a
1050 // plain `! empty()`.
1051 $has_hide_free_tier = array_key_exists( 'hide_free_tier', $filtered_value );
1052 $hide_free_tier = $has_hide_free_tier && rest_sanitize_boolean( $filtered_value['hide_free_tier'] );
1053 unset( $filtered_value['hide_free_tier'] );
1054
1055 array_walk_recursive(
1056 $filtered_value,
1057 function ( &$value ) {
1058 $value = wp_kses(
1059 $value,
1060 array(
1061 'ul' => array(),
1062 'li' => array(),
1063 'p' => array(),
1064 'strong' => array(),
1065 'ol' => array(),
1066 'em' => array(),
1067 'a' => array(
1068 'href' => array(),
1069 ),
1070 )
1071 );
1072 }
1073 );
1074
1075 // Normalize whitespace-only `subscribe_modal_heading` input to empty so
1076 // the modal template's `empty()` fallback fires. PHP's `empty()` treats
1077 // `" "` as non-empty, which would otherwise render a blank heading.
1078 if ( isset( $filtered_value['subscribe_modal_heading'] ) ) {
1079 $filtered_value['subscribe_modal_heading'] = trim( $filtered_value['subscribe_modal_heading'] );
1080 }
1081
1082 // The free tier description is stored as plain markdown source, so strip
1083 // all HTML and cap its length to match the paid-tier description field.
1084 // WordPress core guarantees mb_substr() (polyfilled in wp-includes/compat.php
1085 // when the mbstring extension is unavailable), so it's safe to use directly.
1086 // A JSON payload could supply a non-scalar (array/object) for this field,
1087 // which would fatal in wp_kses()/mb_substr() on PHP 8+, so drop invalid values.
1088 if ( isset( $filtered_value['free_tier_description'] ) ) {
1089 if ( is_scalar( $filtered_value['free_tier_description'] ) ) {
1090 $filtered_value['free_tier_description'] = mb_substr( wp_kses( (string) $filtered_value['free_tier_description'], array() ), 0, 500 );
1091 } else {
1092 unset( $filtered_value['free_tier_description'] );
1093 }
1094 }
1095
1096 if ( $has_hide_free_tier ) {
1097 $filtered_value['hide_free_tier'] = $hide_free_tier;
1098 }
1099
1100 $old_subscription_options = get_option( 'subscription_options' );
1101 if ( ! is_array( $old_subscription_options ) ) {
1102 $old_subscription_options = array();
1103 }
1104 $new_subscription_options = array_merge( $old_subscription_options, $filtered_value );
1105 $updated = true;
1106
1107 if ( serialize( $old_subscription_options ) === serialize( $new_subscription_options ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1108 break; // This prevents the option update to fail when the values are the same.
1109 }
1110
1111 if ( ! update_option( $option, $new_subscription_options ) ) {
1112 $updated = false;
1113 $error = esc_html__( 'Subscription Options failed to process.', 'jetpack' );
1114 }
1115 break;
1116
1117 case Jetpack_Newsletter_Category_Helper::NEWSLETTER_CATEGORIES_OPTION:
1118 if ( ! is_array( $value ) || empty( $value ) ) {
1119 $updated = true;
1120 break;
1121 }
1122
1123 // If we are already current, do nothing
1124 $current_value = Jetpack_Newsletter_Category_Helper::get_category_ids();
1125 if ( $value === $current_value ) {
1126 $updated = true;
1127 break;
1128 }
1129
1130 if ( Jetpack_Newsletter_Category_Helper::save_category_ids( $value ) ) {
1131 $updated = true;
1132 } else {
1133 $updated = false;
1134 $error = esc_html__( 'Newsletter category did not update.', 'jetpack' );
1135 }
1136
1137 break;
1138
1139 default:
1140 // Boolean values are stored as 1 or 0.
1141 if ( isset( $options[ $option ]['type'] ) && 'boolean' === $options[ $option ]['type'] ) {
1142 $value = (int) $value;
1143 }
1144
1145 // If option value was the same as it's current value, or it's default, consider it done.
1146 $default = $options[ $option ]['default'] ?? false;
1147 $updated = get_option( $option, $default ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support scalars or strings saved by update_option.
1148 ? update_option( $option, $value )
1149 : true;
1150 break;
1151 }
1152
1153 // The option was not updated.
1154 if ( ! $updated ) {
1155 $not_updated[ $option ] = $error;
1156 }
1157 }
1158
1159 if ( empty( $invalid ) && empty( $not_updated ) ) {
1160 // The option was updated.
1161 return rest_ensure_response( $response );
1162 } else {
1163 $invalid_count = count( $invalid );
1164 $not_updated_count = count( $not_updated );
1165 $error = '';
1166 if ( $invalid_count > 0 ) {
1167 $error = sprintf(
1168 /* Translators: the plural variable is a comma-separated list. Example: dog, cat, bird. */
1169 _n( 'Invalid option: %s.', 'Invalid options: %s.', $invalid_count, 'jetpack' ),
1170 implode( ', ', $invalid )
1171 );
1172 }
1173 if ( $not_updated_count > 0 ) {
1174 $not_updated_messages = array();
1175 foreach ( $not_updated as $not_updated_option => $not_updated_message ) {
1176 if ( ! empty( $not_updated_message ) ) {
1177 $not_updated_messages[] = sprintf(
1178 /* Translators: the first variable is a module option or slug, or setting. The second is the error message . */
1179 __( '%1$s: %2$s', 'jetpack' ),
1180 $not_updated_option,
1181 $not_updated_message
1182 );
1183 }
1184 }
1185 if ( ! empty( $error ) ) {
1186 $error .= ' ';
1187 }
1188 if ( ! empty( $not_updated_messages ) ) {
1189 $error .= ' ' . implode( '. ', $not_updated_messages );
1190 }
1191 }
1192 // There was an error because some options were updated but others were invalid or failed to update.
1193 return new WP_Error( 'some_updated', esc_html( $error ), array( 'status' => 400 ) );
1194 }
1195 }
1196
1197 /**
1198 * Perform tasks in the site based on onboarding choices.
1199 *
1200 * @since 5.4.0
1201 *
1202 * @deprecated since 13.9
1203 *
1204 * @param array $data Onboarding choices made by user.
1205 *
1206 * @return string Result of onboarding processing and, if there is one, an error message.
1207 */
1208 private function process_onboarding( $data ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1209 _deprecated_function( __METHOD__, '13.9' );
1210 return '';
1211 }
1212
1213 /**
1214 * Add or update Business Address widget.
1215 *
1216 * @deprecated since 13.9
1217 *
1218 * @param array $address Array of business address fields.
1219 *
1220 * @return WP_Error|true True if the data was saved correctly.
1221 */
1222 private static function handle_business_address( $address ) {
1223 _deprecated_function( __METHOD__, '13.9' );
1224 $first_sidebar = Jetpack_Widgets::get_first_sidebar();
1225
1226 $widgets_module_active = Jetpack::is_module_active( 'widgets' );
1227 if ( ! $widgets_module_active ) {
1228 $widgets_module_active = Jetpack::activate_module( 'widgets', false, false );
1229 }
1230 if ( ! $widgets_module_active ) {
1231 return new WP_Error( 'module_activation_failed', 'Failed to activate the widgets module.', 400 );
1232 }
1233
1234 if ( $first_sidebar ) {
1235 $title = isset( $address['name'] ) ? sanitize_text_field( $address['name'] ) : '';
1236 $street = isset( $address['street'] ) ? sanitize_text_field( $address['street'] ) : '';
1237 $city = isset( $address['city'] ) ? sanitize_text_field( $address['city'] ) : '';
1238 $state = isset( $address['state'] ) ? sanitize_text_field( $address['state'] ) : '';
1239 $zip = isset( $address['zip'] ) ? sanitize_text_field( $address['zip'] ) : '';
1240 $country = isset( $address['country'] ) ? sanitize_text_field( $address['country'] ) : '';
1241
1242 $full_address = implode( ' ', array_filter( array( $street, $city, $state, $zip, $country ) ) );
1243
1244 $widget_options = array(
1245 'title' => $title,
1246 'address' => $full_address,
1247 'phone' => '',
1248 'hours' => '',
1249 'showmap' => false,
1250 'email' => '',
1251 );
1252
1253 $widget_updated = '';
1254 if ( ! self::has_business_address_widget( $first_sidebar ) ) {
1255 $widget_updated = Jetpack_Widgets::insert_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
1256 } else {
1257 $widget_updated = Jetpack_Widgets::update_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
1258 }
1259 if ( is_wp_error( $widget_updated ) ) {
1260 return new WP_Error( 'widget_update_failed', 'Widget could not be updated.', 400 );
1261 }
1262
1263 $address_save = array(
1264 'name' => $title,
1265 'street' => $street,
1266 'city' => $city,
1267 'state' => $state,
1268 'zip' => $zip,
1269 'country' => $country,
1270 );
1271 update_option( 'jpo_business_address', $address_save );
1272 return true;
1273 }
1274
1275 // No sidebar to place the widget.
1276 return new WP_Error( 'sidebar_not_found', 'No sidebar.', 400 );
1277 }
1278
1279 /**
1280 * Check whether "Contact Info & Map" widget is present in a given sidebar.
1281 *
1282 * @param string $sidebar ID of the sidebar to which the widget will be added.
1283 *
1284 * @return bool Whether the widget is present in a given sidebar.
1285 */
1286 private static function has_business_address_widget( $sidebar ) {
1287 $sidebars_widgets = get_option( 'sidebars_widgets', array() );
1288 if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
1289 return false;
1290 }
1291 foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
1292 if ( str_contains( $widget, 'widget_contact_info' ) ) {
1293 return true;
1294 }
1295 }
1296 return false;
1297 }
1298
1299 /**
1300 * Check if user is allowed to perform the update.
1301 *
1302 * @since 4.3.0
1303 *
1304 * @param WP_REST_Request $request The request sent to the WP REST API.
1305 *
1306 * @return bool
1307 */
1308 public function can_request( $request ) {
1309 if ( 'GET' === $request->get_method() ) {
1310 return current_user_can( 'jetpack_admin_page' );
1311 } else {
1312 $module = Jetpack_Core_Json_Api_Endpoints::get_module_requested();
1313 if ( empty( $module ) ) {
1314 $params = $request->get_json_params();
1315 if ( ! is_array( $params ) ) {
1316 $params = $request->get_body_params();
1317 }
1318 $options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( $params );
1319
1320 // The Post by Email gate applies only when the request contains nothing else.
1321 $groups = array_values( array_unique( array_column( $options, 'jp_group' ) ) );
1322 if ( array( 'post-by-email' ) === $groups ) {
1323 $module = 'post-by-email';
1324 }
1325 }
1326 // User is trying to create, regenerate or delete its PbE.
1327 if ( 'post-by-email' === $module ) {
1328 return current_user_can( 'edit_posts' ) && current_user_can( 'jetpack_admin_page' );
1329 }
1330 return current_user_can( 'jetpack_configure_modules' );
1331 }
1332 }
1333 }
1334
1335 /**
1336 * Get detailed data from a specific module.
1337 *
1338 * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
1339 */
1340 class Jetpack_Core_API_Module_Data_Endpoint {
1341
1342 /**
1343 * Process request and return different data based on the module we are interested in.
1344 *
1345 * @param WP_REST_Request $request WP API request.
1346 *
1347 * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
1348 */
1349 public function process( $request ) {
1350 switch ( $request['slug'] ) {
1351 case 'protect':
1352 return $this->get_protect_data();
1353 case 'stats':
1354 return $this->get_stats_data( $request );
1355 case 'akismet':
1356 return $this->get_akismet_data();
1357 case 'monitor':
1358 return $this->get_monitor_data();
1359 case 'verification-tools':
1360 return $this->get_verification_tools_data();
1361 case 'vaultpress':
1362 return $this->get_vaultpress_data();
1363 }
1364 }
1365
1366 /**
1367 * Decide against which service to check the key.
1368 *
1369 * @since 4.8.0
1370 *
1371 * @param WP_REST_Request $request WP API request.
1372 *
1373 * @return bool
1374 */
1375 public function key_check( $request ) {
1376 switch ( $request['service'] ) {
1377 case 'akismet':
1378 $params = $request->get_json_params();
1379 if ( isset( $params['api_key'] ) && ! empty( $params['api_key'] ) ) {
1380 return $this->check_akismet_key( $params['api_key'] );
1381 }
1382 return $this->check_akismet_key();
1383 }
1384 return false;
1385 }
1386
1387 /**
1388 * Get number of blocked intrusion attempts.
1389 *
1390 * @since 4.3.0
1391 *
1392 * @return mixed|WP_Error Number of blocked attempts if protection is enabled. Otherwise, a WP_Error instance with the corresponding error.
1393 */
1394 public function get_protect_data() {
1395 if ( Jetpack::is_module_active( 'protect' ) ) {
1396 return (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 );
1397 }
1398
1399 return new WP_Error(
1400 'not_active',
1401 esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1402 array( 'status' => 404 )
1403 );
1404 }
1405
1406 /**
1407 * Get number of spam messages blocked by Akismet.
1408 *
1409 * @since 4.3.0
1410 *
1411 * @return int|string Number of spam blocked by Akismet. Otherwise, an error message.
1412 */
1413 public function get_akismet_data() {
1414 $akismet_status = $this->akismet_is_active_and_registered();
1415 if ( ! is_wp_error( $akismet_status ) ) {
1416 return (int) get_option( 'akismet_spam_count', 0 );
1417 } else {
1418 return $akismet_status->get_error_code();
1419 }
1420 }
1421
1422 /**
1423 * Verify the Akismet API key.
1424 *
1425 * @since 4.8.0
1426 *
1427 * @param string $api_key Optional API key to check.
1428 *
1429 * @return array Information about the key. 'validKey' is true if key is valid, false otherwise.
1430 */
1431 public function check_akismet_key( $api_key = '' ) {
1432 $akismet_status = $this->akismet_class_exists();
1433 if ( is_wp_error( $akismet_status ) ) {
1434 return rest_ensure_response(
1435 array(
1436 'validKey' => false,
1437 'invalidKeyCode' => $akismet_status->get_error_code(),
1438 'invalidKeyMessage' => $akismet_status->get_error_message(),
1439 )
1440 );
1441 }
1442
1443 $key_status = Akismet::check_key_status( empty( $api_key ) ? Akismet::get_api_key() : $api_key );
1444
1445 if ( ! $key_status || 'invalid' === $key_status || 'failed' === $key_status ) {
1446 return rest_ensure_response(
1447 array(
1448 'validKey' => false,
1449 'invalidKeyCode' => 'invalid_key',
1450 'invalidKeyMessage' => esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ),
1451 )
1452 );
1453 }
1454
1455 return rest_ensure_response(
1456 array(
1457 'validKey' => isset( $key_status[1] ) && 'valid' === $key_status[1],
1458 )
1459 );
1460 }
1461
1462 /**
1463 * Check if Akismet class file exists and if class is loaded.
1464 *
1465 * @since 4.8.0
1466 *
1467 * @return bool|WP_Error Returns true if class file exists and class is loaded, WP_Error otherwise.
1468 */
1469 private function akismet_class_exists() {
1470 if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
1471 return new WP_Error( 'not_installed', esc_html__( 'Please install Akismet.', 'jetpack' ), array( 'status' => 400 ) );
1472 }
1473
1474 if ( ! class_exists( 'Akismet' ) ) {
1475 return new WP_Error( 'not_active', esc_html__( 'Please activate Akismet.', 'jetpack' ), array( 'status' => 400 ) );
1476 }
1477
1478 return true;
1479 }
1480
1481 /**
1482 * Is Akismet registered and active?
1483 *
1484 * @since 4.3.0
1485 *
1486 * @return bool|WP_Error True if Akismet is active and registered. Otherwise, a WP_Error instance with the corresponding error.
1487 */
1488 private function akismet_is_active_and_registered() {
1489 $akismet_exists = $this->akismet_class_exists();
1490 if ( is_wp_error( $akismet_exists ) ) {
1491 return $akismet_exists;
1492 }
1493
1494 // What about if Akismet is put in a sub-directory or maybe in mu-plugins?
1495 require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
1496 require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
1497 $akismet_key = Akismet::verify_key( Akismet::get_api_key() );
1498
1499 if ( ! $akismet_key || 'invalid' === $akismet_key || 'failed' === $akismet_key ) {
1500 return new WP_Error( 'invalid_key', esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ), array( 'status' => 400 ) );
1501 }
1502
1503 return true;
1504 }
1505
1506 /**
1507 * Get stats data for this site
1508 *
1509 * @since 4.1.0
1510 *
1511 * @param WP_REST_Request $request {
1512 * Array of parameters received by request.
1513 *
1514 * @type string $date Date range to restrict results to.
1515 * }
1516 *
1517 * @return WP_Error|WP_HTTP_Response|WP_REST_Response Stats information relayed from WordPress.com.
1518 */
1519 public function get_stats_data( WP_REST_Request $request ) {
1520 // Get parameters to fetch Stats data.
1521 $range = $request->get_param( 'range' );
1522
1523 // If no parameters were passed.
1524 if (
1525 empty( $range )
1526 || ! in_array( $range, array( 'day', 'week', 'month' ), true )
1527 ) {
1528 $range = 'day';
1529 }
1530
1531 $wpcom_stats = new WPCOM_Stats();
1532 switch ( $range ) {
1533
1534 // This is always called first on page load.
1535 case 'day':
1536 $initial_stats = $wpcom_stats->convert_stats_array_to_object( $wpcom_stats->get_stats() );
1537 return rest_ensure_response(
1538 array(
1539 'general' => $initial_stats,
1540
1541 // Build data for 'day' as if it was $wpcom_stats ->get_visits( array( 'unit' => 'day, 'quantity' => 30).
1542 'day' => $initial_stats->visits ?? array(),
1543 )
1544 );
1545 case 'week':
1546 return rest_ensure_response(
1547 array(
1548 'week' => $wpcom_stats->convert_stats_array_to_object(
1549 $wpcom_stats->get_visits(
1550 array(
1551 'unit' => 'week',
1552 'quantity' => 14,
1553 )
1554 )
1555 ),
1556 )
1557 );
1558 case 'month':
1559 return rest_ensure_response(
1560 array(
1561 'month' => $wpcom_stats->convert_stats_array_to_object(
1562 $wpcom_stats->get_visits(
1563 array(
1564 'unit' => 'month',
1565 'quantity' => 12,
1566 )
1567 )
1568 ),
1569 )
1570 );
1571 }
1572 }
1573
1574 /**
1575 * Get date of last downtime.
1576 *
1577 * @since 4.3.0
1578 *
1579 * @return mixed|WP_Error Number of days since last downtime. Otherwise, a WP_Error instance with the corresponding error.
1580 */
1581 public function get_monitor_data() {
1582 if ( ! Jetpack::is_module_active( 'monitor' ) ) {
1583 return new WP_Error(
1584 'not_active',
1585 esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1586 array( 'status' => 404 )
1587 );
1588 }
1589
1590 $monitor = new Jetpack_Monitor();
1591 $last_downtime = $monitor->monitor_get_last_downtime();
1592 if ( is_wp_error( $last_downtime ) ) {
1593 return $last_downtime;
1594 } elseif ( false === strtotime( $last_downtime ) ) {
1595 return rest_ensure_response(
1596 array(
1597 'code' => 'success',
1598 'date' => null,
1599 )
1600 );
1601 } else {
1602 return rest_ensure_response(
1603 array(
1604 'code' => 'success',
1605 'date' => human_time_diff( strtotime( $last_downtime ), strtotime( 'now' ) ),
1606 )
1607 );
1608 }
1609 }
1610
1611 /**
1612 * Get services that this site is verified with.
1613 *
1614 * @since 4.3.0
1615 *
1616 * @return mixed|WP_Error List of services that verified this site. Otherwise, a WP_Error instance with the corresponding error.
1617 */
1618 public function get_verification_tools_data() {
1619 if ( ! Jetpack::is_module_active( 'verification-tools' ) ) {
1620 return new WP_Error(
1621 'not_active',
1622 esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1623 array( 'status' => 404 )
1624 );
1625 }
1626
1627 $verification_services_codes = get_option( 'verification_services_codes' );
1628 if (
1629 ! is_array( $verification_services_codes )
1630 || empty( $verification_services_codes )
1631 ) {
1632 return new WP_Error(
1633 'empty',
1634 esc_html__( 'Site not verified with any service.', 'jetpack' ),
1635 array( 'status' => 404 )
1636 );
1637 }
1638
1639 $services = array();
1640 foreach ( jetpack_verification_services() as $name => $service ) {
1641 if ( is_array( $service ) && ! empty( $verification_services_codes[ $name ] ) ) {
1642 switch ( $name ) {
1643 case 'google':
1644 $services[] = 'Google';
1645 break;
1646 case 'bing':
1647 $services[] = 'Bing';
1648 break;
1649 case 'pinterest':
1650 $services[] = 'Pinterest';
1651 break;
1652 case 'yandex':
1653 $services[] = 'Yandex';
1654 break;
1655 case 'facebook':
1656 $services[] = 'Facebook';
1657 break;
1658 }
1659 }
1660 }
1661
1662 if ( empty( $services ) ) {
1663 return new WP_Error(
1664 'empty',
1665 esc_html__( 'Site not verified with any service.', 'jetpack' ),
1666 array( 'status' => 404 )
1667 );
1668 }
1669
1670 if ( 2 > count( $services ) ) {
1671 $message = esc_html(
1672 sprintf(
1673 /* translators: %s is a service name like Google, Bing, Pinterest, etc. */
1674 __( 'Your site is verified with %s.', 'jetpack' ),
1675 $services[0]
1676 )
1677 );
1678 } else {
1679 $copy_services = $services;
1680 $last = count( $copy_services ) - 1;
1681 $last_service = $copy_services[ $last ];
1682 unset( $copy_services[ $last ] );
1683 $message = esc_html(
1684 sprintf(
1685 /* translators: %1$s is a comma-separated list of services, and %2$s is a single service name like Google, Bing, Pinterest, etc. */
1686 __( 'Your site is verified with %1$s and %2$s.', 'jetpack' ),
1687 implode( ', ', $copy_services ),
1688 $last_service
1689 )
1690 );
1691 }
1692
1693 return rest_ensure_response(
1694 array(
1695 'code' => 'success',
1696 'message' => $message,
1697 'services' => $services,
1698 )
1699 );
1700 }
1701
1702 /**
1703 * Get VaultPress site data including, among other things, the date of the last backup if it was completed.
1704 *
1705 * @since 4.3.0
1706 *
1707 * @return mixed|WP_Error VaultPress site data. Otherwise, a WP_Error instance with the corresponding error.
1708 */
1709 public function get_vaultpress_data() {
1710 if ( ! class_exists( 'VaultPress' ) ) {
1711 return new WP_Error(
1712 'not_active',
1713 esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
1714 array( 'status' => 404 )
1715 );
1716 }
1717
1718 $vaultpress = new VaultPress();
1719 if ( ! $vaultpress->is_registered() ) {
1720 return rest_ensure_response(
1721 array(
1722 'code' => 'not_registered',
1723 'message' => esc_html__( 'You need to register for VaultPress.', 'jetpack' ),
1724 )
1725 );
1726 }
1727
1728 $data = json_decode( base64_decode( $vaultpress->contact_service( 'plugin_data' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1729 if ( false === $data ) {
1730 return rest_ensure_response(
1731 array(
1732 'code' => 'not_registered',
1733 'message' => esc_html__( 'Could not connect to VaultPress.', 'jetpack' ),
1734 )
1735 );
1736 } elseif ( is_wp_error( $data ) || ! isset( $data->backups->last_backup ) ) {
1737 return $data;
1738 } elseif ( empty( $data->backups->last_backup ) ) {
1739 return rest_ensure_response(
1740 array(
1741 'code' => 'success',
1742 'message' => esc_html__( 'VaultPress is active and will back up your site soon.', 'jetpack' ),
1743 'data' => $data,
1744 )
1745 );
1746 } else {
1747 return rest_ensure_response(
1748 array(
1749 'code' => 'success',
1750 'message' => esc_html(
1751 sprintf(
1752 /* translators: placeholder is a unit of time (1 hour, 5 days, ...) */
1753 esc_html__( 'Your site was successfully backed up %s ago.', 'jetpack' ),
1754 human_time_diff(
1755 $data->backups->last_backup,
1756 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.
1757 )
1758 )
1759 ),
1760 'data' => $data,
1761 )
1762 );
1763 }
1764 }
1765
1766 /**
1767 * A WordPress REST API permission callback method that accepts a request object and
1768 * decides if the current user has enough privileges to act.
1769 *
1770 * @since 4.3.0
1771 *
1772 * @return bool does a current user have enough privileges.
1773 */
1774 public function can_request() {
1775 return current_user_can( 'jetpack_admin_page' );
1776 }
1777 }
1778
1779 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
1780
1781 /**
1782 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
1783 *
1784 * @since 4.3.1
1785 */
1786 function jetpack_do_after_gravatar_hovercards_activation() {
1787
1788 // When Gravatar Hovercards is activated, enable them automatically.
1789 update_option( 'gravatar_disable_hovercards', 'enabled' );
1790 }
1791 add_action( 'jetpack_activate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_activation' );
1792
1793 /**
1794 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
1795 *
1796 * @since 4.3.1
1797 */
1798 function jetpack_do_after_gravatar_hovercards_deactivation() {
1799
1800 // When Gravatar Hovercards is deactivated, disable them automatically.
1801 update_option( 'gravatar_disable_hovercards', 'disabled' );
1802 }
1803 add_action( 'jetpack_deactivate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_deactivation' );
1804
1805 /**
1806 * Actions performed only when Markdown is activated through the endpoint call.
1807 *
1808 * @since 4.7.0
1809 */
1810 function jetpack_do_after_markdown_activation() {
1811
1812 // When Markdown is activated, enable support for post editing automatically.
1813 update_option( 'wpcom_publish_posts_with_markdown', true );
1814 }
1815 add_action( 'jetpack_activate_module_markdown', 'jetpack_do_after_markdown_activation' );
1816