Make WordPress Core

Changeset 62684


Ignore:
Timestamp:
07/10/2026 10:11:37 AM (2 months ago)
Author:
jorgefilipecosta
Message:

Connectors: Add application password authentication.

Introduce an application_password connector authentication method. Registering a connector with this method auto-generates and registers a setting named connectors_{$type}_{$id}_application_password that stores the username and application password together as an object through the Settings API.
Only the application password is masked in REST responses; non-secret connection metadata is exposed to the admin UI via the connector script module data. Credentials may also be supplied through constants or environment variables, and malformed external values are reported with _doing_it_wrong() and skipped so resolution falls through to the next source.

Props jorgefilipecosta, gziolo, jorbin.
See #64850.

Location:
trunk
Files:
3 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-connector-registry.php

    r62332 r62684  
    3434 *     type: non-empty-string,
    3535 *     authentication: array{
    36  *         method: 'api_key'|'none',
     36 *         method: 'api_key'|'application_password'|'none',
    3737 *         credentials_url?: non-empty-string,
    3838 *         setting_name?: non-empty-string,
     
    7070         *
    7171         * Validates the provided arguments and stores the connector in the registry.
    72          * For connectors with `api_key` authentication, a `setting_name` can be provided
    73          * explicitly. If omitted, one is automatically generated using the pattern
    74          * `connectors_{$type}_{$id}_api_key`, with hyphens in the type and ID normalized
    75          * to underscores (e.g., connector type `spam_filtering` with ID `my_plugin` produces
    76          * `connectors_spam_filtering_my_plugin_api_key`). This setting name is used for the
    77          * Settings API registration and REST API exposure.
     72         * For connectors with `api_key` or `application_password` authentication, a
     73         * `setting_name` can be provided explicitly. When omitted, setting names are
     74         * automatically generated using the pattern `connectors_{$type}_{$id}_{$method}`,
     75         * with hyphens in the type and ID normalized to underscores. These setting
     76         * names are used for Settings API registration and REST API exposure.
    7877         *
    7978         * Registering a connector with an ID that is already registered will trigger a
     
    9796         *         Required. Authentication configuration.
    9897         *
    99          *         @type string $method          Required. The authentication method: 'api_key' or 'none'.
     98         *         @type string $method          Required. The authentication method: 'api_key',
     99         *                                       'application_password', or 'none'.
    100100         *         @type string $credentials_url Optional. URL where users can obtain API credentials.
    101          *         @type string $setting_name    Optional. The setting name for the API key.
    102          *                                       When omitted, auto-generated as
    103          *                                       `connectors_{$type}_{$id}_api_key`.
     101         *         @type string $setting_name    Optional. The setting name for the API key
     102         *                                       or application-password credentials. When
     103         *                                       omitted, auto-generated as
     104         *                                       `connectors_{$type}_{$id}_api_key` for API
     105         *                                       keys and `connectors_{$type}_{$id}_application_password`
     106         *                                       for application passwords.
    104107         *                                       Must be a non-empty string when provided.
    105108         *         @type string $constant_name   Optional. PHP constant name for the API key
    106          *                                       (e.g. 'ANTHROPIC_API_KEY'). Only checked when provided.
     109         *                                       (e.g. 'ANTHROPIC_API_KEY') or for application-password
     110         *                                       credentials in `username:password` format. Only checked
     111         *                                       when provided.
    107112         *         @type string $env_var_name    Optional. Environment variable name for the API key
    108          *                                       (e.g. 'ANTHROPIC_API_KEY'). Only checked when provided.
     113         *                                       (e.g. 'ANTHROPIC_API_KEY') or for application-password
     114         *                                       credentials in `username:password` format. Only checked
     115         *                                       when provided.
    109116         *     }
    110117         *     @type array  $plugin         {
     
    127134         *     type: non-empty-string,
    128135         *     authentication: array{
    129          *         method: 'api_key'|'none',
     136         *         method: 'api_key'|'application_password'|'none',
    130137         *         credentials_url?: non-empty-string,
    131138         *         setting_name?: non-empty-string,
     
    193200                }
    194201
    195                 if ( empty( $args['authentication']['method'] ) || ! in_array( $args['authentication']['method'], array( 'api_key', 'none' ), true ) ) {
    196                         _doing_it_wrong(
    197                                 __METHOD__,
    198                                 /* translators: %s: Connector ID. */
    199                                 sprintf( __( 'Connector "%s" authentication method must be "api_key" or "none".' ), esc_html( $id ) ),
     202                if ( empty( $args['authentication']['method'] ) || ! in_array( $args['authentication']['method'], array( 'api_key', 'application_password', 'none' ), true ) ) {
     203                        _doing_it_wrong(
     204                                __METHOD__,
     205                                /* translators: %s: Connector ID. */
     206                                sprintf( __( 'Connector "%s" authentication method must be "api_key", "application_password", or "none".' ), esc_html( $id ) ),
    200207                                '7.0.0'
    201208                        );
     
    221228                }
    222229
    223                 if ( 'api_key' === $args['authentication']['method'] ) {
     230                $requires_credentials = in_array( $args['authentication']['method'], array( 'api_key', 'application_password' ), true );
     231
     232                if ( $requires_credentials ) {
    224233                        if ( ! empty( $args['authentication']['credentials_url'] ) && is_string( $args['authentication']['credentials_url'] ) ) {
    225234                                $connector['authentication']['credentials_url'] = $args['authentication']['credentials_url'];
    226235                        }
     236
    227237                        if ( isset( $args['authentication']['setting_name'] ) ) {
    228238                                if ( ! is_string( $args['authentication']['setting_name'] ) || '' === $args['authentication']['setting_name'] ) {
     
    237247                                $connector['authentication']['setting_name'] = $args['authentication']['setting_name'];
    238248                        } else {
    239                                 $connector['authentication']['setting_name'] = str_replace( '-', '_', "connectors_{$connector['type']}_{$id}_api_key" );
    240                         }
     249                                $connector['authentication']['setting_name'] = str_replace( '-', '_', "connectors_{$connector['type']}_{$id}_{$args['authentication']['method']}" );
     250                        }
     251
    241252                        if ( isset( $args['authentication']['constant_name'] ) ) {
    242253                                if ( ! is_string( $args['authentication']['constant_name'] ) || '' === $args['authentication']['constant_name'] ) {
  • trunk/src/wp-includes/connectors.php

    r62341 r62684  
    4646 *     @type string $type           The connector type, e.g. 'ai_provider'.
    4747 *     @type array  $authentication {
    48  *         Authentication configuration. When method is 'api_key', includes
    49  *         credentials_url, setting_name, and optionally constant_name and
    50  *         env_var_name. When 'none', only method is present.
    51  *
    52  *         @type string $method          The authentication method: 'api_key' or 'none'.
     48 *         Authentication configuration. When method is 'api_key' or
     49 *         'application_password', includes credentials_url, setting_name, and
     50 *         optionally constant_name and env_var_name. When 'none', only method
     51 *         is present.
     52 *
     53 *         @type string $method          The authentication method: 'api_key',
     54 *                                       'application_password', or 'none'.
    5355 *         @type string $credentials_url Optional. URL where users can obtain API credentials.
    54  *         @type string $setting_name    Optional. The setting name for the API key.
    55  *         @type string $constant_name   Optional. PHP constant name for the API key.
    56  *         @type string $env_var_name    Optional. Environment variable name for the API key.
     56 *         @type string $setting_name    Optional. The setting name for the API key or application-password credentials.
     57 *         @type string $constant_name   Optional. PHP constant name for the API key or application-password credentials.
     58 *         @type string $env_var_name    Optional. Environment variable name for the API key or application-password credentials.
    5759 *     }
    5860 *     @type array  $plugin         {
     
    7173 *     type: non-empty-string,
    7274 *     authentication: array{
    73  *         method: 'api_key'|'none',
     75 *         method: 'api_key'|'application_password'|'none',
    7476 *         credentials_url?: non-empty-string,
    7577 *         setting_name?: non-empty-string,
     
    110112 *         @type string      $type           The connector type, e.g. 'ai_provider'.
    111113 *         @type array       $authentication {
    112  *             Authentication configuration. When method is 'api_key', includes
    113  *             credentials_url, setting_name, and optionally constant_name and
    114  *             env_var_name. When 'none', only method is present.
    115  *
    116  *             @type string $method          The authentication method: 'api_key' or 'none'.
     114 *             Authentication configuration. When method is 'api_key' or
     115 *             'application_password', includes credentials_url, setting_name,
     116 *             and optionally constant_name and env_var_name. When 'none', only
     117 *             method is present.
     118 *
     119 *             @type string $method          The authentication method: 'api_key',
     120 *                                           'application_password', or 'none'.
    117121 *             @type string $credentials_url Optional. URL where users can obtain API credentials.
    118  *             @type string $setting_name    Optional. The setting name for the API key.
    119  *             @type string $constant_name   Optional. PHP constant name for the API key.
    120  *             @type string $env_var_name    Optional. Environment variable name for the API key.
     122 *             @type string $setting_name    Optional. The setting name for the API key or application-password credentials.
     123 *             @type string $constant_name   Optional. PHP constant name for the API key or application-password credentials.
     124 *             @type string $env_var_name    Optional. Environment variable name for the API key or application-password credentials.
    121125 *         }
    122126 *         @type array       $plugin         {
     
    136140 *     type: non-empty-string,
    137141 *     authentication: array{
    138  *         method: 'api_key'|'none',
     142 *         method: 'api_key'|'application_password'|'none',
    139143 *         credentials_url?: non-empty-string,
    140144 *         setting_name?: non-empty-string,
     
    465469
    466470/**
     471 * Parses a `username:password` credentials string.
     472 *
     473 * Splits on the first colon, matching the HTTP Basic authentication
     474 * userinfo format, so passwords may contain colons.
     475 *
     476 * @since 7.1.0
     477 * @access private
     478 *
     479 * @param string $value The raw credentials string.
     480 * @return array{username: string, password: string} Parsed credentials. Both values
     481 *                                                   are empty when the string is malformed.
     482 */
     483function wp_connectors_parse_application_password_credentials( string $value ): array {
     484        $separator = strpos( $value, ':' );
     485        // Trim so surrounding whitespace or a trailing newline (common when the
     486        // value comes from a file or `.env`) does not become part of the credentials.
     487        $username = false === $separator ? '' : trim( substr( $value, 0, $separator ) );
     488        $password = false === $separator ? '' : trim( substr( $value, $separator + 1 ) );
     489
     490        if ( '' === $username || '' === $password ) {
     491                return array(
     492                        'username' => '',
     493                        'password' => '',
     494                );
     495        }
     496
     497        return array(
     498                'username' => $username,
     499                'password' => $password,
     500        );
     501}
     502
     503/**
     504 * Resolves application-password credentials for a connector.
     505 *
     506 * Checks in order: environment variable, PHP constant, database. The
     507 * environment variable and constant are only checked when their respective
     508 * names are provided, and must contain the credentials as a single
     509 * `username:password` string. A non-empty environment variable or constant
     510 * that cannot be parsed as `username:password` is reported with
     511 * `_doing_it_wrong()` and ignored, so resolution falls through to the next
     512 * source.
     513 *
     514 * @since 7.1.0
     515 * @access private
     516 *
     517 * @param array $auth The connector's authentication configuration.
     518 * @return array{username: string, password: string, source: string} Resolved credentials and
     519 *                                                                   their source: 'env', 'constant',
     520 *                                                                   'database', or 'none'.
     521 */
     522function wp_connectors_get_application_password_credentials( array $auth ): array {
     523        // Check environment variable first.
     524        $env_var_name = $auth['env_var_name'] ?? '';
     525        if ( '' !== $env_var_name ) {
     526                $env_value = getenv( $env_var_name );
     527                if ( false !== $env_value && '' !== $env_value ) {
     528                        $credentials = wp_connectors_parse_application_password_credentials( $env_value );
     529                        if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) {
     530                                $credentials['source'] = 'env';
     531                                return $credentials;
     532                        }
     533
     534                        _doing_it_wrong(
     535                                __FUNCTION__,
     536                                sprintf(
     537                                        /* translators: %s: Environment variable name. */
     538                                        __( 'The %s environment variable must contain application password credentials in "username:password" format.' ),
     539                                        esc_html( $env_var_name )
     540                                ),
     541                                '7.1.0'
     542                        );
     543                }
     544        }
     545
     546        // Check PHP constant.
     547        $constant_name = $auth['constant_name'] ?? '';
     548        if ( '' !== $constant_name && defined( $constant_name ) ) {
     549                $const_value = constant( $constant_name );
     550                if ( is_string( $const_value ) && '' !== $const_value ) {
     551                        $credentials = wp_connectors_parse_application_password_credentials( $const_value );
     552                        if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) {
     553                                $credentials['source'] = 'constant';
     554                                return $credentials;
     555                        }
     556
     557                        _doing_it_wrong(
     558                                __FUNCTION__,
     559                                sprintf(
     560                                        /* translators: %s: PHP constant name. */
     561                                        __( 'The %s constant must contain application password credentials in "username:password" format.' ),
     562                                        esc_html( $constant_name )
     563                                ),
     564                                '7.1.0'
     565                        );
     566                }
     567        }
     568
     569        // Check database.
     570        $stored   = get_option( $auth['setting_name'] ?? '', array() );
     571        $username = is_array( $stored ) && isset( $stored['username'] ) && is_string( $stored['username'] ) ? $stored['username'] : '';
     572        $password = is_array( $stored ) && isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : '';
     573
     574        return array(
     575                'username' => $username,
     576                'password' => $password,
     577                'source'   => '' !== $username && '' !== $password ? 'database' : 'none',
     578        );
     579}
     580
     581/**
    467582 * Checks whether an API key is valid for a given provider.
    468583 *
     
    504619
    505620/**
    506  * Masks and validates connector API keys in REST responses.
    507  *
    508  * On every `/wp/v2/settings` response, masks connector API key values so raw
    509  * keys are never exposed via the REST API.
    510  *
    511  * On POST or PUT requests, validates each updated key against the provider
    512  * before masking. If validation fails, the key is reverted to an empty string.
     621 * Sanitizes stored application-password credentials for a connector.
     622 *
     623 * Credential fields that are missing or not strings keep their currently
     624 * stored values, so partial updates cannot silently clear a stored secret.
     625 * A password matching the mask that `_wp_connectors_rest_settings_dispatch()`
     626 * places in REST responses also keeps the stored password, so a masked
     627 * settings response can be submitted back to the endpoint unchanged.
     628 * Pass an empty string to clear a field.
     629 * If the sanitized username is empty, both fields are discarded so partial
     630 * credentials cannot leave an orphaned secret.
     631 *
     632 * @since 7.1.0
     633 * @access private
     634 *
     635 * @param mixed  $value  The submitted setting value.
     636 * @param string $option The option name being sanitized. Passed explicitly by the
     637 *                       registered sanitize callback; falls back to the current
     638 *                       `sanitize_option_{$option}` filter name when omitted.
     639 * @return array{username: string, password: string} Sanitized credentials.
     640 */
     641function wp_connectors_sanitize_application_password_credentials( $value, string $option = '' ): array {
     642        if ( ! is_array( $value ) ) {
     643                $value = array();
     644        }
     645
     646        if ( '' === $option ) {
     647                $option = str_replace( 'sanitize_option_', '', (string) current_filter() );
     648        }
     649
     650        $stored = get_option( $option );
     651        if ( ! is_array( $stored ) ) {
     652                $stored = array();
     653        }
     654
     655        $credentials = array();
     656        foreach ( array( 'username', 'password' ) as $field ) {
     657                if ( isset( $value[ $field ] ) && is_string( $value[ $field ] ) ) {
     658                        $credentials[ $field ] = sanitize_text_field( $value[ $field ] );
     659                } else {
     660                        $credentials[ $field ] = isset( $stored[ $field ] ) && is_string( $stored[ $field ] ) ? $stored[ $field ] : '';
     661                }
     662        }
     663
     664        // A masked password means a client resubmitted a masked REST response.
     665        if ( str_repeat( "\u{2022}", 16 ) === $credentials['password'] ) {
     666                $credentials['password'] = isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : '';
     667        }
     668
     669        if ( '' === $credentials['username'] ) {
     670                return array(
     671                        'username' => '',
     672                        'password' => '',
     673                );
     674        }
     675
     676        return $credentials;
     677}
     678
     679/**
     680 * Masks and validates connector credentials in REST responses.
     681 *
     682 * On every `/wp/v2/settings` response, masks connector API key values and the
     683 * password field of default application-password credential objects.
     684 *
     685 * On POST or PUT requests, validates each updated AI provider API key before
     686 * masking. If validation fails, the key is reverted to an empty string.
     687 * Application password values are masked but not validated.
    513688 *
    514689 * @since 7.0.0
     
    534709        foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
    535710                $auth = $connector_data['authentication'];
     711
     712                if ( 'application_password' === $auth['method'] && ! empty( $auth['setting_name'] ) ) {
     713                        $setting_name = $auth['setting_name'];
     714                        if ( array_key_exists( $setting_name, $data ) && is_array( $data[ $setting_name ] ) ) {
     715                                $password = $data[ $setting_name ]['password'] ?? '';
     716                                if ( is_string( $password ) && '' !== $password ) {
     717                                        $data[ $setting_name ]['password'] = str_repeat( "\u{2022}", 16 );
     718                                }
     719                        }
     720                        continue;
     721                }
     722
    536723                if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
    537724                        continue;
     
    577764        foreach ( wp_get_connectors() as $connector_data ) {
    578765                $auth = $connector_data['authentication'];
    579                 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
     766                if ( 'api_key' !== $auth['method'] && 'application_password' !== $auth['method'] ) {
    580767                        continue;
    581768                }
    582769
    583                 // Skip if the setting is already registered (e.g. by an owning plugin).
    584                 if ( isset( $registered_settings[ $auth['setting_name'] ] ) ) {
     770                if ( empty( $auth['setting_name'] ) || isset( $registered_settings[ $auth['setting_name'] ] ) ) {
    585771                        continue;
    586772                }
     773                $setting_name = $auth['setting_name'];
    587774
    588775                if ( ! isset( $connector_data['plugin']['is_active'] ) || ! is_callable( $connector_data['plugin']['is_active'] ) ) {
     
    594781                }
    595782
    596                 register_setting(
    597                         'connectors',
    598                         $auth['setting_name'],
    599                         array(
    600                                 'type'              => 'string',
    601                                 'label'             => sprintf(
    602                                         /* translators: %s: Connector name. */
    603                                         __( '%s API Key' ),
    604                                         $connector_data['name']
    605                                 ),
    606                                 'description'       => sprintf(
    607                                         /* translators: %s: Connector name. */
    608                                         __( 'API key for the %s connector.' ),
    609                                         $connector_data['name']
    610                                 ),
    611                                 'default'           => '',
    612                                 'show_in_rest'      => true,
    613                                 'sanitize_callback' => 'sanitize_text_field',
    614                         )
    615                 );
     783                if ( 'api_key' === $auth['method'] ) {
     784                        register_setting(
     785                                'connectors',
     786                                $setting_name,
     787                                array(
     788                                        'type'              => 'string',
     789                                        'label'             => sprintf(
     790                                                /* translators: %s: Connector name. */
     791                                                __( '%s API Key' ),
     792                                                $connector_data['name']
     793                                        ),
     794                                        'description'       => sprintf(
     795                                                /* translators: %s: Connector name. */
     796                                                __( 'API key for the %s connector.' ),
     797                                                $connector_data['name']
     798                                        ),
     799                                        'default'           => '',
     800                                        'show_in_rest'      => true,
     801                                        'sanitize_callback' => 'sanitize_text_field',
     802                                )
     803                        );
     804                } elseif ( 'application_password' === $auth['method'] ) {
     805                        register_setting(
     806                                'connectors',
     807                                $setting_name,
     808                                array(
     809                                        'type'              => 'object',
     810                                        'label'             => sprintf(
     811                                                /* translators: %s: Connector name. */
     812                                                __( '%s Credentials' ),
     813                                                $connector_data['name']
     814                                        ),
     815                                        'description'       => sprintf(
     816                                                /* translators: %s: Connector name. */
     817                                                __( 'Application password credentials for the %s connector.' ),
     818                                                $connector_data['name']
     819                                        ),
     820                                        'default'           => array(
     821                                                'username' => '',
     822                                                'password' => '',
     823                                        ),
     824                                        'show_in_rest'      => array(
     825                                                'schema' => array(
     826                                                        'type'                 => 'object',
     827                                                        'properties'           => array(
     828                                                                'username' => array(
     829                                                                        'type' => 'string',
     830                                                                ),
     831                                                                'password' => array(
     832                                                                        'type' => 'string',
     833                                                                ),
     834                                                        ),
     835                                                        'additionalProperties' => false,
     836                                                ),
     837                                        ),
     838                                        'sanitize_callback' => static function ( $value ) use ( $setting_name ) {
     839                                                return wp_connectors_sanitize_application_password_credentials( $value, $setting_name );
     840                                        },
     841                                )
     842                        );
     843                }
    616844        }
    617845}
     
    699927                                $auth_out['isConnected'] = 'none' !== $key_source;
    700928                        }
     929                } elseif ( 'application_password' === $auth['method'] ) {
     930                        $credentials = wp_connectors_get_application_password_credentials( $auth );
     931
     932                        $auth_out['settingName']    = $auth['setting_name'] ?? '';
     933                        $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null;
     934                        $auth_out['keySource']      = $credentials['source'];
     935                        $auth_out['isConnected']    = '' !== $credentials['username'] && '' !== $credentials['password'];
    701936                }
    702937
  • trunk/tests/phpunit/tests/connectors/wpConnectorRegistry.php

    r62288 r62684  
    129129
    130130        /**
     131         * @ticket 64850
     132         */
     133        public function test_register_generates_credentials_setting_name() {
     134                $args                   = self::$default_args;
     135                $args['authentication'] = array(
     136                        'method'          => 'application_password',
     137                        'credentials_url' => 'https://example.com/profile.php',
     138                );
     139
     140                $result = $this->registry->register( 'remote-site', $args );
     141
     142                $this->assertSame( 'application_password', $result['authentication']['method'] );
     143                $this->assertSame( 'https://example.com/profile.php', $result['authentication']['credentials_url'] );
     144                $this->assertSame( 'connectors_test_type_remote_site_application_password', $result['authentication']['setting_name'] );
     145        }
     146
     147        /**
     148         * @ticket 64850
     149         */
     150        public function test_register_uses_custom_credentials_setting_name() {
     151                $args                   = self::$default_args;
     152                $args['authentication'] = array(
     153                        'method'       => 'application_password',
     154                        'setting_name' => 'remote_site_credentials',
     155                );
     156
     157                $result = $this->registry->register( 'remote-site', $args );
     158
     159                $this->assertSame( 'remote_site_credentials', $result['authentication']['setting_name'] );
     160        }
     161
     162        /**
     163         * @ticket 64850
     164         */
     165        public function test_register_accepts_application_password_constant_and_env_names() {
     166                $args                   = self::$default_args;
     167                $args['authentication'] = array(
     168                        'method'        => 'application_password',
     169                        'constant_name' => 'REMOTE_SITE_CREDENTIALS',
     170                        'env_var_name'  => 'REMOTE_SITE_CREDENTIALS',
     171                );
     172
     173                $result = $this->registry->register( 'remote-site', $args );
     174
     175                $this->assertSame( 'REMOTE_SITE_CREDENTIALS', $result['authentication']['constant_name'] );
     176                $this->assertSame( 'REMOTE_SITE_CREDENTIALS', $result['authentication']['env_var_name'] );
     177        }
     178
     179        /**
     180         * @ticket 64850
     181         *
     182         * @dataProvider data_application_password_external_name_keys
     183         *
     184         * @param string $name_key Authentication argument key.
     185         */
     186        public function test_register_rejects_empty_application_password_external_names( string $name_key ) {
     187                $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' );
     188
     189                $args                   = self::$default_args;
     190                $args['authentication'] = array(
     191                        'method'  => 'application_password',
     192                        $name_key => '',
     193                );
     194
     195                $result = $this->registry->register( 'remote-site', $args );
     196
     197                $this->assertNull( $result );
     198        }
     199
     200        /**
     201         * Data provider for application-password constant and environment variable name keys.
     202         *
     203         * @return array<string, array{string}> Test cases.
     204         */
     205        public function data_application_password_external_name_keys(): array {
     206                return array(
     207                        'constant name'             => array( 'constant_name' ),
     208                        'environment variable name' => array( 'env_var_name' ),
     209                );
     210        }
     211
     212        /**
    131213         * @ticket 64957
    132214         */
  • trunk/tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php

    r62310 r62684  
    6262                        $this->assertIsArray( $connector_data['authentication'], "Connector '{$connector_id}' authentication should be an array." );
    6363                        $this->assertArrayHasKey( 'method', $connector_data['authentication'], "Connector '{$connector_id}' authentication is missing 'method'." );
    64                         $this->assertContains( $connector_data['authentication']['method'], array( 'api_key', 'none' ), "Connector '{$connector_id}' has unexpected authentication method." );
     64                        $this->assertContains( $connector_data['authentication']['method'], array( 'api_key', 'application_password', 'none' ), "Connector '{$connector_id}' has unexpected authentication method." );
     65                }
     66        }
     67
     68        /**
     69         * @ticket 64850
     70         */
     71        public function test_application_password_connector_has_setting_name(): void {
     72                $connector_id = 'remote-site';
     73
     74                WP_Connector_Registry::get_instance()->register(
     75                        $connector_id,
     76                        array(
     77                                'name'           => 'Remote Site',
     78                                'description'    => 'Connects to a remote WordPress site.',
     79                                'type'           => 'content_source',
     80                                'authentication' => array(
     81                                        'method' => 'application_password',
     82                                ),
     83                        )
     84                );
     85
     86                try {
     87                        $connector = wp_get_connectors()[ $connector_id ];
     88
     89                        $this->assertSame( 'application_password', $connector['authentication']['method'] );
     90                        $this->assertSame( 'connectors_content_source_remote_site_application_password', $connector['authentication']['setting_name'] );
     91                } finally {
     92                        if ( wp_is_connector_registered( $connector_id ) ) {
     93                                WP_Connector_Registry::get_instance()->unregister( $connector_id );
     94                        }
    6595                }
    6696        }
  • trunk/tests/phpunit/tests/connectors/wpRegisterDefaultConnectorSettings.php

    r62289 r62684  
    99class Tests_Connectors_WpRegisterDefaultConnectorSettings extends WP_UnitTestCase {
    1010
    11         const CONNECTOR_ID = 'wp_test_non_ai_connector';
    12         const SETTING_NAME = 'connectors_test_non_ai_api_key';
     11        const CONNECTOR_ID             = 'wp_test_non_ai_connector';
     12        const SETTING_NAME             = 'connectors_test_non_ai_api_key';
     13        const CREDENTIALS_SETTING_NAME = 'connectors_test_non_ai_credentials';
    1314
    1415        /**
     
    2627
    2728                global $wp_registered_settings;
    28                 $this->original_registered_settings = $wp_registered_settings;
     29                $this->original_registered_settings = is_array( $wp_registered_settings ) ? $wp_registered_settings : array();
    2930        }
    3031
     
    99100                $this->assertArrayHasKey( self::SETTING_NAME, get_registered_settings() );
    100101        }
     102
     103        /**
     104         * @ticket 64850
     105         */
     106        public function test_application_password_connector_registers_credentials_setting(): void {
     107                WP_Connector_Registry::get_instance()->register(
     108                        self::CONNECTOR_ID,
     109                        array(
     110                                'name'           => 'Test Remote WordPress Connector',
     111                                'description'    => '',
     112                                'type'           => 'content_source',
     113                                'authentication' => array(
     114                                        'method'       => 'application_password',
     115                                        'setting_name' => self::CREDENTIALS_SETTING_NAME,
     116                                ),
     117                        )
     118                );
     119
     120                _wp_register_default_connector_settings();
     121
     122                $registered_settings = get_registered_settings();
     123                $this->assertArrayHasKey( self::CREDENTIALS_SETTING_NAME, $registered_settings );
     124                $this->assertSame( 'object', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['type'] );
     125                $this->assertSame( 'Test Remote WordPress Connector Credentials', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['label'] );
     126                $this->assertSame(
     127                        array(
     128                                'username' => '',
     129                                'password' => '',
     130                        ),
     131                        $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['default']
     132                );
     133                $this->assertSame( 'object', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['show_in_rest']['schema']['type'] );
     134                $this->assertArrayHasKey( 'username', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['show_in_rest']['schema']['properties'] );
     135                $this->assertArrayHasKey( 'password', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['show_in_rest']['schema']['properties'] );
     136        }
     137
     138        /**
     139         * @ticket 64850
     140         */
     141        public function test_application_password_connector_skips_already_registered_setting(): void {
     142                register_setting(
     143                        'connectors',
     144                        self::CREDENTIALS_SETTING_NAME,
     145                        array(
     146                                'type'              => 'object',
     147                                'label'             => 'Plugin-owned setting',
     148                                'description'       => 'Registered by the connector plugin.',
     149                                'default'           => array(
     150                                        'username' => 'plugin-default',
     151                                        'password' => '',
     152                                ),
     153                                'show_in_rest'      => false,
     154                                'sanitize_callback' => 'wp_connectors_sanitize_application_password_credentials',
     155                        )
     156                );
     157
     158                WP_Connector_Registry::get_instance()->register(
     159                        self::CONNECTOR_ID,
     160                        array(
     161                                'name'           => 'Test Remote WordPress Connector',
     162                                'description'    => '',
     163                                'type'           => 'content_source',
     164                                'authentication' => array(
     165                                        'method'       => 'application_password',
     166                                        'setting_name' => self::CREDENTIALS_SETTING_NAME,
     167                                ),
     168                        )
     169                );
     170
     171                _wp_register_default_connector_settings();
     172
     173                $registered_settings = get_registered_settings();
     174                $this->assertArrayHasKey( self::CREDENTIALS_SETTING_NAME, $registered_settings );
     175                $this->assertSame( 'Plugin-owned setting', $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['label'] );
     176                $this->assertSame(
     177                        array(
     178                                'username' => 'plugin-default',
     179                                'password' => '',
     180                        ),
     181                        $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['default']
     182                );
     183                        $this->assertFalse( $registered_settings[ self::CREDENTIALS_SETTING_NAME ]['show_in_rest'] );
     184        }
     185
     186        /**
     187         * @ticket 64850
     188         */
     189        public function test_already_registered_setting_skips_before_is_active_callback(): void {
     190                $is_active_called = false;
     191
     192                register_setting(
     193                        'connectors',
     194                        self::CREDENTIALS_SETTING_NAME,
     195                        array(
     196                                'type'         => 'object',
     197                                'show_in_rest' => false,
     198                        )
     199                );
     200
     201                WP_Connector_Registry::get_instance()->register(
     202                        self::CONNECTOR_ID,
     203                        array(
     204                                'name'           => 'Test Remote WordPress Connector',
     205                                'description'    => '',
     206                                'type'           => 'content_source',
     207                                'authentication' => array(
     208                                        'method'       => 'application_password',
     209                                        'setting_name' => self::CREDENTIALS_SETTING_NAME,
     210                                ),
     211                                'plugin'         => array(
     212                                        'is_active' => static function () use ( &$is_active_called ): bool {
     213                                                $is_active_called = true;
     214                                                return true;
     215                                        },
     216                                ),
     217                        )
     218                );
     219
     220                _wp_register_default_connector_settings();
     221
     222                $this->assertFalse( $is_active_called );
     223        }
    101224}
Note: See TracChangeset for help on using the changeset viewer.