<?php
declare(strict_types=1);
/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/
namespace Contao\CoreBundle\Routing;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Routing\Page\PageRegistry;
use Contao\CoreBundle\Routing\Page\PageRoute;
use Contao\CoreBundle\Util\LocaleUtil;
use Contao\Model\Collection;
use Contao\PageModel;
use Symfony\Cmf\Component\Routing\Candidates\CandidatesInterface;
use Symfony\Cmf\Component\Routing\RouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
abstract class AbstractPageRouteProvider implements RouteProviderInterface
{
protected ContaoFramework $framework;
protected CandidatesInterface $candidates;
protected PageRegistry $pageRegistry;
public function __construct(ContaoFramework $framework, CandidatesInterface $candidates, PageRegistry $pageRegistry)
{
$this->framework = $framework;
$this->candidates = $candidates;
$this->pageRegistry = $pageRegistry;
}
/**
* @return array<PageModel>
*/
protected function findCandidatePages(Request $request): array
{
$candidates = array_map('strval', $this->candidates->getCandidates($request));
if (empty($candidates)) {
return [];
}
$ids = [];
$aliases = [];
foreach ($candidates as $candidate) {
if (preg_match('/^[1-9]\d*$/', $candidate)) {
$ids[] = (int) $candidate;
} else {
$aliases[] = $candidate;
}
}
$conditions = [];
if (!empty($ids)) {
$conditions[] = 'tl_page.id IN ('.implode(',', $ids).')';
}
if (!empty($aliases)) {
$conditions[] = 'tl_page.alias IN ('.implode(',', array_fill(0, \count($aliases), '?')).')';
}
$pageModel = $this->framework->getAdapter(PageModel::class);
$pages = $pageModel->findBy([implode(' OR ', $conditions)], $aliases);
if (!$pages instanceof Collection) {
return [];
}
/** @var array<PageModel> $models */
$models = $pages->getModels();
return array_filter($models, fn (PageModel $model) => $this->pageRegistry->isRoutable($model));
}
/**
* @return array<int>
*/
protected function getPageIdsFromNames(array $names): array
{
$ids = [];
foreach ($names as $name) {
if (0 !== strncmp($name, 'tl_page.', 8)) {
continue;
}
[, $id] = explode('.', $name);
if (!preg_match('/^[1-9]\d*$/', $id)) {
continue;
}
$ids[] = (int) $id;
}
return array_unique($ids);
}
protected function compareRoutes(Route $a, Route $b, array $languages = null): int
{
if ('' !== $a->getHost() && '' === $b->getHost()) {
return -1;
}
if ('' === $a->getHost() && '' !== $b->getHost()) {
return 1;
}
/** @var PageModel|null $pageA */
$pageA = $a->getDefault('pageModel');
/** @var PageModel|null $pageB */
$pageB = $b->getDefault('pageModel');
// Check if the page models are valid (should always be the case, as routes are generated from pages)
if (!$pageA instanceof PageModel || !$pageB instanceof PageModel) {
return 0;
}
$langA = null;
$langB = null;
if (null !== $languages && $pageA->rootLanguage !== $pageB->rootLanguage) {
$fallbackA = LocaleUtil::getFallbacks($pageA->rootLanguage);
$fallbackB = LocaleUtil::getFallbacks($pageB->rootLanguage);
$langA = $this->getLocalePriority($fallbackA, $fallbackB, $languages);
$langB = $this->getLocalePriority($fallbackB, $fallbackA, $languages);
if (null === $langA && null === $langB && LocaleUtil::getPrimaryLanguage($pageA->rootLanguage) === LocaleUtil::getPrimaryLanguage($pageB->rootLanguage)) {
// If both pages have the same language without region and neither region has a priority,
// (e.g. user prefers "de" but we have "de-CH" and "de-DE"), sort by their root page order.
$langA = $pageA->rootSorting;
$langB = $pageB->rootSorting;
}
}
if (null === $langA && null === $langB) {
if ($pageA->rootIsFallback && !$pageB->rootIsFallback) {
return -1;
}
if ($pageB->rootIsFallback && !$pageA->rootIsFallback) {
return 1;
}
} else {
if (null === $langA && null !== $langB) {
return 1;
}
if (null !== $langA && null === $langB) {
return -1;
}
if ($langA < $langB) {
return -1;
}
if ($langA > $langB) {
return 1;
}
}
if ('root' !== $pageA->type && 'root' === $pageB->type) {
return -1;
}
if ('root' === $pageA->type && 'root' !== $pageB->type) {
return 1;
}
if ($pageA->routePriority !== $pageB->routePriority) {
return $pageB->routePriority <=> $pageA->routePriority;
}
$pathA = $a instanceof PageRoute && $a->getUrlSuffix() ? substr($a->getPath(), 0, -\strlen($a->getUrlSuffix())) : $a->getPath();
$pathB = $b instanceof PageRoute && $b->getUrlSuffix() ? substr($b->getPath(), 0, -\strlen($b->getUrlSuffix())) : $b->getPath();
// Prioritize the default behaviour when "requireItem" is enabled
if ($pathA === $pathB && '{!parameters}' === substr($pathA, -13)) {
$paramA = $a->getRequirement('parameters');
$paramB = $b->getRequirement('parameters');
if ('/.+?' === $paramA && '(/.+?)?' === $paramB) {
return -1;
}
if ('(/.+?)?' === $paramA && '/.+?' === $paramB) {
return 1;
}
}
$countA = \count(explode('/', $pathA));
$countB = \count(explode('/', $pathB));
if ($countA > $countB) {
return -1;
}
if ($countB > $countA) {
return 1;
}
return strnatcasecmp($pathA, $pathB);
}
protected function convertLanguagesForSorting(array $languages): array
{
$result = [];
foreach ($languages as $language) {
if (!$locales = LocaleUtil::getFallbacks($language)) {
continue;
}
$language = array_pop($locales);
$result[] = $language;
foreach (array_reverse($locales) as $locale) {
if (!\in_array($locale, $result, true)) {
$result[] = $locale;
}
}
}
return array_flip($result);
}
private function getLocalePriority(array $locales, array $notIn, array $languagePriority): ?int
{
foreach (array_reverse($locales) as $locale) {
if (isset($languagePriority[$locale]) && !\in_array($locale, $notIn, true)) {
return $languagePriority[$locale];
}
}
return null;
}
}