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
82 changes: 19 additions & 63 deletions lib/Horde/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Horde\Core\Factory\DriverRepositoryFactory;
use Horde\Core\Factory\EventDispatcherFactory;
use Horde\Core\Factory\HttpClientFactory;
use Horde\Core\LanguageContext;
use Horde\Core\Factory\LoggerFactory;
use Horde\Core\Factory\AuthLinkRepositoryFactory;
use Horde\Core\Factory\IdentityHistoryRepositoryFactory;
Expand Down Expand Up @@ -3088,64 +3089,11 @@ public function getEmailCharset()
*/
public function preferredLang($lang = null)
{
$session = $GLOBALS['injector']->getInstance(HordeSession::class);

/* Check if we have a language set in the session */
if ($session->hasScoped('horde', 'language')) {
return basename($session->getScoped('horde', 'language'));
}

/* If language pref exists, we should use that. */
if (isset($GLOBALS['prefs'])
&& ($language = $GLOBALS['prefs']->getValue('language'))) {
return basename($language);
}

/* Check if the user selected a language from the login screen */
if (!empty($lang) && $this->nlsconfig->validLang($lang)) {
return basename($lang);
}

/* Try browser-accepted languages. */
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
/* The browser supplies a list, so return the first valid one. */
$browser_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
foreach ($browser_langs as $lang) {
/* Strip quality value for language */
if (($pos = strpos($lang, ';')) !== false) {
$lang = substr($lang, 0, $pos);
}

$lang = $this->_mapLang(trim($lang));
if ($this->nlsconfig->validLang($lang)) {
return basename($lang);
}

/* In case there's no full match, save our best guess. Try
* ll_LL, followed by just ll. */
if (!isset($partial_lang)) {
$ll_LL = Horde_String::lower(substr($lang, 0, 2)) . '_' . Horde_String::upper(substr($lang, 0, 2));
if ($this->nlsconfig->validLang($ll_LL)) {
$partial_lang = $ll_LL;
} else {
$ll = $this->_mapLang(substr($lang, 0, 2));
if ($this->nlsconfig->validLang($ll)) {
$partial_lang = $ll;
}
}
}
}

if (isset($partial_lang)) {
return basename($partial_lang);
}
}

/* Use site-wide default, if one is defined */
return $this->nlsconfig->curr_default
? basename($this->nlsconfig->curr_default)
/* No dice auto-detecting, default to US English. */
: 'en_US';
/* This used to be handled internally but is now owned by the
* LanguageContext/Nlsconfig service.
* See Horde\Core\Registry\Nlsconfig. */
return $GLOBALS['injector']->getInstance(LanguageContext::class)
->preferredLang($lang);
}

