Skip to content

Commit 7cd4e55

Browse files
Merge branch '4.4' into 5.0
* 4.4: (30 commits) [Security] Check UserInterface::getPassword is not null before calling needsRehash gracefully handle missing event dispatchers Fix TokenStorage::reset not called in stateless firewall [DotEnv] Remove `usePutEnv` property default value [HttpFoundation] get currently session.gc_maxlifetime if ttl doesnt exists Set up typo fix [DependencyInjection] Handle env var placeholders in CheckTypeDeclarationsPass [Cache] fix memory leak when using PhpArrayAdapter [Validator] Allow underscore character "_" in URL username and password [TwigBridge] Update bootstrap_4_layout.html.twig [FrameworkBundle][SodiumVault] Create secrets directory only when needed fix parsing negative octal numbers [SecurityBundle] Passwords are not encoded when algorithm set to \"true\" [DependencyInjection] Resolve expressions in CheckTypeDeclarationsPass [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass do not validate passwords when the hash is null [DI] fix resolving bindings for named TypedReference [Config] never try loading failed classes twice with ClassExistenceResource [Mailer] Fix SMTP Authentication when using STARTTLS [DI] Fix making the container path-independent when the app is in /app ...
2 parents ab20e90 + 64d5272 commit 7cd4e55

File tree

5 files changed

+155
-9
lines changed

5 files changed

+155
-9
lines changed

Compiler/CheckTypeDeclarationsPass.php

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1515
use Symfony\Component\DependencyInjection\Container;
1616
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
1718
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1819
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
1920
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
21+
use Symfony\Component\DependencyInjection\ExpressionLanguage;
2022
use Symfony\Component\DependencyInjection\Parameter;
23+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
2124
use Symfony\Component\DependencyInjection\Reference;
2225
use Symfony\Component\DependencyInjection\ServiceLocator;
26+
use Symfony\Component\ExpressionLanguage\Expression;
2327

2428
/**
2529
* Checks whether injected parameters are compatible with type declarations.
@@ -39,6 +43,8 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
3943

4044
private $autoload;
4145

46+
private $expressionLanguage;
47+
4248
/**
4349
* @param bool $autoload Whether services who's class in not loaded should be checked or not.
4450
* Defaults to false to save loading code during compilation.
@@ -100,27 +106,29 @@ private function checkTypeDeclarations(Definition $checkedDefinition, \Reflectio
100106
$reflectionParameters = $reflectionFunction->getParameters();
101107
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));
102108

109+
$envPlaceholderUniquePrefix = $this->container->getParameterBag() instanceof EnvPlaceholderParameterBag ? $this->container->getParameterBag()->getEnvPlaceholderUniquePrefix() : null;
110+
103111
for ($i = 0; $i < $checksCount; ++$i) {
104112
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
105113
continue;
106114
}
107115

108-
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]);
116+
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i], $envPlaceholderUniquePrefix);
109117
}
110118

111119
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
112120
$variadicParameters = \array_slice($values, $lastParameter->getPosition());
113121

114122
foreach ($variadicParameters as $variadicParameter) {
115-
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter);
123+
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter, $envPlaceholderUniquePrefix);
116124
}
117125
}
118126
}
119127

120128
/**
121129
* @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
122130
*/
123-
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void
131+
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter, ?string $envPlaceholderUniquePrefix): void
124132
{
125133
$type = $parameter->getType()->getName();
126134

@@ -172,8 +180,24 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar
172180

173181
if ($value instanceof Parameter) {
174182
$value = $this->container->getParameter($value);
175-
} elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
176-
$value = $this->container->getParameter($match[1]);
183+
} elseif ($value instanceof Expression) {
184+
$value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this->container]);
185+
} elseif (\is_string($value)) {
186+
if ('%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
187+
// Only array parameters are not inlined when dumped.
188+
$value = [];
189+
} elseif ($envPlaceholderUniquePrefix && false !== strpos($value, 'env_')) {
190+
// If the value is an env placeholder that is either mixed with a string or with another env placeholder, then its resolved value will always be a string, so we don't need to resolve it.
191+
// We don't need to change the value because it is already a string.
192+
if ('' === preg_replace('/'.$envPlaceholderUniquePrefix.'_\w+_[a-f0-9]{32}/U', '', $value, -1, $c) && 1 === $c) {
193+
try {
194+
$value = $this->container->resolveEnvPlaceholders($value, true);
195+
} catch (EnvNotFoundException | RuntimeException $e) {
196+
// If an env placeholder cannot be resolved, we skip the validation.
197+
return;
198+
}
199+
}
200+
}
177201
}
178202

