allowed_routes = $allowed_routes; } public function name(): string { return 'call_wordpress_rest_api'; } public function description(): string { $routes = empty( $this->allowed_routes ) ? 'none (whitelist empty)' : implode( ', ', $this->allowed_routes ); return 'Call the WordPress REST API as the current user. ' . 'Allowed route prefixes: ' . $routes . '. ' . 'Use this to read or manage posts, pages, media, categories, tags, etc.'; } public function input_schema(): array { return array( 'type' => 'object', 'properties' => array( 'route' => array( 'type' => 'string', 'description' => 'REST route, e.g. /wp/v2/posts or /wp/v2/posts/123', ), 'method' => array( 'type' => 'string', 'enum' => array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), 'default' => 'GET', 'description' => 'HTTP method.', ), 'query' => array( 'type' => 'object', 'description' => 'Query parameters for GET requests.', ), 'body' => array( 'type' => 'object', 'description' => 'Request body for POST/PUT/PATCH requests.', ), ), 'required' => array( 'route' ), ); } public function execute( array $input, array $context = array() ): array { $route = sanitize_text_field( (string) ( $input['route'] ?? '' ) ); $method = strtoupper( sanitize_key( (string) ( $input['method'] ?? 'GET' ) ) ); $query = $input['query'] ?? array(); $body = $input['body'] ?? array(); if ( empty( $route ) ) { return array( 'success' => false, 'error' => 'No route provided.', ); } if ( 0 !== strpos( $route, '/' ) ) { $route = '/' . ltrim( $route, '/' ); } if ( ! $this->is_route_allowed( $route ) ) { return array( 'success' => false, 'error' => 'Route is not in the allowed whitelist: ' . $route, ); } if ( in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) && preg_match( '#^/wp/v2/(?:posts|pages)(?:/\d+)?/?$#', $route ) && is_array( $body ) && array_key_exists( 'content', $body ) ) { return array( 'success' => false, 'code' => 'content_mutation_requires_gutenberg_ability', 'error' => 'Post/page content must be changed with advanced-addons Gutenberg mutation abilities so serialized blocks, assets, revisions, and Tailwind CSS remain valid.', ); } if ( ! function_exists( 'rest_do_request' ) ) { return array( 'success' => false, 'error' => 'REST API is not available.', ); } $request = new WP_REST_Request( $method, $route ); if ( ! empty( $query ) && is_array( $query ) ) { $request->set_query_params( $query ); } if ( in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) && ! empty( $body ) && is_array( $body ) ) { $request->set_body_params( $body ); } $response = rest_do_request( $request ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'error' => $response->get_error_message(), ); } return array( 'success' => true, 'status' => $response->get_status(), 'data' => $response->get_data(), ); } /** * Check if a route matches one of the allowed prefixes. */ private function is_route_allowed( string $route ): bool { if ( empty( $this->allowed_routes ) ) { return false; } foreach ( $this->allowed_routes as $allowed ) { if ( strpos( $route, $allowed ) === 0 ) { return true; } } return false; } }