Skip to content

Commit 1701705

Browse files
committed
AI: Validate filtered default request timeout in WP_AI_Client_Prompt_Builder.
This checks that the return value of the `wp_ai_client_default_request_timeout` filter is a non-negative number before passing it to `RequestOptions`. If the filtered value is invalid, it is discarded in favor of the original default of `30.0` and a `_doing_it_wrong()` notice is issued. Without this check, a fatal error would ensue from the exception thrown in `\WordPress\AiClient\Providers\Http\DTO\RequestOptions::validateTimeout()`. The following static analysis issues are addressed: * Use `float` instead of `int` for the `wp_ai_client_default_request_timeout` filter parameter. * Add missing PHP imports for `Message` and `MessagePart` in the PHPDoc for `wp_ai_client_prompt()`. * Add PHP return type hints for `wp_ai_client_prompt()` and `WP_AI_Client_Cache::getMultiple()`. * Use native property type hints in `WP_AI_Client_HTTP_Client`. Developed in #11596 Props westonruter, justlevine, flixos90, khushdoms, darshitrajyaguru97, adrmf25, jarodortegaaraya, tusharaddweb, gaurangsondagar. Fixes #65094. git-svn-id: https://develop.svn.wordpress.org/trunk@62255 602fd350-edb4-49c9-b593-d223f7449a82
1 parent c9bdbe0 commit 1701705

5 files changed

Lines changed: 90 additions & 13 deletions

File tree

src/wp-includes/ai-client.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010
use WordPress\AiClient\AiClient;
11+
use WordPress\AiClient\Messages\DTO\Message;
12+
use WordPress\AiClient\Messages\DTO\MessagePart;
1113

1214
/**
1315
* Returns whether AI features are supported in the current environment.
@@ -55,6 +57,6 @@ function wp_supports_ai(): bool {
5557
* conversations. Default null.
5658
* @return WP_AI_Client_Prompt_Builder The prompt builder instance.
5759
*/
58-
function wp_ai_client_prompt( $prompt = null ) {
60+
function wp_ai_client_prompt( $prompt = null ): WP_AI_Client_Prompt_Builder {
5961
return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt );
6062
}

src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public function clear(): bool {
104104
* @param mixed $default_value Default value to return for keys that do not exist.
105105
* @return array<string, mixed> A list of key => value pairs.
106106
*/
107-
public function getMultiple( $keys, $default_value = null ) {
107+
public function getMultiple( $keys, $default_value = null ): array {
108108
/**
109109
* Keys array.
110110
*

src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,15 @@ class WP_AI_Client_HTTP_Client implements ClientInterface, ClientWithOptionsInte
3232
* Response factory instance.
3333
*
3434
* @since 7.0.0
35-
* @var ResponseFactoryInterface
3635
*/
37-
private $response_factory;
36+
private ResponseFactoryInterface $response_factory;
3837

3938
/**
4039
* Stream factory instance.
4140
*
4241
* @since 7.0.0
43-
* @var StreamFactoryInterface
4442
*/
45-
private $stream_factory;
43+
private StreamFactoryInterface $stream_factory;
4644

4745
/**
4846
* Constructor.

src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,29 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) {
190190
$this->error = $this->exception_to_wp_error( $e );
191191
}
192192

193+
$default_timeout = 30.0;
194+
193195
/**
194196
* Filters the default request timeout in seconds for AI Client HTTP requests.
195197
*
196198
* @since 7.0.0
197199
*
198-
* @param int $default_timeout The default timeout in seconds.
200+
* @param float $default_timeout The default timeout in seconds.
199201
*/
200-
$default_timeout = (int) apply_filters( 'wp_ai_client_default_request_timeout', 30 );
202+
$filtered_default_timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout );
203+
if ( is_numeric( $filtered_default_timeout ) && (float) $filtered_default_timeout >= 0.0 ) {
204+
$default_timeout = (float) $filtered_default_timeout;
205+
} else {
206+
_doing_it_wrong(
207+
__METHOD__,
208+
sprintf(
209+
/* translators: %s: wp_ai_client_default_request_timeout */
210+
__( 'The %s filter must return a non-negative number.' ),
211+
'<code>wp_ai_client_default_request_timeout</code>'
212+
),
213+
'7.0.0'
214+
);
215+
}
201216

202217
$this->builder->usingRequestOptions(
203218
RequestOptions::fromArray(

tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,64 @@ public function test_constructor_sets_default_request_timeout() {
192192
}
193193

194194
/**
195-
* Test that the constructor allows overriding the default request timeout.
195+
* Test that the constructor allows overriding the default request timeout with a valid value.
196196
*
197197
* @ticket 64591
198+
* @ticket 65094
199+
*
200+
* @dataProvider data_valid_request_timeout_overrides
201+
*
202+
* @param mixed $input The timeout value returned by the filter.
203+
* @param float $expected The expected timeout stored on the request options.
204+
*/
205+
public function test_constructor_allows_overriding_request_timeout_with_valid_timeout( $input, float $expected ) {
206+
add_filter(
207+
'wp_ai_client_default_request_timeout',
208+
static function () use ( $input ) {
209+
return $input;
210+
}
211+
);
212+
213+
$builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
214+
215+
/** @var RequestOptions $request_options */
216+
$request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' );
217+
218+
$this->assertInstanceOf( RequestOptions::class, $request_options );
219+
$this->assertSame( $expected, $request_options->getTimeout() );
220+
}
221+
222+
/**
223+
* Data provider for {@see self::test_constructor_allows_overriding_request_timeout_with_valid_timeout()}.
224+
*
225+
* @return array<string, array{0: mixed, 1: float}>
198226
*/
199-
public function test_constructor_allows_overriding_request_timeout() {
227+
public function data_valid_request_timeout_overrides(): array {
228+
return array(
229+
'float' => array( 45.5, 45.5 ),
230+
'integer' => array( 67, 67.0 ),
231+
'string' => array( '20', 20.0 ),
232+
'infinity' => array( INF, INF ),
233+
'zero' => array( 0.0, 0.0 ),
234+
);
235+
}
236+
237+
/**
238+
* Test that the constructor disallows overriding the default request timeout with an invalid value.
239+
*
240+
* @ticket 65094
241+
*
242+
* @dataProvider data_invalid_request_timeouts
243+
*
244+
* @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct
245+
*
246+
* @param mixed $timeout The invalid timeout value returned by the filter.
247+
*/
248+
public function test_constructor_disallows_overriding_with_invalid_request_timeout( $timeout ) {
200249
add_filter(
201250
'wp_ai_client_default_request_timeout',
202-
static function () {
203-
return 45;
251+
static function () use ( $timeout ) {
252+
return $timeout;
204253
}
205254
);
206255

@@ -210,7 +259,20 @@ static function () {
210259
$request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' );
211260

212261
$this->assertInstanceOf( RequestOptions::class, $request_options );
213-
$this->assertSame( 45.0, $request_options->getTimeout() );
262+
$this->assertSame( 30.0, $request_options->getTimeout() );
263+
}
264+
265+
/**
266+
* Data provider for {@see self::test_constructor_disallows_overriding_with_invalid_request_timeout()}.
267+
*
268+
* @return array<string, array{0: mixed}>
269+
*/
270+
public function data_invalid_request_timeouts(): array {
271+
return array(
272+
'negative number' => array( -1 ),
273+
'array' => array( array() ),
274+
'null' => array( null ),
275+
);
214276
}
215277

216278
/**

0 commit comments

Comments
 (0)