is_wpcom = true; $this->wpcom_is_wpcom_only_endpoint = true; add_action( 'rest_api_init', array( $this, 'maybe_register_routes' ) ); } /** * Register routes on `rest_api_init`, gating on the AI feature state. * * The Jetpack_AI_Helper check (which loads the helper and instantiates * Status/Host classes) runs here rather than in the constructor so that code * is only loaded when the REST API is actually in use, not on every * front-end, cron, or login request. */ public function maybe_register_routes() { if ( ! class_exists( 'Jetpack_AI_Helper' ) ) { require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php'; } // Intentionally gated to Simple, Atomic, and WordPress VIP sites only. // Broader self-hosted Jetpack support is deferred — we want to roll out // on these platforms first before opening to all connected sites. if ( ! \Jetpack_AI_Helper::is_enabled_for_content_guidelines() ) { return; } $this->register_routes(); } /** * Register routes. */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'suggest_guidelines' ), 'permission_callback' => array( $this, 'permission_callback' ), 'args' => array( 'categories' => array( 'description' => __( 'Categories to generate guidelines for.', 'jetpack' ), 'type' => 'object', 'required' => true, ), ), ) ); } /** * Permission check — require manage_options, plus a connected user account * on sites that proxy the request to WordPress.com over the connection. * * @return bool|WP_Error */ public function permission_callback() { if ( ! current_user_can( 'manage_options' ) ) { return false; } // suggest_guidelines() signs with the user token off Simple, so without a // user connection there is no identity to present and WordPress.com rejects // the request on private sites. if ( ! ( new Host() )->is_wpcom_simple() && ! ( new Manager() )->is_user_connected() ) { return new WP_Error( 'rest_cannot_suggest_guidelines', __( 'Please connect your user account to WordPress.com', 'jetpack' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Proxy the suggest-guidelines request to wpcom. * * @param WP_REST_Request $request The request object. * @return mixed|WP_Error */ public function suggest_guidelines( $request ) { $blog_id = \Jetpack_Options::get_option( 'id' ); $body = array( 'categories' => $request->get_param( 'categories' ), ); $path = sprintf( '/sites/%d/jetpack-ai/suggest-guidelines', $blog_id ) . '?force=wpcom'; $args = array( 'method' => 'POST', 'headers' => array( 'content-type' => 'application/json' ), 'timeout' => 90, ); // On a private site WordPress.com resolves access from the requesting user, // and a blog token carries no user_id — so the request is rejected before // the endpoint runs, and again on the internal AI proxy dispatch. Signing // as the user clears both, because the identity holds for the whole request. // permission_callback() has already established that a user token exists. // // Simple sites keep the blog-token call: it short-circuits to an in-process // WPCOM_API_Direct dispatch that never leaves the request, so the logged-in // user is already present and there is no connection to authenticate against. if ( ( new Host() )->is_wpcom_simple() ) { $response = Client::wpcom_json_api_request_as_blog( $path, '2', $args, wp_json_encode( $body, JSON_UNESCAPED_SLASHES ), 'wpcom' ); } else { $response = Client::wpcom_json_api_request_as_user( $path, '2', $args, wp_json_encode( $body, JSON_UNESCAPED_SLASHES ), 'wpcom' ); } if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); $body_str = wp_remote_retrieve_body( $response ); $data = json_decode( $body_str, true ); if ( $status_code !== 200 ) { $message = is_array( $data ) && isset( $data['message'] ) ? $data['message'] : __( 'Failed to generate guidelines.', 'jetpack' ); $code = is_array( $data ) && isset( $data['code'] ) ? $data['code'] : 'upstream_error'; return new WP_Error( $code, $message, array( 'status' => $status_code ) ); } if ( JSON_ERROR_NONE !== json_last_error() ) { return new WP_Error( 'invalid_response', __( 'The guidelines service returned a malformed response.', 'jetpack' ), array( 'status' => 502 ) ); } return $data; } } wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Agent_Guidelines_AI' );