Skip to content
59 changes: 51 additions & 8 deletions inc/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,8 @@ public static function load_remote_commands() {
if ( ! $api_index ) {
WP_CLI::error( "Couldn't find index data from {$api_url}." );
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
$bits = parse_url( $http );
$auth = array();
if ( ! empty( $bits['user'] ) ) {
$auth['type'] = 'basic';
$auth['username'] = $bits['user'];
$auth['password'] = ! empty( $bits['pass'] ) ? $bits['pass'] : '';
}
$auth = self::resolve_auth( $http, WP_CLI::get_runner()->config );

foreach ( $api_index['routes'] as $route => $route_data ) {
if ( empty( $route_data['schema']['title'] ) ) {
WP_CLI::debug( "No schema title found for {$route}, skipping REST command registration.", 'rest' );
Expand Down Expand Up @@ -129,6 +123,55 @@ private static function get_api_index( $api_url ) {
return json_decode( $response->body, true );
}

/**
* Resolve HTTP Basic Auth credentials from the available sources.
*
* Priority (highest wins):
* 1. Credentials embedded in the URL (user:pass@host).
* 2. WP_REST_CLI_AUTH_USER / WP_REST_CLI_AUTH_PASSWORD environment variables.
* 3. http_user / http_password keys in the WP-CLI config.
*
* @param string $http The URL passed to --http.
* @param array $config WP-CLI config array (e.g. WP_CLI::get_runner()->config).
* @return array Auth array with 'type', 'username', 'password' keys, or empty array.
*/
public static function resolve_auth( $http, array $config = array() ) {
$auth = array();

// Lowest priority: wp-cli config (http_user / http_password).
if ( ! empty( $config['http_user'] ) ) {
$auth['type'] = 'basic';
$auth['username'] = $config['http_user'];
$auth['password'] = ! empty( $config['http_password'] ) ? $config['http_password'] : '';
}

// Medium priority: environment variables.
// An empty username is not valid for authentication, so we skip if it is empty.
// An empty password is allowed (e.g. passwordless setups), consistent with URL embedding.
$env_user = getenv( 'WP_REST_CLI_AUTH_USER' );
$env_password = getenv( 'WP_REST_CLI_AUTH_PASSWORD' );
if ( false !== $env_user && '' !== $env_user ) {
$auth['type'] = 'basic';
$auth['username'] = $env_user;
$auth['password'] = ( false !== $env_password ) ? $env_password : '';
}

// Highest priority: credentials embedded in the URL.
// Ensure the URL has a scheme so parse_url() can extract user:pass correctly.
if ( false === stripos( $http, 'http://' ) && false === stripos( $http, 'https://' ) ) {
$http = 'http://' . $http;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
$bits = parse_url( $http );
if ( ! empty( $bits['user'] ) ) {
$auth['type'] = 'basic';
$auth['username'] = $bits['user'];
$auth['password'] = ! empty( $bits['pass'] ) ? $bits['pass'] : '';
}

return $auth;
Comment thread
swissspidy marked this conversation as resolved.
Outdated
}

/**
* Register WP-CLI commands for all endpoints on a route
*
Expand Down
160 changes: 160 additions & 0 deletions tests/Runner_Resolve_Auth_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

use WP_CLI\Tests\TestCase;

class Runner_Resolve_Auth_Test extends TestCase {

private $saved_env = array();

public function set_up() {
foreach ( array( 'WP_REST_CLI_AUTH_USER', 'WP_REST_CLI_AUTH_PASSWORD' ) as $var ) {
$val = getenv( $var );
$this->saved_env[ $var ] = false === $val ? false : $val;
putenv( $var );
}
}

public function tear_down() {
foreach ( $this->saved_env as $var => $val ) {
if ( false === $val ) {
putenv( $var );
} else {
putenv( "{$var}={$val}" );
}
}
}

public function test_no_auth_when_nothing_set() {
$auth = \WP_REST_CLI\Runner::resolve_auth( 'example.com' );
$this->assertSame( array(), $auth );
}

public function test_auth_from_config() {
$auth = \WP_REST_CLI\Runner::resolve_auth(
'example.com',
array(
'http_user' => 'admin',
'http_password' => 'secret',
)
);
$this->assertSame(
array(
'type' => 'basic',
'username' => 'admin',
'password' => 'secret',
),
$auth
);
}

public function test_config_allows_empty_password() {
$auth = \WP_REST_CLI\Runner::resolve_auth(
'example.com',
array( 'http_user' => 'admin' )
);
$this->assertSame(
array(
'type' => 'basic',
'username' => 'admin',
'password' => '',
),
$auth
);
}

public function test_env_vars_override_config() {
putenv( 'WP_REST_CLI_AUTH_USER=envuser' );
putenv( 'WP_REST_CLI_AUTH_PASSWORD=envpass' );
$auth = \WP_REST_CLI\Runner::resolve_auth(
'example.com',
array(
'http_user' => 'cfguser',
'http_password' => 'cfgpass',
)
);
$this->assertSame(
array(
'type' => 'basic',
'username' => 'envuser',
'password' => 'envpass',
),
$auth
);
}

public function test_env_user_without_env_password_uses_empty_password() {
putenv( 'WP_REST_CLI_AUTH_USER=envuser' );
$auth = \WP_REST_CLI\Runner::resolve_auth( 'example.com' );
$this->assertSame(
array(
'type' => 'basic',
'username' => 'envuser',
'password' => '',
),
$auth
);
}

public function test_empty_env_user_skips_env_auth() {
putenv( 'WP_REST_CLI_AUTH_USER=' );
putenv( 'WP_REST_CLI_AUTH_PASSWORD=envpass' );
$auth = \WP_REST_CLI\Runner::resolve_auth( 'example.com' );
$this->assertSame( array(), $auth );
}

public function test_url_credentials_override_env_vars() {
putenv( 'WP_REST_CLI_AUTH_USER=envuser' );
putenv( 'WP_REST_CLI_AUTH_PASSWORD=envpass' );
$auth = \WP_REST_CLI\Runner::resolve_auth( 'http://urluser:urlpass@example.com' );
$this->assertSame(
array(
'type' => 'basic',
'username' => 'urluser',
'password' => 'urlpass',
),
$auth
);
}

public function test_url_credentials_without_scheme() {
$auth = \WP_REST_CLI\Runner::resolve_auth( 'urluser:urlpass@example.com' );
$this->assertSame(
array(
'type' => 'basic',
'username' => 'urluser',
'password' => 'urlpass',
),
$auth
);
}

public function test_url_credentials_with_https_scheme() {
$auth = \WP_REST_CLI\Runner::resolve_auth( 'https://urluser:urlpass@example.com' );
$this->assertSame(
array(
'type' => 'basic',
'username' => 'urluser',
'password' => 'urlpass',
),
$auth
);
}
Comment thread
swissspidy marked this conversation as resolved.
Outdated

public function test_url_credentials_override_config() {
$auth = \WP_REST_CLI\Runner::resolve_auth(
'http://urluser:urlpass@example.com',
array(
'http_user' => 'cfguser',
'http_password' => 'cfgpass',
)
);
$this->assertSame(
array(
'type' => 'basic',
'username' => 'urluser',
'password' => 'urlpass',
),
$auth
);
}
}
Loading