179203
if (null === $value && $parameter->allowsNull()) {
@@ -202,4 +226,13 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar
202226
throw new InvalidParameterTypeException($this->currentId, \is_object($value) ? \get_class($value) : \gettype($value), $parameter);
203227
}
204228
}
229+
230+
private function getExpressionLanguage(): ExpressionLanguage
231+
{
232+
if (null === $this->expressionLanguage) {
233+
$this->expressionLanguage = new ExpressionLanguage(null, $this->container->getExpressionLanguageProviders());
234+
}
235+
236+
return $this->expressionLanguage;
237+
}
205238
}

Compiler/ResolveBindingsPass.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ protected function processValue($value, bool $isRoot = false)
9696
if ($value instanceof TypedReference && $value->getType() === (string) $value) {
9797
// Already checked
9898
$bindings = $this->container->getDefinition($this->currentId)->getBindings();
99+
$name = $value->getName();
100+
101+
if (isset($name, $bindings[$name = $value.' $'.$name])) {
102+
return $this->getBindingValue($bindings[$name]);
103+
}
99104

100105
if (isset($bindings[$value->getType()])) {
101106
return $this->getBindingValue($bindings[$value->getType()]);

Dumper/PhpDumper.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,14 @@ public function dump(array $options = [])
203203
if (!empty($options['file']) && is_dir($dir = \dirname($options['file']))) {
204204
// Build a regexp where the first root dirs are mandatory,
205205
// but every other sub-dir is optional up to the full path in $dir
206-
// Mandate at least 2 root dirs and not more that 5 optional dirs.
206+
// Mandate at least 1 root dir and not more than 5 optional dirs.
207207

208208
$dir = explode(\DIRECTORY_SEPARATOR, realpath($dir));
209209
$i = \count($dir);
210210

211-
if (3 <= $i) {
211+
if (2 + (int) ('\\' === \DIRECTORY_SEPARATOR) <= $i) {
212212
$regex = '';
213-
$lastOptionalDir = $i > 8 ? $i - 5 : 3;
213+
$lastOptionalDir = $i > 8 ? $i - 5 : (2 + (int) ('\\' === \DIRECTORY_SEPARATOR));
214214
$this->targetDirMaxMatches = $i - $lastOptionalDir;
215215

216216
while (--$i >= $lastOptionalDir) {

Tests/Compiler/CheckTypeDeclarationsPassTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1616
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
17+
use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
1819
use Symfony\Component\DependencyInjection\Definition;
20+
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
1921
use Symfony\Component\DependencyInjection\Reference;
2022
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar;
2123
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall;
2224
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument;
2325
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull;
2426
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Foo;
2527
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\FooObject;
28+
use Symfony\Component\ExpressionLanguage\Expression;
2629

2730
/**
2831
* @author Nicolas Grekas <[email protected]>
@@ -569,4 +572,101 @@ public function testProcessThrowsOnIterableTypeWhenScalarPassed()
569572

570573
$this->assertInstanceOf(\stdClass::class, $container->get('bar')->foo);
571574
}
575+
576+
public function testProcessResolveArrayParameters()
577+
{
578+
$container = new ContainerBuilder();
579+
$container->setParameter('ccc', ['foobar']);
580+
581+
$container
582+
->register('foobar', BarMethodCall::class)
583+
->addMethodCall('setArray', ['%ccc%']);
584+
585+
(new CheckTypeDeclarationsPass(true))->process($container);
586+
587+
$this->addToAssertionCount(1);
588+
}
589+
590+
public function testProcessResolveExpressions()
591+
{
592+
$container = new ContainerBuilder();
593+
$container->setParameter('ccc', ['array']);
594+
595+
$container
596+
->register('foobar', BarMethodCall::class)
597+
->addMethodCall('setArray', [new Expression("parameter('ccc')")]);
598+
599+
(new CheckTypeDeclarationsPass(true))->process($container);
600+
601+
$this->addToAssertionCount(1);
602+
}
603+
604+
public function testProcessHandleMixedEnvPlaceholder()
605+
{
606+
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
607+
$this->expectExceptionMessage('Invalid definition for service "foobar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "string" passed.');
608+
609+
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
610+
'ccc' => '%env(FOO)%',
611+
]));
612+
613+
$container
614+
->register('foobar', BarMethodCall::class)
615+
->addMethodCall('setArray', ['foo%ccc%']);
616+
617+
(new CheckTypeDeclarationsPass(true))->process($container);
618+
}
619+
620+
public function testProcessHandleMultipleEnvPlaceholder()
621+
{
622+
$this->expectException(\Symfony\Component\DependencyInjection\Exception\InvalidArgumentException::class);
623+
$this->expectExceptionMessage('Invalid definition for service "foobar": argument 1 of "Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall::setArray" accepts "array", "string" passed.');
624+
625+
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
626+
'ccc' => '%env(FOO)%',
627+
'fcy' => '%env(int:BAR)%',
628+
]));
629+
630+
$container
631+
->register('foobar', BarMethodCall::class)
632+
->addMethodCall('setArray', ['%ccc%%fcy%']);
633+
634+
(new CheckTypeDeclarationsPass(true))->process($container);
635+
}
636+
637+
public function testProcessHandleExistingEnvPlaceholder()
638+
{
639+
putenv('ARRAY={"foo":"bar"}');
640+
641+
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
642+
'ccc' => '%env(json:ARRAY)%',
643+
]));
644+
645+
$container
646+
->register('foobar', BarMethodCall::class)
647+
->addMethodCall('setArray', ['%ccc%']);
648+
649+
(new ResolveParameterPlaceHoldersPass())->process($container);
650+
(new CheckTypeDeclarationsPass(true))->process($container);
651+
652+
$this->addToAssertionCount(1);
653+
654+
putenv('ARRAY=');
655+
}
656+
657+
public function testProcessHandleNotFoundEnvPlaceholder()
658+
{
659+
$container = new ContainerBuilder(new EnvPlaceholderParameterBag([
660+
'ccc' => '%env(json:ARRAY)%',
661+
]));
662+
663+
$container
664+
->register('foobar', BarMethodCall::class)
665+
->addMethodCall('setArray', ['%ccc%']);
666+
667+
(new ResolveParameterPlaceHoldersPass())->process($container);
668+
(new CheckTypeDeclarationsPass(true))->process($container);
669+
670+
$this->addToAssertionCount(1);
671+
}
572672
}

Tests/Compiler/ResolveBindingsPassTest.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ public function testTypedReferenceSupport()
8484
{
8585
$container = new ContainerBuilder();
8686

87-
$bindings = [CaseSensitiveClass::class => new BoundArgument(new Reference('foo'))];
87+
$bindings = [
88+
CaseSensitiveClass::class => new BoundArgument(new Reference('foo')),
89+
CaseSensitiveClass::class.' $c' => new BoundArgument(new Reference('bar')),
90+
];
8891

8992
// Explicit service id
9093
$definition1 = $container->register('def1', NamedArgumentsDummy::class);
@@ -95,11 +98,16 @@ public function testTypedReferenceSupport()
9598
$definition2->addArgument(new TypedReference(CaseSensitiveClass::class, CaseSensitiveClass::class));
9699
$definition2->setBindings($bindings);
97100

101+
$definition3 = $container->register('def3', NamedArgumentsDummy::class);
102+
$definition3->addArgument(new TypedReference(CaseSensitiveClass::class, CaseSensitiveClass::class, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, 'c'));
103+
$definition3->setBindings($bindings);
104+
98105
$pass = new ResolveBindingsPass();
99106
$pass->process($container);
100107

101108
$this->assertEquals([$typedRef], $container->getDefinition('def1')->getArguments());
102109
$this->assertEquals([new Reference('foo')], $container->getDefinition('def2')->getArguments());
110+
$this->assertEquals([new Reference('bar')], $container->getDefinition('def3')->getArguments());
103111
}
104112

105113
public function testScalarSetter()

0 commit comments

Comments
 (0)