Make WordPress Core

Changeset 63348


Ignore:
Timestamp:
08/26/2026 11:48:53 AM (6 hours ago)
Author:
gziolo
Message:

Connectors: Only validate submitted AI provider API keys

_wp_connectors_rest_settings_dispatch() validated every stored AI provider API key on each /wp/v2/settings update, because the response always contains the full set of registered settings. Saving an unrelated setting therefore triggered a live validation request to each configured provider, and a key that failed validation at that moment was silently reset.

Validate a key only when it is present in the current request, using WP_REST_Request::has_param(). Masking of stored keys in the response is unchanged.

Add test coverage for omitted, submitted-invalid, and submitted-valid keys to the existing Tests_Connectors_WpConnectorsRestSettingsDispatch suite.

Developed in: https://github.com/WordPress/wordpress-develop/pull/12350

Props khokansardar, hbhalodia, gziolo.
Fixes #65554. See #65867.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/connectors.php

    r62684 r63348  
    732732                $value = $data[ $setting_name ];
    733733
    734                 // On update, validate AI provider keys before masking.
     734                // On update, validate AI provider keys submitted in the request before masking.
    735735                // Non-AI connectors accept keys as-is; the service plugin handles its own validation.
    736                 if ( $is_update && is_string( $value ) && '' !== $value && 'ai_provider' === $connector_data['type'] ) {
     736                if ( $is_update
     737                        && $request->has_param( $setting_name )
     738                        && is_string( $value ) && '' !== $value
     739                        && 'ai_provider' === $connector_data['type']
     740                ) {
    737741                        if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) {
    738742                                update_option( $setting_name, '' );
  • trunk/tests/phpunit/tests/connectors/wpConnectorsRestSettingsDispatch.php

    r62684 r63348  
    11<?php
     2
     3require_once dirname( __DIR__, 2 ) . '/includes/wp-ai-client-mock-provider-trait.php';
     4
    25/**
    36 * Tests for _wp_connectors_rest_settings_dispatch().
     
    811class Tests_Connectors_WpConnectorsRestSettingsDispatch extends WP_UnitTestCase {
    912
     13        use WP_AI_Client_Mock_Provider_Trait;
     14
    1015        const CONNECTOR_ID             = 'wp_test_application_password_connector';
    1116        const CREDENTIALS_SETTING_NAME = 'connectors_test_remote_credentials';
     17        const AI_KEY_SETTING_NAME      = 'connectors_ai_mock_connectors_test_api_key';
     18
     19        /**
     20         * Registers the mock AI provider connector once before any tests in this class run.
     21         */
     22        public static function set_up_before_class(): void {
     23                parent::set_up_before_class();
     24                self::register_mock_connectors_provider();
     25        }
     26
     27        /**
     28         * Unregisters the mock AI provider's setting after all tests in this class have run.
     29         */
     30        public static function tear_down_after_class(): void {
     31                self::unregister_mock_connector_setting();
     32                parent::tear_down_after_class();
     33        }
    1234
    1335        /**
     
    1638        public function set_up(): void {
    1739                parent::set_up();
     40
     41                self::set_mock_provider_configured( true );
    1842
    1943                WP_Connector_Registry::get_instance()->register(
     
    6488                $this->assertNotSame( $application_password, $data[ self::CREDENTIALS_SETTING_NAME ]['password'] );
    6589        }
     90
     91        /**
     92         * Ensures a stored AI provider key is not re-validated, and therefore not reset,
     93         * when it is not part of the current settings update.
     94         *
     95         * The /wp/v2/settings response always carries every registered setting, so the
     96         * response data alone cannot tell which keys the request actually submitted.
     97         *
     98         * @ticket 65554
     99         */
     100        public function test_does_not_validate_or_reset_unsubmitted_ai_key(): void {
     101                $stored_key = 'sk-stored-valid-key';
     102                update_option( self::AI_KEY_SETTING_NAME, $stored_key );
     103
     104                // A provider that would fail validation if it were consulted.
     105                self::set_mock_provider_configured( false );
     106
     107                // An update that submits an unrelated setting.
     108                $request = new WP_REST_Request( 'POST', '/wp/v2/settings' );
     109                $request->set_param( 'title', 'New Site Title' );
     110                $response = new WP_REST_Response( array( self::AI_KEY_SETTING_NAME => $stored_key ) );
     111
     112                $result = _wp_connectors_rest_settings_dispatch( $response, rest_get_server(), $request );
     113                $data   = $result->get_data();
     114
     115                $this->assertSame(
     116                        $stored_key,
     117                        get_option( self::AI_KEY_SETTING_NAME ),
     118                        'An AI provider key that was not submitted should not be reset.'
     119                );
     120                $this->assertSame(
     121                        _wp_connectors_mask_api_key( $stored_key ),
     122                        $data[ self::AI_KEY_SETTING_NAME ],
     123                        'The stored AI provider key should still be masked in the response.'
     124                );
     125        }
     126
     127        /**
     128         * Ensures a submitted AI provider key that fails validation is still discarded.
     129         *
     130         * @ticket 65554
     131         */
     132        public function test_discards_submitted_invalid_ai_key(): void {
     133                $submitted_key = 'sk-submitted-invalid-key';
     134                update_option( self::AI_KEY_SETTING_NAME, $submitted_key );
     135
     136                self::set_mock_provider_configured( false );
     137
     138                $request = new WP_REST_Request( 'POST', '/wp/v2/settings' );
     139                $request->set_param( self::AI_KEY_SETTING_NAME, $submitted_key );
     140                $response = new WP_REST_Response( array( self::AI_KEY_SETTING_NAME => $submitted_key ) );
     141
     142                $result = _wp_connectors_rest_settings_dispatch( $response, rest_get_server(), $request );
     143                $data   = $result->get_data();
     144
     145                $this->assertSame(
     146                        '',
     147                        get_option( self::AI_KEY_SETTING_NAME ),
     148                        'A submitted AI provider key that fails validation should be discarded.'
     149                );
     150                $this->assertSame(
     151                        '',
     152                        $data[ self::AI_KEY_SETTING_NAME ],
     153                        'The discarded key should be returned as an empty string.'
     154                );
     155        }
     156
     157        /**
     158         * Ensures a submitted AI provider key that passes validation is kept and masked.
     159         *
     160         * @ticket 65554
     161         */
     162        public function test_keeps_and_masks_submitted_valid_ai_key(): void {
     163                $submitted_key = 'sk-submitted-valid-key';
     164                update_option( self::AI_KEY_SETTING_NAME, $submitted_key );
     165
     166                self::set_mock_provider_configured( true );
     167
     168                $request = new WP_REST_Request( 'POST', '/wp/v2/settings' );
     169                $request->set_param( self::AI_KEY_SETTING_NAME, $submitted_key );
     170                $response = new WP_REST_Response( array( self::AI_KEY_SETTING_NAME => $submitted_key ) );
     171
     172                $result = _wp_connectors_rest_settings_dispatch( $response, rest_get_server(), $request );
     173                $data   = $result->get_data();
     174
     175                $this->assertSame(
     176                        $submitted_key,
     177                        get_option( self::AI_KEY_SETTING_NAME ),
     178                        'A submitted AI provider key that passes validation should be kept.'
     179                );
     180                $this->assertSame(
     181                        _wp_connectors_mask_api_key( $submitted_key ),
     182                        $data[ self::AI_KEY_SETTING_NAME ],
     183                        'The submitted AI provider key should be masked in the response.'
     184                );
     185        }
    66186}
Note: See TracChangeset for help on using the changeset viewer.