Merge branch '4.4'

* 4.4:
  [Security] Revise UserPasswordEncoderInterface::needsRehash()
  [Form] update type of form $name arguments
  [HttpClient] Preserve the case of headers when sending them
  [Ldap][Security] use right arguments count in sercurity factories
This commit is contained in:
Nicolas Grekas 2019-07-31 17:14:38 +02:00
commit f1368b4e29
21 changed files with 120 additions and 119 deletions

View File

@ -35,8 +35,8 @@ class FormLoginLdapFactory extends FormLoginFactory
->replaceArgument(2, $id) ->replaceArgument(2, $id)
->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(3, new Reference($config['service']))
->replaceArgument(4, $config['dn_string']) ->replaceArgument(4, $config['dn_string'])
->replaceArgument(5, $config['search_dn']) ->replaceArgument(6, $config['search_dn'])
->replaceArgument(6, $config['search_password']) ->replaceArgument(7, $config['search_password'])
; ;
if (!empty($config['query_string'])) { if (!empty($config['query_string'])) {

View File

@ -36,8 +36,8 @@ class HttpBasicLdapFactory extends HttpBasicFactory
->replaceArgument(2, $id) ->replaceArgument(2, $id)
->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(3, new Reference($config['service']))
->replaceArgument(4, $config['dn_string']) ->replaceArgument(4, $config['dn_string'])
->replaceArgument(5, $config['search_dn']) ->replaceArgument(6, $config['search_dn'])
->replaceArgument(6, $config['search_password']) ->replaceArgument(7, $config['search_password'])
; ;
// entry point // entry point

View File

@ -37,8 +37,8 @@ class JsonLoginLdapFactory extends JsonLoginFactory
->replaceArgument(2, $id) ->replaceArgument(2, $id)
->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(3, new Reference($config['service']))
->replaceArgument(4, $config['dn_string']) ->replaceArgument(4, $config['dn_string'])
->replaceArgument(5, $config['search_dn']) ->replaceArgument(6, $config['search_dn'])
->replaceArgument(6, $config['search_password']) ->replaceArgument(7, $config['search_password'])
; ;
if (!empty($config['query_string'])) { if (!empty($config['query_string'])) {

View File

@ -69,9 +69,8 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
* *
* This method should not be invoked. * This method should not be invoked.
* *
* @param string|int|FormBuilderInterface $child * @param string|FormBuilderInterface $child
* @param string|FormTypeInterface $type * @param string|FormTypeInterface $type
* @param array $options
* *
* @throws BadMethodCallException * @throws BadMethodCallException
*/ */

View File

@ -843,7 +843,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
if (!$child instanceof FormInterface) { if (!$child instanceof FormInterface) {
if (!\is_string($child) && !\is_int($child)) { if (!\is_string($child) && !\is_int($child)) {
throw new UnexpectedTypeException($child, 'string, integer or Symfony\Component\Form\FormInterface'); throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormInterface');
} }
$child = (string) $child; $child = (string) $child;

View File

@ -63,7 +63,7 @@ class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormB
} }
if (!\is_string($child) && !\is_int($child)) { if (!\is_string($child) && !\is_int($child)) {
throw new UnexpectedTypeException($child, 'string, integer or Symfony\Component\Form\FormBuilderInterface'); throw new UnexpectedTypeException($child, 'string or Symfony\Component\Form\FormBuilderInterface');
} }
if (null !== $type && !\is_string($type) && !$type instanceof FormTypeInterface) { if (null !== $type && !\is_string($type) && !$type instanceof FormTypeInterface) {

View File

@ -23,9 +23,8 @@ interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuild
* If you add a nested group, this group should also be represented in the * If you add a nested group, this group should also be represented in the
* object hierarchy. * object hierarchy.
* *
* @param string|int|FormBuilderInterface $child * @param string|FormBuilderInterface $child
* @param string|null $type * @param string|null $type
* @param array $options
* *
* @return self * @return self
*/ */
@ -34,9 +33,8 @@ interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuild
/** /**
* Creates a form builder. * Creates a form builder.
* *
* @param string $name The name of the form or the name of the property * @param string $name The name of the form or the name of the property
* @param string|null $type The type of the form or null if name is a property * @param string|null $type The type of the form or null if name is a property
* @param array $options The options
* *
* @return self * @return self
*/ */

View File

@ -106,10 +106,8 @@ class FormConfigBuilder implements FormConfigBuilderInterface
/** /**
* Creates an empty form configuration. * Creates an empty form configuration.
* *
* @param string|null $name The form name * @param string|null $name The form name
* @param string|null $dataClass The class of the form's data * @param string|null $dataClass The class of the form's data
* @param EventDispatcherInterface $dispatcher The event dispatcher
* @param array $options The form options
* *
* @throws InvalidArgumentException if the data class is not a valid class or if * @throws InvalidArgumentException if the data class is not a valid class or if
* the name contains invalid characters * the name contains invalid characters

View File

@ -38,10 +38,9 @@ interface FormFactoryInterface
* *
* @see createNamedBuilder() * @see createNamedBuilder()
* *
* @param string|int $name The name of the form * @param string $name The name of the form
* @param string $type The type of the form * @param string $type The type of the form
* @param mixed $data The initial data * @param mixed $data The initial data
* @param array $options The options
* *
* @return FormInterface The form * @return FormInterface The form
* *
@ -81,10 +80,9 @@ interface FormFactoryInterface
/** /**
* Returns a form builder. * Returns a form builder.
* *
* @param string|int $name The name of the form * @param string $name The name of the form
* @param string $type The type of the form * @param string $type The type of the form
* @param mixed $data The initial data * @param mixed $data The initial data
* @param array $options The options
* *
* @return FormBuilderInterface The form builder * @return FormBuilderInterface The form builder
* *

View File

@ -43,9 +43,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/** /**
* Adds or replaces a child to the form. * Adds or replaces a child to the form.
* *
* @param FormInterface|string|int $child The FormInterface instance or the name of the child * @param FormInterface|string $child The FormInterface instance or the name of the child
* @param string|null $type The child's type, if a name was passed * @param string|null $type The child's type, if a name was passed
* @param array $options The child's options, if a name was passed * @param array $options The child's options, if a name was passed
* *
* @return $this * @return $this
* *

View File

@ -109,12 +109,14 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) { if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
unset($this->multi->pushedResponses[$url]); unset($this->multi->pushedResponses[$url]);
// Accept pushed responses only if their headers related to authentication match the request // Accept pushed responses only if their headers related to authentication match the request
$expectedHeaders = [ $expectedHeaders = ['authorization', 'cookie', 'x-requested-with', 'range'];
$options['headers']['authorization'] ?? null, foreach ($expectedHeaders as $k => $v) {
$options['headers']['cookie'] ?? null, $expectedHeaders[$k] = null;
$options['headers']['x-requested-with'] ?? null,
$options['headers']['range'] ?? null, foreach ($options['normalized_headers'][$v] ?? [] as $h) {
]; $expectedHeaders[$k][] = substr($h, 2 + \strlen($v));
}
}
if ('GET' === $method && $expectedHeaders === $pushedResponse->headers && !$options['body']) { if ('GET' === $method && $expectedHeaders === $pushedResponse->headers && !$options['body']) {
$this->logger && $this->logger->debug(sprintf('Connecting request to pushed response: "%s %s"', $method, $url)); $this->logger && $this->logger->debug(sprintf('Connecting request to pushed response: "%s %s"', $method, $url));
@ -226,11 +228,11 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$curlopts[CURLOPT_NOSIGNAL] = true; $curlopts[CURLOPT_NOSIGNAL] = true;
} }
if (!isset($options['headers']['accept-encoding'])) { if (!isset($options['normalized_headers']['accept-encoding'])) {
$curlopts[CURLOPT_ENCODING] = ''; // Enable HTTP compression $curlopts[CURLOPT_ENCODING] = ''; // Enable HTTP compression
} }
foreach ($options['request_headers'] as $header) { foreach ($options['headers'] as $header) {
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) { if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
// curl requires a special syntax to send empty headers // curl requires a special syntax to send empty headers
$curlopts[CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2); $curlopts[CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
@ -241,7 +243,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
// Prevent curl from sending its default Accept and Expect headers // Prevent curl from sending its default Accept and Expect headers
foreach (['accept', 'expect'] as $header) { foreach (['accept', 'expect'] as $header) {
if (!isset($options['headers'][$header])) { if (!isset($options['normalized_headers'][$header])) {
$curlopts[CURLOPT_HTTPHEADER][] = $header.':'; $curlopts[CURLOPT_HTTPHEADER][] = $header.':';
} }
} }
@ -257,9 +259,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
}; };
} }
if (isset($options['headers']['content-length'][0])) { if (isset($options['normalized_headers']['content-length'][0])) {
$curlopts[CURLOPT_INFILESIZE] = $options['headers']['content-length'][0]; $curlopts[CURLOPT_INFILESIZE] = substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
} elseif (!isset($options['headers']['transfer-encoding'])) { } elseif (!isset($options['normalized_headers']['transfer-encoding'])) {
$curlopts[CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies $curlopts[CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies
} }
@ -407,12 +409,12 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
$redirectHeaders = []; $redirectHeaders = [];
if (0 < $options['max_redirects']) { if (0 < $options['max_redirects']) {
$redirectHeaders['host'] = $host; $redirectHeaders['host'] = $host;
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) { $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:'); return 0 !== stripos($h, 'Host:');
}); });
if (isset($options['headers']['authorization']) || isset($options['headers']['cookie'])) { if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) { $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
}); });
} }

View File

@ -48,7 +48,10 @@ trait HttpClientTrait
} }
$options['body'] = self::jsonEncode($options['json']); $options['body'] = self::jsonEncode($options['json']);
unset($options['json']); unset($options['json']);
$options['headers']['content-type'] = $options['headers']['content-type'] ?? ['application/json'];
if (!isset($options['normalized_headers']['content-type'])) {
$options['normalized_headers']['content-type'] = [$options['headers'][] = 'Content-Type: application/json'];
}
} }
if (isset($options['body'])) { if (isset($options['body'])) {
@ -59,19 +62,6 @@ trait HttpClientTrait
$options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']); $options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
} }
// Compute request headers
$requestHeaders = $headers = [];
foreach ($options['headers'] as $name => $values) {
foreach ($values as $value) {
$requestHeaders[] = $name.': '.$headers[$name][] = $value = (string) $value;
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
throw new InvalidArgumentException(sprintf('Invalid header value: CR/LF/NUL found in "%s".', $value));
}
}
}
// Validate on_progress // Validate on_progress
if (!\is_callable($onProgress = $options['on_progress'] ?? 'var_dump')) { if (!\is_callable($onProgress = $options['on_progress'] ?? 'var_dump')) {
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, %s given.', \is_object($onProgress) ? \get_class($onProgress) : \gettype($onProgress))); throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, %s given.', \is_object($onProgress) ? \get_class($onProgress) : \gettype($onProgress)));
@ -100,15 +90,14 @@ trait HttpClientTrait
if (null !== $url) { if (null !== $url) {
// Merge auth with headers // Merge auth with headers
if (($options['auth_basic'] ?? false) && !($headers['authorization'] ?? false)) { if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$requestHeaders[] = 'authorization: '.$headers['authorization'][] = 'Basic '.base64_encode($options['auth_basic']); $options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Basic '.base64_encode($options['auth_basic'])];
} }
// Merge bearer with headers // Merge bearer with headers
if (($options['auth_bearer'] ?? false) && !($headers['authorization'] ?? false)) { if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$requestHeaders[] = 'authorization: '.$headers['authorization'][] = 'Bearer '.$options['auth_bearer']; $options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Bearer '.$options['auth_bearer']];
} }
$options['request_headers'] = $requestHeaders;
unset($options['auth_basic'], $options['auth_bearer']); unset($options['auth_basic'], $options['auth_bearer']);
// Parse base URI // Parse base URI
@ -122,7 +111,6 @@ trait HttpClientTrait
} }
// Finalize normalization of options // Finalize normalization of options
$options['headers'] = $headers;
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null; $options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout')); $options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
@ -134,31 +122,38 @@ trait HttpClientTrait
*/ */
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
{ {
unset($options['request_headers'], $defaultOptions['request_headers']); $options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
$options['headers'] = self::normalizeHeaders($options['headers'] ?? []);
if ($defaultOptions['headers'] ?? false) { if ($defaultOptions['headers'] ?? false) {
$options['headers'] += self::normalizeHeaders($defaultOptions['headers']); $options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
} }
if ($options['resolve'] ?? false) { $options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
$options['resolve'] = array_change_key_case($options['resolve']);
if ($resolve = $options['resolve'] ?? false) {
$options['resolve'] = [];
foreach ($resolve as $k => $v) {
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
}
} }
// Option "query" is never inherited from defaults // Option "query" is never inherited from defaults
$options['query'] = $options['query'] ?? []; $options['query'] = $options['query'] ?? [];
foreach ($defaultOptions as $k => $v) { foreach ($defaultOptions as $k => $v) {
$options[$k] = $options[$k] ?? $v; if ('normalized_headers' !== $k && !isset($options[$k])) {
$options[$k] = $v;
}
} }
if (isset($defaultOptions['extra'])) { if (isset($defaultOptions['extra'])) {
$options['extra'] += $defaultOptions['extra']; $options['extra'] += $defaultOptions['extra'];
} }
if ($defaultOptions['resolve'] ?? false) { if ($resolve = $defaultOptions['resolve'] ?? false) {
$options['resolve'] += array_change_key_case($defaultOptions['resolve']); foreach ($resolve as $k => $v) {
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
}
} }
if ($allowExtraOptions || !$defaultOptions) { if ($allowExtraOptions || !$defaultOptions) {
@ -167,7 +162,7 @@ trait HttpClientTrait
// Look for unsupported options // Look for unsupported options
foreach ($options as $name => $v) { foreach ($options as $name => $v) {
if (\array_key_exists($name, $defaultOptions)) { if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
continue; continue;
} }
@ -190,9 +185,9 @@ trait HttpClientTrait
} }
/** /**
* Normalizes headers by putting their names as lowercased keys.
*
* @return string[][] * @return string[][]
*
* @throws InvalidArgumentException When an invalid header is found
*/ */
private static function normalizeHeaders(array $headers): array private static function normalizeHeaders(array $headers): array
{ {
@ -206,10 +201,15 @@ trait HttpClientTrait
$values = (array) $values; $values = (array) $values;
} }
$normalizedHeaders[$name = strtolower($name)] = []; $lcName = strtolower($name);
$normalizedHeaders[$lcName] = [];
foreach ($values as $value) { foreach ($values as $value) {
$normalizedHeaders[$name][] = $value; $normalizedHeaders[$lcName][] = $value = $name.': '.$value;
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
}
} }
} }

View File

@ -71,13 +71,13 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$options['body'] = self::getBodyAsString($options['body']); $options['body'] = self::getBodyAsString($options['body']);
if ('' !== $options['body'] && 'POST' === $method && !isset($options['headers']['content-type'])) { if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
$options['request_headers'][] = 'content-type: application/x-www-form-urlencoded'; $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
} }
if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['headers']['accept-encoding'])) { if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
// gzip is the most widely available algo, no need to deal with deflate // gzip is the most widely available algo, no need to deal with deflate
$options['request_headers'][] = 'accept-encoding: gzip'; $options['headers'][] = 'Accept-Encoding: gzip';
} }
if ($options['peer_fingerprint']) { if ($options['peer_fingerprint']) {
@ -158,12 +158,12 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
[$host, $port, $url['authority']] = self::dnsResolve($url, $this->multi, $info, $onProgress); [$host, $port, $url['authority']] = self::dnsResolve($url, $this->multi, $info, $onProgress);
if (!isset($options['headers']['host'])) { if (!isset($options['normalized_headers']['host'])) {
$options['request_headers'][] = 'host: '.$host.$port; $options['headers'][] = 'Host: '.$host.$port;
} }
if (!isset($options['headers']['user-agent'])) { if (!isset($options['normalized_headers']['user-agent'])) {
$options['request_headers'][] = 'user-agent: Symfony HttpClient/Native'; $options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
} }
$context = [ $context = [
@ -206,7 +206,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$resolveRedirect = self::createRedirectResolver($options, $host, $proxy, $noProxy, $info, $onProgress); $resolveRedirect = self::createRedirectResolver($options, $host, $proxy, $noProxy, $info, $onProgress);
$context = stream_context_create($context, ['notification' => $notification]); $context = stream_context_create($context, ['notification' => $notification]);
self::configureHeadersAndProxy($context, $host, $options['request_headers'], $proxy, $noProxy); self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, $noProxy);
return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger); return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger);
} }
@ -333,12 +333,12 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$redirectHeaders = []; $redirectHeaders = [];
if (0 < $maxRedirects = $options['max_redirects']) { if (0 < $maxRedirects = $options['max_redirects']) {
$redirectHeaders = ['host' => $host]; $redirectHeaders = ['host' => $host];
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) { $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:'); return 0 !== stripos($h, 'Host:');
}); });
if (isset($options['headers']['authorization']) || isset($options['headers']['cookie'])) { if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($options['request_headers'], static function ($h) { $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
}); });
} }
@ -391,7 +391,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
if (false !== (parse_url($location, PHP_URL_HOST) ?? false)) { if (false !== (parse_url($location, PHP_URL_HOST) ?? false)) {
// Authorization and Cookie headers MUST NOT follow except for the initial host name // Authorization and Cookie headers MUST NOT follow except for the initial host name
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth']; $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
$requestHeaders[] = 'host: '.$host.$port; $requestHeaders[] = 'Host: '.$host.$port;
self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, $noProxy); self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, $noProxy);
} }

