Merge branch '5.1'

* 5.1:
  minor #37121 [Contracts] Add missing "extra.thanks" entries in composer.json (nicolas-grekas)
  [Process] Fix Permission Denied error when writing sf_proc_00 lock files on Windows
  fix handling null as empty data
  [Security\Http] Skip remember-me logout on empty token
  Missing return in loadValuesForChoices method
  No need to create an issue when creating a PR
  Use ">=" for the "php" requirement
  [HttpClient] Fix promise behavior in HttplugClient
  [Console] Fixes question input encoding on Windows
This commit is contained in:
Nicolas Grekas 2020-07-06 15:25:45 +02:00
commit c046229c9e
12 changed files with 229 additions and 7 deletions

View File

@ -4,7 +4,7 @@
| Bug fix? | yes/no
| New feature? | yes/no <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | yes/no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets | Fix #... <!-- prefix each issue number with "Fix #", if any -->
| Tickets | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License | MIT
| Doc PR | symfony/symfony-docs#... <!-- required for new features -->
<!--

View File

@ -110,6 +110,11 @@ class QuestionHelper extends Helper
$inputStream = $this->inputStream ?: STDIN;
$autocomplete = $question->getAutocompleterCallback();
if (\function_exists('sapi_windows_cp_set')) {
// Codepage used by cmd.exe on Windows to allow special characters (éàüñ).
sapi_windows_cp_set(1252);
}
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
$ret = false;
if ($question->isHidden()) {

View File

@ -46,6 +46,6 @@ final class ChoiceLoader extends AbstractStaticOption implements ChoiceLoaderInt
*/
public function loadValuesForChoices(array $choices, callable $value = null)
{
$this->getOption()->loadValuesForChoices($choices, $value);
return $this->getOption()->loadValuesForChoices($choices, $value);
}
}

View File

