diff --git a/assets/modules/store/js/store.js b/assets/modules/store/js/store.js index eada6f561d..baf0d03f52 100755 --- a/assets/modules/store/js/store.js +++ b/assets/modules/store/js/store.js @@ -1555,7 +1555,7 @@ store = { } }); }, - refreshManagerUiAfterPermissionSync: function(){ + refreshManagerUiAfterPermissionSync: function(delay){ try { if (window.top && window.top.mainMenu && typeof window.top.mainMenu.reloadtree === 'function') { window.top.mainMenu.reloadtree(); @@ -1566,7 +1566,7 @@ store = { if (window.top && window.top.location) { setTimeout(function(){ window.top.location.reload(); - }, 700); + }, 700 + (parseInt(delay || 0, 10) || 0)); } } catch (e) {} }, @@ -1916,13 +1916,16 @@ store = { if ( task && task.status === 'succeeded' - && $.inArray(task.type, ['console_install', 'console_uninstall']) >= 0 + && $.inArray(task.type, ['console_install', 'console_uninstall', 'site_update']) >= 0 && store.systemTaskRefreshPermissionsTaskId !== parseInt(task.id || 0, 10) ) { store.systemTaskRefreshPermissionsTaskId = parseInt(task.id || 0, 10); + var reloadAlways = task.type === 'site_update'; store.refreshManagerPermissions(function(response){ - if (response && response.ok) { - store.refreshManagerUiAfterPermissionSync(); + // After a core update the whole manager must reload so the + // session and menu pick up the new version. + if (reloadAlways || (response && response.ok)) { + store.refreshManagerUiAfterPermissionSync(reloadAlways ? 2000 : 0); } }); } diff --git a/assets/plugins/updater/plugin.updater.php b/assets/plugins/updater/plugin.updater.php index 7026a09773..d1eb0d966d 100644 --- a/assets/plugins/updater/plugin.updater.php +++ b/assets/plugins/updater/plugin.updater.php @@ -325,7 +325,7 @@ function updaterHandleSystemTaskRequest() ]); } - $requiresToken = in_array($action, ['create', 'cancel'], true); + $requiresToken = in_array($action, ['create', 'cancel', 'refresh_session'], true); if ($requiresToken) { $token = isset($_REQUEST['updater_task_token']) ? (string)$_REQUEST['updater_task_token'] : ''; if ($token === '' || !hash_equals(updaterEnsureSystemTaskToken(), $token)) { @@ -394,6 +394,9 @@ function updaterHandleSystemTaskRequest() $isSuperAdmin )); + case 'refresh_session': + updaterJsonResponse($context->refreshCurrentManagerPermissions()); + case 'cancel': updaterJsonResponse($taskService->cancelQueuedTaskPayload( isset($_REQUEST['task_id']) ? (int)$_REQUEST['task_id'] : 0, @@ -534,7 +537,15 @@ function close() { } if (reloadOnClose) { - window.location.reload(); + var target = window; + try { + if (window.top && window.top.location && window.top !== window) { + target = window.top; + } + } catch (error) { + target = window; + } + target.location.reload(); } } @@ -640,6 +651,9 @@ function renderTask(task, result) { } if (isSucceeded) { + if (!reloadOnClose) { + request('refresh_session').catch(function () {}); + } reloadOnClose = true; } diff --git a/core/src/Console/SiteUpdateCommand.php b/core/src/Console/SiteUpdateCommand.php index 1e39c1d830..48876a6fbf 100644 --- a/core/src/Console/SiteUpdateCommand.php +++ b/core/src/Console/SiteUpdateCommand.php @@ -2,6 +2,7 @@ use EvolutionCMS\Models\Category; use EvolutionCMS\Models\SiteModule; +use EvolutionCMS\Models\SystemSetting; use EvolutionCMS\Services\ComposerVersionSynchronizer; use EvolutionCMS\Services\Store\RemoteTransportService; use Illuminate\Console\Command; @@ -229,6 +230,7 @@ public function startUpdate() $this->runCoreMigrations(); $this->runUpdateSeeders(); $this->updateBundledExtrasModule(); + $this->syncSettingsVersion(); $this->line('Remove Install Directory'); self::rmdirs(EVO_BASE_PATH . 'install'); @@ -306,6 +308,69 @@ protected function runUpdateSeeders(): void } } + /** + * Persist the installed version and drop the settings cache. + * + * The manager menu and plugins read settings_version, which only a manual + * settings save used to refresh, so the old version stayed visible after an update. + * + * @since 3.5.8 + * @return void + */ + protected function syncSettingsVersion(): void + { + $version = $this->readInstalledVersion(); + if ($version === '') { + return; + } + + SystemSetting::query()->updateOrCreate( + ['setting_name' => 'settings_version'], + ['setting_value' => $version] + ); + + foreach (['siteCache.idx.php', 'sitePublishing.idx.php'] as $file) { + $path = $this->bootstrapCachePath() . $file; + if (is_file($path)) { + unlink($path); + } + } + + $this->line('Settings version set to ' . $version . ''); + } + + /** + * Read the version from the freshly overlaid factory/version.php. + * + * @since 3.5.8 + * @return string + */ + protected function readInstalledVersion(): string + { + $file = EVO_CORE_PATH . 'factory/version.php'; + if (!is_file($file)) { + return ''; + } + if (function_exists('opcache_invalidate')) { + @opcache_invalidate($file, true); + } + + $version = include $file; + + return is_array($version) ? trim((string) ($version['version'] ?? '')) : ''; + } + + /** + * Directory holding the compiled settings cache. + * + * @since 3.5.8 + * @return string + */ + protected function bootstrapCachePath(): string + { + return rtrim(evo()->bootstrapPath(), '/\\') . '/'; + } + /** * Remove placeholder files and root distribution artifacts from the update archive. * diff --git a/core/tests/Feature/SiteUpdateE2ETest.php b/core/tests/Feature/SiteUpdateE2ETest.php index c5d737ab30..fcadada249 100644 --- a/core/tests/Feature/SiteUpdateE2ETest.php +++ b/core/tests/Feature/SiteUpdateE2ETest.php @@ -13,6 +13,7 @@ | - the core-only migration that creates the system task tables | - SiteUpdateCommand::runUpdateSeeders() (the install update seeders) | - SiteUpdateCommand::updateBundledExtrasModule() +| - SiteUpdateCommand::syncSettingsVersion() (settings_version + settings cache) | | and asserts the resulting schema/data deltas. The file-replacement mechanics | (download/extract/move) are covered separately via the static helpers, since @@ -107,6 +108,15 @@ function seedVersionNDatabase(Capsule $capsule): void $table->string('category')->nullable(); $table->integer('rank')->default(0); }); + // settings_version is what the manager menu shows; version N left it stale. + $schema->create('system_settings', function (Blueprint $table) { + $table->string('setting_name')->primary(); + $table->text('setting_value')->nullable(); + }); + $capsule->getConnection()->table('system_settings')->insert([ + 'setting_name' => 'settings_version', + 'setting_value' => '0.0.0', + ]); $schema->create('site_modules', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); @@ -175,13 +185,19 @@ function seedVersionNDatabase(Capsule $capsule): void /** * Run the database-affecting steps of an update, exactly as the update command does. */ -function runSiteUpdateDatabaseSteps(): void +function runSiteUpdateDatabaseSteps(string $bootstrapDir): void { // 1. Core-only migration that ships the system task tables and permissions. (new \CreateSystemCliTasksTables())->up(); - // 2 + 3. The real update command steps, with console output suppressed. - $command = new class extends \EvolutionCMS\Console\SiteUpdateCommand { + // 2-4. The real update command steps, with console output suppressed and the + // settings cache pointed at a scratch directory instead of the live storage path. + $command = new class($bootstrapDir) extends \EvolutionCMS\Console\SiteUpdateCommand { + public function __construct(private string $bootstrapDir) + { + parent::__construct(); + } + public function line($string, $style = null, $verbosity = null) { // Suppress: there is no console output bound in the test harness. @@ -196,10 +212,21 @@ public function applyExtrasModule(): void { $this->updateBundledExtrasModule(); } + + public function applySettingsVersion(): void + { + $this->syncSettingsVersion(); + } + + protected function bootstrapCachePath(): string + { + return $this->bootstrapDir; + } }; $command->applyUpdateSeeders(); $command->applyExtrasModule(); + $command->applySettingsVersion(); } test('update from version N to N+1 applies migrations, update seeders and refreshes Extras', function () { @@ -211,7 +238,13 @@ public function applyExtrasModule(): void expect($db->getSchemaBuilder()->hasTable('system_cli_tasks'))->toBeFalse() ->and($db->table('permissions')->where('key', 'logout')->exists())->toBeTrue(); - runSiteUpdateDatabaseSteps(); + // A stale settings cache from version N that must be dropped so the DB value is read. + $bootstrapDir = sys_get_temp_dir() . '/evo_update_cache_' . uniqid() . '/'; + mkdir($bootstrapDir, 0777, true); + file_put_contents($bootstrapDir . 'siteCache.idx.php', 'getSchemaBuilder()->hasTable('system_cli_tasks'))->toBeTrue() @@ -238,6 +271,16 @@ public function applyExtrasModule(): void expect($extras->description)->toContain('0.2.0') ->and($extras->modulecode)->toContain('store/core.php') ->and($extras->modulecode)->not->toBe('OUTDATED MODULE CODE'); + + // settings_version now matches the installed core and the stale settings cache is gone, + // so the manager menu shows the new version without a settings save or re-login. + $installed = include EVO_CORE_PATH . 'factory/version.php'; + expect($db->table('system_settings')->where('setting_name', 'settings_version')->value('setting_value')) + ->toBe($installed['version']) + ->and(is_file($bootstrapDir . 'siteCache.idx.php'))->toBeFalse() + ->and(is_file($bootstrapDir . 'sitePublishing.idx.php'))->toBeFalse(); + + rmdir($bootstrapDir); }); test('moveFiles replaces files into the destination tree', function () { diff --git a/core/tests/Unit/UpdaterManagerUiTest.php b/core/tests/Unit/UpdaterManagerUiTest.php index 95ead67af9..0649143463 100644 --- a/core/tests/Unit/UpdaterManagerUiTest.php +++ b/core/tests/Unit/UpdaterManagerUiTest.php @@ -8,7 +8,13 @@ expect($source) ->toContain('reloadOnClose') - ->toContain('window.location.reload();') + // Reload the whole manager so the top menu shows the new version, and + // rebuild session permissions before that reload. + ->toContain('target = window.top;') + ->toContain('target.location.reload();') + ->toContain("request('refresh_session')") + ->toContain("case 'refresh_session':") + ->toContain("['create', 'cancel', 'refresh_session']") ->toContain('updater_live_update_close_reload') ->toContain('updater_live_update_response_changed') ->toContain('normalized.substring(firstJsonChar, lastJsonChar + 1)') diff --git a/install/cli-install.php b/install/cli-install.php index 4bd63c2294..b4bc97c206 100644 --- a/install/cli-install.php +++ b/install/cli-install.php @@ -174,10 +174,20 @@ public function update() bootstrapInstallMigrationHistory($installMigrationsPath); Console::call('migrate', ['--path' => $installMigrationsPath, '--realpath' => true, '--force' => true]); seed('update'); + // The manager menu reads settings_version; keep it on the installed version. + \EvolutionCMS\Models\SystemSetting::query()->updateOrCreate( + ['setting_name' => 'settings_version'], + ['setting_value' => (string) evo()->getVersionData('version')] + ); // Apply core-only migrations (core/database/migrations) not present in the // install/stubs chain, e.g. the system task tables. Runs after seeding so the // guarded ACL repairs inside those migrations detect a healthy baseline. Console::call('migrate', ['--force' => true]); + foreach ([evo()->getSiteCacheFilePath(), evo()->getSitePublishingFilePath()] as $cacheFile) { + if (is_file($cacheFile)) { + unlink($cacheFile); + } + } echo 'Evolution CMS updated!' . "\n"; $this->checkRemoveInstall(); $this->removeInstall(); diff --git a/install/src/controllers/install.php b/install/src/controllers/install.php index e261952465..3301e54e6d 100644 --- a/install/src/controllers/install.php +++ b/install/src/controllers/install.php @@ -237,6 +237,11 @@ \EvolutionCMS\Models\SystemSetting::insert($systemSettings); } else { seed('update'); + // The manager menu reads settings_version; keep it on the installed version. + \EvolutionCMS\Models\SystemSetting::query()->updateOrCreate( + ['setting_name' => 'settings_version'], + ['setting_value' => (string) evo()->getVersionData('version')] + ); } // Apply core-only migrations (core/database/migrations) that are not part