diff --git a/typo3/sysext/core/Classes/Routing/Aspect/PersistedAliasMapper.php b/typo3/sysext/core/Classes/Routing/Aspect/PersistedAliasMapper.php index 9b049bfa86701bf83c33742ecf641d1ac23d4cbc..e0d23f6f6ca7fa66df0848a64926dbc4efc512e2 100644 --- a/typo3/sysext/core/Classes/Routing/Aspect/PersistedAliasMapper.php +++ b/typo3/sysext/core/Classes/Routing/Aspect/PersistedAliasMapper.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Routing\Aspect; * The TYPO3 project - inspiring people to share! */ +use Doctrine\DBAL\Connection; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\LanguageAspectFactory; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -23,7 +24,6 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Routing\Legacy\PersistedAliasMapperLegacyTrait; use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface; -use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -47,7 +47,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; */ class PersistedAliasMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface { - use SiteLanguageAwareTrait; + use SiteLanguageAccessorTrait; use PersistedAliasMapperLegacyTrait; /** @@ -196,16 +196,36 @@ class PersistedAliasMapper implements PersistedMappableAspectInterface, StaticMa protected function findByRouteFieldValue(string $value): ?array { + $languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null; + $queryBuilder = $this->createQueryBuilder(); - $result = $queryBuilder - ->select(...$this->persistenceFieldNames) - ->where($queryBuilder->expr()->eq( + $constraints = [ + $queryBuilder->expr()->eq( $this->routeFieldName, $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR) - )) + ), + ]; + + $languageIds = null; + if ($languageAware) { + $languageIds = $this->resolveAllRelevantLanguageIds(); + $constraints[] = $queryBuilder->expr()->in( + $this->languageFieldName, + $queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY) + ); + } + + $results = $queryBuilder + ->select(...$this->persistenceFieldNames) + ->where(...$constraints) ->execute() - ->fetch(); - return $result !== false ? $result : null; + ->fetchAll(); + // return first result record in case table is not language aware + if (!$languageAware) { + return $results[0] ?? null; + } + // post-process language fallbacks + return $this->resolveLanguageFallback($results, $this->languageFieldName, $languageIds); } protected function createQueryBuilder(): QueryBuilder diff --git a/typo3/sysext/core/Classes/Routing/Aspect/PersistedPatternMapper.php b/typo3/sysext/core/Classes/Routing/Aspect/PersistedPatternMapper.php index f0c5b22a811baee76d1bdab76380969928965d9c..0f1cfa3cc2036fafe59cee21a0baaa77aa5c61ff 100644 --- a/typo3/sysext/core/Classes/Routing/Aspect/PersistedPatternMapper.php +++ b/typo3/sysext/core/Classes/Routing/Aspect/PersistedPatternMapper.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Routing\Aspect; * The TYPO3 project - inspiring people to share! */ +use Doctrine\DBAL\Connection; use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Context\LanguageAspectFactory; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -23,7 +24,6 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Routing\Legacy\PersistedPatternMapperLegacyTrait; use TYPO3\CMS\Core\Site\SiteLanguageAwareInterface; -use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait; use TYPO3\CMS\Core\Utility\GeneralUtility; /** @@ -50,7 +50,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; */ class PersistedPatternMapper implements PersistedMappableAspectInterface, StaticMappableAspectInterface, SiteLanguageAwareInterface { - use SiteLanguageAwareTrait; + use SiteLanguageAccessorTrait; use PersistedPatternMapperLegacyTrait; protected const PATTERN_RESULT = '#\{(?P<fieldName>[^}]+)\}#'; @@ -80,6 +80,11 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static */ protected $routeFieldResultNames; + /** + * @var string|null + */ + protected $languageFieldName; + /** * @var string|null */ @@ -116,6 +121,7 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static $this->routeFieldPattern = $routeFieldPattern; $this->routeFieldResult = $routeFieldResult; $this->routeFieldResultNames = $routeFieldResultNames['fieldName'] ?? []; + $this->languageFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['languageField'] ?? null; $this->languageParentFieldName = $GLOBALS['TCA'][$this->tableName]['ctrl']['transOrigPointerField'] ?? null; } @@ -204,13 +210,21 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static protected function findByRouteFieldValues(array $values): ?array { + $languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null; + $queryBuilder = $this->createQueryBuilder(); - $result = $queryBuilder + $results = $queryBuilder ->select('*') ->where(...$this->createRouteFieldConstraints($queryBuilder, $values)) ->execute() - ->fetch(); - return $result !== false ? $result : null; + ->fetchAll(); + // return first result record in case table is not language aware + if (!$languageAware) { + return $results[0] ?? null; + } + // post-process language fallbacks + $languageIds = $this->resolveAllRelevantLanguageIds(); + return $this->resolveLanguageFallback($results, $this->languageFieldName, $languageIds); } protected function createQueryBuilder(): QueryBuilder @@ -227,7 +241,8 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static */ protected function createRouteFieldConstraints(QueryBuilder $queryBuilder, array $values): array { - $languageExpansion = $this->languageParentFieldName && isset($values['uid']); + $languageAware = $this->languageFieldName !== null && $this->languageParentFieldName !== null; + $languageExpansion = $languageAware && isset($values['uid']); $constraints = []; foreach ($values as $fieldName => $fieldValue) { @@ -242,7 +257,7 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static ) ); } - // If requested, either match uid or language parent field value + // either match uid or language parent field value (for any language) if ($languageExpansion) { $idParameter = $queryBuilder->createNamedParameter( $values['uid'], @@ -252,6 +267,13 @@ class PersistedPatternMapper implements PersistedMappableAspectInterface, Static $queryBuilder->expr()->eq('uid', $idParameter), $queryBuilder->expr()->eq($this->languageParentFieldName, $idParameter) ); + // otherwise - basically uid is not in pattern - restrict to languages and apply fallbacks + } elseif ($languageAware) { + $languageIds = $this->resolveAllRelevantLanguageIds(); + $constraints[] = $queryBuilder->expr()->in( + $this->languageFieldName, + $queryBuilder->createNamedParameter($languageIds, Connection::PARAM_INT_ARRAY) + ); } return $constraints; diff --git a/typo3/sysext/core/Classes/Routing/Aspect/SiteLanguageAccessorTrait.php b/typo3/sysext/core/Classes/Routing/Aspect/SiteLanguageAccessorTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..afe3111b3376732f098fadf9f1cdc94d6e8a1eef --- /dev/null +++ b/typo3/sysext/core/Classes/Routing/Aspect/SiteLanguageAccessorTrait.php @@ -0,0 +1,99 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\Routing\Aspect; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Context\LanguageAspectFactory; +use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait; +use TYPO3\CMS\Core\Utility\MathUtility; + +trait SiteLanguageAccessorTrait +{ + use SiteLanguageAwareTrait; + + /** + * @var LanguageAspect + */ + protected $languageAspect; + + /** + * Resolves one record out of given language fallbacks. + * + * @param array $results + * @param string|null $languageFieldName + * @param array|null $languageIds + * @return array|null + */ + protected function resolveLanguageFallback(array $results, ?string $languageFieldName, ?array $languageIds): ?array + { + if ($results === []) { + return null; + } + if ($languageFieldName === null || $languageIds === null) { + return $results[0]; + } + usort( + $results, + // orders records by there occurrence in $languageFallbackIds + function (array $a, array $b) use ($languageFieldName, $languageIds): int { + $languageA = (int)$a[$languageFieldName]; + $languageB = (int)$b[$languageFieldName]; + return array_search($languageA, $languageIds, true) + - array_search($languageB, $languageIds, true); + } + ); + return $results[0]; + } + + /** + * Resolves all language ids that are relevant to retrieve the most specific variant of a record. + * The order of these ids defines the processing order concerning language fallback - most specific + * language comes first in this array. + * + * + "all language (-1)", most specific if present since there cannot be any localizations + * + "current language" most specific for the current given request context + * + "language fallbacks" falling back to language alternatives (might include "default language") + * + * @return int[] + */ + protected function resolveAllRelevantLanguageIds() + { + $languageIds = [-1, $this->siteLanguage->getLanguageId()]; + foreach ($this->getLanguageAspect()->getFallbackChain() as $item) { + if (in_array($item, $languageIds, true) || !MathUtility::canBeInterpretedAsInteger($item)) { + continue; + } + $languageIds[] = (int)$item; + } + return $languageIds; + } + + /** + * Provides LanguageAspect which contains the logic how fallbacks + * for a given context/overlay-mode shall be handled. + * + * @return LanguageAspect + * @see LanguageAspectFactory::createFromSiteLanguage + */ + protected function getLanguageAspect(): LanguageAspect + { + if ($this->languageAspect === null) { + $this->languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->siteLanguage); + } + return $this->languageAspect; + } +} diff --git a/typo3/sysext/core/Tests/Functional/Routing/Aspect/PersistedAliasMapperTest.php b/typo3/sysext/core/Tests/Functional/Routing/Aspect/PersistedAliasMapperTest.php index c8d32b7b7b0a2506392f45e9749b0b5d27fac5fb..9224cd5f0151170c816bd10dfc8e28e1a8ef9f0c 100644 --- a/typo3/sysext/core/Tests/Functional/Routing/Aspect/PersistedAliasMapperTest.php +++ b/typo3/sysext/core/Tests/Functional/Routing/Aspect/PersistedAliasMapperTest.php @@ -112,28 +112,28 @@ class PersistedAliasMapperTest extends FunctionalTestCase '30xx-slug-fr-ca, fr-ca language' => ['30xx-slug-fr-ca', 'fr-ca', '3010'], // '30xx-slug-fr-ca' available in default language as well, fallbacks to that one - '30xx-slug-fr-ca, fr-fr language' => ['30xx-slug-fr-ca', 'fr-fr', '3010'], + '30xx-slug-fr-ca, fr-fr language' => ['30xx-slug-fr-ca', 'fr-fr', '3030'], // '30xx-slug-fr-ca' available in default language, use it directly - '30xx-slug-fr-ca, default language' => ['30xx-slug-fr-ca', 'default', '3010'], + '30xx-slug-fr-ca, default language' => ['30xx-slug-fr-ca', 'default', '3030'], '30xx-slug-fr, fr-ca language' => ['30xx-slug-fr', 'fr-ca', '3010'], '30xx-slug-fr, fr-fr language' => ['30xx-slug-fr', 'fr-fr', '3010'], // '30xx-slug-fr-ca' available in default language, use it directly - '30xx-slug-fr, default language' => ['30xx-slug-fr', 'default', '3010'], + '30xx-slug-fr, default language' => ['30xx-slug-fr', 'default', '3020'], // basically the same, but being stored in reverse order in database '40xx-slug, default language' => ['40xx-slug', 'default', '4040'], '40xx-slug, fr-fr language' => ['40xx-slug', 'fr-fr', '4040'], '40xx-slug, fr-ca language' => ['40xx-slug', 'fr-ca', '4040'], - '40xx-slug-fr-ca, fr-ca language' => ['40xx-slug-fr-ca', 'fr-ca', '4030'], + '40xx-slug-fr-ca, fr-ca language' => ['40xx-slug-fr-ca', 'fr-ca', '4040'], // '40xx-slug-fr-ca' available in default language as well, fallbacks to that one '40xx-slug-fr-ca, fr-fr language' => ['40xx-slug-fr-ca', 'fr-fr', '4030'], // '40xx-slug-fr-ca' available in default language, use it directly '40xx-slug-fr-ca, default language' => ['40xx-slug-fr-ca', 'default', '4030'], - '40xx-slug-fr, fr-ca language' => ['40xx-slug-fr', 'fr-ca', '4020'], - '40xx-slug-fr, fr-fr language' => ['40xx-slug-fr', 'fr-fr', '4020'], + '40xx-slug-fr, fr-ca language' => ['40xx-slug-fr', 'fr-ca', '4040'], + '40xx-slug-fr, fr-fr language' => ['40xx-slug-fr', 'fr-fr', '4040'], // '40xx-slug-fr-ca' available in default language, use it directly '40xx-slug-fr, default language' => ['40xx-slug-fr', 'default', '4020'], ]; @@ -146,7 +146,6 @@ class PersistedAliasMapperTest extends FunctionalTestCase * * @test * @dataProvider languageAwareRecordsAreResolvedDataProvider - * @group not-postgres */ public function languageAwareRecordsAreResolved(string $requestValue, string $language, ?string $expectation): void {