View File

@ -241,6 +241,7 @@ final class NativeResponse implements ResponseInterface
try { try {
// Notify the progress callback so that it can e.g. cancel // Notify the progress callback so that it can e.g. cancel
// the request if the stream is inactive for too long // the request if the stream is inactive for too long
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress(); $onProgress();
} catch (\Throwable $e) { } catch (\Throwable $e) {
// no-op // no-op

View File

@ -172,8 +172,8 @@ class HttpClientTraitTest extends TestCase
public function testAuthBearerOption() public function testAuthBearerOption()
{ {
[, $options] = self::prepareRequest('POST', 'http://example.com', ['auth_bearer' => 'foobar'], HttpClientInterface::OPTIONS_DEFAULTS); [, $options] = self::prepareRequest('POST', 'http://example.com', ['auth_bearer' => 'foobar'], HttpClientInterface::OPTIONS_DEFAULTS);
$this->assertSame('Bearer foobar', $options['headers']['authorization'][0]); $this->assertSame(['Authorization: Bearer foobar'], $options['headers']);
$this->assertSame('authorization: Bearer foobar', $options['request_headers'][0]); $this->assertSame(['Authorization: Bearer foobar'], $options['normalized_headers']['authorization']);
} }
/** /**
@ -226,7 +226,7 @@ class HttpClientTraitTest extends TestCase
public function testPrepareAuthBasic($arg, $result) public function testPrepareAuthBasic($arg, $result)
{ {
[, $options] = $this->prepareRequest('POST', 'http://example.com', ['auth_basic' => $arg], HttpClientInterface::OPTIONS_DEFAULTS); [, $options] = $this->prepareRequest('POST', 'http://example.com', ['auth_basic' => $arg], HttpClientInterface::OPTIONS_DEFAULTS);
$this->assertSame('Basic '.$result, $options['headers']['authorization'][0]); $this->assertSame('Authorization: Basic '.$result, $options['normalized_headers']['authorization'][0]);
} }
public function provideFingerprints() public function provideFingerprints()

View File

@ -44,9 +44,9 @@ class ScopingHttpClientTest extends TestCase
$client = new ScopingHttpClient($mockClient, $options); $client = new ScopingHttpClient($mockClient, $options);
$response = $client->request('GET', $url); $response = $client->request('GET', $url);
$reuestedOptions = $response->getRequestOptions(); $requestedOptions = $response->getRequestOptions();
$this->assertEquals($reuestedOptions['case'], $options[$regexp]['case']); $this->assertSame($options[$regexp]['case'], $requestedOptions['case']);
} }
public function provideMatchingUrls() public function provideMatchingUrls()
@ -64,8 +64,8 @@ class ScopingHttpClientTest extends TestCase
public function testMatchingUrlsAndOptions() public function testMatchingUrlsAndOptions()
{ {
$defaultOptions = [ $defaultOptions = [
'.*/foo-bar' => ['headers' => ['x-app' => 'unit-test-foo-bar']], '.*/foo-bar' => ['headers' => ['X-FooBar' => 'unit-test-foo-bar']],
'.*' => ['headers' => ['content-type' => 'text/html']], '.*' => ['headers' => ['Content-Type' => 'text/html']],
]; ];
$mockClient = new MockHttpClient(); $mockClient = new MockHttpClient();
@ -73,20 +73,20 @@ class ScopingHttpClientTest extends TestCase
$response = $client->request('GET', 'http://example.com/foo-bar', ['json' => ['url' => 'http://example.com']]); $response = $client->request('GET', 'http://example.com/foo-bar', ['json' => ['url' => 'http://example.com']]);
$requestOptions = $response->getRequestOptions(); $requestOptions = $response->getRequestOptions();
$this->assertEquals($requestOptions['headers']['content-type'][0], 'application/json'); $this->assertSame('Content-Type: application/json', $requestOptions['headers'][1]);
$requestJson = json_decode($requestOptions['body'], true); $requestJson = json_decode($requestOptions['body'], true);
$this->assertEquals($requestJson['url'], 'http://example.com'); $this->assertSame('http://example.com', $requestJson['url']);
$this->assertEquals($requestOptions['headers']['x-app'][0], $defaultOptions['.*/foo-bar']['headers']['x-app']); $this->assertSame('X-FooBar: '.$defaultOptions['.*/foo-bar']['headers']['X-FooBar'], $requestOptions['headers'][0]);
$response = $client->request('GET', 'http://example.com/bar-foo', ['headers' => ['x-app' => 'unit-test']]); $response = $client->request('GET', 'http://example.com/bar-foo', ['headers' => ['X-FooBar' => 'unit-test']]);
$requestOptions = $response->getRequestOptions(); $requestOptions = $response->getRequestOptions();
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test'); $this->assertSame('X-FooBar: unit-test', $requestOptions['headers'][0]);
$this->assertEquals($requestOptions['headers']['content-type'][0], 'text/html'); $this->assertSame('Content-Type: text/html', $requestOptions['headers'][1]);
$response = $client->request('GET', 'http://example.com/foobar-foo', ['headers' => ['x-app' => 'unit-test']]); $response = $client->request('GET', 'http://example.com/foobar-foo', ['headers' => ['X-FooBar' => 'unit-test']]);
$requestOptions = $response->getRequestOptions(); $requestOptions = $response->getRequestOptions();
$this->assertEquals($requestOptions['headers']['x-app'][0], 'unit-test'); $this->assertSame('X-FooBar: unit-test', $requestOptions['headers'][0]);
$this->assertEquals($requestOptions['headers']['content-type'][0], 'text/html'); $this->assertSame('Content-Type: text/html', $requestOptions['headers'][1]);
} }
public function testForBaseUri() public function testForBaseUri()

View File

@ -50,10 +50,10 @@ class UserPasswordEncoder implements UserPasswordEncoderInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function needsRehash(UserInterface $user, string $encoded): bool public function needsRehash(UserInterface $user): bool
{ {
$encoder = $this->encoderFactory->getEncoder($user); $encoder = $this->encoderFactory->getEncoder($user);
return $encoder->needsRehash($encoded); return $encoder->needsRehash($user->getPassword());
} }
} }

View File

@ -35,5 +35,5 @@ interface UserPasswordEncoderInterface
/** /**
* Checks if an encoded password would benefit from rehashing. * Checks if an encoded password would benefit from rehashing.
*/ */
public function needsRehash(UserInterface $user, string $encoded): bool; public function needsRehash(UserInterface $user): bool;
} }

View File

@ -85,9 +85,9 @@ class UserPasswordEncoderTest extends TestCase
$passwordEncoder = new UserPasswordEncoder($mockEncoderFactory); $passwordEncoder = new UserPasswordEncoder($mockEncoderFactory);
$hash = $passwordEncoder->encodePassword($user, 'foo', 'salt'); $user->setPassword($passwordEncoder->encodePassword($user, 'foo', 'salt'));
$this->assertFalse($passwordEncoder->needsRehash($user, $hash)); $this->assertFalse($passwordEncoder->needsRehash($user));
$this->assertTrue($passwordEncoder->needsRehash($user, $hash)); $this->assertTrue($passwordEncoder->needsRehash($user));
$this->assertFalse($passwordEncoder->needsRehash($user, $hash)); $this->assertFalse($passwordEncoder->needsRehash($user));
} }
} }

View File

@ -165,4 +165,9 @@ final class User implements UserInterface, EquatableInterface
return true; return true;
} }
public function setPassword(string $password)
{
$this->password = $password;
}
} }

View File

@ -40,8 +40,8 @@ interface HttpClientInterface
// value if they are not defined - typically "application/json" // value if they are not defined - typically "application/json"
'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that 'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that
// MUST be available via $response->getInfo('user_data') - not used internally // MUST be available via $response->getInfo('user_data') - not used internally
'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower or equal to 0 means 'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0
// redirects should not be followed; "Authorization" and "Cookie" headers MUST // means redirects should not be followed; "Authorization" and "Cookie" headers MUST
// NOT follow except for the initial host name // NOT follow except for the initial host name
'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0 '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 'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2