namespace, '/' . $this->rest_base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_mcp_settings' ), 'permission_callback' => array( $this, 'permissions_check' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_mcp_settings' ), 'permission_callback' => array( $this, 'permissions_check' ), 'args' => array( 'mcp_abilities' => array( 'type' => 'object', 'required' => true, ), ), ), ) ); } /** * Check permissions. * * @return bool|WP_Error */ public function permissions_check() { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'rest_forbidden', __( 'You do not have permission to manage MCP settings.', 'jetpack' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Fetch MCP abilities from the WPCOM wpcom/v2/sites/{blog_id}/mcp-abilities endpoint. * * The WPCOM endpoint accepts user token auth and returns an array of abilities. * The response is shaped to match the format the client utilities expect: * { account: { tool-id: {...} }, site: { tool-id: {...} }, * sites: [{ blog_id, site_level_enabled, abilities }], * site_level_enabled_default: bool } * * All state is owned by WPCOM and persisted via POST /sites/{blog_id}/mcp-abilities. * * @return WP_REST_Response|WP_Error */ public function get_mcp_settings() { $blog_id = Manager::get_site_id(); if ( is_wp_error( $blog_id ) ) { return rest_ensure_response( $blog_id ); } $response = Client::wpcom_json_api_request_as_user( sprintf( '/sites/%d/mcp-abilities', (int) $blog_id ), '2', array( 'method' => 'GET' ), null, 'wpcom' ); if ( is_wp_error( $response ) ) { return $response; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); // has_mcp_plan is set explicitly by the WPCOM endpoint using PaidPlanMiddleware. // Fall back to 402/403 status handling for older WPCOM builds that predate the field. $http_status = wp_remote_retrieve_response_code( $response ); $has_mcp_plan = isset( $body['has_mcp_plan'] ) ? (bool) $body['has_mcp_plan'] : ! in_array( $http_status, array( 402, 403 ), true ); if ( ! $has_mcp_plan ) { return rest_ensure_response( array( 'has_mcp_access' => false, 'mcp_abilities' => array(), ) ); } // Transform [ {name, title, readonly, site_context, enabled, …}, … ] → { name: {…}, … }. // readonly and site_context are now provided by the WPCOM endpoint (wpcom#205108). // Fall back to name-suffix heuristic for readonly only if the field is absent, so // this endpoint remains functional against older WPCOM builds. $account_abilities = new stdClass(); $site_abilities = array(); if ( ! empty( $body['abilities'] ) && is_array( $body['abilities'] ) ) { $account_abilities = array(); foreach ( $body['abilities'] as $ability ) { if ( empty( $ability['name'] ) ) { continue; } if ( ! array_key_exists( 'readonly', $ability ) ) { $ability['readonly'] = ! (bool) preg_match( '/-(create|update|delete)$/i', $ability['name'] ); } $account_abilities[ (string) $ability['name'] ] = $ability; // `site` subset: tools marked site_context=true by WPCOM are the only ones // relevant to the site-level settings UI. getSiteContextToolIds() in JS uses // this to filter out account/notifications/billing/domains tools. if ( ! empty( $ability['site_context'] ) ) { $site_abilities[ $ability['name'] ] = $ability; } } } // site_level_enabled comes directly from WPCOM — it is the authoritative effective // state for this site (account default applied, site exceptions factored in). // Fall back to the enabled-abilities heuristic only if the field is absent. $site_level_enabled = isset( $body['site_level_enabled'] ) ? (bool) $body['site_level_enabled'] : ! empty( array_filter( (array) $account_abilities, function ( $a ) { return ! empty( $a['enabled'] ); } ) ); // site_level_enabled_default mirrors Calypso: same value as site_level_enabled // when derived from WPCOM (no per-site override concept at this layer). $site_level_enabled_default = $site_level_enabled; // Group descriptors (AIINT-469) — pass through if the WPCOM endpoint returns them. $groups = array(); if ( isset( $body['groups'] ) && is_array( $body['groups'] ) ) { $groups = $body['groups']; } // Use only the explicit per-site user overrides returned by WPCOM in user_overrides.abilities. // These are the raw values stored via SettingsHelper — not the computed effective states // (account defaults merged with overrides). Keeping only explicit overrides here lets the // JS fall back to site_level_enabled as the default for any tool not yet overridden, // matching Calypso's display behaviour (all tools on when site_level_enabled:true and no // per-tool overrides exist). $site_tool_abilities = array(); if ( isset( $body['user_overrides']['abilities'] ) && is_array( $body['user_overrides']['abilities'] ) ) { foreach ( $body['user_overrides']['abilities'] as $name => $value ) { $site_tool_abilities[ (string) $name ] = (bool) $value; } } return rest_ensure_response( array( 'has_mcp_access' => true, 'mcp_abilities' => array( 'account' => $account_abilities, 'site' => $site_abilities, 'sites' => array( array( 'blog_id' => (int) $blog_id, 'site_level_enabled' => $site_level_enabled, 'abilities' => (object) $site_tool_abilities, ), ), 'site_level_enabled_default' => $site_level_enabled_default, 'groups' => $groups, ), ) ); } /** * Proxy mcp_abilities update to WPCOM POST /sites/{blog_id}/mcp-abilities. * * Accepts the sites[] format used by the client: * { sites: [{ blog_id, site_level_enabled?, abilities?: { tool_id: bool } }] } * * Extracts site_level_enabled and abilities from sites[0] and forwards them * to the WPCOM endpoint which persists them to user settings via SettingsHelper. * This keeps Jetpack and Calypso in sync — both read/write the same store. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error */ public function update_mcp_settings( $request ) { $blog_id = Manager::get_site_id(); if ( is_wp_error( $blog_id ) ) { return rest_ensure_response( $blog_id ); } $incoming = $request->get_param( 'mcp_abilities' ); if ( is_object( $incoming ) ) { $incoming = get_object_vars( $incoming ); } elseif ( ! is_array( $incoming ) ) { $incoming = array(); } // Unpack sites[0] into the flat format WPCOM POST /sites/{id}/mcp-abilities expects. $wpcom_body = array(); if ( ! empty( $incoming['sites'] ) && is_array( $incoming['sites'] ) ) { $site_update = $incoming['sites'][0]; if ( is_object( $site_update ) ) { $site_update = get_object_vars( $site_update ); } if ( isset( $site_update['site_level_enabled'] ) ) { $wpcom_body['site_level_enabled'] = (bool) $site_update['site_level_enabled']; } if ( isset( $site_update['abilities'] ) ) { $abilities = is_object( $site_update['abilities'] ) ? get_object_vars( $site_update['abilities'] ) : (array) $site_update['abilities']; $normalised = array(); foreach ( $abilities as $name => $value ) { $normalised[ $name ] = (bool) $value; } $wpcom_body['abilities'] = $normalised; } } if ( ! empty( $wpcom_body ) ) { $response = Client::wpcom_json_api_request_as_user( sprintf( '/sites/%d/mcp-abilities', (int) $blog_id ), '2', array( 'method' => 'POST' ), $wpcom_body, 'wpcom' ); if ( is_wp_error( $response ) ) { return $response; } $status = wp_remote_retrieve_response_code( $response ); if ( $status < 200 || $status >= 300 ) { return new WP_Error( 'wpcom_update_failed', __( 'Failed to save MCP settings on WordPress.com.', 'jetpack' ), array( 'status' => 502 ) ); } } return $this->get_mcp_settings(); } } wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_MCP_Settings' );