diff --git a/Bundle/Bundle.php b/Bundle/Bundle.php index e8057737ed..2e65f67c9d 100644 --- a/Bundle/Bundle.php +++ b/Bundle/Bundle.php @@ -69,7 +69,7 @@ public function getContainerExtension() if (null !== $extension) { if (!$extension instanceof ExtensionInterface) { - throw new \LogicException(sprintf('Extension "%s" must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', \get_class($extension))); + throw new \LogicException(sprintf('Extension "%s" must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', get_debug_type($extension))); } // check naming convention diff --git a/CHANGELOG.md b/CHANGELOG.md index adcbe66f66..b74c4b8757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,24 @@ CHANGELOG ========= +5.1.0 +----- + + * allowed to use a specific logger channel for deprecations + * made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+; + not returning an array is deprecated + * made kernels implementing `WarmableInterface` be part of the cache warmup stage + * deprecated support for `service:action` syntax to reference controllers, use `serviceOrFqcn::method` instead + * allowed using public aliases to reference controllers + * added session usage reporting when the `_stateless` attribute of the request is set to `true` + * added `AbstractSessionListener::onSessionUsage()` to report when the session is used while a request is stateless + 5.0.0 ----- * removed support for getting the container from a non-booted kernel - * removed the first and second constructor argument of `ConfigDataCollector` - * removed `ConfigDataCollector::getApplicationName()` + * removed the first and second constructor argument of `ConfigDataCollector` + * removed `ConfigDataCollector::getApplicationName()` * removed `ConfigDataCollector::getApplicationVersion()` * removed support for `Symfony\Component\Templating\EngineInterface` in `HIncludeFragmentRenderer`, use a `Twig\Environment` only * removed `TranslatorListener` in favor of `LocaleAwareListener` @@ -18,7 +30,7 @@ CHANGELOG * removed `GetResponseForControllerResultEvent`, use `ViewEvent` instead * removed `GetResponseForExceptionEvent`, use `ExceptionEvent` instead * removed `PostResponseEvent`, use `TerminateEvent` instead - * removed `SaveSessionListener` in favor of `AbstractSessionListener` + * removed `SaveSessionListener` in favor of `AbstractSessionListener` * removed `Client`, use `HttpKernelBrowser` instead * added method `getProjectDir()` to `KernelInterface` * removed methods `serialize` and `unserialize` from `DataCollector`, store the serialized state in the data property instead diff --git a/CacheWarmer/CacheWarmerAggregate.php b/CacheWarmer/CacheWarmerAggregate.php index 1b3d73b7e1..f89b1cd7a0 100644 --- a/CacheWarmer/CacheWarmerAggregate.php +++ b/CacheWarmer/CacheWarmerAggregate.php @@ -45,6 +45,8 @@ public function enableOnlyOptionalWarmers() /** * Warms up the cache. + * + * @return string[] A list of classes or files to preload on PHP 7.4+ */ public function warmUp(string $cacheDir) { @@ -83,6 +85,7 @@ public function warmUp(string $cacheDir) }); } + $preload = []; try { foreach ($this->warmers as $warmer) { if (!$this->optionalsEnabled && $warmer->isOptional()) { @@ -92,7 +95,7 @@ public function warmUp(string $cacheDir) continue; } - $warmer->warmUp($cacheDir); + $preload[] = array_values((array) $warmer->warmUp($cacheDir)); } } finally { if ($collectDeprecations) { @@ -106,6 +109,8 @@ public function warmUp(string $cacheDir) file_put_contents($this->deprecationLogsFilepath, serialize(array_values($collectedLogs))); } } + + return array_values(array_unique(array_merge([], ...$preload))); } /** diff --git a/CacheWarmer/WarmableInterface.php b/CacheWarmer/WarmableInterface.php index e7715a33f6..2f442cb536 100644 --- a/CacheWarmer/WarmableInterface.php +++ b/CacheWarmer/WarmableInterface.php @@ -20,6 +20,8 @@ interface WarmableInterface { /** * Warms up the cache. + * + * @return string[] A list of classes or files to preload on PHP 7.4+ */ public function warmUp(string $cacheDir); } diff --git a/Controller/ArgumentResolver.php b/Controller/ArgumentResolver.php index 0c14a1da69..4285ba7631 100644 --- a/Controller/ArgumentResolver.php +++ b/Controller/ArgumentResolver.php @@ -62,7 +62,7 @@ public function getArguments(Request $request, callable $controller): array } if (!$atLeastOne) { - throw new \InvalidArgumentException(sprintf('"%s::resolve()" must yield at least one value.', \get_class($resolver))); + throw new \InvalidArgumentException(sprintf('"%s::resolve()" must yield at least one value.', get_debug_type($resolver))); } // continue to the next controller argument diff --git a/Controller/ArgumentResolver/VariadicValueResolver.php b/Controller/ArgumentResolver/VariadicValueResolver.php index ed61420e67..a8f7e0f440 100644 --- a/Controller/ArgumentResolver/VariadicValueResolver.php +++ b/Controller/ArgumentResolver/VariadicValueResolver.php @@ -38,7 +38,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $values = $request->attributes->get($argument->getName()); if (!\is_array($values)) { - throw new \InvalidArgumentException(sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), \gettype($values))); + throw new \InvalidArgumentException(sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); } yield from $values; diff --git a/Controller/ContainerControllerResolver.php b/Controller/ContainerControllerResolver.php index 6e4ae20c34..3b9468465c 100644 --- a/Controller/ContainerControllerResolver.php +++ b/Controller/ContainerControllerResolver.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\Container; /** - * A controller resolver searching for a controller in a psr-11 container when using the "service:method" notation. + * A controller resolver searching for a controller in a psr-11 container when using the "service::method" notation. * * @author Fabien Potencier * @author Maxime Steinhausser @@ -36,7 +36,7 @@ protected function createController(string $controller) { if (1 === substr_count($controller, ':')) { $controller = str_replace(':', '::', $controller); - // TODO deprecate this in 5.1 + trigger_deprecation('symfony/http-kernel', '5.1', 'Referencing controllers with a single colon is deprecated. Use "%s" instead.', $controller); } return parent::createController($controller); diff --git a/Controller/ControllerResolver.php b/Controller/ControllerResolver.php index 1f8354291b..cffe45904d 100644 --- a/Controller/ControllerResolver.php +++ b/Controller/ControllerResolver.php @@ -64,7 +64,7 @@ public function getController(Request $request) } if (!\is_callable($controller)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s.', $request->getPathInfo(), $this->getControllerError($controller))); + throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $controller; @@ -72,7 +72,7 @@ public function getController(Request $request) if (\is_object($controller)) { if (!\is_callable($controller)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s.', $request->getPathInfo(), $this->getControllerError($controller))); + throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $controller; @@ -85,11 +85,11 @@ public function getController(Request $request) try { $callable = $this->createController($controller); } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s.', $request->getPathInfo(), $e->getMessage()), 0, $e); + throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$e->getMessage(), 0, $e); } if (!\is_callable($callable)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s.', $request->getPathInfo(), $this->getControllerError($callable))); + throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($callable)); } return $callable; @@ -161,11 +161,11 @@ private function getControllerError($callable): string $availableMethods = $this->getClassMethodsWithoutMagicMethods($callable); $alternativeMsg = $availableMethods ? sprintf(' or use one of the available methods: "%s"', implode('", "', $availableMethods)) : ''; - return sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', \get_class($callable), $alternativeMsg); + return sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', get_debug_type($callable), $alternativeMsg); } if (!\is_array($callable)) { - return sprintf('Invalid type for controller given, expected string, array or object, got "%s".', \gettype($callable)); + return sprintf('Invalid type for controller given, expected string, array or object, got "%s".', get_debug_type($callable)); } if (!isset($callable[0]) || !isset($callable[1]) || 2 !== \count($callable)) { @@ -178,7 +178,7 @@ private function getControllerError($callable): string return sprintf('Class "%s" does not exist.', $controller); } - $className = \is_object($controller) ? \get_class($controller) : $controller; + $className = \is_object($controller) ? get_debug_type($controller) : $controller; if (method_exists($controller, $method)) { return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className); diff --git a/ControllerMetadata/ArgumentMetadataFactory.php b/ControllerMetadata/ArgumentMetadataFactory.php index 9370174c25..05a68229a3 100644 --- a/ControllerMetadata/ArgumentMetadataFactory.php +++ b/ControllerMetadata/ArgumentMetadataFactory.php @@ -48,7 +48,7 @@ private function getType(\ReflectionParameter $parameter, \ReflectionFunctionAbs if (!$type = $parameter->getType()) { return null; } - $name = $type->getName(); + $name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; if ($function instanceof \ReflectionMethod) { $lcName = strtolower($name); diff --git a/DataCollector/ConfigDataCollector.php b/DataCollector/ConfigDataCollector.php index 1e435cd21d..0632f92a18 100644 --- a/DataCollector/ConfigDataCollector.php +++ b/DataCollector/ConfigDataCollector.php @@ -49,7 +49,7 @@ public function collect(Request $request, Response $response, \Throwable $except 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', 'php_version' => PHP_VERSION, - 'php_architecture' => PHP_INT_SIZE * 8, + 'php_architecture' => \PHP_INT_SIZE * 8, 'php_intl_locale' => class_exists('Locale', false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', 'php_timezone' => date_default_timezone_get(), 'xdebug_enabled' => \extension_loaded('xdebug'), diff --git a/DataCollector/DumpDataCollector.php b/DataCollector/DumpDataCollector.php index dbffdca24d..ba45a2ddce 100644 --- a/DataCollector/DumpDataCollector.php +++ b/DataCollector/DumpDataCollector.php @@ -230,13 +230,7 @@ public function __destruct() --$i; } - if (isset($_SERVER['VAR_DUMPER_FORMAT'])) { - $html = 'html' === $_SERVER['VAR_DUMPER_FORMAT']; - } else { - $html = !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && stripos($h[$i], 'html'); - } - - if ($html) { + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && stripos($h[$i], 'html')) { $dumper = new HtmlDumper('php://output', $this->charset); $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } else { diff --git a/DataCollector/LoggerDataCollector.php b/DataCollector/LoggerDataCollector.php index f8dc8859ef..34c6dc4297 100644 --- a/DataCollector/LoggerDataCollector.php +++ b/DataCollector/LoggerDataCollector.php @@ -177,7 +177,7 @@ private function sanitizeLogs(array $logs) continue; } - $message = $log['message']; + $message = '_'.$log['message']; $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { diff --git a/DataCollector/RequestDataCollector.php b/DataCollector/RequestDataCollector.php index 9585c130f0..b92518d806 100644 --- a/DataCollector/RequestDataCollector.php +++ b/DataCollector/RequestDataCollector.php @@ -400,7 +400,7 @@ protected function parseController($controller) $r = new \ReflectionMethod($controller[0], $controller[1]); return [ - 'class' => \is_object($controller[0]) ? \get_class($controller[0]) : $controller[0], + 'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0], 'method' => $controller[1], 'file' => $r->getFileName(), 'line' => $r->getStartLine(), @@ -409,7 +409,7 @@ protected function parseController($controller) if (\is_callable($controller)) { // using __call or __callStatic return [ - 'class' => \is_object($controller[0]) ? \get_class($controller[0]) : $controller[0], + 'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0], 'method' => $controller[1], 'file' => 'n/a', 'line' => 'n/a', diff --git a/DependencyInjection/AddAnnotatedClassesToCachePass.php b/DependencyInjection/AddAnnotatedClassesToCachePass.php index 70a987ebf4..5eb833b51d 100644 --- a/DependencyInjection/AddAnnotatedClassesToCachePass.php +++ b/DependencyInjection/AddAnnotatedClassesToCachePass.php @@ -127,10 +127,10 @@ private function patternsToRegexps(array $patterns): array private function matchAnyRegexps(string $class, array $regexps): bool { - $blacklisted = false !== strpos($class, 'Test'); + $isTest = false !== strpos($class, 'Test'); foreach ($regexps as $regex) { - if ($blacklisted && false === strpos($regex, 'Test')) { + if ($isTest && false === strpos($regex, 'Test')) { continue; } diff --git a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index a3f5012e32..82d45a8dcf 100644 --- a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -53,6 +53,13 @@ public function process(ContainerBuilder $container) $parameterBag = $container->getParameterBag(); $controllers = []; + $publicAliases = []; + foreach ($container->getAliases() as $id => $alias) { + if ($alias->isPublic() && !$alias->isPrivate()) { + $publicAliases[(string) $alias][] = $id; + } + } + foreach ($container->findTaggedServiceIds($this->controllerTag, true) as $id => $tags) { $def = $container->getDefinition($id); $def->setPublic(true); @@ -170,15 +177,22 @@ public function process(ContainerBuilder $container) $message .= ' Did you forget to add a use statement?'; } - throw new InvalidArgumentException($message); - } + $container->register($erroredId = '.errored.'.$container->hash($message), $type) + ->addError($message); - $target = ltrim($target, '\\'); - $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior); + $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); + } else { + $target = ltrim($target, '\\'); + $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior); + } } // register the maps as a per-method service-locators if ($args) { $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args); + + foreach ($publicAliases[$id] ?? [] as $alias) { + $controllers[$alias.'::'.$r->name] = clone $controllers[$id.'::'.$r->name]; + } } } } diff --git a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php index cc11091109..f69d37619e 100644 --- a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php @@ -43,6 +43,11 @@ public function process(ContainerBuilder $container) // any methods listed for call-at-instantiation cannot be actions $reason = false; list($id, $action) = explode('::', $controller); + + if ($container->hasAlias($id)) { + continue; + } + $controllerDef = $container->getDefinition($id); foreach ($controllerDef->getMethodCalls() as list($method)) { if (0 === strcasecmp($action, $method)) { diff --git a/EventListener/AbstractSessionListener.php b/EventListener/AbstractSessionListener.php index 9c9c422e4e..1fe3264f7d 100644 --- a/EventListener/AbstractSessionListener.php +++ b/EventListener/AbstractSessionListener.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\KernelEvents; /** @@ -41,10 +42,12 @@ abstract class AbstractSessionListener implements EventSubscriberInterface protected $container; private $sessionUsageStack = []; + private $debug; - public function __construct(ContainerInterface $container = null) + public function __construct(ContainerInterface $container = null, bool $debug = false) { $this->container = $container; + $this->debug = $debug; } public function onKernelRequest(RequestEvent $event) @@ -82,16 +85,6 @@ public function onKernelResponse(ResponseEvent $event) return; } - if ($session instanceof Session ? $session->getUsageIndex() !== end($this->sessionUsageStack) : $session->isStarted()) { - if ($autoCacheControl) { - $response - ->setExpires(new \DateTime()) - ->setPrivate() - ->setMaxAge(0) - ->headers->addCacheControlDirective('must-revalidate'); - } - } - if ($session->isStarted()) { /* * Saves the session, in case it is still open, before sending the response/headers. @@ -120,6 +113,30 @@ public function onKernelResponse(ResponseEvent $event) */ $session->save(); } + + if ($session instanceof Session ? $session->getUsageIndex() === end($this->sessionUsageStack) : !$session->isStarted()) { + return; + } + + if ($autoCacheControl) { + $response + ->setExpires(new \DateTime()) + ->setPrivate() + ->setMaxAge(0) + ->headers->addCacheControlDirective('must-revalidate'); + } + + if (!$event->getRequest()->attributes->get('_stateless', false)) { + return; + } + + if ($this->debug) { + throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.'); + } + + if ($this->container->has('logger')) { + $this->container->get('logger')->warning('Session was used while the request was declared stateless.'); + } } public function onFinishRequest(FinishRequestEvent $event) @@ -129,6 +146,37 @@ public function onFinishRequest(FinishRequestEvent $event) } } + public function onSessionUsage(): void + { + if (!$this->debug) { + return; + } + + if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) { + return; + } + + $stateless = false; + $clonedRequestStack = clone $requestStack; + while (null !== ($request = $clonedRequestStack->pop()) && !$stateless) { + $stateless = $request->attributes->get('_stateless'); + } + + if (!$stateless) { + return; + } + + if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $requestStack->getCurrentRequest()->getSession()) { + return; + } + + if ($session->isStarted()) { + $session->save(); + } + + throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.'); + } + public static function getSubscribedEvents(): array { return [ diff --git a/EventListener/DebugHandlersListener.php b/EventListener/DebugHandlersListener.php index 5efd762171..44b1ddb187 100644 --- a/EventListener/DebugHandlersListener.php +++ b/EventListener/DebugHandlersListener.php @@ -32,6 +32,7 @@ class DebugHandlersListener implements EventSubscriberInterface { private $exceptionHandler; private $logger; + private $deprecationLogger; private $levels; private $throwAt; private $scream; @@ -48,7 +49,7 @@ class DebugHandlersListener implements EventSubscriberInterface * @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files * @param bool $scope Enables/disables scoping mode */ - public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true) + public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, LoggerInterface $deprecationLogger = null) { $this->exceptionHandler = $exceptionHandler; $this->logger = $logger; @@ -57,6 +58,7 @@ public function __construct(callable $exceptionHandler = null, LoggerInterface $ $this->scream = $scream; $this->fileLinkFormat = $fileLinkFormat; $this->scope = $scope; + $this->deprecationLogger = $deprecationLogger; } /** @@ -76,31 +78,30 @@ public function configure(object $event = null) $handler = \is_array($handler) ? $handler[0] : null; restore_exception_handler(); - if ($this->logger || null !== $this->throwAt) { - if ($handler instanceof ErrorHandler) { - if ($this->logger) { - $handler->setDefaultLogger($this->logger, $this->levels); - if (\is_array($this->levels)) { - $levels = 0; - foreach ($this->levels as $type => $log) { - $levels |= $type; - } - } else { - $levels = $this->levels; - } - if ($this->scream) { - $handler->screamAt($levels); + if ($handler instanceof ErrorHandler) { + if ($this->logger || $this->deprecationLogger) { + $this->setDefaultLoggers($handler); + if (\is_array($this->levels)) { + $levels = 0; + foreach ($this->levels as $type => $log) { + $levels |= $type; } - if ($this->scope) { - $handler->scopeAt($levels & ~E_USER_DEPRECATED & ~E_DEPRECATED); - } else { - $handler->scopeAt(0, true); - } - $this->logger = $this->levels = null; + } else { + $levels = $this->levels; + } + + if ($this->scream) { + $handler->screamAt($levels); } - if (null !== $this->throwAt) { - $handler->throwAt($this->throwAt, true); + if ($this->scope) { + $handler->scopeAt($levels & ~E_USER_DEPRECATED & ~E_DEPRECATED); + } else { + $handler->scopeAt(0, true); } + $this->logger = $this->deprecationLogger = $this->levels = null; + } + if (null !== $this->throwAt) { + $handler->throwAt($this->throwAt, true); } } if (!$this->exceptionHandler) { @@ -123,15 +124,7 @@ public function configure(object $event = null) $output = $output->getErrorOutput(); } $this->exceptionHandler = static function (\Throwable $e) use ($app, $output) { - if (method_exists($app, 'renderThrowable')) { - $app->renderThrowable($e, $output); - } else { - if (!$e instanceof \Exception) { - $e = new FatalThrowableError($e); - } - - $app->renderException($e, $output); - } + $app->renderThrowable($e, $output); }; } } @@ -143,6 +136,34 @@ public function configure(object $event = null) } } + private function setDefaultLoggers(ErrorHandler $handler): void + { + if (\is_array($this->levels)) { + $levelsDeprecatedOnly = []; + $levelsWithoutDeprecated = []; + foreach ($this->levels as $type => $log) { + if (E_DEPRECATED == $type || E_USER_DEPRECATED == $type) { + $levelsDeprecatedOnly[$type] = $log; + } else { + $levelsWithoutDeprecated[$type] = $log; + } + } + } else { + $levelsDeprecatedOnly = $this->levels & (E_DEPRECATED | E_USER_DEPRECATED); + $levelsWithoutDeprecated = $this->levels & ~E_DEPRECATED & ~E_USER_DEPRECATED; + } + + $defaultLoggerLevels = $this->levels; + if ($this->deprecationLogger && $levelsDeprecatedOnly) { + $handler->setDefaultLogger($this->deprecationLogger, $levelsDeprecatedOnly); + $defaultLoggerLevels = $levelsWithoutDeprecated; + } + + if ($this->logger && $defaultLoggerLevels) { + $handler->setDefaultLogger($this->logger, $defaultLoggerLevels); + } + } + public static function getSubscribedEvents(): array { $events = [KernelEvents::REQUEST => ['configure', 2048]]; diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php index 26c361f754..f5cac76bd8 100644 --- a/EventListener/ErrorListener.php +++ b/EventListener/ErrorListener.php @@ -14,11 +14,11 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -33,7 +33,7 @@ class ErrorListener implements EventSubscriberInterface protected $logger; protected $debug; - public function __construct($controller, LoggerInterface $logger = null, $debug = false) + public function __construct($controller, LoggerInterface $logger = null, bool $debug = false) { $this->controller = $controller; $this->logger = $logger; @@ -47,7 +47,7 @@ public function logKernelException(ExceptionEvent $event) $this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine())); } - public function onKernelException(ExceptionEvent $event, string $eventName = null, EventDispatcherInterface $eventDispatcher = null) + public function onKernelException(ExceptionEvent $event) { if (null === $this->controller) { return; @@ -79,12 +79,15 @@ public function onKernelException(ExceptionEvent $event, string $eventName = nul $event->setResponse($response); - if ($this->debug && $eventDispatcher instanceof EventDispatcherInterface) { - $cspRemovalListener = function ($event) use (&$cspRemovalListener, $eventDispatcher) { - $event->getResponse()->headers->remove('Content-Security-Policy'); - $eventDispatcher->removeListener(KernelEvents::RESPONSE, $cspRemovalListener); - }; - $eventDispatcher->addListener(KernelEvents::RESPONSE, $cspRemovalListener, -128); + if ($this->debug) { + $event->getRequest()->attributes->set('_remove_csp_headers', true); + } + } + + public function removeCspHeader(ResponseEvent $event): void + { + if ($this->debug && $event->getRequest()->attributes->get('_remove_csp_headers', false)) { + $event->getResponse()->headers->remove('Content-Security-Policy'); } } @@ -99,7 +102,7 @@ public function onControllerArguments(ControllerArgumentsEvent $event) $r = new \ReflectionFunction(\Closure::fromCallable($event->getController())); $r = $r->getParameters()[$k] ?? null; - if ($r && (!$r->hasType() || \in_array($r->getType()->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { + if ($r && (!($r = $r->getType()) instanceof \ReflectionNamedType || \in_array($r->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); @@ -114,6 +117,7 @@ public static function getSubscribedEvents(): array ['logKernelException', 0], ['onKernelException', -128], ], + KernelEvents::RESPONSE => ['removeCspHeader', -128], ]; } diff --git a/EventListener/FragmentListener.php b/EventListener/FragmentListener.php index afe5cb163d..14c6aa63d1 100644 --- a/EventListener/FragmentListener.php +++ b/EventListener/FragmentListener.php @@ -83,8 +83,7 @@ protected function validateRequest(Request $request) } // is the Request signed? - // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) - if ($this->signer->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().(null !== ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''))) { + if ($this->signer->checkRequest($request)) { return; } diff --git a/EventListener/SessionListener.php b/EventListener/SessionListener.php index a53ade797c..e982a795b2 100644 --- a/EventListener/SessionListener.php +++ b/EventListener/SessionListener.php @@ -28,9 +28,9 @@ */ class SessionListener extends AbstractSessionListener { - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container, bool $debug = false) { - $this->container = $container; + parent::__construct($container, $debug); } protected function getSession(): ?SessionInterface diff --git a/Exception/UnexpectedSessionUsageException.php b/Exception/UnexpectedSessionUsageException.php new file mode 100644 index 0000000000..0145b1691c --- /dev/null +++ b/Exception/UnexpectedSessionUsageException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Exception; + +/** + * @author Mathias Arlaud + */ +class UnexpectedSessionUsageException extends \LogicException +{ +} diff --git a/HttpCache/Store.php b/HttpCache/Store.php index b7953e1303..22a03f261f 100644 --- a/HttpCache/Store.php +++ b/HttpCache/Store.php @@ -150,8 +150,8 @@ public function lookup(Request $request) } $headers = $match[1]; - if (file_exists($body = $this->getPath($headers['x-content-digest'][0]))) { - return $this->restoreResponse($headers, $body); + if (file_exists($path = $this->getPath($headers['x-content-digest'][0]))) { + return $this->restoreResponse($headers, $path); } // TODO the metaStore referenced an entity that doesn't exist in @@ -175,16 +175,25 @@ public function write(Request $request, Response $response) $key = $this->getCacheKey($request); $storedEnv = $this->persistRequest($request); - // write the response body to the entity store if this is the original response - if (!$response->headers->has('X-Content-Digest')) { + if ($response->headers->has('X-Body-File')) { + // Assume the response came from disk, but at least perform some safeguard checks + if (!$response->headers->has('X-Content-Digest')) { + throw new \RuntimeException('A restored response must have the X-Content-Digest header.'); + } + + $digest = $response->headers->get('X-Content-Digest'); + if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) { + throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.'); + } + // Everything seems ok, omit writing content to disk + } else { $digest = $this->generateContentDigest($response); + $response->headers->set('X-Content-Digest', $digest); - if (!$this->save($digest, $response->getContent())) { + if (!$this->save($digest, $response->getContent(), false)) { throw new \RuntimeException('Unable to store the entity.'); } - $response->headers->set('X-Content-Digest', $digest); - if (!$response->headers->has('Transfer-Encoding')) { $response->headers->set('Content-Length', \strlen($response->getContent())); } @@ -344,10 +353,14 @@ private function load(string $key): ?string /** * Save data for the given key. */ - private function save(string $key, string $data): bool + private function save(string $key, string $data, bool $overwrite = true): bool { $path = $this->getPath($key); + if (!$overwrite && file_exists($path)) { + return true; + } + if (isset($this->locks[$key])) { $fp = $this->locks[$key]; @ftruncate($fp, 0); @@ -446,15 +459,15 @@ private function persistResponse(Response $response): array /** * Restores a Response from the HTTP headers and body. */ - private function restoreResponse(array $headers, string $body = null): Response + private function restoreResponse(array $headers, string $path = null): Response { $status = $headers['X-Status'][0]; unset($headers['X-Status']); - if (null !== $body) { - $headers['X-Body-File'] = [$body]; + if (null !== $path) { + $headers['X-Body-File'] = [$path]; } - return new Response($body, $status, $headers); + return new Response($path, $status, $headers); } } diff --git a/HttpClientKernel.php b/HttpClientKernel.php index 4d20e96916..a5e207d546 100644 --- a/HttpClientKernel.php +++ b/HttpClientKernel.php @@ -58,6 +58,10 @@ public function handle(Request $request, int $type = HttpKernelInterface::MASTER $response = new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch)); + $response->headers->remove('X-Body-File'); + $response->headers->remove('X-Body-Eval'); + $response->headers->remove('X-Content-Digest'); + $response->headers = new class($response->headers->all()) extends ResponseHeaderBag { protected function computeCacheControlValue(): string { diff --git a/Kernel.php b/Kernel.php index d03d4e1d0a..c90c77ae77 100644 --- a/Kernel.php +++ b/Kernel.php @@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; +use Symfony\Component\DependencyInjection\Dumper\Preloader; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; @@ -34,10 +35,14 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\AddAnnotatedClassesToCachePass; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; +// Help opcache.preload discover always-needed symbols +class_exists(ConfigCache::class); + /** * The Kernel is the heart of the Symfony system. * @@ -68,15 +73,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '5.0.7'; - const VERSION_ID = 50007; + const VERSION = '5.1.5'; + const VERSION_ID = 50105; const MAJOR_VERSION = 5; - const MINOR_VERSION = 0; - const RELEASE_VERSION = 7; + const MINOR_VERSION = 1; + const RELEASE_VERSION = 5; const EXTRA_VERSION = ''; - const END_OF_MAINTENANCE = '07/2020'; - const END_OF_LIFE = '07/2020'; + const END_OF_MAINTENANCE = '01/2021'; + const END_OF_LIFE = '01/2021'; public function __construct(string $environment, bool $debug) { @@ -218,10 +223,7 @@ public function getBundles() public function getBundle(string $name) { if (!isset($this->bundles[$name])) { - $class = static::class; - $class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; - - throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, $class)); + throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, get_debug_type($this))); } return $this->bundles[$name]; @@ -280,12 +282,12 @@ public function getProjectDir() if (null === $this->projectDir) { $r = new \ReflectionObject($this); - if (!file_exists($dir = $r->getFileName())) { + if (!is_file($dir = $r->getFileName())) { throw new \LogicException(sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name)); } $dir = $rootDir = \dirname($dir); - while (!file_exists($dir.'/composer.json')) { + while (!is_file($dir.'/composer.json')) { if ($dir === \dirname($dir)) { return $this->projectDir = $rootDir; } @@ -394,8 +396,9 @@ protected function build(ContainerBuilder $container) protected function getContainerClass() { $class = static::class; - $class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class; + $class = false !== strpos($class, "@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class; $class = str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container'; + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) { throw new \InvalidArgumentException(sprintf('The environment "%s" contains invalid characters, it can only contain characters allowed in PHP class names.', $this->environment)); } @@ -432,7 +435,7 @@ protected function initializeContainer() $errorLevel = error_reporting(E_ALL ^ E_WARNING); try { - if (file_exists($cachePath) && \is_object($this->container = include $cachePath) + if (is_file($cachePath) && \is_object($this->container = include $cachePath) && (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh())) ) { self::$freshCache[$cachePath] = true; @@ -491,6 +494,18 @@ protected function initializeContainer() break; } } + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (!isset($backtrace[$i]['file'], $backtrace[$i]['line'], $backtrace[$i]['function'])) { + continue; + } + if (!isset($backtrace[$i]['class']) && 'trigger_deprecation' === $backtrace[$i]['function']) { + $file = $backtrace[$i]['file']; + $line = $backtrace[$i]['line']; + $backtrace = \array_slice($backtrace, 1 + $i); + break; + } + } + // Remove frames added by DebugClassLoader. for ($i = \count($backtrace) - 2; 0 < $i; --$i) { if (\in_array($backtrace[$i]['class'] ?? null, [DebugClassLoader::class, LegacyDebugClassLoader::class], true)) { @@ -551,8 +566,14 @@ protected function initializeContainer() touch($oldContainerDir.'.legacy'); } + $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($this->container->getParameter('kernel.cache_dir')) : []; + if ($this->container->has('cache_warmer')) { - $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir')); + $preload = array_merge($preload, (array) $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'))); + } + + if ($preload && method_exists(Preloader::class, 'append') && file_exists($preloadFile = $cacheDir.'/'.$class.'.preload.php')) { + Preloader::append($preloadFile, $preload); } } diff --git a/KernelEvents.php b/KernelEvents.php index 682561c324..0e1c9083e5 100644 --- a/KernelEvents.php +++ b/KernelEvents.php @@ -39,17 +39,6 @@ final class KernelEvents */ const EXCEPTION = 'kernel.exception'; - /** - * The VIEW event occurs when the return value of a controller - * is not a Response instance. - * - * This event allows you to create a response for the return value of the - * controller. - * - * @Event("Symfony\Component\HttpKernel\Event\ViewEvent") - */ - const VIEW = 'kernel.view'; - /** * The CONTROLLER event occurs once a controller was found for * handling a request. @@ -71,6 +60,17 @@ final class KernelEvents */ const CONTROLLER_ARGUMENTS = 'kernel.controller_arguments'; + /** + * The VIEW event occurs when the return value of a controller + * is not a Response instance. + * + * This event allows you to create a response for the return value of the + * controller. + * + * @Event("Symfony\Component\HttpKernel\Event\ViewEvent") + */ + const VIEW = 'kernel.view'; + /** * The RESPONSE event occurs once a response was created for * replying to a request. @@ -82,15 +82,6 @@ final class KernelEvents */ const RESPONSE = 'kernel.response'; - /** - * The TERMINATE event occurs once a response was sent. - * - * This event allows you to run expensive post-response jobs. - * - * @Event("Symfony\Component\HttpKernel\Event\TerminateEvent") - */ - const TERMINATE = 'kernel.terminate'; - /** * The FINISH_REQUEST event occurs when a response was generated for a request. * @@ -100,4 +91,13 @@ final class KernelEvents * @Event("Symfony\Component\HttpKernel\Event\FinishRequestEvent") */ const FINISH_REQUEST = 'kernel.finish_request'; + + /** + * The TERMINATE event occurs once a response was sent. + * + * This event allows you to run expensive post-response jobs. + * + * @Event("Symfony\Component\HttpKernel\Event\TerminateEvent") + */ + const TERMINATE = 'kernel.terminate'; } diff --git a/Log/Logger.php b/Log/Logger.php index 6ea3b464f0..1e6308d6e0 100644 --- a/Log/Logger.php +++ b/Log/Logger.php @@ -37,10 +37,10 @@ class Logger extends AbstractLogger private $formatter; private $handle; - public function __construct(string $minLevel = null, $output = 'php://stderr', callable $formatter = null) + public function __construct(string $minLevel = null, $output = null, callable $formatter = null) { if (null === $minLevel) { - $minLevel = 'php://stdout' === $output || 'php://stderr' === $output ? LogLevel::CRITICAL : LogLevel::WARNING; + $minLevel = null === $output || 'php://stdout' === $output || 'php://stderr' === $output ? LogLevel::ERROR : LogLevel::WARNING; if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) { switch ((int) (isset($_ENV['SHELL_VERBOSITY']) ? $_ENV['SHELL_VERBOSITY'] : $_SERVER['SHELL_VERBOSITY'])) { @@ -58,7 +58,7 @@ public function __construct(string $minLevel = null, $output = 'php://stderr', c $this->minLevelIndex = self::$levels[$minLevel]; $this->formatter = $formatter ?: [$this, 'format']; - if (false === $this->handle = \is_resource($output) ? $output : @fopen($output, 'a')) { + if ($output && false === $this->handle = \is_resource($output) ? $output : @fopen($output, 'a')) { throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output)); } } @@ -79,10 +79,14 @@ public function log($level, $message, array $context = []) } $formatter = $this->formatter; - fwrite($this->handle, $formatter($level, $message, $context)); + if ($this->handle) { + @fwrite($this->handle, $formatter($level, $message, $context)); + } else { + error_log($formatter($level, $message, $context, false)); + } } - private function format(string $level, string $message, array $context): string + private function format(string $level, string $message, array $context, bool $prefixDate = true): string { if (false !== strpos($message, '{')) { $replacements = []; @@ -101,6 +105,11 @@ private function format(string $level, string $message, array $context): string $message = strtr($message, $replacements); } - return sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message).PHP_EOL; + $log = sprintf('[%s] %s', $level, $message).PHP_EOL; + if ($prefixDate) { + $log = date(\DateTime::RFC3339).' '.$log; + } + + return $log; } } diff --git a/Profiler/Profile.php b/Profiler/Profile.php index c18e9980a1..37b690ed86 100644 --- a/Profiler/Profile.php +++ b/Profiler/Profile.php @@ -101,7 +101,7 @@ public function getIp() return $this->ip; } - public function setIp(string $ip) + public function setIp(?string $ip) { $this->ip = $ip; } @@ -131,7 +131,7 @@ public function getUrl() return $this->url; } - public function setUrl(string $url) + public function setUrl(?string $url) { $this->url = $url; } diff --git a/Resources/welcome.html.php b/Resources/welcome.html.php index e1c0ff1192..b8337dc737 100644 --- a/Resources/welcome.html.php +++ b/Resources/welcome.html.php @@ -65,7 +65,7 @@
- You're seeing this page because you haven't configured any homepage URL. + You're seeing this page because you haven't configured any homepage URL and debug mode is enabled.
diff --git a/Tests/CacheWarmer/CacheWarmerTest.php b/Tests/CacheWarmer/CacheWarmerTest.php index 9cced03a47..eeb39e4dca 100644 --- a/Tests/CacheWarmer/CacheWarmerTest.php +++ b/Tests/CacheWarmer/CacheWarmerTest.php @@ -54,9 +54,14 @@ public function __construct(string $file) $this->file = $file; } + /** + * @return string[] + */ public function warmUp(string $cacheDir) { $this->writeCacheFile($this->file, 'content'); + + return []; } public function isOptional(): bool diff --git a/Tests/Controller/ArgumentResolverTest.php b/Tests/Controller/ArgumentResolverTest.php index d663297121..449bd1ae94 100644 --- a/Tests/Controller/ArgumentResolverTest.php +++ b/Tests/Controller/ArgumentResolverTest.php @@ -204,19 +204,19 @@ public function testGetNullableArguments() $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', new \stdClass()); - $request->attributes->set('mandatory', 'mandatory'); + $request->attributes->set('last', 'last'); $controller = [new NullableController(), 'action']; - $this->assertEquals(['foo', new \stdClass(), 'value', 'mandatory'], self::$resolver->getArguments($request, $controller)); + $this->assertEquals(['foo', new \stdClass(), 'value', 'last'], self::$resolver->getArguments($request, $controller)); } public function testGetNullableArgumentsWithDefaults() { $request = Request::create('/'); - $request->attributes->set('mandatory', 'mandatory'); + $request->attributes->set('last', 'last'); $controller = [new NullableController(), 'action']; - $this->assertEquals([null, null, 'value', 'mandatory'], self::$resolver->getArguments($request, $controller)); + $this->assertEquals([null, null, 'value', 'last'], self::$resolver->getArguments($request, $controller)); } public function testGetSessionArguments() diff --git a/Tests/Controller/ContainerControllerResolverTest.php b/Tests/Controller/ContainerControllerResolverTest.php index c39dac3ca5..d394c3bce4 100644 --- a/Tests/Controller/ContainerControllerResolverTest.php +++ b/Tests/Controller/ContainerControllerResolverTest.php @@ -13,14 +13,22 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; class ContainerControllerResolverTest extends ControllerResolverTest { + use ExpectDeprecationTrait; + + /** + * @group legacy + */ public function testGetControllerServiceWithSingleColon() { + $this->expectDeprecation('Since symfony/http-kernel 5.1: Referencing controllers with a single colon is deprecated. Use "foo::action" instead.'); + $service = new ControllerTestService('foo'); $container = $this->createMockContainer(); @@ -145,7 +153,6 @@ public function getControllers() { return [ ['\\'.ControllerTestService::class.'::action'], - ['\\'.ControllerTestService::class.':action'], ]; } diff --git a/Tests/Controller/ControllerResolverTest.php b/Tests/Controller/ControllerResolverTest.php index 014b21e83a..d5ac4ad5c2 100644 --- a/Tests/Controller/ControllerResolverTest.php +++ b/Tests/Controller/ControllerResolverTest.php @@ -173,17 +173,17 @@ public function getUndefinedControllers() ['foo', \Error::class, 'Class \'foo\' not found'], ['oof::bar', \Error::class, 'Class \'oof\' not found'], [['oof', 'bar'], \Error::class, 'Class \'oof\' not found'], - ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], - ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], - ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], - ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], - ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest', \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], - [[$controller, 'staticsAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], - [[$controller, 'privateAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], - [[$controller, 'protectedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], - [[$controller, 'undefinedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], - [$controller, \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], - [['a' => 'foo', 'b' => 'bar'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable. Invalid array callable, expected [controller, method].'], + ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], + ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], + ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], + ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], + ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], + [[$controller, 'staticsAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], + [[$controller, 'privateAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], + [[$controller, 'protectedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], + [[$controller, 'undefinedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], + [$controller, \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], + [['a' => 'foo', 'b' => 'bar'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Invalid array callable, expected [controller, method].'], ]; } diff --git a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index ba8ab56604..dfab909802 100644 --- a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -80,7 +80,7 @@ public function testSignature5() $this->assertEquals([ new ArgumentMetadata('foo', 'array', false, true, null, true), - new ArgumentMetadata('bar', null, false, false, null), + new ArgumentMetadata('bar', null, false, true, null, true), ], $arguments); } @@ -113,7 +113,7 @@ public function testNullableTypesSignature() new ArgumentMetadata('foo', 'string', false, false, null, true), new ArgumentMetadata('bar', \stdClass::class, false, false, null, true), new ArgumentMetadata('baz', 'string', false, true, 'value', true), - new ArgumentMetadata('mandatory', null, false, false, null, true), + new ArgumentMetadata('last', 'string', false, true, '', false), ], $arguments); } @@ -133,7 +133,7 @@ private function signature4($foo = 'default', $bar = 500, $baz = []) { } - private function signature5(array $foo = null, $bar) + private function signature5(array $foo = null, $bar = null) { } } diff --git a/Tests/DataCollector/ConfigDataCollectorTest.php b/Tests/DataCollector/ConfigDataCollectorTest.php index bf89b877c4..9cb75a6051 100644 --- a/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/Tests/DataCollector/ConfigDataCollectorTest.php @@ -30,9 +30,9 @@ public function testCollect() $this->assertSame('test', $c->getEnv()); $this->assertTrue($c->isDebug()); $this->assertSame('config', $c->getName()); - $this->assertRegExp('~^'.preg_quote($c->getPhpVersion(), '~').'~', PHP_VERSION); - $this->assertRegExp('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', PHP_VERSION); - $this->assertSame(PHP_INT_SIZE * 8, $c->getPhpArchitecture()); + $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', PHP_VERSION); + $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', PHP_VERSION); + $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); $this->assertSame(class_exists('Locale', false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); diff --git a/Tests/DataCollector/LoggerDataCollectorTest.php b/Tests/DataCollector/LoggerDataCollectorTest.php index adfba5d422..9c175397fc 100644 --- a/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/Tests/DataCollector/LoggerDataCollectorTest.php @@ -176,13 +176,15 @@ public function getCollectTestData() [ ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo3', 'context' => ['exception' => new SilencedErrorContext(E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => '0', 'context' => ['exception' => new SilencedErrorContext(E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], ], [ ['message' => 'foo3', 'context' => ['exception' => ['warning', E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo3', 'context' => ['exception' => [E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], + ['message' => '0', 'context' => ['exception' => [E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], ], 0, - 1, + 2, ]; } } diff --git a/Tests/DataCollector/RequestDataCollectorTest.php b/Tests/DataCollector/RequestDataCollectorTest.php index 5753dc88da..a8a6bad61f 100644 --- a/Tests/DataCollector/RequestDataCollectorTest.php +++ b/Tests/DataCollector/RequestDataCollectorTest.php @@ -51,7 +51,7 @@ public function testCollect() $this->assertEquals(['name' => 'foo'], $c->getRouteParams()); $this->assertSame([], $c->getSessionAttributes()); $this->assertSame('en', $c->getLocale()); - $this->assertContains(__FILE__, $attributes->get('resource')); + $this->assertContainsEquals(__FILE__, $attributes->get('resource')); $this->assertSame('stdClass', $attributes->get('object')->getType()); $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getResponseHeaders()); diff --git a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index a3b7969be1..2cda6c4f56 100644 --- a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -197,7 +197,7 @@ public function testSkipSetContainer() public function testExceptionOnNonExistentTypeHint() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException('RuntimeException'); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClass". Did you forget to add a use statement?'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -207,11 +207,17 @@ public function testExceptionOnNonExistentTypeHint() $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); + + $error = $container->getDefinition('argument_resolver.service')->getArgument(0); + $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; + $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; + + $container->get($error); } public function testExceptionOnNonExistentTypeHintDifferentNamespace() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException('RuntimeException'); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassDifferentNamespaceController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Acme\NonExistentClass".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -221,6 +227,12 @@ public function testExceptionOnNonExistentTypeHintDifferentNamespace() $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); + + $error = $container->getDefinition('argument_resolver.service')->getArgument(0); + $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; + $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; + + $container->get($error); } public function testNoExceptionOnNonExistentTypeHintOptionalArg() @@ -367,6 +379,23 @@ public function testNotTaggedControllerServiceReceivesLocatorArgument() $this->assertInstanceOf(Reference::class, $locatorArgument); } + + public function testAlias() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service')->addArgument([]); + + $container->register('foo', RegisterTestController::class) + ->addTag('controller.service_arguments'); + + $container->setAlias(RegisterTestController::class, 'foo')->setPublic(true); + + $pass = new RegisterControllerArgumentLocatorsPass(); + $pass->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator)); + } } class RegisterTestController diff --git a/Tests/EventListener/DebugHandlersListenerTest.php b/Tests/EventListener/DebugHandlersListenerTest.php index 6f04c0a4c6..abd202f381 100644 --- a/Tests/EventListener/DebugHandlersListenerTest.php +++ b/Tests/EventListener/DebugHandlersListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -150,4 +151,89 @@ public function testReplaceExistingExceptionHandler() $this->assertSame($userHandler, $eHandler->setExceptionHandler('var_dump')); } + + public function provideLevelsAssignedToLoggers(): array + { + return [ + [false, false, '0', null, null], + [false, false, E_ALL, null, null], + [false, false, [], null, null], + [false, false, [E_WARNING => LogLevel::WARNING, E_USER_DEPRECATED => LogLevel::NOTICE], null, null], + + [true, false, E_ALL, E_ALL, null], + [true, false, E_DEPRECATED, E_DEPRECATED, null], + [true, false, [], null, null], + [true, false, [E_WARNING => LogLevel::WARNING, E_DEPRECATED => LogLevel::NOTICE], [E_WARNING => LogLevel::WARNING, E_DEPRECATED => LogLevel::NOTICE], null], + + [false, true, '0', null, null], + [false, true, E_ALL, null, E_DEPRECATED | E_USER_DEPRECATED], + [false, true, E_ERROR, null, null], + [false, true, [], null, null], + [false, true, [E_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::DEBUG], null, [E_DEPRECATED => LogLevel::DEBUG]], + + [true, true, '0', null, null], + [true, true, E_ALL, E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED), E_DEPRECATED | E_USER_DEPRECATED], + [true, true, E_ERROR, E_ERROR, null], + [true, true, E_USER_DEPRECATED, null, E_USER_DEPRECATED], + [true, true, [E_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::DEBUG], [E_ERROR => LogLevel::ERROR], [E_DEPRECATED => LogLevel::DEBUG]], + [true, true, [E_ERROR => LogLevel::ALERT], [E_ERROR => LogLevel::ALERT], null], + [true, true, [E_USER_DEPRECATED => LogLevel::NOTICE], null, [E_USER_DEPRECATED => LogLevel::NOTICE]], + ]; + } + + /** + * @dataProvider provideLevelsAssignedToLoggers + * + * @param array|string $levels + * @param array|string|null $expectedLoggerLevels + * @param array|string|null $expectedDeprecationLoggerLevels + */ + public function testLevelsAssignedToLoggers(bool $hasLogger, bool $hasDeprecationLogger, $levels, $expectedLoggerLevels, $expectedDeprecationLoggerLevels) + { + if (!class_exists(ErrorHandler::class)) { + $this->markTestSkipped('ErrorHandler component is required to run this test.'); + } + + $handler = $this->createMock(ErrorHandler::class); + + $expectedCalls = []; + $logger = null; + + $deprecationLogger = null; + if ($hasDeprecationLogger) { + $deprecationLogger = $this->createMock(LoggerInterface::class); + if (null !== $expectedDeprecationLoggerLevels) { + $expectedCalls[] = [$deprecationLogger, $expectedDeprecationLoggerLevels]; + } + } + + if ($hasLogger) { + $logger = $this->createMock(LoggerInterface::class); + if (null !== $expectedLoggerLevels) { + $expectedCalls[] = [$logger, $expectedLoggerLevels]; + } + } + + $handler + ->expects($this->exactly(\count($expectedCalls))) + ->method('setDefaultLogger') + ->withConsecutive(...$expectedCalls); + + $sut = new DebugHandlersListener(null, $logger, $levels, null, true, null, true, $deprecationLogger); + $prevHander = set_exception_handler([$handler, 'handleError']); + + try { + $handler + ->method('handleError') + ->willReturnCallback(function () use ($prevHander) { + $prevHander(...\func_get_args()); + }); + + $sut->configure(); + set_exception_handler($prevHander); + } catch (\Exception $e) { + set_exception_handler($prevHander); + throw $e; + } + } } diff --git a/Tests/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php index cdf6874f35..408b580630 100644 --- a/Tests/EventListener/ErrorListenerTest.php +++ b/Tests/EventListener/ErrorListenerTest.php @@ -155,7 +155,6 @@ public function testCSPHeaderIsRemoved() $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); - $this->assertFalse($dispatcher->hasListeners(KernelEvents::RESPONSE), 'CSP removal listener has been removed'); } /** diff --git a/Tests/EventListener/LocaleAwareListenerTest.php b/Tests/EventListener/LocaleAwareListenerTest.php index ef3b7d1b42..0064429048 100644 --- a/Tests/EventListener/LocaleAwareListenerTest.php +++ b/Tests/EventListener/LocaleAwareListenerTest.php @@ -47,13 +47,15 @@ public function testLocaleIsSetInOnKernelRequest() public function testDefaultLocaleIsUsedOnExceptionsInOnKernelRequest() { $this->localeAwareService - ->expects($this->at(0)) + ->expects($this->exactly(2)) ->method('setLocale') - ->will($this->throwException(new \InvalidArgumentException())); - $this->localeAwareService - ->expects($this->at(1)) - ->method('setLocale') - ->with($this->equalTo('en')); + ->withConsecutive( + [$this->anything()], + ['en'] + ) + ->willReturnOnConsecutiveCalls( + $this->throwException(new \InvalidArgumentException()) + ); $event = new RequestEvent($this->createHttpKernel(), $this->createRequest('fr'), HttpKernelInterface::MASTER_REQUEST); $this->listener->onKernelRequest($event); @@ -89,13 +91,15 @@ public function testLocaleIsSetToDefaultOnKernelFinishRequestWhenParentRequestDo public function testDefaultLocaleIsUsedOnExceptionsInOnKernelFinishRequest() { $this->localeAwareService - ->expects($this->at(0)) + ->expects($this->exactly(2)) ->method('setLocale') - ->will($this->throwException(new \InvalidArgumentException())); - $this->localeAwareService - ->expects($this->at(1)) - ->method('setLocale') - ->with($this->equalTo('en')); + ->withConsecutive( + [$this->anything()], + ['en'] + ) + ->willReturnOnConsecutiveCalls( + $this->throwException(new \InvalidArgumentException()) + ); $this->requestStack->push($this->createRequest('fr')); $this->requestStack->push($subRequest = $this->createRequest('de')); diff --git a/Tests/EventListener/SessionListenerTest.php b/Tests/EventListener/SessionListenerTest.php index 8fc9f6bc9c..8df2ce5169 100644 --- a/Tests/EventListener/SessionListenerTest.php +++ b/Tests/EventListener/SessionListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; @@ -24,6 +25,7 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener; use Symfony\Component\HttpKernel\EventListener\SessionListener; +use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\HttpKernelInterface; class SessionListenerTest extends TestCase @@ -178,4 +180,127 @@ public function testSurrogateMasterRequestIsPublic() $this->assertTrue($response->headers->has('Expires')); $this->assertLessThanOrEqual((new \DateTime('now', new \DateTimeZone('UTC'))), (new \DateTime($response->headers->get('Expires')))); } + + public function testSessionUsageExceptionIfStatelessAndSessionUsed() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container, true); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $request->attributes->set('_stateless', true); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + + $this->expectException(UnexpectedSessionUsageException::class); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response())); + } + + public function testSessionUsageLogIfStatelessAndSessionUsed() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + + $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + $logger->expects($this->exactly(1))->method('warning'); + + $container = new Container(); + $container->set('initialized_session', $session); + $container->set('logger', $logger); + + $listener = new SessionListener($container, false); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $request->attributes->set('_stateless', true); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response())); + } + + public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->method('isStarted')->willReturn(true); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + $session->expects($this->exactly(1))->method('save'); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container, true); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $request->attributes->set('_stateless', true); + + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + + $response = new Response(); + $this->expectException(UnexpectedSessionUsageException::class); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response)); + } + + public function testSessionUsageCallbackWhenDebugAndStateless() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->method('isStarted')->willReturn(true); + $session->expects($this->exactly(1))->method('save'); + + $requestStack = new RequestStack(); + + $request = new Request(); + $request->attributes->set('_stateless', true); + + $requestStack->push(new Request()); + $requestStack->push($request); + $requestStack->push(new Request()); + + $container = new Container(); + $container->set('initialized_session', $session); + $container->set('request_stack', $requestStack); + + $this->expectException(UnexpectedSessionUsageException::class); + (new SessionListener($container, true))->onSessionUsage(); + } + + public function testSessionUsageCallbackWhenNoDebug() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->method('isStarted')->willReturn(true); + $session->expects($this->exactly(0))->method('save'); + + $request = new Request(); + $request->attributes->set('_stateless', true); + + $requestStack = $this->getMockBuilder(RequestStack::class)->getMock(); + $requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request); + + $container = new Container(); + $container->set('initialized_session', $session); + $container->set('request_stack', $requestStack); + + (new SessionListener($container))->onSessionUsage(); + } + + public function testSessionUsageCallbackWhenNoStateless() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->method('isStarted')->willReturn(true); + $session->expects($this->never())->method('save'); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + $requestStack->push(new Request()); + + $container = new Container(); + $container->set('initialized_session', $session); + $container->set('request_stack', $requestStack); + + (new SessionListener($container, true))->onSessionUsage(); + } } diff --git a/Tests/Fixtures/Controller/NullableController.php b/Tests/Fixtures/Controller/NullableController.php index 9db4df7b4c..aacae0e3e3 100644 --- a/Tests/Fixtures/Controller/NullableController.php +++ b/Tests/Fixtures/Controller/NullableController.php @@ -13,7 +13,7 @@ class NullableController { - public function action(?string $foo, ?\stdClass $bar, ?string $baz = 'value', $mandatory) + public function action(?string $foo, ?\stdClass $bar, ?string $baz = 'value', string $last = '') { } } diff --git a/Tests/HttpCache/HttpCacheTest.php b/Tests/HttpCache/HttpCacheTest.php index 9c64b98fee..802f1d1007 100644 --- a/Tests/HttpCache/HttpCacheTest.php +++ b/Tests/HttpCache/HttpCacheTest.php @@ -654,7 +654,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation() $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=10/', $this->response->headers->get('Cache-Control')); $this->cacheConfig['default_ttl'] = 10; $this->request('GET', '/'); @@ -663,7 +663,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation() $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=10/', $this->response->headers->get('Cache-Control')); } public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired() @@ -676,7 +676,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); @@ -684,7 +684,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); // expires the cache $values = $this->getMetaStorageValues(); @@ -704,7 +704,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('invalid'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->setNextResponse(); @@ -714,7 +714,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); } public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304() @@ -727,7 +727,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); @@ -755,7 +755,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('store'); $this->assertTraceNotContains('miss'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); @@ -763,7 +763,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); - $this->assertRegExp('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); + $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); } public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective() @@ -776,7 +776,7 @@ public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirectiv $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTraceNotContains('store'); - $this->assertNotRegExp('/s-maxage/', $this->response->headers->get('Cache-Control')); + $this->assertDoesNotMatchRegularExpression('/s-maxage/', $this->response->headers->get('Cache-Control')); $this->assertEquals('Hello World', $this->response->getContent()); } diff --git a/Tests/HttpCache/HttpCacheTestCase.php b/Tests/HttpCache/HttpCacheTestCase.php index a73a327b53..a058a15f15 100644 --- a/Tests/HttpCache/HttpCacheTestCase.php +++ b/Tests/HttpCache/HttpCacheTestCase.php @@ -91,7 +91,7 @@ public function assertTraceContains($trace) $traces = $this->cache->getTraces(); $traces = current($traces); - $this->assertRegExp('/'.$trace.'/', implode(', ', $traces)); + $this->assertMatchesRegularExpression('/'.$trace.'/', implode(', ', $traces)); } public function assertTraceNotContains($trace) @@ -99,7 +99,7 @@ public function assertTraceNotContains($trace) $traces = $this->cache->getTraces(); $traces = current($traces); - $this->assertNotRegExp('/'.$trace.'/', implode(', ', $traces)); + $this->assertDoesNotMatchRegularExpression('/'.$trace.'/', implode(', ', $traces)); } public function assertExceptionsAreCaught() diff --git a/Tests/HttpCache/StoreTest.php b/Tests/HttpCache/StoreTest.php index 6e56dbe0bb..1f5f472802 100644 --- a/Tests/HttpCache/StoreTest.php +++ b/Tests/HttpCache/StoreTest.php @@ -97,6 +97,60 @@ public function testSetsTheXContentDigestResponseHeaderBeforeStoring() $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $res['x-content-digest'][0]); } + public function testDoesNotTrustXContentDigestFromUpstream() + { + $response = new Response('test', 200, ['X-Content-Digest' => 'untrusted-from-elsewhere']); + + $cacheKey = $this->store->write($this->request, $response); + $entries = $this->getStoreMetadata($cacheKey); + list(, $res) = $entries[0]; + + $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $res['x-content-digest'][0]); + $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $response->headers->get('X-Content-Digest')); + } + + public function testWritesResponseEvenIfXContentDigestIsPresent() + { + // Prime the store + $this->store->write($this->request, new Response('test', 200, ['X-Content-Digest' => 'untrusted-from-elsewhere'])); + + $response = $this->store->lookup($this->request); + $this->assertNotNull($response); + } + + public function testWritingARestoredResponseDoesNotCorruptCache() + { + /* + * This covers the regression reported in https://github.com/symfony/symfony/issues/37174. + * + * A restored response does *not* load the body, but only keep the file path in a special X-Body-File + * header. For reasons (?), the file path was also used as the restored response body. + * It would be up to others (HttpCache...?) to honor this header and actually load the response content + * from there. + * + * When a restored response was stored again, the Store itself would ignore the header. In the first + * step, this would compute a new Content Digest based on the file path in the restored response body; + * this is covered by "Checkpoint 1" below. But, since the X-Body-File header was left untouched (Checkpoint 2), downstream + * code (HttpCache...) would not immediately notice. + * + * Only upon performing the lookup for a second time, we'd get a Response where the (wrong) Content Digest + * is also reflected in the X-Body-File header, this time also producing wrong content when the downstream + * evaluates it. + */ + $this->store->write($this->request, $this->response); + $digest = $this->response->headers->get('X-Content-Digest'); + $path = $this->getStorePath($digest); + + $response = $this->store->lookup($this->request); + $this->store->write($this->request, $response); + $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); // Checkpoint 1 + $this->assertEquals($path, $response->headers->get('X-Body-File')); // Checkpoint 2 + + $response = $this->store->lookup($this->request); + $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); + $this->assertEquals($path, $response->headers->get('X-Body-File')); + } + public function testFindsAStoredEntryWithLookup() { $this->storeSimpleEntry(); diff --git a/Tests/HttpKernelTest.php b/Tests/HttpKernelTest.php index 676e755aa7..b1d225b84e 100644 --- a/Tests/HttpKernelTest.php +++ b/Tests/HttpKernelTest.php @@ -305,8 +305,8 @@ public function testVerifyRequestStackPushPopDuringHandle() $request = new Request(); $stack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->setMethods(['push', 'pop'])->getMock(); - $stack->expects($this->at(0))->method('push')->with($this->equalTo($request)); - $stack->expects($this->at(1))->method('pop'); + $stack->expects($this->once())->method('push')->with($this->equalTo($request)); + $stack->expects($this->once())->method('pop'); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, null, $stack); diff --git a/Tests/KernelTest.php b/Tests/KernelTest.php index e9f100a8af..ece28e00c8 100644 --- a/Tests/KernelTest.php +++ b/Tests/KernelTest.php @@ -15,10 +15,12 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -86,7 +88,7 @@ public function testInitializeContainerClearsOldContainers() $containerDir = __DIR__.'/Fixtures/var/cache/custom/'.substr(\get_class($kernel->getContainer()), 0, 16); $this->assertTrue(unlink(__DIR__.'/Fixtures/var/cache/custom/Symfony_Component_HttpKernel_Tests_CustomProjectDirKernelCustomDebugContainer.php.meta')); $this->assertFileExists($containerDir); - $this->assertFileNotExists($containerDir.'.legacy'); + $this->assertFileDoesNotExist($containerDir.'.legacy'); $kernel = new CustomProjectDirKernel(function ($container) { $container->register('foo', 'stdClass')->setPublic(true); }); $kernel->boot(); @@ -94,8 +96,8 @@ public function testInitializeContainerClearsOldContainers() $this->assertFileExists($containerDir); $this->assertFileExists($containerDir.'.legacy'); - $this->assertFileNotExists($legacyContainerDir); - $this->assertFileNotExists($legacyContainerDir.'.legacy'); + $this->assertFileDoesNotExist($legacyContainerDir); + $this->assertFileDoesNotExist($legacyContainerDir.'.legacy'); } public function testBootInitializesBundlesAndContainer() @@ -166,9 +168,12 @@ public function testShutdownCallsShutdownOnAllBundles() public function testShutdownGivesNullContainerToAllBundles() { $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle')->getMock(); - $bundle->expects($this->at(3)) + $bundle->expects($this->exactly(2)) ->method('setContainer') - ->with(null); + ->withConsecutive( + [$this->isInstanceOf(ContainerInterface::class)], + [null] + ); $kernel = $this->getKernel(['getBundles']); $kernel->expects($this->any()) @@ -480,6 +485,14 @@ public function testKernelPass() $this->assertTrue($kernel->getContainer()->getParameter('test.processed')); } + public function testWarmup() + { + $kernel = new CustomProjectDirKernel(); + $kernel->boot(); + + $this->assertTrue($kernel->warmedUp); + } + public function testServicesResetter() { $httpKernelMock = $this->getMockBuilder(HttpKernelInterface::class) @@ -527,6 +540,27 @@ public function testKernelStartTimeIsResetWhileBootingAlreadyBootedKernel() $this->assertGreaterThan($preReBoot, $kernel->getStartTime()); } + public function testAnonymousKernelGeneratesValidContainerClass(): void + { + $kernel = new class('test', true) extends Kernel { + public function registerBundles(): iterable + { + return []; + } + + public function registerContainerConfiguration(LoaderInterface $loader): void + { + } + + public function getContainerClass(): string + { + return parent::getContainerClass(); + } + }; + + $this->assertMatchesRegularExpression('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*TestDebugContainer$/', $kernel->getContainerClass()); + } + /** * Returns a mock for the BundleInterface. */ @@ -603,8 +637,9 @@ public function getProjectDir(): string } } -class CustomProjectDirKernel extends Kernel +class CustomProjectDirKernel extends Kernel implements WarmableInterface { + public $warmedUp = false; private $baseDir; private $buildContainer; private $httpKernel; @@ -631,6 +666,13 @@ public function getProjectDir(): string return __DIR__.'/Fixtures'; } + public function warmUp(string $cacheDir): array + { + $this->warmedUp = true; + + return []; + } + protected function build(ContainerBuilder $container) { if ($build = $this->buildContainer) { diff --git a/Tests/Profiler/FileProfilerStorageTest.php b/Tests/Profiler/FileProfilerStorageTest.php index 2bd2a2705b..2df9a94884 100644 --- a/Tests/Profiler/FileProfilerStorageTest.php +++ b/Tests/Profiler/FileProfilerStorageTest.php @@ -228,9 +228,9 @@ public function testStoreTime() $records = $this->storage->find('', '', 3, 'GET', $start, time() + 3 * 60); $this->assertCount(3, $records, '->find() returns all previously added records'); - $this->assertEquals($records[0]['token'], 'time_2', '->find() returns records ordered by time in descendant order'); - $this->assertEquals($records[1]['token'], 'time_1', '->find() returns records ordered by time in descendant order'); - $this->assertEquals($records[2]['token'], 'time_0', '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_2', $records[0]['token'], '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_1', $records[1]['token'], '->find() returns records ordered by time in descendant order'); + $this->assertEquals('time_0', $records[2]['token'], '->find() returns records ordered by time in descendant order'); $records = $this->storage->find('', '', 3, 'GET', $start, time() + 2 * 60); $this->assertCount(2, $records, '->find() should return only first two of the previously added records'); @@ -318,8 +318,8 @@ public function testStatusCode() $tokens = $this->storage->find('', '', 10, ''); $this->assertCount(2, $tokens); - $this->assertContains($tokens[0]['status_code'], [200, 404]); - $this->assertContains($tokens[1]['status_code'], [200, 404]); + $this->assertContains((int) $tokens[0]['status_code'], [200, 404]); + $this->assertContains((int) $tokens[1]['status_code'], [200, 404]); } public function testMultiRowIndexFile() diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php index b2eb59206b..4801776cce 100644 --- a/Tests/UriSignerTest.php +++ b/Tests/UriSignerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\UriSigner; class UriSignerTest extends TestCase @@ -52,6 +53,15 @@ public function testCheckWithDifferentArgSeparator() $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); } + public function testCheckWithRequest() + { + $signer = new UriSigner('foobar'); + + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo')))); + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar')))); + $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer')))); + } + public function testCheckWithDifferentParameter() { $signer = new UriSigner('foobar', 'qux'); diff --git a/UriSigner.php b/UriSigner.php index 4dfb2bbefc..df08dd69fc 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Component\HttpFoundation\Request; + /** * Signs URIs. * @@ -78,6 +80,14 @@ public function check(string $uri) return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); } + public function checkRequest(Request $request): bool + { + $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; + + // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) + return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); + } + private function computeHash(string $uri): string { return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); diff --git a/composer.json b/composer.json index 73621a5a7b..c016af2369 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,14 @@ } ], "require": { - "php": "^7.2.5", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/error-handler": "^4.4|^5.0", "symfony/event-dispatcher": "^5.0", "symfony/http-foundation": "^4.4|^5.0", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.15", "psr/log": "~1.0" }, "require-dev": { @@ -48,6 +50,7 @@ "symfony/browser-kit": "<4.4", "symfony/cache": "<5.0", "symfony/config": "<5.0", + "symfony/console": "<4.4", "symfony/form": "<5.0", "symfony/dependency-injection": "<4.4", "symfony/doctrine-bridge": "<5.0", @@ -74,7 +77,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } } }