diff --git a/_build/test/Tests/Model/modMenuTest.php b/_build/test/Tests/Model/modMenuTest.php new file mode 100644 index 00000000000..661e2d9b819 --- /dev/null +++ b/_build/test/Tests/Model/modMenuTest.php @@ -0,0 +1,107 @@ +modx->setOption('cultureKey', 'en'); + $this->modx->getCacheManager(); + + /** @var modMenu $menu */ + $menu = $this->modx->newObject(modMenu::class); + $menus = $menu->rebuildCache('topnav'); + $this->assertIsArray($menus); + $this->assertSame([], $sessionWarnings); + } finally { + restore_error_handler(); + if ($hadSession) { + $_SESSION = $previousSession; + } else { + unset($GLOBALS['_SESSION']); + } + } + } + + /** + * clearCache (used by save/remove) must wipe language shards without $_SESSION. + */ + public function testClearCacheWithoutSessionRemovesMenuKeys() + { + $hadSession = array_key_exists('_SESSION', $GLOBALS); + $previousSession = $hadSession ? $_SESSION : null; + unset($GLOBALS['_SESSION']); + + $this->modx->setOption('cultureKey', 'en'); + $cacheManager = $this->modx->getCacheManager(); + $cacheOptions = [ + xPDO::OPT_CACHE_KEY => $this->modx->getOption('cache_menu_key', null, 'menu'), + xPDO::OPT_CACHE_HANDLER => $this->modx->getOption( + 'cache_menu_handler', + null, + $this->modx->getOption(xPDO::OPT_CACHE_HANDLER) + ), + ]; + + $cacheKey = 'menus/topnav/en'; + $probe = ['probe' => true]; + $cacheManager->set($cacheKey, $probe, 0, $cacheOptions); + $this->assertSame(['probe' => true], $cacheManager->get($cacheKey, $cacheOptions)); + + try { + /** @var modMenu $menu */ + $menu = $this->modx->newObject(modMenu::class); + $clearCache = new \ReflectionMethod(modMenu::class, 'clearCache'); + $clearCache->setAccessible(true); + $clearCache->invoke($menu); + $this->assertNull($cacheManager->get($cacheKey, $cacheOptions)); + } finally { + if ($hadSession) { + $_SESSION = $previousSession; + } else { + unset($GLOBALS['_SESSION']); + } + } + } +} diff --git a/_build/test/Tests/Model/modXTest.php b/_build/test/Tests/Model/modXTest.php index 50bcc81f80c..be87acc2d5a 100644 --- a/_build/test/Tests/Model/modXTest.php +++ b/_build/test/Tests/Model/modXTest.php @@ -118,6 +118,55 @@ public function testGetCacheManager() { $this->assertInstanceOf(modCacheManager::class,$this->modx->cacheManager, "Failed to load a modCacheManager instance"); } + /** + * getManagerLanguage must not require $_SESSION and must honor session when present. + */ + public function testGetManagerLanguageWithoutSession() + { + $this->withSessionFixture(null, function () { + $this->modx->setOption('cultureKey', 'de'); + $this->modx->setOption('manager_language', null); + $this->assertSame('de', $this->modx->getManagerLanguage()); + }); + } + + /** + * @depends testGetManagerLanguageWithoutSession + */ + public function testGetManagerLanguagePrefersSessionValue() + { + $this->withSessionFixture(['manager_language' => 'fr'], function () { + $this->modx->setOption('cultureKey', 'de'); + $this->assertSame('fr', $this->modx->getManagerLanguage()); + }); + } + + /** + * @param array|null $session null unsets $_SESSION; array replaces it + * @param callable $callback + */ + private function withSessionFixture($session, callable $callback) + { + $hadSession = array_key_exists('_SESSION', $GLOBALS); + $previousSession = $hadSession ? $_SESSION : null; + + if ($session === null) { + unset($GLOBALS['_SESSION']); + } else { + $_SESSION = $session; + } + + try { + $callback(); + } finally { + if ($hadSession) { + $_SESSION = $previousSession; + } else { + unset($GLOBALS['_SESSION']); + } + } + } + /** * @param string $expected * @param string $string diff --git a/_build/test/phpunit.xml b/_build/test/phpunit.xml index 6eba756c6c6..dc713db0142 100644 --- a/_build/test/phpunit.xml +++ b/_build/test/phpunit.xml @@ -20,6 +20,7 @@ Tests/Model/modXTest.php Tests/Model/modXLoggingTest.php Tests/Model/modParserTest.php + Tests/Model/modMenuTest.php Tests/Model/Dashboard Tests/Model/Element Tests/Model/Error diff --git a/core/src/Revolution/Processors/System/Country/GetList.php b/core/src/Revolution/Processors/System/Country/GetList.php index 3fd0dfe12e2..cc83b79c7c3 100644 --- a/core/src/Revolution/Processors/System/Country/GetList.php +++ b/core/src/Revolution/Processors/System/Country/GetList.php @@ -52,7 +52,7 @@ public function getCountryList() { $_country_lang = []; include $this->modx->getOption('core_path') . 'lexicon/country/en.inc.php'; - $ml = $this->modx->getOption('manager_language', $_SESSION, $this->modx->getOption('cultureKey', null, 'en')); + $ml = $this->modx->getManagerLanguage(); if ($ml !== 'en' && file_exists($this->modx->getOption('core_path') . 'lexicon/country/' . $ml . '.inc.php')) { include $this->modx->getOption('core_path') . 'lexicon/country/' . $ml . '.inc.php'; } diff --git a/core/src/Revolution/Transport/modTransportProvider.php b/core/src/Revolution/Transport/modTransportProvider.php index 9108acb67e7..abc75fbfa15 100644 --- a/core/src/Revolution/Transport/modTransportProvider.php +++ b/core/src/Revolution/Transport/modTransportProvider.php @@ -474,8 +474,7 @@ protected function args(array $args = []) 'supports' => $this->xpdo->version['code_name'] . '-' . $this->xpdo->version['full_version'], 'http_host' => $this->xpdo->getOption('http_host'), 'php_version' => PHP_VERSION, - 'language' => $this->xpdo->getOption('manager_language', $_SESSION, - $this->xpdo->getOption('cultureKey', null, 'en')), + 'language' => $this->xpdo->getManagerLanguage(), ]; return array_merge($baseArgs, $args); diff --git a/core/src/Revolution/modConnectorRequest.php b/core/src/Revolution/modConnectorRequest.php index e8dda2d94e1..31c6399b162 100644 --- a/core/src/Revolution/modConnectorRequest.php +++ b/core/src/Revolution/modConnectorRequest.php @@ -38,8 +38,7 @@ public function initialize() if ($this->modx && is_object($this->modx->context) && $this->modx->context instanceof modContext) { $ctx = $this->modx->context->get('key'); if (!empty($ctx) && $ctx == 'mgr') { - $ml = $this->modx->getOption('manager_language', $_SESSION ?? [], - $this->modx->getOption('cultureKey', null, 'en')); + $ml = $this->modx->getManagerLanguage(); if (!empty($ml)) { $this->modx->setOption('cultureKey', $ml); } diff --git a/core/src/Revolution/modLexicon.php b/core/src/Revolution/modLexicon.php index 78bf57ff397..0b8a026f622 100644 --- a/core/src/Revolution/modLexicon.php +++ b/core/src/Revolution/modLexicon.php @@ -194,7 +194,7 @@ public function load() $topics = func_get_args(); /* allow for dynamic number of lexicons to load */ if ($this->modx->context && $this->modx->context->get('key') == 'mgr') { - $defaultLanguage = $this->modx->getOption('manager_language', $_SESSION ?? [], $this->modx->getOption('cultureKey', null, 'en')); + $defaultLanguage = $this->modx->getManagerLanguage(); } else { $defaultLanguage = $this->modx->getOption('cultureKey', null, 'en'); } diff --git a/core/src/Revolution/modManagerRequest.php b/core/src/Revolution/modManagerRequest.php index cb1ac5e0ab2..44718460129 100644 --- a/core/src/Revolution/modManagerRequest.php +++ b/core/src/Revolution/modManagerRequest.php @@ -104,7 +104,7 @@ public function initialize() $this->modx->getVersionData(); } - $ml = $this->modx->getOption('manager_language', $_SESSION, $this->modx->getOption('cultureKey', null, 'en')); + $ml = $this->modx->getManagerLanguage(); if (!empty($ml)) { $this->modx->setOption('cultureKey', $ml); } diff --git a/core/src/Revolution/modMenu.php b/core/src/Revolution/modMenu.php index bb4b50b7345..21be7870b46 100644 --- a/core/src/Revolution/modMenu.php +++ b/core/src/Revolution/modMenu.php @@ -24,7 +24,7 @@ class modMenu extends modAccessibleObject { /** - * Overrides xPDOObject::save to cache the menus. + * Overrides xPDOObject::save to invalidate the menu cache. * * {@inheritdoc} */ @@ -32,14 +32,14 @@ public function save($cacheFlag = null) { $saved = parent::save($cacheFlag); if ($saved && empty($this->xpdo->config[xPDO::OPT_SETUP])) { - $this->rebuildCache(); + $this->clearCache(); } return $saved; } /** - * Overrides xPDOObject::remove to cache the menus. + * Overrides xPDOObject::remove to invalidate the menu cache. * * {@inheritdoc} */ @@ -47,12 +47,29 @@ public function remove(array $ancestors = []) { $removed = parent::remove($ancestors); if ($removed && empty($this->xpdo->config[xPDO::OPT_SETUP])) { - $this->rebuildCache(); + $this->clearCache(); } return $removed; } + /** + * Clear all language shards of the menu cache partition without firing OnCacheUpdate. + * + * Menu processors still call refresh() once so plugins see a single cache event. + */ + protected function clearCache() + { + $this->xpdo->getCacheManager()->clean([ + xPDO::OPT_CACHE_KEY => $this->xpdo->getOption('cache_menu_key', null, 'menu'), + xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption( + 'cache_menu_handler', + null, + $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER) + ), + ]); + } + /** * Rebuilds the menu map cache. * @@ -66,8 +83,7 @@ public function rebuildCache($start = '') if ($start !== '') { $cacheKey .= "{$start}/"; } - $cacheKey .= $this->xpdo->getOption('manager_language', $_SESSION, - $this->xpdo->getOption('cultureKey', null, 'en')); + $cacheKey .= $this->xpdo->getManagerLanguage(); $menus = $this->getSubMenus($start); $cached = $this->xpdo->cacheManager->set($cacheKey, $menus, 0, [ xPDO::OPT_CACHE_KEY => $this->xpdo->cacheManager->getOption('cache_menu_key', null, 'menu'), @@ -147,45 +163,29 @@ public function getSubMenus($start = '') $list = []; /** @var modMenu $menu */ foreach ($menus as $menu) { + $textKey = $menu->get('text'); $ma = $menu->toArray(); - $ma['id'] = $menu->get('text'); - $action = $menu->get('action'); + $ma['id'] = $textKey; $namespace = $menu->get('namespace'); if ($namespace !== 'core') { $this->xpdo->lexicon->load($namespace . ':default'); } - /* if 3rd party menu item, load proper text */ - if (!empty($action)) { - if (!empty($namespace) && $namespace !== 'core') { - $ma['text'] = $menu->get('text') === 'user' - ? $this->xpdo->lexicon($menu->get('text'), ['username' => $this->xpdo->getLoginUserName()]) - : $this->xpdo->lexicon($menu->get('text')); - } else { - $ma['text'] = $menu->get('text') === 'user' - ? $this->xpdo->lexicon($menu->get('text'), ['username' => $this->xpdo->getLoginUserName()]) - : $this->xpdo->lexicon($menu->get('text')); - } - } else { - $ma['text'] = $menu->get('text') === 'user' - ? $this->xpdo->lexicon($menu->get('text'), ['username' => $this->xpdo->getLoginUserName()]) - : $this->xpdo->lexicon($menu->get('text')); - } + $ma['text'] = $this->xpdo->lexicon( + $textKey, + $textKey === 'user' ? ['username' => $this->xpdo->getLoginUserName()] : [] + ); $desc = $menu->get('description'); $ma['description'] = !empty($desc) ? $this->xpdo->lexicon($desc) : ''; - $ma['children'] = $menu->get('text') != '' ? $this->getSubMenus($menu->get('text')) : []; + $ma['children'] = $textKey != '' ? $this->getSubMenus($textKey) : []; - if ($ma['id'] === 'language') { + if ($textKey === 'language') { $ma['children'] = $this->getLanguageMenu(); } - if ($menu->get('controller')) { - $ma['controller'] = $menu->get('controller'); - } else { - $ma['controller'] = ''; - } + $ma['controller'] = $menu->get('controller') ?: ''; $list[] = $ma; } unset($menu, $desc, $namespace, $ma); diff --git a/core/src/Revolution/modX.php b/core/src/Revolution/modX.php index c2307d0f393..84f57ab44a2 100644 --- a/core/src/Revolution/modX.php +++ b/core/src/Revolution/modX.php @@ -2285,6 +2285,23 @@ public function getSessionState() { return $this->_sessionState; } + /** + * Resolve the manager UI language without requiring $_SESSION. + * + * Looks up session `manager_language`, then config `manager_language`, + * then cultureKey, then en. + * + * @return string + */ + public function getManagerLanguage() + { + return (string) $this->getOption( + 'manager_language', + $_SESSION ?? [], + $this->getOption('cultureKey', null, 'en') + ); + } + /** * Executed before parser processing of an element. */ diff --git a/manager/controllers/default/header.php b/manager/controllers/default/header.php index 43e4ead5f11..9cc27fe574a 100644 --- a/manager/controllers/default/header.php +++ b/manager/controllers/default/header.php @@ -258,9 +258,7 @@ protected function getCache($name) */ protected function getCacheKey($name) { - $ml = $this->modx->getOption('manager_language', $_SESSION, $this->modx->getOption('cultureKey', null, 'en')); - - return "menus/{$name}/" . $ml; + return "menus/{$name}/" . $this->modx->getManagerLanguage(); } /** diff --git a/manager/controllers/default/security/login.class.php b/manager/controllers/default/security/login.class.php index 1b0f6cbb1af..7a086b5e378 100644 --- a/manager/controllers/default/security/login.class.php +++ b/manager/controllers/default/security/login.class.php @@ -205,7 +205,7 @@ public function handleLanguageChange() $ml = $this->modx->sanitizeString($this->modx->getOption('manager_language', $_REQUEST)); if (!$ml || !in_array($ml, $languages)) { - $ml = $this->modx->getOption('manager_language', $_SESSION); + $ml = $this->modx->getOption('manager_language', $_SESSION ?? []); if (!$ml) { // Try to detect default browser language $accept_languages = strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']);