From 010968fe06275067e7744e00f24ac8ac27dc0097 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 25 Oct 2021 15:13:46 +0200 Subject: [PATCH 01/12] Move interest-cohort header to response listener (see #3599) Description ----------- related to #3263 Commits ------- d8a7323c Move interest-cohort header to response listener 295e8259 Correctly register the event listener 93e92793 CS --- .../EventListener/InterestCohortListener.php | 38 +++++++++++++++++++ core-bundle/src/Resources/config/listener.yml | 6 +++ .../contao/library/Contao/Template.php | 1 - 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 core-bundle/src/EventListener/InterestCohortListener.php diff --git a/core-bundle/src/EventListener/InterestCohortListener.php b/core-bundle/src/EventListener/InterestCohortListener.php new file mode 100644 index 00000000000..12c69cf172a --- /dev/null +++ b/core-bundle/src/EventListener/InterestCohortListener.php @@ -0,0 +1,38 @@ +scopeMatcher = $scopeMatcher; + } + + public function __invoke(ResponseEvent $event): void + { + if (!$this->scopeMatcher->isFrontendMasterRequest($event)) { + return; + } + + $event->getResponse()->headers->set('Permissions-Policy', 'interest-cohort=()'); + } +} diff --git a/core-bundle/src/Resources/config/listener.yml b/core-bundle/src/Resources/config/listener.yml index 8d72df447ab..8ca06b0e45e 100644 --- a/core-bundle/src/Resources/config/listener.yml +++ b/core-bundle/src/Resources/config/listener.yml @@ -114,6 +114,12 @@ services: tags: - { name: contao.callback, table: tl_content, target: fields.customTpl.options } + Contao\CoreBundle\EventListener\InterestCohortListener: + arguments: + - '@contao.routing.scope_matcher' + tags: + - kernel.event_listener + contao.listener.module_template_options: class: Contao\CoreBundle\EventListener\DataContainer\TemplateOptionsListener arguments: diff --git a/core-bundle/src/Resources/contao/library/Contao/Template.php b/core-bundle/src/Resources/contao/library/Contao/Template.php index 63fceee4b76..c6a75037787 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Template.php +++ b/core-bundle/src/Resources/contao/library/Contao/Template.php @@ -320,7 +320,6 @@ public function getResponse() $response = new Response($this->strBuffer); $response->headers->set('Content-Type', $this->strContentType . '; charset=' . Config::get('characterSet')); - $response->headers->set('Permissions-Policy', 'interest-cohort=()'); // Mark this response to affect the caching of the current page but remove any default cache headers $response->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, true); From 6e17748f69bb85d80bd5ed0fe6d11983545b46f3 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 25 Oct 2021 15:16:32 +0200 Subject: [PATCH 02/12] Adjust cache headers for Twig responses (see #3598) Description ----------- If a developer uses `$this->render()` to generate the response from Twig, the response contains incorrect `private` cache header that destroy the page cache. related to #3263 Commits ------- --- .../src/Controller/AbstractFragmentController.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core-bundle/src/Controller/AbstractFragmentController.php b/core-bundle/src/Controller/AbstractFragmentController.php index e838bfb608b..0e7876fbffa 100644 --- a/core-bundle/src/Controller/AbstractFragmentController.php +++ b/core-bundle/src/Controller/AbstractFragmentController.php @@ -12,6 +12,7 @@ namespace Contao\CoreBundle\Controller; +use Contao\CoreBundle\EventListener\SubrequestCacheSubscriber; use Contao\CoreBundle\Fragment\FragmentOptionsAwareInterface; use Contao\CoreBundle\Routing\ScopeMatcher; use Contao\FrontendTemplate; @@ -21,6 +22,7 @@ use Contao\Template; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; abstract class AbstractFragmentController extends AbstractController implements FragmentOptionsAwareInterface { @@ -154,4 +156,15 @@ protected function getType(): string return Container::underscore($className); } + + protected function render(string $view, array $parameters = [], Response $response = null): Response + { + $response = parent::render($view, $parameters, $response); + + // Mark this response to affect the caching of the current page but remove any default cache headers + $response->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, true); + $response->headers->remove('Cache-Control'); + + return $response; + } } From e58356170677f0cefd4ac8c629730378d8329dc0 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Mon, 25 Oct 2021 15:53:42 +0200 Subject: [PATCH 03/12] Fix the base path in the asset context (see #3595) Description ----------- | Q | A | -----------------| --- | Fixed issues | Fixes #3448 Commits ------- 46f53690 Fixed base path in asset context cf29d0c4 CS e91654af Replace getMainRequest with getMasterRequest c3194d2f CS 768d0566 Merge branch '4.9' into bugfix/asset-context 2c07c5e5 Fix the prefer-lowest test 9a1436eb Correctly strip base path in template class c792f670 CS 9eaa4d08 CS --- composer.json | 2 +- core-bundle/composer.json | 2 +- core-bundle/src/Asset/ContaoContext.php | 12 ++-- .../contao/library/Contao/Template.php | 15 +++- core-bundle/tests/Asset/ContaoContextTest.php | 21 +++++- core-bundle/tests/Contao/TemplateTest.php | 68 ++++++++++++++++++- 6 files changed, 105 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 4a0c0507f26..6e1a64e565b 100644 --- a/composer.json +++ b/composer.json @@ -143,7 +143,7 @@ "ext-fileinfo": "*", "bamarni/composer-bin-plugin": "^1.4", "composer/composer": "^1.0 || ^2.0", - "contao/test-case": "^4.3", + "contao/test-case": "^4.4", "phpunit/phpunit": "^8.5", "symfony/browser-kit": "4.4.*", "symfony/phpunit-bridge": "4.4.*" diff --git a/core-bundle/composer.json b/core-bundle/composer.json index 87ad95899c2..70ad904f9c0 100644 --- a/core-bundle/composer.json +++ b/core-bundle/composer.json @@ -117,7 +117,7 @@ "ext-fileinfo": "*", "composer/composer": "^1.0 || ^2.0", "contao/manager-plugin": "^2.3.1", - "contao/test-case": "^4.3", + "contao/test-case": "^4.4", "lexik/maintenance-bundle": "^2.1.5", "phpunit/phpunit": "^8.5", "symfony/browser-kit": "4.4.*", diff --git a/core-bundle/src/Asset/ContaoContext.php b/core-bundle/src/Asset/ContaoContext.php index 215abf97887..d0853fb6637 100644 --- a/core-bundle/src/Asset/ContaoContext.php +++ b/core-bundle/src/Asset/ContaoContext.php @@ -52,14 +52,12 @@ public function __construct(RequestStack $requestStack, ContaoFramework $framewo public function getBasePath(): string { - if ($this->debug) { + if (null === ($request = $this->requestStack->getMasterRequest())) { return ''; } - $request = $this->requestStack->getCurrentRequest(); - - if (null === $request || '' === ($staticUrl = $this->getFieldValue($this->getPageModel()))) { - return ''; + if ($this->debug || '' === ($staticUrl = $this->getFieldValue($this->getPageModel()))) { + return $request->getBasePath(); } $protocol = $this->isSecure() ? 'https' : 'http'; @@ -76,7 +74,7 @@ public function isSecure(): bool return (bool) $page->loadDetails()->rootUseSSL; } - $request = $this->requestStack->getCurrentRequest(); + $request = $this->requestStack->getMasterRequest(); if (null === $request) { return false; @@ -99,7 +97,7 @@ public function getStaticUrl(): string private function getPageModel(): ?PageModel { - $request = $this->requestStack->getCurrentRequest(); + $request = $this->requestStack->getMasterRequest(); if (null === $request || !$request->attributes->has('pageModel')) { if (isset($GLOBALS['objPage']) && $GLOBALS['objPage'] instanceof PageModel) { diff --git a/core-bundle/src/Resources/contao/library/Contao/Template.php b/core-bundle/src/Resources/contao/library/Contao/Template.php index c6a75037787..12deb6251a1 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Template.php +++ b/core-bundle/src/Resources/contao/library/Contao/Template.php @@ -400,8 +400,21 @@ public function asset($path, $packageName = null) { $url = System::getContainer()->get('assets.packages')->getUrl($path, $packageName); + $basePath = '/'; + $request = System::getContainer()->get('request_stack')->getMasterRequest(); + + if ($request !== null) + { + $basePath = $request->getBasePath() . '/'; + } + + if (0 === strncmp($url, $basePath, \strlen($basePath))) + { + return substr($url, \strlen($basePath)); + } + // Contao paths are relative to the tag, so remove leading slashes - return ltrim($url, '/'); + return $url; } /** diff --git a/core-bundle/tests/Asset/ContaoContextTest.php b/core-bundle/tests/Asset/ContaoContextTest.php index e5322647e2a..cd1cc9402a5 100644 --- a/core-bundle/tests/Asset/ContaoContextTest.php +++ b/core-bundle/tests/Asset/ContaoContextTest.php @@ -37,15 +37,30 @@ public function testReturnsAnEmptyBasePathIfThereIsNoRequest(): void $this->assertSame('', $context->getBasePath()); } - public function testReturnsAnEmptyBasePathIfThePageDoesNotDefineIt(): void + public function testReturnsTheBasePathIfThePageDoesNotDefineIt(): void { $page = $this->getPageWithDetails(); $GLOBALS['objPage'] = $page; - $context = $this->getContaoContext('staticPlugins'); + $request = Request::create( + 'https://example.com/foobar/index.php', + 'GET', + [], + [], + [], + [ + 'SCRIPT_FILENAME' => '/foobar/index.php', + 'SCRIPT_NAME' => '/foobar/index.php', + ] + ); - $this->assertSame('', $context->getBasePath()); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $context = $this->getContaoContext('staticPlugins', $requestStack); + + $this->assertSame('/foobar', $context->getBasePath()); unset($GLOBALS['objPage']); } diff --git a/core-bundle/tests/Contao/TemplateTest.php b/core-bundle/tests/Contao/TemplateTest.php index 68e2d9c8645..384ec01ca4a 100644 --- a/core-bundle/tests/Contao/TemplateTest.php +++ b/core-bundle/tests/Contao/TemplateTest.php @@ -19,6 +19,8 @@ use Contao\System; use Symfony\Component\Asset\Packages; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\VarDumper\VarDumper; use Webmozart\PathUtil\Path; @@ -214,7 +216,7 @@ public function testParsesNestedBlocks(): void $this->assertSame($obLevel, ob_get_level()); } - public function testLoadsTheAssetsPackages(): void + public function testStripsLeadingSlashFromAssetUrl(): void { $packages = $this->createMock(Packages::class); $packages @@ -226,11 +228,73 @@ public function testLoadsTheAssetsPackages(): void $container = $this->getContainerWithContaoConfiguration(); $container->set('assets.packages', $packages); + $container->set('request_stack', new RequestStack()); System::setContainer($container); $template = new FrontendTemplate(); - $template->asset('/path/to/asset', 'package_name'); + $url = $template->asset('/path/to/asset', 'package_name'); + + $this->assertSame('path/to/asset', $url); + } + + public function testStripsTheBasePathFromAssetUrl(): void + { + $packages = $this->createMock(Packages::class); + $packages + ->expects($this->once()) + ->method('getUrl') + ->with('/path/to/asset', 'package_name') + ->willReturn('/foo/path/to/asset') + ; + + $request = Request::create( + 'https://example.com/foo/index.php', + 'GET', + [], + [], + [], + [ + 'SCRIPT_FILENAME' => '/foo/index.php', + 'SCRIPT_NAME' => '/foo/index.php', + ] + ); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $container = $this->getContainerWithContaoConfiguration(); + $container->set('assets.packages', $packages); + $container->set('request_stack', $requestStack); + + System::setContainer($container); + + $template = new FrontendTemplate(); + $url = $template->asset('/path/to/asset', 'package_name'); + + $this->assertSame('path/to/asset', $url); + } + + public function testDoesNotModifyAbsoluteAssetUrl(): void + { + $packages = $this->createMock(Packages::class); + $packages + ->expects($this->once()) + ->method('getUrl') + ->with('/path/to/asset', 'package_name') + ->willReturn('https://cdn.example.com/path/to/asset') + ; + + $container = $this->getContainerWithContaoConfiguration(); + $container->set('assets.packages', $packages); + $container->set('request_stack', new RequestStack()); + + System::setContainer($container); + + $template = new FrontendTemplate(); + $url = $template->asset('/path/to/asset', 'package_name'); + + $this->assertSame('https://cdn.example.com/path/to/asset', $url); } public function testCanDumpTemplateVars(): void From b9cd28d141b243e31d6e3d93fcad2f8c6536f9f9 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Tue, 26 Oct 2021 12:12:14 +0200 Subject: [PATCH 04/12] Only change the cache header if no response is passed to Twig render() (see #3608) Description ----------- Enhances #3598 Commits ------- f867ce7f Only change cache header if no response is given to Twig --- .../src/Controller/AbstractFragmentController.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core-bundle/src/Controller/AbstractFragmentController.php b/core-bundle/src/Controller/AbstractFragmentController.php index 0e7876fbffa..29500fda6cb 100644 --- a/core-bundle/src/Controller/AbstractFragmentController.php +++ b/core-bundle/src/Controller/AbstractFragmentController.php @@ -159,12 +159,14 @@ protected function getType(): string protected function render(string $view, array $parameters = [], Response $response = null): Response { - $response = parent::render($view, $parameters, $response); + if (null === $response) { + $response = new Response(); - // Mark this response to affect the caching of the current page but remove any default cache headers - $response->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, true); - $response->headers->remove('Cache-Control'); + // Mark this response to affect the caching of the current page but remove any default cache headers + $response->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, true); + $response->headers->remove('Cache-Control'); + } - return $response; + return parent::render($view, $parameters, $response); } } From 0352442f3734adf8a38fa70ba361be35e23bd3fc Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Wed, 3 Nov 2021 10:28:22 +0100 Subject: [PATCH 05/12] Ignore routes without root ID instead of throwing an exception (see #3607) Description ----------- This reverts some changes from #3220. There are valid use cases for pages without a root page (e.g. placing our `terminal42/contao-folderpage` in the root to group root pages). The system should never fail in such a case but simply ignore these pages. Commits ------- e89eba73 Re-added handling of missing root pages --- core-bundle/src/Routing/Route404Provider.php | 11 +++- core-bundle/src/Routing/RouteProvider.php | 11 +++- .../tests/Routing/Route404ProviderTest.php | 61 +++++++++++++++++++ .../tests/Routing/RouteProviderTest.php | 54 ++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/core-bundle/src/Routing/Route404Provider.php b/core-bundle/src/Routing/Route404Provider.php index f462860a8e3..e3424a2d410 100644 --- a/core-bundle/src/Routing/Route404Provider.php +++ b/core-bundle/src/Routing/Route404Provider.php @@ -13,6 +13,7 @@ namespace Contao\CoreBundle\Routing; use Contao\CoreBundle\ContaoCoreBundle; +use Contao\CoreBundle\Exception\NoRootPageFoundException; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\PageModel; use Symfony\Cmf\Component\Routing\RouteProviderInterface; @@ -100,7 +101,15 @@ private function getRoutes(array $languages = null): array private function addRoutesForPage(PageModel $page, array &$routes): void { - $page->loadDetails(); + try { + $page->loadDetails(); + + if (!$page->rootId) { + return; + } + } catch (NoRootPageFoundException $e) { + return; + } $defaults = [ '_token_check' => true, diff --git a/core-bundle/src/Routing/RouteProvider.php b/core-bundle/src/Routing/RouteProvider.php index 0c5223fdd33..d0129a945db 100644 --- a/core-bundle/src/Routing/RouteProvider.php +++ b/core-bundle/src/Routing/RouteProvider.php @@ -14,6 +14,7 @@ use Contao\Config; use Contao\CoreBundle\ContaoCoreBundle; +use Contao\CoreBundle\Exception\NoRootPageFoundException; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\Model; use Contao\Model\Collection; @@ -235,7 +236,15 @@ private function createCollectionForRoutes(array $routes, array $languages): Rou private function addRoutesForPage(PageModel $page, array &$routes): void { - $page->loadDetails(); + try { + $page->loadDetails(); + + if (!$page->rootId) { + return; + } + } catch (NoRootPageFoundException $e) { + return; + } $defaults = $this->getRouteDefaults($page); $defaults['parameters'] = ''; diff --git a/core-bundle/tests/Routing/Route404ProviderTest.php b/core-bundle/tests/Routing/Route404ProviderTest.php index 9cdef6920c4..436b050d98e 100644 --- a/core-bundle/tests/Routing/Route404ProviderTest.php +++ b/core-bundle/tests/Routing/Route404ProviderTest.php @@ -12,6 +12,7 @@ namespace Contao\CoreBundle\Tests\Routing; +use Contao\CoreBundle\Exception\NoRootPageFoundException; use Contao\CoreBundle\Routing\Route404Provider; use Contao\CoreBundle\Tests\TestCase; use Contao\Model\Collection; @@ -324,6 +325,66 @@ public function sortRoutesProvider(): \Generator ]; } + public function testIgnoresRoutesWithoutRootId(): void + { + /** @var PageModel&MockObject $page */ + $page = $this->mockClassWithProperties(PageModel::class); + $page->id = 17; + + $page + ->expects($this->once()) + ->method('loadDetails') + ; + + $pageAdapter = $this->mockAdapter(['findByType']); + $pageAdapter + ->expects($this->once()) + ->method('findByType') + ->with('error_404') + ->willReturn(new Collection([$page], 'tl_page')) + ; + + $framework = $this->mockContaoFramework([PageModel::class => $pageAdapter]); + $request = $this->mockRequestWithPath('/'); + + $provider = new Route404Provider($framework, false); + $routes = $provider->getRouteCollectionForRequest($request)->all(); + + $this->assertIsArray($routes); + $this->assertEmpty($routes); + } + + public function testIgnoresPagesWithNoRootPageFoundException(): void + { + /** @var PageModel&MockObject $page */ + $page = $this->mockClassWithProperties(PageModel::class); + $page->id = 17; + $page->rootId = 1; + + $page + ->expects($this->once()) + ->method('loadDetails') + ->willThrowException(new NoRootPageFoundException()) + ; + + $pageAdapter = $this->mockAdapter(['findByType']); + $pageAdapter + ->expects($this->once()) + ->method('findByType') + ->with('error_404') + ->willReturn(new Collection([$page], 'tl_page')) + ; + + $framework = $this->mockContaoFramework([PageModel::class => $pageAdapter]); + $request = $this->mockRequestWithPath('/'); + + $provider = new Route404Provider($framework, false); + $routes = $provider->getRouteCollectionForRequest($request)->all(); + + $this->assertIsArray($routes); + $this->assertEmpty($routes); + } + /** * @return Request&MockObject */ diff --git a/core-bundle/tests/Routing/RouteProviderTest.php b/core-bundle/tests/Routing/RouteProviderTest.php index 8c597b76c00..8d9172e187f 100644 --- a/core-bundle/tests/Routing/RouteProviderTest.php +++ b/core-bundle/tests/Routing/RouteProviderTest.php @@ -13,6 +13,7 @@ namespace Contao\CoreBundle\Tests\Routing; use Contao\Config; +use Contao\CoreBundle\Exception\NoRootPageFoundException; use Contao\CoreBundle\Framework\Adapter; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Routing\RouteProvider; @@ -570,6 +571,59 @@ public function getPageRoutes(): \Generator } } + public function testIgnoresRoutesWithoutRootId(): void + { + /** @var PageModel&MockObject $page */ + $page = $this->createPage('de', 'foo'); + $page->rootId = null; + + $page + ->expects($this->once()) + ->method('loadDetails') + ; + + $pageAdapter = $this->mockAdapter(['findBy']); + $pageAdapter + ->expects($this->once()) + ->method('findBy') + ->willReturn(new Collection([$page], 'tl_page')) + ; + + $framework = $this->mockFramework($pageAdapter); + $request = $this->mockRequestWithPath('/foo.html'); + + $routes = $this->getRouteProvider($framework)->getRouteCollectionForRequest($request)->all(); + + $this->assertIsArray($routes); + $this->assertEmpty($routes); + } + + public function testIgnoresPagesWithNoRootPageFoundException(): void + { + /** @var PageModel&MockObject $page */ + $page = $this->createPage('de', 'foo'); + $page + ->expects($this->once()) + ->method('loadDetails') + ->willThrowException(new NoRootPageFoundException()) + ; + + $pageAdapter = $this->mockAdapter(['findBy']); + $pageAdapter + ->expects($this->once()) + ->method('findBy') + ->willReturn(new Collection([$page], 'tl_page')) + ; + + $framework = $this->mockFramework($pageAdapter); + $request = $this->mockRequestWithPath('/foo.html'); + + $routes = $this->getRouteProvider($framework)->getRouteCollectionForRequest($request)->all(); + + $this->assertIsArray($routes); + $this->assertEmpty($routes); + } + private function mockConfigAdapter(array $config): Adapter { $configAdapter = $this->mockAdapter(['get']); From d2b5cd3c9a0a3559fdf27febf12e4622d097ea49 Mon Sep 17 00:00:00 2001 From: Andreas Schempp Date: Wed, 3 Nov 2021 10:42:36 +0100 Subject: [PATCH 06/12] Add the missing dot-env features (see #3627) Description ----------- This enables the Contao Manager to know what configuration can be done in the `.env` file. PS: this needs to be updated after merging into 4.12 Commits ------- e4d491be Added missing dot-env features 7e4d55d1 Updated tests --- manager-bundle/src/ContaoManager/Plugin.php | 7 +++++++ manager-bundle/tests/ContaoManager/PluginTest.php | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/manager-bundle/src/ContaoManager/Plugin.php b/manager-bundle/src/ContaoManager/Plugin.php index 5ab35496f38..86efa75664d 100644 --- a/manager-bundle/src/ContaoManager/Plugin.php +++ b/manager-bundle/src/ContaoManager/Plugin.php @@ -197,6 +197,13 @@ public function getApiFeatures(): array { return [ 'dot-env' => [ + 'APP_SECRET', + 'APP_ENV', + 'COOKIE_WHITELIST', + 'DATABASE_URL', + 'DISABLE_HTTP_CACHE', + 'MAILER_URL', + 'TRACE_LEVEL', 'TRUSTED_PROXIES', 'TRUSTED_HOSTS', ], diff --git a/manager-bundle/tests/ContaoManager/PluginTest.php b/manager-bundle/tests/ContaoManager/PluginTest.php index 86d35a4991b..7a570cf58e8 100644 --- a/manager-bundle/tests/ContaoManager/PluginTest.php +++ b/manager-bundle/tests/ContaoManager/PluginTest.php @@ -288,6 +288,13 @@ public function testReturnsApiFeatures(): void $this->assertSame( [ 'dot-env' => [ + 'APP_SECRET', + 'APP_ENV', + 'COOKIE_WHITELIST', + 'DATABASE_URL', + 'DISABLE_HTTP_CACHE', + 'MAILER_URL', + 'TRACE_LEVEL', 'TRUSTED_PROXIES', 'TRUSTED_HOSTS', ], From 2a19b186afd59495e9fbd5174d0deb40543c5079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ausw=C3=B6ger?= Date: Wed, 3 Nov 2021 11:11:15 +0100 Subject: [PATCH 07/12] Do not set the server_version (see #3623) Description ----------- | Q | A | -----------------| --- | Fixed issues | Fixes #1696 For the record as described in https://github.com/contao/contao/pull/3623#issuecomment-952708988 the `CommandSchedulerListener` must only load the `Cron` service when it needs it. Otherwise the install tool would fail when terminating because the `Cron` service needs ORM metadata which itself needs to know the `server_version`. Commits ------- e071bfdc Do not set server_version to wrong value 2236de4a Skip the cache warmer only if the platform version is missing 1bda30f9 Merge branch '4.9' into fix/doctrine-server-version --- .../ORM/FailTolerantProxyCacheWarmer.php | 56 ++++++++++ .../CommandSchedulerListener.php | 25 +++-- core-bundle/src/Resources/config/listener.yml | 2 +- core-bundle/src/Resources/config/services.yml | 6 + .../ContaoCoreExtensionTest.php | 3 +- .../CommandSchedulerListenerTest.php | 29 +++-- manager-bundle/src/ContaoManager/Plugin.php | 62 ----------- .../tests/ContaoManager/PluginTest.php | 104 +----------------- 8 files changed, 107 insertions(+), 180 deletions(-) create mode 100644 core-bundle/src/Doctrine/ORM/FailTolerantProxyCacheWarmer.php diff --git a/core-bundle/src/Doctrine/ORM/FailTolerantProxyCacheWarmer.php b/core-bundle/src/Doctrine/ORM/FailTolerantProxyCacheWarmer.php new file mode 100644 index 00000000000..725b548b758 --- /dev/null +++ b/core-bundle/src/Doctrine/ORM/FailTolerantProxyCacheWarmer.php @@ -0,0 +1,56 @@ +inner = $inner; + $this->connection = $connection; + } + + public function isOptional(): bool + { + return (bool) $this->inner->isOptional(); + } + + public function warmUp($cacheDir): void + { + // If there are no DB credentials yet (install tool) and the + // server_version was not configured, we have to skip the ORM warmup to + // prevent a DBAL exception during the automatic version detection + try { + $this->connection->getDatabasePlatform(); + } catch (DoctrineDbalException | DoctrineDbalDbalException | \mysqli_sql_exception $e) { + return; + } + + $this->inner->warmUp($cacheDir); + } +} diff --git a/core-bundle/src/EventListener/CommandSchedulerListener.php b/core-bundle/src/EventListener/CommandSchedulerListener.php index 12cb47af070..28bda136077 100644 --- a/core-bundle/src/EventListener/CommandSchedulerListener.php +++ b/core-bundle/src/EventListener/CommandSchedulerListener.php @@ -17,14 +17,21 @@ use Contao\CoreBundle\Framework\ContaoFramework; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\DriverException; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\TerminateEvent; +use Symfony\Contracts\Service\ServiceSubscriberInterface; /** * @internal */ -class CommandSchedulerListener +class CommandSchedulerListener implements ServiceSubscriberInterface { + /** + * @var ContainerInterface + */ + private $locator; + /** * @var ContaoFramework */ @@ -40,16 +47,11 @@ class CommandSchedulerListener */ private $fragmentPath; - /** - * @var Cron - */ - private $cron; - - public function __construct(ContaoFramework $framework, Connection $connection, Cron $cron, string $fragmentPath = '_fragment') + public function __construct(ContainerInterface $locator, ContaoFramework $framework, Connection $connection, string $fragmentPath = '_fragment') { + $this->locator = $locator; $this->framework = $framework; $this->connection = $connection; - $this->cron = $cron; $this->fragmentPath = $fragmentPath; } @@ -59,10 +61,15 @@ public function __construct(ContaoFramework $framework, Connection $connection, public function __invoke(TerminateEvent $event): void { if ($this->framework->isInitialized() && $this->canRunCron($event->getRequest())) { - $this->cron->run(Cron::SCOPE_WEB); + $this->locator->get(Cron::class)->run(Cron::SCOPE_WEB); } } + public static function getSubscribedServices(): array + { + return [Cron::class]; + } + private function canRunCron(Request $request): bool { $pathInfo = $request->getPathInfo(); diff --git a/core-bundle/src/Resources/config/listener.yml b/core-bundle/src/Resources/config/listener.yml index 8ca06b0e45e..109ca80e32d 100644 --- a/core-bundle/src/Resources/config/listener.yml +++ b/core-bundle/src/Resources/config/listener.yml @@ -84,9 +84,9 @@ services: contao.listener.command_scheduler: class: Contao\CoreBundle\EventListener\CommandSchedulerListener arguments: + - '@Psr\Container\ContainerInterface' - '@contao.framework' - '@database_connection' - - '@Contao\CoreBundle\Cron\Cron' - '%fragment.path%' tags: - kernel.event_listener diff --git a/core-bundle/src/Resources/config/services.yml b/core-bundle/src/Resources/config/services.yml index 17eca9a1068..bacb55bb919 100644 --- a/core-bundle/src/Resources/config/services.yml +++ b/core-bundle/src/Resources/config/services.yml @@ -231,6 +231,12 @@ services: tags: - { name: data_collector, template: '@ContaoCore/Collector/contao.html.twig', id: contao } + Contao\CoreBundle\Doctrine\ORM\FailTolerantProxyCacheWarmer: + decorates: doctrine.orm.proxy_cache_warmer + arguments: + - '@Contao\CoreBundle\Doctrine\ORM\FailTolerantProxyCacheWarmer.inner' + - '@database_connection' + contao.doctrine.schema_provider: class: Contao\CoreBundle\Doctrine\Schema\DcaSchemaProvider arguments: diff --git a/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php b/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php index ba6a6f22866..bce5e2ebc06 100644 --- a/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php +++ b/core-bundle/tests/DependencyInjection/ContaoCoreExtensionTest.php @@ -149,6 +149,7 @@ use Contao\ModuleProxy; use Knp\Menu\Matcher\Matcher; use Knp\Menu\Renderer\ListRenderer; +use Psr\Container\ContainerInterface as PsrContainerInterface; use Symfony\Cmf\Component\Routing\DynamicRouter; use Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher; use Symfony\Cmf\Component\Routing\ProviderBasedGenerator; @@ -447,9 +448,9 @@ public function testRegistersTheCommandSchedulerListener(): void $this->assertEquals( [ + new Reference(PsrContainerInterface::class), new Reference('contao.framework'), new Reference('database_connection'), - new Reference(Cron::class), new Reference('%fragment.path%'), ], $definition->getArguments() diff --git a/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php b/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php index 6370755b35c..f6a18cae0aa 100644 --- a/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php +++ b/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php @@ -22,6 +22,7 @@ use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Schema\MySqlSchemaManager; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\TerminateEvent; @@ -38,7 +39,14 @@ public function testRunsTheCommandScheduler(): void ->with(Cron::SCOPE_WEB) ; - $listener = new CommandSchedulerListener($this->mockContaoFramework(), $this->mockConnection(), $cron); + $locator = $this->createMock(ContainerInterface::class); + $locator + ->method('get') + ->with(Cron::class) + ->willReturn($cron) + ; + + $listener = new CommandSchedulerListener($locator, $this->mockContaoFramework(), $this->mockConnection()); $listener($this->getTerminateEvent('contao_frontend')); } @@ -55,7 +63,7 @@ public function testDoesNotRunTheCommandSchedulerIfTheContaoFrameworkIsNotInitia ->method('getAdapter') ; - $listener = new CommandSchedulerListener($framework, $this->mockConnection(), $this->createMock(Cron::class)); + $listener = new CommandSchedulerListener($this->createMock(ContainerInterface::class), $framework, $this->mockConnection()); $listener($this->getTerminateEvent('contao_backend')); } @@ -78,7 +86,7 @@ public function testDoesNotRunTheCommandSchedulerInTheInstallTool(): void $event = new TerminateEvent($this->createMock(KernelInterface::class), $request, new Response()); - $listener = new CommandSchedulerListener($framework, $this->mockConnection(), $this->createMock(Cron::class)); + $listener = new CommandSchedulerListener($this->createMock(ContainerInterface::class), $framework, $this->mockConnection()); $listener($event); } @@ -101,7 +109,7 @@ public function testDoesNotRunTheCommandSchedulerUponFragmentRequests(): void $event = new TerminateEvent($this->createMock(KernelInterface::class), $request, new Response()); - $listener = new CommandSchedulerListener($framework, $this->mockConnection(), $this->createMock(Cron::class)); + $listener = new CommandSchedulerListener($this->createMock(ContainerInterface::class), $framework, $this->mockConnection()); $listener($event); } @@ -124,7 +132,7 @@ public function testDoesNotRunTheCommandSchedulerIfTheInstallationIsIncomplete() ->method('createInstance') ; - $listener = new CommandSchedulerListener($framework, $this->mockConnection(), $this->createMock(Cron::class)); + $listener = new CommandSchedulerListener($this->createMock(ContainerInterface::class), $framework, $this->mockConnection()); $listener($this->getTerminateEvent('contao_backend')); } @@ -148,7 +156,7 @@ public function testDoesNotRunTheCommandSchedulerIfCronjobsAreDisabled(): void ->method('createInstance') ; - $listener = new CommandSchedulerListener($framework, $this->mockConnection(), $this->createMock(Cron::class)); + $listener = new CommandSchedulerListener($this->createMock(ContainerInterface::class), $framework, $this->mockConnection()); $listener($this->getTerminateEvent('contao_frontend')); } @@ -166,13 +174,20 @@ public function testDoesNotRunTheCommandSchedulerIfThereIsADatabaseConnectionErr ->method('run') ; + $locator = $this->createMock(ContainerInterface::class); + $locator + ->method('get') + ->with(Cron::class) + ->willReturn($cron) + ; + $connection = $this->createMock(Connection::class); $connection ->method('isConnected') ->willThrowException(new DriverException('Could not connect', new MysqliException('Invalid password'))) ; - $listener = new CommandSchedulerListener($framework, $connection, $cron); + $listener = new CommandSchedulerListener($locator, $framework, $connection); $listener($this->getTerminateEvent('contao_backend')); } diff --git a/manager-bundle/src/ContaoManager/Plugin.php b/manager-bundle/src/ContaoManager/Plugin.php index 86efa75664d..e9a9623d17c 100644 --- a/manager-bundle/src/ContaoManager/Plugin.php +++ b/manager-bundle/src/ContaoManager/Plugin.php @@ -31,9 +31,6 @@ use Contao\ManagerPlugin\Dependency\DependentPluginInterface; use Contao\ManagerPlugin\Routing\RoutingPluginInterface; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\DBAL\DBALException as DoctrineDbalDbalException; -use Doctrine\DBAL\DriverManager; -use Doctrine\DBAL\Exception as DoctrineDbalException; use FOS\HttpCacheBundle\FOSHttpCacheBundle; use Lexik\Bundle\MaintenanceBundle\LexikMaintenanceBundle; use Nelmio\CorsBundle\NelmioCorsBundle; @@ -64,16 +61,6 @@ class Plugin implements BundlePluginInterface, ConfigPluginInterface, RoutingPlu */ private static $autoloadModules; - /** - * @var callable - */ - private $dbalConnectionFactory; - - public function __construct(callable $dbalConnectionFactory = null) - { - $this->dbalConnectionFactory = $dbalConnectionFactory ?: [DriverManager::class, 'getConnection']; - } - /** * Sets the path to enable autoloading of legacy Contao modules. */ @@ -247,8 +234,6 @@ public function getExtensionConfig($extensionName, array $extensionConfigs, Plug $container->setParameter('env(DATABASE_URL)', $this->getDatabaseUrl($container, $extensionConfigs)); } - $extensionConfigs = $this->addDefaultServerVersion($extensionConfigs, $container); - return $this->addDefaultPdoDriverOptions($extensionConfigs, $container); case 'swiftmailer': @@ -290,53 +275,6 @@ private function handlePrependLocale(array $extensionConfigs, ContainerBuilder $ return $extensionConfigs; } - /** - * Adds the database server version to the Doctrine DBAL configuration. - * - * @return array>>> - */ - private function addDefaultServerVersion(array $extensionConfigs, ContainerBuilder $container): array - { - $params = []; - - foreach ($extensionConfigs as $extensionConfig) { - if (isset($extensionConfig['dbal']['connections']['default'])) { - $params[] = $extensionConfig['dbal']['connections']['default']; - } - } - - if (!empty($params)) { - $params = array_merge(...$params); - } - - $parameterBag = $container->getParameterBag(); - - foreach ($params as $key => $value) { - $params[$key] = $parameterBag->unescapeValue($container->resolveEnvPlaceholders($value, true)); - } - - // If there are no DB credentials yet (install tool), we have to set - // the server version to prevent a DBAL exception (see #1422) - try { - $connection = \call_user_func($this->dbalConnectionFactory, $params); - $connection->connect(); - $connection->query('SHOW TABLES'); - $connection->close(); - } catch (DoctrineDbalException | DoctrineDbalDbalException | \mysqli_sql_exception $e) { - $extensionConfigs[] = [ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'server_version' => '5.5', - ], - ], - ], - ]; - } - - return $extensionConfigs; - } - /** * Sets the PDO driver options if applicable (#2459). * diff --git a/manager-bundle/tests/ContaoManager/PluginTest.php b/manager-bundle/tests/ContaoManager/PluginTest.php index 7a570cf58e8..7f9b1b57bc5 100644 --- a/manager-bundle/tests/ContaoManager/PluginTest.php +++ b/manager-bundle/tests/ContaoManager/PluginTest.php @@ -23,7 +23,6 @@ use Contao\ManagerPlugin\PluginLoader; use Contao\TestCase\ContaoTestCase; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\DBAL\Connection; use FOS\HttpCacheBundle\FOSHttpCacheBundle; use Lexik\Bundle\MaintenanceBundle\LexikMaintenanceBundle; use Nelmio\CorsBundle\NelmioCorsBundle; @@ -439,7 +438,7 @@ public function testSetsTheDatabaseDriverUrl(): void $this->assertSame('mysqli://root:foobar@localhost:3306/contao_test', $bag['env(DATABASE_URL)']); } - public function testAddsTheDefaultServerVersionAndPdoOptions(): void + public function testAddsThePdoOptions(): void { $extensionConfigs = [ [ @@ -456,15 +455,6 @@ public function testAddsTheDefaultServerVersionAndPdoOptions(): void $expect = array_merge( $extensionConfigs, [ - [ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'server_version' => '5.5', - ], - ], - ], - ], [ 'dbal' => [ 'connections' => [ @@ -500,23 +490,10 @@ public function testDoesNotAddDefaultPdoOptionsIfDriverIsNotPdo(): void ], ]; - $expect = array_merge( - $extensionConfigs, - [[ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'server_version' => '5.5', - ], - ], - ], - ]] - ); - $container = $this->getContainer(); $extensionConfig = (new Plugin())->getExtensionConfig('doctrine', $extensionConfigs, $container); - $this->assertSame($expect, $extensionConfig); + $this->assertSame($extensionConfigs, $extensionConfig); } public function testDoesNotAddDefaultPdoOptionsIfUrlIsMysqli(): void @@ -535,19 +512,6 @@ public function testDoesNotAddDefaultPdoOptionsIfUrlIsMysqli(): void ], ]; - $expect = array_merge( - $extensionConfigs, - [[ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'server_version' => '5.5', - ], - ], - ], - ]] - ); - // Adjust the error reporting to suppress mysqli warnings $er = error_reporting(); error_reporting($er ^ E_WARNING ^ E_DEPRECATED); @@ -557,7 +521,7 @@ public function testDoesNotAddDefaultPdoOptionsIfUrlIsMysqli(): void error_reporting($er); - $this->assertSame($expect, $extensionConfig); + $this->assertSame($extensionConfigs, $extensionConfig); } public function testDoesNotOverrideThePdoMultiStatementsOption(): void @@ -577,23 +541,10 @@ public function testDoesNotOverrideThePdoMultiStatementsOption(): void ], ]; - $expect = array_merge( - $extensionConfigs, - [[ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'server_version' => '5.5', - ], - ], - ], - ]] - ); - $container = $this->getContainer(); $extensionConfig = (new Plugin())->getExtensionConfig('doctrine', $extensionConfigs, $container); - $this->assertSame($expect, $extensionConfig); + $this->assertSame($extensionConfigs, $extensionConfig); } public function testUpdatesTheMailerTransport(): void @@ -709,53 +660,6 @@ public function getMailerParameters(): \Generator ]; } - public function testRetrievesTheConnectionParametersFromTheConfiguration(): void - { - $pluginLoader = $this->createMock(PluginLoader::class); - $container = new PluginContainerBuilder($pluginLoader, []); - - $extensionConfigs = [ - [ - 'dbal' => [ - 'connections' => [ - 'default' => [ - 'url' => '%env(DATABASE_URL)%', - 'password' => '@foobar', - ], - ], - ], - ], - ]; - - $connection = $this->createMock(Connection::class); - $connection - ->expects($this->once()) - ->method('connect') - ; - - $connection - ->expects($this->once()) - ->method('close') - ; - - $dbalConnectionFactory = function ($params) use ($connection) { - $this->assertSame( - [ - 'url' => 'mysql://root:%%40foobar@localhost:3306/database', - 'password' => '@foobar', - ], - $params - ); - - return $connection; - }; - - $_SERVER['DATABASE_URL'] = $_ENV['DATABASE_URL'] = 'mysql://root:%%40foobar@localhost:3306/database'; - - $plugin = new Plugin($dbalConnectionFactory); - $plugin->getExtensionConfig('doctrine', $extensionConfigs, $container); - } - private function getContainer(): PluginContainerBuilder { $pluginLoader = $this->createMock(PluginLoader::class); From 142fedd74dd968b8289c1ed4d77c3e264002bc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Severin=20Gl=C3=B6ckle?= Date: Wed, 3 Nov 2021 12:35:53 +0100 Subject: [PATCH 08/12] Handle pages from PageRegistry in DebugPagesCommand (see #3507) Description ----------- | Q | A | -----------------| --- | Fixed issues | Fixes #3505 The `DebugPagesCommand` now lists both pages from `$GLOBALS['TL_PTY']` and from the `PageRegistry`. Additionally, the output of the command is optimized for empty values: - Show token `-` for empty arrays in requirements, defaults & options instead of empty space - Show token `-` for empty strings in path & url suffix instead of empty space Commits ------- 9186d708 handle pages from PageRegistry in DebugPagesCommand ccdd683c cleanup test setup 1acbc91c simplify generator of empty values f7c89f3b Merge branch '4.12' into fix/debug-pages-command --- core-bundle/src/Command/DebugPagesCommand.php | 8 +- .../tests/Command/DebugPagesCommandTest.php | 157 ++++++++++++++++++ 2 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 core-bundle/tests/Command/DebugPagesCommandTest.php diff --git a/core-bundle/src/Command/DebugPagesCommand.php b/core-bundle/src/Command/DebugPagesCommand.php index 0f444689ac8..4dc7d690837 100644 --- a/core-bundle/src/Command/DebugPagesCommand.php +++ b/core-bundle/src/Command/DebugPagesCommand.php @@ -75,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $rows = []; - $types = array_keys($GLOBALS['TL_PTY']); + $types = array_unique(array_merge(array_keys($GLOBALS['TL_PTY']), $this->pageRegistry->keys())); natsort($types); foreach ($types as $type) { @@ -86,8 +86,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $rows[] = [ $type, - $config ? $config->getPath() : '*', - $config ? $config->getUrlSuffix() : '*', + $config && $config->getPath() ? $config->getPath() : '*', + $config && $config->getUrlSuffix() ? $config->getUrlSuffix() : '*', $this->pageRegistry->supportsContentComposition($page) ? 'yes' : 'no', isset($this->routeEnhancers[$type]) ? \get_class($this->routeEnhancers[$type]) : '-', $config ? $this->generateArray($config->getRequirements()) : '-', @@ -124,6 +124,6 @@ static function ($carry, $item): int { $return[] = sprintf('%s : %s', str_pad($k, $length, ' ', STR_PAD_RIGHT), (string) $v); } - return implode("\n", $return); + return !empty($return) ? implode("\n", $return) : '-'; } } diff --git a/core-bundle/tests/Command/DebugPagesCommandTest.php b/core-bundle/tests/Command/DebugPagesCommandTest.php new file mode 100644 index 00000000000..c2b926c10e4 --- /dev/null +++ b/core-bundle/tests/Command/DebugPagesCommandTest.php @@ -0,0 +1,157 @@ +mockContaoFramework(); + $pageRegistry = $this->createMock(PageRegistry::class); + $command = new DebugPagesCommand($framework, $pageRegistry); + + $this->assertSame('debug:pages', $command->getName()); + $this->assertSame(0, $command->getDefinition()->getArgumentCount()); + $this->assertEmpty($command->getDefinition()->getOptions()); + } + + /** + * @dataProvider commandOutputProvider + */ + public function testCommandOutput(array $pages, array $legacyPages, string $expectedOutput): void + { + $container = $this->getContainerWithContaoConfiguration(); + $container->setParameter('contao.resources_paths', $this->getTempDir()); + + (new Filesystem())->mkdir($this->getTempDir().'/languages/en'); + + System::setContainer($container); + + $pageRegistry = $this->createMock(PageRegistry::class); + $pageRegistry + ->expects($this->once()) + ->method('keys') + ->willReturn(array_values(array_column($pages, 0))) + ; + + $pageRegistry + ->method('supportsContentComposition') + ->willReturnCallback( + static function (PageModel $pageModel): bool { + return 'regular' === $pageModel->type; + } + ) + ; + + $command = new DebugPagesCommand($this->mockContaoFramework(), $pageRegistry); + + $GLOBALS['TL_PTY'] = $legacyPages; + + foreach ($pages as $page) { + $command->add($page[0], $page[1], $page[2]); + } + + $commandTester = new CommandTester($command); + $commandTester->execute([]); + + $this->assertSame($expectedOutput, $commandTester->getDisplay(true)); + + unset($GLOBALS['TL_PTY']); + } + + public function commandOutputProvider(): \Generator + { + yield 'Regular pages list' => [ + [ + ['root', new RouteConfig('foo', null, '.php', [], [], ['_controller' => RootPageController::class]), null], + ], + [ + 'regular' => PageRegular::class, + 'forward' => PageForward::class, + 'redirect' => PageRedirect::class, + 'root' => PageRoot::class, + 'logout' => PageLogout::class, + 'error_401' => PageError401::class, + 'error_403' => PageError403::class, + 'error_404' => PageError404::class, + ], + <<<'OUTPUT' + + Contao Pages + ============ + + ----------- ------ ------------ --------------------- ---------------- -------------- -------------------------------------------------------------------- --------- + Type Path URL Suffix Content Composition Route Enhancer Requirements Defaults Options + ----------- ------ ------------ --------------------- ---------------- -------------- -------------------------------------------------------------------- --------- + error_401 * * no - - - - + error_403 * * no - - - - + error_404 * * no - - - - + forward * * no - - - - + logout * * no - - - - + redirect * * no - - - - + regular * * yes - - - - + root foo .php no - - _controller : Contao\CoreBundle\Controller\Page\RootPageController - + ----------- ------ ------------ --------------------- ---------------- -------------- -------------------------------------------------------------------- --------- + + + OUTPUT + ]; + + yield 'With custom pages' => [ + [ + ['root', new RouteConfig('foo', null, '.php', [], [], ['_controller' => RootPageController::class]), null], + ['bar', new RouteConfig('foo/bar', null, '.html', [], [], ['_controller' => TestPageController::class]), null], + ['baz', new RouteConfig(null, null, null, ['page' => '\d+'], ['utf8' => false], []), null], + ], + [ + 'regular' => PageRegular::class, + 'root' => PageRoot::class, + ], + <<<'OUTPUT' + + Contao Pages + ============ + + --------- --------- ------------ --------------------- ---------------- -------------- ----------------------------------------------------------------------------- -------------- + Type Path URL Suffix Content Composition Route Enhancer Requirements Defaults Options + --------- --------- ------------ --------------------- ---------------- -------------- ----------------------------------------------------------------------------- -------------- + bar foo/bar .html no - - _controller : Contao\CoreBundle\Fixtures\Controller\Page\TestPageController - + baz * * no - page : \d+ - utf8 : false + regular * * yes - - - - + root foo .php no - - _controller : Contao\CoreBundle\Controller\Page\RootPageController - + --------- --------- ------------ --------------------- ---------------- -------------- ----------------------------------------------------------------------------- -------------- + + + OUTPUT + ]; + } +} From c27d651580d29c6a665ac4bb900c42599aaf57bf Mon Sep 17 00:00:00 2001 From: Leo Feyer Date: Wed, 3 Nov 2021 17:23:45 +0100 Subject: [PATCH 09/12] Add the HtmlDecoder service (see #3602) Description ----------- Implements #3015 Commits ------- 4178d959 Add the HtmlDecoder service bb4157bb Fix PhpStan 032608d1 Remove the substrHtml() method d202680c Add more deprecations --- .../src/Resources/contao/classes/Events.php | 9 +- .../contao/modules/ModuleEventReader.php | 8 +- core-bundle/src/Resources/config/services.yml | 4 + .../src/Resources/contao/classes/Frontend.php | 6 +- .../contao/library/Contao/StringUtil.php | 94 ++++++---------- .../contao/library/Contao/Template.php | 5 +- .../src/Resources/contao/models/PageModel.php | 6 +- .../contao/modules/ModuleArticle.php | 7 +- .../contao/modules/ModuleBreadcrumb.php | 4 +- .../CoreResponseContextFactory.php | 10 +- core-bundle/src/String/HtmlDecoder.php | 70 ++++++++++++ core-bundle/tests/Contao/StringUtilTest.php | 59 ---------- .../CoreResponseContextFactoryTest.php | 5 + core-bundle/tests/String/HtmlDecoderTest.php | 101 ++++++++++++++++++ .../Resources/contao/modules/ModuleFaq.php | 7 +- .../contao/modules/ModuleFaqReader.php | 8 +- .../src/Resources/contao/classes/News.php | 8 +- .../contao/modules/ModuleNewsReader.php | 8 +- .../contao/modules/ModuleNewsletterReader.php | 5 +- 19 files changed, 270 insertions(+), 154 deletions(-) create mode 100644 core-bundle/src/String/HtmlDecoder.php create mode 100644 core-bundle/tests/String/HtmlDecoderTest.php diff --git a/calendar-bundle/src/Resources/contao/classes/Events.php b/calendar-bundle/src/Resources/contao/classes/Events.php index 63a90be4669..f4db3ceed76 100644 --- a/calendar-bundle/src/Resources/contao/classes/Events.php +++ b/calendar-bundle/src/Resources/contao/classes/Events.php @@ -11,6 +11,7 @@ namespace Contao; use Contao\CoreBundle\Security\ContaoCorePermissions; +use Contao\CoreBundle\String\HtmlDecoder; /** * Provide methods to get all events of a certain period from the database. @@ -490,10 +491,12 @@ public static function generateEventUrl($objEvent, $blnAbsolute=false) */ public static function getSchemaOrgData(CalendarEventsModel $objEvent): array { + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); + $jsonLd = array( '@type' => 'Event', 'identifier' => '#/schema/events/' . $objEvent->id, - 'name' => StringUtil::inputEncodedToPlainText($objEvent->title), + 'name' => $htmlDecoder->inputEncodedToPlainText($objEvent->title), 'url' => self::generateEventUrl($objEvent), 'startDate' => $objEvent->addTime ? date('Y-m-d\TH:i:sP', $objEvent->startTime) : date('Y-m-d', $objEvent->startTime) ); @@ -507,14 +510,14 @@ public static function getSchemaOrgData(CalendarEventsModel $objEvent): array { $jsonLd['location'] = array( '@type' => 'Place', - 'name' => StringUtil::inputEncodedToPlainText($objEvent->location) + 'name' => $htmlDecoder->inputEncodedToPlainText($objEvent->location) ); if ($objEvent->address) { $jsonLd['location']['address'] = array( '@type' => 'PostalAddress', - 'description' => StringUtil::inputEncodedToPlainText($objEvent->address) + 'description' => $htmlDecoder->inputEncodedToPlainText($objEvent->address) ); } } diff --git a/calendar-bundle/src/Resources/contao/modules/ModuleEventReader.php b/calendar-bundle/src/Resources/contao/modules/ModuleEventReader.php index 83184febed9..6f4a3eeecda 100644 --- a/calendar-bundle/src/Resources/contao/modules/ModuleEventReader.php +++ b/calendar-bundle/src/Resources/contao/modules/ModuleEventReader.php @@ -16,6 +16,7 @@ use Contao\CoreBundle\Image\Studio\Studio; use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; /** * Front end module "event reader". @@ -135,6 +136,7 @@ protected function compile() { /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); if ($objEvent->pageTitle) { @@ -142,16 +144,16 @@ protected function compile() } elseif ($objEvent->title) { - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($objEvent->title)); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($objEvent->title)); } if ($objEvent->description) { - $htmlHeadBag->setMetaDescription(StringUtil::inputEncodedToPlainText($objEvent->description)); + $htmlHeadBag->setMetaDescription($htmlDecoder->inputEncodedToPlainText($objEvent->description)); } elseif ($objEvent->teaser) { - $htmlHeadBag->setMetaDescription(StringUtil::htmlToPlainText($objEvent->teaser)); + $htmlHeadBag->setMetaDescription($htmlDecoder->htmlToPlainText($objEvent->teaser)); } if ($objEvent->robots) diff --git a/core-bundle/src/Resources/config/services.yml b/core-bundle/src/Resources/config/services.yml index 87ecb4b8a82..f8351e6f572 100644 --- a/core-bundle/src/Resources/config/services.yml +++ b/core-bundle/src/Resources/config/services.yml @@ -708,6 +708,7 @@ services: - '@Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor' - '@event_dispatcher' - '@contao.security.token_checker' + - '@Contao\CoreBundle\String\HtmlDecoder' public: true contao.search.indexer.default: @@ -900,6 +901,9 @@ services: - '@translator' public: true + Contao\CoreBundle\String\HtmlDecoder: + public: true + contao.token_generator: class: Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator arguments: diff --git a/core-bundle/src/Resources/contao/classes/Frontend.php b/core-bundle/src/Resources/contao/classes/Frontend.php index 7cde95685d3..b0639b14562 100644 --- a/core-bundle/src/Resources/contao/classes/Frontend.php +++ b/core-bundle/src/Resources/contao/classes/Frontend.php @@ -574,12 +574,12 @@ public static function getMetaData($strData, $strLanguage) * * @return string * - * @deprecated Deprecated since Contao 4.12, to be removed in Contao 5.0. - * Use StringUtil::htmlToPlainText() instead. + * @deprecated Deprecated since Contao 4.12, to be removed in Contao 5.0; + * use the Contao\CoreBundle\String\HtmlDecoder service instead */ protected function prepareMetaDescription($strText) { - trigger_deprecation('contao/core-bundle', '4.12', 'Using "Contao\Frontend::prepareMetaDescription()" has been deprecated and will no longer work Contao 5.0. Use Contao\StringUtil::htmlToPlainText() instead.'); + trigger_deprecation('contao/core-bundle', '4.12', 'Using "Contao\Frontend::prepareMetaDescription()" has been deprecated and will no longer work Contao 5.0. Use the "Contao\CoreBundle\String\HtmlDecoder" service instead.'); $strText = $this->replaceInsertTags($strText, false); $strText = strip_tags($strText); diff --git a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php index 3e731863204..9ed2fd77645 100644 --- a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php +++ b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php @@ -10,6 +10,7 @@ namespace Contao; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\CoreBundle\Util\SimpleTokenParser; use Webmozart\PathUtil\Path; @@ -51,7 +52,6 @@ public static function substr($strString, $intNumberOfChars, $strEllipsis=' …' $intCharCount = 0; $arrWords = array(); $arrChunks = preg_split('/\s+/', $strString); - $blnAddEllipsis = false; foreach ($arrChunks as $strChunk) { @@ -70,27 +70,27 @@ public static function substr($strString, $intNumberOfChars, $strEllipsis=' …' $arrWords[] = mb_substr($strChunk, 0, $intNumberOfChars); } - if ($strEllipsis !== false) - { - $blnAddEllipsis = true; - } - break; } + if ($strEllipsis === false) + { + trigger_deprecation('contao/core-bundle', '4.0', 'Passing "false" as third argument to "Contao\StringUtil::substr()" has been deprecated and will no longer work in Contao 5.0. Pass an empty string instead.'); + $strEllipsis = ''; + } + // Deprecated since Contao 4.0, to be removed in Contao 5.0 if ($strEllipsis === true) { trigger_deprecation('contao/core-bundle', '4.0', 'Passing "true" as third argument to "Contao\StringUtil::substr()" has been deprecated and will no longer work in Contao 5.0. Pass the ellipsis string instead.'); - $strEllipsis = ' …'; } - return implode(' ', $arrWords) . ($blnAddEllipsis ? $strEllipsis : ''); + return implode(' ', $arrWords) . $strEllipsis; } /** - * Shorten a HTML string to a given number of characters + * Shorten an HTML string to a given number of characters * * The function preserves words, so the result might be a bit shorter or * longer than the number of characters given. It preserves allowed tags. @@ -234,6 +234,10 @@ public static function decodeEntities($strString, $strQuoteStyle=ENT_QUOTES, $st { $strCharset = 'UTF-8'; } + else + { + trigger_deprecation('contao/core-bundle', '4.13', 'Passing a charset to StringUtil::decodeEntities() has been deprecated and will no longer work in Contao 5.0. Always use UTF-8 instead.'); + } $strString = preg_replace('/(&#*\w+)[\x00-\x20]+;/i', '$1;', $strString); $strString = preg_replace('/(&#x*)([0-9a-f]+);/i', '$1$2;', $strString); @@ -483,9 +487,13 @@ public static function splitCsv($strString, $strDelimiter=',') * @param string $strString The HTML5 string * * @return string The XHTML string + * + * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0 */ public static function toXhtml($strString) { + trigger_deprecation('contao/core-bundle', '4.13', 'The "StringUtil::toXhtml()" method has been deprecated and will no longer work in Contao 5.0.'); + $arrPregReplace = array ( '/<(br|hr|img)([^>]*)>/i' => '<$1$2 />', // Close stand-alone tags @@ -679,9 +687,7 @@ public static function sanitizeFileName($strName) } // Remove special characters not supported on e.g. Windows - $strName = str_replace(array('\\', '/', ':', '*', '?', '"', '<', '>', '|'), '-', $strName); - - return $strName; + return str_replace(array('\\', '/', ':', '*', '?', '"', '<', '>', '|'), '-', $strName); } /** @@ -827,16 +833,7 @@ public static function specialcharsAttribute($strString, $blnStripInsertTags=fal $strString = str_replace('|urlattr|attr}}', '|urlattr}}', $strString); // Encode all remaining single closing curly braces - $strString = preg_replace_callback( - '/}}?/', - static function ($match) - { - return \strlen($match[0]) === 2 ? $match[0] : '}'; - }, - $strString - ); - - return $strString; + return preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === 2 ? $match[0] : '}', $strString); } /** @@ -856,14 +853,7 @@ public static function specialcharsUrl($strString, $blnStripInsertTags=false, $b $strString = preg_replace('/(?:\|urlattr|\|attr)?}}/', '|urlattr}}', $strString); // Encode all remaining single closing curly braces - $strString = preg_replace_callback( - '/}}?/', - static function ($match) - { - return \strlen($match[0]) === 2 ? $match[0] : '}'; - }, - $strString - ); + $strString = preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === 2 ? $match[0] : '}', $strString); $colonRegEx = '(' . ':' // Plain text colon @@ -1096,26 +1086,16 @@ public static function revertInputEncoding(string $strValue): string * * Strips or replaces insert tags, strips HTML tags, decodes entities, escapes insert tag braces. * - * @see StringUtil::revertInputEncoding() - * * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them + * + * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5; + * use the Contao\CoreBundle\String\HtmlDecoder service instead */ public static function inputEncodedToPlainText(string $strValue, bool $blnRemoveInsertTags = false): string { - if ($blnRemoveInsertTags) - { - $strValue = static::stripInsertTags($strValue); - } - else - { - $strValue = Controller::replaceInsertTags($strValue, false); - } + trigger_deprecation('contao/core-bundle', '4.13', 'Using "StringUtil::inputEncodedToPlainText()" has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\HtmlDecoder" service instead.'); - $strValue = strip_tags($strValue); - $strValue = static::revertInputEncoding($strValue); - $strValue = str_replace(array('{{', '}}'), array('[{]', '[}]'), $strValue); - - return $strValue; + return System::getContainer()->get(HtmlDecoder::class)->inputEncodedToPlainText($strValue, $blnRemoveInsertTags); } /** @@ -1125,30 +1105,16 @@ public static function inputEncodedToPlainText(string $strValue, bool $blnRemove * entities and encoded entities and is meant to be used with content from * fields that have the allowHtml flag enabled. * - * @see StringUtil::inputEncodedToPlainText() - * * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them + * + * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5; + * use the Contao\CoreBundle\String\HtmlDecoder service instead */ public static function htmlToPlainText(string $strValue, bool $blnRemoveInsertTags = false): string { - if (!$blnRemoveInsertTags) - { - $strValue = Controller::replaceInsertTags($strValue, false); - } + trigger_deprecation('contao/core-bundle', '4.13', 'Using "StringUtil::htmlToPlainText()" has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\HtmlDecoder" service instead.'); - // Add new lines before and after block level elements - $strValue = preg_replace( - array('/[\r\n]+/', '/<\/?(?:br|blockquote|div|dl|figcaption|figure|footer|h\d|header|hr|li|p|pre|tr)\b/i'), - array(' ', "\n$0"), - $strValue - ); - - $strValue = static::inputEncodedToPlainText($strValue, true); - - // Remove duplicate line breaks and spaces - $strValue = trim(preg_replace(array('/[^\S\n]+/', '/\s*\n\s*/'), array(' ', "\n"), $strValue)); - - return $strValue; + return System::getContainer()->get(HtmlDecoder::class)->htmlToPlainText($strValue, $blnRemoveInsertTags); } } diff --git a/core-bundle/src/Resources/contao/library/Contao/Template.php b/core-bundle/src/Resources/contao/library/Contao/Template.php index d8339af0685..2e82f517e05 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Template.php +++ b/core-bundle/src/Resources/contao/library/Contao/Template.php @@ -15,6 +15,7 @@ use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager; use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\Image\ImageInterface; use Contao\Image\PictureConfiguration; use MatthiasMullie\Minify\CSS; @@ -408,7 +409,7 @@ public function trans($strId, array $arrParams=array(), $strDomain='contao_defau */ public function rawPlainText(string $value, bool $removeInsertTags = false): string { - return StringUtil::inputEncodedToPlainText($value, $removeInsertTags); + return System::getContainer()->get(HtmlDecoder::class)->inputEncodedToPlainText($value, $removeInsertTags); } /** @@ -423,7 +424,7 @@ public function rawPlainText(string $value, bool $removeInsertTags = false): str */ public function rawHtmlToPlainText(string $value, bool $removeInsertTags = false): string { - return StringUtil::htmlToPlainText($value, $removeInsertTags); + return System::getContainer()->get(HtmlDecoder::class)->htmlToPlainText($value, $removeInsertTags); } /** diff --git a/core-bundle/src/Resources/contao/models/PageModel.php b/core-bundle/src/Resources/contao/models/PageModel.php index cce68af136f..a0e797ed3a1 100644 --- a/core-bundle/src/Resources/contao/models/PageModel.php +++ b/core-bundle/src/Resources/contao/models/PageModel.php @@ -16,6 +16,7 @@ use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager; use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\CoreBundle\Util\LocaleUtil; use Contao\Model\Collection; use Contao\Model\Registry; @@ -326,15 +327,16 @@ public function __set($strKey, $varValue) { /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); switch ($strKey) { case 'pageTitle': - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($varValue ?? '')); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($varValue ?? '')); break; case 'description': - $htmlHeadBag->setMetaDescription(StringUtil::inputEncodedToPlainText($varValue ?? '')); + $htmlHeadBag->setMetaDescription($htmlDecoder->inputEncodedToPlainText($varValue ?? '')); break; case 'robots': diff --git a/core-bundle/src/Resources/contao/modules/ModuleArticle.php b/core-bundle/src/Resources/contao/modules/ModuleArticle.php index be2196e76c7..54f26e56fde 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleArticle.php +++ b/core-bundle/src/Resources/contao/modules/ModuleArticle.php @@ -12,6 +12,7 @@ use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; /** * Provides methodes to handle articles. @@ -171,13 +172,15 @@ protected function compile() if ($responseContext && $responseContext->has(HtmlHeadBag::class)) { + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); + /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($this->title ?? '')); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($this->title ?? '')); if ($this->teaser) { - $htmlHeadBag->setMetaDescription(StringUtil::htmlToPlainText($this->teaser)); + $htmlHeadBag->setMetaDescription($htmlDecoder->htmlToPlainText($this->teaser)); } } } diff --git a/core-bundle/src/Resources/contao/modules/ModuleBreadcrumb.php b/core-bundle/src/Resources/contao/modules/ModuleBreadcrumb.php index 7b71fd33210..e1aa1f26a3b 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleBreadcrumb.php +++ b/core-bundle/src/Resources/contao/modules/ModuleBreadcrumb.php @@ -10,6 +10,7 @@ namespace Contao; +use Contao\CoreBundle\String\HtmlDecoder; use Symfony\Component\Routing\Exception\ExceptionInterface; /** @@ -227,6 +228,7 @@ protected function compile() ); $position = 0; + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); foreach ($items as $item) { @@ -235,7 +237,7 @@ protected function compile() 'position' => ++$position, 'item' => array( '@id' => $item['href'] ?: './', - 'name' => StringUtil::inputEncodedToPlainText($item['link']) + 'name' => $htmlDecoder->inputEncodedToPlainText($item['link']) ) ); } diff --git a/core-bundle/src/Routing/ResponseContext/CoreResponseContextFactory.php b/core-bundle/src/Routing/ResponseContext/CoreResponseContextFactory.php index a93f61f2e95..d1d266f1753 100644 --- a/core-bundle/src/Routing/ResponseContext/CoreResponseContextFactory.php +++ b/core-bundle/src/Routing/ResponseContext/CoreResponseContextFactory.php @@ -16,8 +16,8 @@ use Contao\CoreBundle\Routing\ResponseContext\JsonLd\ContaoPageSchema; use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager; use Contao\CoreBundle\Security\Authentication\Token\TokenChecker; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\PageModel; -use Contao\StringUtil; use Spatie\SchemaOrg\WebPage; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -26,12 +26,14 @@ class CoreResponseContextFactory private ResponseContextAccessor $responseContextAccessor; private EventDispatcherInterface $eventDispatcher; private TokenChecker $tokenChecker; + private HtmlDecoder $htmlDecoder; - public function __construct(ResponseContextAccessor $responseContextAccessor, EventDispatcherInterface $eventDispatcher, TokenChecker $tokenChecker) + public function __construct(ResponseContextAccessor $responseContextAccessor, EventDispatcherInterface $eventDispatcher, TokenChecker $tokenChecker, HtmlDecoder $htmlDecoder) { $this->responseContextAccessor = $responseContextAccessor; $this->eventDispatcher = $eventDispatcher; $this->tokenChecker = $tokenChecker; + $this->htmlDecoder = $htmlDecoder; } public function createResponseContext(): ResponseContext @@ -72,11 +74,11 @@ public function createContaoWebpageResponseContext(PageModel $pageModel): Respon /** @var JsonLdManager $jsonLdManager */ $jsonLdManager = $context->get(JsonLdManager::class); - $title = StringUtil::inputEncodedToPlainText($pageModel->pageTitle ?: $pageModel->title ?: ''); + $title = $this->htmlDecoder->inputEncodedToPlainText($pageModel->pageTitle ?: $pageModel->title ?: ''); $htmlHeadBag ->setTitle($title ?: '') - ->setMetaDescription(StringUtil::inputEncodedToPlainText($pageModel->description ?: '')) + ->setMetaDescription($this->htmlDecoder->inputEncodedToPlainText($pageModel->description ?: '')) ; if ($pageModel->robots) { diff --git a/core-bundle/src/String/HtmlDecoder.php b/core-bundle/src/String/HtmlDecoder.php new file mode 100644 index 00000000000..bb8328a8a5f --- /dev/null +++ b/core-bundle/src/String/HtmlDecoder.php @@ -0,0 +1,70 @@ +replace($val, false); + } + + $val = strip_tags($val); + $val = StringUtil::revertInputEncoding($val); + + return str_replace(['{{', '}}'], ['[{]', '[}]'], $val); + } + + /** + * Converts an HTML string to plain text with normalized white space. + * + * It handles all Contao input encoding specifics like insert tags, basic + * entities and encoded entities and is meant to be used with content from + * fields that have the allowHtml flag enabled. + * + * @param bool $removeInsertTags True to remove insert tags instead of replacing them + */ + public function htmlToPlainText(string $val, bool $removeInsertTags = false): string + { + if (!$removeInsertTags) { + $val = (new InsertTags())->replace($val, false); + } + + // Add new lines before and after block level elements + $val = preg_replace( + ['/[\r\n]+/', '/<\/?(?:br|blockquote|div|dl|figcaption|figure|footer|h\d|header|hr|li|p|pre|tr)\b/i'], + [' ', "\n$0"], + $val + ); + + $val = $this->inputEncodedToPlainText($val, true); + + // Remove duplicate line breaks and spaces + return trim(preg_replace(['/[^\S\n]+/', '/\s*\n\s*/'], [' ', "\n"], $val)); + } +} diff --git a/core-bundle/tests/Contao/StringUtilTest.php b/core-bundle/tests/Contao/StringUtilTest.php index be49cff618f..0dc76f68f1f 100644 --- a/core-bundle/tests/Contao/StringUtilTest.php +++ b/core-bundle/tests/Contao/StringUtilTest.php @@ -175,65 +175,6 @@ public function getRevertInputEncoding(): \Generator yield ["Cont\xE4o invalid UTF-8", "Cont\u{FFFD}o invalid UTF-8"]; } - /** - * @dataProvider getInputEncodedToPlainText - */ - public function testInputEncodedToPlainText(string $source, string $expected, bool $removeInsertTags = false): void - { - $this->assertSame($expected, StringUtil::inputEncodedToPlainText($source, $removeInsertTags)); - - Input::setGet('value', $expected); - $inputEncoded = Input::get('value'); - Input::setGet('value', null); - - // Test input encoding round trip - $this->assertSame($expected, StringUtil::inputEncodedToPlainText($inputEncoded, true)); - $this->assertSame($expected, StringUtil::inputEncodedToPlainText($inputEncoded, false)); - } - - public function getInputEncodedToPlainText(): \Generator - { - yield ['foobar', 'foobar']; - yield ['foo{{email::test@example.com}}bar', 'footest@example.combar']; - yield ['foo{{email::test@example.com}}bar', 'foobar', true]; - yield ['{{date::...}}', '...']; - yield ['{{date::...}}', '', true]; - yield ["<>&\u{A0}[lt][gt][&][nbsp]", "<>&\u{A0}<>&\u{A0}", true]; - yield ['I <3 Contao', 'I <3 Contao']; - yield ['Remove unexpected HTML tags', 'Remove unexpected HTML tags']; - yield ['Keep non-HTML <tags> intact', 'Keep non-HTML intact']; - yield ["Cont\xE4o invalid UTF-8", "Cont\u{FFFD}o invalid UTF-8"]; - yield ['{{date}}', '[{]date[}]']; - } - - /** - * @dataProvider getHtmlToPlainText - */ - public function testHtmlToPlainText(string $source, string $expected, bool $removeInsertTags = false): void - { - $this->assertSame($expected, StringUtil::htmlToPlainText($source, $removeInsertTags)); - - Input::setPost('value', str_replace(['{{', '}}'], ['[{]', '[}]'], $source)); - $inputXssStripped = str_replace(['{{', '}}'], ['{{', '}}'], Input::postHtml('value', true)); - Input::setPost('value', null); - - $this->assertSame($expected, StringUtil::htmlToPlainText($inputXssStripped, $removeInsertTags)); - } - - public function getHtmlToPlainText(): \Generator - { - yield from $this->getInputEncodedToPlainText(); - - yield ['foo
bar{{br}}baz', "foo\nbar\nbaz"]; - yield [" \t\r\nfoo \t\r\n \r\n\t bar \t\r\n", 'foo bar']; - yield [" \t\r\n
foo \t
\r\n \r\n\t
bar
\t\r\n", "foo\nbar"]; - - yield [ - '

Headline

Text
  • List 1
  • List 2

Inlinetext and link

single newline', - "Headline\nText\nList 1\nList 2\nInlinetext and link\nsingle newline", - ]; - } - /** * @dataProvider validEncodingsProvider */ diff --git a/core-bundle/tests/Routing/ResponseContext/CoreResponseContextFactoryTest.php b/core-bundle/tests/Routing/ResponseContext/CoreResponseContextFactoryTest.php index e1ca4b2e036..800dba156db 100644 --- a/core-bundle/tests/Routing/ResponseContext/CoreResponseContextFactoryTest.php +++ b/core-bundle/tests/Routing/ResponseContext/CoreResponseContextFactoryTest.php @@ -18,6 +18,7 @@ use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\CoreBundle\Security\Authentication\Token\TokenChecker; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\PageModel; use Contao\System; use Contao\TestCase\ContaoTestCase; @@ -38,6 +39,7 @@ public function testResponseContext(): void $responseAccessor, $this->createMock(EventDispatcherInterface::class), $this->createMock(TokenChecker::class), + new HtmlDecoder() ); $responseContext = $factory->createResponseContext(); @@ -57,6 +59,7 @@ public function testWebpageResponseContext(): void $responseAccessor, $this->createMock(EventDispatcherInterface::class), $this->createMock(TokenChecker::class), + new HtmlDecoder() ); $responseContext = $factory->createWebpageResponseContext(); @@ -103,6 +106,7 @@ public function testContaoWebpageResponseContext(): void $responseAccessor, $this->createMock(EventDispatcherInterface::class), $this->createMock(TokenChecker::class), + new HtmlDecoder() ); $responseContext = $factory->createContaoWebpageResponseContext($pageModel); @@ -146,6 +150,7 @@ public function testDecodingAndCleanupOnContaoResponseContext(): void $this->createMock(ResponseContextAccessor::class), $this->createMock(EventDispatcherInterface::class), $this->createMock(TokenChecker::class), + new HtmlDecoder() ); $responseContext = $factory->createContaoWebpageResponseContext($pageModel); diff --git a/core-bundle/tests/String/HtmlDecoderTest.php b/core-bundle/tests/String/HtmlDecoderTest.php new file mode 100644 index 00000000000..19c879da0b1 --- /dev/null +++ b/core-bundle/tests/String/HtmlDecoderTest.php @@ -0,0 +1,101 @@ +createMock(TokenChecker::class); + $tokenChecker + ->method('hasFrontendUser') + ->willReturn(false) + ; + + $container = $this->getContainerWithContaoConfiguration(); + $container->set('contao.security.token_checker', $tokenChecker); + + System::setContainer($container); + } + + /** + * @dataProvider getInputEncodedToPlainText + */ + public function testInputEncodedToPlainText(string $source, string $expected, bool $removeInsertTags = false): void + { + $htmlDecoder = new HtmlDecoder(); + + $this->assertSame($expected, $htmlDecoder->inputEncodedToPlainText($source, $removeInsertTags)); + + Input::setGet('value', $expected); + $inputEncoded = Input::get('value'); + Input::setGet('value', null); + + // Test input encoding round trip + $this->assertSame($expected, $htmlDecoder->inputEncodedToPlainText($inputEncoded, true)); + $this->assertSame($expected, $htmlDecoder->inputEncodedToPlainText($inputEncoded, false)); + } + + public function getInputEncodedToPlainText(): \Generator + { + yield ['foobar', 'foobar']; + yield ['foo{{email::test@example.com}}bar', 'footest@example.combar']; + yield ['foo{{email::test@example.com}}bar', 'foobar', true]; + yield ['{{date::...}}', '...']; + yield ['{{date::...}}', '', true]; + yield ["<>&\u{A0}[lt][gt][&][nbsp]", "<>&\u{A0}<>&\u{A0}", true]; + yield ['I <3 Contao', 'I <3 Contao']; + yield ['Remove unexpected HTML tags', 'Remove unexpected HTML tags']; + yield ['Keep non-HTML <tags> intact', 'Keep non-HTML intact']; + yield ["Cont\xE4o invalid UTF-8", "Cont\u{FFFD}o invalid UTF-8"]; + yield ['{{date}}', '[{]date[}]']; + } + + /** + * @dataProvider getHtmlToPlainText + */ + public function testHtmlToPlainText(string $source, string $expected, bool $removeInsertTags = false): void + { + $htmlDecoder = new HtmlDecoder(); + + $this->assertSame($expected, $htmlDecoder->htmlToPlainText($source, $removeInsertTags)); + + Input::setPost('value', str_replace(['{{', '}}'], ['[{]', '[}]'], $source)); + $inputXssStripped = str_replace(['{{', '}}'], ['{{', '}}'], Input::postHtml('value', true)); + Input::setPost('value', null); + + $this->assertSame($expected, $htmlDecoder->htmlToPlainText($inputXssStripped, $removeInsertTags)); + } + + public function getHtmlToPlainText(): \Generator + { + yield from $this->getInputEncodedToPlainText(); + + yield ['foo
bar{{br}}baz', "foo\nbar\nbaz"]; + yield [" \t\r\nfoo \t\r\n \r\n\t bar \t\r\n", 'foo bar']; + yield [" \t\r\n
foo \t
\r\n \r\n\t
bar
\t\r\n", "foo\nbar"]; + + yield [ + '

Headline

Text
  • List 1
  • List 2

Inlinetext and link

single newline', + "Headline\nText\nList 1\nList 2\nInlinetext and link\nsingle newline", + ]; + } +} diff --git a/faq-bundle/src/Resources/contao/modules/ModuleFaq.php b/faq-bundle/src/Resources/contao/modules/ModuleFaq.php index 921c15dffcc..ffd64592ce9 100644 --- a/faq-bundle/src/Resources/contao/modules/ModuleFaq.php +++ b/faq-bundle/src/Resources/contao/modules/ModuleFaq.php @@ -10,6 +10,7 @@ namespace Contao; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\Model\Collection; /** @@ -133,14 +134,16 @@ public static function getSchemaOrgData(iterable $arrFaqs): array 'mainEntity' => array(), ); + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); + foreach ($arrFaqs as $objFaq) { $jsonLd['mainEntity'][] = array( '@type' => 'Question', - 'name' => StringUtil::inputEncodedToPlainText($objFaq->question), + 'name' => $htmlDecoder->inputEncodedToPlainText($objFaq->question), 'acceptedAnswer' => array( '@type' => 'Answer', - 'text' => StringUtil::htmlToPlainText(StringUtil::encodeEmail($objFaq->answer)) + 'text' => $htmlDecoder->htmlToPlainText(StringUtil::encodeEmail($objFaq->answer)) ) ); } diff --git a/faq-bundle/src/Resources/contao/modules/ModuleFaqReader.php b/faq-bundle/src/Resources/contao/modules/ModuleFaqReader.php index 70825929196..53325157871 100644 --- a/faq-bundle/src/Resources/contao/modules/ModuleFaqReader.php +++ b/faq-bundle/src/Resources/contao/modules/ModuleFaqReader.php @@ -15,6 +15,7 @@ use Contao\CoreBundle\Image\Studio\Studio; use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; /** * Class ModuleFaqReader @@ -104,6 +105,7 @@ protected function compile() { /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); if ($objFaq->pageTitle) { @@ -111,16 +113,16 @@ protected function compile() } elseif ($objFaq->question) { - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($objFaq->question)); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($objFaq->question)); } if ($objFaq->description) { - $htmlHeadBag->setMetaDescription(StringUtil::inputEncodedToPlainText($objFaq->description)); + $htmlHeadBag->setMetaDescription($htmlDecoder->inputEncodedToPlainText($objFaq->description)); } elseif ($objFaq->question) { - $htmlHeadBag->setMetaDescription(StringUtil::inputEncodedToPlainText($objFaq->question)); + $htmlHeadBag->setMetaDescription($htmlDecoder->inputEncodedToPlainText($objFaq->question)); } if ($objFaq->robots) diff --git a/news-bundle/src/Resources/contao/classes/News.php b/news-bundle/src/Resources/contao/classes/News.php index b407359da0e..8feb932f5d4 100644 --- a/news-bundle/src/Resources/contao/classes/News.php +++ b/news-bundle/src/Resources/contao/classes/News.php @@ -10,6 +10,8 @@ namespace Contao; +use Contao\CoreBundle\String\HtmlDecoder; + /** * Provide methods regarding news archives. * @@ -460,17 +462,19 @@ public static function generateNewsUrl($objItem, $blnAddArchive=false, $blnAbsol */ public static function getSchemaOrgData(NewsModel $objArticle): array { + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); + $jsonLd = array( '@type' => 'NewsArticle', 'identifier' => '#/schema/news/' . $objArticle->id, 'url' => self::generateNewsUrl($objArticle), - 'headline' => StringUtil::inputEncodedToPlainText($objArticle->headline), + 'headline' => $htmlDecoder->inputEncodedToPlainText($objArticle->headline), 'datePublished' => date('Y-m-d\TH:i:sP', $objArticle->date), ); if ($objArticle->teaser) { - $jsonLd['description'] = StringUtil::htmlToPlainText($objArticle->teaser); + $jsonLd['description'] = $htmlDecoder->htmlToPlainText($objArticle->teaser); } /** @var UserModel $objAuthor */ diff --git a/news-bundle/src/Resources/contao/modules/ModuleNewsReader.php b/news-bundle/src/Resources/contao/modules/ModuleNewsReader.php index e64b6f02e91..c252c395b8c 100644 --- a/news-bundle/src/Resources/contao/modules/ModuleNewsReader.php +++ b/news-bundle/src/Resources/contao/modules/ModuleNewsReader.php @@ -15,6 +15,7 @@ use Contao\CoreBundle\Exception\RedirectResponseException; use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; /** * Front end module "news reader". @@ -139,6 +140,7 @@ protected function compile() { /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); if ($objArticle->pageTitle) { @@ -146,16 +148,16 @@ protected function compile() } elseif ($objArticle->headline) { - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($objArticle->headline)); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($objArticle->headline)); } if ($objArticle->description) { - $htmlHeadBag->setMetaDescription(StringUtil::inputEncodedToPlainText($objArticle->description)); + $htmlHeadBag->setMetaDescription($htmlDecoder->inputEncodedToPlainText($objArticle->description)); } elseif ($objArticle->teaser) { - $htmlHeadBag->setMetaDescription(StringUtil::htmlToPlainText($objArticle->teaser)); + $htmlHeadBag->setMetaDescription($htmlDecoder->htmlToPlainText($objArticle->teaser)); } if ($objArticle->robots) diff --git a/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php b/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php index 9163202b6df..f0f6231eac4 100644 --- a/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php +++ b/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php @@ -14,6 +14,7 @@ use Contao\CoreBundle\Exception\PageNotFoundException; use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use Contao\CoreBundle\String\HtmlDecoder; use Contao\CoreBundle\Util\SimpleTokenParser; /** @@ -97,9 +98,11 @@ protected function compile() if ($responseContext && $responseContext->has(HtmlHeadBag::class)) { + $htmlDecoder = System::getContainer()->get(HtmlDecoder::class); + /** @var HtmlHeadBag $htmlHeadBag */ $htmlHeadBag = $responseContext->get(HtmlHeadBag::class); - $htmlHeadBag->setTitle(StringUtil::inputEncodedToPlainText($objNewsletter->subject)); + $htmlHeadBag->setTitle($htmlDecoder->inputEncodedToPlainText($objNewsletter->subject)); } } From 9a51349b7f3105b54a838d2c1e51fe21f59b9fec Mon Sep 17 00:00:00 2001 From: Leo Feyer Date: Wed, 3 Nov 2021 17:33:51 +0100 Subject: [PATCH 10/12] Move the simple token parser into the String namespace (see #3609) Description ----------- The String namespace is added in #3602. Commits ------- f1dc8fa3 Move the simple token parser into the String namespace 0a3196ad Merge branch '4.x' into feature/simple-tokens --- core-bundle/src/Resources/config/services.yml | 18 ++ .../contao/library/Contao/Controller.php | 2 +- .../contao/library/Contao/StringUtil.php | 2 +- .../contao/modules/ModulePassword.php | 2 +- .../contao/modules/ModuleRegistration.php | 2 +- .../String/SimpleTokenExpressionLanguage.php | 35 ++ core-bundle/src/String/SimpleTokenParser.php | 304 ++++++++++++++++++ .../Util/SimpleTokenExpressionLanguage.php | 20 +- core-bundle/src/Util/SimpleTokenParser.php | 289 +---------------- core-bundle/tests/Contao/InputTest.php | 2 +- .../SimpleTokenParserTest.php | 6 +- .../Resources/contao/classes/Newsletter.php | 2 +- .../contao/modules/ModuleNewsletterReader.php | 2 +- .../contao/modules/ModuleSubscribe.php | 2 +- .../contao/modules/ModuleUnsubscribe.php | 2 +- 15 files changed, 383 insertions(+), 307 deletions(-) create mode 100644 core-bundle/src/String/SimpleTokenExpressionLanguage.php create mode 100644 core-bundle/src/String/SimpleTokenParser.php rename core-bundle/tests/{Util => String}/SimpleTokenParserTest.php (99%) diff --git a/core-bundle/src/Resources/config/services.yml b/core-bundle/src/Resources/config/services.yml index f8351e6f572..925d8dad5e6 100644 --- a/core-bundle/src/Resources/config/services.yml +++ b/core-bundle/src/Resources/config/services.yml @@ -904,6 +904,16 @@ services: Contao\CoreBundle\String\HtmlDecoder: public: true + Contao\CoreBundle\String\SimpleTokenExpressionLanguage: + arguments: + - ~ + - !tagged_iterator contao.simple_token_extension + + Contao\CoreBundle\String\SimpleTokenParser: + arguments: + - '@Contao\CoreBundle\String\SimpleTokenExpressionLanguage' + public: true + contao.token_generator: class: Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator arguments: @@ -990,8 +1000,16 @@ services: arguments: - ~ - !tagged_iterator contao.simple_token_extension + deprecated: + package: contao/core-bundle + version: 4.13 + message: Using the "%service_id%" service ID has been deprecated and will no longer work in Contao 5.0. Please use "Contao\CoreBundle\String\SimpleTokenExpressionLanguage" instead. Contao\CoreBundle\Util\SimpleTokenParser: arguments: - '@Contao\CoreBundle\Util\SimpleTokenExpressionLanguage' + deprecated: + package: contao/core-bundle + version: 4.13 + message: Using the "%service_id%" service ID has been deprecated and will no longer work in Contao 5.0. Please use "Contao\CoreBundle\String\SimpleTokenParser" instead. public: true diff --git a/core-bundle/src/Resources/contao/library/Contao/Controller.php b/core-bundle/src/Resources/contao/library/Contao/Controller.php index ea8c80fa61b..e3d793127f8 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Controller.php +++ b/core-bundle/src/Resources/contao/library/Contao/Controller.php @@ -20,8 +20,8 @@ use Contao\CoreBundle\Image\Studio\Studio; use Contao\CoreBundle\Monolog\ContaoContext as ContaoMonologContext; use Contao\CoreBundle\Security\ContaoCorePermissions; +use Contao\CoreBundle\String\SimpleTokenParser; use Contao\CoreBundle\Util\LocaleUtil; -use Contao\CoreBundle\Util\SimpleTokenParser; use Contao\Database\Result; use Contao\Image\PictureConfiguration; use Contao\Model\Collection; diff --git a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php index 9ed2fd77645..6742796ec1d 100644 --- a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php +++ b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php @@ -11,7 +11,7 @@ namespace Contao; use Contao\CoreBundle\String\HtmlDecoder; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; use Webmozart\PathUtil\Path; /** diff --git a/core-bundle/src/Resources/contao/modules/ModulePassword.php b/core-bundle/src/Resources/contao/modules/ModulePassword.php index 30977eb7877..b419723921e 100644 --- a/core-bundle/src/Resources/contao/modules/ModulePassword.php +++ b/core-bundle/src/Resources/contao/modules/ModulePassword.php @@ -11,7 +11,7 @@ namespace Contao; use Contao\CoreBundle\OptIn\OptIn; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; use Symfony\Component\HttpFoundation\Session\Session; /** diff --git a/core-bundle/src/Resources/contao/modules/ModuleRegistration.php b/core-bundle/src/Resources/contao/modules/ModuleRegistration.php index ddd14caab96..e81e6cc6c58 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleRegistration.php +++ b/core-bundle/src/Resources/contao/modules/ModuleRegistration.php @@ -12,7 +12,7 @@ use Contao\CoreBundle\Exception\ResponseException; use Contao\CoreBundle\OptIn\OptIn; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; /** * Front end module "registration". diff --git a/core-bundle/src/String/SimpleTokenExpressionLanguage.php b/core-bundle/src/String/SimpleTokenExpressionLanguage.php new file mode 100644 index 00000000000..b56cf95fa62 --- /dev/null +++ b/core-bundle/src/String/SimpleTokenExpressionLanguage.php @@ -0,0 +1,35 @@ +getIterator()) : []; + + parent::__construct($cache, $providers); + + // Disable the constant() function for security reasons + $this->register( + 'constant', + static fn () => "throw new \\InvalidArgumentException('Cannot use the constant() function in the expression for security reasons.');", + static function (): void { + throw new \InvalidArgumentException('Cannot use the constant() function in the expression for security reasons.'); + } + ); + } +} diff --git a/core-bundle/src/String/SimpleTokenParser.php b/core-bundle/src/String/SimpleTokenParser.php new file mode 100644 index 00000000000..60b9af81b9c --- /dev/null +++ b/core-bundle/src/String/SimpleTokenParser.php @@ -0,0 +1,304 @@ +expressionLanguage = $expressionLanguage; + } + + /** + * Parse simple tokens. + * + * @param array $tokens Key value pairs ([token => value, ...]) + * + * @throws \RuntimeException If $subject cannot be parsed + * @throws \InvalidArgumentException If there are incorrectly formatted if-tags + */ + public function parse(string $subject, array $tokens, bool $allowHtml = true): string + { + // Check if we can use the expression language or if legacy tokens have been used + $canUseExpressionLanguage = $this->canUseExpressionLanguage($tokens); + + // The last item is true if it is inside a matching if-tag + $stack = [true]; + + // The last item is true if any if/elseif at that level was true + $ifStack = [true]; + + // Tokenize the string into tag and text blocks + $tags = preg_split( + $allowHtml + ? '/((?:{|{)(?:(?! [35];)[^{}])+(?:}|}))\n?/' + : '/({[^{}]+})\n?/', + $subject, + -1, + PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + ); + + // Parse the tokens + $return = ''; + + foreach ($tags as $tag) { + $decodedTag = $allowHtml + ? html_entity_decode(StringUtil::restoreBasicEntities($tag), ENT_QUOTES, 'UTF-8') + : $tag; + + // True if it is inside a matching if-tag + $current = $stack[\count($stack) - 1]; + $currentIf = $ifStack[\count($ifStack) - 1]; + + if (0 === strncmp($decodedTag, '{if ', 4)) { + $expression = $this->evaluateExpression(substr($decodedTag, 4, -1), $tokens, $canUseExpressionLanguage); + $stack[] = $current && $expression; + $ifStack[] = $expression; + } elseif (0 === strncmp($decodedTag, '{elseif ', 8)) { + $expression = $this->evaluateExpression(substr($decodedTag, 8, -1), $tokens, $canUseExpressionLanguage); + array_pop($stack); + array_pop($ifStack); + $stack[] = !$currentIf && $stack[\count($stack) - 1] && $expression; + $ifStack[] = $currentIf || $expression; + } elseif (0 === strncmp($decodedTag, '{else}', 6)) { + array_pop($stack); + array_pop($ifStack); + $stack[] = !$currentIf && $stack[\count($stack) - 1]; + $ifStack[] = true; + } elseif (0 === strncmp($decodedTag, '{endif}', 7)) { + array_pop($stack); + array_pop($ifStack); + } elseif ($current) { + $return .= $this->replaceTokens($tag, $tokens); + } + } + + if (1 !== \count($stack)) { + throw new \RuntimeException('Error parsing simple tokens'); + } + + return $return; + } + + /** + * @deprecated Deprecated since Contao 4.10, to be removed in Contao 5.0; + * use the parse() method instead + */ + public function parseTokens(string $subject, array $tokens): string + { + trigger_deprecation('contao/core-bundle', '4.10', 'Using the parseTokens() method has been deprecated and will no longer work in Contao 5.0. Use the parse() method instead.'); + + return $this->parse($subject, $tokens); + } + + private function replaceTokens(string $subject, array $data): string + { + // Replace tokens + return preg_replace_callback( + '/##([^=!<>\s]+?)##/', + function (array $matches) use ($data) { + if (!\array_key_exists($matches[1], $data)) { + if (null !== $this->logger) { + $this->logger->log(LogLevel::INFO, sprintf('Tried to parse unknown simple token "%s".', $matches[1])); + } + + return '##'.$matches[1].'##'; + } + + return $data[$matches[1]]; + }, + $subject + ); + } + + private function canUseExpressionLanguage(array $data): bool + { + foreach (array_keys($data) as $token) { + if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', (string) $token)) { + trigger_deprecation('contao/core-bundle', '4.10', 'Using tokens that are not valid PHP variables has been deprecated and will no longer work in Contao 5.0. Falling back to legacy token parsing.'); + + return false; + } + } + + return true; + } + + private function evaluateExpression(string $expression, array $data, bool $canUseExpressionLanguage): bool + { + if (!$canUseExpressionLanguage) { + return $this->evaluateExpressionLegacy($expression, $data); + } + + $unmatchedVariables = array_diff($this->getVariables($expression), array_keys($data)); + + if (!empty($unmatchedVariables)) { + $this->logUnmatchedVariables(...$unmatchedVariables); + + // Define variables that weren't provided with the value 'null' + $data = array_merge( + array_combine($unmatchedVariables, array_fill(0, \count($unmatchedVariables), null)), + $data + ); + } + + try { + return (bool) $this->expressionLanguage->evaluate($expression, $data); + } catch (SyntaxError $e) { + throw new \InvalidArgumentException($e->getMessage(), 0, $e); + } + } + + private function evaluateExpressionLegacy(string $expression, array $data): bool + { + if (!preg_match('/^([^=!<>\s]+) *([=!<>]+)(.+)$/s', $expression, $matches)) { + return false; + } + + [, $token, $operator, $value] = $matches; + + if (!\array_key_exists($token, $data)) { + $this->logUnmatchedVariables($token); + + $tokenValue = null; + } else { + $tokenValue = $data[$token]; + } + + // Normalize types + $value = trim($value, ' '); + + if (is_numeric($value)) { + if (false === strpos($value, '.')) { + $value = (int) $value; + } else { + $value = (float) $value; + } + } elseif ('true' === strtolower($value)) { + $value = true; + } elseif ('false' === strtolower($value)) { + $value = false; + } elseif ('null' === strtolower($value)) { + $value = null; + } elseif (0 === strncmp($value, '"', 1) && '"' === substr($value, -1)) { + $value = str_replace('\"', '"', substr($value, 1, -1)); + } elseif (0 === strncmp($value, "'", 1) && "'" === substr($value, -1)) { + $value = str_replace("\\'", "'", substr($value, 1, -1)); + } else { + throw new \InvalidArgumentException(sprintf('Unknown data type of comparison value "%s".', $value)); + } + + // Evaluate + switch ($operator) { + case '==': + // We explicitly want to compare with type juggling here + return \in_array($tokenValue, [$value], false); + + case '!=': + // We explicitly want to compare with type juggling here + return !\in_array($tokenValue, [$value], false); + + case '===': + return $tokenValue === $value; + + case '!==': + return $tokenValue !== $value; + + case '<': + return $tokenValue < $value; + + case '>': + return $tokenValue > $value; + + case '<=': + return $tokenValue <= $value; + + case '>=': + return $tokenValue >= $value; + + default: + throw new \InvalidArgumentException(sprintf('Unknown simple token comparison operator "%s".', $operator)); + } + } + + private function getVariables(string $expression): array + { + /** @var array $tokens */ + $tokens = []; + + try { + $tokenStream = (new Lexer())->tokenize($expression); + + while (!$tokenStream->isEOF()) { + $tokens[] = $tokenStream->current; + $tokenStream->next(); + } + } catch (SyntaxError $e) { + // We cannot identify the variables if tokenizing fails + return []; + } + + $variables = []; + + for ($i = 0, $c = \count($tokens); $i < $c; ++$i) { + if (!$tokens[$i]->test(Token::NAME_TYPE)) { + continue; + } + + $value = $tokens[$i]->value; + + // Skip constant nodes (see Symfony/Component/ExpressionLanguage/Parser#parsePrimaryExpression() + if (\in_array($value, ['true', 'TRUE', 'false', 'FALSE', 'null'], true)) { + continue; + } + + // Skip functions + if (isset($tokens[$i + 1]) && '(' === $tokens[$i + 1]->value) { + ++$i; + + continue; + } + + if (!\in_array($value, $variables, true)) { + $variables[] = $value; + } + } + + return $variables; + } + + private function logUnmatchedVariables(string ...$tokenNames): void + { + if (null === $this->logger) { + return; + } + + $this->logger->log( + LogLevel::INFO, + sprintf('Tried to evaluate unknown simple token(s): "%s".', implode('", "', $tokenNames)) + ); + } +} diff --git a/core-bundle/src/Util/SimpleTokenExpressionLanguage.php b/core-bundle/src/Util/SimpleTokenExpressionLanguage.php index f90ebb8ede0..f731079fbd9 100644 --- a/core-bundle/src/Util/SimpleTokenExpressionLanguage.php +++ b/core-bundle/src/Util/SimpleTokenExpressionLanguage.php @@ -13,23 +13,17 @@ namespace Contao\CoreBundle\Util; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -class SimpleTokenExpressionLanguage extends ExpressionLanguage +/** + * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0; use + * the Contao\CoreBundle\String\SimpleTokenExpressionLanguage class instead + */ +class SimpleTokenExpressionLanguage extends \Contao\CoreBundle\String\SimpleTokenExpressionLanguage { public function __construct(CacheItemPoolInterface $cache = null, \IteratorAggregate $taggedProviders = null) { - $providers = null !== $taggedProviders ? iterator_to_array($taggedProviders->getIterator()) : []; - - parent::__construct($cache, $providers); + trigger_deprecation('contao/core-bundle', '4.13', 'Using the "Contao\CoreBundle\Util\SimpleTokenExpressionLanguage" class has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\SimpleTokenExpressionLanguage" class instead.'); - // Disable `constant()` function for security reasons. - $this->register( - 'constant', - static fn () => "throw new \\InvalidArgumentException('Cannot use the constant() function in the expression for security reasons.');", - static function (): void { - throw new \InvalidArgumentException('Cannot use the constant() function in the expression for security reasons.'); - } - ); + parent::__construct($cache, $taggedProviders); } } diff --git a/core-bundle/src/Util/SimpleTokenParser.php b/core-bundle/src/Util/SimpleTokenParser.php index 6c47a6e4074..6954644daf7 100644 --- a/core-bundle/src/Util/SimpleTokenParser.php +++ b/core-bundle/src/Util/SimpleTokenParser.php @@ -12,293 +12,18 @@ namespace Contao\CoreBundle\Util; -use Contao\StringUtil; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; -use Psr\Log\LogLevel; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\Lexer; -use Symfony\Component\ExpressionLanguage\SyntaxError; -use Symfony\Component\ExpressionLanguage\Token; -class SimpleTokenParser implements LoggerAwareInterface +/** + * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0; use + * the Contao\CoreBundle\String\SimpleTokenParser class instead + */ +class SimpleTokenParser extends \Contao\CoreBundle\String\SimpleTokenParser { - use LoggerAwareTrait; - - private ExpressionLanguage $expressionLanguage; - public function __construct(ExpressionLanguage $expressionLanguage) { - $this->expressionLanguage = $expressionLanguage; - } - - /** - * Parse simple tokens. - * - * @param array $tokens Key value pairs ([token => value, ...]) - * - * @throws \RuntimeException If $subject cannot be parsed - * @throws \InvalidArgumentException If there are incorrectly formatted if-tags - */ - public function parse(string $subject, array $tokens, bool $allowHtml = true): string - { - // Check if we can use the expression language or if legacy tokens have been used - $canUseExpressionLanguage = $this->canUseExpressionLanguage($tokens); - - // The last item is true if it is inside a matching if-tag - $stack = [true]; - - // The last item is true if any if/elseif at that level was true - $ifStack = [true]; - - // Tokenize the string into tag and text blocks - $tags = preg_split( - $allowHtml - ? '/((?:{|{)(?:(?! [35];)[^{}])+(?:}|}))\n?/' - : '/({[^{}]+})\n?/', - $subject, - -1, - PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY - ); - - // Parse the tokens - $return = ''; - - foreach ($tags as $tag) { - $decodedTag = $allowHtml - ? html_entity_decode(StringUtil::restoreBasicEntities($tag), ENT_QUOTES, 'UTF-8') - : $tag; - - // True if it is inside a matching if-tag - $current = $stack[\count($stack) - 1]; - $currentIf = $ifStack[\count($ifStack) - 1]; - - if (0 === strncmp($decodedTag, '{if ', 4)) { - $expression = $this->evaluateExpression(substr($decodedTag, 4, -1), $tokens, $canUseExpressionLanguage); - $stack[] = $current && $expression; - $ifStack[] = $expression; - } elseif (0 === strncmp($decodedTag, '{elseif ', 8)) { - $expression = $this->evaluateExpression(substr($decodedTag, 8, -1), $tokens, $canUseExpressionLanguage); - array_pop($stack); - array_pop($ifStack); - $stack[] = !$currentIf && $stack[\count($stack) - 1] && $expression; - $ifStack[] = $currentIf || $expression; - } elseif (0 === strncmp($decodedTag, '{else}', 6)) { - array_pop($stack); - array_pop($ifStack); - $stack[] = !$currentIf && $stack[\count($stack) - 1]; - $ifStack[] = true; - } elseif (0 === strncmp($decodedTag, '{endif}', 7)) { - array_pop($stack); - array_pop($ifStack); - } elseif ($current) { - $return .= $this->replaceTokens($tag, $tokens); - } - } - - if (1 !== \count($stack)) { - throw new \RuntimeException('Error parsing simple tokens'); - } - - return $return; - } - - /** - * @deprecated Deprecated since Contao 4.10, to be removed in Contao 5.0; - * use the parse() method instead - */ - public function parseTokens(string $subject, array $tokens): string - { - trigger_deprecation('contao/core-bundle', '4.10', 'Using the parseTokens() method has been deprecated and will no longer work in Contao 5.0. Use the parse() method instead.'); - - return $this->parse($subject, $tokens); - } - - private function replaceTokens(string $subject, array $data): string - { - // Replace tokens - return preg_replace_callback( - '/##([^=!<>\s]+?)##/', - function (array $matches) use ($data) { - if (!\array_key_exists($matches[1], $data)) { - if (null !== $this->logger) { - $this->logger->log(LogLevel::INFO, sprintf('Tried to parse unknown simple token "%s".', $matches[1])); - } - - return '##'.$matches[1].'##'; - } - - return $data[$matches[1]]; - }, - $subject - ); - } - - private function canUseExpressionLanguage(array $data): bool - { - foreach (array_keys($data) as $token) { - if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', (string) $token)) { - trigger_deprecation('contao/core-bundle', '4.10', 'Using tokens that are not valid PHP variables has been deprecated and will no longer work in Contao 5.0. Falling back to legacy token parsing.'); - - return false; - } - } - - return true; - } - - private function evaluateExpression(string $expression, array $data, bool $canUseExpressionLanguage): bool - { - if (!$canUseExpressionLanguage) { - return $this->evaluateExpressionLegacy($expression, $data); - } - - $unmatchedVariables = array_diff($this->getVariables($expression), array_keys($data)); - - if (!empty($unmatchedVariables)) { - $this->logUnmatchedVariables(...$unmatchedVariables); - - // Define variables that weren't provided with the value 'null' - $data = array_merge( - array_combine($unmatchedVariables, array_fill(0, \count($unmatchedVariables), null)), - $data - ); - } - - try { - return (bool) $this->expressionLanguage->evaluate($expression, $data); - } catch (SyntaxError $e) { - throw new \InvalidArgumentException($e->getMessage(), 0, $e); - } - } - - private function evaluateExpressionLegacy(string $expression, array $data): bool - { - if (!preg_match('/^([^=!<>\s]+) *([=!<>]+)(.+)$/s', $expression, $matches)) { - return false; - } - - [, $token, $operator, $value] = $matches; - - if (!\array_key_exists($token, $data)) { - $this->logUnmatchedVariables($token); - - $tokenValue = null; - } else { - $tokenValue = $data[$token]; - } - - // Normalize types - $value = trim($value, ' '); - - if (is_numeric($value)) { - if (false === strpos($value, '.')) { - $value = (int) $value; - } else { - $value = (float) $value; - } - } elseif ('true' === strtolower($value)) { - $value = true; - } elseif ('false' === strtolower($value)) { - $value = false; - } elseif ('null' === strtolower($value)) { - $value = null; - } elseif (0 === strncmp($value, '"', 1) && '"' === substr($value, -1)) { - $value = str_replace('\"', '"', substr($value, 1, -1)); - } elseif (0 === strncmp($value, "'", 1) && "'" === substr($value, -1)) { - $value = str_replace("\\'", "'", substr($value, 1, -1)); - } else { - throw new \InvalidArgumentException(sprintf('Unknown data type of comparison value "%s".', $value)); - } - - // Evaluate - switch ($operator) { - case '==': - // We explicitly want to compare with type juggling here - return \in_array($tokenValue, [$value], false); - - case '!=': - // We explicitly want to compare with type juggling here - return !\in_array($tokenValue, [$value], false); - - case '===': - return $tokenValue === $value; - - case '!==': - return $tokenValue !== $value; - - case '<': - return $tokenValue < $value; - - case '>': - return $tokenValue > $value; - - case '<=': - return $tokenValue <= $value; - - case '>=': - return $tokenValue >= $value; - - default: - throw new \InvalidArgumentException(sprintf('Unknown simple token comparison operator "%s".', $operator)); - } - } - - private function getVariables(string $expression): array - { - /** @var array $tokens */ - $tokens = []; - - try { - $tokenStream = (new Lexer())->tokenize($expression); - - while (!$tokenStream->isEOF()) { - $tokens[] = $tokenStream->current; - $tokenStream->next(); - } - } catch (SyntaxError $e) { - // We cannot identify the variables if tokenizing fails - return []; - } - - $variables = []; - - for ($i = 0, $c = \count($tokens); $i < $c; ++$i) { - if (!$tokens[$i]->test(Token::NAME_TYPE)) { - continue; - } - - $value = $tokens[$i]->value; - - // Skip constant nodes (see Symfony/Component/ExpressionLanguage/Parser#parsePrimaryExpression() - if (\in_array($value, ['true', 'TRUE', 'false', 'FALSE', 'null'], true)) { - continue; - } - - // Skip functions - if (isset($tokens[$i + 1]) && '(' === $tokens[$i + 1]->value) { - ++$i; - - continue; - } - - if (!\in_array($value, $variables, true)) { - $variables[] = $value; - } - } - - return $variables; - } - - private function logUnmatchedVariables(string ...$tokenNames): void - { - if (null === $this->logger) { - return; - } + trigger_deprecation('contao/core-bundle', '4.13', 'Using the "Contao\CoreBundle\Util\SimpleTokenParser" class has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\SimpleTokenParser" class instead.'); - $this->logger->log( - LogLevel::INFO, - sprintf('Tried to evaluate unknown simple token(s): "%s".', implode('", "', $tokenNames)) - ); + parent::__construct($expressionLanguage); } } diff --git a/core-bundle/tests/Contao/InputTest.php b/core-bundle/tests/Contao/InputTest.php index 09bf5415a13..ad2849914c0 100644 --- a/core-bundle/tests/Contao/InputTest.php +++ b/core-bundle/tests/Contao/InputTest.php @@ -13,8 +13,8 @@ namespace Contao\CoreBundle\Tests\Contao; use Contao\Config; +use Contao\CoreBundle\String\SimpleTokenParser; use Contao\CoreBundle\Tests\TestCase; -use Contao\CoreBundle\Util\SimpleTokenParser; use Contao\Input; use Contao\StringUtil; use Contao\System; diff --git a/core-bundle/tests/Util/SimpleTokenParserTest.php b/core-bundle/tests/String/SimpleTokenParserTest.php similarity index 99% rename from core-bundle/tests/Util/SimpleTokenParserTest.php rename to core-bundle/tests/String/SimpleTokenParserTest.php index dfe85c2ef4a..6834ff106fe 100644 --- a/core-bundle/tests/Util/SimpleTokenParserTest.php +++ b/core-bundle/tests/String/SimpleTokenParserTest.php @@ -10,12 +10,12 @@ * @license LGPL-3.0-or-later */ -namespace Contao\CoreBundle\Tests\Util; +namespace Contao\CoreBundle\Tests\String; +use Contao\CoreBundle\String\SimpleTokenExpressionLanguage; +use Contao\CoreBundle\String\SimpleTokenParser; use Contao\CoreBundle\Tests\Fixtures\IteratorAggregateStub; use Contao\CoreBundle\Tests\TestCase; -use Contao\CoreBundle\Util\SimpleTokenExpressionLanguage; -use Contao\CoreBundle\Util\SimpleTokenParser; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; diff --git a/newsletter-bundle/src/Resources/contao/classes/Newsletter.php b/newsletter-bundle/src/Resources/contao/classes/Newsletter.php index 522bd6ef6ce..49f4ccf5f35 100644 --- a/newsletter-bundle/src/Resources/contao/classes/Newsletter.php +++ b/newsletter-bundle/src/Resources/contao/classes/Newsletter.php @@ -11,7 +11,7 @@ namespace Contao; use Contao\CoreBundle\Exception\InternalServerErrorException; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; use Contao\Database\Result; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Mime\Exception\RfcComplianceException; diff --git a/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php b/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php index f0f6231eac4..88a00333fff 100644 --- a/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php +++ b/newsletter-bundle/src/Resources/contao/modules/ModuleNewsletterReader.php @@ -15,7 +15,7 @@ use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag; use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\CoreBundle\String\HtmlDecoder; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; /** * Front end module "newsletter reader". diff --git a/newsletter-bundle/src/Resources/contao/modules/ModuleSubscribe.php b/newsletter-bundle/src/Resources/contao/modules/ModuleSubscribe.php index 078b4345f44..8bb1e2d6463 100644 --- a/newsletter-bundle/src/Resources/contao/modules/ModuleSubscribe.php +++ b/newsletter-bundle/src/Resources/contao/modules/ModuleSubscribe.php @@ -11,7 +11,7 @@ namespace Contao; use Contao\CoreBundle\OptIn\OptIn; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; /** * Front end module "newsletter subscribe". diff --git a/newsletter-bundle/src/Resources/contao/modules/ModuleUnsubscribe.php b/newsletter-bundle/src/Resources/contao/modules/ModuleUnsubscribe.php index 1f7417b80c2..d6d7011e93b 100644 --- a/newsletter-bundle/src/Resources/contao/modules/ModuleUnsubscribe.php +++ b/newsletter-bundle/src/Resources/contao/modules/ModuleUnsubscribe.php @@ -10,7 +10,7 @@ namespace Contao; -use Contao\CoreBundle\Util\SimpleTokenParser; +use Contao\CoreBundle\String\SimpleTokenParser; /** * Front end module "newsletter unsubscribe". From 2cabec850046d20fd005e11501f581d980121323 Mon Sep 17 00:00:00 2001 From: Leo Feyer Date: Thu, 4 Nov 2021 07:51:31 +0100 Subject: [PATCH 11/12] Run the CS tools (see #3639) Description ----------- - Commits ------- 00de45fa Run the CS fixer 7d32b3ee Run the code inspector --- .../src/Resources/contao/classes/Events.php | 2 +- .../tests/Picker/EventPickerProviderTest.php | 4 +- .../src/Resources/contao/classes/Comments.php | 2 +- core-bundle/src/Command/MigrateCommand.php | 2 +- core-bundle/src/Crawl/Escargot/Factory.php | 6 +- .../contao/classes/BackendTemplate.php | 9 +- .../Resources/contao/classes/FileUpload.php | 4 +- .../contao/classes/FrontendTemplate.php | 2 +- .../contao/controllers/BackendIndex.php | 4 +- .../src/Resources/contao/dca/tl_settings.php | 2 +- .../src/Resources/contao/dca/tl_templates.php | 2 +- .../src/Resources/contao/helper/functions.php | 2 +- .../contao/library/Contao/Automator.php | 5 +- .../contao/library/Contao/Config.php | 2 +- .../contao/library/Contao/Controller.php | 17 +- .../Resources/contao/library/Contao/Dbafs.php | 2 +- .../Resources/contao/library/Contao/File.php | 4 +- .../contao/library/Contao/Folder.php | 8 +- .../Resources/contao/library/Contao/Image.php | 2 +- .../Resources/contao/library/Contao/Input.php | 2 +- .../Resources/contao/library/Contao/Model.php | 4 +- .../contao/library/Contao/Pagination.php | 4 +- .../contao/library/Contao/Picture.php | 4 +- .../contao/library/Contao/StringUtil.php | 2 +- .../contao/library/Contao/ZipReader.php | 2 +- .../contao/library/Contao/ZipWriter.php | 2 +- .../contao/modules/ModuleArticle.php | 2 +- .../contao/modules/ModuleNavigation.php | 2 +- .../contao/modules/ModuleQuicknav.php | 2 +- .../contao/modules/ModuleSitemap.php | 2 +- .../Resources/contao/themes/flexible/main.css | 2 +- .../Resources/contao/widgets/FileSelector.php | 2 +- core-bundle/src/String/HtmlDecoder.php | 4 +- .../src/Twig/Extension/ContaoExtension.php | 5 +- .../Runtime/PictureConfigurationRuntime.php | 5 +- .../tests/Command/DebugPagesCommandTest.php | 4 +- .../tests/Command/MigrateCommandTest.php | 5 +- core-bundle/tests/Contao/ArrayUtilTest.php | 40 +---- core-bundle/tests/Contao/WidgetTest.php | 8 +- core-bundle/tests/ContaoCoreBundleTest.php | 4 +- .../Compiler/RegisterPagesPassTest.php | 20 +-- .../DataContainer/PageUrlListenerTest.php | 16 +- .../FilterPageTypeListenerTest.php | 5 +- .../Menu/BackendLogoutListenerTest.php | 4 +- .../Menu/BackendMenuListenerTest.php | 4 +- .../Menu/BackendPreviewListenerTest.php | 4 +- .../tests/Image/PictureFactoryTest.php | 12 +- .../tests/Image/Studio/FigureBuilderTest.php | 4 +- .../Picker/ArticlePickerProviderTest.php | 4 +- .../tests/Picker/FilePickerProviderTest.php | 4 +- .../tests/Picker/PagePickerProviderTest.php | 4 +- .../Routing/AbstractPageRouteProviderTest.php | 10 +- .../Routing/Matcher/LegacyMatcherTest.php | 4 +- .../tests/Picker/FaqPickerProviderTest.php | 4 +- .../tests/Database/InstallerTest.php | 4 +- .../tests/ContaoManager/PluginTest.php | 13 +- .../EventListener/BackendMenuListenerTest.php | 4 +- .../contao/modules/ModuleNewsMenu.php | 2 +- .../tests/Picker/NewsPickerProviderTest.php | 4 +- tools/ecs/composer.lock | 48 +++--- tools/psalm/composer.lock | 161 +++++++++--------- 61 files changed, 199 insertions(+), 324 deletions(-) diff --git a/calendar-bundle/src/Resources/contao/classes/Events.php b/calendar-bundle/src/Resources/contao/classes/Events.php index f4db3ceed76..519ab914dc8 100644 --- a/calendar-bundle/src/Resources/contao/classes/Events.php +++ b/calendar-bundle/src/Resources/contao/classes/Events.php @@ -354,7 +354,7 @@ protected function addEvent($objEvents, $intStart, $intEnd, $intBegin, $intLimit }; } - // Get todays start and end timestamp + // Get today's start and end timestamp if ($this->intTodayBegin === null) { $this->intTodayBegin = strtotime('00:00:00'); diff --git a/calendar-bundle/tests/Picker/EventPickerProviderTest.php b/calendar-bundle/tests/Picker/EventPickerProviderTest.php index aab777c9a93..c3d7b432b85 100644 --- a/calendar-bundle/tests/Picker/EventPickerProviderTest.php +++ b/calendar-bundle/tests/Picker/EventPickerProviderTest.php @@ -244,9 +244,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; $translator = $this->createMock(TranslatorInterface::class); diff --git a/comments-bundle/src/Resources/contao/classes/Comments.php b/comments-bundle/src/Resources/contao/classes/Comments.php index 255518a434b..0a0bce46074 100644 --- a/comments-bundle/src/Resources/contao/classes/Comments.php +++ b/comments-bundle/src/Resources/contao/classes/Comments.php @@ -661,7 +661,7 @@ public static function notifyCommentsSubscribers(CommentsModel $objComment) while ($objNotify->next()) { - // Don't notify the commentor about his own comment + // Don't notify the commenter about his own comment if ($objNotify->email == $objComment->email) { continue; diff --git a/core-bundle/src/Command/MigrateCommand.php b/core-bundle/src/Command/MigrateCommand.php index ae6df0372c3..8e61100de93 100644 --- a/core-bundle/src/Command/MigrateCommand.php +++ b/core-bundle/src/Command/MigrateCommand.php @@ -121,7 +121,7 @@ private function executeCommand(InputInterface $input): int return 1; } - if (!$dryRun && !$this->executeMigrations($dryRun, $asJson)) { + if (!$dryRun && !$this->executeMigrations(false, $asJson)) { return 1; } diff --git a/core-bundle/src/Crawl/Escargot/Factory.php b/core-bundle/src/Crawl/Escargot/Factory.php index 1d91c0c5c4c..cb9879ec789 100644 --- a/core-bundle/src/Crawl/Escargot/Factory.php +++ b/core-bundle/src/Crawl/Escargot/Factory.php @@ -93,11 +93,7 @@ public function createLazyQueue(): LazyQueue { return new LazyQueue( new InMemoryQueue(), - new DoctrineQueue( - $this->connection, - static fn (): string => Uuid::uuid4()->toString(), - 'tl_crawl_queue' - ) + new DoctrineQueue($this->connection, static fn (): string => Uuid::uuid4()->toString(), 'tl_crawl_queue') ); } diff --git a/core-bundle/src/Resources/contao/classes/BackendTemplate.php b/core-bundle/src/Resources/contao/classes/BackendTemplate.php index c5d21af1f51..428222a05c4 100644 --- a/core-bundle/src/Resources/contao/classes/BackendTemplate.php +++ b/core-bundle/src/Resources/contao/classes/BackendTemplate.php @@ -145,14 +145,7 @@ protected function compile() // MooTools scripts (added at the page bottom) if (!empty($GLOBALS['TL_MOOTOOLS']) && \is_array($GLOBALS['TL_MOOTOOLS'])) { - $strMootools = ''; - - foreach (array_unique($GLOBALS['TL_MOOTOOLS']) as $script) - { - $strMootools .= $script; - } - - $this->mootools .= $strMootools; + $this->mootools .= implode('', array_unique($GLOBALS['TL_MOOTOOLS'])); } $strBuffer = $this->parse(); diff --git a/core-bundle/src/Resources/contao/classes/FileUpload.php b/core-bundle/src/Resources/contao/classes/FileUpload.php index 8686157e21f..c69cec48756 100644 --- a/core-bundle/src/Resources/contao/classes/FileUpload.php +++ b/core-bundle/src/Resources/contao/classes/FileUpload.php @@ -175,7 +175,7 @@ public function uploadTo($strTarget) $this->import(Files::class, 'Files'); $strNewFile = $strTarget . '/' . $file['name']; - // Set CHMOD and resize if neccessary + // Set CHMOD and resize if necessary if ($this->Files->move_uploaded_file($file['tmp_name'], $strNewFile)) { $this->Files->chmod($strNewFile, 0666 & ~umask()); @@ -279,7 +279,7 @@ public static function getMaxUploadSize() } /** - * Resize an uploaded image if neccessary + * Resize an uploaded image if necessary * * @param string $strImage * diff --git a/core-bundle/src/Resources/contao/classes/FrontendTemplate.php b/core-bundle/src/Resources/contao/classes/FrontendTemplate.php index 6d4132a5006..2cd27683c91 100644 --- a/core-bundle/src/Resources/contao/classes/FrontendTemplate.php +++ b/core-bundle/src/Resources/contao/classes/FrontendTemplate.php @@ -30,7 +30,7 @@ class FrontendTemplate extends Template use FrontendTemplateTrait; /** - * Unsued $_GET check + * Unused $_GET check * @var boolean */ protected $blnCheckRequest = false; diff --git a/core-bundle/src/Resources/contao/controllers/BackendIndex.php b/core-bundle/src/Resources/contao/controllers/BackendIndex.php index e9e2edae8d0..0cc13377e29 100644 --- a/core-bundle/src/Resources/contao/controllers/BackendIndex.php +++ b/core-bundle/src/Resources/contao/controllers/BackendIndex.php @@ -18,7 +18,7 @@ use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\UriSigner; -use Symfony\Component\Routing\Router; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -71,7 +71,7 @@ public function run() } $router = $container->get('router'); - $targetPath = $router->generate('contao_backend', array(), Router::ABSOLUTE_URL); + $targetPath = $router->generate('contao_backend', array(), UrlGeneratorInterface::ABSOLUTE_URL); $request = $container->get('request_stack')->getCurrentRequest(); if ($request && $request->query->has('redirect')) diff --git a/core-bundle/src/Resources/contao/dca/tl_settings.php b/core-bundle/src/Resources/contao/dca/tl_settings.php index 1418fa7cfac..1ac974d0756 100644 --- a/core-bundle/src/Resources/contao/dca/tl_settings.php +++ b/core-bundle/src/Resources/contao/dca/tl_settings.php @@ -123,7 +123,7 @@ static function ($strValue) $arrValue = StringUtil::deserialize($strValue, true); $arrAllowedAttributes = array(); - foreach ($arrValue as $intIndex => $arrRow) + foreach ($arrValue as $arrRow) { foreach (StringUtil::trimsplit(',', strtolower($arrRow['key'])) as $strKey) { diff --git a/core-bundle/src/Resources/contao/dca/tl_templates.php b/core-bundle/src/Resources/contao/dca/tl_templates.php index 520bf15f257..d9a60a630a2 100644 --- a/core-bundle/src/Resources/contao/dca/tl_templates.php +++ b/core-bundle/src/Resources/contao/dca/tl_templates.php @@ -419,7 +419,7 @@ public function compareTemplate(DataContainer $dc) } else { - // Try to find the base template by strippig suffixes + // Try to find the base template by stripping suffixes while (strpos($strName, '_') !== false) { $strName = substr($strName, 0, strrpos($strName, '_')); diff --git a/core-bundle/src/Resources/contao/helper/functions.php b/core-bundle/src/Resources/contao/helper/functions.php index 0e9a2d0e2f4..c1a0a67c2c5 100644 --- a/core-bundle/src/Resources/contao/helper/functions.php +++ b/core-bundle/src/Resources/contao/helper/functions.php @@ -723,7 +723,7 @@ function utf8_strlen($str) } /** - * Find the position of the first occurence of a string in another string + * Find the position of the first occurrence of a string in another string * * @param string $haystack * @param string $needle diff --git a/core-bundle/src/Resources/contao/library/Contao/Automator.php b/core-bundle/src/Resources/contao/library/Contao/Automator.php index b626bd1b24b..75f5336d0ce 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Automator.php +++ b/core-bundle/src/Resources/contao/library/Contao/Automator.php @@ -11,6 +11,7 @@ namespace Contao; use Contao\CoreBundle\OptIn\OptIn; +use FOS\HttpCache\CacheInvalidator; use FOS\HttpCacheBundle\CacheManager; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\NullOutput; @@ -23,7 +24,7 @@ class Automator extends System { /** - * Make the constuctor public + * Make the constructor public */ public function __construct() { @@ -173,7 +174,7 @@ public function purgePageCache() /** @var CacheManager $cacheManager */ $cacheManager = $container->get('fos_http_cache.cache_manager'); - if (!$cacheManager->supports(CacheManager::CLEAR)) + if (!$cacheManager->supports(CacheInvalidator::CLEAR)) { $this->log('Cannot purge the shared cache; invalid reverse proxy configuration', __METHOD__, TL_ERROR); diff --git a/core-bundle/src/Resources/contao/library/Contao/Config.php b/core-bundle/src/Resources/contao/library/Contao/Config.php index 02045323415..2dbd1f1e41a 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Config.php +++ b/core-bundle/src/Resources/contao/library/Contao/Config.php @@ -54,7 +54,7 @@ class Config protected $blnIsModified = false; /** - * Local file existance + * Local file existence * @var boolean */ protected static $blnHasLcf; diff --git a/core-bundle/src/Resources/contao/library/Contao/Controller.php b/core-bundle/src/Resources/contao/library/Contao/Controller.php index e3d793127f8..5af8f962f01 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Controller.php +++ b/core-bundle/src/Resources/contao/library/Contao/Controller.php @@ -829,10 +829,7 @@ public static function replaceDynamicScriptTags($strBuffer) // Add the internal jQuery scripts if (!empty($GLOBALS['TL_JQUERY']) && \is_array($GLOBALS['TL_JQUERY'])) { - foreach (array_unique($GLOBALS['TL_JQUERY']) as $script) - { - $strScripts .= $script; - } + $strScripts .= implode('', array_unique($GLOBALS['TL_JQUERY'])); } $arrReplace['[[TL_JQUERY]]'] = $strScripts; @@ -841,10 +838,7 @@ public static function replaceDynamicScriptTags($strBuffer) // Add the internal MooTools scripts if (!empty($GLOBALS['TL_MOOTOOLS']) && \is_array($GLOBALS['TL_MOOTOOLS'])) { - foreach (array_unique($GLOBALS['TL_MOOTOOLS']) as $script) - { - $strScripts .= $script; - } + $strScripts .= implode('', array_unique($GLOBALS['TL_MOOTOOLS'])); } $arrReplace['[[TL_MOOTOOLS]]'] = $strScripts; @@ -853,17 +847,14 @@ public static function replaceDynamicScriptTags($strBuffer) // Add the internal tags if (!empty($GLOBALS['TL_BODY']) && \is_array($GLOBALS['TL_BODY'])) { - foreach (array_unique($GLOBALS['TL_BODY']) as $script) - { - $strScripts .= $script; - } + $strScripts .= implode('', array_unique($GLOBALS['TL_BODY'])); } /** @var PageModel|null $objPage */ global $objPage; $objLayout = ($objPage !== null) ? LayoutModel::findByPk($objPage->layoutId) : null; - $blnCombineScripts = ($objLayout === null) ? false : $objLayout->combineScripts; + $blnCombineScripts = $objLayout !== null && $objLayout->combineScripts; $arrReplace['[[TL_BODY]]'] = $strScripts; $strScripts = ''; diff --git a/core-bundle/src/Resources/contao/library/Contao/Dbafs.php b/core-bundle/src/Resources/contao/library/Contao/Dbafs.php index 4ee5bf32f3a..e7b98bcebad 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Dbafs.php +++ b/core-bundle/src/Resources/contao/library/Contao/Dbafs.php @@ -775,7 +775,7 @@ public static function syncFiles() } /** - * Get the folder hash from the databse by combining the hashes of all children + * Get the folder hash from the database by combining the hashes of all children * * @param string $strPath The relative path * diff --git a/core-bundle/src/Resources/contao/library/Contao/File.php b/core-bundle/src/Resources/contao/library/Contao/File.php index 29884946aa7..c4f61d4315f 100644 --- a/core-bundle/src/Resources/contao/library/Contao/File.php +++ b/core-bundle/src/Resources/contao/library/Contao/File.php @@ -349,13 +349,13 @@ public function __get($strKey) return $this->arrImageViewSize; case 'viewWidth': - // Store in variable as empty() calls __isset() which is not implemented and thus alway true + // Store in variable as empty() calls __isset() which is not implemented and thus always true $imageViewSize = $this->imageViewSize; return !empty($imageViewSize) ? $imageViewSize[0] : null; case 'viewHeight': - // Store in variable as empty() calls __isset() which is not implemented and thus alway true + // Store in variable as empty() calls __isset() which is not implemented and thus always true $imageViewSize = $this->imageViewSize; return !empty($imageViewSize) ? $imageViewSize[1] : null; diff --git a/core-bundle/src/Resources/contao/library/Contao/Folder.php b/core-bundle/src/Resources/contao/library/Contao/Folder.php index 56cfe72a7b4..5c1a3e9983e 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Folder.php +++ b/core-bundle/src/Resources/contao/library/Contao/Folder.php @@ -556,8 +556,6 @@ protected function getPathinfo() */ public static function scan($strFolder, $blnUncached=false): array { - static::$arrScanCache; - // Add a trailing slash if (substr($strFolder, -1, 1) != '/') { @@ -565,9 +563,9 @@ public static function scan($strFolder, $blnUncached=false): array } // Load from cache - if (!$blnUncached && isset($arrScanCache[$strFolder])) + if (!$blnUncached && isset(self::$arrScanCache[$strFolder])) { - return $arrScanCache[$strFolder]; + return self::$arrScanCache[$strFolder]; } $arrReturn = array(); @@ -586,7 +584,7 @@ public static function scan($strFolder, $blnUncached=false): array // Cache the result if (!$blnUncached) { - $arrScanCache[$strFolder] = $arrReturn; + self::$arrScanCache[$strFolder] = $arrReturn; } return $arrReturn; diff --git a/core-bundle/src/Resources/contao/library/Contao/Image.php b/core-bundle/src/Resources/contao/library/Contao/Image.php index a7e138f702d..e4c73febf90 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Image.php +++ b/core-bundle/src/Resources/contao/library/Contao/Image.php @@ -743,7 +743,7 @@ public static function resize($image, $width, $height, $mode='') * Create an image instance from the given image path and size * * @param string|File $image The image path or File instance - * @param array|integer|string $size The image size as array (width, height, resize mode) or an tl_image_size ID or a predifined image size key + * @param array|integer|string $size The image size as array (width, height, resize mode) or an tl_image_size ID or a predefined image size key * * @return static The created image instance * diff --git a/core-bundle/src/Resources/contao/library/Contao/Input.php b/core-bundle/src/Resources/contao/library/Contao/Input.php index 78d58fadc48..dc8c6e452d7 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Input.php +++ b/core-bundle/src/Resources/contao/library/Contao/Input.php @@ -756,7 +756,7 @@ public static function xssClean($varValue, $blnStrictMode=false) return $varValue; } - // Validate standard character entites and UTF16 two byte encoding + // Validate standard character entities and UTF16 two byte encoding $varValue = preg_replace('/(&#*\w+)[\x00-\x20]+;/i', '$1;', $varValue); // Remove carriage returns diff --git a/core-bundle/src/Resources/contao/library/Contao/Model.php b/core-bundle/src/Resources/contao/library/Contao/Model.php index 3c28f3c3dce..cace7e9da55 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Model.php +++ b/core-bundle/src/Resources/contao/library/Contao/Model.php @@ -444,7 +444,7 @@ public function save() $arrSet = $this->preSave($arrSet); - // No modified fiels + // No modified fields if (empty($arrSet)) { return $this; @@ -483,7 +483,7 @@ public function save() $arrSet = $this->preSave($arrSet); - // No modified fiels + // No modified fields if (empty($arrSet)) { return $this; diff --git a/core-bundle/src/Resources/contao/library/Contao/Pagination.php b/core-bundle/src/Resources/contao/library/Contao/Pagination.php index aa1da1ca749..ede20704b79 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Pagination.php +++ b/core-bundle/src/Resources/contao/library/Contao/Pagination.php @@ -11,7 +11,7 @@ namespace Contao; /** - * Provide methodes to render a pagination menu. + * Provide methods to render a pagination menu. * * @author Leo Feyer */ @@ -96,7 +96,7 @@ class Pagination protected $strUrl = ''; /** - * Page paramenter + * Page parameter * @var string */ protected $strParameter = 'page'; diff --git a/core-bundle/src/Resources/contao/library/Contao/Picture.php b/core-bundle/src/Resources/contao/library/Contao/Picture.php index e079807974f..902c35828a3 100644 --- a/core-bundle/src/Resources/contao/library/Contao/Picture.php +++ b/core-bundle/src/Resources/contao/library/Contao/Picture.php @@ -82,7 +82,7 @@ public function __construct(File $file) * Create a picture instance from the given image path and size * * @param string|File $file The image path or File instance - * @param array|integer|string $size The image size as array (width, height, resize mode) or an tl_image_size ID or a predifined image size key + * @param array|integer|string $size The image size as array (width, height, resize mode) or an tl_image_size ID or a predefined image size key * * @return static The created picture instance */ @@ -179,7 +179,7 @@ public function setImportantPart(array $importantPart = null) /** * Set the image size * - * @param ImageSizeModel|object|string $imageSize The image size or a predifined image size key + * @param ImageSizeModel|object|string $imageSize The image size or a predefined image size key * * @return $this The picture object */ diff --git a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php index 6742796ec1d..5f2be943006 100644 --- a/core-bundle/src/Resources/contao/library/Contao/StringUtil.php +++ b/core-bundle/src/Resources/contao/library/Contao/StringUtil.php @@ -112,7 +112,7 @@ public static function substrHtml($strString, $intNumberOfChars) $strString = strip_tags($strString, Config::get('allowedTags')); $strString = preg_replace('/ +/', ' ', $strString); - // Seperate tags and text + // Separate tags and text $arrChunks = preg_split('/(<[^>]+>)/', $strString, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); for ($i=0, $c=\count($arrChunks); $i<$c; $i++) diff --git a/core-bundle/src/Resources/contao/library/Contao/ZipReader.php b/core-bundle/src/Resources/contao/library/Contao/ZipReader.php index 51ab9ea31aa..58f926a9a07 100644 --- a/core-bundle/src/Resources/contao/library/Contao/ZipReader.php +++ b/core-bundle/src/Resources/contao/library/Contao/ZipReader.php @@ -58,7 +58,7 @@ class ZipReader { /** - * File signatur + * File signature */ const FILE_SIGNATURE = "\x50\x4b\x03\x04"; diff --git a/core-bundle/src/Resources/contao/library/Contao/ZipWriter.php b/core-bundle/src/Resources/contao/library/Contao/ZipWriter.php index 61d5e069672..32e7b9cda7f 100644 --- a/core-bundle/src/Resources/contao/library/Contao/ZipWriter.php +++ b/core-bundle/src/Resources/contao/library/Contao/ZipWriter.php @@ -24,7 +24,7 @@ class ZipWriter { /** - * File signatur + * File signature */ const FILE_SIGNATURE = "\x50\x4b\x03\x04"; diff --git a/core-bundle/src/Resources/contao/modules/ModuleArticle.php b/core-bundle/src/Resources/contao/modules/ModuleArticle.php index 54f26e56fde..ce038c61c09 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleArticle.php +++ b/core-bundle/src/Resources/contao/modules/ModuleArticle.php @@ -15,7 +15,7 @@ use Contao\CoreBundle\String\HtmlDecoder; /** - * Provides methodes to handle articles. + * Provides methods to handle articles. * * @property integer $tstamp * @property string $title diff --git a/core-bundle/src/Resources/contao/modules/ModuleNavigation.php b/core-bundle/src/Resources/contao/modules/ModuleNavigation.php index ab381e59e2a..4d9f5e629b9 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleNavigation.php +++ b/core-bundle/src/Resources/contao/modules/ModuleNavigation.php @@ -72,7 +72,7 @@ protected function compile() $lang = null; $host = null; - // Overwrite the domain and language if the reference page belongs to a differnt root page (see #3765) + // Overwrite the domain and language if the reference page belongs to a different root page (see #3765) if ($this->defineRoot && $this->rootPage > 0) { $objRootPage = PageModel::findWithDetails($this->rootPage); diff --git a/core-bundle/src/Resources/contao/modules/ModuleQuicknav.php b/core-bundle/src/Resources/contao/modules/ModuleQuicknav.php index a9310befc37..43dd32a0956 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleQuicknav.php +++ b/core-bundle/src/Resources/contao/modules/ModuleQuicknav.php @@ -70,7 +70,7 @@ protected function compile() $this->rootPage = $objPage->rootId; } - // Overwrite the domain and language if the reference page belongs to a differnt root page (see #3765) + // Overwrite the domain and language if the reference page belongs to a different root page (see #3765) else { $objRootPage = PageModel::findWithDetails($this->rootPage); diff --git a/core-bundle/src/Resources/contao/modules/ModuleSitemap.php b/core-bundle/src/Resources/contao/modules/ModuleSitemap.php index 72dadcb588e..f3b99ff25f9 100644 --- a/core-bundle/src/Resources/contao/modules/ModuleSitemap.php +++ b/core-bundle/src/Resources/contao/modules/ModuleSitemap.php @@ -64,7 +64,7 @@ protected function compile() $this->rootPage = $objPage->rootId; } - // Overwrite the domain and language if the reference page belongs to a differnt root page (see #3765) + // Overwrite the domain and language if the reference page belongs to a different root page (see #3765) else { $objRootPage = PageModel::findWithDetails($this->rootPage); diff --git a/core-bundle/src/Resources/contao/themes/flexible/main.css b/core-bundle/src/Resources/contao/themes/flexible/main.css index 54a7a68f112..3c7147105f9 100644 --- a/core-bundle/src/Resources/contao/themes/flexible/main.css +++ b/core-bundle/src/Resources/contao/themes/flexible/main.css @@ -1851,7 +1851,7 @@ time[title] { float:right; } -/* Backwards compatiblity */ +/* Backwards compatibility */ #manager { padding:9px 6px 0; border-bottom:1px solid #ddd; diff --git a/core-bundle/src/Resources/contao/widgets/FileSelector.php b/core-bundle/src/Resources/contao/widgets/FileSelector.php index 6e2a8c6cc28..c5751c70cb0 100644 --- a/core-bundle/src/Resources/contao/widgets/FileSelector.php +++ b/core-bundle/src/Resources/contao/widgets/FileSelector.php @@ -471,7 +471,7 @@ protected function renderFiletree($path, $intMargin, $mount=false, $blnProtected $blnIsOpen = (!empty($arrFound) || $session[$node][$tid] == 1 || \count(preg_grep('/^' . preg_quote($currentFolder, '/') . '\//', $this->varValue)) > 0); $return .= "\n " . '
  • '; - // Add a toggle button if there are childs + // Add a toggle button if there are children if ($countFiles > 0) { $folderAttribute = ''; diff --git a/core-bundle/src/String/HtmlDecoder.php b/core-bundle/src/String/HtmlDecoder.php index bb8328a8a5f..18b684abbdb 100644 --- a/core-bundle/src/String/HtmlDecoder.php +++ b/core-bundle/src/String/HtmlDecoder.php @@ -31,7 +31,7 @@ public function inputEncodedToPlainText(string $val, bool $removeInsertTags = fa if ($removeInsertTags) { $val = StringUtil::stripInsertTags($val); } else { - $val = (new InsertTags())->replace($val, false); + $val = (string) (new InsertTags())->replace($val, false); } $val = strip_tags($val); @@ -52,7 +52,7 @@ public function inputEncodedToPlainText(string $val, bool $removeInsertTags = fa public function htmlToPlainText(string $val, bool $removeInsertTags = false): string { if (!$removeInsertTags) { - $val = (new InsertTags())->replace($val, false); + $val = (string) (new InsertTags())->replace($val, false); } // Add new lines before and after block level elements diff --git a/core-bundle/src/Twig/Extension/ContaoExtension.php b/core-bundle/src/Twig/Extension/ContaoExtension.php index ad5841caeaa..11acea169eb 100644 --- a/core-bundle/src/Twig/Extension/ContaoExtension.php +++ b/core-bundle/src/Twig/Extension/ContaoExtension.php @@ -208,10 +208,7 @@ public function renderLegacyTemplate(string $name, array $blocks, array $context public function setBlocks(array $blocks): void { - $this->arrBlocks = array_map( - static fn ($block) => \is_array($block) ? $block : [$block], - $blocks - ); + $this->arrBlocks = array_map(static fn ($block) => \is_array($block) ? $block : [$block], $blocks); } public function parse(): string diff --git a/core-bundle/src/Twig/Runtime/PictureConfigurationRuntime.php b/core-bundle/src/Twig/Runtime/PictureConfigurationRuntime.php index 46c672b8fca..37a43ede706 100644 --- a/core-bundle/src/Twig/Runtime/PictureConfigurationRuntime.php +++ b/core-bundle/src/Twig/Runtime/PictureConfigurationRuntime.php @@ -109,10 +109,7 @@ private function throwInvalidArgumentException(array $unmappedConfig, string $pr // Prepend prefix if (null !== $prefix) { - $keys = array_map( - static fn (string $v): string => "$prefix.$v", - $keys - ); + $keys = array_map(static fn (string $v): string => "$prefix.$v", $keys); } throw new \InvalidArgumentException(sprintf('Could not map picture configuration key(s) "%s".', implode('", "', $keys))); diff --git a/core-bundle/tests/Command/DebugPagesCommandTest.php b/core-bundle/tests/Command/DebugPagesCommandTest.php index ac614d6fa50..5dcb7baae78 100644 --- a/core-bundle/tests/Command/DebugPagesCommandTest.php +++ b/core-bundle/tests/Command/DebugPagesCommandTest.php @@ -65,9 +65,7 @@ public function testCommandOutput(array $pages, array $legacyPages, string $expe $pageRegistry ->method('supportsContentComposition') - ->willReturnCallback( - static fn (PageModel $pageModel): bool => 'regular' === $pageModel->type - ) + ->willReturnCallback(static fn (PageModel $pageModel): bool => 'regular' === $pageModel->type) ; $command = new DebugPagesCommand($this->mockContaoFramework(), $pageRegistry); diff --git a/core-bundle/tests/Command/MigrateCommandTest.php b/core-bundle/tests/Command/MigrateCommandTest.php index f8a4d7cb1d2..c602f819bf2 100644 --- a/core-bundle/tests/Command/MigrateCommandTest.php +++ b/core-bundle/tests/Command/MigrateCommandTest.php @@ -414,9 +414,6 @@ private function getCommand(array $pendingMigrations = [], array $migrationResul private function jsonArrayFromNdjson(string $ndjson): array { - return array_map( - static fn (string $line) => json_decode($line, true), - explode("\n", trim($ndjson)) - ); + return array_map(static fn (string $line) => json_decode($line, true), explode("\n", trim($ndjson))); } } diff --git a/core-bundle/tests/Contao/ArrayUtilTest.php b/core-bundle/tests/Contao/ArrayUtilTest.php index 46f73ac9e55..a1e09717ec2 100644 --- a/core-bundle/tests/Contao/ArrayUtilTest.php +++ b/core-bundle/tests/Contao/ArrayUtilTest.php @@ -24,50 +24,26 @@ public function testSortsByOrderField(array $items, array $order, array $expecte { $this->assertSame($expected, ArrayUtil::sortByOrderField($items, $order)); - $itemArrays = array_map( - static fn ($item): array => ['uuid' => $item], - $items - ); - $expectedArrays = array_map( - static fn ($item): array => ['uuid' => $item], - $expected - ); + $itemArrays = array_map(static fn ($item): array => ['uuid' => $item], $items); + $expectedArrays = array_map(static fn ($item): array => ['uuid' => $item], $expected); $this->assertSame($expectedArrays, ArrayUtil::sortByOrderField($itemArrays, $order)); $this->assertSame($expectedArrays, ArrayUtil::sortByOrderField($itemArrays, serialize($order))); - $itemArrays = array_map( - static fn ($item): array => ['id' => $item], - $items - ); - $expectedArrays = array_map( - static fn ($item): array => ['id' => $item], - $expected - ); + $itemArrays = array_map(static fn ($item): array => ['id' => $item], $items); + $expectedArrays = array_map(static fn ($item): array => ['id' => $item], $expected); $this->assertSame($expectedArrays, ArrayUtil::sortByOrderField($itemArrays, $order, 'id')); $this->assertSame($expectedArrays, ArrayUtil::sortByOrderField($itemArrays, serialize($order), 'id')); - $itemObjects = array_map( - static fn ($item): \stdClass => (object) ['uuid' => $item], - $items - ); - $expectedObjects = array_map( - static fn ($item): \stdClass => (object) ['uuid' => $item], - $expected - ); + $itemObjects = array_map(static fn ($item): \stdClass => (object) ['uuid' => $item], $items); + $expectedObjects = array_map(static fn ($item): \stdClass => (object) ['uuid' => $item], $expected); $this->assertSame(array_map('get_object_vars', $expectedObjects), array_map('get_object_vars', ArrayUtil::sortByOrderField($itemObjects, $order))); $this->assertSame(array_map('get_object_vars', $expectedObjects), array_map('get_object_vars', ArrayUtil::sortByOrderField($itemObjects, serialize($order)))); - $itemObjects = array_map( - static fn ($item): \stdClass => (object) ['id' => $item], - $items - ); - $expectedObjects = array_map( - static fn ($item): \stdClass => (object) ['id' => $item], - $expected - ); + $itemObjects = array_map(static fn ($item): \stdClass => (object) ['id' => $item], $items); + $expectedObjects = array_map(static fn ($item): \stdClass => (object) ['id' => $item], $expected); $this->assertSame(array_map('get_object_vars', $expectedObjects), array_map('get_object_vars', ArrayUtil::sortByOrderField($itemObjects, $order, 'id'))); $this->assertSame(array_map('get_object_vars', $expectedObjects), array_map('get_object_vars', ArrayUtil::sortByOrderField($itemObjects, serialize($order), 'id'))); diff --git a/core-bundle/tests/Contao/WidgetTest.php b/core-bundle/tests/Contao/WidgetTest.php index 020476001ba..c31f2f22b3c 100644 --- a/core-bundle/tests/Contao/WidgetTest.php +++ b/core-bundle/tests/Contao/WidgetTest.php @@ -143,18 +143,14 @@ public function testValidatesThePostData(): void ; $widget - ->setInputCallback( - static fn (): string => 'foobar' - ) + ->setInputCallback(static fn (): string => 'foobar') ->validate() ; $this->assertSame('foobar', $widget->value); $widget - ->setInputCallback( - static fn () => null - ) + ->setInputCallback(static fn () => null) ->validate() ; diff --git a/core-bundle/tests/ContaoCoreBundleTest.php b/core-bundle/tests/ContaoCoreBundleTest.php index d5fcbd10747..c697e4e0388 100644 --- a/core-bundle/tests/ContaoCoreBundleTest.php +++ b/core-bundle/tests/ContaoCoreBundleTest.php @@ -83,9 +83,7 @@ public function testAddsTheCompilerPasses(): void $security ->expects($this->once()) ->method('addSecurityListenerFactory') - ->with($this->callback( - static fn ($param) => $param instanceof ContaoLoginFactory - )) + ->with($this->callback(static fn ($param) => $param instanceof ContaoLoginFactory)) ; $container = $this->createMock(ContainerBuilder::class); diff --git a/core-bundle/tests/DependencyInjection/Compiler/RegisterPagesPassTest.php b/core-bundle/tests/DependencyInjection/Compiler/RegisterPagesPassTest.php index 77828eed0fb..30004489ce8 100644 --- a/core-bundle/tests/DependencyInjection/Compiler/RegisterPagesPassTest.php +++ b/core-bundle/tests/DependencyInjection/Compiler/RegisterPagesPassTest.php @@ -54,9 +54,7 @@ public function testGetsPageTypeFromAttributes(): void ->method('addMethodCall') ->with( 'add', - $this->callback( - static fn ($arguments) => 'my_type' === $arguments[0] - ) + $this->callback(static fn ($arguments) => 'my_type' === $arguments[0]) ) ; @@ -79,9 +77,7 @@ public function testGetsPageTypeFromClass(): void ->method('addMethodCall') ->with( 'add', - $this->callback( - static fn ($arguments) => 'contao_core_bundle' === $arguments[0] - ) + $this->callback(static fn ($arguments) => 'contao_core_bundle' === $arguments[0]) ) ; @@ -104,9 +100,7 @@ public function testStripsControllerSuffixOnGetPageTypeFromClass(): void ->method('addMethodCall') ->with( 'add', - $this->callback( - static fn ($arguments) => 'two_factor' === $arguments[0] - ) + $this->callback(static fn ($arguments) => 'two_factor' === $arguments[0]) ) ; @@ -129,9 +123,7 @@ public function testStripsPageControllerSuffixOnGetPageTypeFromClass(): void ->method('addMethodCall') ->with( 'add', - $this->callback( - static fn ($arguments) => 'test' === $arguments[0] - ) + $this->callback(static fn ($arguments) => 'test' === $arguments[0]) ) ; @@ -258,9 +250,7 @@ public function testAddsContainerCallIfClassExtendsSymfonyAbstractController(): ->expects($this->once()) ->method('addMethodCall') ->with( - $this->callback( - static fn ($method) => 'setContainer' === $method - ), + $this->callback(static fn ($method) => 'setContainer' === $method), $this->callback( static fn (array $arguments) => 1 === \count($arguments) && $arguments[0] instanceof Reference diff --git a/core-bundle/tests/EventListener/DataContainer/PageUrlListenerTest.php b/core-bundle/tests/EventListener/DataContainer/PageUrlListenerTest.php index 0de2a23b8ea..58b958c0739 100644 --- a/core-bundle/tests/EventListener/DataContainer/PageUrlListenerTest.php +++ b/core-bundle/tests/EventListener/DataContainer/PageUrlListenerTest.php @@ -1634,31 +1634,23 @@ private function mockFrameworkWithPages(array $inputData, array ...$data): Conta $pageAdapter = $this->mockAdapter(['findByPk', 'findWithDetails', 'findByPid']); $pageAdapter ->method('findByPk') - ->willReturnCallback( - static fn (int $id) => $pagesById[$id] ?? null - ) + ->willReturnCallback(static fn (int $id) => $pagesById[$id] ?? null) ; $pageAdapter ->method('findWithDetails') - ->willReturnCallback( - static fn (int $id) => $pagesById[$id] ?? null - ) + ->willReturnCallback(static fn (int $id) => $pagesById[$id] ?? null) ; $pageAdapter ->method('findByPid') - ->willReturnCallback( - static fn (int $pid) => $pagesByPid[$pid] ?? null - ) + ->willReturnCallback(static fn (int $pid) => $pagesByPid[$pid] ?? null) ; $inputAdapter = $this->mockAdapter(['post']); $inputAdapter ->method('post') - ->willReturnCallback( - static fn ($key) => $inputData[$key] ?? null - ) + ->willReturnCallback(static fn ($key) => $inputData[$key] ?? null) ; return $this->mockContaoFramework( diff --git a/core-bundle/tests/EventListener/FilterPageTypeListenerTest.php b/core-bundle/tests/EventListener/FilterPageTypeListenerTest.php index 9928e7ecddf..d2de85431f0 100644 --- a/core-bundle/tests/EventListener/FilterPageTypeListenerTest.php +++ b/core-bundle/tests/EventListener/FilterPageTypeListenerTest.php @@ -124,10 +124,7 @@ public function testRemovesErrorTypesAlreadyPresentInTheRootPage(): void */ private function mockDataContainer(?int $pid, int $id = null): DataContainer { - $activeRecord = array_filter( - compact('id', 'pid'), - static fn ($v): bool => null !== $v - ); + $activeRecord = array_filter(compact('id', 'pid'), static fn ($v): bool => null !== $v); /** @var DataContainer&MockObject */ return $this->mockClassWithProperties( diff --git a/core-bundle/tests/EventListener/Menu/BackendLogoutListenerTest.php b/core-bundle/tests/EventListener/Menu/BackendLogoutListenerTest.php index d30fab27382..915d214098f 100644 --- a/core-bundle/tests/EventListener/Menu/BackendLogoutListenerTest.php +++ b/core-bundle/tests/EventListener/Menu/BackendLogoutListenerTest.php @@ -212,9 +212,7 @@ private function getTranslator(): TranslatorInterface $translator = $this->createMock(TranslatorInterface::class); $translator ->method('trans') - ->willReturnCallback( - static fn (string $id): string => $id - ) + ->willReturnCallback(static fn (string $id): string => $id) ; return $translator; diff --git a/core-bundle/tests/EventListener/Menu/BackendMenuListenerTest.php b/core-bundle/tests/EventListener/Menu/BackendMenuListenerTest.php index 41a1305f5da..75b2438a9f3 100644 --- a/core-bundle/tests/EventListener/Menu/BackendMenuListenerTest.php +++ b/core-bundle/tests/EventListener/Menu/BackendMenuListenerTest.php @@ -403,9 +403,7 @@ private function getTranslator(): TranslatorInterface $translator = $this->createMock(TranslatorInterface::class); $translator ->method('trans') - ->willReturnCallback( - static fn (string $id): string => $id - ) + ->willReturnCallback(static fn (string $id): string => $id) ; return $translator; diff --git a/core-bundle/tests/EventListener/Menu/BackendPreviewListenerTest.php b/core-bundle/tests/EventListener/Menu/BackendPreviewListenerTest.php index 036f16c7271..480f343f3b3 100644 --- a/core-bundle/tests/EventListener/Menu/BackendPreviewListenerTest.php +++ b/core-bundle/tests/EventListener/Menu/BackendPreviewListenerTest.php @@ -247,9 +247,7 @@ private function getTranslator(): TranslatorInterface $translator = $this->createMock(TranslatorInterface::class); $translator ->method('trans') - ->willReturnCallback( - static fn (string $id): string => $id - ) + ->willReturnCallback(static fn (string $id): string => $id) ; return $translator; diff --git a/core-bundle/tests/Image/PictureFactoryTest.php b/core-bundle/tests/Image/PictureFactoryTest.php index 5541cd70fee..d6286128ccb 100644 --- a/core-bundle/tests/Image/PictureFactoryTest.php +++ b/core-bundle/tests/Image/PictureFactoryTest.php @@ -395,9 +395,7 @@ public function testCreatesAPictureObjectInLegacyMode(): void ->expects($this->exactly(2)) ->method('generate') ->with( - $this->callback( - static fn (): bool => true - ), + $this->callback(static fn (): bool => true), $this->callback( function (PictureConfiguration $config): bool { $this->assertSame($config->getSizeItems(), []); @@ -411,9 +409,7 @@ function (PictureConfiguration $config): bool { return true; } ), - $this->callback( - static fn (): bool => true - ) + $this->callback(static fn (): bool => true) ) ->willReturn($pictureMock) ; @@ -436,9 +432,7 @@ function (string $imagePath) use ($path): bool { ->expects($this->exactly(2)) ->method('getImportantPartFromLegacyMode') ->with( - $this->callback( - static fn (): bool => true - ), + $this->callback(static fn (): bool => true), $this->callback( function (string $mode): bool { $this->assertSame('left_top', $mode); diff --git a/core-bundle/tests/Image/Studio/FigureBuilderTest.php b/core-bundle/tests/Image/Studio/FigureBuilderTest.php index 43126faa993..3aefd5b8c68 100644 --- a/core-bundle/tests/Image/Studio/FigureBuilderTest.php +++ b/core-bundle/tests/Image/Studio/FigureBuilderTest.php @@ -311,9 +311,7 @@ public function testFromMixed($identifier): void $validatorAdapter = $this->mockAdapter(['isUuid']); $validatorAdapter ->method('isUuid') - ->willReturnCallback( - static fn ($value): bool => '1d902bf1-2683-406e-b004-f0b59095e5a1' === $value - ) + ->willReturnCallback(static fn ($value): bool => '1d902bf1-2683-406e-b004-f0b59095e5a1' === $value) ; $framework = $this->mockContaoFramework([ diff --git a/core-bundle/tests/Picker/ArticlePickerProviderTest.php b/core-bundle/tests/Picker/ArticlePickerProviderTest.php index ce62837c174..26b3cba7df2 100644 --- a/core-bundle/tests/Picker/ArticlePickerProviderTest.php +++ b/core-bundle/tests/Picker/ArticlePickerProviderTest.php @@ -182,9 +182,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; return new ArticlePickerProvider($menuFactory, $router, null, $security); diff --git a/core-bundle/tests/Picker/FilePickerProviderTest.php b/core-bundle/tests/Picker/FilePickerProviderTest.php index 97cd3a50451..d44dca5b342 100644 --- a/core-bundle/tests/Picker/FilePickerProviderTest.php +++ b/core-bundle/tests/Picker/FilePickerProviderTest.php @@ -245,9 +245,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; /** @var FilesModel&MockObject $filesModel */ diff --git a/core-bundle/tests/Picker/PagePickerProviderTest.php b/core-bundle/tests/Picker/PagePickerProviderTest.php index 482f8a116c5..1e7b09b9b1d 100644 --- a/core-bundle/tests/Picker/PagePickerProviderTest.php +++ b/core-bundle/tests/Picker/PagePickerProviderTest.php @@ -207,9 +207,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; $translator = $this->createMock(TranslatorInterface::class); diff --git a/core-bundle/tests/Routing/AbstractPageRouteProviderTest.php b/core-bundle/tests/Routing/AbstractPageRouteProviderTest.php index e1d4bf65ba2..456e42db418 100644 --- a/core-bundle/tests/Routing/AbstractPageRouteProviderTest.php +++ b/core-bundle/tests/Routing/AbstractPageRouteProviderTest.php @@ -263,15 +263,9 @@ public function testOrdersRoutesByPreferredLanguages(array $pageLanguages, array $pageLanguages ); - usort( - $routes, - static fn ($a, $b) => $method->invoke($instance, $a, $b, $preferredLanguages) - ); + usort($routes, static fn ($a, $b) => $method->invoke($instance, $a, $b, $preferredLanguages)); - $result = array_map( - static fn (Route $route) => $route->getDefault('pageModel')->rootLanguage, - $routes - ); + $result = array_map(static fn (Route $route) => $route->getDefault('pageModel')->rootLanguage, $routes); $this->assertSame($expected, $result); } diff --git a/core-bundle/tests/Routing/Matcher/LegacyMatcherTest.php b/core-bundle/tests/Routing/Matcher/LegacyMatcherTest.php index 053c24e072e..b8789dd66dc 100644 --- a/core-bundle/tests/Routing/Matcher/LegacyMatcherTest.php +++ b/core-bundle/tests/Routing/Matcher/LegacyMatcherTest.php @@ -651,9 +651,7 @@ private function mockConfigAdapter(array $config): Adapter $configAdapter = $this->mockAdapter(['get']); $configAdapter ->method('get') - ->willReturnCallback( - static fn ($param) => $config[$param] ?? null - ) + ->willReturnCallback(static fn ($param) => $config[$param] ?? null) ; return $configAdapter; diff --git a/faq-bundle/tests/Picker/FaqPickerProviderTest.php b/faq-bundle/tests/Picker/FaqPickerProviderTest.php index 0012ee91a75..792f23104be 100644 --- a/faq-bundle/tests/Picker/FaqPickerProviderTest.php +++ b/faq-bundle/tests/Picker/FaqPickerProviderTest.php @@ -244,9 +244,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; $translator = $this->createMock(TranslatorInterface::class); diff --git a/installation-bundle/tests/Database/InstallerTest.php b/installation-bundle/tests/Database/InstallerTest.php index 08ac0c5ecc6..72e79aa776d 100644 --- a/installation-bundle/tests/Database/InstallerTest.php +++ b/installation-bundle/tests/Database/InstallerTest.php @@ -16,7 +16,7 @@ use Contao\InstallationBundle\Database\Installer; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\MySQLSchemaManager; use Doctrine\DBAL\Schema\Schema; use PHPUnit\Framework\MockObject\MockObject; @@ -546,7 +546,7 @@ private function getInstaller(Schema $fromSchema = null, Schema $toSchema = null $connection ->method('getDatabasePlatform') - ->willReturn(new MySqlPlatform()) + ->willReturn(new MySQLPlatform()) ; $connection diff --git a/manager-bundle/tests/ContaoManager/PluginTest.php b/manager-bundle/tests/ContaoManager/PluginTest.php index 0f449e7685e..be13afc39d0 100644 --- a/manager-bundle/tests/ContaoManager/PluginTest.php +++ b/manager-bundle/tests/ContaoManager/PluginTest.php @@ -144,9 +144,7 @@ public function testRegistersModuleBundles(): void $parser ->expects($this->atLeastOnce()) ->method('parse') - ->willReturnCallback( - static fn ($resource): array => [$resource] - ) + ->willReturnCallback(static fn ($resource): array => [$resource]) ; $plugin = new Plugin(); @@ -863,10 +861,7 @@ public function getInvalidMailerUrlParameters(): \Generator public function testDoesNotAddDefaultDoctrineMappingIfEntityFolderDoesNotExists(): void { - $plugin = new Plugin( - fn () => $this->createMock(Connection::class) - ); - + $plugin = new Plugin(); $extensionConfig = $plugin->getExtensionConfig('doctrine', [], $this->getContainer()); // Ignore the DBAL entry @@ -931,9 +926,7 @@ public function testOnlyAddsTheDefaultDoctrineMappingIfAutoMappingIsEnabledAndNo ); } - $plugin = new Plugin( - fn () => $this->createMock(Connection::class) - ); + $plugin = new Plugin(); $container = $this->getContainer(); $container->setParameter('kernel.project_dir', __DIR__.'/../Fixtures/app-with-entities'); diff --git a/manager-bundle/tests/EventListener/BackendMenuListenerTest.php b/manager-bundle/tests/EventListener/BackendMenuListenerTest.php index 586b9414e5a..ad760c0498a 100644 --- a/manager-bundle/tests/EventListener/BackendMenuListenerTest.php +++ b/manager-bundle/tests/EventListener/BackendMenuListenerTest.php @@ -283,9 +283,7 @@ private function getTranslator(): TranslatorInterface $translator = $this->createMock(TranslatorInterface::class); $translator ->method('trans') - ->willReturnCallback( - static fn (string $id): string => $id - ) + ->willReturnCallback(static fn (string $id): string => $id) ; return $translator; diff --git a/news-bundle/src/Resources/contao/modules/ModuleNewsMenu.php b/news-bundle/src/Resources/contao/modules/ModuleNewsMenu.php index 180c4a2e5f4..fa29e730ba4 100644 --- a/news-bundle/src/Resources/contao/modules/ModuleNewsMenu.php +++ b/news-bundle/src/Resources/contao/modules/ModuleNewsMenu.php @@ -205,7 +205,7 @@ protected function compileMonthlyMenu() } /** - * Generate the dayil menu + * Generate the daily menu */ protected function compileDailyMenu() { diff --git a/news-bundle/tests/Picker/NewsPickerProviderTest.php b/news-bundle/tests/Picker/NewsPickerProviderTest.php index be2d91f4765..f97ae2ad6d1 100644 --- a/news-bundle/tests/Picker/NewsPickerProviderTest.php +++ b/news-bundle/tests/Picker/NewsPickerProviderTest.php @@ -244,9 +244,7 @@ static function (string $name, array $data) use ($menuFactory): ItemInterface { $router = $this->createMock(RouterInterface::class); $router ->method('generate') - ->willReturnCallback( - static fn (string $name, array $params): string => $name.'?'.http_build_query($params) - ) + ->willReturnCallback(static fn (string $name, array $params): string => $name.'?'.http_build_query($params)) ; $translator = $this->createMock(TranslatorInterface::class); diff --git a/tools/ecs/composer.lock b/tools/ecs/composer.lock index 9136464d1a4..d5818a7c8d9 100644 --- a/tools/ecs/composer.lock +++ b/tools/ecs/composer.lock @@ -123,16 +123,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "0.5.6", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fac86158ffc7392e49636f77e63684c026df43b8" + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fac86158ffc7392e49636f77e63684c026df43b8", - "reference": "fac86158ffc7392e49636f77e63684c026df43b8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", "shasum": "" }, "require": { @@ -141,15 +141,15 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.87", - "phpstan/phpstan-strict-rules": "^0.12.5", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.5-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -166,38 +166,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/0.5.6" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" }, - "time": "2021-08-31T08:08:22+00:00" + "time": "2021-09-16T20:46:02+00:00" }, { "name": "slevomat/coding-standard", - "version": "7.0.15", + "version": "7.0.16", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "cc80e59f9b4ca642f02dc1b615c37a9afc2a0f80" + "reference": "14c324b2f2f0072933036c2f3abaeda16a56dcd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/cc80e59f9b4ca642f02dc1b615c37a9afc2a0f80", - "reference": "cc80e59f9b4ca642f02dc1b615c37a9afc2a0f80", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/14c324b2f2f0072933036c2f3abaeda16a56dcd3", + "reference": "14c324b2f2f0072933036c2f3abaeda16a56dcd3", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "0.5.1 - 0.5.6", - "squizlabs/php_codesniffer": "^3.6.0" + "phpstan/phpdoc-parser": "^1.0.0", + "squizlabs/php_codesniffer": "^3.6.1" }, "require-dev": { "phing/phing": "2.17.0", "php-parallel-lint/php-parallel-lint": "1.3.1", - "phpstan/phpstan": "0.12.98", + "phpstan/phpstan": "0.12.99", "phpstan/phpstan-deprecation-rules": "0.12.6", "phpstan/phpstan-phpunit": "0.12.22", "phpstan/phpstan-strict-rules": "0.12.11", - "phpunit/phpunit": "7.5.20|8.5.5|9.5.9" + "phpunit/phpunit": "7.5.20|8.5.5|9.5.10" }, "type": "phpcodesniffer-standard", "extra": { @@ -217,7 +217,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/7.0.15" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.16" }, "funding": [ { @@ -229,20 +229,20 @@ "type": "tidelift" } ], - "time": "2021-09-09T10:29:09+00:00" + "time": "2021-10-22T06:56:51+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.0", + "version": "3.6.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625" + "reference": "f268ca40d54617c6e06757f83f699775c9b3ff2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625", - "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/f268ca40d54617c6e06757f83f699775c9b3ff2e", + "reference": "f268ca40d54617c6e06757f83f699775c9b3ff2e", "shasum": "" }, "require": { @@ -285,7 +285,7 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-04-09T00:54:41+00:00" + "time": "2021-10-11T04:00:11+00:00" }, { "name": "symplify/easy-coding-standard", diff --git a/tools/psalm/composer.lock b/tools/psalm/composer.lock index 748b2ae68b9..760db2b9592 100644 --- a/tools/psalm/composer.lock +++ b/tools/psalm/composer.lock @@ -247,16 +247,16 @@ }, { "name": "composer/semver", - "version": "3.2.5", + "version": "3.2.6", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9" + "reference": "83e511e247de329283478496f7a1e114c9517506" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/31f3ea725711245195f62e54ffa402d8ef2fdba9", - "reference": "31f3ea725711245195f62e54ffa402d8ef2fdba9", + "url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506", + "reference": "83e511e247de329283478496f7a1e114c9517506", "shasum": "" }, "require": { @@ -308,7 +308,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.5" + "source": "https://github.com/composer/semver/tree/3.2.6" }, "funding": [ { @@ -324,7 +324,7 @@ "type": "tidelift" } ], - "time": "2021-05-24T12:41:47+00:00" + "time": "2021-10-25T11:34:17+00:00" }, { "name": "composer/xdebug-handler", @@ -743,16 +743,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", "shasum": "" }, "require": { @@ -763,7 +763,8 @@ "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -793,9 +794,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" }, - "time": "2020-09-03T19:13:55+00:00" + "time": "2021-10-19T17:43:47+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -909,16 +910,16 @@ }, { "name": "psalm/plugin-symfony", - "version": "v3.0.3", + "version": "v3.0.4", "source": { "type": "git", "url": "https://github.com/psalm/psalm-plugin-symfony.git", - "reference": "7b19393d11204e63a1c4b3cdee8585aa72a0ea70" + "reference": "65586604237c9062c15adc92faee5f84d1698af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/psalm/psalm-plugin-symfony/zipball/7b19393d11204e63a1c4b3cdee8585aa72a0ea70", - "reference": "7b19393d11204e63a1c4b3cdee8585aa72a0ea70", + "url": "https://api.github.com/repos/psalm/psalm-plugin-symfony/zipball/65586604237c9062c15adc92faee5f84d1698af6", + "reference": "65586604237c9062c15adc92faee5f84d1698af6", "shasum": "" }, "require": { @@ -967,9 +968,9 @@ "description": "Psalm Plugin for Symfony", "support": { "issues": "https://github.com/psalm/psalm-plugin-symfony/issues", - "source": "https://github.com/psalm/psalm-plugin-symfony/tree/v3.0.3" + "source": "https://github.com/psalm/psalm-plugin-symfony/tree/v3.0.4" }, - "time": "2021-09-06T15:39:00+00:00" + "time": "2021-10-14T04:48:39+00:00" }, { "name": "psr/cache", @@ -1236,16 +1237,16 @@ }, { "name": "symfony/cache", - "version": "v5.3.8", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "945bcebfef0aeef105de61843dd14105633ae38f" + "reference": "2056f2123f47c9f63102a8b92974c362f4fba568" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/945bcebfef0aeef105de61843dd14105633ae38f", - "reference": "945bcebfef0aeef105de61843dd14105633ae38f", + "url": "https://api.github.com/repos/symfony/cache/zipball/2056f2123f47c9f63102a8b92974c362f4fba568", + "reference": "2056f2123f47c9f63102a8b92974c362f4fba568", "shasum": "" }, "require": { @@ -1313,7 +1314,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v5.3.8" + "source": "https://github.com/symfony/cache/tree/v5.3.10" }, "funding": [ { @@ -1329,7 +1330,7 @@ "type": "tidelift" } ], - "time": "2021-09-26T18:29:18+00:00" + "time": "2021-10-11T15:41:55+00:00" }, { "name": "symfony/cache-contracts", @@ -1412,16 +1413,16 @@ }, { "name": "symfony/config", - "version": "v5.3.4", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4268f3059c904c61636275182707f81645517a37" + "reference": "ac23c2f24d5634966d665d836c3933d54347e5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4268f3059c904c61636275182707f81645517a37", - "reference": "4268f3059c904c61636275182707f81645517a37", + "url": "https://api.github.com/repos/symfony/config/zipball/ac23c2f24d5634966d665d836c3933d54347e5d4", + "reference": "ac23c2f24d5634966d665d836c3933d54347e5d4", "shasum": "" }, "require": { @@ -1471,7 +1472,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.3.4" + "source": "https://github.com/symfony/config/tree/v5.3.10" }, "funding": [ { @@ -1487,20 +1488,20 @@ "type": "tidelift" } ], - "time": "2021-07-21T12:40:44+00:00" + "time": "2021-10-22T09:06:52+00:00" }, { "name": "symfony/console", - "version": "v5.3.7", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a" + "reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8b1008344647462ae6ec57559da166c2bfa5e16a", - "reference": "8b1008344647462ae6ec57559da166c2bfa5e16a", + "url": "https://api.github.com/repos/symfony/console/zipball/d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3", + "reference": "d4e409d9fbcfbf71af0e5a940abb7b0b4bad0bd3", "shasum": "" }, "require": { @@ -1570,7 +1571,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.7" + "source": "https://github.com/symfony/console/tree/v5.3.10" }, "funding": [ { @@ -1586,20 +1587,20 @@ "type": "tidelift" } ], - "time": "2021-08-25T20:02:16+00:00" + "time": "2021-10-26T09:30:15+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.3.8", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "e39c344e06a3ceab531ebeb6c077e6652c4a0829" + "reference": "be833dd336c248ef2bdabf24665351455f52afdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/e39c344e06a3ceab531ebeb6c077e6652c4a0829", - "reference": "e39c344e06a3ceab531ebeb6c077e6652c4a0829", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/be833dd336c248ef2bdabf24665351455f52afdb", + "reference": "be833dd336c248ef2bdabf24665351455f52afdb", "shasum": "" }, "require": { @@ -1658,7 +1659,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.3.8" + "source": "https://github.com/symfony/dependency-injection/tree/v5.3.10" }, "funding": [ { @@ -1674,7 +1675,7 @@ "type": "tidelift" } ], - "time": "2021-09-21T20:52:44+00:00" + "time": "2021-10-22T18:11:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2102,16 +2103,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v5.3.8", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "ea6e30a8c9534d87187375ef4ee39d48ee40dd2d" + "reference": "2ff74f86abf2f67f2d0b53e23ab7a268b105dcfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/ea6e30a8c9534d87187375ef4ee39d48ee40dd2d", - "reference": "ea6e30a8c9534d87187375ef4ee39d48ee40dd2d", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/2ff74f86abf2f67f2d0b53e23ab7a268b105dcfe", + "reference": "2ff74f86abf2f67f2d0b53e23ab7a268b105dcfe", "shasum": "" }, "require": { @@ -2233,7 +2234,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.3.8" + "source": "https://github.com/symfony/framework-bundle/tree/v5.3.10" }, "funding": [ { @@ -2249,7 +2250,7 @@ "type": "tidelift" } ], - "time": "2021-09-28T07:17:01+00:00" + "time": "2021-10-26T11:54:54+00:00" }, { "name": "symfony/http-client-contracts", @@ -2331,16 +2332,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.3.7", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e36c8e5502b4f3f0190c675f1c1f1248a64f04e5" + "reference": "9f34f02e8a5fdc7a56bafe011cea1ce97300e54c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e36c8e5502b4f3f0190c675f1c1f1248a64f04e5", - "reference": "e36c8e5502b4f3f0190c675f1c1f1248a64f04e5", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9f34f02e8a5fdc7a56bafe011cea1ce97300e54c", + "reference": "9f34f02e8a5fdc7a56bafe011cea1ce97300e54c", "shasum": "" }, "require": { @@ -2384,7 +2385,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.3.7" + "source": "https://github.com/symfony/http-foundation/tree/v5.3.10" }, "funding": [ { @@ -2400,20 +2401,20 @@ "type": "tidelift" } ], - "time": "2021-08-27T11:20:35+00:00" + "time": "2021-10-11T15:41:55+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.3.9", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ceaf46a992f60e90645e7279825a830f733a17c5" + "reference": "703e4079920468e9522b72cf47fd76ce8d795e86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ceaf46a992f60e90645e7279825a830f733a17c5", - "reference": "ceaf46a992f60e90645e7279825a830f733a17c5", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/703e4079920468e9522b72cf47fd76ce8d795e86", + "reference": "703e4079920468e9522b72cf47fd76ce8d795e86", "shasum": "" }, "require": { @@ -2496,7 +2497,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.3.9" + "source": "https://github.com/symfony/http-kernel/tree/v5.3.10" }, "funding": [ { @@ -2512,7 +2513,7 @@ "type": "tidelift" } ], - "time": "2021-09-28T10:25:11+00:00" + "time": "2021-10-29T08:36:48+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3250,16 +3251,16 @@ }, { "name": "symfony/string", - "version": "v5.3.7", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5" + "reference": "d70c35bb20bbca71fc4ab7921e3c6bda1a82a60c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/8d224396e28d30f81969f083a58763b8b9ceb0a5", - "reference": "8d224396e28d30f81969f083a58763b8b9ceb0a5", + "url": "https://api.github.com/repos/symfony/string/zipball/d70c35bb20bbca71fc4ab7921e3c6bda1a82a60c", + "reference": "d70c35bb20bbca71fc4ab7921e3c6bda1a82a60c", "shasum": "" }, "require": { @@ -3313,7 +3314,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.7" + "source": "https://github.com/symfony/string/tree/v5.3.10" }, "funding": [ { @@ -3329,20 +3330,20 @@ "type": "tidelift" } ], - "time": "2021-08-26T08:00:08+00:00" + "time": "2021-10-27T18:21:46+00:00" }, { "name": "symfony/var-dumper", - "version": "v5.3.8", + "version": "v5.3.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "eaaea4098be1c90c8285543e1356a09c8aa5c8da" + "reference": "875432adb5f5570fff21036fd22aee244636b7d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/eaaea4098be1c90c8285543e1356a09c8aa5c8da", - "reference": "eaaea4098be1c90c8285543e1356a09c8aa5c8da", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/875432adb5f5570fff21036fd22aee244636b7d1", + "reference": "875432adb5f5570fff21036fd22aee244636b7d1", "shasum": "" }, "require": { @@ -3401,7 +3402,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.3.8" + "source": "https://github.com/symfony/var-dumper/tree/v5.3.10" }, "funding": [ { @@ -3417,7 +3418,7 @@ "type": "tidelift" } ], - "time": "2021-09-24T15:59:58+00:00" + "time": "2021-10-26T09:30:15+00:00" }, { "name": "symfony/var-exporter", @@ -3494,16 +3495,16 @@ }, { "name": "vimeo/psalm", - "version": "4.10.0", + "version": "4.11.2", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa" + "reference": "6fba5eb554f9507b72932f9c75533d8af593688d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/916b098b008f6de4543892b1e0651c1c3b92cbfa", - "reference": "916b098b008f6de4543892b1e0651c1c3b92cbfa", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/6fba5eb554f9507b72932f9c75533d8af593688d", + "reference": "6fba5eb554f9507b72932f9c75533d8af593688d", "shasum": "" }, "require": { @@ -3523,7 +3524,7 @@ "felixfbecker/advanced-json-rpc": "^3.0.3", "felixfbecker/language-server-protocol": "^1.5", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.12", + "nikic/php-parser": "^4.13", "openlss/lib-array2xml": "^1.0", "php": "^7.1|^8", "sebastian/diff": "^3.0 || ^4.0", @@ -3593,9 +3594,9 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.10.0" + "source": "https://github.com/vimeo/psalm/tree/4.11.2" }, - "time": "2021-09-04T21:00:09+00:00" + "time": "2021-10-26T17:28:17+00:00" }, { "name": "webmozart/assert", From e01d0e63cf5b5edba7fd690c55e7068e90097e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ausw=C3=B6ger?= Date: Thu, 4 Nov 2021 08:35:14 +0100 Subject: [PATCH 12/12] Add a template element and module (see #3625) Description ----------- | Q | A | -----------------| --- | Fixed issues | Fixes https://github.com/contao/core-bundle/pull/1315#issuecomment-358484679 | Docs PR or issue | contao/docs#866 Commits ------- f094021f Add template element 97368c9f Coding style 1fec68a5 Add template module b9ecfea6 Fine tuning --- .../ContentElement/TemplateController.php | 31 +++++ .../FrontendModule/TemplateController.php | 31 +++++ core-bundle/src/Resources/config/services.yml | 8 ++ .../src/Resources/contao/dca/tl_content.php | 7 ++ .../src/Resources/contao/dca/tl_module.php | 7 ++ .../Resources/contao/languages/en/default.xlf | 6 + .../Resources/contao/languages/en/modules.xlf | 6 + .../contao/languages/en/tl_content.xlf | 6 + .../contao/languages/en/tl_module.xlf | 6 + .../templates/elements/ce_template.html5 | 12 ++ .../templates/modules/mod_template.html5 | 12 ++ .../contao/themes/flexible/basic.css | 2 +- .../contao/themes/flexible/basic.min.css | 2 +- .../ContentElement/TemplateControllerTest.php | 116 ++++++++++++++++++ 14 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 core-bundle/src/Controller/ContentElement/TemplateController.php create mode 100644 core-bundle/src/Controller/FrontendModule/TemplateController.php create mode 100644 core-bundle/src/Resources/contao/templates/elements/ce_template.html5 create mode 100644 core-bundle/src/Resources/contao/templates/modules/mod_template.html5 create mode 100644 core-bundle/tests/Controller/ContentElement/TemplateControllerTest.php diff --git a/core-bundle/src/Controller/ContentElement/TemplateController.php b/core-bundle/src/Controller/ContentElement/TemplateController.php new file mode 100644 index 00000000000..ed598232045 --- /dev/null +++ b/core-bundle/src/Controller/ContentElement/TemplateController.php @@ -0,0 +1,31 @@ +initializeContaoFramework(); + + $template->data = StringUtil::deserialize($model->data, true); + + return $template->getResponse(); + } +} diff --git a/core-bundle/src/Controller/FrontendModule/TemplateController.php b/core-bundle/src/Controller/FrontendModule/TemplateController.php new file mode 100644 index 00000000000..869f8e61598 --- /dev/null +++ b/core-bundle/src/Controller/FrontendModule/TemplateController.php @@ -0,0 +1,31 @@ +initializeContaoFramework(); + + $template->data = StringUtil::deserialize($model->data, true); + + return $template->getResponse(); + } +} diff --git a/core-bundle/src/Resources/config/services.yml b/core-bundle/src/Resources/config/services.yml index 925d8dad5e6..b86160b346a 100644 --- a/core-bundle/src/Resources/config/services.yml +++ b/core-bundle/src/Resources/config/services.yml @@ -111,6 +111,10 @@ services: tags: - { name: contao.content_element, category: texts } + Contao\CoreBundle\Controller\ContentElement\TemplateController: + tags: + - { name: contao.content_element, category: includes } + Contao\CoreBundle\Controller\FaviconController: arguments: - '@contao.framework' @@ -123,6 +127,10 @@ services: tags: - controller.service_arguments + Contao\CoreBundle\Controller\FrontendModule\TemplateController: + tags: + - { name: contao.frontend_module, category: miscellaneous } + Contao\CoreBundle\Controller\FrontendModule\TwoFactorController: tags: - { name: contao.frontend_module, category: user } diff --git a/core-bundle/src/Resources/contao/dca/tl_content.php b/core-bundle/src/Resources/contao/dca/tl_content.php index 95eaea1c344..d52a14c2a39 100644 --- a/core-bundle/src/Resources/contao/dca/tl_content.php +++ b/core-bundle/src/Resources/contao/dca/tl_content.php @@ -131,6 +131,7 @@ 'sliderStop' => '{type_legend},type;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests;{invisible_legend:hide},invisible,start,stop', 'code' => '{type_legend},type,headline;{text_legend},highlight,code;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', 'markdown' => '{type_legend},type,headline;{text_legend},markdownSource;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', + 'template' => '{type_legend},type,headline;{template_legend},data,customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', 'hyperlink' => '{type_legend},type,headline;{link_legend},url,target,linkTitle,embed,titleText,rel;{imglink_legend:hide},useImage;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', 'toplink' => '{type_legend},type;{link_legend},linkTitle;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', 'image' => '{type_legend},type,headline;{source_legend},singleSRC,size,imagemargin,fullsize,overwriteMeta;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID;{invisible_legend:hide},invisible,start,stop', @@ -756,6 +757,12 @@ 'eval' => array('tl_class'=>'w50 m12'), 'sql' => "char(1) NOT NULL default ''" ), + 'data' => array + ( + 'exclude' => true, + 'inputType' => 'keyValueWizard', + 'sql' => "text NULL" + ), 'cteAlias' => array ( 'exclude' => true, diff --git a/core-bundle/src/Resources/contao/dca/tl_module.php b/core-bundle/src/Resources/contao/dca/tl_module.php index 42afaf31d92..27f030dd0ae 100644 --- a/core-bundle/src/Resources/contao/dca/tl_module.php +++ b/core-bundle/src/Resources/contao/dca/tl_module.php @@ -121,6 +121,7 @@ 'articlelist' => '{title_legend},name,headline,type;{config_legend},skipFirst,inColumn;{reference_legend:hide},defineRoot;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID', 'randomImage' => '{title_legend},name,headline,type;{source_legend},multiSRC,imgSize,fullsize,useCaption;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID', 'html' => '{title_legend},name,type;{html_legend},html;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests', + 'template' => '{title_legend},name,headline,type;{template_legend},data,customTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID', 'rssReader' => '{title_legend},name,headline,type;{config_legend},rss_feed,numberOfItems,perPage,skipFirst,rss_cache;{template_legend:hide},rss_template;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID', 'two_factor' => '{title_legend},name,headline,type;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID', ), @@ -596,6 +597,12 @@ ), 'sql' => "text NULL" ), + 'data' => array + ( + 'exclude' => true, + 'inputType' => 'keyValueWizard', + 'sql' => "text NULL" + ), 'protected' => array ( 'exclude' => true, diff --git a/core-bundle/src/Resources/contao/languages/en/default.xlf b/core-bundle/src/Resources/contao/languages/en/default.xlf index 69508ba4cbc..22623ffb948 100644 --- a/core-bundle/src/Resources/contao/languages/en/default.xlf +++ b/core-bundle/src/Resources/contao/languages/en/default.xlf @@ -338,6 +338,12 @@ Generates HTML code from a markdown text. + + Custom template + + + Generates an element to insert a custom template. + Content slider diff --git a/core-bundle/src/Resources/contao/languages/en/modules.xlf b/core-bundle/src/Resources/contao/languages/en/modules.xlf index 131e15382db..b4e559f2ec9 100644 --- a/core-bundle/src/Resources/contao/languages/en/modules.xlf +++ b/core-bundle/src/Resources/contao/languages/en/modules.xlf @@ -290,6 +290,12 @@ Allows you to add custom HTML code + + Custom template + + + Generates a module to insert a custom template. + RSS reader diff --git a/core-bundle/src/Resources/contao/languages/en/tl_content.xlf b/core-bundle/src/Resources/contao/languages/en/tl_content.xlf index 2c2326a64ab..3ed379897a6 100644 --- a/core-bundle/src/Resources/contao/languages/en/tl_content.xlf +++ b/core-bundle/src/Resources/contao/languages/en/tl_content.xlf @@ -230,6 +230,12 @@ File + + Template data + + + These values are available as <code>$this->data</code> in the template. + Link text diff --git a/core-bundle/src/Resources/contao/languages/en/tl_module.xlf b/core-bundle/src/Resources/contao/languages/en/tl_module.xlf index a6923cba910..3194233ba62 100644 --- a/core-bundle/src/Resources/contao/languages/en/tl_module.xlf +++ b/core-bundle/src/Resources/contao/languages/en/tl_module.xlf @@ -374,6 +374,12 @@ You can use the wildcards <em>##domain##</em> (domain name), <em>##link##</em> (activation link) and any member property (e.g. <em>##lastname##</em>). + + Template data + + + These values are available as <code>$this->data</code> in the template. + Title and type diff --git a/core-bundle/src/Resources/contao/templates/elements/ce_template.html5 b/core-bundle/src/Resources/contao/templates/elements/ce_template.html5 new file mode 100644 index 00000000000..39231712fa3 --- /dev/null +++ b/core-bundle/src/Resources/contao/templates/elements/ce_template.html5 @@ -0,0 +1,12 @@ +extend('block_searchable'); ?> + +block('content'); ?> + + data as $entry): ?> +
    +
    +
    +
    + + +endblock(); ?> diff --git a/core-bundle/src/Resources/contao/templates/modules/mod_template.html5 b/core-bundle/src/Resources/contao/templates/modules/mod_template.html5 new file mode 100644 index 00000000000..4664a39a36c --- /dev/null +++ b/core-bundle/src/Resources/contao/templates/modules/mod_template.html5 @@ -0,0 +1,12 @@ +extend('block_unsearchable'); ?> + +block('content'); ?> + + data as $entry): ?> +
    +
    +
    +
    + + +endblock(); ?> diff --git a/core-bundle/src/Resources/contao/themes/flexible/basic.css b/core-bundle/src/Resources/contao/themes/flexible/basic.css index 63a9adc6063..6334b99ba98 100644 --- a/core-bundle/src/Resources/contao/themes/flexible/basic.css +++ b/core-bundle/src/Resources/contao/themes/flexible/basic.css @@ -183,7 +183,7 @@ p { font-size:.875rem; } .widget pre,.widget code { - font-size:.75rem; + font-size:.7rem; } .widget h3 { min-height:16px; diff --git a/core-bundle/src/Resources/contao/themes/flexible/basic.min.css b/core-bundle/src/Resources/contao/themes/flexible/basic.min.css index e8d74881358..46dcfe8092a 100644 --- a/core-bundle/src/Resources/contao/themes/flexible/basic.min.css +++ b/core-bundle/src/Resources/contao/themes/flexible/basic.min.css @@ -1 +1 @@ -html{font-size:100%;-webkit-text-size-adjust:100%}header,footer,nav,section,aside,main,article,figure,figcaption{display:block}body,button,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,figure,form,fieldset,legend,p,blockquote,table{margin:0;padding:0}img{border:0}ul,li{list-style:none}table{border-spacing:0;border-collapse:collapse;empty-cells:show}th,td{padding:0;text-align:left}input,select,button,label,img,a.tl_submit,.tl_select{vertical-align:middle}button{cursor:pointer}body{font-family:-apple-system,system-ui,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:400;font-size:.875rem;line-height:1;color:#444}h1,h2,h3,h4,h5,h6,strong,b,th{color:#222;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body{color:#222;font-weight:300}h1,h2,h3,h4,h5,h6,strong,b,th{font-weight:500}}pre,code,.tl_textarea.monospace{font:300 .75rem/1.25 SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}h1,h2,h3,h4,h5,h6{font-size:1rem}input,textarea,select,button{font:inherit;color:inherit;line-height:inherit}input,select{line-height:17px}@supports (display:-ms-grid){input,select{line-height:1.1}}.tl_gray{color:#999}.tl_green{color:#589b0e}.tl_red{color:#c33}.tl_blue{color:#006494}.tl_orange{color:#f90}span.mandatory{color:#c33}.upper{text-transform:uppercase}a{color:#444;text-decoration:none}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){a{color:#222}}a:hover,a:active{color:#006494}hr{height:1px;margin:18px 0;border:0;background:#ddd;color:#ddd}p{margin-bottom:1em;padding:0}.hidden{display:none}.unselectable{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.clear{clear:both;height:.1px;line-height:.1px;font-size:.1px}.cf:before,.cf:after{content:" ";display:table}.cf:after{clear:both}.invisible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.widget{margin-left:15px;margin-right:15px;position:relative}.widget{font-size:0}.widget *{font-size:.875rem}.widget>div{font-size:0}.widget>div>*{font-size:.875rem}.widget pre,.widget code{font-size:.75rem}.widget h3{min-height:16px}.widget h3 label,.widget h3 img{margin-right:3px}.widget legend img{vertical-align:-3px}.widget-captcha{display:initial!important}.widget .mce-tinymce{margin:3px 0}optgroup{padding-top:3px;padding-bottom:3px;font-style:normal;background:#fff}fieldset.tl_checkbox_container,fieldset.tl_radio_container{border:0;margin:15px 0 1px;padding:0}fieldset.tl_checkbox_container{line-height:15px}fieldset.tl_radio_container{line-height:16px}fieldset.tl_checkbox_container legend,fieldset.tl_radio_container legend{font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){fieldset.tl_checkbox_container legend,fieldset.tl_radio_container legend{font-weight:500}}fieldset.checkbox_container,fieldset.radio_container{border:0;margin:0;padding:0}.tl_text{width:100%}.tl_text_2,.tl_text_interval{width:49%}.tl_text_3{width:32.333%}.tl_text_4{width:24%}.tl_textarea{width:100%}.tl_text_unit{width:79%}.tl_text_trbl{width:19%}.tl_text,.tl_text_2,.tl_text_3,.tl_text_4,.tl_textarea,.tl_text_unit,.tl_text_trbl,.tl_text_interval{height:30px;margin:3px 0;box-sizing:border-box;padding:5px 6px 6px;border:1px solid #aaa;border-radius:2px;background-color:#fff;-moz-appearance:none;-webkit-appearance:none}.tl_text[disabled],.tl_text_2[disabled],.tl_text_3[disabled],.tl_text_4[disabled],.tl_textarea[disabled],.tl_text_unit[disabled],.tl_text_trbl[disabled],.tl_text_interval[disabled],.tl_text[readonly],.tl_text_2[readonly],.tl_text_3[readonly],.tl_text_4[readonly],.tl_textarea[readonly],.tl_text_unit[readonly],.tl_text_trbl[readonly],.tl_text_interval[readonly]{color:#bbb;background-color:#f9f9f9;border:1px solid #c8c8c8;cursor:not-allowed}.tl_textarea{height:240px;padding:4px 6px;line-height:1.45}.tl_text_2,.tl_text_3,.tl_text_4,.tl_text_unit,.tl_text_trbl,.tl_text_interval{margin-right:1%}.tl_text_2:last-child,.tl_text_3:last-child,.tl_text_4:last-child,.tl_text_trbl:last-child{margin-right:0}.tl_text_field .tl_text_2{width:49.5%}.tl_imageSize_0{margin-left:1%}input[type=search]{height:27px;padding-top:0;padding-bottom:1px}input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;height:14px;width:14px;margin:0;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzc3NyI+PHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNSAxMiAxMC41OSA2LjQxIDUgNSA2LjQxIDEwLjU5IDEyIDUgMTcuNTkgNi40MSAxOSAxMiAxMy40MSAxNy41OSAxOSAxOSAxNy41OSAxMy40MSAxMnoiLz48L3N2Zz4=)}@-moz-document url-prefix(){.tl_text::placeholder,.tl_text_2::placeholder,.tl_text_3::placeholder,.tl_text_4::placeholder,.tl_textarea::placeholder,.tl_text_unit::placeholder,.tl_text_trbl::placeholder,.tl_text_interval::placeholder{line-height:18px}}@media not all and (min-resolution:.001dpcm){@supports (-webkit-appearance:none){.tl_text::placeholder,.tl_text_2::placeholder,.tl_text_3::placeholder,.tl_text_4::placeholder,.tl_textarea::placeholder,.tl_text_unit::placeholder,.tl_text_trbl::placeholder,.tl_text_interval::placeholder{line-height:16px}input[type=search]{padding-right:0}input[type=search]:focus{outline-offset:-2px}input[type=search]::-webkit-search-cancel-button{margin:7px 4px 0 0}}}@supports (display:-ms-grid){.tl_text,.tl_text_2,.tl_text_3,.tl_text_4,.tl_textarea,.tl_text_unit,.tl_text_trbl,.tl_text_interval{padding:4px 6px 5px}}select{text-transform:none;-moz-appearance:none;-webkit-appearance:none}select::-ms-expand{display:none}select[multiple]{height:auto}.tl_select,.tl_mselect,.tl_select_column{width:100%}.tl_select_unit{width:20%}.tl_select_interval{width:50%}.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{height:30px;margin:3px 0;box-sizing:border-box;border:1px solid #aaa;padding:5px 22px 6px 6px;border-radius:2px;background:#fff url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDUwMCA1MDAiPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxMzEuNTIzIiB5MT0iNDIuNjMiIHgyPSIzNjguNDc4IiB5Mj0iMjc5LjU4NCI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjNiM2IzIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOTk5Ii8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBmaWxsPSJ1cmwoI2EpIiBkPSJNMjUwIDM5Ni42NjZjLTEuMTU1IDAtNC4xMS0xLjgzMi03LjExMy02Ljc1bC0xNjkuNi0yNzcuNDU1Yy0yLjUxNy00LjExNC0zLjE5LTYuOTgtMy4yOC04LjMxNC44MjctLjMzIDIuNTY1LS44MTIgNS42MjctLjgxMmgzNDguNzMzYzMuMDYzIDAgNC43OTguNDgyIDUuNjI3LjgxMi0uMDkgMS4zMzQtLjc2NiA0LjItMy4yOCA4LjMxNWwtMTY5LjYgMjc3LjQ1N2MtMy4wMDUgNC45MTctNS45NiA2Ljc1LTcuMTE0IDYuNzV6Ii8+PC9zdmc+) right -16px top 3px no-repeat;background-origin:content-box;cursor:pointer}.tl_select[disabled],.tl_mselect[disabled],.tl_select_column[disabled],.tl_select_unit[disabled],.tl_select_interval[disabled],.tl_select[readonly],.tl_mselect[readonly],.tl_select_column[readonly],.tl_select_unit[readonly],.tl_select_interval[readonly]{color:#bbb;background-color:#f9f9f9;border:1px solid #c8c8c8;cursor:not-allowed}.tl_select[multiple],.tl_mselect[multiple],.tl_select_column[multiple],.tl_select_unit[multiple],.tl_select_interval[multiple]{background-image:none}@-moz-document url-prefix(){.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{padding-left:2px}}@supports (display:-ms-grid){.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{padding:5px 18px 5px 2px}}.tl_checkbox{margin:0 1px 0 0}.tl_tree_checkbox{margin:1px 1px 1px 0}.tl_checkbox_single_container{height:16px;margin:14px 0 1px}.tl_checkbox_single_container label{margin-left:4px;margin-right:3px;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.tl_checkbox_single_container label{font-weight:500}}.checkbox_toggler{font-weight:600}.checkbox_toggler_first{margin-top:3px;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.checkbox_toggler,.checkbox_toggler_first{font-weight:500}}.checkbox_toggler img,.checkbox_toggler_first img{position:relative;top:-1px;margin-right:2px}.checkbox_options{margin:0 0 6px 21px!important}.tl_checkbox_container .checkbox_options:last-of-type{margin-bottom:0!important}.tl_radio{margin:0 1px 0 0}.win .tl_radio{margin:1px 1px 3px 0}.tl_tree_radio{margin:1px 1px 1px 0}.tl_radio_table{margin-top:3px}.tl_radio_table td{padding:0 24px 0 0}.tl_upload_field{margin:1px 0}.tl_submit{height:30px;padding:7px 12px;border:1px solid #aaa;border-radius:2px;box-sizing:border-box;cursor:pointer;background:#eee;transition:background .2s ease}.tl_panel .tl_submit,.tl_version_panel .tl_submit,.tl_formbody_submit .tl_submit{background:#fff}.tl_submit:hover{color:inherit;background-color:#f6f6f6}.tl_submit:active{color:#aaa}.tl_submit:disabled{cursor:default;background:#e9e9e9;color:#999}a.tl_submit{display:inline-block}.split-button{display:inline-block;position:relative;z-index:1}::-moz-placeholder{padding-top:1px}::-webkit-input-placeholder{padding-top:1px}.clr{clear:both;width:calc(100% - 30px)}.clr:not(.widget){width:100%}.clr:before{content:"";display:table}.w50{width:calc(50% - 30px);float:left;min-height:80px}.long{width:calc(100% - 30px)}.wizard>a{position:relative;top:-2px;vertical-align:middle}.wizard .tl_text,.wizard .tl_select{width:calc(100% - 24px)}.wizard .tl_text_2{width:45%}.wizard img{margin-left:4px}.long .tl_text,.long .tl_select{width:100%}.m12{margin:0 15px;padding:18px 0 16px}.cbx{min-height:46px}.cbx.m12{min-height:80px;box-sizing:border-box}.subpal{clear:both}.inline div{display:inline}.autoheight{height:auto}.dcapicker .tl_text{padding-right:26px}.tl_tip{height:15px;overflow:hidden;cursor:help}.tip-wrap{z-index:99}.tip{max-width:320px;padding:6px 9px;border-radius:2px;background:#333}.tip div{line-height:1.3}.tip div,.tip a{color:#fff}.tip-top{height:6px;position:absolute;top:-13px;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #333}.hover-div:hover,.hover-row:hover td,.hover-div:hover .limit_toggler,.hover-row:hover .limit_toggler{background-color:#fffce1!important}.badge-title{float:right;margin-left:8px;margin-top:-8px;border-radius:8px;padding:2px 5px;background:#0f1c26;color:#fff;font-size:.75rem;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.badge-title{font-weight:500}}@media (min-width:1280px){#sbtog{display:none}.split-button ul{display:inline-flex;clip:initial;height:auto;margin:0 0 0 -4px;overflow:initial;position:initial;width:auto}.split-button li{margin-left:4px}}@media (max-width:1279px){.split-button ul{position:absolute;right:0;bottom:20px;min-width:100%;background:#fff;border:1px solid #aaa;border-radius:2px;box-sizing:border-box;padding:3px 0;margin-bottom:1em}.split-button ul button{border:0;width:100%;text-align:left;white-space:nowrap}.split-button ul .tl_submit{margin-top:0;margin-bottom:0;background:#fff}.split-button ul .tl_submit:hover{background:#f3f3f3}.split-button ul:before{content:"";display:block;height:0;width:0;position:absolute;right:4px;bottom:-12px;z-index:89;border:6px inset transparent;border-top:6px solid #fff}.split-button ul:after{content:"";display:block;height:0;width:0;position:absolute;right:3px;bottom:-14px;z-index:88;border:7px inset transparent;border-top:7px solid #aaa}.split-button button img{vertical-align:0}.split-button>button[type=submit]{position:relative;border-radius:2px 0 0 2px}.split-button>button[type=button]{padding:7px 4px;background:#fff;border:1px solid #aaa;border-left:0;border-radius:0 2px 2px 0;box-sizing:border-box;transition:background .2s ease}.split-button>button[type=button].active,.split-button>button[type=button]:hover{background:#f6f6f6}.split-button>button[type=button]:focus{outline:0}}@media (max-width:767px){.w50{float:none;width:calc(100% - 30px)}.m12{padding-top:0;padding-bottom:0}.cbx,.cbx.m12{min-height:auto}.tip{max-width:80vw}.tl_checkbox_container .tl_checkbox{margin-top:1px;margin-bottom:1px}} \ No newline at end of file +html{font-size:100%;-webkit-text-size-adjust:100%}header,footer,nav,section,aside,main,article,figure,figcaption{display:block}body,button,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,figure,form,fieldset,legend,p,blockquote,table{margin:0;padding:0}img{border:0}ul,li{list-style:none}table{border-spacing:0;border-collapse:collapse;empty-cells:show}th,td{padding:0;text-align:left}input,select,button,label,img,a.tl_submit,.tl_select{vertical-align:middle}button{cursor:pointer}body{font-family:-apple-system,system-ui,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:400;font-size:.875rem;line-height:1;color:#444}h1,h2,h3,h4,h5,h6,strong,b,th{color:#222;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){body{color:#222;font-weight:300}h1,h2,h3,h4,h5,h6,strong,b,th{font-weight:500}}pre,code,.tl_textarea.monospace{font:300 .75rem/1.25 SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}h1,h2,h3,h4,h5,h6{font-size:1rem}input,textarea,select,button{font:inherit;color:inherit;line-height:inherit}input,select{line-height:17px}@supports (display:-ms-grid){input,select{line-height:1.1}}.tl_gray{color:#999}.tl_green{color:#589b0e}.tl_red{color:#c33}.tl_blue{color:#006494}.tl_orange{color:#f90}span.mandatory{color:#c33}.upper{text-transform:uppercase}a{color:#444;text-decoration:none}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){a{color:#222}}a:hover,a:active{color:#006494}hr{height:1px;margin:18px 0;border:0;background:#ddd;color:#ddd}p{margin-bottom:1em;padding:0}.hidden{display:none}.unselectable{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none}.clear{clear:both;height:.1px;line-height:.1px;font-size:.1px}.cf:before,.cf:after{content:" ";display:table}.cf:after{clear:both}.invisible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.widget{margin-left:15px;margin-right:15px;position:relative}.widget{font-size:0}.widget *{font-size:.875rem}.widget>div{font-size:0}.widget>div>*{font-size:.875rem}.widget pre,.widget code{font-size:.7rem}.widget h3{min-height:16px}.widget h3 label,.widget h3 img{margin-right:3px}.widget legend img{vertical-align:-3px}.widget-captcha{display:initial!important}.widget .mce-tinymce{margin:3px 0}optgroup{padding-top:3px;padding-bottom:3px;font-style:normal;background:#fff}fieldset.tl_checkbox_container,fieldset.tl_radio_container{border:0;margin:15px 0 1px;padding:0}fieldset.tl_checkbox_container{line-height:15px}fieldset.tl_radio_container{line-height:16px}fieldset.tl_checkbox_container legend,fieldset.tl_radio_container legend{font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){fieldset.tl_checkbox_container legend,fieldset.tl_radio_container legend{font-weight:500}}fieldset.checkbox_container,fieldset.radio_container{border:0;margin:0;padding:0}.tl_text{width:100%}.tl_text_2,.tl_text_interval{width:49%}.tl_text_3{width:32.333%}.tl_text_4{width:24%}.tl_textarea{width:100%}.tl_text_unit{width:79%}.tl_text_trbl{width:19%}.tl_text,.tl_text_2,.tl_text_3,.tl_text_4,.tl_textarea,.tl_text_unit,.tl_text_trbl,.tl_text_interval{height:30px;margin:3px 0;box-sizing:border-box;padding:5px 6px 6px;border:1px solid #aaa;border-radius:2px;background-color:#fff;-moz-appearance:none;-webkit-appearance:none}.tl_text[disabled],.tl_text_2[disabled],.tl_text_3[disabled],.tl_text_4[disabled],.tl_textarea[disabled],.tl_text_unit[disabled],.tl_text_trbl[disabled],.tl_text_interval[disabled],.tl_text[readonly],.tl_text_2[readonly],.tl_text_3[readonly],.tl_text_4[readonly],.tl_textarea[readonly],.tl_text_unit[readonly],.tl_text_trbl[readonly],.tl_text_interval[readonly]{color:#bbb;background-color:#f9f9f9;border:1px solid #c8c8c8;cursor:not-allowed}.tl_textarea{height:240px;padding:4px 6px;line-height:1.45}.tl_text_2,.tl_text_3,.tl_text_4,.tl_text_unit,.tl_text_trbl,.tl_text_interval{margin-right:1%}.tl_text_2:last-child,.tl_text_3:last-child,.tl_text_4:last-child,.tl_text_trbl:last-child{margin-right:0}.tl_text_field .tl_text_2{width:49.5%}.tl_imageSize_0{margin-left:1%}input[type=search]{height:27px;padding-top:0;padding-bottom:1px}input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;height:14px;width:14px;margin:0;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzc3NyI+PHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNSAxMiAxMC41OSA2LjQxIDUgNSA2LjQxIDEwLjU5IDEyIDUgMTcuNTkgNi40MSAxOSAxMiAxMy40MSAxNy41OSAxOSAxOSAxNy41OSAxMy40MSAxMnoiLz48L3N2Zz4=)}@-moz-document url-prefix(){.tl_text::placeholder,.tl_text_2::placeholder,.tl_text_3::placeholder,.tl_text_4::placeholder,.tl_textarea::placeholder,.tl_text_unit::placeholder,.tl_text_trbl::placeholder,.tl_text_interval::placeholder{line-height:18px}}@media not all and (min-resolution:.001dpcm){@supports (-webkit-appearance:none){.tl_text::placeholder,.tl_text_2::placeholder,.tl_text_3::placeholder,.tl_text_4::placeholder,.tl_textarea::placeholder,.tl_text_unit::placeholder,.tl_text_trbl::placeholder,.tl_text_interval::placeholder{line-height:16px}input[type=search]{padding-right:0}input[type=search]:focus{outline-offset:-2px}input[type=search]::-webkit-search-cancel-button{margin:7px 4px 0 0}}}@supports (display:-ms-grid){.tl_text,.tl_text_2,.tl_text_3,.tl_text_4,.tl_textarea,.tl_text_unit,.tl_text_trbl,.tl_text_interval{padding:4px 6px 5px}}select{text-transform:none;-moz-appearance:none;-webkit-appearance:none}select::-ms-expand{display:none}select[multiple]{height:auto}.tl_select,.tl_mselect,.tl_select_column{width:100%}.tl_select_unit{width:20%}.tl_select_interval{width:50%}.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{height:30px;margin:3px 0;box-sizing:border-box;border:1px solid #aaa;padding:5px 22px 6px 6px;border-radius:2px;background:#fff url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDUwMCA1MDAiPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxMzEuNTIzIiB5MT0iNDIuNjMiIHgyPSIzNjguNDc4IiB5Mj0iMjc5LjU4NCI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjYjNiM2IzIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOTk5Ii8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBmaWxsPSJ1cmwoI2EpIiBkPSJNMjUwIDM5Ni42NjZjLTEuMTU1IDAtNC4xMS0xLjgzMi03LjExMy02Ljc1bC0xNjkuNi0yNzcuNDU1Yy0yLjUxNy00LjExNC0zLjE5LTYuOTgtMy4yOC04LjMxNC44MjctLjMzIDIuNTY1LS44MTIgNS42MjctLjgxMmgzNDguNzMzYzMuMDYzIDAgNC43OTguNDgyIDUuNjI3LjgxMi0uMDkgMS4zMzQtLjc2NiA0LjItMy4yOCA4LjMxNWwtMTY5LjYgMjc3LjQ1N2MtMy4wMDUgNC45MTctNS45NiA2Ljc1LTcuMTE0IDYuNzV6Ii8+PC9zdmc+) right -16px top 3px no-repeat;background-origin:content-box;cursor:pointer}.tl_select[disabled],.tl_mselect[disabled],.tl_select_column[disabled],.tl_select_unit[disabled],.tl_select_interval[disabled],.tl_select[readonly],.tl_mselect[readonly],.tl_select_column[readonly],.tl_select_unit[readonly],.tl_select_interval[readonly]{color:#bbb;background-color:#f9f9f9;border:1px solid #c8c8c8;cursor:not-allowed}.tl_select[multiple],.tl_mselect[multiple],.tl_select_column[multiple],.tl_select_unit[multiple],.tl_select_interval[multiple]{background-image:none}@-moz-document url-prefix(){.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{padding-left:2px}}@supports (display:-ms-grid){.tl_select,.tl_mselect,.tl_select_column,.tl_select_unit,.tl_select_interval{padding:5px 18px 5px 2px}}.tl_checkbox{margin:0 1px 0 0}.tl_tree_checkbox{margin:1px 1px 1px 0}.tl_checkbox_single_container{height:16px;margin:14px 0 1px}.tl_checkbox_single_container label{margin-left:4px;margin-right:3px;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.tl_checkbox_single_container label{font-weight:500}}.checkbox_toggler{font-weight:600}.checkbox_toggler_first{margin-top:3px;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.checkbox_toggler,.checkbox_toggler_first{font-weight:500}}.checkbox_toggler img,.checkbox_toggler_first img{position:relative;top:-1px;margin-right:2px}.checkbox_options{margin:0 0 6px 21px!important}.tl_checkbox_container .checkbox_options:last-of-type{margin-bottom:0!important}.tl_radio{margin:0 1px 0 0}.win .tl_radio{margin:1px 1px 3px 0}.tl_tree_radio{margin:1px 1px 1px 0}.tl_radio_table{margin-top:3px}.tl_radio_table td{padding:0 24px 0 0}.tl_upload_field{margin:1px 0}.tl_submit{height:30px;padding:7px 12px;border:1px solid #aaa;border-radius:2px;box-sizing:border-box;cursor:pointer;background:#eee;transition:background .2s ease}.tl_panel .tl_submit,.tl_version_panel .tl_submit,.tl_formbody_submit .tl_submit{background:#fff}.tl_submit:hover{color:inherit;background-color:#f6f6f6}.tl_submit:active{color:#aaa}.tl_submit:disabled{cursor:default;background:#e9e9e9;color:#999}a.tl_submit{display:inline-block}.split-button{display:inline-block;position:relative;z-index:1}::-moz-placeholder{padding-top:1px}::-webkit-input-placeholder{padding-top:1px}.clr{clear:both;width:calc(100% - 30px)}.clr:not(.widget){width:100%}.clr:before{content:"";display:table}.w50{width:calc(50% - 30px);float:left;min-height:80px}.long{width:calc(100% - 30px)}.wizard>a{position:relative;top:-2px;vertical-align:middle}.wizard .tl_text,.wizard .tl_select{width:calc(100% - 24px)}.wizard .tl_text_2{width:45%}.wizard img{margin-left:4px}.long .tl_text,.long .tl_select{width:100%}.m12{margin:0 15px;padding:18px 0 16px}.cbx{min-height:46px}.cbx.m12{min-height:80px;box-sizing:border-box}.subpal{clear:both}.inline div{display:inline}.autoheight{height:auto}.dcapicker .tl_text{padding-right:26px}.tl_tip{height:15px;overflow:hidden;cursor:help}.tip-wrap{z-index:99}.tip{max-width:320px;padding:6px 9px;border-radius:2px;background:#333}.tip div{line-height:1.3}.tip div,.tip a{color:#fff}.tip-top{height:6px;position:absolute;top:-13px;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #333}.hover-div:hover,.hover-row:hover td,.hover-div:hover .limit_toggler,.hover-row:hover .limit_toggler{background-color:#fffce1!important}.badge-title{float:right;margin-left:8px;margin-top:-8px;border-radius:8px;padding:2px 5px;background:#0f1c26;color:#fff;font-size:.75rem;font-weight:600}@media (-webkit-min-device-pixel-ratio:2),(min-resolution:192dpi){.badge-title{font-weight:500}}@media (min-width:1280px){#sbtog{display:none}.split-button ul{display:inline-flex;clip:initial;height:auto;margin:0 0 0 -4px;overflow:initial;position:initial;width:auto}.split-button li{margin-left:4px}}@media (max-width:1279px){.split-button ul{position:absolute;right:0;bottom:20px;min-width:100%;background:#fff;border:1px solid #aaa;border-radius:2px;box-sizing:border-box;padding:3px 0;margin-bottom:1em}.split-button ul button{border:0;width:100%;text-align:left;white-space:nowrap}.split-button ul .tl_submit{margin-top:0;margin-bottom:0;background:#fff}.split-button ul .tl_submit:hover{background:#f3f3f3}.split-button ul:before{content:"";display:block;height:0;width:0;position:absolute;right:4px;bottom:-12px;z-index:89;border:6px inset transparent;border-top:6px solid #fff}.split-button ul:after{content:"";display:block;height:0;width:0;position:absolute;right:3px;bottom:-14px;z-index:88;border:7px inset transparent;border-top:7px solid #aaa}.split-button button img{vertical-align:0}.split-button>button[type=submit]{position:relative;border-radius:2px 0 0 2px}.split-button>button[type=button]{padding:7px 4px;background:#fff;border:1px solid #aaa;border-left:0;border-radius:0 2px 2px 0;box-sizing:border-box;transition:background .2s ease}.split-button>button[type=button].active,.split-button>button[type=button]:hover{background:#f6f6f6}.split-button>button[type=button]:focus{outline:0}}@media (max-width:767px){.w50{float:none;width:calc(100% - 30px)}.m12{padding-top:0;padding-bottom:0}.cbx,.cbx.m12{min-height:auto}.tip{max-width:80vw}.tl_checkbox_container .tl_checkbox{margin-top:1px;margin-bottom:1px}} \ No newline at end of file diff --git a/core-bundle/tests/Controller/ContentElement/TemplateControllerTest.php b/core-bundle/tests/Controller/ContentElement/TemplateControllerTest.php new file mode 100644 index 00000000000..a23c67cbcf2 --- /dev/null +++ b/core-bundle/tests/Controller/ContentElement/TemplateControllerTest.php @@ -0,0 +1,116 @@ + 'Key 1', 'value' => 'Value 1'], + ['key' => 'Key 1', 'value' => 'Value 1'], + ]; + + $container = $this->mockContainer($data, 'ce_template'); + + /** @var ContentModel&MockObject $contentModel */ + $contentModel = $this->mockClassWithProperties(ContentModel::class); + $contentModel->data = serialize($data); + + $controller = new TemplateController(); + $controller->setContainer($container); + + $controller(new Request(), $contentModel, 'main'); + } + + public function testWithoutDataInput(): void + { + $container = $this->mockContainer([], 'ce_template'); + + /** @var ContentModel&MockObject $contentModel */ + $contentModel = $this->mockClassWithProperties(ContentModel::class); + $contentModel->data = null; + + $controller = new TemplateController(); + $controller->setContainer($container); + + $controller(new Request(), $contentModel, 'main'); + } + + public function testWithCustomTemplate(): void + { + $data = [ + ['key' => 'Key 1', 'value' => 'Value 1'], + ['key' => 'Key 1', 'value' => 'Value 1'], + ]; + + $container = $this->mockContainer($data, 'ce_template_custom1'); + + /** @var ContentModel&MockObject $contentModel */ + $contentModel = $this->mockClassWithProperties(ContentModel::class); + $contentModel->data = serialize($data); + $contentModel->customTpl = 'ce_template_custom1'; + + $controller = new TemplateController(); + $controller->setContainer($container); + + $controller(new Request(), $contentModel, 'main'); + } + + private function mockContainer(array $expectedData, string $expectedTemplate): Container + { + /** @var FrontendTemplate&MockObject $template */ + $template = $this->createMock(FrontendTemplate::class); + $template + ->expects($this->once()) + ->method('getResponse') + ->willReturn(new Response()) + ; + + $template + ->method('__set') + ->withConsecutive( + [$this->equalTo('headline'), $this->isNull()], + [$this->equalTo('hl'), $this->equalTo('h1')], + [$this->equalTo('class'), $this->equalTo('ce_template')], + [$this->equalTo('cssID'), $this->equalTo('')], + [$this->equalTo('inColumn'), $this->equalTo('main')], + [$this->equalTo('data'), $this->equalTo($expectedData)], + ) + ; + + $framework = $this->mockContaoFramework(); + $framework + ->expects($this->once()) + ->method('createInstance') + ->with(FrontendTemplate::class, [$expectedTemplate]) + ->willReturn($template) + ; + + $container = new Container(); + $container->set('contao.framework', $framework); + $container->set('request_stack', $this->createMock(RequestStack::class)); + + return $container; + } +}