Skip to content
Snippets Groups Projects
Commit e8310ca6 authored by Nicole Cordes's avatar Nicole Cordes Committed by Benni Mack
Browse files

[BUGFIX] Consider language context and fallbacks in persisted aspects

For PersistedAliasMapper and PersistedPatternMapper, language handling
when resolving a URL route paramter was not explicit enough.

PersistedAliasMapper incorrectly resolved language default records even
when the HTTP request contained a language aspect and a more specific
record (having the same slug value) would have been available.

That was similar in PersistedPatternMapper when `uid` field was not
defined in corresponding pattern (e.g. `^(?P<title>.+)-(?P<uid>\d+)$).

For both mentioned scenarios language restrictions and fallback handling
has been integrated. Records are retrieved in the following order:

+ "all language (-1)", most specific if present, can't be localized
+ "current language" most specific for the current given request context
+ "language fallbacks" (might include "default language")

Resolves: #89153
Releases: master, 9.5
Change-Id: I25b17d1d618bb1509737d43b877a16c3a6da9f28
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61668


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarOliver Hader <oliver.hader@typo3.org>
Tested-by: default avatarBenni Mack <benni@typo3.org>
Reviewed-by: default avatarOliver Hader <oliver.hader@typo3.org>
Reviewed-by: default avatarBenni Mack <benni@typo3.org>
parent 3d81163d
Branches
Tags
No related merge requests found
......@@ -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
......
......@@ -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;
......
<?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;
}
}
......@@ -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
{
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment