<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
 * Tools to interact with Jetpack modules via API requests.
 *
 * @package automattic/jetpack
 */

use Automattic\Jetpack\Connection\REST_Connector;
use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
use Automattic\Jetpack\Stats\WPCOM_Stats;
use Automattic\Jetpack\Stats_Admin\Main as Stats_Admin_Main;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection_Shared_Functions;

if ( ! defined( 'ABSPATH' ) ) {
	exit( 0 );
}

/**
 * This is the base class for every Core API endpoint Jetpack uses.
 */
class Jetpack_Core_API_Module_Toggle_Endpoint extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {

	/**
	 * Check if the module requires the site to be publicly accessible from WPCOM.
	 * If the site meets this requirement, the module is activated. Otherwise an error is returned.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 *     @type bool   $active should module be activated.
	 * }
	 *
	 * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
	 */
	public function process( $request ) {
		if ( $request['active'] ) {
			return $this->activate_module( $request );
		} else {
			return $this->deactivate_module( $request );
		}
	}

	/**
	 * If it's a valid Jetpack module, activate it.
	 *
	 * @since 4.3.0
	 *
	 * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
	 *                                        and a string when called from Jetpack_Core_API_Data->update_data.
	 * {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 * }
	 *
	 * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function activate_module( $request ) {
		$module_slug = '';

		if (
			(
				is_array( $request )
				|| is_object( $request )
			)
			&& isset( $request['slug'] )
		) {
			$module_slug = $request['slug'];
		} else {
			$module_slug = $request;
		}

		if ( ! Jetpack::is_module( $module_slug ) ) {
			return new WP_Error(
				'not_found',
				esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		if ( ! Jetpack_Plan::supports( $module_slug ) ) {
			return new WP_Error(
				'not_supported',
				esc_html__( 'The requested Jetpack module is not supported by your plan.', 'jetpack' ),
				array( 'status' => 424 )
			);
		}

		if ( Jetpack::activate_module( $module_slug, false, false ) ) {
			return rest_ensure_response(
				array(
					'code'    => 'success',
					'message' => esc_html__( 'The requested Jetpack module was activated.', 'jetpack' ),
				)
			);
		}

		return new WP_Error(
			'activation_failed',
			esc_html__( 'The requested Jetpack module could not be activated.', 'jetpack' ),
			array( 'status' => 424 )
		);
	}

	/**
	 * If it's a valid Jetpack module, deactivate it.
	 *
	 * @since 4.3.0
	 *
	 * @param string|WP_REST_Request $request It's a WP_REST_Request when called from endpoint /module/<slug>/*
	 *                                        and a string when called from Jetpack_Core_API_Data->update_data.
	 * {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 * }
	 *
	 * @return bool|WP_Error True if module was activated. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function deactivate_module( $request ) {
		$module_slug = '';

		if (
			(
				is_array( $request )
				|| is_object( $request )
			)
			&& isset( $request['slug'] )
		) {
			$module_slug = $request['slug'];
		} else {
			$module_slug = $request;
		}

		if ( ! Jetpack::is_module( $module_slug ) ) {
			return new WP_Error(
				'not_found',
				esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		if ( ! Jetpack::is_module_active( $module_slug ) ) {
			return new WP_Error(
				'already_inactive',
				esc_html__( 'The requested Jetpack module was already inactive.', 'jetpack' ),
				array( 'status' => 409 )
			);
		}

		if ( Jetpack::deactivate_module( $module_slug ) ) {
			return rest_ensure_response(
				array(
					'code'    => 'success',
					'message' => esc_html__( 'The requested Jetpack module was deactivated.', 'jetpack' ),
				)
			);
		}
		return new WP_Error(
			'deactivation_failed',
			esc_html__( 'The requested Jetpack module could not be deactivated.', 'jetpack' ),
			array( 'status' => 400 )
		);
	}

	/**
	 * Check that the current user has permissions to manage Jetpack modules.
	 *
	 * @since 4.3.0
	 *
	 * @return bool
	 */
	public function can_request() {
		return current_user_can( 'jetpack_manage_modules' );
	}
}

/**
 * Interact with multiple modules at once (list or activate).
 *
 * // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
 */
class Jetpack_Core_API_Module_List_Endpoint {
	// phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound

	/**
	 * A WordPress REST API callback method that accepts a request object and decides what to do with it.
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @since 4.3.0
	 *
	 * @return bool|Array|WP_Error a resulting value or object, or an error.
	 */
	public function process( $request ) {
		if ( 'GET' === $request->get_method() ) {
			return $this->get_modules();
		} else {
			return static::activate_modules( $request );
		}
	}

	/**
	 * Get a list of all Jetpack modules and their information.
	 *
	 * @since 4.3.0
	 *
	 * @return array Array of Jetpack modules.
	 */
	public function get_modules() {
		require_once JETPACK__PLUGIN_DIR . 'class.jetpack-admin.php';

		$modules = Jetpack_Admin::init()->get_modules();
		foreach ( $modules as $slug => $properties ) {
			$modules[ $slug ]['options'] =
				Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $slug );
			if (
				isset( $modules[ $slug ]['requires_connection'] )
				&& $modules[ $slug ]['requires_connection']
				&& ( new Status() )->is_offline_mode()
			) {
				$modules[ $slug ]['activated'] = false;
			}
		}

		$modules = Jetpack::get_translated_modules( $modules );

		return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $modules );
	}

	/**
	 * Activate a list of valid Jetpack modules.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 * }
	 *
	 * @return bool|WP_Error True if modules were activated. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public static function activate_modules( $request ) {

		if (
			! isset( $request['modules'] )
			|| ! is_array( $request['modules'] )
		) {
			return new WP_Error(
				'not_found',
				esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		$activated = array();
		$failed    = array();

		foreach ( $request['modules'] as $module ) {
			if ( Jetpack::activate_module( $module, false, false ) ) {
				$activated[] = $module;
			} else {
				$failed[] = $module;
			}
		}

		if ( empty( $failed ) ) {
			return rest_ensure_response(
				array(
					'code'    => 'success',
					'message' => esc_html__( 'All modules activated.', 'jetpack' ),
				)
			);
		}

		$error = '';

		$activated_count = count( $activated );
		if ( $activated_count > 0 ) {
			$activated_last = array_pop( $activated );
			$activated_text = $activated_count > 1 ? sprintf(
				/* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
				__( '%1$s and %2$s', 'jetpack' ),
				implode( ', ', $activated ),
				$activated_last
			) : $activated_last;

			$error = sprintf(
				/* Translators: the variable is a module name. */
				_n( 'The module %s was activated.', 'The modules %s were activated.', $activated_count, 'jetpack' ),
				$activated_text
			) . ' ';
		}

		$failed_count = count( $failed );
		if ( count( $failed ) > 0 ) {
			$failed_last = array_pop( $failed );
			$failed_text = $failed_count > 1 ? sprintf(
				/* Translators: first variable is a list followed by the last item, which is the second variable. Example: dog, cat and bird. */
				__( '%1$s and %2$s', 'jetpack' ),
				implode( ', ', $failed ),
				$failed_last
			) : $failed_last;

			$error = sprintf(
				/* Translators: the variable is a module name. */
				_n( 'The module %s failed to be activated.', 'The modules %s failed to be activated.', $failed_count, 'jetpack' ),
				$failed_text
			) . ' ';
		}

		return new WP_Error(
			'activation_failed',
			esc_html( $error ),
			array( 'status' => 424 )
		);
	}

	/**
	 * A WordPress REST API permission callback method that accepts a request object and decides
	 * if the current user has enough privileges to act.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return bool does the current user have enough privilege.
	 */
	public function can_request( $request ) {
		if ( 'GET' === $request->get_method() ) {
			return current_user_can( 'jetpack_admin_page' );
		} else {
			return current_user_can( 'jetpack_manage_modules' );
		}
	}
}

/**
 * Class that manages updating of Jetpack module options and general Jetpack settings or retrieving module data.
 * If no module is specified, all module settings are retrieved/updated.
 *
 * @since 4.3.0
 * @since 4.4.0 Renamed Jetpack_Core_API_Module_Endpoint from to Jetpack_Core_API_Data.
 *
 * @author Automattic
 *
 * // phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
 */
class Jetpack_Core_API_Data extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
	// phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound

	/**
	 * Process request by returning the module or updating it.
	 * If no module is specified, settings for all modules are assumed.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request WP API request.
	 *
	 * @return bool|mixed|void|WP_Error
	 */
	public function process( $request ) {
		if ( 'GET' === $request->get_method() ) {
			if ( isset( $request['slug'] ) ) {
				return $this->get_module( $request );
			}

			return $this->get_all_options();
		} else {
			return $this->update_data( $request );
		}
	}

	/**
	 * Get information about a specific and valid Jetpack module.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 * }
	 *
	 * @return mixed|void|WP_Error
	 */
	public function get_module( $request ) {
		if ( Jetpack::is_module( $request['slug'] ) ) {

			$module = Jetpack::get_module( $request['slug'] );

			$module['options'] = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $request['slug'] );

			if (
				isset( $module['requires_connection'] )
				&& $module['requires_connection']
				&& ( new Status() )->is_offline_mode()
			) {
				$module['activated'] = false;
			}

			$i18n = jetpack_get_module_i18n( $request['slug'] );
			if ( $i18n ) {
				if ( isset( $module['name'] ) ) {
					$module['name'] = $i18n['name'];
				}
				if ( isset( $module['description'] ) ) {
					$module['description']       = $i18n['description'];
					$module['short_description'] = $i18n['description'];
				}
			}
			if ( isset( $module['module_tags'] ) ) {
				$module['module_tags'] = array_map( 'jetpack_get_module_i18n_tag', $module['module_tags'] );
			}

			return Jetpack_Core_Json_Api_Endpoints::prepare_modules_for_response( $module );
		}

		return new WP_Error(
			'not_found',
			esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ),
			array( 'status' => 404 )
		);
	}

	/**
	 * Get information about all Jetpack module options and settings.
	 *
	 * @since 4.6.0
	 *
	 * @return WP_REST_Response $response
	 */
	public function get_all_options() {
		$response = array();

		$modules = Jetpack::get_available_modules();
		if ( is_array( $modules ) && ! empty( $modules ) ) {
			foreach ( $modules as $module ) {
				// Add all module options.
				$options = Jetpack_Core_Json_Api_Endpoints::prepare_options_for_response( $module );
				foreach ( $options as $option_name => $option ) {
					$response[ $option_name ] = $option['current_value'];
				}

				// Add the module activation state.
				$response[ $module ] = Jetpack::is_module_active( $module );
			}
		}

		$settings = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( 'settings' );

		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$response['categories'] = get_categories( array( 'get' => 'all' ) );

		foreach ( $settings as $setting => $properties ) {
			switch ( $setting ) {
				case 'lang_id':
					if ( ! current_user_can( 'install_languages' ) ) {
						// The user doesn't have caps to install language packs, so warn the client.
						$response[ $setting ] = 'error_cap';
						break;
					}

					$value = get_option( 'WPLANG', '' );
					if ( empty( $value ) && defined( 'WPLANG' ) ) {
						$value = WPLANG;
					}
					$response[ $setting ] = empty( $value ) ? 'en_US' : $value;
					break;

				case 'wordpress_api_key':
					// When field is clear, return empty. Otherwise it would return "false".
					if ( '' === get_option( 'wordpress_api_key', '' ) ) {
						$response[ $setting ] = '';
					} else {
						if ( ! class_exists( 'Akismet' ) ) {
							if ( is_readable( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
								require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
							}
						}
						$response[ $setting ] = class_exists( 'Akismet' ) ? Akismet::get_api_key() : '';
					}
					break;

				case 'search_auto_config':
					// Only writable.
					$response[ $setting ] = 1;
					break;

				default:
					$default              = $settings[ $setting ]['default'] ?? false;
					$response[ $setting ] = Jetpack_Core_Json_Api_Endpoints::cast_value( get_option( $setting, $default ), $settings[ $setting ] );
					break;
			}
		}

		$response['akismet'] = is_plugin_active( 'akismet/akismet.php' );

		require_once JETPACK__PLUGIN_DIR . '/modules/memberships/class-jetpack-memberships.php';
		if ( class_exists( 'Jetpack_Memberships' ) ) {
			$response['newsletter_has_active_plan'] = count( Jetpack_Memberships::get_all_newsletter_plan_ids( false ) ) > 0;
		}

		// Make sure we are returning a consistent type
		if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
			require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
		}
		$response['wpcom_newsletter_categories'] = Jetpack_Newsletter_Category_Helper::get_category_ids();

		return rest_ensure_response( $response );
	}

	/**
	 * If it's a valid Jetpack module and configuration parameters have been sent, update it.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request {
	 *     Array of parameters received by request.
	 *
	 *     @type string $slug Module slug.
	 * }
	 *
	 * @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.
	 */
	public function update_data( $request ) {

		// If it's null, we're trying to update many module options from different modules.
		if ( $request['slug'] === null ) {

			// Value admitted by Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list that will make it return all module options.
			// It will not be passed. It's just checked in this method to pass that method a string or array.
			$request['slug'] = 'any';
		} else {
			if ( ! Jetpack::is_module( $request['slug'] ) ) {
				return new WP_Error( 'not_found', esc_html__( 'The requested Jetpack module was not found.', 'jetpack' ), array( 'status' => 404 ) );
			}

			if ( ! Jetpack::is_module_active( $request['slug'] ) ) {
				return new WP_Error( 'inactive', esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' ), array( 'status' => 409 ) );
			}
		}

		/*
		 * Get parameters to update the module.
		 * We cannot simply use $request->get_params() because when we registered this route,
		 * we are adding the entire output of Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list()
		 * to the current request object's params. We are interested in body of the actual request.
		 * This may be JSON:
		 */
		$params = $request->get_json_params();
		if ( ! is_array( $params ) ) {
			// Or it may be standard POST key-value pairs.
			$params = $request->get_body_params();
		}

		// Exit if no parameters were passed.
		if ( ! is_array( $params ) ) {
			return new WP_Error( 'missing_options', esc_html__( 'Missing options.', 'jetpack' ), array( 'status' => 404 ) );
		}

		// If $params was set via `get_body_params()` there may be some additional variables in the request that can
		// cause validation to fail. This method verifies that each param was in fact updated and will throw a `some_updated`
		// error if unused variables are included in the request.
		foreach ( array_keys( $params ) as $key ) {
			if ( is_int( $key ) || 'slug' === $key || 'context' === $key ) {
				unset( $params[ $key ] );
			}
		}

		// Get available module options.
		$options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list(
			'any' === $request['slug']
			? $params
			: $request['slug']
		);

		// Prepare to toggle module if needed.
		$toggle_module = new Jetpack_Core_API_Module_Toggle_Endpoint( new Jetpack_IXR_Client() );

		// Options that are invalid or failed to update.
		$invalid     = array_keys( array_diff_key( $params, $options ) );
		$not_updated = array();

		// Remove invalid options.
		$params = array_intersect_key( $params, $options );

		// Used if response is successful. The message can be overwritten and additional data can be added here.
		$response = array(
			'code'    => 'success',
			'message' => esc_html__( 'The requested Jetpack data updates were successful.', 'jetpack' ),
		);

		// If there are modules to activate, activate them first so they're ready when their options are set.
		foreach ( $params as $option => $value ) {
			if ( 'modules' === $options[ $option ]['jp_group'] ) {

				// Used if there was an error. Can be overwritten with specific error messages.
				$error = '';

				// Set to true if the module toggling was successful.
				$updated = false;

				// Check if user can toggle the module.
				if ( $toggle_module->can_request() ) {

					// Activate or deactivate the module according to the value passed.
					$toggle_result = $value
						? $toggle_module->activate_module( $option )
						: $toggle_module->deactivate_module( $option );

					if (
						is_wp_error( $toggle_result )
						&& 'already_inactive' === $toggle_result->get_error_code()
					) {

						// If the module is already inactive, we don't fail.
						$updated = true;
					} elseif ( is_wp_error( $toggle_result ) ) {
						$error = $toggle_result->get_error_message();
					} else {
						$updated = true;
					}
				} else {
					$error = REST_Connector::get_user_permissions_error_msg();
				}

				// The module was not toggled.
				if ( ! $updated ) {
					$not_updated[ $option ] = $error;
				}

				if ( $updated ) {
					// Return the module state.
					$response[ $option ] = $value;
				}

				// Remove module from list so we don't go through it again.
				unset( $params[ $option ] );
			}
		}

		if ( ! class_exists( 'Jetpack_Newsletter_Category_Helper' ) ) {
			require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-newsletter-category-helper.php';
		}

		foreach ( $params as $option => $value ) {

			// Used if there was an error. Can be overwritten with specific error messages.
			$error = '';

			// Set to true if the option update was successful.
			$updated = false;

			// Get option attributes, including the group it belongs to.
			$option_attrs = $options[ $option ];

			// If this is a module option and the related module isn't active for any reason, continue with the next one.
			if ( 'settings' !== $option_attrs['jp_group'] ) {
				if ( ! Jetpack::is_module( $option_attrs['jp_group'] ) ) {
					$not_updated[ $option ] = esc_html__( 'The requested Jetpack module was not found.', 'jetpack' );
					continue;
				}

				if (
					'any' !== $request['slug']
					&& ! Jetpack::is_module_active( $option_attrs['jp_group'] )
				) {

					// We only take note of skipped options when updating one module.
					$not_updated[ $option ] = esc_html__( 'The requested Jetpack module is inactive.', 'jetpack' );
					continue;
				}
			}

			// Properly cast value based on its type defined in endpoint accepted args.
			$value = Jetpack_Core_Json_Api_Endpoints::cast_value( $value, $option_attrs );

			switch ( $option ) {
				case 'lang_id':
					if ( ! current_user_can( 'install_languages' ) ) {
						// We can't affect this setting.
						$updated = false;
						break;
					}

					if ( 'en_US' === $value || empty( $value ) ) {
						return delete_option( 'WPLANG' );
					}

					if ( ! function_exists( 'request_filesystem_credentials' ) ) {
						require_once ABSPATH . 'wp-admin/includes/file.php';
					}

					if ( ! function_exists( 'wp_download_language_pack' ) ) {
						require_once ABSPATH . 'wp-admin/includes/translation-install.php';
					}

					// `wp_download_language_pack` only tries to download packs if they're not already available.
					$language = wp_download_language_pack( $value );
					if ( false === $language ) {
						// The language pack download failed.
						$updated = false;
						break;
					}
					$updated = get_option( 'WPLANG' ) === $language ? true : update_option( 'WPLANG', $language );
					break;

				case 'monitor_receive_notifications':
					if ( ! class_exists( 'Jetpack_Monitor' ) ) {
						$updated = false;
						break;
					}

					$monitor = new Jetpack_Monitor();

					// If we got true as response, consider it done.
					$updated = true === $monitor->update_option_receive_jetpack_monitor_notification( $value );
					break;

				case 'post_by_email_address':
					if ( ! class_exists( 'Jetpack_Post_By_Email' ) ) {
						$updated = false;
						break;
					}

					$result = Jetpack_Post_By_Email::init()->process_api_request( $value );

					// If we got an email address (create or regenerate) or 1 (delete), consider it done.
					if ( is_string( $result ) && preg_match( '/[a-z0-9]+@post.wordpress.com/', $result ) ) {
						$response[ $option ] = $result;
						$updated             = true;
					} elseif ( 1 == $result ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
						$updated = true;
					} elseif ( is_array( $result ) && isset( $result['message'] ) ) {
						$error = $result['message'];
					}
					break;

				case 'jetpack_protect_key':
					$brute_force_protection = Brute_Force_Protection::instance();
					if ( 'create' === $value ) {
						$result = $brute_force_protection->get_protect_key();
					} else {
						$result = false;
					}

					// If we got one of Protect keys, consider it done.
					if ( is_string( $result ) && preg_match( '/[a-z0-9]{40,}/i', $result ) ) {
						$response[ $option ] = $result;
						$updated             = true;
					}
					break;

				case 'jetpack_protect_global_whitelist':
					$updated = Brute_Force_Protection_Shared_Functions::save_allow_list( explode( PHP_EOL, str_replace( array( ' ', ',' ), array( '', "\n" ), $value ) ) );

					if ( is_wp_error( $updated ) ) {
						$error = $updated->get_error_message();
					}
					break;

				case 'show_headline':
				case 'show_thumbnails':
					$grouped_options_current    = (array) Jetpack_Options::get_option( 'relatedposts' );
					$grouped_options            = $grouped_options_current;
					$grouped_options[ $option ] = $value;

					// If option value was the same, consider it done.
					$updated = $grouped_options_current !== $grouped_options ? Jetpack_Options::update_option( 'relatedposts', $grouped_options ) : true;
					break;

				case 'search_auto_config':
					if ( ! $value ) {
						// Skip execution if no value is specified.
						$updated = true;
					} else {
						$plan = new Automattic\Jetpack\Search\Plan();
						if ( ! $plan->supports_instant_search() ) {
							$updated = new WP_Error( 'instant_search_not_supported', 'Instant Search is not supported by this site', array( 'status' => 400 ) );
							$error   = $updated->get_error_message();
						} elseif ( ! Automattic\Jetpack\Search\Options::is_instant_enabled() ) {
							$updated = new WP_Error( 'instant_search_disabled', 'Instant Search is disabled', array( 'status' => 400 ) );
							$error   = $updated->get_error_message();
						} else {
							$blog_id  = Automattic\Jetpack\Search\Helper::get_wpcom_site_id();
							$instance = Automattic\Jetpack\Search\Instant_Search::instance( $blog_id );
							$instance->auto_config_search();
							$updated = true;
						}
					}
					break;

				case 'google':
				case 'bing':
				case 'pinterest':
				case 'yandex':
				case 'facebook':
					$grouped_options_current = (array) get_option( 'verification_services_codes' );
					$grouped_options         = $grouped_options_current;

					// Extracts the content attribute from the HTML meta tag if needed.
					if ( preg_match( '#.*<meta name="(?:[^"]+)" content="([^"]+)" />.*#i', $value, $matches ) ) {
						$grouped_options[ $option ] = $matches[1];
					} else {
						$grouped_options[ $option ] = $value;
					}

					// If option value was the same, consider it done.
					$updated = $grouped_options_current !== $grouped_options
						? update_option( 'verification_services_codes', $grouped_options )
						: true;
					break;

				case 'sharing_services':
					if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
						break;
					}

					$sharer = new Sharing_Service();

					// If option value was the same, consider it done.
					$updated = $value !== $sharer->get_blog_services()
						? $sharer->set_blog_services( $value['visible'], $value['hidden'] )
						: true;
					break;

				case 'button_style':
				case 'sharing_label':
				case 'show':
					if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
						break;
					}

					$sharer                     = new Sharing_Service();
					$grouped_options            = $sharer->get_global_options();
					$grouped_options[ $option ] = $value;
					$updated                    = $sharer->set_global_options( $grouped_options );
					break;

				case 'custom':
					if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
						break;
					}

					$sharer  = new Sharing_Service();
					$updated = $sharer->new_service( stripslashes( $value['sharing_name'] ), stripslashes( $value['sharing_url'] ), stripslashes( $value['sharing_icon'] ) );

					// Return new custom service.
					$response[ $option ] = $updated;
					break;

				case 'sharing_delete_service':
					if ( ! class_exists( 'Sharing_Service' ) && ! include_once JETPACK__PLUGIN_DIR . 'modules/sharedaddy/sharing-service.php' ) {
						break;
					}

					$sharer  = new Sharing_Service();
					$updated = $sharer->delete_service( $value );
					break;

				case 'jetpack-twitter-cards-site-tag':
					$value   = trim( ltrim( wp_strip_all_tags( $value ), '@' ) );
					$updated = get_option( $option ) !== $value ? update_option( $option, $value ) : true;
					break;

				case 'admin_bar':
				case 'roles':
				case 'count_roles':
				case 'blog_id':
				case 'do_not_track':
				case 'version':
				case 'collapse_nudges':
					$grouped_options_current    = (array) get_option( 'stats_options' );
					$grouped_options            = $grouped_options_current;
					$grouped_options[ $option ] = $value;

					// If option value was the same, consider it done.
					$updated = $grouped_options_current !== $grouped_options
						? update_option( 'stats_options', $grouped_options )
						: true;
					break;

				case 'enable_odyssey_stats':
					$updated = Stats_Admin_Main::update_new_stats_status( $value );

					break;

				case 'akismet_show_user_comments_approved':
					// Save Akismet option '1' or '0' like it's done in akismet/class.akismet-admin.php.
					$updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
						? update_option( $option, $value ? '1' : '0' )
						: true;
					break;

				case 'wordpress_api_key':
					if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
						$error   = esc_html__( 'Please install Akismet.', 'jetpack' );
						$updated = false;
						break;
					}

					if ( ! defined( 'AKISMET_VERSION' ) ) {
						$error   = esc_html__( 'Please activate Akismet.', 'jetpack' );
						$updated = false;
						break;
					}

					// Allow to clear the API key field.
					if ( '' === $value ) {
						$updated = get_option( $option ) !== $value
							? update_option( $option, $value )
							: true;
						break;
					}

					require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
					require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';

					if ( class_exists( 'Akismet_Admin' ) && method_exists( 'Akismet_Admin', 'save_key' ) ) {
						if ( Akismet::verify_key( $value ) === 'valid' ) {
							$akismet_user = Akismet_Admin::get_akismet_user( $value );
							if ( $akismet_user ) {
								if ( in_array( $akismet_user->status, array( 'active', 'active-dunning', 'no-sub' ), true ) ) {
									$updated = get_option( $option ) !== $value
										? update_option( $option, $value )
										: true;
									break;
								} else {
									$error = esc_html__( "Akismet user status doesn't allow to update the key", 'jetpack' );
								}
							} else {
								$error = esc_html__( 'Invalid Akismet user', 'jetpack' );
							}
						} else {
							$error = esc_html__( 'Invalid Akismet key', 'jetpack' );
						}
					} else {
						$error = esc_html__( 'Akismet is not installed or active', 'jetpack' );
					}
					$updated = false;
					break;

				case 'google_analytics_tracking_id':
					$grouped_options_current = (array) get_option( 'jetpack_wga' );
					$grouped_options         = $grouped_options_current;
					$grouped_options['code'] = $value;

					// If option value was the same, consider it done.
					$updated = $grouped_options_current !== $grouped_options
						? update_option( 'jetpack_wga', $grouped_options )
						: true;
					break;

				case 'dismiss_empty_stats_card':
				case 'dismiss_dash_backup_getting_started':
				case 'dismiss_dash_agencies_learn_more':
					// If option value was the same, consider it done.
					$updated = get_option( $option ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support bools or strings saved by update_option.
						? update_option( $option, (bool) $value )
						: true;
					break;

				case 'jetpack_subscriptions_reply_to':
					// If option value was the same, consider it done.
					require_once JETPACK__PLUGIN_DIR . 'modules/subscriptions/class-settings.php';
					$sub_value = Automattic\Jetpack\Modules\Subscriptions\Settings::is_valid_reply_to( $value )
						? $value
						: Automattic\Jetpack\Modules\Subscriptions\Settings::$default_reply_to;

						$updated = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
					break;
				case 'jetpack_subscriptions_from_name':
					// If option value was the same, consider it done.
					$sub_value = sanitize_text_field( $value );
					$updated   = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
					break;

				case 'stb_enabled':
				case 'stc_enabled':
				case 'sm_enabled':
				case 'jetpack_subscribe_overlay_enabled':
				case 'jetpack_subscribe_floating_button_enabled':
				case 'wpcom_newsletter_categories_enabled':
				case 'wpcom_featured_image_in_email':
				case 'jetpack_gravatar_in_email':
				case 'jetpack_author_in_email':
				case 'jetpack_post_date_in_email':
				case 'wpcom_subscription_emails_use_excerpt':
				case 'jetpack_subscriptions_subscribe_post_end_enabled':
				case 'jetpack_subscriptions_login_navigation_enabled':
				case 'jetpack_subscriptions_subscribe_navigation_enabled':
					// Convert the false value to 0. This allows the option to be updated if it doesn't exist yet.
					$sub_value = $value ? $value : 0;
					$updated   = (string) get_option( $option ) !== (string) $sub_value ? update_option( $option, $sub_value ) : true;
					break;

				case 'jetpack_blocks_disabled':
					$updated = (bool) get_option( $option ) !== (bool) $value ? update_option( $option, (bool) $value ) : true;
					break;

				case 'subscription_options':
					if ( ! is_array( $value ) ) {
						break;
					}

					$allowed_keys   = array( 'invitation', 'comment_follow', 'welcome', 'subscribe_modal_heading', 'free_tier_description', 'hide_free_tier' );
					$filtered_value = array_filter(
						$value,
						function ( $key ) use ( $allowed_keys ) {
							return in_array( $key, $allowed_keys, true );
						},
						ARRAY_FILTER_USE_KEY
					);

					if ( empty( $filtered_value ) ) {
						break;
					}

					// `hide_free_tier` is a boolean flag, so pull it out before the HTML
					// sanitization below (which expects strings). Sanitize it with
					// rest_sanitize_boolean() so stringy booleans (e.g. "false", "0")
					// are interpreted correctly rather than being treated as truthy by a
					// plain `! empty()`.
					$has_hide_free_tier = array_key_exists( 'hide_free_tier', $filtered_value );
					$hide_free_tier     = $has_hide_free_tier && rest_sanitize_boolean( $filtered_value['hide_free_tier'] );
					unset( $filtered_value['hide_free_tier'] );

					array_walk_recursive(
						$filtered_value,
						function ( &$value ) {
							$value = wp_kses(
								$value,
								array(
									'ul'     => array(),
									'li'     => array(),
									'p'      => array(),
									'strong' => array(),
									'ol'     => array(),
									'em'     => array(),
									'a'      => array(
										'href' => array(),
									),
								)
							);
						}
					);

					// Normalize whitespace-only `subscribe_modal_heading` input to empty so
					// the modal template's `empty()` fallback fires. PHP's `empty()` treats
					// `"   "` as non-empty, which would otherwise render a blank heading.
					if ( isset( $filtered_value['subscribe_modal_heading'] ) ) {
						$filtered_value['subscribe_modal_heading'] = trim( $filtered_value['subscribe_modal_heading'] );
					}

					// The free tier description is stored as plain markdown source, so strip
					// all HTML and cap its length to match the paid-tier description field.
					// WordPress core guarantees mb_substr() (polyfilled in wp-includes/compat.php
					// when the mbstring extension is unavailable), so it's safe to use directly.
					// A JSON payload could supply a non-scalar (array/object) for this field,
					// which would fatal in wp_kses()/mb_substr() on PHP 8+, so drop invalid values.
					if ( isset( $filtered_value['free_tier_description'] ) ) {
						if ( is_scalar( $filtered_value['free_tier_description'] ) ) {
							$filtered_value['free_tier_description'] = mb_substr( wp_kses( (string) $filtered_value['free_tier_description'], array() ), 0, 500 );
						} else {
							unset( $filtered_value['free_tier_description'] );
						}
					}

					if ( $has_hide_free_tier ) {
						$filtered_value['hide_free_tier'] = $hide_free_tier;
					}

					$old_subscription_options = get_option( 'subscription_options' );
					if ( ! is_array( $old_subscription_options ) ) {
						$old_subscription_options = array();
					}
					$new_subscription_options = array_merge( $old_subscription_options, $filtered_value );
					$updated                  = true;

					if ( serialize( $old_subscription_options ) === serialize( $new_subscription_options ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
						break; // This prevents the option update to fail when the values are the same.
					}

					if ( ! update_option( $option, $new_subscription_options ) ) {
						$updated = false;
						$error   = esc_html__( 'Subscription Options failed to process.', 'jetpack' );
					}
					break;

				case Jetpack_Newsletter_Category_Helper::NEWSLETTER_CATEGORIES_OPTION:
					if ( ! is_array( $value ) || empty( $value ) ) {
						$updated = true;
						break;
					}

					// If we are already current, do nothing
					$current_value = Jetpack_Newsletter_Category_Helper::get_category_ids();
					if ( $value === $current_value ) {
						$updated = true;
						break;
					}

					if ( Jetpack_Newsletter_Category_Helper::save_category_ids( $value ) ) {
						$updated = true;
					} else {
						$updated = false;
						$error   = esc_html__( 'Newsletter category did not update.', 'jetpack' );
					}

					break;

				default:
					// Boolean values are stored as 1 or 0.
					if ( isset( $options[ $option ]['type'] ) && 'boolean' === $options[ $option ]['type'] ) {
						$value = (int) $value;
					}

					// If option value was the same as it's current value, or it's default, consider it done.
					$default = $options[ $option ]['default'] ?? false;
					$updated = get_option( $option, $default ) != $value // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- ensure we support scalars or strings saved by update_option.
						? update_option( $option, $value )
						: true;
					break;
			}

			// The option was not updated.
			if ( ! $updated ) {
				$not_updated[ $option ] = $error;
			}
		}

		if ( empty( $invalid ) && empty( $not_updated ) ) {
			// The option was updated.
			return rest_ensure_response( $response );
		} else {
			$invalid_count     = count( $invalid );
			$not_updated_count = count( $not_updated );
			$error             = '';
			if ( $invalid_count > 0 ) {
				$error = sprintf(
				/* Translators: the plural variable is a comma-separated list. Example: dog, cat, bird. */
					_n( 'Invalid option: %s.', 'Invalid options: %s.', $invalid_count, 'jetpack' ),
					implode( ', ', $invalid )
				);
			}
			if ( $not_updated_count > 0 ) {
				$not_updated_messages = array();
				foreach ( $not_updated as $not_updated_option => $not_updated_message ) {
					if ( ! empty( $not_updated_message ) ) {
						$not_updated_messages[] = sprintf(
							/* Translators: the first variable is a module option or slug, or setting. The second is the error message . */
							__( '%1$s: %2$s', 'jetpack' ),
							$not_updated_option,
							$not_updated_message
						);
					}
				}
				if ( ! empty( $error ) ) {
					$error .= ' ';
				}
				if ( ! empty( $not_updated_messages ) ) {
					$error .= ' ' . implode( '. ', $not_updated_messages );
				}
			}
			// There was an error because some options were updated but others were invalid or failed to update.
			return new WP_Error( 'some_updated', esc_html( $error ), array( 'status' => 400 ) );
		}
	}

	/**
	 * Perform tasks in the site based on onboarding choices.
	 *
	 * @since 5.4.0
	 *
	 * @deprecated since 13.9
	 *
	 * @param array $data Onboarding choices made by user.
	 *
	 * @return string Result of onboarding processing and, if there is one, an error message.
	 */
	private function process_onboarding( $data ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		_deprecated_function( __METHOD__, '13.9' );
		return '';
	}

	/**
	 * Add or update Business Address widget.
	 *
	 * @deprecated since 13.9
	 *
	 * @param array $address Array of business address fields.
	 *
	 * @return WP_Error|true True if the data was saved correctly.
	 */
	private static function handle_business_address( $address ) {
		_deprecated_function( __METHOD__, '13.9' );
		$first_sidebar = Jetpack_Widgets::get_first_sidebar();

		$widgets_module_active = Jetpack::is_module_active( 'widgets' );
		if ( ! $widgets_module_active ) {
			$widgets_module_active = Jetpack::activate_module( 'widgets', false, false );
		}
		if ( ! $widgets_module_active ) {
			return new WP_Error( 'module_activation_failed', 'Failed to activate the widgets module.', 400 );
		}

		if ( $first_sidebar ) {
			$title   = isset( $address['name'] ) ? sanitize_text_field( $address['name'] ) : '';
			$street  = isset( $address['street'] ) ? sanitize_text_field( $address['street'] ) : '';
			$city    = isset( $address['city'] ) ? sanitize_text_field( $address['city'] ) : '';
			$state   = isset( $address['state'] ) ? sanitize_text_field( $address['state'] ) : '';
			$zip     = isset( $address['zip'] ) ? sanitize_text_field( $address['zip'] ) : '';
			$country = isset( $address['country'] ) ? sanitize_text_field( $address['country'] ) : '';

			$full_address = implode( ' ', array_filter( array( $street, $city, $state, $zip, $country ) ) );

			$widget_options = array(
				'title'   => $title,
				'address' => $full_address,
				'phone'   => '',
				'hours'   => '',
				'showmap' => false,
				'email'   => '',
			);

			$widget_updated = '';
			if ( ! self::has_business_address_widget( $first_sidebar ) ) {
				$widget_updated = Jetpack_Widgets::insert_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
			} else {
				$widget_updated = Jetpack_Widgets::update_widget_in_sidebar( 'widget_contact_info', $widget_options, $first_sidebar );
			}
			if ( is_wp_error( $widget_updated ) ) {
				return new WP_Error( 'widget_update_failed', 'Widget could not be updated.', 400 );
			}

			$address_save = array(
				'name'    => $title,
				'street'  => $street,
				'city'    => $city,
				'state'   => $state,
				'zip'     => $zip,
				'country' => $country,
			);
			update_option( 'jpo_business_address', $address_save );
			return true;
		}

		// No sidebar to place the widget.
		return new WP_Error( 'sidebar_not_found', 'No sidebar.', 400 );
	}

	/**
	 * Check whether "Contact Info & Map" widget is present in a given sidebar.
	 *
	 * @param string $sidebar ID of the sidebar to which the widget will be added.
	 *
	 * @return bool Whether the widget is present in a given sidebar.
	 */
	private static function has_business_address_widget( $sidebar ) {
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
		if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
			return false;
		}
		foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
			if ( str_contains( $widget, 'widget_contact_info' ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check if user is allowed to perform the update.
	 *
	 * @since 4.3.0
	 *
	 * @param WP_REST_Request $request The request sent to the WP REST API.
	 *
	 * @return bool
	 */
	public function can_request( $request ) {
		if ( 'GET' === $request->get_method() ) {
			return current_user_can( 'jetpack_admin_page' );
		} else {
			$module = Jetpack_Core_Json_Api_Endpoints::get_module_requested();
			if ( empty( $module ) ) {
				$params = $request->get_json_params();
				if ( ! is_array( $params ) ) {
					$params = $request->get_body_params();
				}
				$options = Jetpack_Core_Json_Api_Endpoints::get_updateable_data_list( $params );
				foreach ( $options as $option => $definition ) {
					if ( in_array( $options[ $option ]['jp_group'], array( 'post-by-email' ), true ) ) {
						$module = $options[ $option ]['jp_group'];
						break;
					}
				}
			}
			// User is trying to create, regenerate or delete its PbE.
			if ( 'post-by-email' === $module ) {
				return current_user_can( 'edit_posts' ) && current_user_can( 'jetpack_admin_page' );
			}
			return current_user_can( 'jetpack_configure_modules' );
		}
	}
}

/**
 * Get detailed data from a specific module.
 *
 * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
 */
class Jetpack_Core_API_Module_Data_Endpoint {

	/**
	 * Process request and return different data based on the module we are interested in.
	 *
	 * @param WP_REST_Request $request WP API request.
	 *
	 * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
	 */
	public function process( $request ) {
		switch ( $request['slug'] ) {
			case 'protect':
				return $this->get_protect_data();
			case 'stats':
				return $this->get_stats_data( $request );
			case 'akismet':
				return $this->get_akismet_data();
			case 'monitor':
				return $this->get_monitor_data();
			case 'verification-tools':
				return $this->get_verification_tools_data();
			case 'vaultpress':
				return $this->get_vaultpress_data();
		}
	}

	/**
	 * Decide against which service to check the key.
	 *
	 * @since 4.8.0
	 *
	 * @param WP_REST_Request $request WP API request.
	 *
	 * @return bool
	 */
	public function key_check( $request ) {
		switch ( $request['service'] ) {
			case 'akismet':
				$params = $request->get_json_params();
				if ( isset( $params['api_key'] ) && ! empty( $params['api_key'] ) ) {
					return $this->check_akismet_key( $params['api_key'] );
				}
				return $this->check_akismet_key();
		}
		return false;
	}

	/**
	 * Get number of blocked intrusion attempts.
	 *
	 * @since 4.3.0
	 *
	 * @return mixed|WP_Error Number of blocked attempts if protection is enabled. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function get_protect_data() {
		if ( Jetpack::is_module_active( 'protect' ) ) {
			return (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 );
		}

		return new WP_Error(
			'not_active',
			esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
			array( 'status' => 404 )
		);
	}

	/**
	 * Get number of spam messages blocked by Akismet.
	 *
	 * @since 4.3.0
	 *
	 * @return int|string Number of spam blocked by Akismet. Otherwise, an error message.
	 */
	public function get_akismet_data() {
		$akismet_status = $this->akismet_is_active_and_registered();
		if ( ! is_wp_error( $akismet_status ) ) {
			return (int) get_option( 'akismet_spam_count', 0 );
		} else {
			return $akismet_status->get_error_code();
		}
	}

	/**
	 * Verify the Akismet API key.
	 *
	 * @since 4.8.0
	 *
	 * @param string $api_key Optional API key to check.
	 *
	 * @return array Information about the key. 'validKey' is true if key is valid, false otherwise.
	 */
	public function check_akismet_key( $api_key = '' ) {
		$akismet_status = $this->akismet_class_exists();
		if ( is_wp_error( $akismet_status ) ) {
			return rest_ensure_response(
				array(
					'validKey'          => false,
					'invalidKeyCode'    => $akismet_status->get_error_code(),
					'invalidKeyMessage' => $akismet_status->get_error_message(),
				)
			);
		}

		$key_status = Akismet::check_key_status( empty( $api_key ) ? Akismet::get_api_key() : $api_key );

		if ( ! $key_status || 'invalid' === $key_status || 'failed' === $key_status ) {
			return rest_ensure_response(
				array(
					'validKey'          => false,
					'invalidKeyCode'    => 'invalid_key',
					'invalidKeyMessage' => esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ),
				)
			);
		}

		return rest_ensure_response(
			array(
				'validKey' => isset( $key_status[1] ) && 'valid' === $key_status[1],
			)
		);
	}

	/**
	 * Check if Akismet class file exists and if class is loaded.
	 *
	 * @since 4.8.0
	 *
	 * @return bool|WP_Error Returns true if class file exists and class is loaded, WP_Error otherwise.
	 */
	private function akismet_class_exists() {
		if ( ! file_exists( WP_PLUGIN_DIR . '/akismet/class.akismet.php' ) ) {
			return new WP_Error( 'not_installed', esc_html__( 'Please install Akismet.', 'jetpack' ), array( 'status' => 400 ) );
		}

		if ( ! class_exists( 'Akismet' ) ) {
			return new WP_Error( 'not_active', esc_html__( 'Please activate Akismet.', 'jetpack' ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Is Akismet registered and active?
	 *
	 * @since 4.3.0
	 *
	 * @return bool|WP_Error True if Akismet is active and registered. Otherwise, a WP_Error instance with the corresponding error.
	 */
	private function akismet_is_active_and_registered() {
		$akismet_exists = $this->akismet_class_exists();
		if ( is_wp_error( $akismet_exists ) ) {
			return $akismet_exists;
		}

		// What about if Akismet is put in a sub-directory or maybe in mu-plugins?
		require_once WP_PLUGIN_DIR . '/akismet/class.akismet.php';
		require_once WP_PLUGIN_DIR . '/akismet/class.akismet-admin.php';
		$akismet_key = Akismet::verify_key( Akismet::get_api_key() );

		if ( ! $akismet_key || 'invalid' === $akismet_key || 'failed' === $akismet_key ) {
			return new WP_Error( 'invalid_key', esc_html__( 'Invalid Akismet key. Please contact support.', 'jetpack' ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Get stats data for this site
	 *
	 * @since 4.1.0
	 *
	 * @param WP_REST_Request $request {
	 *     Array of parameters received by request.
	 *
	 *     @type string $date Date range to restrict results to.
	 * }
	 *
	 * @return WP_Error|WP_HTTP_Response|WP_REST_Response Stats information relayed from WordPress.com.
	 */
	public function get_stats_data( WP_REST_Request $request ) {
		// Get parameters to fetch Stats data.
		$range = $request->get_param( 'range' );

		// If no parameters were passed.
		if (
			empty( $range )
			|| ! in_array( $range, array( 'day', 'week', 'month' ), true )
		) {
			$range = 'day';
		}

		$wpcom_stats = new WPCOM_Stats();
		switch ( $range ) {

			// This is always called first on page load.
			case 'day':
				$initial_stats = $wpcom_stats->convert_stats_array_to_object( $wpcom_stats->get_stats() );
				return rest_ensure_response(
					array(
						'general' => $initial_stats,

						// Build data for 'day' as if it was $wpcom_stats ->get_visits( array( 'unit' => 'day, 'quantity' => 30).
						'day'     => $initial_stats->visits ?? array(),
					)
				);
			case 'week':
				return rest_ensure_response(
					array(
						'week' => $wpcom_stats->convert_stats_array_to_object(
							$wpcom_stats->get_visits(
								array(
									'unit'     => 'week',
									'quantity' => 14,
								)
							)
						),
					)
				);
			case 'month':
				return rest_ensure_response(
					array(
						'month' => $wpcom_stats->convert_stats_array_to_object(
							$wpcom_stats->get_visits(
								array(
									'unit'     => 'month',
									'quantity' => 12,
								)
							)
						),
					)
				);
		}
	}

	/**
	 * Get date of last downtime.
	 *
	 * @since 4.3.0
	 *
	 * @return mixed|WP_Error Number of days since last downtime. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function get_monitor_data() {
		if ( ! Jetpack::is_module_active( 'monitor' ) ) {
			return new WP_Error(
				'not_active',
				esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		$monitor       = new Jetpack_Monitor();
		$last_downtime = $monitor->monitor_get_last_downtime();
		if ( is_wp_error( $last_downtime ) ) {
			return $last_downtime;
		} elseif ( false === strtotime( $last_downtime ) ) {
			return rest_ensure_response(
				array(
					'code' => 'success',
					'date' => null,
				)
			);
		} else {
			return rest_ensure_response(
				array(
					'code' => 'success',
					'date' => human_time_diff( strtotime( $last_downtime ), strtotime( 'now' ) ),
				)
			);
		}
	}

	/**
	 * Get services that this site is verified with.
	 *
	 * @since 4.3.0
	 *
	 * @return mixed|WP_Error List of services that verified this site. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function get_verification_tools_data() {
		if ( ! Jetpack::is_module_active( 'verification-tools' ) ) {
			return new WP_Error(
				'not_active',
				esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		$verification_services_codes = get_option( 'verification_services_codes' );
		if (
			! is_array( $verification_services_codes )
			|| empty( $verification_services_codes )
		) {
			return new WP_Error(
				'empty',
				esc_html__( 'Site not verified with any service.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		$services = array();
		foreach ( jetpack_verification_services() as $name => $service ) {
			if ( is_array( $service ) && ! empty( $verification_services_codes[ $name ] ) ) {
				switch ( $name ) {
					case 'google':
						$services[] = 'Google';
						break;
					case 'bing':
						$services[] = 'Bing';
						break;
					case 'pinterest':
						$services[] = 'Pinterest';
						break;
					case 'yandex':
						$services[] = 'Yandex';
						break;
					case 'facebook':
						$services[] = 'Facebook';
						break;
				}
			}
		}

		if ( empty( $services ) ) {
			return new WP_Error(
				'empty',
				esc_html__( 'Site not verified with any service.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		if ( 2 > count( $services ) ) {
			$message = esc_html(
				sprintf(
					/* translators: %s is a service name like Google, Bing, Pinterest, etc. */
					__( 'Your site is verified with %s.', 'jetpack' ),
					$services[0]
				)
			);
		} else {
			$copy_services = $services;
			$last          = count( $copy_services ) - 1;
			$last_service  = $copy_services[ $last ];
			unset( $copy_services[ $last ] );
			$message = esc_html(
				sprintf(
					/* translators: %1$s is a comma-separated list of services, and %2$s is a single service name like Google, Bing, Pinterest, etc. */
					__( 'Your site is verified with %1$s and %2$s.', 'jetpack' ),
					implode( ', ', $copy_services ),
					$last_service
				)
			);
		}

		return rest_ensure_response(
			array(
				'code'     => 'success',
				'message'  => $message,
				'services' => $services,
			)
		);
	}

	/**
	 * Get VaultPress site data including, among other things, the date of the last backup if it was completed.
	 *
	 * @since 4.3.0
	 *
	 * @return mixed|WP_Error VaultPress site data. Otherwise, a WP_Error instance with the corresponding error.
	 */
	public function get_vaultpress_data() {
		if ( ! class_exists( 'VaultPress' ) ) {
			return new WP_Error(
				'not_active',
				esc_html__( 'The requested Jetpack module is not active.', 'jetpack' ),
				array( 'status' => 404 )
			);
		}

		$vaultpress = new VaultPress();
		if ( ! $vaultpress->is_registered() ) {
			return rest_ensure_response(
				array(
					'code'    => 'not_registered',
					'message' => esc_html__( 'You need to register for VaultPress.', 'jetpack' ),
				)
			);
		}

		$data = json_decode( base64_decode( $vaultpress->contact_service( 'plugin_data' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		if ( false === $data ) {
			return rest_ensure_response(
				array(
					'code'    => 'not_registered',
					'message' => esc_html__( 'Could not connect to VaultPress.', 'jetpack' ),
				)
			);
		} elseif ( is_wp_error( $data ) || ! isset( $data->backups->last_backup ) ) {
			return $data;
		} elseif ( empty( $data->backups->last_backup ) ) {
			return rest_ensure_response(
				array(
					'code'    => 'success',
					'message' => esc_html__( 'VaultPress is active and will back up your site soon.', 'jetpack' ),
					'data'    => $data,
				)
			);
		} else {
			return rest_ensure_response(
				array(
					'code'    => 'success',
					'message' => esc_html(
						sprintf(
							/* translators: placeholder is a unit of time (1 hour, 5 days, ...) */
							esc_html__( 'Your site was successfully backed up %s ago.', 'jetpack' ),
							human_time_diff(
								$data->backups->last_backup,
								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.
							)
						)
					),
					'data'    => $data,
				)
			);
		}
	}

	/**
	 * A WordPress REST API permission callback method that accepts a request object and
	 * decides if the current user has enough privileges to act.
	 *
	 * @since 4.3.0
	 *
	 * @return bool does a current user have enough privileges.
	 */
	public function can_request() {
		return current_user_can( 'jetpack_admin_page' );
	}
}

// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.

/**
 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
 *
 * @since 4.3.1
 */
function jetpack_do_after_gravatar_hovercards_activation() {

	// When Gravatar Hovercards is activated, enable them automatically.
	update_option( 'gravatar_disable_hovercards', 'enabled' );
}
add_action( 'jetpack_activate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_activation' );

/**
 * Actions performed only when Gravatar Hovercards is activated through the endpoint call.
 *
 * @since 4.3.1
 */
function jetpack_do_after_gravatar_hovercards_deactivation() {

	// When Gravatar Hovercards is deactivated, disable them automatically.
	update_option( 'gravatar_disable_hovercards', 'disabled' );
}
add_action( 'jetpack_deactivate_module_gravatar-hovercards', 'jetpack_do_after_gravatar_hovercards_deactivation' );

/**
 * Actions performed only when Markdown is activated through the endpoint call.
 *
 * @since 4.7.0
 */
function jetpack_do_after_markdown_activation() {

	// When Markdown is activated, enable support for post editing automatically.
	update_option( 'wpcom_publish_posts_with_markdown', true );
}
add_action( 'jetpack_activate_module_markdown', 'jetpack_do_after_markdown_activation' );
