From 1c9bc64bbe7db0a9dea37b7e8e7285f2eafb1c19 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 18 Sep 2026 11:06:52 +0200 Subject: [PATCH 1/3] refactor: Extract Nlsconfig into a DI service --- src/Factory/NlsconfigFactory.php | 52 +++++++++ src/LanguageContext.php | 92 +++++++++++++++ src/LanguageContextSelector.php | 54 +++++++++ src/Registry/Nlsconfig.php | 168 +++++++++++++++++++++++++++ test/Unit/Registry/NlsconfigTest.php | 132 +++++++++++++++++++++ 5 files changed, 498 insertions(+) create mode 100644 src/Factory/NlsconfigFactory.php create mode 100644 src/LanguageContext.php create mode 100644 src/LanguageContextSelector.php create mode 100644 src/Registry/Nlsconfig.php create mode 100644 test/Unit/Registry/NlsconfigTest.php diff --git a/src/Factory/NlsconfigFactory.php b/src/Factory/NlsconfigFactory.php new file mode 100644 index 00000000..96c22cc1 --- /dev/null +++ b/src/Factory/NlsconfigFactory.php @@ -0,0 +1,52 @@ +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(SessionAccess::class), + $injector->getInstance(BackendConfigLoader::class), + $prefs, + ); + } +} diff --git a/src/LanguageContext.php b/src/LanguageContext.php new file mode 100644 index 00000000..f2e77370 --- /dev/null +++ b/src/LanguageContext.php @@ -0,0 +1,92 @@ + preference -> explicit $lang -> + * Accept-Language -> site default) and the resolved $language value + * itself. Unlike its legacy predecessor, it does not read/write + * $GLOBALS and does not require a bootstrapped `Horde_Registry` — it + * only needs a session accessor, the merged `nls.php` config, and + * (optionally) preferences, all constructor-injected. + * + * `Horde_Registry` is a *consumer* of this service, not the other way + * around: it asks this class to resolve/persist the language, then + * layers its own legacy side effects (setlocale()/putenv(), gettext + * domain reload, `$GLOBALS['language']` mirroring, per-app + * `changeLanguage()` callbacks) on top. See {@see LanguageContext} for + * the accepted gap when this service is used without going through + * `Horde_Registry`. + * + * @category Horde + * @copyright 2026 The Horde Project + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package Core + */ +final class Nlsconfig implements LanguageContext +{ + /** Merged `nls.php` config (`horde_nls_config` variable). */ + private array $config; + + private ?string $language = null; + + public function __construct( + private SessionAccess $session, + BackendConfigLoader $configLoader, + private ?Horde_Prefs $prefs = null, + ) { + $this->config = $configLoader + ->load('horde', 'nls.php', 'horde_nls_config') + ->toArray(); + } + + public function validLang(string $lang): bool + { + return isset($this->config['languages'][$lang]); + } + + public function preferredLang(?string $lang = null): string + { + if ($this->session->hasCurrent() + && $this->session->hasScoped('horde', 'language')) { + return basename((string) $this->session->getScoped('horde', 'language')); + } + + if ($this->prefs !== null + && ($prefLang = $this->prefs->getValue('language'))) { + return basename($prefLang); + } + + if (!empty($lang) && $this->validLang($lang)) { + return basename($lang); + } + + if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $partialLang = null; + + foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $browserLang) { + if (($pos = strpos($browserLang, ';')) !== false) { + $browserLang = substr($browserLang, 0, $pos); + } + + $mapped = $this->mapLang(trim($browserLang)); + if ($this->validLang($mapped)) { + return basename($mapped); + } + + /* In case there's no full match, save our best guess. Try + * ll_LL, followed by just ll. */ + if ($partialLang === null) { + $llLL = Horde_String::lower(substr($mapped, 0, 2)) + . '_' . Horde_String::upper(substr($mapped, 0, 2)); + if ($this->validLang($llLL)) { + $partialLang = $llLL; + } else { + $ll = $this->mapLang(substr($mapped, 0, 2)); + if ($this->validLang($ll)) { + $partialLang = $ll; + } + } + } + } + + if ($partialLang !== null) { + return basename($partialLang); + } + } + + $default = $this->config['defaults']['language'] ?? ''; + + return $default !== '' ? basename($default) : 'en_US'; + } + + public function setLanguage(?string $lang = null): string + { + $resolved = (!empty($lang) && $this->validLang($lang)) + ? basename($lang) + : $this->preferredLang($lang); + + $this->language = $resolved; + + if ($this->session->hasCurrent()) { + $this->session->setScoped('horde', 'language', $resolved); + } + + return $resolved; + } + + public function getLanguage(): string + { + return $this->language ??= $this->preferredLang(); + } + + public function getLocale(): string + { + return $this->getLanguage(); + } + + /** + * Maps languages with common two-letter codes (such as nl) to the + * full locale code (in this case, nl_NL). Returns the language + * unmodified if it isn't an alias. + */ + private function mapLang(string $language): string + { + // Translate the $language to get broader matches. + // (e.g. de-DE should match de_DE) + $transLang = str_replace('-', '_', $language); + $langParts = explode('_', $transLang); + $transLang = Horde_String::lower($langParts[0]); + if (isset($langParts[1])) { + $transLang .= '_' . Horde_String::upper($langParts[1]); + } + + return $this->config['aliases'][$transLang] ?? $transLang; + } +} diff --git a/test/Unit/Registry/NlsconfigTest.php b/test/Unit/Registry/NlsconfigTest.php new file mode 100644 index 00000000..29a1dea8 --- /dev/null +++ b/test/Unit/Registry/NlsconfigTest.php @@ -0,0 +1,132 @@ +tempDir = sys_get_temp_dir() . '/horde-nlsconfig-test-' . uniqid(); + $vendorDir = $this->tempDir . '/vendor/horde'; + $configDir = $this->tempDir . '/config'; + mkdir($vendorDir . '/horde/config', 0o755, true); + mkdir($configDir . '/horde', 0o755, true); + + file_put_contents( + $vendorDir . '/horde/config/nls.php', + <<<'PHP' + ['language' => ''], + 'languages' => [ + 'en_US' => 'English (American)', + 'de_DE' => 'Deutsch', + ], + 'aliases' => ['de' => 'de_DE'], + ]; + PHP + ); + + $this->configLoader = new BackendConfigLoader($configDir, $vendorDir); + + $hordeSession = new HordeSession(new SessionId('test-session')); + $this->sessionAccessor = new SessionAccessor(); + $this->sessionAccessor->replaceWith($hordeSession); + } + + protected function tearDown(): void + { + $files = array_diff(scandir($this->tempDir . '/vendor/horde/horde/config'), ['.', '..']); + foreach ($files as $file) { + unlink($this->tempDir . '/vendor/horde/horde/config/' . $file); + } + } + + private function makeNlsconfig(): Nlsconfig + { + return new Nlsconfig($this->sessionAccessor, $this->configLoader); + } + + public function testImplementsLanguageContext(): void + { + $this->assertInstanceOf(LanguageContext::class, $this->makeNlsconfig()); + } + + public function testValidLang(): void + { + $nls = $this->makeNlsconfig(); + $this->assertTrue($nls->validLang('en_US')); + $this->assertFalse($nls->validLang('xy_XY')); + } + + public function testPreferredLangFallsBackToDefaultWhenNothingElseMatches(): void + { + $nls = $this->makeNlsconfig(); + $this->assertSame('en_US', $nls->preferredLang()); + } + + public function testPreferredLangUsesExplicitLangArgument(): void + { + $nls = $this->makeNlsconfig(); + $this->assertSame('de_DE', $nls->preferredLang('de_DE')); + } + + public function testSetLanguagePersistsToSessionAndGetLanguageReflectsIt(): void + { + $nls = $this->makeNlsconfig(); + $this->assertSame('de_DE', $nls->setLanguage('de_DE')); + $this->assertSame('de_DE', $nls->getLanguage()); + $this->assertSame('de_DE', $this->sessionAccessor->getScoped('horde', 'language')); + } + + public function testPreferredLangPrefersSessionOverExplicitLang(): void + { + $this->sessionAccessor->setScoped('horde', 'language', 'de_DE'); + $nls = $this->makeNlsconfig(); + $this->assertSame('de_DE', $nls->preferredLang('en_US')); + } + + public function testGetLocaleIsAliasForGetLanguage(): void + { + $nls = $this->makeNlsconfig(); + $nls->setLanguage('de_DE'); + $this->assertSame($nls->getLanguage(), $nls->getLocale()); + } +} From e4b78d619dbff1e7b6e80a591fb89c349e9efc4e Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 18 Sep 2026 11:14:52 +0200 Subject: [PATCH 2/3] refactor: Extract Language handling into an Injectable service. This allows application code to stop relying on $GLOBALS['language'] refactor: Wire up LanguageContext as the provider for DateFormatPrefs language refactor: Registry internally uses LanguageContext (Horde\Core\Registry\Nlsconfig) refactor: Move access to superglobal variable $_SERVER['HTTP_ACCEPT_LANGUAGE'] out of Nlsconfig into Factory --- lib/Horde/Registry.php | 80 ++++++-------------------- src/DefaultInjectorBindings.php | 5 ++ src/Factory/DateFormatPrefsFactory.php | 3 +- src/Factory/NlsconfigFactory.php | 4 +- src/LanguageContext.php | 22 +------ src/Registry/Nlsconfig.php | 46 ++++++++------- 6 files changed, 53 insertions(+), 107 deletions(-) diff --git a/lib/Horde/Registry.php b/lib/Horde/Registry.php index e0cccaa1..75019016 100644 --- a/lib/Horde/Registry.php +++ b/lib/Horde/Registry.php @@ -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; @@ -3084,64 +3085,11 @@ public function getEmailCharset() */ public function preferredLang($lang = null) { - $session = $GLOBALS['injector']->get(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']->get(LanguageContext::class) + ->preferredLang($lang); } /** @@ -3155,12 +3103,18 @@ public function preferredLang($lang = null) */ public function setLanguage($lang = null) { - if (empty($lang) || !$this->nlsconfig->validLang($lang)) { - $lang = $this->preferredLang(); - } - $GLOBALS['injector']->get(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']->get(LanguageContext::class) + ->setLanguage($lang); $changed = false; if (isset($GLOBALS['language'])) { diff --git a/src/DefaultInjectorBindings.php b/src/DefaultInjectorBindings.php index e9e79c8a..80edd998 100644 --- a/src/DefaultInjectorBindings.php +++ b/src/DefaultInjectorBindings.php @@ -18,6 +18,9 @@ use Horde\Core\ActiveSync\Ops\SnapshotService; 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; @@ -185,6 +188,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', diff --git a/src/Factory/DateFormatPrefsFactory.php b/src/Factory/DateFormatPrefsFactory.php index df036cac..3769b63b 100644 --- a/src/Factory/DateFormatPrefsFactory.php +++ b/src/Factory/DateFormatPrefsFactory.php @@ -16,6 +16,7 @@ namespace Horde\Core\Factory; +use Horde\Core\LanguageContext; use Horde\Core\Prefs\DateFormatPrefs; use Horde_Injector; use Horde\Injector\Injector; @@ -44,7 +45,7 @@ public function create(Horde_Injector|Injector $injector): DateFormatPrefs return new DateFormatPrefs( prefs: $injector->get('Horde_Prefs'), - locale: $GLOBALS['language'] ?? 'en_US', + locale: $injector->get(LanguageContext::class)->getLocale(), logger: $logger, ); } diff --git a/src/Factory/NlsconfigFactory.php b/src/Factory/NlsconfigFactory.php index 96c22cc1..82115cd9 100644 --- a/src/Factory/NlsconfigFactory.php +++ b/src/Factory/NlsconfigFactory.php @@ -44,8 +44,10 @@ public function create(Horde_Injector|Injector $injector): Nlsconfig } return new Nlsconfig( - $injector->getInstance(SessionAccess::class), $injector->getInstance(BackendConfigLoader::class), + // TODO: Delegate this to an appropriate place. + $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null, + $injector->getInstance(SessionAccess::class), $prefs, ); } diff --git a/src/LanguageContext.php b/src/LanguageContext.php index f2e77370..cc976fb5 100644 --- a/src/LanguageContext.php +++ b/src/LanguageContext.php @@ -19,28 +19,8 @@ /** * Owner of the request's display language / locale. * - * Replaces reads of $GLOBALS['language'] with an injectable service. - * This is the source of truth: {@see \Horde_Registry} delegates its - * legacy preferredLang()/setLanguage() to an instance of this - * interface and mirrors the result into $GLOBALS['language'] for - * backward compatibility, rather than the other way around. + * Replaces reads of $GLOBALS['language'] with an injectable service for READING. * - * Implementations resolve the language cascade: - * 1. Session ('horde' / 'language' scope) - * 2. Preference ('language') - * 3. Explicit $lang argument (e.g. login-screen selection), if valid - * 4. Browser Accept-Language header - * 5. Site-wide default / 'en_US' fallback - * - * Known MVP gap: code that calls setLanguage() directly on this - * service (bypassing Horde_Registry) resolves and persists the - * session value, but does not trigger Horde_Registry's side effects - * (setlocale()/putenv(), gettext domain reload, per-app - * changeLanguage() callbacks) or refresh $GLOBALS['language']. This is - * accepted for now; a future PSR-14 LanguageChanged event dispatched - * from setLanguage() is the intended remediation, letting - * Horde_Registry (and other interested listeners) react without this - * service depending back on the legacy registry. * * @category Horde * @copyright 2026 The Horde Project diff --git a/src/Registry/Nlsconfig.php b/src/Registry/Nlsconfig.php index 0ea79a34..6ad11eac 100644 --- a/src/Registry/Nlsconfig.php +++ b/src/Registry/Nlsconfig.php @@ -20,32 +20,35 @@ use Horde\Core\LanguageContext; use Horde\Core\Session\SessionAccess; use Horde_Prefs; -use Horde_String; /** - * DI-friendly, PSR-4 replacement for the legacy `Horde_Registry_Nlsconfig`. + * DI-friendly successor to the legacy `Horde_Registry_Nlsconfig`. * * Owns the language cascade (session -> preference -> explicit $lang -> * Accept-Language -> site default) and the resolved $language value - * itself. Unlike its legacy predecessor, it does not read/write - * $GLOBALS and does not require a bootstrapped `Horde_Registry` — it - * only needs a session accessor, the merged `nls.php` config, and - * (optionally) preferences, all constructor-injected. + * itself. * - * `Horde_Registry` is a *consumer* of this service, not the other way - * around: it asks this class to resolve/persist the language, then - * layers its own legacy side effects (setlocale()/putenv(), gettext - * domain reload, `$GLOBALS['language']` mirroring, per-app - * `changeLanguage()` callbacks) on top. See {@see LanguageContext} for - * the accepted gap when this service is used without going through - * `Horde_Registry`. + * Unlike Horde_Registry_Nlsconfig it does not read/write + * $GLOBALS['language'] and does not require a bootstrapped `Horde_Registry`. + * + * `Horde_Registry` is a *consumer* of this service: + * It asks this class to resolve/persist the language, then applies side effects + * - setlocale()/putenv() + * - gettext domain reload + * - `$GLOBALS['language']` mirroring + * - per-app `changeLanguage()` callbacks + * + * When using this service directly without going through registry, side effects are not triggered. + * + * TODO: Optionally wire a PSR-14 event for language changes. + * TODO: Upgrade to a PrefsService rather than the legacy Horde_Prefs. * * @category Horde * @copyright 2026 The Horde Project * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Core */ -final class Nlsconfig implements LanguageContext +final class Nlsconfig implements LanguageContext, LanguageContextSelector { /** Merged `nls.php` config (`horde_nls_config` variable). */ private array $config; @@ -53,8 +56,9 @@ final class Nlsconfig implements LanguageContext private ?string $language = null; public function __construct( - private SessionAccess $session, BackendConfigLoader $configLoader, + private ?string $serverEnvironment = null, + private ?SessionAccess $session = null, private ?Horde_Prefs $prefs = null, ) { $this->config = $configLoader @@ -83,10 +87,10 @@ public function preferredLang(?string $lang = null): string return basename($lang); } - if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + if (!empty($serverEnvironment)) { $partialLang = null; - foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $browserLang) { + foreach (explode(',', $serverEnvironment) as $browserLang) { if (($pos = strpos($browserLang, ';')) !== false) { $browserLang = substr($browserLang, 0, $pos); } @@ -99,8 +103,8 @@ public function preferredLang(?string $lang = null): string /* In case there's no full match, save our best guess. Try * ll_LL, followed by just ll. */ if ($partialLang === null) { - $llLL = Horde_String::lower(substr($mapped, 0, 2)) - . '_' . Horde_String::upper(substr($mapped, 0, 2)); + $llLL = mb_strtolower(substr($mapped, 0, 2)) + . '_' . mb_strtoupper(substr($mapped, 0, 2)); if ($this->validLang($llLL)) { $partialLang = $llLL; } else { @@ -158,9 +162,9 @@ private function mapLang(string $language): string // (e.g. de-DE should match de_DE) $transLang = str_replace('-', '_', $language); $langParts = explode('_', $transLang); - $transLang = Horde_String::lower($langParts[0]); + $transLang = mb_strtolower($langParts[0]); if (isset($langParts[1])) { - $transLang .= '_' . Horde_String::upper($langParts[1]); + $transLang .= '_' . mb_strtoupper($langParts[1]); } return $this->config['aliases'][$transLang] ?? $transLang; From 2d8baf7ac2f3e7d366d7215dbf08143d2bd49b50 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 18 Sep 2026 11:35:27 +0200 Subject: [PATCH 3/3] style: php-cs-fixer --- lib/Horde/Script/File/ThemeDir.php | 8 ++++---- lib/Horde/Session.php | 2 +- lib/Horde/Session/Null.php | 2 +- lib/Horde/Themes/Cache.php | 8 ++++---- src/Factory/AjaxApplicationFactory.php | 1 - src/Factory/PermsUi.php | 14 ++++++++------ src/Perms/Ui.php | 13 +++++++------ src/Session/SessionAccess.php | 3 ++- src/Session/SessionLifecycle.php | 6 ++++-- .../ActiveSync/ConnectorContactsGetGalTest.php | 2 +- test/Unit/Assets/PhpThemeInfoReaderTest.php | 4 ++-- test/Unit/Assets/ThemeJsDiscovererTest.php | 4 ++-- test/Unit/Registry/HasPermissionTest.php | 1 + test/Unit/Session/HordeSessionNullTest.php | 9 +++++---- test/Unit/Session/HordeSessionShimBeginTest.php | 1 + test/Unit/Topbar/TopbarBuilderTest.php | 2 +- 16 files changed, 44 insertions(+), 36 deletions(-) diff --git a/lib/Horde/Script/File/ThemeDir.php b/lib/Horde/Script/File/ThemeDir.php index df672dc4..0b29fca4 100644 --- a/lib/Horde/Script/File/ThemeDir.php +++ b/lib/Horde/Script/File/ThemeDir.php @@ -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') ); } diff --git a/lib/Horde/Session.php b/lib/Horde/Session.php index 2dff3a98..87e81db3 100644 --- a/lib/Horde/Session.php +++ b/lib/Horde/Session.php @@ -254,7 +254,7 @@ private function _resolveAccess(): ?SessionAccess try { $access = $GLOBALS['injector']->get(SessionAccess::class); return $access instanceof SessionAccess ? $access : null; - } catch (\Throwable) { + } catch (Throwable) { return null; } } diff --git a/lib/Horde/Session/Null.php b/lib/Horde/Session/Null.php index 9ee31afc..6b677bf6 100644 --- a/lib/Horde/Session/Null.php +++ b/lib/Horde/Session/Null.php @@ -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. diff --git a/lib/Horde/Themes/Cache.php b/lib/Horde/Themes/Cache.php index bb1d09d4..b35513f0 100644 --- a/lib/Horde/Themes/Cache.php +++ b/lib/Horde/Themes/Cache.php @@ -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)) { diff --git a/src/Factory/AjaxApplicationFactory.php b/src/Factory/AjaxApplicationFactory.php index 570c9845..d349ee3b 100644 --- a/src/Factory/AjaxApplicationFactory.php +++ b/src/Factory/AjaxApplicationFactory.php @@ -101,4 +101,3 @@ public function resolveClass(string $app): ?string return null; } } - diff --git a/src/Factory/PermsUi.php b/src/Factory/PermsUi.php index 960caf83..fd325ad7 100644 --- a/src/Factory/PermsUi.php +++ b/src/Factory/PermsUi.php @@ -29,6 +29,8 @@ use Horde_Registry; use Psr\Http\Message\ServerRequestInterface; use Horde_Core_Factory_Auth; +use Horde_Notification_Handler; +use Throwable; /** * Factory selecting the administrative permission UI implementation. @@ -106,12 +108,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->get('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; } } @@ -137,7 +139,7 @@ private function resolveGroups(Injector $injector): ?Horde_Group try { $groups = $injector->get(Horde_Group::class); return $groups instanceof Horde_Group ? $groups : null; - } catch (\Throwable $e) { + } catch (Throwable $e) { return null; } } @@ -153,7 +155,7 @@ private function resolveAuth(Injector $injector): ?Horde_Auth_Base $factory = $injector->get(Horde_Core_Factory_Auth::class); $auth = $factory->create(); return $auth instanceof Horde_Auth_Base ? $auth : null; - } catch (\Throwable $e) { + } catch (Throwable $e) { return null; } } @@ -180,7 +182,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. } diff --git a/src/Perms/Ui.php b/src/Perms/Ui.php index ddc87583..ceea48f6 100644 --- a/src/Perms/Ui.php +++ b/src/Perms/Ui.php @@ -31,6 +31,8 @@ use Horde_Registry; use Horde_View; use Psr\Http\Message\ServerRequestInterface; +use Horde_Array; +use Throwable; /** * Horde_View based responsive permissions administration UI. @@ -98,8 +100,7 @@ public function __construct( private readonly ?Horde_Notification_Handler $notification = null, private readonly ?Horde_Group $groups = null, private readonly ?Horde_Auth_Base $auth = null - ) { - } + ) {} public function setVars(mixed $vars): void { @@ -136,7 +137,7 @@ public function renderTree(int|string|null $current = Horde_Perms::ROOT): void // externally visible: extraRight and the tree id are the same // opaque token, and the action links use the unprefixed id // for the URL query params. - $nodeId = static fn (int|string $rawId): string => 'p:' . $rawId; + $nodeId = static fn(int|string $rawId): string => 'p:' . $rawId; foreach ($nodes as $perm_id => $node) { $params = []; @@ -178,7 +179,7 @@ public function renderTree(int|string|null $current = Horde_Perms::ROOT): void $links = []; if (isset($app_perms['tree']) - && is_array(\Horde_Array::getElement($app_perms['tree'], $parents))) { + && is_array(Horde_Array::getElement($app_perms['tree'], $parents))) { $links[] = $this->treeAddLink((string) $perm_id); } $links[] = $this->treeEditLink((string) $perm_id); @@ -827,7 +828,7 @@ private function fetchUserList(): array } try { return $this->auth->listNames(); - } catch (\Throwable $e) { + } catch (Throwable $e) { return []; } } @@ -839,7 +840,7 @@ private function fetchGroupList(): array } try { return $this->groups->listAll(); - } catch (\Throwable $e) { + } catch (Throwable $e) { return []; } } diff --git a/src/Session/SessionAccess.php b/src/Session/SessionAccess.php index 21337322..5cc4114f 100644 --- a/src/Session/SessionAccess.php +++ b/src/Session/SessionAccess.php @@ -14,6 +14,7 @@ namespace Horde\Core\Session; use DateTimeImmutable; +use LogicException; /** * Access to the request's current {@see HordeSession} value. @@ -65,7 +66,7 @@ interface SessionAccess * `SessionLifecycle::clean()`, or `SessionLifecycle::destroy()` — * ask again by calling this method afresh. * - * @throws \LogicException When no session has been established + * @throws LogicException When no session has been established * yet. Guard with {@see hasCurrent()} at * call sites that legitimately run before * session setup. diff --git a/src/Session/SessionLifecycle.php b/src/Session/SessionLifecycle.php index cabe8ef2..db91a70b 100644 --- a/src/Session/SessionLifecycle.php +++ b/src/Session/SessionLifecycle.php @@ -24,6 +24,8 @@ use Horde_Exception; use Horde_Shutdown; use Horde_Shutdown_Task; +use RuntimeException; +use Throwable; /** * Orchestrates the lifecycle of the current PHP session under Horde control. @@ -464,7 +466,7 @@ private function mintSuccessor(HordeSession $old): HordeSession // HordeSessionFactory whose restore() actually returns a // HordeSession, but assert the type for the type checker. if (!$successor instanceof HordeSession) { - throw new \RuntimeException( + throw new RuntimeException( 'HordeSessionFactory::restore() returned ' . get_debug_type($successor) . '; expected HordeSession.', @@ -592,7 +594,7 @@ private function accessor(): SessionAccessor $resolved = null; try { $resolved = $this->injector->get(SessionAccess::class); - } catch (\Throwable) { + } catch (Throwable) { // No SessionAccess binding — legacy test contexts and // partial DI setups build a SessionLifecycle without // wiring the accessor. Fall through to the private diff --git a/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php b/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php index fad0d086..8143edce 100644 --- a/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php +++ b/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php @@ -52,7 +52,7 @@ public function testContactsGetGalReturnsFalseWhenMethodMissing(): void public function testContactsGetGalReturnsUidWhenAvailable(): void { - $contacts = new class () { + $contacts = new class { public function getGalUid() { return 'gal-source'; diff --git a/test/Unit/Assets/PhpThemeInfoReaderTest.php b/test/Unit/Assets/PhpThemeInfoReaderTest.php index 94f54294..578e410e 100644 --- a/test/Unit/Assets/PhpThemeInfoReaderTest.php +++ b/test/Unit/Assets/PhpThemeInfoReaderTest.php @@ -91,7 +91,7 @@ public function missingInfoFileYieldsEmpty(): void { // Theme dir exists but no info.php. $dir = $this->root . '/horde/silver'; - mkdir($dir, 0777, true); + mkdir($dir, 0o777, true); $reader = new PhpThemeInfoReader($this->pathBuilder, $this->filesystem); @@ -125,7 +125,7 @@ public function resolvesPerApp(): void private function writeTheme(string $app, string $theme, string $info, array $scriptFiles): void { $dir = $this->root . '/' . $app . '/' . $theme; - mkdir($dir, 0777, true); + mkdir($dir, 0o777, true); file_put_contents($dir . '/info.php', $info); foreach ($scriptFiles as $file) { file_put_contents($dir . '/' . $file, '// js'); diff --git a/test/Unit/Assets/ThemeJsDiscovererTest.php b/test/Unit/Assets/ThemeJsDiscovererTest.php index ab26ac66..860ad33c 100644 --- a/test/Unit/Assets/ThemeJsDiscovererTest.php +++ b/test/Unit/Assets/ThemeJsDiscovererTest.php @@ -29,7 +29,7 @@ class ThemeJsDiscovererTest extends TestCase protected function setUp(): void { $this->root = sys_get_temp_dir() . '/horde-theme-js-' . uniqid('', true); - mkdir($this->root, 0777, true); + mkdir($this->root, 0o777, true); $this->pathBuilder = $this->createPathMock($this->root); $this->uriBuilder = $this->createUriMock(); $this->filesystem = new LocalAssetFilesystem(); @@ -164,7 +164,7 @@ private function createFile(string $relative): void $full = $this->root . '/' . $relative; $dir = dirname($full); if (!is_dir($dir)) { - mkdir($dir, 0777, true); + mkdir($dir, 0o777, true); } file_put_contents($full, '// js'); } diff --git a/test/Unit/Registry/HasPermissionTest.php b/test/Unit/Registry/HasPermissionTest.php index 6b645705..09d2be3d 100644 --- a/test/Unit/Registry/HasPermissionTest.php +++ b/test/Unit/Registry/HasPermissionTest.php @@ -31,6 +31,7 @@ * * Guards against regressions where SHOW (menu-appearance) checks trigger * per-app transparent authentication (e.g. futile IMAP logins for IMP). + * @coversNothing */ class HasPermissionTest extends TestCase { diff --git a/test/Unit/Session/HordeSessionNullTest.php b/test/Unit/Session/HordeSessionNullTest.php index c5382ced..15b53935 100644 --- a/test/Unit/Session/HordeSessionNullTest.php +++ b/test/Unit/Session/HordeSessionNullTest.php @@ -21,6 +21,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use ReflectionMethod; /** * Regression coverage for horde/Core#207. @@ -102,7 +103,7 @@ public function testSetupPublishesInMemorySessionToAccessor(): void // _start(), which is what setup() eventually calls; invoke it // directly to isolate the publish step from PHP's session // machinery. - (new \ReflectionMethod($null, '_start'))->invoke($null); + (new ReflectionMethod($null, '_start'))->invoke($null); self::assertTrue( $accessor->hasCurrent(), @@ -124,7 +125,7 @@ public function testPublishedSessionIsAlsoRegisteredAsSharedSingleton(): void $GLOBALS['injector'] = $injector; $null = new Horde_Session_Null(); - (new \ReflectionMethod($null, '_start'))->invoke($null); + (new ReflectionMethod($null, '_start'))->invoke($null); // Consumers that resolve HordeSession directly (not through the // accessor) must reach the same instance the accessor points at, @@ -154,7 +155,7 @@ public function testScopedWritesFlowThroughAccessor(): void $GLOBALS['injector'] = $injector; $null = new Horde_Session_Null(); - (new \ReflectionMethod($null, '_start'))->invoke($null); + (new ReflectionMethod($null, '_start'))->invoke($null); $accessor->setScoped('horde', 'nls/curr_default', 'de_DE'); self::assertTrue($accessor->hasScoped('horde', 'nls/curr_default')); @@ -189,7 +190,7 @@ public function testStartDoesNotMarkPersistedSessionActive(): void } $null = new Horde_Session_Null(); - (new \ReflectionMethod($null, '_start'))->invoke($null); + (new ReflectionMethod($null, '_start'))->invoke($null); self::assertNotSame( PHP_SESSION_ACTIVE, diff --git a/test/Unit/Session/HordeSessionShimBeginTest.php b/test/Unit/Session/HordeSessionShimBeginTest.php index 84d11d58..8d102f50 100644 --- a/test/Unit/Session/HordeSessionShimBeginTest.php +++ b/test/Unit/Session/HordeSessionShimBeginTest.php @@ -30,6 +30,7 @@ * collapsed to 0 once `_active` was false, max_time + 0 compares less * than the current timestamp for any non-zero max_time, incorrectly * failing auth for every closed, read-only request. See horde/imp#88. + * @coversNothing */ class HordeSessionShimBeginTest extends TestCase { diff --git a/test/Unit/Topbar/TopbarBuilderTest.php b/test/Unit/Topbar/TopbarBuilderTest.php index ac31f522..07e7d53c 100644 --- a/test/Unit/Topbar/TopbarBuilderTest.php +++ b/test/Unit/Topbar/TopbarBuilderTest.php @@ -347,7 +347,7 @@ public function testResolvesAppNameViaOwningAppDomain(): void * another test that bound the same domain to a different directory. */ $domain = 'fixtureapp' . substr(md5($dir), 0, 8); $moDir = $dir . '/locale/' . $lang . '/LC_MESSAGES'; - mkdir($moDir, 0777, true); + mkdir($moDir, 0o777, true); self::writeMo($moDir . '/' . $domain . '.mo', [ '' => "Content-Type: text/plain; charset=UTF-8\n", 'Mail' => 'FIXTURE-MAIL',