PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | src/ai/http-request/infrastructure/api-client.php +75 -17 28.028.5 View file →
@@ -5,8 +5,9 @@
5 5 namespace Yoast\WP\SEO\AI\HTTP_Request\Infrastructure;
6 6
7 7 use WPSEO_Utils;
8 8 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\WP_Request_Exception;
9 +use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
9 10
10 11 /**
11 12 * Class API_Client
12 13 * Handles the API requests to the AI Generator API.
@@ -24,20 +25,19 @@
24 25
25 26 /**
26 27 * Performs a request to the API.
27 28 *
28 - * @param string $action_path The action path for the request.
29 - * @param array<string> $body The body of the request.
30 - * @param array<string> $headers The headers for the request.
31 - * @param bool $is_post Whether the request is a POST request.
29 + * @param string $action_path The action path for the request.
30 + * @param array<string>|null $body The body of the request, or null/empty to send no body.
31 + * @param array<string> $headers The headers for the request.
32 + * @param string $http_method The HTTP method for the request. One of `Request::METHOD_*`.
32 33 *
33 34 * @return array<int|string|array<string>> The response from the API.
34 35 *
35 - * @throws WP_Request_Exception When the wp_remote_post() returns an error.
36 + * @throws WP_Request_Exception When the underlying WordPress HTTP call returns an error, or the HTTP method is not supported.
36 37 */
37 - public function perform_request( string $action_path, $body, $headers, bool $is_post ): array {
38 + public function perform_request( string $action_path, $body, $headers, string $http_method ): array {
38 39 // Our API expects JSON.
39 - // The request times out after 30 seconds.
40 40 $headers = \array_merge( $headers, [ 'Content-Type' => 'application/json' ] );
41 41 $arguments = [
42 42 'timeout' => $this->get_request_timeout(),
43 43 'headers' => $headers,
@@ -42,13 +42,79 @@
42 42 'timeout' => $this->get_request_timeout(),
43 43 'headers' => $headers,
44 44 ];
45 45
46 - if ( $is_post ) {
46 + // Only POST sends a body to the AI API today; GET and DELETE endpoints do not. An empty body is
47 + // omitted entirely: an empty array is ambiguous once JSON-encoded (`[]` vs `{}`) and the AI
48 + // service rejects it, so a bodyless POST is sent instead.
49 + if ( $http_method === Request::METHOD_POST && ! empty( $body ) ) {
47 50 // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Reason: We don't want the debug/pretty possibility.
48 51 $arguments['body'] = WPSEO_Utils::format_json_encode( $body );
49 52 }
50 53
54 + $url = $this->get_url( $action_path );
55 +
56 + switch ( $http_method ) {
57 + case Request::METHOD_POST:
58 + $response = \wp_remote_post( $url, $arguments );
59 + break;
60 + case Request::METHOD_GET:
61 + $response = \wp_remote_get( $url, $arguments );
62 + break;
63 + case Request::METHOD_DELETE:
64 + $response = \wp_remote_request( $url, \array_merge( $arguments, [ 'method' => 'DELETE' ] ) );
65 + break;
66 + default:
67 + // Defensive: the Request constructor already validates the method, so we should never reach this branch.
68 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
69 + throw new WP_Request_Exception( "Unsupported HTTP method: $http_method" );
70 + }
71 +
72 + if ( \is_wp_error( $response ) ) {
73 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
74 + throw new WP_Request_Exception( $response->get_error_message() );
75 + }
76 +
77 + return $response;
78 + }
79 +
80 + /**
81 + * Builds the full URL for a request to the AI API, applying the same filter perform_request() uses.
82 + *
83 + * @param string $action_path The action path for the request.
84 + *
85 + * @return string The full URL for the request.
86 + */
87 + public function get_url( string $action_path ): string {
88 + return $this->get_base_url() . $action_path;
89 + }
90 +
91 + /**
92 + * Returns the RFC 8707 resource indicator for the AI API: the origin (scheme + host + optional port)
93 + * of the configured base URL, without the path. This is the audience AI access tokens are bound to,
94 + * so it must track the same filter that decides where requests are actually sent.
95 + *
96 + * @return string The AI resource server origin (e.g. https://ai.yoa.st).
97 + */
98 + public function get_resource_url(): string {
99 + $parsed = \wp_parse_url( $this->get_base_url() );
100 + if ( $parsed === false ) {
101 + return $this->get_base_url();
102 + }
103 +
104 + $scheme = ( $parsed['scheme'] ?? 'https' );
105 + $host = ( $parsed['host'] ?? '' );
106 + $port = isset( $parsed['port'] ) ? ( ':' . $parsed['port'] ) : '';
107 +
108 + return $scheme . '://' . $host . $port;
109 + }
110 +
111 + /**
112 + * Resolves the base URL for the AI API, applying the configurable override filter.
113 + *
114 + * @return string The (possibly filtered) base URL, including its path (e.g. https://ai.yoa.st/api/v1).
115 + */
116 + private function get_base_url(): string {
51 117 /**
52 118 * Filter: 'Yoast\WP\SEO\ai_api_url' - Replaces the default URL for the AI API with a custom one.
53 119 *
54 120 * @internal
@@ -54,17 +120,9 @@
54 120 * @internal
55 121 *
56 122 * @param string $url The default URL for the AI API.
57 123 */
58 - $url = \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url );
59 - $response = ( $is_post ) ? \wp_remote_post( $url . $action_path, $arguments ) : \wp_remote_get( $url . $action_path, $arguments );
60 -
61 - if ( \is_wp_error( $response ) ) {
62 - // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
63 - throw new WP_Request_Exception( $response->get_error_message() );
64 - }
65 -
66 - return $response;
124 + return (string) \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url );
67 125 }
68 126
69 127 /**
70 128 * Gets the timeout of the requests in seconds.