Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -1423,12 +1423,6 @@
'count' => 1,
'path' => __DIR__ . '/src/Controller/Frontend/ListingController.php',
];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Controller\\\\Frontend\\\\ListingController\\:\\:listing\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\Response but returns Symfony\\\\Component\\\\HttpFoundation\\\\Response\\|null\\.$#',
'identifier' => 'return.type',
'count' => 1,
'path' => __DIR__ . '/src/Controller/Frontend/ListingController.php',
];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Controller\\\\Frontend\\\\ListingController\\:\\:parseQueryParams\\(\\) return type has no value type specified in iterable type array\\.$#',
'identifier' => 'missingType.iterableValue',
Expand Down Expand Up @@ -1513,12 +1507,6 @@
'count' => 1,
'path' => __DIR__ . '/src/Controller/TwigAwareController.php',
];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Controller\\\\TwigAwareController\\:\\:renderSingle\\(\\) should return Symfony\\\\Component\\\\HttpFoundation\\\\Response but returns Symfony\\\\Component\\\\HttpFoundation\\\\Response\\|null\\.$#',
'identifier' => 'return.type',
'count' => 1,
'path' => __DIR__ . '/src/Controller/TwigAwareController.php',
];
$ignoreErrors[] = [
'message' => '#^Method Bolt\\\\Controller\\\\TwigAwareController\\:\\:renderTemplate\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#',
'identifier' => 'missingType.iterableValue',
Expand Down
53 changes: 52 additions & 1 deletion src/Controller/ErrorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Throwable;
use Twig\Environment;
use Twig\Error\LoaderError;
Expand All @@ -37,10 +39,17 @@ public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly Security $security,
private readonly RequestStack $requestStack,
private readonly TranslatorInterface $translator,
string $locales,
) {
parent::__construct($httpKernel, $this->templateController, $errorRenderer);

$this->localeCodes = explode('|', $locales);
}

/** @var list<string> */
private readonly array $localeCodes;

/**
* Show an exception. Mainly used for custom 404 pages, otherwise falls back
* to Symfony's error handling
Expand All @@ -61,6 +70,11 @@ public function showAction(Environment $twig, Throwable $exception): Response

// We need the parent request here, but fall back to current if not found
if ($request = $this->requestStack->getParentRequest() ?? $this->requestStack->getCurrentRequest()) {
// On a 404/403, no route matched, so Symfony's LocaleListener never set
// the locale from the URL. Recover it from the path so localized error
// pages (and their records) render in the right language.
$this->setLocaleFromPath($request);

if ($code === Response::HTTP_SERVICE_UNAVAILABLE || $this->isMaintenanceEnabled($code)) {
$twig->addGlobal('exception', $exception);

Expand Down Expand Up @@ -156,6 +170,41 @@ private function isMaintenanceEnabled(int $code): bool
return filter_var($this->config->get('general/maintenance_mode', false), FILTER_VALIDATE_BOOLEAN);
}

/**
* Sets the locale based on the first segment of the path, if it matches one
* of the configured locales (e.g. `/de/...` => `de`).
*
* The locale is applied to both the given request (used when rendering a
* record) and the current request (which Twig's `app.request` resolves to,
* and is a sub-request when an error page is being rendered). It's also set on
* the translator, so `{% trans %}` strings in the error template are localized
* too - normally Symfony's `LocaleListener`/`LocaleAwareListener` does this,
* but neither runs when no route matched.
*/
private function setLocaleFromPath(Request $request): void
{
// Cast: on PHP 8.4 `mb_trim()` is analysed as `string|false`, but `getPathInfo()`
// always yields a string, so the result is effectively always a string here.
$segment = explode('/', (string) mb_trim($request->getPathInfo(), '/'))[0];

if ($segment !== '' && in_array($segment, $this->localeCodes, true)) {
$request->setLocale($segment);

$currentRequest = $this->requestStack->getCurrentRequest();
if ($currentRequest instanceof Request && $currentRequest !== $request) {
$currentRequest->setLocale($segment);
}
}

// The concrete translator is locale-aware; the contracts interface we depend
// on isn't, so guard the call to keep the dependency narrow. Always sync it to
// the (possibly default) request locale so a previous request's locale can't
// leak into a default-locale error page in long-running runtimes.
if ($this->translator instanceof LocaleAwareInterface) {
$this->translator->setLocale($request->getLocale());
}
}
Comment thread
Vondry marked this conversation as resolved.

private function attemptToRender(Request $request, string $item): ?Response
{
// First, see if it's a contenttype/slug pair:
Expand All @@ -165,7 +214,9 @@ private function attemptToRender(Request $request, string $item): ?Response
// We wrap it in a try/catch, because we wouldn't want to
// trigger a 404 within a 404 now, would we?
try {
return $this->detailController->record($request, $slug, $contentType, false, null);
// Pass the request's locale explicitly, so `DetailController` keeps
// it instead of falling back to the default locale.
return $this->detailController->record($request, $slug, $contentType, false, $request->getLocale());
} catch (NotFoundHttpException) {
// Just continue to the next one.
}
Expand Down
9 changes: 6 additions & 3 deletions src/Controller/Frontend/ListingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ public function listing(Request $request, ContentRepository $contentRepository,
throw new NotFoundHttpException('Content is not viewable');
}

// If the locale is the wrong locale
if (! $this->validLocaleForContentType($request, $contentType)) {
return $this->redirectToDefaultLocale($request);
// If the locale is the wrong locale, redirect to the default locale, or -
// when that's not possible (e.g. a forwarded request without a matched
// route) - render the listing in the default locale.
if (! $this->validLocaleForContentType($request, $contentType)
&& ($redirect = $this->redirectToDefaultLocaleOrFallback($request)) instanceof Response) {
return $redirect;
}

$page = (int) $this->getFromRequest($request, 'page', '1');
Expand Down
43 changes: 39 additions & 4 deletions src/Controller/TwigAwareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ public function renderSingle(Request $request, ?Content $record, bool $requirePu
throw new NotFoundHttpException('Content is not viewable');
}

// If the locale is the wrong locale
if (! $this->validLocaleForContentType($request, $recordDefinition)) {
return $this->redirectToDefaultLocale($request);
// If the locale is the wrong locale, redirect to the default locale, or -
// when that's not possible - render the record in the default locale.
if (! $this->validLocaleForContentType($request, $recordDefinition)
&& ($redirect = $this->redirectToDefaultLocaleOrFallback($request)) instanceof Response) {
return $redirect;
}

$singularSlug = $record->getContentTypeSingularSlug();
Expand Down Expand Up @@ -145,8 +147,41 @@ protected function validLocaleForContentType(Request $request, ContentType $cont
return $request->getLocale() === $this->defaultLocale;
}

/**
* Either redirect to the same route in the default locale, or - when there's
* no route to redirect to (e.g. a forwarded request, or an error page where
* routing never matched) - reset the request to the default locale and return
* `null`, so the caller can render in the default locale instead.
*
* Note: this resets the locale on the _given_ request only. When rendering an
* error page, Twig's `app.request` is a sub-request whose locale was set
* separately (see ErrorController::setLocaleFromPath()), so the `<html lang>`
* may still reflect the URL locale while the - non-localizable - record content
* is rendered in the default locale. That's harmless: such content is identical
* across locales.
*/
protected function redirectToDefaultLocaleOrFallback(Request $request): ?Response
{
$redirect = $this->redirectToDefaultLocale($request);

if ($redirect instanceof Response) {
return $redirect;
}

$request->setLocale($this->defaultLocale);

return null;
}

protected function redirectToDefaultLocale(Request $request): ?Response
{
// No route was matched (e.g. on an error page): there's nothing to
// redirect to, so let the caller decide how to handle this.
$route = $request->attributes->get('_route');
if (! $route) {
return null;
}

$request->getSession()->set('_locale', $this->defaultLocale);

$params = $request->attributes->get('_route_params');
Expand All @@ -155,7 +190,7 @@ protected function redirectToDefaultLocale(Request $request): ?Response
$params['_locale'] = $this->defaultLocale;
}

return $this->redirectToRoute($request->get('_route'), $params);
return $this->redirectToRoute($route, $params);
}

private function setTwigLoader(): void
Expand Down
Loading
Loading