Skip to content

Commit aa06e5e

Browse files
committed
Merge branch 'trunk' into html-api/ensure-deep-nesting-no-exception
2 parents b847559 + f041be2 commit aa06e5e

11 files changed

Lines changed: 646 additions & 9 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://develop.svn.wordpress.org/trunk"
88
},
99
"gutenberg": {
10-
"ref": "23b566c72e9c4a36219ef5d6e62890f05551f6cb"
10+
"ref": "022d8dd3d461f91b15c1f0410649d3ebb027207f"
1111
},
1212
"engines": {
1313
"node": ">=20.10.0",

src/wp-includes/class-wp-icons-registry.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ class WP_Icons_Registry {
1818
*
1919
* @var array[]
2020
*/
21-
private $registered_icons = array();
21+
protected $registered_icons = array();
2222

2323

2424
/**
2525
* Container for the main instance of the class.
2626
*
2727
* @var WP_Icons_Registry|null
2828
*/
29-
private static $instance = null;
29+
protected static $instance = null;
3030

3131
/**
3232
* Constructor.
3333
*
34-
* WP_Icons_Registry is a singleton class, so keep this private.
34+
* WP_Icons_Registry is a singleton class, so keep this protected.
3535
*
3636
* For 7.0, the Icons Registry is closed for third-party icon registry,
3737
* serving only a subset of core icons.
@@ -40,7 +40,7 @@ class WP_Icons_Registry {
4040
* SVG files and as entries in a single manifest file. On init, the
4141
* registry is loaded with those icons listed in the manifest.
4242
*/
43-
private function __construct() {
43+
protected function __construct() {
4444
$icons_directory = __DIR__ . '/icons/';
4545
$icons_directory = trailingslashit( $icons_directory );
4646
$manifest_path = $icons_directory . 'manifest.php';
@@ -101,7 +101,7 @@ private function __construct() {
101101
* }
102102
* @return bool True if the icon was registered with success and false otherwise.
103103
*/
104-
private function register( $icon_name, $icon_properties ) {
104+
protected function register( $icon_name, $icon_properties ) {
105105
if ( ! isset( $icon_name ) || ! is_string( $icon_name ) ) {
106106
_doing_it_wrong(
107107
__METHOD__,
@@ -188,7 +188,7 @@ private function register( $icon_name, $icon_properties ) {
188188
* @param string $icon_content The icon SVG content to sanitize.
189189
* @return string The sanitized icon SVG content.
190190
*/
191-
private function sanitize_icon_content( $icon_content ) {
191+
protected function sanitize_icon_content( $icon_content ) {
192192
$allowed_tags = array(
193193
'svg' => array(
194194
'class' => true,
@@ -223,7 +223,7 @@ private function sanitize_icon_content( $icon_content ) {
223223
* @param string $icon_name Icon name including namespace.
224224
* @return string|null The content of the icon, if found.
225225
*/
226-
private function get_content( $icon_name ) {
226+
protected function get_content( $icon_name ) {
227227
if ( ! isset( $this->registered_icons[ $icon_name ]['content'] ) ) {
228228
$content = file_get_contents(
229229
$this->registered_icons[ $icon_name ]['filePath']

src/wp-includes/connectors.php

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
<?php
2+
/**
3+
* Connectors API.
4+
*
5+
* @package WordPress
6+
* @subpackage Connectors
7+
* @since 7.0.0
8+
*/
9+
10+
use WordPress\AiClient\AiClient;
11+
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
12+
13+
/**
14+
* Registers the Connectors menu item under Settings.
15+
*
16+
* @since 7.0.0
17+
* @access private
18+
*/
19+
function _wp_connectors_add_settings_menu_item(): void {
20+
if ( ! class_exists( '\WordPress\AiClient\AiClient' ) || ! function_exists( 'wp_connectors_wp_admin_render_page' ) ) {
21+
return;
22+
}
23+
24+
add_submenu_page(
25+
'options-general.php',
26+
__( 'Connectors' ),
27+
__( 'Connectors' ),
28+
'manage_options',
29+
'connectors-wp-admin',
30+
'wp_connectors_wp_admin_render_page',
31+
1
32+
);
33+
}
34+
add_action( 'admin_menu', '_wp_connectors_add_settings_menu_item' );
35+
36+
/**
37+
* Masks an API key, showing only the last 4 characters.
38+
*
39+
* @since 7.0.0
40+
* @access private
41+
*
42+
* @param string $key The API key to mask.
43+
* @return string The masked key, e.g. "************fj39".
44+
*/
45+
function _wp_connectors_mask_api_key( string $key ): string {
46+
if ( strlen( $key ) <= 4 ) {
47+
return $key;
48+
}
49+
50+
return str_repeat( "\u{2022}", min( strlen( $key ) - 4, 16 ) ) . substr( $key, -4 );
51+
}
52+
53+
/**
54+
* Checks whether an API key is valid for a given provider.
55+
*
56+
* @since 7.0.0
57+
* @access private
58+
*
59+
* @param string $key The API key to check.
60+
* @param string $provider_id The WP AI client provider ID.
61+
* @return bool|null True if valid, false if invalid, null if unable to determine.
62+
*/
63+
function _wp_connectors_is_api_key_valid( string $key, string $provider_id ): ?bool {
64+
try {
65+
$registry = AiClient::defaultRegistry();
66+
67+
if ( ! $registry->hasProvider( $provider_id ) ) {
68+
_doing_it_wrong(
69+
__FUNCTION__,
70+
sprintf(
71+
/* translators: %s: AI provider ID. */
72+
__( 'The provider "%s" is not registered in the AI client registry.' ),
73+
$provider_id
74+
),
75+
'7.0.0'
76+
);
77+
return null;
78+
}
79+
80+
$registry->setProviderRequestAuthentication(
81+
$provider_id,
82+
new ApiKeyRequestAuthentication( $key )
83+
);
84+
85+
return $registry->isProviderConfigured( $provider_id );
86+
} catch ( Exception $e ) {
87+
wp_trigger_error( __FUNCTION__, $e->getMessage() );
88+
return null;
89+
}
90+
}
91+
92+
/**
93+
* Retrieves the real (unmasked) value of a connector API key.
94+
*
95+
* Temporarily removes the masking filter, reads the option, then re-adds it.
96+
*
97+
* @since 7.0.0
98+
* @access private
99+
*
100+
* @param string $option_name The option name for the API key.
101+
* @param callable $mask_callback The mask filter function.
102+
* @return string The real API key value.
103+
*/
104+
function _wp_connectors_get_real_api_key( string $option_name, callable $mask_callback ): string {
105+
remove_filter( "option_{$option_name}", $mask_callback );
106+
$value = get_option( $option_name, '' );
107+
add_filter( "option_{$option_name}", $mask_callback );
108+
return (string) $value;
109+
}
110+
111+
/**
112+
* Gets the registered connector provider settings.
113+
*
114+
* @since 7.0.0
115+
* @access private
116+
*
117+
* @return array<string, array{provider: string, label: string, description: string, mask: callable, sanitize: callable}> Provider settings keyed by setting name.
118+
*/
119+
function _wp_connectors_get_provider_settings(): array {
120+
$providers = array(
121+
'google' => array(
122+
'name' => 'Google',
123+
),
124+
'openai' => array(
125+
'name' => 'OpenAI',
126+
),
127+
'anthropic' => array(
128+
'name' => 'Anthropic',
129+
),
130+
);
131+
132+
$provider_settings = array();
133+
foreach ( $providers as $provider => $data ) {
134+
$setting_name = "connectors_ai_{$provider}_api_key";
135+
136+
$provider_settings[ $setting_name ] = array(
137+
'provider' => $provider,
138+
'label' => sprintf(
139+
/* translators: %s: AI provider name. */
140+
__( '%s API Key' ),
141+
$data['name']
142+
),
143+
'description' => sprintf(
144+
/* translators: %s: AI provider name. */
145+
__( 'API key for the %s AI provider.' ),
146+
$data['name']
147+
),
148+
'mask' => '_wp_connectors_mask_api_key',
149+
'sanitize' => static function ( string $value ) use ( $provider ): string {
150+
$value = sanitize_text_field( $value );
151+
if ( '' === $value ) {
152+
return $value;
153+
}
154+
155+
$valid = _wp_connectors_is_api_key_valid( $value, $provider );
156+
return true === $valid ? $value : '';
157+
},
158+
);
159+
}
160+
return $provider_settings;
161+
}
162+
163+
/**
164+
* Validates connector API keys in the REST response when explicitly requested.
165+
*
166+
* Runs on `rest_post_dispatch` for `/wp/v2/settings` requests that include connector
167+
* fields via `_fields`. For each requested connector field, it validates the unmasked
168+
* key against the provider and replaces the response value with `invalid_key` if
169+
* validation fails.
170+
*
171+
* @since 7.0.0
172+
* @access private
173+
*
174+
* @param WP_REST_Response $response The response object.
175+
* @param WP_REST_Server $server The server instance.
176+
* @param WP_REST_Request $request The request object.
177+
* @return WP_REST_Response The potentially modified response.
178+
*/
179+
function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response {
180+
if ( '/wp/v2/settings' !== $request->get_route() ) {
181+
return $response;
182+
}
183+
184+
if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
185+
return $response;
186+
}
187+
188+
$fields = $request->get_param( '_fields' );
189+
if ( ! $fields ) {
190+
return $response;
191+
}
192+
193+
if ( is_array( $fields ) ) {
194+
$requested = $fields;
195+
} else {
196+
$requested = array_map( 'trim', explode( ',', $fields ) );
197+
}
198+
199+
$data = $response->get_data();
200+
if ( ! is_array( $data ) ) {
201+
return $response;
202+
}
203+
204+
foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) {
205+
if ( ! in_array( $setting_name, $requested, true ) ) {
206+
continue;
207+
}
208+
209+
$real_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] );
210+
if ( '' === $real_key ) {
211+
continue;
212+
}
213+
214+
if ( true !== _wp_connectors_is_api_key_valid( $real_key, $config['provider'] ) ) {
215+
$data[ $setting_name ] = 'invalid_key';
216+
}
217+
}
218+
219+
$response->set_data( $data );
220+
return $response;
221+
}
222+
add_filter( 'rest_post_dispatch', '_wp_connectors_validate_keys_in_rest', 10, 3 );
223+
224+
/**
225+
* Registers default connector settings and mask/sanitize filters.
226+
*
227+
* @since 7.0.0
228+
* @access private
229+
*/
230+
function _wp_register_default_connector_settings(): void {
231+
if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
232+
return;
233+
}
234+
235+
foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) {
236+
register_setting(
237+
'connectors',
238+
$setting_name,
239+
array(
240+
'type' => 'string',
241+
'label' => $config['label'],
242+
'description' => $config['description'],
243+
'default' => '',
244+
'show_in_rest' => true,
245+
'sanitize_callback' => $config['sanitize'],
246+
)
247+
);
248+
add_filter( "option_{$setting_name}", $config['mask'] );
249+
}
250+
}
251+
add_action( 'init', '_wp_register_default_connector_settings' );
252+
253+
/**
254+
* Passes stored connector API keys to the WP AI client.
255+
*
256+
* @since 7.0.0
257+
* @access private
258+
*/
259+
function _wp_connectors_pass_default_keys_to_ai_client(): void {
260+
if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
261+
return;
262+
}
263+
try {
264+
$registry = AiClient::defaultRegistry();
265+
foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) {
266+
$api_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] );
267+
if ( '' === $api_key || ! $registry->hasProvider( $config['provider'] ) ) {
268+
continue;
269+
}
270+
271+
$registry->setProviderRequestAuthentication(
272+
$config['provider'],
273+
new ApiKeyRequestAuthentication( $api_key )
274+
);
275+
}
276+
} catch ( Exception $e ) {
277+
wp_trigger_error( __FUNCTION__, $e->getMessage() );
278+
}
279+
}
280+
add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client' );

src/wp-includes/version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @global string $wp_version
1818
*/
19-
$wp_version = '7.0-beta1-61709-src';
19+
$wp_version = '7.0-beta2-61752-src';
2020

2121
/**
2222
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

src/wp-settings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@
294294
require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-ability-function-resolver.php';
295295
require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-prompt-builder.php';
296296
require ABSPATH . WPINC . '/ai-client.php';
297+
require ABSPATH . WPINC . '/connectors.php';
297298
require ABSPATH . WPINC . '/class-wp-icons-registry.php';
298299
require ABSPATH . WPINC . '/widgets.php';
299300
require ABSPATH . WPINC . '/class-wp-widget.php';

0 commit comments

Comments
 (0)