summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYury German <blueknight@gentoo.org>2019-05-22 01:01:36 -0400
committerYury German <blueknight@gentoo.org>2019-05-22 01:01:36 -0400
commit0914c92da22824025992c368c745546e41fbeb84 (patch)
tree965f6adf3b725e56d559fe4a93eff02281499dcc /plugins/jetpack/_inc/lib/core-api
parentDeleting plugins for update (diff)
downloadblogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.tar.gz
blogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.tar.bz2
blogs-gentoo-0914c92da22824025992c368c745546e41fbeb84.zip
Adding Plugins
Updating the following akismet.4.1.2, google-authenticator.0.52, jetpack.7.3.1 Signed-off-by: Yury German <blueknight@gentoo.org>
Diffstat (limited to 'plugins/jetpack/_inc/lib/core-api')
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php333
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php1708
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php60
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php56
-rw-r--r--plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php39
-rw-r--r--plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php40
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php49
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php79
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php71
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php22
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php187
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php121
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php194
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php167
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php281
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php37
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php62
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php171
-rw-r--r--plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php353
19 files changed, 4030 insertions, 0 deletions
diff --git a/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php b/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php
new file mode 100644
index 00000000..2a2245e4
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class-wpcom-rest-field-controller.php
@@ -0,0 +1,333 @@
+<?php
+
+// @todo - nicer API for array values?
+
+/**
+ * `WP_REST_Controller` is basically a wrapper for `register_rest_route()`
+ * `WPCOM_REST_API_V2_Field_Controller` is a mostly-analogous wrapper for `register_rest_field()`
+ */
+abstract class WPCOM_REST_API_V2_Field_Controller {
+ /**
+ * @var string|string[] $object_type The REST Object Type(s) to which the field should be added.
+ */
+ protected $object_type;
+
+ /**
+ * @var string $field_name The name of the REST API field to add.
+ */
+ protected $field_name;
+
+ public function __construct() {
+ if ( ! $this->object_type ) {
+ /* translators: %s: object_type */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$object_type', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'object_type' ), 'Jetpack 6.8' );
+ return;
+ }
+
+ if ( ! $this->field_name ) {
+ /* translators: %s: field_name */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::$field_name', sprintf( __( "Property '%s' must be overridden.", 'jetpack' ), 'field_name' ), 'Jetpack 6.8' );
+ return;
+ }
+
+ add_action( 'rest_api_init', array( $this, 'register_fields' ) );
+
+ // do this again later to collect any CPTs that get registered later
+ add_action( 'restapi_theme_init', array( $this, 'register_fields' ), 20 );
+ }
+
+ /**
+ * Registers the field with the appropriate schema and callbacks.
+ */
+ public function register_fields() {
+ foreach ( (array) $this->object_type as $object_type ) {
+ register_rest_field(
+ $object_type,
+ $this->field_name,
+ array(
+ 'get_callback' => array( $this, 'get_for_response' ),
+ 'update_callback' => array( $this, 'update_from_request' ),
+ 'schema' => $this->get_schema(),
+ )
+ );
+ }
+ }
+
+ /**
+ * Ensures the response matches the schema and request context.
+ *
+ * @param mixed $value
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ private function prepare_for_response( $value, $request ) {
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $schema = $this->get_schema();
+
+ $is_valid = rest_validate_value_from_schema( $value, $schema, $this->field_name );
+ if ( is_wp_error( $is_valid ) ) {
+ return $is_valid;
+ }
+
+ return $this->filter_response_by_context( $value, $schema, $context );
+ }
+
+ /**
+ * Returns the schema's default value
+ *
+ * If there is no default, returns the type's falsey value.
+ *
+ * @param array $schema
+ * @return mixed
+ */
+ final public function get_default_value( $schema ) {
+ if ( isset( $schema['default'] ) ) {
+ return $schema['default'];
+ }
+
+ // If you have something more complicated, use $schema['default'];
+ switch ( isset( $schema['type'] ) ? $schema['type'] : 'null' ) {
+ case 'string':
+ return '';
+ case 'integer':
+ case 'number':
+ return 0;
+ case 'object':
+ return (object) array();
+ case 'array':
+ return array();
+ case 'boolean':
+ return false;
+ case 'null':
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * The field's wrapped getter. Does permission checks and output preparation.
+ *
+ * This cannot be extended: implement `->get()` instead.
+ *
+ * @param mixed $object_data Probably an array. Whatever the endpoint returns.
+ * @param string $field_name Should always match `->field_name`
+ * @param WP_REST_Request $request
+ * @param string $object_type Should always match `->object_type`
+ * @return mixed
+ */
+ final public function get_for_response( $object_data, $field_name, $request, $object_type ) {
+ $permission_check = $this->get_permission_check( $object_data, $request );
+
+ if ( ! $permission_check ) {
+ /* translators: %s: get_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'get_permission_check' ), 'Jetpack 6.8' );
+ return $this->get_default_value( $this->get_schema() );
+ }
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $this->get_default_value( $this->get_schema() );
+ }
+
+ $value = $this->get( $object_data, $request );
+
+ return $this->prepare_for_response( $value, $request );
+ }
+
+ /**
+ * The field's wrapped setter. Does permission checks.
+ *
+ * This cannot be extended: implement `->update()` instead.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param string $field_name Should always match `->field_name`
+ * @param WP_REST_Request $request
+ * @param string $object_type Should always match `->object_type`
+ * @return void|WP_Error
+ */
+ final public function update_from_request( $value, $object_data, $field_name, $request, $object_type ) {
+ $permission_check = $this->update_permission_check( $value, $object_data, $request );
+
+ if ( ! $permission_check ) {
+ /* translators: %s: update_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must return either true or WP_Error.", 'jetpack' ), 'update_permission_check' ), 'Jetpack 6.8' );
+ /* translators: %s: the name of an API response field */
+ return new WP_Error( 'invalid_user_permission', sprintf( __( "You are not allowed to access the '%s' field.", 'jetpack' ), $this->field_name ) );
+ }
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $permission_check;
+ }
+
+ $updated = $this->update( $value, $object_data, $request );
+
+ if ( is_wp_error( $updated ) ) {
+ return $updated;
+ }
+ }
+
+ /**
+ * Permission Check for the field's getter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $object_data Whatever the endpoint would return for its response.
+ * @param WP_REST_Request $request
+ * @return true|WP_Error
+ */
+ public function get_permission_check( $object_data, $request ) {
+ /* translators: %s: get_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The field's "raw" getter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $object_data Whatever the endpoint would return for its response.
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ public function get( $object_data, $request ) {
+ /* translators: %s: get() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * Permission Check for the field's setter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param WP_REST_Request $request
+ * @return true|WP_Error
+ */
+ public function update_permission_check( $value, $object_data, $request ) {
+ /* translators: %s: update_permission_check() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update_permission_check', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The field's "raw" setter. Must be implemented in the inheriting class.
+ *
+ * @param mixed $value The new value for the field.
+ * @param mixed $object_data Probably a WordPress object (e.g., WP_Post)
+ * @param WP_REST_Request $request
+ * @return mixed
+ */
+ public function update( $value, $object_data, $request ) {
+ /* translators: %s: update() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::update', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * The JSON Schema for the field
+ *
+ * @link https://json-schema.org/understanding-json-schema/
+ * As of WordPress 5.0, Core currently understands:
+ * * type
+ * * string - not minLength, not maxLength, not pattern
+ * * integer - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
+ * * number - minimum, maximum, exclusiveMinimum, exclusiveMaximum, not multipleOf
+ * * boolean
+ * * null
+ * * object - properties, additionalProperties, not propertyNames, not dependencies, not patternProperties, not required
+ * * array: only lists, not tuples - items, not minItems, not maxItems, not uniqueItems, not contains
+ * * enum
+ * * format
+ * * date-time
+ * * email
+ * * ip
+ * * uri
+ * As of WordPress 5.0, Core does not support:
+ * * Multiple type: `type: [ 'string', 'integer' ]`
+ * * $ref, allOf, anyOf, oneOf, not, const
+ *
+ * @return array
+ */
+ public function get_schema() {
+ /* translators: %s: get_schema() */
+ _doing_it_wrong( 'WPCOM_REST_API_V2_Field_Controller::get_schema', sprintf( __( "Method '%s' must be overridden.", 'jetpack' ), __METHOD__ ), 'Jetpack 6.8' );
+ }
+
+ /**
+ * @param array $schema
+ * @param string $context REST API Request context
+ * @return bool
+ */
+ private function is_valid_for_context( $schema, $context ) {
+ return empty( $schema['context'] ) || in_array( $context, $schema['context'], true );
+ }
+
+ /**
+ * Removes properties that should not appear in the current
+ * request's context
+ *
+ * $context is a Core REST API Framework request attribute that is
+ * always one of:
+ * * view (what you see on the blog)
+ * * edit (what you see in an editor)
+ * * embed (what you see in, e.g., an oembed)
+ *
+ * Fields (and sub-fields, and sub-sub-...) can be flagged for a
+ * set of specific contexts via the field's schema.
+ *
+ * The Core API will filter out top-level fields with the wrong
+ * context, but will not recurse deeply enough into arrays/objects
+ * to remove all levels of sub-fields with the wrong context.
+ *
+ * This function handles that recursion.
+ *
+ * @param mixed $value
+ * @param array $schema
+ * @param string $context REST API Request context
+ * @return mixed Filtered $value
+ */
+ final public function filter_response_by_context( $value, $schema, $context ) {
+ if ( ! $this->is_valid_for_context( $schema, $context ) ) {
+ // We use this intentionally odd looking WP_Error object
+ // internally only in this recursive function (see below
+ // in the `object` case). It will never be output by the REST API.
+ // If we return this for the top level object, Core
+ // correctly remove the top level object from the response
+ // for us.
+ return new WP_Error( '__wrong-context__' );
+ }
+
+ switch ( $schema['type'] ) {
+ case 'array':
+ if ( ! isset( $schema['items'] ) ) {
+ return $value;
+ }
+
+ // Shortcircuit if we know none of the items are valid for this context.
+ // This would only happen in a strangely written schema.
+ if ( ! $this->is_valid_for_context( $schema['items'], $context ) ) {
+ return array();
+ }
+
+ // Recurse to prune sub-properties of each item.
+ foreach ( $value as $key => $item ) {
+ $value[ $key ] = $this->filter_response_by_context( $item, $schema['items'], $context );
+ }
+
+ return $value;
+ case 'object':
+ if ( ! isset( $schema['properties'] ) ) {
+ return $value;
+ }
+
+ foreach ( $value as $field_name => $field_value ) {
+ if ( isset( $schema['properties'][ $field_name ] ) ) {
+ $field_value = $this->filter_response_by_context( $field_value, $schema['properties'][ $field_name ], $context );
+ if ( is_wp_error( $field_value ) && '__wrong-context__' === $field_value->get_error_code() ) {
+ unset( $value[ $field_name ] );
+ } else {
+ // Respect recursion that pruned sub-properties of each property.
+ $value[ $field_name ] = $field_value;
+ }
+ }
+ }
+
+ return (object) $value;
+ }
+
+ return $value;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php
new file mode 100644
index 00000000..96a47a08
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-module-endpoints.php
@@ -0,0 +1,1708 @@
+<?php
+/**
+ * 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' );
+ }
+}
+
+class Jetpack_Core_API_Module_List_Endpoint {
+
+ /**
+ * 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( $request );
+ } else {
+ return $this->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']
+ && Jetpack::is_development_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' ),
+ join( ', ', $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' ),
+ join( ', ', $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
+ */
+class Jetpack_Core_API_Data extends Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
+
+ /**
+ * 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
+ *
+ * @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']
+ && Jetpack::is_development_mode()
+ ) {
+ $module['activated'] = false;
+ }
+
+ $i18n = jetpack_get_module_i18n( $request['slug'] );
+ if ( isset( $module['name'] ) ) {
+ $module['name'] = $i18n['name'];
+ }
+ if ( isset( $module['description'] ) ) {
+ $module['description'] = $i18n['description'];
+ $module['short_description'] = $i18n['description'];
+ }
+
+ 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';
+ }
+
+ foreach ( $settings as $setting => $properties ) {
+ switch ( $setting ) {
+ case 'lang_id':
+ if ( defined( 'WPLANG' ) ) {
+ // We can't affect this setting, so warn the client
+ $response[ $setting ] = 'error_const';
+ break;
+ }
+
+ 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' );
+ $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 'onboarding':
+ $business_address = get_option( 'jpo_business_address' );
+ $business_address = is_array( $business_address ) ? array_map( array( $this, 'decode_special_characters' ), $business_address ) : $business_address;
+
+ $response[ $setting ] = array(
+ 'siteTitle' => $this->decode_special_characters( get_option( 'blogname' ) ),
+ 'siteDescription' => $this->decode_special_characters( get_option( 'blogdescription' ) ),
+ 'siteType' => get_option( 'jpo_site_type' ),
+ 'homepageFormat' => get_option( 'jpo_homepage_format' ),
+ 'addContactForm' => intval( get_option( 'jpo_contact_page' ) ),
+ 'businessAddress' => $business_address,
+ 'installWooCommerce' => is_plugin_active( 'woocommerce/woocommerce.php' ),
+ 'stats' => Jetpack::is_active() && Jetpack::is_module_active( 'stats' ),
+ );
+ break;
+
+ default:
+ $response[ $setting ] = Jetpack_Core_Json_Api_Endpoints::cast_value( get_option( $setting ), $settings[ $setting ] );
+ break;
+ }
+ }
+
+ $response['akismet'] = is_plugin_active( 'akismet/akismet.php' );
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Decode the special HTML characters in a certain value.
+ *
+ * @since 5.8
+ *
+ * @param string $value Value to decode.
+ *
+ * @return string Value with decoded HTML characters.
+ */
+ private function decode_special_characters( $value ) {
+ return (string) htmlspecialchars_decode( $value, ENT_QUOTES );
+ }
+
+ /**
+ * 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_Error True 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 ( is_null( $request['slug'] ) ) {
+
+ // 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 can not 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 = Jetpack_Core_Json_Api_Endpoints::$user_permissions_error_msg;
+ }
+
+ // The module was not toggled.
+ if ( ! $updated ) {
+ $not_updated[ $option ] = $error;
+ }
+
+ // Remove module from list so we don't go through it again.
+ unset( $params[ $option ] );
+ }
+ }
+
+ 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 ( defined( 'WPLANG' ) || ! current_user_can( 'install_languages' ) ) {
+ // We can't affect this setting
+ $updated = false;
+ break;
+ }
+
+ if ( $value === 'en_US' || 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 ( $language === false ) {
+ // The language pack download failed.
+ $updated = false;
+ break;
+ }
+ $updated = get_option( 'WPLANG' ) === $language ? true : update_option( 'WPLANG', $language );
+ break;
+
+ case 'monitor_receive_notifications':
+ $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 ( 'create' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.createPostByEmailAddress',
+ esc_html__( 'Unable to create the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } elseif ( 'regenerate' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.regeneratePostByEmailAddress',
+ esc_html__( 'Unable to regenerate the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } elseif ( 'delete' == $value ) {
+ $result = $this->_process_post_by_email(
+ 'jetpack.deletePostByEmailAddress',
+ esc_html__( 'Unable to delete the Post by Email address. Please try again later.', 'jetpack' )
+ );
+ } else {
+ $result = false;
+ }
+
+ // 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 ) {
+ $updated = true;
+ } elseif ( is_array( $result ) && isset( $result['message'] ) ) {
+ $error = $result['message'];
+ }
+ break;
+
+ case 'jetpack_protect_key':
+ $protect = Jetpack_Protect_Module::instance();
+ if ( 'create' == $value ) {
+ $result = $protect->get_protect_key();
+ } else {
+ $result = false;
+ }
+
+ // If we got one of Protect keys, consider it done.
+ if ( preg_match( '/[a-z0-9]{40,}/i', $result ) ) {
+ $response[$option] = $result;
+ $updated = true;
+ }
+ break;
+
+ case 'jetpack_protect_global_whitelist':
+ $updated = jetpack_protect_save_whitelist( 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 = $grouped_options_current = (array) Jetpack_Options::get_option( 'relatedposts' );
+ $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 'google':
+ case 'bing':
+ case 'pinterest':
+ case 'yandex':
+ $grouped_options = $grouped_options_current = (array) get_option( 'verification_services_codes' );
+
+ // 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( strip_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 'hide_smile':
+ case 'version':
+ $grouped_options = $grouped_options_current = (array) get_option( 'stats_options' );
+ $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 '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 ? update_option( $option, (bool) $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' ) ) ) {
+ $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 = $grouped_options_current = (array) get_option( 'jetpack_wga' );
+ $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_dash_app_card':
+ case 'dismiss_empty_stats_card':
+ // If option value was the same, consider it done.
+ $updated = get_option( $option ) != $value ? update_option( $option, (bool) $value ) : true;
+ break;
+
+ case 'onboarding':
+ jetpack_require_lib( 'widgets' );
+ // Break apart and set Jetpack onboarding options.
+ $result = $this->_process_onboarding( (array) $value );
+ if ( empty( $result ) ) {
+ $updated = true;
+ } else {
+ $error = sprintf( esc_html__( 'Onboarding failed to process: %s', 'jetpack' ), $result );
+ $updated = false;
+ }
+ break;
+
+ default:
+ // If option value was the same, consider it done.
+ $updated = get_option( $option ) != $value ? 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' ),
+ join( ', ', $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 .= ' ' . join( '. ', $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
+ *
+ * @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 ) {
+ if ( isset( $data['end'] ) && $data['end'] ) {
+ return Jetpack::invalidate_onboarding_token()
+ ? ''
+ : esc_html__( "The onboarding token couldn't be deleted.", 'jetpack' );
+ }
+
+ $error = array();
+
+ if ( ! empty( $data['siteTitle'] ) ) {
+ // If option value was the same, consider it done.
+ if ( ! ( update_option( 'blogname', $data['siteTitle'] ) || get_option( 'blogname' ) == $data['siteTitle'] ) ) {
+ $error[] = 'siteTitle';
+ }
+ }
+
+ if ( isset( $data['siteDescription'] ) ) {
+ // If option value was the same, consider it done.
+ if ( ! ( update_option( 'blogdescription', $data['siteDescription'] ) || get_option( 'blogdescription' ) == $data['siteDescription'] ) ) {
+ $error[] = 'siteDescription';
+ }
+ }
+
+ $site_title = get_option( 'blogname' );
+ $author = get_current_user_id() || 1;
+
+ if ( ! empty( $data['siteType'] ) ) {
+ if ( ! ( update_option( 'jpo_site_type', $data['siteType'] ) || get_option( 'jpo_site_type' ) == $data['siteType'] ) ) {
+ $error[] = 'siteType';
+ }
+ }
+
+ if ( isset( $data['homepageFormat'] ) ) {
+ // If $data['homepageFormat'] is 'posts', we have nothing to do since it's WordPress' default
+ // if it exists, just update
+ $homepage_format = get_option( 'jpo_homepage_format' );
+ if ( ! $homepage_format || $homepage_format !== $data['homepageFormat'] ) {
+ if ( 'page' === $data['homepageFormat'] ) {
+ if ( ! ( update_option( 'show_on_front', 'page' ) || get_option( 'show_on_front' ) == 'page' ) ) {
+ $error[] = 'homepageFormat';
+ }
+
+ $home = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references the home page of a site, also called front page. */
+ 'post_title' => esc_html_x( 'Home Page', 'The home page of a website.', 'jetpack' ),
+ 'post_content' => sprintf( esc_html__( 'Welcome to %s.', 'jetpack' ), $site_title ),
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $home ) {
+ $error[] = 'home insert: 0';
+ } elseif ( is_wp_error( $home ) ) {
+ $error[] = 'home creation: '. $home->get_error_message();
+ }
+ if ( ! ( update_option( 'page_on_front', $home ) || get_option( 'page_on_front' ) == $home ) ) {
+
+ $error[] = 'home set';
+ }
+
+ $blog = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references the page where blog posts are listed. */
+ 'post_title' => esc_html_x( 'Blog', 'The blog of a website.', 'jetpack' ),
+ 'post_content' => sprintf( esc_html__( 'These are the latest posts in %s.', 'jetpack' ), $site_title ),
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $blog ) {
+ $error[] = 'blog insert: 0';
+ } elseif ( is_wp_error( $blog ) ) {
+ $error[] = 'blog creation: '. $blog->get_error_message();
+ }
+ if ( ! ( update_option( 'page_for_posts', $blog ) || get_option( 'page_for_posts' ) == $blog ) ) {
+ $error[] = 'blog set';
+ }
+ } else {
+ $front_page = get_option( 'page_on_front' );
+ $posts_page = get_option( 'page_for_posts' );
+ if ( $posts_page && get_post( $posts_page ) ) {
+ wp_delete_post( $posts_page );
+ }
+ if ( $front_page && get_post( $front_page ) ) {
+ wp_delete_post( $front_page );
+ }
+ update_option( 'show_on_front', 'posts' );
+ }
+ }
+ update_option( 'jpo_homepage_format', $data['homepageFormat'] );
+ }
+
+ // Setup contact page and add a form and/or business info
+ $contact_page = '';
+ if ( ! empty( $data['addContactForm'] ) && ! get_option( 'jpo_contact_page' ) ) {
+ $contact_form_module_active = Jetpack::is_module_active( 'contact-form' );
+ if ( ! $contact_form_module_active ) {
+ $contact_form_module_active = Jetpack::activate_module( 'contact-form', false, false );
+ }
+
+ if ( $contact_form_module_active ) {
+ $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]';
+ } else {
+ $error[] = 'contact-form activate';
+ }
+ }
+
+ if ( isset( $data['businessPersonal'] ) && 'business' === $data['businessPersonal'] ) {
+ $contact_page .= "\n" . join( "\n", $data['businessInfo'] );
+ }
+
+ if ( ! empty( $contact_page ) ) {
+ $form = wp_insert_post( array(
+ 'post_type' => 'page',
+ /* translators: this references a page with contact details and possibly a form. */
+ 'post_title' => esc_html_x( 'Contact us', 'Contact page for your website.', 'jetpack' ),
+ 'post_content' => esc_html__( 'Send us a message!', 'jetpack' ) . "\n" . $contact_page,
+ 'post_status' => 'publish',
+ 'post_author' => $author,
+ ) );
+ if ( 0 == $form ) {
+ $error[] = 'form insert: 0';
+ } elseif ( is_wp_error( $form ) ) {
+ $error[] = 'form creation: '. $form->get_error_message();
+ } else {
+ update_option( 'jpo_contact_page', $form );
+ }
+ }
+
+ if ( isset( $data['businessAddress'] ) ) {
+ $handled_business_address = self::handle_business_address( $data['businessAddress'] );
+ if ( is_wp_error( $handled_business_address ) ) {
+ $error[] = 'BusinessAddress';
+ }
+ }
+
+ if ( ! empty( $data['installWooCommerce'] ) ) {
+ jetpack_require_lib( 'plugins' );
+ $wc_install_result = Jetpack_Plugins::install_and_activate_plugin( 'woocommerce' );
+ delete_transient( '_wc_activation_redirect' ); // Redirecting to WC setup would kill our users' flow
+ if ( is_wp_error( $wc_install_result ) ) {
+ $error[] = 'woocommerce installation';
+ }
+ }
+
+ if ( ! empty( $data['stats'] ) ) {
+ if ( Jetpack::is_active() ) {
+ $stats_module_active = Jetpack::is_module_active( 'stats' );
+ if ( ! $stats_module_active ) {
+ $stats_module_active = Jetpack::activate_module( 'stats', false, false );
+ }
+
+ if ( ! $stats_module_active ) {
+ $error[] = 'stats activate';
+ }
+ } else {
+ $error[] = 'stats not connected';
+ }
+ }
+
+ return empty( $error )
+ ? ''
+ : join( ', ', $error );
+ }
+
+ /**
+ * Add or update Business Address widget.
+ *
+ * @param array $address Array of business address fields.
+ *
+ * @return WP_Error|true True if the data was saved correctly.
+ */
+ static function handle_business_address( $address ) {
+ $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.
+ */
+ 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 ( strpos( $widget, 'widget_contact_info' ) !== false ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calls WPCOM through authenticated request to create, regenerate or delete the Post by Email address.
+ * @todo: When all settings are updated to use endpoints, move this to the Post by Email module and replace __process_ajax_proxy_request.
+ *
+ * @since 4.3.0
+ *
+ * @param string $endpoint Process to call on WPCOM to create, regenerate or delete the Post by Email address.
+ * @param string $error Error message to return.
+ *
+ * @return array
+ */
+ private function _process_post_by_email( $endpoint, $error ) {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ return array( 'message' => $error );
+ }
+
+ $this->xmlrpc->query( $endpoint );
+
+ if ( $this->xmlrpc->isError() ) {
+ return array( 'message' => $error );
+ }
+
+ $response = $this->xmlrpc->getResponse();
+ if ( empty( $response ) ) {
+ return array( 'message' => $error );
+ }
+
+ // Used only in Jetpack_Core_Json_Api_Endpoints::get_remote_value.
+ update_option( 'post_by_email_address' . get_current_user_id(), $response );
+
+ return $response;
+ }
+
+ /**
+ * 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 ) {
+ $req_params = $request->get_params();
+ if ( ! empty( $req_params['onboarding']['token'] ) && isset( $req_params['rest_route'] ) ) {
+ return Jetpack::validate_onboarding_token_action( $req_params['onboarding']['token'], $req_params['rest_route'] );
+ }
+
+ 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' ) ) ) {
+ $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' );
+ }
+ }
+}
+
+class Jetpack_Core_API_Module_Data_Endpoint {
+
+ 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
+ *
+ * @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 get_site_option( 'jetpack_protect_blocked_attempts' );
+ }
+
+ 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() {
+ if ( ! is_wp_error( $status = $this->akismet_is_active_and_registered() ) ) {
+ return rest_ensure_response( Akismet_Admin::get_stats( Akismet::get_api_key() ) );
+ } else {
+ return $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() {
+ if ( is_wp_error( $akismet_exists = $this->akismet_class_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';
+ }
+
+ if ( ! function_exists( 'stats_get_from_restapi' ) ) {
+ require_once( JETPACK__PLUGIN_DIR . 'modules/stats.php' );
+ }
+
+ switch ( $range ) {
+
+ // This is always called first on page load
+ case 'day':
+ $initial_stats = stats_get_from_restapi();
+ return rest_ensure_response( array(
+ 'general' => $initial_stats,
+
+ // Build data for 'day' as if it was stats_get_from_restapi( array(), 'visits?unit=day&quantity=30' );
+ 'day' => isset( $initial_stats->visits )
+ ? $initial_stats->visits
+ : array(),
+ ) );
+ case 'week':
+ return rest_ensure_response( array(
+ 'week' => stats_get_from_restapi( array(), 'visits?unit=week&quantity=14' ),
+ ) );
+ case 'month':
+ return rest_ensure_response( array(
+ 'month' => stats_get_from_restapi( array(), 'visits?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;
+ } else if ( 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;
+ }
+ }
+ }
+
+ 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' ),
+ join( ', ', $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' ) ) );
+ if ( false == $data ) {
+ return rest_ensure_response( array(
+ 'code' => 'not_registered',
+ 'message' => esc_html__( 'Could not connect to VaultPress.', 'jetpack' )
+ ) );
+ } else if ( is_wp_error( $data ) || ! isset( $data->backups->last_backup ) ) {
+ return $data;
+ } else if ( 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(
+ __( 'Your site was successfully backed-up %s ago.', 'jetpack' ),
+ human_time_diff(
+ $data->backups->last_backup,
+ current_time( 'timestamp' )
+ )
+ )
+ ),
+ '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' );
+ }
+}
+
+/**
+ * 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' );
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php
new file mode 100644
index 00000000..68327f51
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * This is the endpoint class for `/site` endpoints.
+ *
+ */
+class Jetpack_Core_API_Site_Endpoint {
+
+ /**
+ * Returns the result of `/sites/%s/features` endpoint call.
+ * @return object $features has 'active' and 'available' properties each of which contain feature slugs.
+ * 'active' is a simple array of slugs that are active on the current plan.
+ * 'available' is an object with keys that represent feature slugs and values are arrays
+ * of plan slugs that enable these features
+ */
+ public static function get_features() {
+
+ // Make the API request
+ $request = sprintf( '/sites/%d/features', Jetpack_Options::get_option( 'id' ) );
+ $response = Jetpack_Client::wpcom_json_api_request_as_blog( $request, '1.1' );
+
+ // Bail if there was an error or malformed response
+ if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) {
+ return new WP_Error(
+ 'failed_to_fetch_data',
+ esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ // Decode the results
+ $results = json_decode( $response['body'], true );
+
+ // Bail if there were no results or plan details returned
+ if ( ! is_array( $results ) ) {
+ return new WP_Error(
+ 'failed_to_fetch_data',
+ esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ return rest_ensure_response( array(
+ 'code' => 'success',
+ 'message' => esc_html__( 'Site features correctly received.', 'jetpack' ),
+ 'data' => wp_remote_retrieve_body( $response ),
+ )
+ );
+ }
+
+ /**
+ * Check that the current user has permissions to request information about this site.
+ *
+ * @since 5.1.0
+ *
+ * @return bool
+ */
+ public static function can_request() {
+ return current_user_can( 'jetpack_manage_modules' );
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php
new file mode 100644
index 00000000..ffd62bb3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-widgets-endpoints.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Widget information getter endpoint.
+ *
+ */
+class Jetpack_Core_API_Widget_Endpoint {
+
+ /**
+ * @since 5.5.0
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $id Widget id.
+ * }
+ *
+ * @return WP_REST_Response|WP_Error A REST response if the request was served successfully, otherwise an error.
+ */
+ public function process( $request ) {
+ $widget_base = _get_widget_id_base( $request['id'] );
+ $widget_id = (int) substr( $request['id'], strlen( $widget_base ) + 1 );
+
+ switch( $widget_base ) {
+ case 'milestone_widget':
+ $instances = get_option( 'widget_milestone_widget', array() );
+
+ if (
+ class_exists( 'Milestone_Widget' )
+ && is_active_widget( false, $widget_base . '-' . $widget_id, $widget_base )
+ && isset( $instances[ $widget_id ] )
+ ) {
+ $instance = $instances[ $widget_id ];
+ $widget = new Milestone_Widget();
+ return $widget->get_widget_data( $instance );
+ }
+ }
+
+ return new WP_Error(
+ 'not_found',
+ esc_html__( 'The requested widget was not found.', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Check that the current user has permissions to view widget information.
+ * For the currently supported widget there are no permissions required.
+ *
+ * @since 5.5.0
+ *
+ * @return bool
+ */
+ public function can_request() {
+ return true;
+ }
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php
new file mode 100644
index 00000000..abfc8627
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/class.jetpack-core-api-xmlrpc-consumer-endpoint.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * This is the base class for every Core API endpoint that needs an XMLRPC client.
+ *
+ */
+abstract class Jetpack_Core_API_XMLRPC_Consumer_Endpoint {
+
+ /**
+ * An instance of the Jetpack XMLRPC client to make WordPress.com requests
+ *
+ * @private
+ * @var Jetpack_IXR_Client
+ */
+ protected $xmlrpc;
+
+ /**
+ *
+ * @since 4.3.0
+ *
+ * @param Jetpack_IXR_Client $xmlrpc
+ */
+ public function __construct( $xmlrpc = null ) {
+ $this->xmlrpc = $xmlrpc;
+ }
+
+ /**
+ * Checks if the site is public and returns the result.
+ *
+ * @since 4.3.0
+ *
+ * @return Boolean $is_public
+ */
+ protected function is_site_public() {
+ if ( $this->xmlrpc->query( 'jetpack.isSitePubliclyAccessible', home_url() ) ) {
+ return $this->xmlrpc->getResponse();
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php b/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php
new file mode 100644
index 00000000..2b26f78c
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/load-wpcom-endpoints.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * Loader for WP REST API endpoints that are synced with WP.com.
+ *
+ * On WP.com see:
+ * - wp-content/mu-plugins/rest-api.php
+ * - wp-content/rest-api-plugins/jetpack-endpoints/
+ */
+
+function wpcom_rest_api_v2_load_plugin_files( $file_pattern ) {
+ $plugins = glob( dirname( __FILE__ ) . '/' . $file_pattern );
+
+ if ( ! is_array( $plugins ) ) {
+ return;
+ }
+
+ foreach ( array_filter( $plugins, 'is_file' ) as $plugin ) {
+ require_once $plugin;
+ }
+}
+
+// API v2 plugins: define a class, then call this function.
+function wpcom_rest_api_v2_load_plugin( $class_name ) {
+ global $wpcom_rest_api_v2_plugins;
+
+ if ( ! isset( $wpcom_rest_api_v2_plugins ) ) {
+ $_GLOBALS['wpcom_rest_api_v2_plugins'] = $wpcom_rest_api_v2_plugins = array();
+ }
+
+ if ( ! isset( $wpcom_rest_api_v2_plugins[ $class_name ] ) ) {
+ $wpcom_rest_api_v2_plugins[ $class_name ] = new $class_name;
+ }
+}
+
+require dirname( __FILE__ ) . '/class-wpcom-rest-field-controller.php';
+
+// Now load the endpoint files.
+wpcom_rest_api_v2_load_plugin_files( 'wpcom-endpoints/*.php' );
+wpcom_rest_api_v2_load_plugin_files( 'wpcom-fields/*.php' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php
new file mode 100644
index 00000000..2bf80939
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/business-hours.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Business Hours: Localized week
+ *
+ * @since 7.1
+ */
+class WPCOM_REST_API_V2_Endpoint_Business_Hours extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'business-hours';
+ // This endpoint *does not* need to connect directly to Jetpack sites.
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ // GET /sites/<blog_id>/business-hours/localized-week - Return the localized
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/localized-week', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_localized_week' ),
+ )
+ ) );
+ }
+
+ /**
+ * Retreives localized business hours
+ *
+ * @return array data object containing information about business hours
+ */
+ public function get_localized_week() {
+ global $wp_locale;
+
+ return array(
+ 'days' => array(
+ 'Sun' => $wp_locale->get_weekday( 0 ),
+ 'Mon' => $wp_locale->get_weekday( 1 ),
+ 'Tue' => $wp_locale->get_weekday( 2 ),
+ 'Wed' => $wp_locale->get_weekday( 3 ),
+ 'Thu' => $wp_locale->get_weekday( 4 ),
+ 'Fri' => $wp_locale->get_weekday( 5 ),
+ 'Sat' => $wp_locale->get_weekday( 6 ),
+ ),
+ 'startOfWeek' => (int) get_option( 'start_of_week', 0 ),
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Business_Hours' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php
new file mode 100644
index 00000000..354880ed
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mailchimp.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * Mailchimp: Get Mailchimp Status.
+ * API to determine if current site has linked Mailchimp account and mailing list selected.
+ * This API is meant to be used in Jetpack and on WPCOM.
+ *
+ * @since 7.1
+ */
+class WPCOM_REST_API_V2_Endpoint_Mailchimp extends WP_REST_Controller {
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'mailchimp';
+ $this->wpcom_is_wpcom_only_endpoint = true;
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_mailchimp_status' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Check if MailChimp is set up properly.
+ *
+ * @return bool
+ */
+ private function is_connected() {
+ $option = get_option( 'jetpack_mailchimp' );
+ if ( ! $option ) {
+ return false;
+ }
+ $data = json_decode( $option, true );
+ if ( ! $data ) {
+ return false;
+ }
+ return isset( $data['follower_list_id'], $data['keyring_id'] );
+ }
+
+ /**
+ * Get the status of current blog's Mailchimp connection
+ *
+ * @return mixed
+ * code:string (connected|unconnected),
+ * connect_url:string
+ * site_id:int
+ */
+ public function get_mailchimp_status() {
+ $is_wpcom = ( defined( 'IS_WPCOM' ) && IS_WPCOM );
+ $site_id = $is_wpcom ? get_current_blog_id() : Jetpack_Options::get_option( 'id' );
+ if ( ! $site_id ) {
+ return new WP_Error(
+ 'unavailable_site_id',
+ __( 'Sorry, something is wrong with your Jetpack connection.', 'jetpack' ),
+ 403
+ );
+ }
+ $connect_url = sprintf( 'https://wordpress.com/marketing/connections/%s', rawurlencode( $site_id ) );
+ return array(
+ 'code' => $this->is_connected() ? 'connected' : 'not_connected',
+ 'connect_url' => $connect_url,
+ 'site_id' => $site_id,
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Mailchimp' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php
new file mode 100644
index 00000000..a10a4056
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/gutenberg-available-extensions.php
@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * Gutenberg: List Available Gutenberg Extensions (Blocks and Plugins)
+ *
+ * [
+ * { # Availabilty Object. See schema for more detail.
+ * available: (boolean) Whether the extension is available
+ * unavailable_reason: (string) Reason for the extension not being available
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'gutenberg';
+ $this->wpcom_is_site_specific_endpoint = true;
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route( $this->namespace, $this->rest_base . '/available-extensions', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( 'Jetpack_Gutenberg', 'get_availability' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_item_schema' ),
+ ) );
+ }
+
+ /**
+ * Return the available Gutenberg extensions schema
+ *
+ * @return array Available Gutenberg extensions schema
+ */
+ public function get_public_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'gutenberg-available-extensions',
+ 'type' => 'object',
+ 'properties' => array(
+ 'available' => array(
+ 'description' => __( 'Whether the extension is available', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'unavailable_reason' => array(
+ 'description' => __( 'Reason for the extension not being available', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Ensure the user has proper permissions
+ *
+ * @return boolean
+ */
+ public function get_items_permission_check() {
+ return current_user_can( 'edit_posts' );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Gutenberg_Available_Extensions' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php
new file mode 100644
index 00000000..a05769b2
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/hello.php
@@ -0,0 +1,22 @@
+<?php
+
+class WPCOM_REST_API_V2_Endpoint_Hello {
+ public function __construct() {
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route( 'wpcom/v2', '/hello', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_data' ),
+ ),
+ ) );
+ }
+
+ public function get_data( $request ) {
+ return array( 'hello' => 'world' );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Hello' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php
new file mode 100644
index 00000000..ec997739
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/memberships.php
@@ -0,0 +1,187 @@
+<?php // phpcs:disable WordPress.Files.FileName.InvalidClassFileName
+/**
+ * Memberships: API to communicate with "product" database.
+ *
+ * @package Jetpack
+ * @since 7.3.0
+ */
+
+/**
+ * Class WPCOM_REST_API_V2_Endpoint_Memberships
+ * This introduces V2 endpoints.
+ */
+class WPCOM_REST_API_V2_Endpoint_Memberships extends WP_REST_Controller {
+
+ /**
+ * WPCOM_REST_API_V2_Endpoint_Memberships constructor.
+ */
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'memberships';
+ $this->wpcom_is_wpcom_only_endpoint = true;
+ $this->wpcom_is_site_specific_endpoint = true;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base . '/status',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_status' ),
+ 'permission_callback' => array( $this, 'get_status_permission_check' ),
+ ),
+ )
+ );
+ register_rest_route(
+ $this->namespace,
+ $this->rest_base . '/product',
+ array(
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_product' ),
+ 'permission_callback' => array( $this, 'get_status_permission_check' ),
+ 'args' => array(
+ 'title' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'price' => array(
+ 'type' => 'float',
+ 'required' => true,
+ ),
+ 'currency' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'interval' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Ensure the user has proper permissions
+ *
+ * @return boolean
+ */
+ public function get_status_permission_check() {
+ return current_user_can( 'edit_posts' );
+ }
+
+ /**
+ * Do create a product based on data, or pass request to wpcom.
+ *
+ * @param object $request - request passed from WP.
+ *
+ * @return array|WP_Error
+ */
+ public function create_product( $request ) {
+ if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
+ require_lib( 'memberships' );
+ $connected_destination_account_id = Jetpack_Memberships::get_connected_account_id();
+ if ( ! $connected_destination_account_id ) {
+ return new WP_Error( 'no-destination-account', __( 'Please set up a Stripe account for this site first', 'jetpack' ) );
+ }
+ $product = Memberships_Product::create(
+ get_current_blog_id(),
+ array(
+ 'title' => $request['title'],
+ 'price' => $request['price'],
+ 'currency' => $request['currency'],
+ 'interval' => $request['interval'],
+ 'connected_destination_account_id' => $connected_destination_account_id,
+ )
+ );
+ if ( is_wp_error( $product ) ) {
+ return new WP_Error( $product->get_error_code(), __( 'Creating product has failed.', 'jetpack' ) );
+ }
+ return $product->to_array();
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ "/sites/$blog_id/{$this->rest_base}/product",
+ 'v2',
+ array(
+ 'method' => 'POST',
+ ),
+ array(
+ 'title' => $request['title'],
+ 'price' => $request['price'],
+ 'currency' => $request['currency'],
+ 'interval' => $request['interval'],
+ )
+ );
+ if ( is_wp_error( $response ) ) {
+ if ( $response->get_error_code() === 'missing_token' ) {
+ return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), 404 );
+ }
+ return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), 404 );
+ }
+ $data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
+ // If endpoint returned error, we have to detect it.
+ if ( 200 !== $response['response']['code'] && $data['code'] && $data['message'] ) {
+ return new WP_Error( $data['code'], $data['message'], 401 );
+ }
+ return $data;
+ }
+
+ return $request;
+ }
+
+ /**
+ * Get a status of connection for the site. If this is Jetpack, pass the request to wpcom.
+ *
+ * @return array|WP_Error
+ */
+ public function get_status() {
+ $connected_account_id = Jetpack_Memberships::get_connected_account_id();
+ $connect_url = '';
+ if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
+ require_lib( 'memberships' );
+ $blog_id = get_current_blog_id();
+ if ( ! $connected_account_id ) {
+ $connect_url = get_memberships_connected_account_redirect( get_current_user_id(), $blog_id );
+ }
+ $products = get_memberships_plans( $blog_id );
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ $response = Jetpack_Client::wpcom_json_api_request_as_user(
+ "/sites/$blog_id/{$this->rest_base}/status",
+ 'v2',
+ array(),
+ null
+ );
+ if ( is_wp_error( $response ) ) {
+ if ( $response->get_error_code() === 'missing_token' ) {
+ return new WP_Error( 'missing_token', __( 'Please connect your user account to WordPress.com', 'jetpack' ), 404 );
+ }
+ return new WP_Error( 'wpcom_connection_error', __( 'Could not connect to WordPress.com', 'jetpack' ), 404 );
+ }
+ $data = isset( $response['body'] ) ? json_decode( $response['body'], true ) : null;
+ if ( ! $connected_account_id ) {
+ $connect_url = empty( $data['connect_url'] ) ? '' : $data['connect_url'];
+ }
+ $products = empty( $data['products'] ) ? array() : $data['products'];
+ }
+ return array(
+ 'connected_account_id' => $connected_account_id,
+ 'connect_url' => $connect_url,
+ 'products' => $products,
+ );
+ }
+}
+
+if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || Jetpack::is_active() ) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Memberships' );
+}
+
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php
new file mode 100644
index 00000000..6e04a289
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connection-test-results.php
@@ -0,0 +1,121 @@
+<?php
+
+require_once dirname( __FILE__ ) . '/publicize-connections.php';
+
+/**
+ * Publicize: List Connection Test Result Data
+ *
+ * All the same data as the Publicize Connections Endpoint, plus test results.
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Connection_Test_Results extends WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections {
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/connection-test-results';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Adds the test results properties to the Connection schema.
+ *
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-connection-test-results',
+ 'type' => 'object',
+ 'properties' => $this->get_connection_schema_properties() + array(
+ 'test_success' => array(
+ 'description' => __( 'Did the Publicize Connection test pass?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'test_message' => array(
+ 'description' => __( 'Publicize Connection success or error message', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'can_refresh' => array(
+ 'description' => __( 'Can the current user refresh the Publicize Connection?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ 'refresh_text' => array(
+ 'description' => __( 'Message instructing the user to refresh their Connection to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'refresh_url' => array(
+ 'description' => __( 'URL for refreshing the Connection to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * @param WP_REST_Request
+ * @see Publicize::get_publicize_conns_test_results()
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ global $publicize;
+
+ $items = $this->get_connections();
+
+ $test_results = $publicize->get_publicize_conns_test_results();
+ $test_results_by_unique_id = array();
+ foreach ( $test_results as $test_result ) {
+ $test_results_by_unique_id[ $test_result['unique_id'] ] = $test_result;
+ }
+
+ $mapping = array(
+ 'test_success' => 'connectionTestPassed',
+ 'test_message' => 'connectionTestMessage',
+ 'can_refresh' => 'userCanRefresh',
+ 'refresh_text' => 'refreshText',
+ 'refresh_url' => 'refreshURL',
+ );
+
+ foreach ( $items as &$item ) {
+ $test_result = $test_results_by_unique_id[ $item['id'] ];
+
+ foreach ( $mapping as $field => $test_result_field ) {
+ $item[ $field ] = $test_result[ $test_result_field ];
+ }
+ }
+
+ if ( 'linkedin' === $item['id'] && 'must_reauth' === $test_result['connectionTestPassed'] ) {
+ $item['test_success'] = 'must_reauth';
+ }
+
+ $response = rest_ensure_response( $items );
+
+ $response->header( 'X-WP-Total', count( $items ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Connection_Test_Results' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php
new file mode 100644
index 00000000..34d6b2a6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-connections.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * Publicize: List Connections
+ *
+ * [
+ * { # Connnection Object. See schema for more detail.
+ * id: (string) Connection unique_id
+ * service_name: (string) Service slug
+ * display_name: (string) User name/display name of user/connection on Service
+ * global: (boolean) Is the Connection available to all users of the site?
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections extends WP_REST_Controller {
+ /**
+ * Flag to help WordPress.com decide where it should look for
+ * Publicize data. Ignored for direct requests to Jetpack sites.
+ *
+ * @var bool $wpcom_is_wpcom_only_endpoint
+ */
+ public $wpcom_is_wpcom_only_endpoint = true;
+
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/connections';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Helper for generating schema. Used by this endpoint and by the
+ * Connection Test Result endpoint.
+ *
+ * @internal
+ * @return array
+ */
+ protected function get_connection_schema_properties() {
+ return array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service_name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'display_name' => array(
+ 'description' => __( 'Username of the connected account', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'global' => array(
+ 'description' => __( 'Is this connection available to all users?', 'jetpack' ),
+ 'type' => 'boolean',
+ ),
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-connection',
+ 'type' => 'object',
+ 'properties' => $this->get_connection_schema_properties(),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Helper for retrieving Connections. Used by this endpoint and by
+ * the Connection Test Result endpoint.
+ *
+ * @internal
+ * @return array
+ */
+ protected function get_connections() {
+ global $publicize;
+
+ $items = array();
+
+ foreach ( (array) $publicize->get_services( 'connected' ) as $service_name => $connections ) {
+ foreach ( $connections as $connection ) {
+ $connection_meta = $publicize->get_connection_meta( $connection );
+ $connection_data = $connection_meta['connection_data'];
+
+ $items[] = array(
+ 'id' => (string) $publicize->get_connection_unique_id( $connection ),
+ 'service_name' => $service_name,
+ 'display_name' => $publicize->get_display_name( $service_name, $connection ),
+ // We expect an integer, but do loose comparison below in case some other type is stored
+ 'global' => 0 == $connection_data['user_id'],
+ );
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ $items = array();
+
+ foreach ( $this->get_connections() as $item ) {
+ $items[] = $this->prepare_item_for_response( $item, $request );
+ }
+
+ $response = rest_ensure_response( $items );
+ $response->header( 'X-WP-Total', count( $items ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+
+ /**
+ * Filters out data based on ?_fields= request parameter
+ *
+ * @param array $connection
+ * @param WP_REST_Request $request
+ * @return array filtered $connection
+ */
+ public function prepare_item_for_response( $connection, $request ) {
+ if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) {
+ return $connection;
+ }
+
+ $fields = $this->get_fields_for_response( $request );
+
+ $response_data = array();
+ foreach ( $connection as $field => $value ) {
+ if ( in_array( $field, $fields, true ) ) {
+ $response_data[ $field ] = $value;
+ }
+ }
+
+ return $response_data;
+ }
+
+ /**
+ * Verify that user can access Publicize data
+ *
+ * @return true|WP_Error
+ */
+ public function get_items_permission_check() {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data on this site.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Connections' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php
new file mode 100644
index 00000000..4641b218
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/publicize-services.php
@@ -0,0 +1,167 @@
+<?php
+
+/**
+ * Publicize: List Publicize Services
+ *
+ * [
+ * { # Service Object. See schema for more detail.
+ * name: (string) Service slug
+ * label: (string) Human readable label for the Service
+ * url: (string) Connect URL
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.8
+ */
+class WPCOM_REST_API_V2_Endpoint_List_Publicize_Services extends WP_REST_Controller {
+ /**
+ * Flag to help WordPress.com decide where it should look for
+ * Publicize data. Ignored for direct requests to Jetpack sites.
+ *
+ * @var bool $wpcom_is_wpcom_only_endpoint
+ */
+ public $wpcom_is_wpcom_only_endpoint = true;
+
+ public function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'publicize/services';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ /**
+ * Called automatically on `rest_api_init()`.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_items' ),
+ 'permission_callback' => array( $this, 'get_items_permission_check' ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function get_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-service',
+ 'type' => 'object',
+ 'properties' => array(
+ 'name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'label' => array(
+ 'description' => __( 'Human readable label for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'url' => array(
+ 'description' => __( 'The URL used to connect to the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'format' => 'uri',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Retrieves available Publicize Services.
+ *
+ * @see Publicize::get_available_service_data()
+ *
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response suitable for 1-page collection
+ */
+ public function get_items( $request ) {
+ global $publicize;
+ /**
+ * We need this because Publicize::get_available_service_data() uses `Jetpack_Keyring_Service_Helper`
+ * and `Jetpack_Keyring_Service_Helper` relies on `menu_page_url()`.
+ *
+ * We also need add_submenu_page(), as the URLs for connecting each service
+ * rely on the `sharing` menu subpage being present.
+ */
+ include_once ABSPATH . 'wp-admin/includes/plugin.php';
+
+ // The `sharing` submenu page must exist for service connect URLs to be correct.
+ add_submenu_page( 'options-general.php', '', '', 'manage_options', 'sharing', '__return_empty_string' );
+
+ $services_data = $publicize->get_available_service_data();
+
+ $services = array();
+ foreach ( $services_data as $service_data ) {
+ $services[] = $this->prepare_item_for_response( $service_data, $request );
+ }
+
+ $response = rest_ensure_response( $services );
+ $response->header( 'X-WP-Total', count( $services ) );
+ $response->header( 'X-WP-TotalPages', 1 );
+
+ return $response;
+ }
+
+ /**
+ * Filters out data based on ?_fields= request parameter
+ *
+ * @param array $service
+ * @param WP_REST_Request $request
+ * @return array filtered $service
+ */
+ public function prepare_item_for_response( $service, $request ) {
+ if ( ! is_callable( array( $this, 'get_fields_for_response' ) ) ) {
+ return $service;
+ }
+
+ $fields = $this->get_fields_for_response( $request );
+
+ $response_data = array();
+ foreach ( $service as $field => $value ) {
+ if ( in_array( $field, $fields, true ) ) {
+ $response_data[ $field ] = $value;
+ }
+ }
+
+ return $response_data;
+ }
+
+ /**
+ * Verify that user can access Publicize data
+ *
+ * @return true|WP_Error
+ */
+ public function get_items_permission_check() {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data on this site.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_List_Publicize_Services' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php
new file mode 100644
index 00000000..05d0ddd3
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/service-api-keys.php
@@ -0,0 +1,281 @@
+<?php
+/*
+ * Service API Keys: Exposes 3rd party api keys that are used on a site.
+ *
+ * [
+ * { # Availabilty Object. See schema for more detail.
+ * code: (string) Displays success if the operation was successfully executed and an error code if it was not
+ * service: (string) The name of the service in question
+ * service_api_key: (string) The API key used by the service empty if one is not set yet
+ * message: (string) User friendly message
+ * },
+ * ...
+ * ]
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Service_API_Keys extends WP_REST_Controller {
+
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'service-api-keys';
+
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ register_rest_route(
+ 'wpcom/v2',
+ '/service-api-keys/(?P<service>[a-z\-_]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( __CLASS__, 'get_service_api_key' ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( __CLASS__, 'update_service_api_key' ),
+ 'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
+ 'args' => array(
+ 'service_api_key' => array(
+ 'required' => true,
+ 'type' => 'text',
+ ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( __CLASS__, 'delete_service_api_key' ),
+ 'permission_callback' => array( __CLASS__, 'edit_others_posts_check' ),
+ ),
+ )
+ );
+ }
+
+ public static function edit_others_posts_check() {
+ if ( current_user_can( 'edit_others_posts' ) ) {
+ return true;
+ }
+
+ $user_permissions_error_msg = esc_html__(
+ 'You do not have the correct user permissions to perform this action.
+ Please contact your site admin if you think this is a mistake.',
+ 'jetpack'
+ );
+
+ return new WP_Error( 'invalid_user_permission_edit_others_posts', $user_permissions_error_msg, rest_authorization_required_code() );
+ }
+
+ /**
+ * Return the available Gutenberg extensions schema
+ *
+ * @return array Service API Key schema
+ */
+ public function get_public_item_schema() {
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'service-api-keys',
+ 'type' => 'object',
+ 'properties' => array(
+ 'code' => array(
+ 'description' => __( 'Displays success if the operation was successfully executed and an error code if it was not', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service' => array(
+ 'description' => __( 'The name of the service in question', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'service_api_key' => array(
+ 'description' => __( 'The API key used by the service. Empty if none has been set yet', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ 'message' => array(
+ 'description' => __( 'User friendly message', 'jetpack' ),
+ 'type' => 'string',
+ ),
+ ),
+ );
+
+ return $this->add_additional_fields_schema( $schema );
+ }
+
+ /**
+ * Get third party plugin API keys.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function get_service_api_key( $request ) {
+
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $option = self::key_for_api_service( $service );
+ $message = esc_html__( 'API key retrieved successfully.', 'jetpack' );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Update third party plugin API keys.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function update_service_api_key( $request ) {
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $json_params = $request->get_json_params();
+ $params = ! empty( $json_params ) ? $json_params : $request->get_body_params();
+ $service_api_key = trim( $params['service_api_key'] );
+ $option = self::key_for_api_service( $service );
+
+ $validation = self::validate_service_api_key( $service_api_key, $service, $params );
+ if ( ! $validation['status'] ) {
+ return new WP_Error( 'invalid_key', esc_html__( 'Invalid API Key', 'jetpack' ), array( 'status' => 404 ) );
+ }
+ $message = esc_html__( 'API key updated successfully.', 'jetpack' );
+ Jetpack_Options::update_option( $option, $service_api_key );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Delete a third party plugin API key.
+ *
+ * @param WP_REST_Request $request {
+ * Array of parameters received by request.
+ *
+ * @type string $slug Plugin slug with the syntax 'plugin-directory/plugin-main-file.php'.
+ * }
+ */
+ public static function delete_service_api_key( $request ) {
+ $service = self::validate_service_api_service( $request['service'] );
+ if ( ! $service ) {
+ return self::service_api_invalid_service_response();
+ }
+ $option = self::key_for_api_service( $service );
+ Jetpack_Options::delete_option( $option );
+ $message = esc_html__( 'API key deleted successfully.', 'jetpack' );
+ return array(
+ 'code' => 'success',
+ 'service' => $service,
+ 'service_api_key' => Jetpack_Options::get_option( $option, '' ),
+ 'message' => $message,
+ );
+ }
+
+ /**
+ * Validate the service provided in /service-api-keys/ endpoints.
+ * To add a service to these endpoints, add the service name to $valid_services
+ * and add '{service name}_api_key' to the non-compact return array in get_option_names(),
+ * in class-jetpack-options.php
+ *
+ * @param string $service The service the API key is for.
+ * @return string Returns the service name if valid, null if invalid.
+ */
+ public static function validate_service_api_service( $service = null ) {
+ $valid_services = array(
+ 'mapbox',
+ );
+ return in_array( $service, $valid_services, true ) ? $service : null;
+ }
+
+ /**
+ * Error response for invalid service API key requests with an invalid service.
+ */
+ public static function service_api_invalid_service_response() {
+ return new WP_Error(
+ 'invalid_service',
+ esc_html__( 'Invalid Service', 'jetpack' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ /**
+ * Validate API Key
+ *
+ * @param string $key The API key to be validated.
+ * @param string $service The service the API key is for.
+ */
+ public static function validate_service_api_key( $key = null, $service = null ) {
+ $validation = false;
+ switch ( $service ) {
+ case 'mapbox':
+ $validation = self::validate_service_api_key_mapbox( $key );
+ break;
+ }
+ return $validation;
+ }
+
+ /**
+ * Validate Mapbox API key
+ * Based loosely on https://github.com/mapbox/geocoding-example/blob/master/php/MapboxTest.php
+ *
+ * @param string $key The API key to be validated.
+ */
+ public static function validate_service_api_key_mapbox( $key ) {
+ $status = true;
+ $msg = null;
+ $mapbox_url = sprintf(
+ 'https://api.mapbox.com?%s',
+ $key
+ );
+ $mapbox_response = wp_safe_remote_get( esc_url_raw( $mapbox_url ) );
+ $mapbox_body = wp_remote_retrieve_body( $mapbox_response );
+ if ( '{"api":"mapbox"}' !== $mapbox_body ) {
+ $status = false;
+ $msg = esc_html__( 'Can\'t connect to Mapbox', 'jetpack' );
+ return array(
+ 'status' => $status,
+ 'error_message' => $msg,
+ );
+ }
+ $mapbox_geocode_url = esc_url_raw(
+ sprintf(
+ 'https://api.mapbox.com/geocoding/v5/mapbox.places/%s.json?access_token=%s',
+ '1+broadway+new+york+ny+usa',
+ $key
+ )
+ );
+ $mapbox_geocode_response = wp_safe_remote_get( esc_url_raw( $mapbox_geocode_url ) );
+ $mapbox_geocode_body = wp_remote_retrieve_body( $mapbox_geocode_response );
+ $mapbox_geocode_json = json_decode( $mapbox_geocode_body );
+ if ( isset( $mapbox_geocode_json->message ) && ! isset( $mapbox_geocode_json->query ) ) {
+ $status = false;
+ $msg = $mapbox_geocode_json->message;
+ }
+ return array(
+ 'status' => $status,
+ 'error_message' => $msg,
+ );
+ }
+
+ /**
+ * Create site option key for service
+ *
+ * @param string $service The service to create key for.
+ */
+ private static function key_for_api_service( $service ) {
+ return $service . '_api_key';
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Service_API_Keys' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php
new file mode 100644
index 00000000..4c34161c
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/sites-posts-featured-media-url.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * Plugin Name: WPCOM Add Featured Media URL
+ *
+ * Adds `jetpack_featured_media_url` to post responses
+ */
+
+class WPCOM_REST_API_V2_Sites_Posts_Add_Featured_Media_URL {
+ function __construct() {
+ add_action( 'rest_api_init', array( $this, 'add_featured_media_url' ) );
+ }
+
+ function add_featured_media_url() {
+ register_rest_field( 'post', 'jetpack_featured_media_url',
+ array(
+ 'get_callback' => array( $this, 'get_featured_media_url' ),
+ 'update_callback' => null,
+ 'schema' => null,
+ )
+ );
+ }
+
+ function get_featured_media_url( $object, $field_name, $request ) {
+ $featured_media_url = '';
+ $image_attributes = wp_get_attachment_image_src(
+ get_post_thumbnail_id( $object['id'] ),
+ 'full'
+ );
+ if ( is_array( $image_attributes ) && isset( $image_attributes[0] ) ) {
+ $featured_media_url = (string) $image_attributes[0];
+ }
+ return $featured_media_url;
+ }
+}
+
+wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Sites_Posts_Add_Featured_Media_URL' );
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php
new file mode 100644
index 00000000..c1a712bd
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/subscribers.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Subscribers: Get subscriber count
+ *
+ * @since 6.9
+ */
+class WPCOM_REST_API_V2_Endpoint_Subscribers extends WP_REST_Controller {
+ function __construct() {
+ $this->namespace = 'wpcom/v2';
+ $this->rest_base = 'subscribers';
+ // This endpoint *does not* need to connect directly to Jetpack sites.
+ $this->wpcom_is_wpcom_only_endpoint = true;
+ add_action( 'rest_api_init', array( $this, 'register_routes' ) );
+ }
+
+ public function register_routes() {
+ // GET /sites/<blog_id>/subscribers/count - Return number of subscribers for this site.
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/count', array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_subscriber_count' ),
+ 'permission_callback' => array( $this, 'readable_permission_check' ),
+ )
+ ) );
+ }
+
+ public function readable_permission_check() {
+ if ( ! current_user_can_for_blog( get_current_blog_id(), 'edit_posts' ) ) {
+ return new WP_Error( 'authorization_required', 'Only users with the permission to edit posts can see the subscriber count.', array( 'status' => 401 ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Retrieves subscriber count
+ *
+ * @param WP_REST_Request $request incoming API request info
+ * @return array data object containing subscriber count
+ */
+ public function get_subscriber_count( $request ) {
+ // Get the most up to date subscriber count when request is not a test
+ if ( ! Jetpack_Constants::is_defined( 'TESTING_IN_JETPACK' ) ) {
+ delete_transient( 'wpcom_subscribers_total' );
+ }
+
+ $subscriber_info = Jetpack_Subscriptions_Widget::fetch_subscriber_count();
+ $subscriber_count = $subscriber_info['value'];
+
+ return array(
+ 'count' => $subscriber_count
+ );
+ }
+}
+
+if (
+ Jetpack::is_module_active( 'subscriptions' ) ||
+ ( Jetpack_Constants::is_defined( 'TESTING_IN_JETPACK' ) && Jetpack_Constants::get_constant( 'TESTING_IN_JETPACK' ) )
+) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Subscribers' );
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php
new file mode 100644
index 00000000..b615c4e6
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/attachment-fields-videopress.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * Extend the REST API functionality for VideoPress users.
+ *
+ * @package Jetpack
+ */
+
+/**
+ * Add per-attachment VideoPress data.
+ *
+ * { # Attachment Object
+ * ...
+ * jetpack_videopress_guid: (string) VideoPress identifier
+ * ...
+ * }
+ *
+ * @since 7.1.0
+ */
+class WPCOM_REST_API_V2_Attachment_VideoPress_Field extends WPCOM_REST_API_V2_Field_Controller {
+ /**
+ * The REST Object Type to which the jetpack_videopress_guid field will be added.
+ *
+ * @var string
+ */
+ protected $object_type = 'attachment';
+
+ /**
+ * The name of the REST API field to add.
+ *
+ * @var string $field_name
+ */
+ protected $field_name = 'jetpack_videopress_guid';
+
+ /**
+ * Registers the jetpack_videopress field and adds a filter to remove it for attachments that are not videos.
+ */
+ public function register_fields() {
+ parent::register_fields();
+
+ add_filter( 'rest_prepare_attachment', array( $this, 'remove_field_for_non_videos' ), 10, 2 );
+ }
+
+ /**
+ * Defines data structure and what elements are visible in which contexts
+ */
+ public function get_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->field_name,
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ 'description' => __( 'Unique VideoPress ID', 'jetpack' ),
+ );
+ }
+
+ /**
+ * Getter: Retrieve current VideoPress data for a given attachment.
+ *
+ * @param array $attachment Response from the attachment endpoint.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return string
+ */
+ public function get( $attachment, $request ) {
+ if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
+ $blog_id = get_current_blog_id();
+ } else {
+ $blog_id = Jetpack_Options::get_option( 'id' );
+ }
+
+ $post_id = absint( $attachment['id'] );
+
+ $videopress_guid = $this->get_videopress_guid( $post_id, $blog_id );
+
+ if ( ! $videopress_guid ) {
+ return '';
+ }
+
+ return $videopress_guid;
+ }
+
+ /**
+ * Gets the VideoPress GUID for a given attachment.
+ *
+ * This is pulled out into a separate method to support unit test mocking.
+ *
+ * @param int $attachment_id Attachment ID.
+ * @param int $blog_id Blog ID.
+ *
+ * @return string
+ */
+ public function get_videopress_guid( $attachment_id, $blog_id ) {
+ return video_get_info_by_blogpostid( $blog_id, $attachment_id )->guid;
+ }
+
+ /**
+ * Checks if the given attachment is a video.
+ *
+ * @param object $attachment The attachment object.
+ *
+ * @return false|int
+ */
+ public function is_video( $attachment ) {
+ return wp_startswith( $attachment->post_mime_type, 'video/' );
+ }
+
+ /**
+ * Removes the jetpack_videopress_guid field from the response if the
+ * given attachment is not a video.
+ *
+ * @param WP_REST_Response $response Response from the attachment endpoint.
+ * @param WP_Post $attachment The original attachment object.
+ *
+ * @return mixed
+ */
+ public function remove_field_for_non_videos( $response, $attachment ) {
+ if ( ! $this->is_video( $attachment ) ) {
+ unset( $response->data[ $this->field_name ] );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Setter: It does nothing since `jetpack_videopress` is a read-only field.
+ *
+ * @param mixed $value The new value for the field.
+ * @param WP_Post $object The attachment object.
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return null
+ */
+ public function update( $value, $object, $request ) {
+ return null;
+ }
+
+ /**
+ * Permission Check for the field's getter. Delegate the responsibility to the
+ * attachment endpoint, so it always returns true.
+ *
+ * @param mixed $object Response from the attachment endpoint.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return true
+ */
+ public function get_permission_check( $object, $request ) {
+ return true;
+ }
+
+ /**
+ * Permission Check for the field's setter. Delegate the responsibility to the
+ * attachment endpoint, so it always returns true.
+ *
+ * @param mixed $value The new value for the field.
+ * @param WP_Post $object The attachment object.
+ * @param WP_REST_Request $request Request to the attachment endpoint.
+ *
+ * @return true
+ */
+ public function update_permission_check( $value, $object, $request ) {
+ return true;
+ }
+}
+
+if (
+ ( method_exists( 'Jetpack', 'is_active' ) && Jetpack::is_active() ) ||
+ ( defined( 'IS_WPCOM' ) && IS_WPCOM )
+) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Attachment_VideoPress_Field' );
+}
diff --git a/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php
new file mode 100644
index 00000000..c4254a9d
--- /dev/null
+++ b/plugins/jetpack/_inc/lib/core-api/wpcom-fields/post-fields-publicize-connections.php
@@ -0,0 +1,353 @@
+<?php
+
+/**
+ * Add per-post Publicize Connection data.
+ *
+ * { # Post Object
+ * ...
+ * jetpack_publicize_connections: { # Defined below in this file. See schema for more detail.
+ * id: (string) Connection unique_id
+ * service_name: (string) Service slug
+ * display_name: (string) User name/display name of user/connection on Service
+ * enabled: (boolean) Is this connection slated to be shared to? context=edit only
+ * done: (boolean) Is this post (or connection) done sharing? context=edit only
+ * toggleable: (boolean) Can the current user change the `enabled` setting for this Connection+Post? context=edit only
+ * }
+ * ...
+ * meta: { # Not defined in this file. Handled in modules/publicize/publicize.php via `register_meta()`
+ * jetpack_publicize_message: (string) The message to use instead of the post's title when sharing.
+ * }
+ * ...
+ * }
+ *
+ * @since 6.8.0
+ */
+class WPCOM_REST_API_V2_Post_Publicize_Connections_Field extends WPCOM_REST_API_V2_Field_Controller {
+ protected $object_type = 'post';
+ protected $field_name = 'jetpack_publicize_connections';
+
+ public $memoized_updates = array();
+
+ /**
+ * Registers the jetpack_publicize_connections field. Called
+ * automatically on `rest_api_init()`.
+ */
+ public function register_fields() {
+ $this->object_type = get_post_types_by_support( 'publicize' );
+
+ foreach ( $this->object_type as $post_type ) {
+ // Adds meta support for those post types that don't already have it.
+ // Only runs during REST API requests, so it doesn't impact UI.
+ if ( ! post_type_supports( $post_type, 'custom-fields' ) ) {
+ add_post_type_support( $post_type, 'custom-fields' );
+ }
+
+ add_filter( 'rest_pre_insert_' . $post_type, array( $this, 'rest_pre_insert' ), 10, 2 );
+ add_action( 'rest_insert_' . $post_type, array( $this, 'rest_insert' ), 10, 3 );
+ }
+
+ parent::register_fields();
+ }
+
+ /**
+ * Defines data structure and what elements are visible in which contexts
+ */
+ public function get_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-post-connections',
+ 'type' => 'array',
+ 'context' => array( 'view', 'edit' ),
+ 'items' => $this->post_connection_schema(),
+ 'default' => array(),
+ );
+ }
+
+ private function post_connection_schema() {
+ return array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => 'jetpack-publicize-post-connection',
+ 'type' => 'object',
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the Publicize Connection', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'service_name' => array(
+ 'description' => __( 'Alphanumeric identifier for the Publicize Service', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'display_name' => array(
+ 'description' => __( 'Username of the connected account', 'jetpack' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'enabled' => array(
+ 'description' => __( 'Whether to share to this connection', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ ),
+ 'done' => array(
+ 'description' => __( 'Whether Publicize has already finished sharing for this post', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ 'toggleable' => array(
+ 'description' => __( 'Whether `enable` can be changed for this post/connection', 'jetpack' ),
+ 'type' => 'boolean',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @param int $post_id
+ * @return true|WP_Error
+ */
+ function permission_check( $post_id ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return new WP_Error(
+ 'publicize_not_available',
+ __( 'Sorry, Publicize is not available on your site right now.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ if ( $publicize->current_user_can_access_publicize_data( $post_id ) ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'invalid_user_permission_publicize',
+ __( 'Sorry, you are not allowed to access Publicize data for this post.', 'jetpack' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
+ /**
+ * Getter permission check
+ *
+ * @param array $post_array Response data from Post Endpoint
+ * @return true|WP_Error
+ */
+ function get_permission_check( $post_array, $request ) {
+ return $this->permission_check( isset( $post_array['id'] ) ? $post_array['id'] : 0 );
+
+ }
+
+ /**
+ * Setter permission check
+ *
+ * @param WP_Post $post
+ * @return true|WP_Error
+ */
+ public function update_permission_check( $value, $post, $request ) {
+ return $this->permission_check( isset( $post->ID ) ? $post->ID : 0 );
+ }
+
+ /**
+ * Getter: Retrieve current list of connected social accounts for a given post.
+ *
+ * @see Publicize::get_filtered_connection_data()
+ *
+ * @param array $post_array Response from Post Endpoint
+ * @param WP_REST_Request
+ *
+ * @return array List of connections
+ */
+ public function get( $post_array, $request ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return array();
+ }
+
+ $schema = $this->post_connection_schema();
+ $properties = array_keys( $schema['properties'] );
+
+ $connections = $publicize->get_filtered_connection_data( $post_array['id'] );
+
+ $output_connections = array();
+ foreach ( $connections as $connection ) {
+ $output_connection = array();
+ foreach ( $properties as $property ) {
+ if ( isset( $connection[ $property ] ) ) {
+ $output_connection[ $property ] = $connection[ $property ];
+ }
+ }
+
+ $output_connection['id'] = (string) $connection['unique_id'];
+
+ $output_connections[] = $output_connection;
+ }
+
+ return $output_connections;
+ }
+
+ /**
+ * Prior to updating the post, first calculate which Services to
+ * Publicize to and which to skip.
+ *
+ * @param object $post Post data to insert/update.
+ * @param WP_REST_Request $request
+ * @return Filtered $post
+ */
+ public function rest_pre_insert( $post, $request ) {
+ if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
+ return $post;
+ }
+
+ $permission_check = $this->update_permission_check( $request['jetpack_publicize_connections'], $post, $request );
+
+ if ( is_wp_error( $permission_check ) ) {
+ return $permission_check;
+ }
+
+ // memoize
+ $this->get_meta_to_update( $request['jetpack_publicize_connections'], isset( $post->ID ) ? $post->ID : 0 );
+
+ return $post;
+ }
+
+ /**
+ * After creating a new post, update our cached data to reflect
+ * the new post ID.
+ *
+ * @param WP_Post $post
+ * @param WP_REST_Request $request
+ * @param bool $is_new
+ */
+ public function rest_insert( $post, $request, $is_new ) {
+ if ( ! $is_new ) {
+ // An existing post was edited - no need to update
+ // our cache - we started out knowing the correct
+ // post ID.
+ return;
+ }
+
+ if ( ! isset( $request['jetpack_publicize_connections'] ) ) {
+ return;
+ }
+
+ if ( ! isset( $this->memoized_updates[0] ) ) {
+ return;
+ }
+
+ $this->memoized_updates[ $post->ID ] = $this->memoized_updates[0];
+ unset( $this->memoized_updates[0] );
+ }
+
+ protected function get_meta_to_update( $requested_connections, $post_id = 0 ) {
+ global $publicize;
+
+ if ( ! $publicize ) {
+ return array();
+ }
+
+ if ( isset( $this->memoized_updates[$post_id] ) ) {
+ return $this->memoized_updates[$post_id];
+ }
+
+ $available_connections = $publicize->get_filtered_connection_data( $post_id );
+
+ $changed_connections = array();
+
+ // Build lookup mappings
+ $available_connections_by_unique_id = array();
+ $available_connections_by_service_name = array();
+ foreach ( $available_connections as $available_connection ) {
+ $available_connections_by_unique_id[ $available_connection['unique_id'] ] = $available_connection;
+
+ if ( ! isset( $available_connections_by_service_name[ $available_connection['service_name'] ] ) ) {
+ $available_connections_by_service_name[ $available_connection['service_name'] ] = array();
+ }
+ $available_connections_by_service_name[ $available_connection['service_name'] ][] = $available_connection;
+ }
+
+ // Handle { service_name: $service_name, enabled: (bool) }
+ foreach ( $requested_connections as $requested_connection ) {
+ if ( ! isset( $requested_connection['service_name'] ) ) {
+ continue;
+ }
+
+ if ( ! isset( $available_connections_by_service_name[ $requested_connection['service_name'] ] ) ) {
+ continue;
+ }
+
+ foreach ( $available_connections_by_service_name[ $requested_connection['service_name'] ] as $available_connection ) {
+ $changed_connections[ $available_connection['unique_id'] ] = $requested_connection['enabled'];
+ }
+ }
+
+ // Handle { id: $id, enabled: (bool) }
+ // These override the service_name settings
+ foreach ( $requested_connections as $requested_connection ) {
+ if ( ! isset( $requested_connection['id'] ) ) {
+ continue;
+ }
+
+ if ( ! isset( $available_connections_by_unique_id[ $requested_connection['id'] ] ) ) {
+ continue;
+ }
+
+ $changed_connections[ $requested_connection['id'] ] = $requested_connection['enabled'];
+ }
+
+ // Set all changed connections to their new value
+ foreach ( $changed_connections as $unique_id => $enabled ) {
+ $connection = $available_connections_by_unique_id[ $unique_id ];
+
+ if ( $connection['done'] || ! $connection['toggleable'] ) {
+ continue;
+ }
+
+ $available_connections_by_unique_id[ $unique_id ]['enabled'] = $enabled;
+ }
+
+ $meta_to_update = array();
+ // For all connections, ensure correct post_meta
+ foreach ( $available_connections_by_unique_id as $unique_id => $available_connection ) {
+ if ( $available_connection['enabled'] ) {
+ $meta_to_update[$publicize->POST_SKIP . $unique_id] = null;
+ } else {
+ $meta_to_update[$publicize->POST_SKIP . $unique_id] = 1;
+ }
+ }
+
+ $this->memoized_updates[$post_id] = $meta_to_update;
+
+ return $meta_to_update;
+ }
+
+ /**
+ * Update the connections slated to be shared to.
+ *
+ * @param array $requested_connections
+ * Items are either `{ id: (string) }` or `{ service_name: (string) }`
+ * @param WP_Post $post
+ * @param WP_REST_Request
+ */
+ public function update( $requested_connections, $post, $request ) {
+ foreach ( $this->get_meta_to_update( $requested_connections, $post->ID ) as $meta_key => $meta_value ) {
+ if ( is_null( $meta_value ) ) {
+ delete_post_meta( $post->ID, $meta_key );
+ } else {
+ update_post_meta( $post->ID, $meta_key, $meta_value );
+ }
+ }
+ }
+}
+
+if ( Jetpack::is_module_active( 'publicize' ) ) {
+ wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Post_Publicize_Connections_Field' );
+}