PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.2.2
Jetpack – WP Security, Backup, Speed, & Growth v7.2.2
16.3-a.1 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 All 503 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 7.2.2, at _inc/lib/core-api/class.jetpack-core-api-module-endpoints.php

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