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
wordpress-seo / src / ai / http-request / infrastructure / api-client.php

api-client.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at src/ai/http-request/infrastructure/api-client.php

144 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4
5 namespace Yoast\WP\SEO\AI\HTTP_Request\Infrastructure;
6
7 use WPSEO_Utils;
8 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\WP_Request_Exception;
9 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Request;
10
11 /**
12 * Class API_Client
13 * Handles the API requests to the AI Generator API.
14 *
15 * @makePublic
16 */
17 class API_Client implements API_Client_Interface {
18
19 /**
20 * The base URL for the API.
21 *
22 * @var string
23 */
24 private $base_url = 'https://ai.yoa.st/api/v1';
25
26 /**
27 * Performs a request to the API.
28 *
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_*`.
33 *
34 * @return array<int|string|array<string>> The response from the API.
35 *
36 * @throws WP_Request_Exception When the underlying WordPress HTTP call returns an error, or the HTTP method is not supported.
37 */
38 public function perform_request( string $action_path, $body, $headers, string $http_method ): array {
39 // Our API expects JSON.
40 $headers = \array_merge( $headers, [ 'Content-Type' => 'application/json' ] );
41 $arguments = [
42 'timeout' => $this->get_request_timeout(),
43 'headers' => $headers,
44 ];
45
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 ) ) {
50 // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Reason: We don't want the debug/pretty possibility.
51 $arguments['body'] = WPSEO_Utils::format_json_encode( $body );
52 }
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 {
117 /**
118 * Filter: 'Yoast\WP\SEO\ai_api_url' - Replaces the default URL for the AI API with a custom one.
119 *
120 * @internal
121 *
122 * @param string $url The default URL for the AI API.
123 */
124 return (string) \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url );
125 }
126
127 /**
128 * Gets the timeout of the requests in seconds.
129 *
130 * @return int The timeout of the suggestion requests in seconds.
131 */
132 public function get_request_timeout(): int {
133 /**
134 * Filter: 'Yoast\WP\SEO\ai_suggestions_timeout' - Replaces the default timeout with a custom one, for testing purposes.
135 *
136 * @since 22.7
137 * @internal
138 *
139 * @param int $timeout The default timeout in seconds.
140 */
141 return (int) \apply_filters( 'Yoast\WP\SEO\ai_suggestions_timeout', 60 );
142 }
143 }
144