/**
Expand All @@ -3159,12 +3107,20 @@ public function preferredLang($lang = null)
*/
public function setLanguage($lang = null)
{
if (empty($lang) || !$this->nlsconfig->validLang($lang)) {
$lang = $this->preferredLang();
}

$GLOBALS['injector']->getInstance(HordeSession::class)
->setScoped('horde', 'language', $lang);


/**
* Resolution and session persistence used to be handled internally but is now owned by the
* LanguageContext/Nlsconfig service.
* See Horde\Core\Registry\Nlsconfig.
*
* Horde_Registry::setLanguage only applies its own side effects
* (locale/gettext/app callbacks) and mirrors the result into
* $GLOBALS['language'] for backward compatibility.
**/
$lang = $GLOBALS['injector']->getInstance(LanguageContext::class)
->setLanguage($lang);

$changed = false;
if (isset($GLOBALS['language'])) {
Expand Down
8 changes: 4 additions & 4 deletions lib/Horde/Script/File/ThemeDir.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ public function __get($name)
{
switch ($name) {
case 'path':
return $GLOBALS['registry']->get('themesfs', $this->_app) .
'/' . $this->_theme . '/';
return $GLOBALS['registry']->get('themesfs', $this->_app)
. '/' . $this->_theme . '/';

case 'url':
case 'url_full':
return $this->_url(
$GLOBALS['registry']->get('themesuri', $this->_app) .
'/' . $this->_theme . '/' . $this->_file,
$GLOBALS['registry']->get('themesuri', $this->_app)
. '/' . $this->_theme . '/' . $this->_file,
($name == 'url_full')
);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Horde/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ private function _resolveAccess(): ?SessionAccess
try {
$access = $GLOBALS['injector']->getInstance(SessionAccess::class);
return $access instanceof SessionAccess ? $access : null;
} catch (\Throwable) {
} catch (Throwable) {
return null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/Horde/Session/Null.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/
class Horde_Session_Null extends Horde_Session implements Horde_Shutdown_Task
{
use \Horde\Core\Session\PublishesModernSessionToAccessorTrait;
use Horde\Core\Session\PublishesModernSessionToAccessorTrait;

/**
* Constructor.
Expand Down
8 changes: 4 additions & 4 deletions lib/Horde/Themes/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,16 +328,16 @@ public function themeScripts()
{
if (!isset($this->_scripts)) {
$info = $this->_themeInfo();
$base = $GLOBALS['registry']->get('themesfs', 'horde') .
'/' . $this->_theme . '/';
$base = $GLOBALS['registry']->get('themesfs', 'horde')
. '/' . $this->_theme . '/';
$this->_scripts = [];

foreach ((array) ($info['theme_scripts'] ?? []) as $script) {
/* Plain file names only: no directory separators, no '..'.
* The theme directory is the only place a script may come
* from. */
if (!preg_match('/^[A-Za-z0-9_.-]+\.js$/', (string) $script) ||
strpos($script, '..') !== false) {
if (!preg_match('/^[A-Za-z0-9_.-]+\.js$/', (string) $script)
|| strpos($script, '..') !== false) {
continue;
}
if (is_readable($base . $script)) {
Expand Down
5 changes: 5 additions & 0 deletions src/DefaultInjectorBindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
namespace Horde\Core;

use Horde\Core\Api\ApiRegistry;
use Horde\Core\LanguageContext;
use Horde\Core\LanguageContextSelector;
use Horde\Core\Factory\NlsconfigFactory;
use Horde\Core\Auth\AuthService;
use Horde\Core\Config\ConfigLoader;
use Horde\Core\Config\ConfigMetadataProvider;
Expand Down Expand Up @@ -182,6 +185,8 @@ public function register(Injector $injector): void
'Horde_Core_Auth_Signup' => 'Horde_Core_Factory_AuthSignup',
'Horde_Auth_Base' => AuthBaseFactory::class,
ApiRegistry::class => ApiRegistryFactory::class,
LanguageContext::class => NlsconfigFactory::class,
LanguageContextSelector::class => NlsconfigFactory::class,
'Horde_Core_CssCache' => 'Horde_Core_Factory_CssCache',
'Horde_Core_JavascriptCache' => 'Horde_Core_Factory_JavascriptCache',
'Horde_Core_Perms' => 'Horde_Core_Factory_PermsCore',
Expand Down
1 change: 0 additions & 1 deletion src/Factory/AjaxApplicationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,3 @@ public function resolveClass(string $app): ?string
return null;
}
}

3 changes: 2 additions & 1 deletion src/Factory/DateFormatPrefsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

namespace Horde\Core\Factory;

use Horde\Core\LanguageContext;
use Horde\Core\Prefs\DateFormatPrefs;
use Horde_Injector;
use Horde\Injector\Injector;
Expand Down Expand Up @@ -44,7 +45,7 @@ public function create(Horde_Injector|Injector $injector): DateFormatPrefs

return new DateFormatPrefs(
prefs: $injector->getInstance('Horde_Prefs'),
locale: $GLOBALS['language'] ?? 'en_US',
locale: $injector->getInstance(LanguageContext::class)->getLocale(),
logger: $logger,
);
}
Expand Down
54 changes: 54 additions & 0 deletions src/Factory/NlsconfigFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

namespace Horde\Core\Factory;

use Horde\Core\Config\BackendConfigLoader;
use Horde\Core\Registry\Nlsconfig;
use Horde\Core\Session\SessionAccess;
use Horde_Injector;
use Horde\Injector\Injector;
use Throwable;

/**
* Factory for the {@see Nlsconfig} `LanguageContext` implementation.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
class NlsconfigFactory
{
public function create(Horde_Injector|Injector $injector): Nlsconfig
{
$prefs = null;
try {
$prefs = $injector->getInstance('Horde_Prefs');
} catch (Throwable) {
// Preferences unavailable (e.g. pre-auth request) — proceed
// without; the cascade falls through to the next source.
}

return new Nlsconfig(
$injector->getInstance(BackendConfigLoader::class),
// TODO: Delegate this to an appropriate place.
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null,
$injector->getInstance(SessionAccess::class),
$prefs,
);
}
}
14 changes: 8 additions & 6 deletions src/Factory/PermsUi.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
use Horde_Perms_Base;
use Horde_Registry;
use Psr\Http\Message\ServerRequestInterface;
use Horde_Notification_Handler;
use Throwable;

/**
* Factory selecting the administrative permission UI implementation.
Expand Down Expand Up @@ -105,12 +107,12 @@ private function createWithDenies(
* injector can't supply one so the modern UI degrades to silent
* rejection rather than failing to construct.
*/
private function resolveNotification(Injector $injector): ?\Horde_Notification_Handler
private function resolveNotification(Injector $injector): ?Horde_Notification_Handler
{
try {
$handler = $injector->getInstance('Horde_Notification');
return $handler instanceof \Horde_Notification_Handler ? $handler : null;
} catch (\Throwable $e) {
return $handler instanceof Horde_Notification_Handler ? $handler : null;
} catch (Throwable $e) {
return null;
}
}
Expand All @@ -136,7 +138,7 @@ private function resolveGroups(Injector $injector): ?Horde_Group
try {
$groups = $injector->getInstance(Horde_Group::class);
return $groups instanceof Horde_Group ? $groups : null;
} catch (\Throwable $e) {
} catch (Throwable $e) {
return null;
}
}
Expand All @@ -152,7 +154,7 @@ private function resolveAuth(Injector $injector): ?Horde_Auth_Base
$factory = $injector->getInstance('Horde_Core_Factory_Auth');
$auth = $factory->create();
return $auth instanceof Horde_Auth_Base ? $auth : null;
} catch (\Throwable $e) {
} catch (Throwable $e) {
return null;
}
}
Expand All @@ -179,7 +181,7 @@ private function negativePermissionsEnabled(Injector $injector): bool
if ($modern !== null) {
return (string) $modern === 'on';
}
} catch (\Throwable $e) {
} catch (Throwable $e) {
// ConfigLoader unavailable. Fall through to the legacy
// globals lookup below.
}
Expand Down
72 changes: 72 additions & 0 deletions src/LanguageContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

namespace Horde\Core;

/**
* Owner of the request's display language / locale.
*
* Replaces reads of $GLOBALS['language'] with an injectable service for READING.
*
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
interface LanguageContext
{
/**
* The currently resolved display language (e.g. 'de_DE').
*
* Resolves and caches via preferredLang() on first access if
* setLanguage() has not been called yet.
*/
public function getLanguage(): string;

/**
* The ICU-compatible locale identifier for the current language.
*
* Currently identical to getLanguage(); kept as a distinct method
* so callers expressing an ICU/locale need (e.g. date formatting)
* are not coupled to the language-cascade wording.
*/
public function getLocale(): string;

/**
* Run the language cascade without persisting anything.
*
* @param string|null $lang Explicit candidate language (e.g. from
* a login-screen selector), used only if
* no session/preference value exists.
*
* @return string The selected language abbreviation.
*/
public function preferredLang(?string $lang = null): string;

/**
* Resolve (via preferredLang()) and persist the language for this
* request (writes the 'horde' / 'language' session scope).
*
* @return string The resolved, now-current language.
*/
public function setLanguage(?string $lang = null): string;

/**
* Whether $lang is a configured, selectable language.
*/
public function validLang(string $lang): bool;
}
Loading
Loading