Merge branch '4.4' into 5.0

* 4.4:
  Add missing use statements
  [Translation] Add missing use statement
  [Translation] Add missing use statement
  [Config][XmlReferenceDumper] Prevent potential \TypeError
  [Mailer] Fix broken mandrill http send for recipients with names
  [Translation] prefer intl domain when adding messages to catalogue
  Fix CS
  Fix CS
  Fail on empty password verification (without warning on any implementation)
  [Translation][Debug] Add installation and minimal example to README
  [Validator] try to call __get method if property is uninitialized
  Show both missing packages in the same error message
  Fix handling of empty_data's \Closure value in Date/Time form types
This commit is contained in:
Fabien Potencier 2020-02-04 08:41:34 +01:00
commit 85f793bec6
28 changed files with 297 additions and 25 deletions

View File

@ -41,12 +41,17 @@ class NotificationEmail extends TemplatedEmail
public function __construct(Headers $headers = null, AbstractPart $body = null)
{
$missingPackages = [];
if (!class_exists(CssInlinerExtension::class)) {
throw new \LogicException(sprintf('You cannot use "%s" if the CSS Inliner Twig extension is not available; try running "composer require twig/cssinliner-extra".', static::class));
$missingPackages['twig/cssinliner-extra'] = ' CSS Inliner';
}
if (!class_exists(InkyExtension::class)) {
throw new \LogicException(sprintf('You cannot use "%s" if the Inky Twig extension is not available; try running "composer require twig/inky-extra".', static::class));
$missingPackages['twig/inky-extra'] = 'Inky';
}
if ($missingPackages) {
throw new \LogicException(sprintf('You cannot use "%s" if the %s Twig extension%s not available; try running "composer require %s".', static::class, implode(' and ', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', implode(' ', array_keys($missingPackages))));
}
parent::__construct($headers, $body);

View File

@ -12,7 +12,6 @@
namespace Symfony\Bundle\SecurityBundle\Debug;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* Wraps a security listener for calls record.

View File

@ -91,7 +91,7 @@ class XmlReferenceDumper
}
if ($prototype instanceof PrototypedArrayNode) {
$prototype->setName($key);
$prototype->setName($key ?? '');
$children = [$key => $prototype];
} elseif ($prototype instanceof ArrayNode) {
$children = $prototype->getChildren();

View File

@ -109,7 +109,17 @@ class DateTimeType extends AbstractType
'invalid_message_parameters',
]));
if (isset($emptyData['date'])) {
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return isset($emptyData[$option]) ? $emptyData[$option] : '';
};
};
$dateOptions['empty_data'] = $lazyEmptyData('date');
} elseif (isset($emptyData['date'])) {
$dateOptions['empty_data'] = $emptyData['date'];
}
@ -128,7 +138,9 @@ class DateTimeType extends AbstractType
'invalid_message_parameters',
]));
if (isset($emptyData['time'])) {
if ($emptyData instanceof \Closure) {
$timeOptions['empty_data'] = $lazyEmptyData('time');
} elseif (isset($emptyData['time'])) {
$timeOptions['empty_data'] = $emptyData['time'];
}

View File

@ -83,14 +83,28 @@ class DateType extends AbstractType
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
if (isset($emptyData['year'])) {
$yearOptions['empty_data'] = $emptyData['year'];
}
if (isset($emptyData['month'])) {
$monthOptions['empty_data'] = $emptyData['month'];
}
if (isset($emptyData['day'])) {
$dayOptions['empty_data'] = $emptyData['day'];
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return isset($emptyData[$option]) ? $emptyData[$option] : '';
};
};
$yearOptions['empty_data'] = $lazyEmptyData('year');
$monthOptions['empty_data'] = $lazyEmptyData('month');
$dayOptions['empty_data'] = $lazyEmptyData('day');
} else {
if (isset($emptyData['year'])) {
$yearOptions['empty_data'] = $emptyData['year'];
}
if (isset($emptyData['month'])) {
$monthOptions['empty_data'] = $emptyData['month'];
}
if (isset($emptyData['day'])) {
$dayOptions['empty_data'] = $emptyData['day'];
}
}
if (isset($options['invalid_message'])) {

View File

@ -94,7 +94,17 @@ class TimeType extends AbstractType
// so we need to handle the cascade setting here
$emptyData = $builder->getEmptyData() ?: [];
if (isset($emptyData['hour'])) {
if ($emptyData instanceof \Closure) {
$lazyEmptyData = static function ($option) use ($emptyData) {
return static function (FormInterface $form) use ($emptyData, $option) {
$emptyData = $emptyData($form->getParent());
return isset($emptyData[$option]) ? $emptyData[$option] : '';
};
};
$hourOptions['empty_data'] = $lazyEmptyData('hour');
} elseif (isset($emptyData['hour'])) {
$hourOptions['empty_data'] = $emptyData['hour'];
}
@ -161,14 +171,18 @@ class TimeType extends AbstractType
$builder->add('hour', self::$widgets[$options['widget']], $hourOptions);
if ($options['with_minutes']) {
if (isset($emptyData['minute'])) {
if ($emptyData instanceof \Closure) {
$minuteOptions['empty_data'] = $lazyEmptyData('minute');
} elseif (isset($emptyData['minute'])) {
$minuteOptions['empty_data'] = $emptyData['minute'];
}
$builder->add('minute', self::$widgets[$options['widget']], $minuteOptions);
}
if ($options['with_seconds']) {
if (isset($emptyData['second'])) {
if ($emptyData instanceof \Closure) {
$secondOptions['empty_data'] = $lazyEmptyData('second');
} elseif (isset($emptyData['second'])) {
$secondOptions['empty_data'] = $emptyData['second'];
}
$builder->add('second', self::$widgets[$options['widget']], $secondOptions);

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
class DateTimeTypeTest extends BaseTypeTest
{
@ -659,6 +660,9 @@ class DateTimeTypeTest extends BaseTypeTest
]);
$form->submit(null);
if ($emptyData instanceof \Closure) {
$emptyData = $emptyData($form);
}
$this->assertSame($emptyData, $form->getViewData());
$this->assertEquals($expectedData, $form->getNormData());
$this->assertEquals($expectedData, $form->getData());
@ -667,11 +671,17 @@ class DateTimeTypeTest extends BaseTypeTest
public function provideEmptyData()
{
$expectedData = \DateTime::createFromFormat('Y-m-d H:i', '2018-11-11 21:23');
$lazyEmptyData = static function (FormInterface $form) {
return $form->getConfig()->getCompound() ? ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']] : '2018-11-11T21:23:00';
};
return [
'Simple field' => ['single_text', '2018-11-11T21:23:00', $expectedData],
'Compound text field' => ['text', ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']], $expectedData],
'Compound choice field' => ['choice', ['date' => ['year' => '2018', 'month' => '11', 'day' => '11'], 'time' => ['hour' => '21', 'minute' => '23']], $expectedData],
'Simple field lazy' => ['single_text', $lazyEmptyData, $expectedData],
'Compound text field lazy' => ['text', $lazyEmptyData, $expectedData],
'Compound choice field lazy' => ['choice', $lazyEmptyData, $expectedData],
];
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Intl\Util\IntlTestHelper;
class DateTypeTest extends BaseTypeTest
@ -1022,6 +1023,9 @@ class DateTypeTest extends BaseTypeTest
]);
$form->submit(null);
if ($emptyData instanceof \Closure) {
$emptyData = $emptyData($form);
}
$this->assertSame($emptyData, $form->getViewData());
$this->assertEquals($expectedData, $form->getNormData());
$this->assertEquals($expectedData, $form->getData());
@ -1030,11 +1034,17 @@ class DateTypeTest extends BaseTypeTest
public function provideEmptyData()
{
$expectedData = \DateTime::createFromFormat('Y-m-d H:i:s', '2018-11-11 00:00:00');
$lazyEmptyData = static function (FormInterface $form) {
return $form->getConfig()->getCompound() ? ['year' => '2018', 'month' => '11', 'day' => '11'] : '2018-11-11';
};
return [
'Simple field' => ['single_text', '2018-11-11', $expectedData],
'Compound text fields' => ['text', ['year' => '2018', 'month' => '11', 'day' => '11'], $expectedData],
'Compound choice fields' => ['choice', ['year' => '2018', 'month' => '11', 'day' => '11'], $expectedData],
'Simple field lazy' => ['single_text', $lazyEmptyData, $expectedData],
'Compound text fields lazy' => ['text', $lazyEmptyData, $expectedData],
'Compound choice fields lazy' => ['choice', $lazyEmptyData, $expectedData],
];
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
class TimeTypeTest extends BaseTypeTest
{
@ -928,6 +929,9 @@ class TimeTypeTest extends BaseTypeTest
]);
$form->submit(null);
if ($emptyData instanceof \Closure) {
$emptyData = $emptyData($form);
}
$this->assertSame($emptyData, $form->getViewData());
$this->assertEquals($expectedData, $form->getNormData());
$this->assertEquals($expectedData, $form->getData());
@ -936,11 +940,17 @@ class TimeTypeTest extends BaseTypeTest
public function provideEmptyData()
{
$expectedData = \DateTime::createFromFormat('Y-m-d H:i', '1970-01-01 21:23');
$lazyEmptyData = static function (FormInterface $form) {
return $form->getConfig()->getCompound() ? ['hour' => '21', 'minute' => '23'] : '21:23';
};
return [
'Simple field' => ['single_text', '21:23', $expectedData],
'Compound text field' => ['text', ['hour' => '21', 'minute' => '23'], $expectedData],
'Compound choice field' => ['choice', ['hour' => '21', 'minute' => '23'], $expectedData],
'Simple field lazy' => ['single_text', $lazyEmptyData, $expectedData],
'Compound text field lazy' => ['text', $lazyEmptyData, $expectedData],
'Compound choice field lazy' => ['choice', $lazyEmptyData, $expectedData],
];
}
}

View File

@ -54,7 +54,7 @@ class SessionHandlerFactory
case 0 === strpos($connection, 'rediss:'):
case 0 === strpos($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
}
$handlerClass = 0 === strpos($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);

View File

@ -0,0 +1,71 @@
<?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\Mailer\Bridge\Mailchimp\Http;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\SmtpEnvelope;
use Symfony\Component\Mailer\Transport\Http\AbstractHttpTransport;
use Symfony\Component\Mime\Address;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @author Kevin Verschaeve
*
* @experimental in 4.3
*/
class MandrillTransport extends AbstractHttpTransport
{
private const ENDPOINT = 'https://mandrillapp.com/api/1.0/messages/send-raw.json';
private $key;
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
$this->key = $key;
parent::__construct($client, $dispatcher, $logger);
}
protected function doSend(SentMessage $message): void
{
$envelope = $message->getEnvelope();
$response = $this->client->request('POST', self::ENDPOINT, [
'json' => [
'key' => $this->key,
'to' => $this->getRecipients($envelope),
'from_email' => $envelope->getSender()->getAddress(),
'raw_message' => $message->toString(),
],
]);
if (200 !== $response->getStatusCode()) {
$result = $response->toArray(false);
if ('error' === ($result['status'] ?? false)) {
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']));
}
throw new TransportException(sprintf('Unable to send an email (code %s).', $result['code']));
}
}
/**
* @return string[]
*/
private function getRecipients(SmtpEnvelope $envelope): array
{
return array_map(function (Address $recipient): string {
return $recipient->getAddress();
}, $envelope->getRecipients());
}
}

View File

@ -12,9 +12,11 @@
namespace Symfony\Component\Mailer\Bridge\Mailchimp\Transport;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractHttpTransport;
use Symfony\Component\Mime\Address;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
@ -45,7 +47,7 @@ class MandrillHttpTransport extends AbstractHttpTransport
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/1.0/messages/send-raw.json', [
'json' => [
'key' => $this->key,
'to' => $this->stringifyAddresses($envelope->getRecipients()),
'to' => $this->getRecipients($envelope->getRecipients()),
'from_email' => $envelope->getSender()->toString(),
'raw_message' => $message->toString(),
],
@ -69,4 +71,14 @@ class MandrillHttpTransport extends AbstractHttpTransport
{
return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : '');
}
/**
* @return string[]
*/
private function getRecipients(Envelope $envelope): array
{
return array_map(function (Address $recipient): string {
return $recipient->getAddress();
}, $envelope->getRecipients());
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailAttachmentCount extends Constraint

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailHtmlBodyContains extends Constraint
{

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailTextBodyContains extends Constraint
{

View File

@ -3,6 +3,19 @@ Nexmo Notifier
Provides Nexmo integration for Symfony Notifier.
Getting Started
---------------
```
$ composer install symfony/debug
```
```php
use Symfony\Component\Debug\Debug;
Debug::enable();
```
Resources
---------

View File

@ -76,6 +76,10 @@ final class NativePasswordEncoder implements PasswordEncoderInterface, SelfSalti
*/
public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
{
if ('' === $raw) {
return false;
}
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
return false;
}

View File

@ -76,6 +76,10 @@ final class SodiumPasswordEncoder implements PasswordEncoderInterface, SelfSalti
*/
public function isPasswordValid(string $encoded, string $raw, ?string $salt): bool
{
if ('' === $raw) {
return false;
}
if (\strlen($raw) > self::MAX_PASSWORD_LENGTH) {
return false;
}

View File

@ -53,6 +53,7 @@ class NativePasswordEncoderTest extends TestCase
$result = $encoder->encodePassword('password', null);
$this->assertTrue($encoder->isPasswordValid($result, 'password', null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
$this->assertFalse($encoder->isPasswordValid($result, '', null));
}
public function testNonArgonValidation()

View File

@ -29,6 +29,7 @@ class SodiumPasswordEncoderTest extends TestCase
$result = $encoder->encodePassword('password', null);
$this->assertTrue($encoder->isPasswordValid($result, 'password', null));
$this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
$this->assertFalse($encoder->isPasswordValid($result, '', null));
}
public function testBCryptValidation()

View File

@ -15,6 +15,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

View File

@ -18,6 +18,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Util\XliffUtils;
/**

View File

@ -154,9 +154,17 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
public function add(array $messages, string $domain = 'messages')
{
if (!isset($this->messages[$domain])) {
$this->messages[$domain] = $messages;
} else {
foreach ($messages as $id => $message) {
$this->messages[$domain] = [];
}
$intlDomain = $domain;
$suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX);
if (\strlen($domain) > $suffixLength && false !== strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) {
$intlDomain .= self::INTL_DOMAIN_SUFFIX;
}
foreach ($messages as $id => $message) {
if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) {
$this->messages[$intlDomain][$id] = $message;
} else {
$this->messages[$domain][$id] = $message;
}
}

View File

@ -3,10 +3,28 @@ Translation Component
The Translation component provides tools to internationalize your application.
Getting Started
---------------
```
$ composer require symfony/translation
```
```php
use Symfony\Component\Translation\Translator;
$translator = new Translator('fr_FR');
$translator->addResource('array', [
'Hello World!' => 'Bonjour !',
], 'fr_FR');
echo $translator->trans('Hello World!'); // outputs « Bonjour ! »
```
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/translation.html)
* [Documentation](https://symfony.com/doc/current/translation.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)

View File

@ -67,6 +67,19 @@ class TargetOperationTest extends AbstractOperationTest
);
}
public function testGetResultWithMixedDomains()
{
$this->assertEquals(
new MessageCatalogue('en', [
'messages+intl-icu' => ['a' => 'old_a'],
]),
$this->createOperation(
new MessageCatalogue('en', ['messages' => ['a' => 'old_a']]),
new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'new_a']])
)->getResult()
);
}
public function testGetResultWithMetadata()
{
$leftCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]);

View File

@ -50,8 +50,21 @@ class PropertyMetadata extends MemberMetadata
{
$reflProperty = $this->getReflectionMember($object);
if (\PHP_VERSION_ID >= 70400 && !$reflProperty->isInitialized($object)) {
return null;
if (\PHP_VERSION_ID >= 70400 && $reflProperty->hasType() && !$reflProperty->isInitialized($object)) {
// There is no way to check if a property has been unset or if it is uninitialized.
// When trying to access an uninitialized property, __get method is triggered.
// If __get method is not present, no fallback is possible
// Otherwise we need to catch an Error in case we are trying to access an uninitialized but set property.
if (!method_exists($object, '__get')) {
return null;
}
try {
return $reflProperty->getValue($object);
} catch (\Error $e) {
return null;
}
}
return $reflProperty->getValue($object);

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\Validator\Tests\Fixtures;
class Entity_74_Proxy extends Entity_74
{
public string $notUnset;
public function __construct()
{
unset($this->uninitialized);
}
public function __get($name)
{
return 42;
}
}

View File

@ -15,11 +15,13 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Mapping\PropertyMetadata;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Tests\Fixtures\Entity_74;
use Symfony\Component\Validator\Tests\Fixtures\Entity_74_Proxy;
class PropertyMetadataTest extends TestCase
{
const CLASSNAME = 'Symfony\Component\Validator\Tests\Fixtures\Entity';
const CLASSNAME_74 = 'Symfony\Component\Validator\Tests\Fixtures\Entity_74';
const CLASSNAME_74_PROXY = 'Symfony\Component\Validator\Tests\Fixtures\Entity_74_Proxy';
const PARENTCLASS = 'Symfony\Component\Validator\Tests\Fixtures\EntityParent';
public function testInvalidPropertyName()
@ -66,4 +68,17 @@ class PropertyMetadataTest extends TestCase
$this->assertNull($metadata->getPropertyValue($entity));
}
/**
* @requires PHP 7.4
*/
public function testGetPropertyValueFromUninitializedPropertyShouldNotReturnNullIfMagicGetIsPresent()
{
$entity = new Entity_74_Proxy();
$metadata = new PropertyMetadata(self::CLASSNAME_74_PROXY, 'uninitialized');
$notUnsetMetadata = new PropertyMetadata(self::CLASSNAME_74_PROXY, 'notUnset');
$this->assertNull($notUnsetMetadata->getPropertyValue($entity));
$this->assertEquals(42, $metadata->getPropertyValue($entity));
}
}