From 8d5096a638865bb56d601c77cb00766ca0c6cecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 13 Mar 2019 18:34:10 +0100 Subject: [PATCH] [HttpClient] Allow to pass user/pw as an array --- .../Component/HttpClient/HttpClientTrait.php | 11 +++++- .../Component/HttpClient/HttpOptions.php | 7 +++- .../HttpClient/Tests/HttpClientTraitTest.php | 19 +++++++++- .../HttpClient/Tests/HttpOptionsTest.php | 37 +++++++++++++++++++ .../HttpClient/HttpClientInterface.php | 14 ++++--- 5 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 4804118449da3..d3ecafbd4ec8c 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -71,8 +71,17 @@ private static function prepareRequest(?string $method, ?string $url, array $opt throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, %s given.', \is_object($onProgress) ? \get_class($onProgress) : \gettype($onProgress))); } + if (\is_array($options['auth_basic'] ?? null)) { + $count = \count($options['auth_basic']); + if ($count <= 0 || $count > 2) { + throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, %s given.', $count)); + } + + $options['auth_basic'] = implode(':', $options['auth_basic']); + } + if (!\is_string($options['auth_basic'] ?? '')) { - throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string, %s given.', \gettype($options['auth_basic']))); + throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, %s given.', \gettype($options['auth_basic']))); } if (!\is_string($options['auth_bearer'] ?? '')) { diff --git a/src/Symfony/Component/HttpClient/HttpOptions.php b/src/Symfony/Component/HttpClient/HttpOptions.php index f723229a05ffc..dc4b82afdcdab 100644 --- a/src/Symfony/Component/HttpClient/HttpOptions.php +++ b/src/Symfony/Component/HttpClient/HttpOptions.php @@ -34,9 +34,12 @@ public function toArray(): array /** * @return $this */ - public function setAuthBasic(string $userinfo) + public function setAuthBasic(string $user, string $password = '') { - $this->options['auth'] = $userinfo; + $this->options['auth_basic'] = $user; + if ('' !== $password) { + $this->options['auth_basic'] .= ':'.$password; + } return $this; } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php index 022e1de229e15..90062278df0cc 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php @@ -185,8 +185,25 @@ public function testInvalidAuthBearerOption() * @expectedException \Symfony\Component\HttpClient\Exception\InvalidArgumentException * @expectedExceptionMessage Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported. */ - public function testSetBasicAndBearerOption() + public function testSetAuthBasicAndBearerOptions() { self::prepareRequest('POST', 'http://example.com', ['auth_bearer' => 'foo', 'auth_basic' => 'foo:bar'], HttpClientInterface::OPTIONS_DEFAULTS); } + + public function providePrepareAuthBasic() + { + yield ['foo:bar', 'Zm9vOmJhcg==']; + yield [['foo', 'bar'], 'Zm9vOmJhcg==']; + yield ['foo', 'Zm9v']; + yield [['foo'], 'Zm9v']; + } + + /** + * @dataProvider providePrepareAuthBasic + */ + public function testPrepareAuthBasic($arg, $result) + { + [, $options] = $this->prepareRequest('POST', 'http://example.com', ['auth_basic' => $arg], HttpClientInterface::OPTIONS_DEFAULTS); + $this->assertSame('Basic '.$result, $options['headers']['authorization'][0]); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php new file mode 100644 index 0000000000000..d319bf1680419 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/HttpOptionsTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\HttpOptions; + +/** + * @author Kévin Dunglas + */ +class HttpOptionsTest extends TestCase +{ + public function provideSetAuth() + { + yield ['user:password', 'user', 'password']; + yield ['user:password', 'user:password']; + yield ['user', 'user']; + yield ['user:0', 'user', '0']; + } + + /** + * @dataProvider provideSetAuth + */ + public function testSetAuth(string $expected, string $user, string $password = '') + { + $this->assertSame($expected, (new HttpOptions())->setAuthBasic($user, $password)->toArray()['auth_basic']); + } +} diff --git a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php index 2609ba4bbf48c..6abc459fee787 100644 --- a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php +++ b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php @@ -26,7 +26,9 @@ interface HttpClientInterface { public const OPTIONS_DEFAULTS = [ - 'auth_basic' => null, // string - a username:password enabling HTTP Basic authentication (RFC 7617) + 'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the + // password as the second one; or string like username:password - enabling HTTP Basic + // authentication (RFC 7617) 'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750) 'query' => [], // string[] - associative array of query string values to merge with the request's URL 'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values @@ -37,16 +39,16 @@ interface HttpClientInterface // the JSON-encoded value and set the "content-type" headers to a JSON-compatible // value it is they are not defined - typically "application/json" 'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that - // MUST be available via $response->getInfo('data') - not used internally + // MUST be available via $response->getInfo('data') - not used internally 'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower or equal to 0 means - // redirects should not be followed; "Authorization" and "Cookie" headers MUST - // NOT follow except for the initial host name + // redirects should not be followed; "Authorization" and "Cookie" headers MUST + // NOT follow except for the initial host name 'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0 'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2 'buffer' => true, // bool - whether the content of the response should be buffered or not 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort - // the request; it MUST be called on DNS resolution, on arrival of headers and on - // completion; it SHOULD be called on upload/download of data and at least 1/s + // the request; it MUST be called on DNS resolution, on arrival of headers and on + // completion; it SHOULD be called on upload/download of data and at least 1/s 'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution 'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored 'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached