Merge branch '3.4' into 4.3

* 3.4:
  Sync Twig templateExists behaviors
  Fix the :only-of-type pseudo class selector
  [Serializer] Add CsvEncoder tests for PHP 7.4
  Copy phpunit.xsd to a predictable path
  [Security/Http] fix parsing X509 emailAddress
  [Serializer] fix denormalization of string-arrays with only one element #33731
  [Cache] fix known tag versions ttl check
This commit is contained in:
Nicolas Grekas 2019-10-02 10:36:26 +02:00
commit b628210df7
17 changed files with 263 additions and 84 deletions

View File

@ -110,6 +110,7 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__
passthru(sprintf('\\' === DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s': 'rm -rf %s', "phpunit-$PHPUNIT_VERSION.old"));
}
passthru("$COMPOSER create-project --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit phpunit-$PHPUNIT_VERSION \"$PHPUNIT_VERSION.*\"");
@copy("phpunit-$PHPUNIT_VERSION/phpunit.xsd", 'phpunit.xsd');
chdir("phpunit-$PHPUNIT_VERSION");
if ($SYMFONY_PHPUNIT_REMOVE) {
passthru("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE);

View File

@ -21,6 +21,7 @@ use Twig\Environment;
use Twig\Error\Error;
use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface;
use Twig\Loader\SourceContextLoaderInterface;
use Twig\Template;
/**
@ -78,19 +79,24 @@ class TwigEngine implements EngineInterface, StreamingEngineInterface
$loader = $this->environment->getLoader();
if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) {
return $loader->exists((string) $name);
}
if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) {
try {
// cast possible TemplateReferenceInterface to string because the
// EngineInterface supports them but LoaderInterface does not
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext((string) $name);
} else {
$loader->getSource((string) $name);
}
return true;
} catch (LoaderError $e) {
}
try {
// cast possible TemplateReferenceInterface to string because the
// EngineInterface supports them but LoaderInterface does not
$loader->getSourceContext((string) $name)->getCode();
} catch (LoaderError $e) {
return false;
}
return true;
return $loader->exists((string) $name);
}
/**

View File

@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface;
use Twig\Loader\SourceContextLoaderInterface;
/**
* ExceptionController renders error or exception pages for a given
@ -120,23 +121,28 @@ class ExceptionController
return sprintf('@Twig/Exception/%s.html.twig', $showException ? 'exception_full' : $name);
}
// to be removed when the minimum required version of Twig is >= 3.0
// to be removed when the minimum required version of Twig is >= 2.0
protected function templateExists($template)
{
$template = (string) $template;
$loader = $this->twig->getLoader();
if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) {
return $loader->exists($template);
if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) {
try {
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
}
return true;
} catch (LoaderError $e) {
}
return false;
}
try {
$loader->getSourceContext($template)->getCode();
return true;
} catch (LoaderError $e) {
}
return false;
return $loader->exists($template);
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\Profiler\Profiler;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface;
use Twig\Loader\SourceContextLoaderInterface;
/**
* ExceptionController.
@ -118,17 +119,22 @@ class ExceptionController
protected function templateExists($template)
{
$loader = $this->twig->getLoader();
if ($loader instanceof ExistsLoaderInterface) {
return $loader->exists($template);
if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) {
try {
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
}
return true;
} catch (LoaderError $e) {
}
return false;
}
try {
$loader->getSource($template);
return true;
} catch (LoaderError $e) {
}
return false;
return $loader->exists($template);
}
}

View File

@ -99,21 +99,22 @@ class TemplateManager
protected function templateExists($template)
{
$loader = $this->twig->getLoader();
if ($loader instanceof ExistsLoaderInterface) {
return $loader->exists($template);
}
try {
if ($loader instanceof SourceContextLoaderInterface || method_exists($loader, 'getSourceContext')) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) {
try {
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
}
return true;
} catch (LoaderError $e) {
}
return true;
} catch (LoaderError $e) {
return false;
}
return false;
return $loader->exists($template);
}
}

View File

@ -15,6 +15,8 @@ use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
use Symfony\Bundle\WebProfilerBundle\Tests\TestCase;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Twig\Environment;
use Twig\Loader\LoaderInterface;
use Twig\Loader\SourceContextLoaderInterface;
/**
* Test for TemplateManager class.
@ -107,11 +109,16 @@ class TemplateManagerTest extends TestCase
->method('loadTemplate')
->willReturn('loadedTemplate');
if (interface_exists('Twig\Loader\SourceContextLoaderInterface')) {
$loader = $this->getMockBuilder('Twig\Loader\SourceContextLoaderInterface')->getMock();
if (Environment::MAJOR_VERSION > 1) {
$loader = $this->createMock(LoaderInterface::class);
$loader
->expects($this->any())
->method('exists')
->willReturn(true);
} else {
$loader = $this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock();
$loader = $this->createMock(SourceContextLoaderInterface::class);
}
$this->twigEnvironment->expects($this->any())->method('getLoader')->willReturn($loader);
return $this->twigEnvironment;

View File

@ -354,7 +354,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac
continue;
}
$version -= $this->knownTagVersions[$tag][1];
if ((0 !== $version && 1 !== $version) || $this->knownTagVersionsTtl > $now - $this->knownTagVersions[$tag][0]) {
if ((0 !== $version && 1 !== $version) || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
// reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises
$fetchTagVersions = true;
} else {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Cache\Tests\Adapter;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
@ -65,6 +66,39 @@ class TagAwareAdapterTest extends AdapterTestCase
$this->assertFalse($cache->prune());
}
public function testKnownTagVersionsTtl()
{
$itemsPool = new FilesystemAdapter('', 10);
$tagsPool = $this
->getMockBuilder(AdapterInterface::class)
->getMock();
$pool = new TagAwareAdapter($itemsPool, $tagsPool, 10);
$item = $pool->getItem('foo');
$item->tag(['baz']);
$item->expiresAfter(100);
$tag = $this->getMockBuilder(CacheItemInterface::class)->getMock();
$tag->expects(self::exactly(2))->method('get')->willReturn(10);
$tagsPool->expects(self::exactly(2))->method('getItems')->willReturn([
'baz'.TagAwareAdapter::TAGS_PREFIX => $tag,
]);
$pool->save($item);
$this->assertTrue($pool->getItem('foo')->isHit());
$this->assertTrue($pool->getItem('foo')->isHit());
sleep(20);
$this->assertTrue($pool->getItem('foo')->isHit());
sleep(5);
$this->assertTrue($pool->getItem('foo')->isHit());
}
/**
* @return MockObject|PruneableCacheInterface
*/

View File

@ -98,7 +98,7 @@ class TranslatorTest extends TestCase
$elements = $document->xpath($translator->cssToXPath($css));
$this->assertCount(\count($elementsId), $elements);
foreach ($elements as $element) {
$this->assertTrue(\in_array($element->attributes()->id, $elementsId));
$this->assertContains((string) $element->attributes()->id, $elementsId);
}
}
@ -116,7 +116,7 @@ class TranslatorTest extends TestCase
$this->assertCount(\count($elementsId), $elementsId);
foreach ($elements as $element) {
if (null !== $element->attributes()->id) {
$this->assertTrue(\in_array($element->attributes()->id, $elementsId));
$this->assertContains((string) $element->attributes()->id, $elementsId);
}
}
libxml_clear_errors();
@ -137,6 +137,33 @@ class TranslatorTest extends TestCase
$this->assertCount($count, $elements);
}
public function testOnlyOfTypeFindsSingleChildrenOfGivenType()
{
$translator = new Translator();
$translator->registerExtension(new HtmlExtension($translator));
$document = new \DOMDocument();
$document->loadHTML(<<<'HTML'
<html>
<body>
<p>
<span>A</span>
</p>
<p>
<span>B</span>
<span>C</span>
</p>
</body>
</html>
HTML
);
$xpath = new \DOMXPath($document);
$nodeList = $xpath->query($translator->cssToXPath('span:only-of-type'));
$this->assertSame(1, $nodeList->length);
$this->assertSame('A', $nodeList->item(0)->textContent);
}
public function getXpathLiteralTestData()
{
return [
@ -175,7 +202,7 @@ class TranslatorTest extends TestCase
['e:first-of-type', '*/e[position() = 1]'],
['e:last-of-type', '*/e[position() = last()]'],
['e:only-child', "*/*[(name() = 'e') and (last() = 1)]"],
['e:only-of-type', 'e[last() = 1]'],
['e:only-of-type', 'e[count(preceding-sibling::e)=0 and count(following-sibling::e)=0]'],
['e:empty', 'e[not(*) and not(string-length())]'],
['e:EmPTY', 'e[not(*) and not(string-length())]'],
['e:root', 'e[not(parent::*)]'],

View File

@ -123,11 +123,13 @@ class PseudoClassExtension extends AbstractExtension
*/
public function translateOnlyOfType(XPathExpr $xpath)
{
if ('*' === $xpath->getElement()) {
$element = $xpath->getElement();
if ('*' === $element) {
throw new ExpressionErrorException('"*:only-of-type" is not implemented.');
}
return $xpath->addCondition('last() = 1');
return $xpath->addCondition(sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element));
}
/**

View File

@ -19,6 +19,7 @@ use Symfony\Component\Templating\EngineInterface;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface;
use Twig\Loader\SourceContextLoaderInterface;
/**
* Implements the Hinclude rendering strategy.
@ -135,22 +136,23 @@ class HIncludeFragmentRenderer extends RoutableFragmentRenderer
}
$loader = $this->templating->getLoader();
if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) {
return $loader->exists($template);
}
try {
if (method_exists($loader, 'getSourceContext')) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) {
try {
if ($loader instanceof SourceContextLoaderInterface) {
$loader->getSourceContext($template);
} else {
$loader->getSource($template);
}
return true;
} catch (LoaderError $e) {
}
return true;
} catch (LoaderError $e) {
return false;
}
return false;
return $loader->exists($template);
}
/**

View File

@ -46,7 +46,7 @@ class X509AuthenticationListener extends AbstractPreAuthenticatedListener
$user = $request->server->get($this->userKey);
} elseif (
$request->server->has($this->credentialKey)
&& preg_match('#emailAddress=(.+\@.+\.[^,/]+)($|,|/)#', $request->server->get($this->credentialKey), $matches)
&& preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialKey), $matches)
) {
$user = $matches[1];
}

View File

@ -81,6 +81,7 @@ class X509AuthenticationListenerTest extends TestCase
yield ['cert+something@example.com', 'CN=Sample certificate DN,emailAddress=cert+something@example.com'];
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com,CN=Sample certificate DN'];
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com'];
yield ['firstname.lastname@mycompany.co.uk', 'emailAddress=firstname.lastname@mycompany.co.uk,CN=Firstname.Lastname,OU=london,OU=company design and engineering,OU=Issuer London,OU=Roaming,OU=Interactive,OU=Users,OU=Standard,OU=Business,DC=england,DC=core,DC=company,DC=co,DC=uk'];
}
public function testGetPreAuthenticatedDataNoData()

View File

@ -45,8 +45,12 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
/**
* @param array $defaultContext
*/
public function __construct($defaultContext = [], string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.', bool $escapeFormulas = false)
public function __construct($defaultContext = [], string $enclosure = '"', string $escapeChar = '', string $keySeparator = '.', bool $escapeFormulas = false)
{
if ('' === $escapeChar && \PHP_VERSION_ID < 70400) {
$escapeChar = '\\';
}
if (!\is_array($defaultContext)) {
@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', E_USER_DEPRECATED);

View File

@ -397,16 +397,18 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
return null;
}
if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null;
// Fix a collection that contains the only one element
// This is special to xml format only
if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
$data = [$data];
}
if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$builtinType = Type::BUILTIN_TYPE_OBJECT;
$class = $collectionValueType->getClassName().'[]';
// Fix a collection that contains the only one element
// This is special to xml format only
if ('xml' === $format && !\is_int(key($data))) {
$data = [$data];
}
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
$context['key_type'] = $collectionKeyType;
}

View File

@ -36,15 +36,51 @@ class CsvEncoderTest extends TestCase
'int' => 2,
'false' => false,
'true' => true,
'int_one' => 1,
'string_one' => '1',
];
// Check that true and false are appropriately handled
$this->assertEquals(<<<'CSV'
string,int,false,true
foo,2,0,1
$this->assertSame($csv = <<<'CSV'
string,int,false,true,int_one,string_one
foo,2,0,1,1,1
CSV
, $this->encoder->encode($data, 'csv'));
, $this->encoder->encode($data, 'csv'));
$this->assertSame([
'string' => 'foo',
'int' => '2',
'false' => '0',
'true' => '1',
'int_one' => '1',
'string_one' => '1',
], $this->encoder->decode($csv, 'csv'));
}
/**
* @requires PHP 7.4
*/
public function testDoubleQuotesAndSlashes()
{
$this->assertSame($csv = <<<'CSV'
0,1,2,3,4,5
,"""","foo""","\""",\,foo\
CSV
, $this->encoder->encode($data = ['', '"', 'foo"', '\\"', '\\', 'foo\\'], 'csv'));
$this->assertSame($data, $this->encoder->decode($csv, 'csv'));
}
/**
* @requires PHP 7.4
*/
public function testSingleSlash()
{
$this->assertSame($csv = "0\n\\\n", $this->encoder->encode($data = ['\\'], 'csv'));
$this->assertSame($data, $this->encoder->decode($csv, 'csv'));
$this->assertSame($data, $this->encoder->decode(trim($csv), 'csv'));
}
public function testSupportEncoding()

View File

@ -132,16 +132,54 @@ class AbstractObjectNormalizerTest extends TestCase
$extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock();
$extractor->method('getTypes')
->will($this->onConsecutiveCalls(
[
new Type(
'array',
false,
null,
true,
new Type('int'),
new Type('object', false, DummyChild::class)
),
],
[new Type('array', false, null, true, new Type('int'), new Type('object', false, DummyChild::class))],
null
));
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
$arrayDenormalizer = new ArrayDenormalizerDummy();
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
$arrayDenormalizer->setSerializer($serializer);
$denormalizer->setSerializer($serializer);
return $denormalizer;
}
public function testDenormalizeStringCollectionDecodedFromXmlWithOneChild()
{
$denormalizer = $this->getDenormalizerForStringCollection();
// if an xml-node can have children which should be deserialized as string[]
// and only one child exists
$stringCollection = $denormalizer->denormalize(['children' => 'foo'], StringCollection::class, 'xml');
$this->assertInstanceOf(StringCollection::class, $stringCollection);
$this->assertIsArray($stringCollection->children);
$this->assertCount(1, $stringCollection->children);
$this->assertEquals('foo', $stringCollection->children[0]);
}
public function testDenormalizeStringCollectionDecodedFromXmlWithTwoChildren()
{
$denormalizer = $this->getDenormalizerForStringCollection();
// if an xml-node can have children which should be deserialized as string[]
// and only one child exists
$stringCollection = $denormalizer->denormalize(['children' => ['foo', 'bar']], StringCollection::class, 'xml');
$this->assertInstanceOf(StringCollection::class, $stringCollection);
$this->assertIsArray($stringCollection->children);
$this->assertCount(2, $stringCollection->children);
$this->assertEquals('foo', $stringCollection->children[0]);
$this->assertEquals('bar', $stringCollection->children[1]);
}
private function getDenormalizerForStringCollection()
{
$extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock();
$extractor->method('getTypes')
->will($this->onConsecutiveCalls(
[new Type('array', false, null, true, new Type('int'), new Type('string'))],
null
));
@ -258,6 +296,12 @@ class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
}
}
class StringCollection
{
/** @var string[] */
public $children;
}
class DummyCollection
{
/** @var DummyChild[] */