@ -126,11 +126,11 @@ class NumberToLocalizedStringTransformer implements DataTransformerInterface
*/
public function reverseTransform($value)
{
if (!\is_string($value)) {
if (null !== $value && !\is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
if ('' === $value) {
if (null === $value || '' === $value) {
return null;
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\ChoiceList\Factory\Cache;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Factory\Cache\ChoiceLoader;
use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\FormTypeInterface;
class ChoiceLoaderTest extends TestCase
{
public function testSameFormTypeUseCachedLoader()
{
$choices = ['f' => 'foo', 'b' => 'bar', 'z' => 'baz'];
$choiceList = new ArrayChoiceList($choices);
$type = $this->createMock(FormTypeInterface::class);
$decorated = new CallbackChoiceLoader(static function () use ($choices) {
return $choices;
});
$loader1 = new ChoiceLoader($type, $decorated);
$loader2 = new ChoiceLoader($type, $this->createMock(ChoiceLoaderInterface::class));
$this->assertEquals($choiceList, $loader1->loadChoiceList());
$this->assertEquals($choiceList, $loader2->loadChoiceList());
$this->assertSame($choices, $loader1->loadChoicesForValues($choices));
$this->assertSame($choices, $loader2->loadChoicesForValues($choices));
$this->assertSame($choices, $loader1->loadValuesForChoices($choices));
$this->assertSame($choices, $loader2->loadValuesForChoices($choices));
}
}

View File

@ -124,6 +124,21 @@ class NumberTypeTest extends BaseTypeTest
$this->assertSame($expectedData, $form->getData());
}
public function testSubmitNullWithEmptyDataSetToNull()
{
$form = $this->factory->create(static::TESTED_TYPE, null, [
'empty_data' => null,
]);
$form->submit(null);
$this->assertTrue($form->isSubmitted());
$this->assertTrue($form->isSynchronized());
$this->assertTrue($form->isValid());
$this->assertSame('', $form->getViewData());
$this->assertNull($form->getNormData());
$this->assertNull($form->getData());
}
public function testSubmitNumericInput(): void
{
$form = $this->factory->create(static::TESTED_TYPE, null, ['input' => 'number']);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpClient;
use GuzzleHttp\Promise\Promise as GuzzlePromise;
use GuzzleHttp\Promise\RejectedPromise;
use Http\Client\Exception\NetworkException;
use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
@ -22,7 +23,6 @@ use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Message\UriFactory;
use Http\Promise\Promise;
use Http\Promise\RejectedPromise;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Uri;
@ -114,7 +114,7 @@ final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestF
try {
$response = $this->sendPsr7Request($request, true);
} catch (NetworkException $e) {
return new RejectedPromise($e);
return new HttplugPromise(new RejectedPromise($e));
}
$waitLoop = $this->waitLoop;

View File

@ -54,6 +54,12 @@ final class HttplugPromise implements HttplugPromiseInterface
*/
public function wait($unwrap = true)
{
return $this->promise->wait($unwrap);
$result = $this->promise->wait($unwrap);
while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) {
$result = $result->wait($unwrap);
}
return $result;
}
}

View File

@ -11,13 +11,18 @@
namespace Symfony\Component\HttpClient\Tests;
use GuzzleHttp\Promise\FulfilledPromise as GuzzleFulfilledPromise;
use Http\Client\Exception\NetworkException;
use Http\Client\Exception\RequestException;
use Http\Promise\FulfilledPromise;
use Http\Promise\Promise;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttplugClient;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
class HttplugClientTest extends TestCase
@ -152,4 +157,114 @@ class HttplugClientTest extends TestCase
$this->expectException(RequestException::class);
$client->sendRequest($client->createRequest('BAD.METHOD', 'http://localhost:8057'));
}
public function testRetry404()
{
$client = new HttplugClient(new NativeHttpClient());
$successCallableCalled = false;
$failureCallableCalled = false;
$promise = $client
->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/404'))
->then(
function (ResponseInterface $response) use (&$successCallableCalled, $client) {
$this->assertSame(404, $response->getStatusCode());
$successCallableCalled = true;
return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057'));
},
function (\Exception $exception) use (&$failureCallableCalled) {
$failureCallableCalled = true;
throw $exception;
}
)
;
$response = $promise->wait(true);
$this->assertTrue($successCallableCalled);
$this->assertFalse($failureCallableCalled);
$this->assertSame(200, $response->getStatusCode());
}
public function testRetryNetworkError()
{
$client = new HttplugClient(new NativeHttpClient());
$successCallableCalled = false;
$failureCallableCalled = false;
$promise = $client
->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057/chunked-broken'))
->then(function (ResponseInterface $response) use (&$successCallableCalled) {
$successCallableCalled = true;
return $response;
}, function (\Exception $exception) use (&$failureCallableCalled, $client) {
$this->assertSame(NetworkException::class, \get_class($exception));
$this->assertSame(TransportException::class, \get_class($exception->getPrevious()));
$failureCallableCalled = true;
return $client->sendAsyncRequest($client->createRequest('GET', 'http://localhost:8057'));
})
;
$response = $promise->wait(true);
$this->assertFalse($successCallableCalled);
$this->assertTrue($failureCallableCalled);
$this->assertSame(200, $response->getStatusCode());
}
public function testRetryEarlierError()
{
$isFirstRequest = true;
$errorMessage = 'Error occurred before making the actual request.';
$client = new HttplugClient(new MockHttpClient(function () use (&$isFirstRequest, $errorMessage) {
if ($isFirstRequest) {
$isFirstRequest = false;
throw new TransportException($errorMessage);
}
return new MockResponse('OK', ['http_code' => 200]);
}));
$request = $client->createRequest('GET', 'http://test');
$successCallableCalled = false;
$failureCallableCalled = false;
$promise = $client
->sendAsyncRequest($request)
->then(
function (ResponseInterface $response) use (&$successCallableCalled) {
$successCallableCalled = true;
return $response;
},
function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $client, $request) {
$this->assertSame(NetworkException::class, \get_class($exception));
$this->assertSame($errorMessage, $exception->getMessage());
$failureCallableCalled = true;
// Ensure arbitrary levels of promises work.
return (new FulfilledPromise(null))->then(function () use ($client, $request) {
return (new GuzzleFulfilledPromise(null))->then(function () use ($client, $request) {
return $client->sendAsyncRequest($request);
});
});
}
)
;
$response = $promise->wait(true);
$this->assertFalse($successCallableCalled);
$this->assertTrue($failureCallableCalled);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('OK', (string) $response->getBody());
}
}

View File

@ -56,6 +56,9 @@ class WindowsPipes extends AbstractPipes
$file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name);
if (!$h = fopen($file.'.lock', 'w')) {
if (file_exists($file.'.lock')) {
continue 2;
}
restore_error_handler();
throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError);
}

View File

@ -40,6 +40,10 @@ class RememberMeLogoutListener implements EventSubscriberInterface
return;
}
if (!$event->getToken()) {
return;
}
if (null === $event->getResponse()) {
throw new LogicException(sprintf('No response was set for this logout action. Make sure the DefaultLogoutListener or another listener has set the response before "%s" is called.', __CLASS__));
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Tests\EventListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
class RememberMeLogoutListenerTest extends TestCase
{
public function testOnLogoutDoesNothingIfNoToken()
{
$rememberMeServices = $this->createMock(AbstractRememberMeServices::class);
$rememberMeServices->expects($this->never())->method('logout');
$rememberMeLogoutListener = new RememberMeLogoutListener($rememberMeServices);
$rememberMeLogoutListener->onLogout(new LogoutEvent(new Request(), null));
}
}