From 25555456449d6e25af8cd0c37e938aa6960b2096 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 25 Feb 2026 17:03:39 +0100 Subject: [PATCH] refactor(updater): Move updater to a Controller Signed-off-by: Carl Schwan --- core/Controller/UpdateController.php | 170 ++++++++++++++++++++ core/ajax/update.php | 151 ----------------- core/routes.php | 5 - core/src/views/UpdaterAdmin.vue | 4 +- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + ocs/v1.php | 24 +-- 7 files changed, 187 insertions(+), 169 deletions(-) create mode 100644 core/Controller/UpdateController.php delete mode 100644 core/ajax/update.php diff --git a/core/Controller/UpdateController.php b/core/Controller/UpdateController.php new file mode 100644 index 0000000000000..32f2c66ebdd4e --- /dev/null +++ b/core/Controller/UpdateController.php @@ -0,0 +1,170 @@ +eventSourceFactory->create(); + // need to send an initial message to force-init the event source, + // which will then trigger its own CSRF check and produces its own CSRF error + // message + $eventSource->send('success', $this->l->t('Preparing update')); + if (!Util::needUpgrade()) { + $eventSource->send('notice', $this->l->t('Already up to date')); + $eventSource->send('done', ''); + $eventSource->close(); + return new DataResponse([]); + } + + if ($this->config->getSystemValueBool('upgrade.disable-web', false)) { + $eventSource->send('failure', $this->l->t('Please use the command line updater because updating via browser is disabled in your config.php.')); + $eventSource->close(); + return new DataResponse([]); + } + + // if a user is currently logged in, their session must be ignored to + // avoid side effects + \OC_User::setIncognitoMode(true); + + $incompatibleApps = []; + $incompatibleOverwrites = $this->config->getSystemValue('app_install_overwrite', []); + + $this->dispatcher->addListener( + MigratorExecuteSqlEvent::class, + function (MigratorExecuteSqlEvent $event) use ($eventSource): void { + $eventSource->send('success', $this->l->t('[%d / %d]: %s', [$event->getCurrentStep(), $event->getMaxStep(), $event->getSql()])); + } + ); + $feedBack = new FeedBackHandler($eventSource, $this->l); + $this->dispatcher->addListener(RepairStartEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairAdvanceEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairFinishEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairStepEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairInfoEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairWarningEvent::class, $feedBack->handleRepairFeedback(...)); + $this->dispatcher->addListener(RepairErrorEvent::class, $feedBack->handleRepairFeedback(...)); + + $this->updater->listen('\OC\Updater', 'maintenanceEnabled', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Turned on maintenance mode')); + }); + $this->updater->listen('\OC\Updater', 'maintenanceDisabled', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Turned off maintenance mode')); + }); + $this->updater->listen('\OC\Updater', 'maintenanceActive', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Maintenance mode is kept active')); + }); + $this->updater->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Updating database schema')); + }); + $this->updater->listen('\OC\Updater', 'dbUpgrade', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Updated database')); + }); + $this->updater->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($eventSource): void { + $eventSource->send('success', $this->l->t('Update app "%s" from App Store', [$app])); + }); + $this->updater->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($eventSource): void { + $eventSource->send('success', $this->l->t('Checking whether the database schema for %s can be updated (this can take a long time depending on the database size)', [$app])); + }); + $this->updater->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($eventSource): void { + $eventSource->send('success', $this->l->t('Updated "%1$s" to %2$s', [$app, $version])); + }); + $this->updater->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use (&$incompatibleApps, &$incompatibleOverwrites): void { + if (!in_array($app, $incompatibleOverwrites)) { + $incompatibleApps[] = $app; + } + }); + $this->updater->listen('\OC\Updater', 'failure', function ($message) use ($eventSource): void { + $eventSource->send('failure', $message); + $this->config->setSystemValue('maintenance', false); + }); + $this->updater->listen('\OC\Updater', 'setDebugLogLevel', function ($logLevel, $logLevelName) use ($eventSource): void { + $eventSource->send('success', $this->l->t('Set log level to debug')); + }); + $this->updater->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($eventSource): void { + $eventSource->send('success', $this->l->t('Reset log level')); + }); + $this->updater->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Starting code integrity check')); + }); + $this->updater->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($eventSource): void { + $eventSource->send('success', $this->l->t('Finished code integrity check')); + }); + + try { + $this->updater->upgrade(); + } catch (\Exception $e) { + $this->logger->error( + $e->getMessage(), + [ + 'exception' => $e, + 'app' => 'update', + ]); + $eventSource->send('failure', get_class($e) . ': ' . $e->getMessage()); + $eventSource->close(); + return new DataResponse([]); + } + + $disabledApps = []; + foreach ($incompatibleApps as $app) { + $disabledApps[$app] = $this->l->t('%s (incompatible)', [$app]); + } + + if (!empty($disabledApps)) { + $eventSource->send('notice', $this->l->t('The following apps have been disabled: %s', [implode(', ', $disabledApps)])); + } + + $eventSource->send('done', ''); + $eventSource->close(); + return new DataResponse([]); + } +} diff --git a/core/ajax/update.php b/core/ajax/update.php deleted file mode 100644 index 0a882929537d9..0000000000000 --- a/core/ajax/update.php +++ /dev/null @@ -1,151 +0,0 @@ -get('core'); - -$eventSource = Server::get(IEventSourceFactory::class)->create(); -// need to send an initial message to force-init the event source, -// which will then trigger its own CSRF check and produces its own CSRF error -// message -$eventSource->send('success', $l->t('Preparing update')); - -if (Util::needUpgrade()) { - $config = Server::get(SystemConfig::class); - if ($config->getValue('upgrade.disable-web', false)) { - $eventSource->send('failure', $l->t('Please use the command line updater because updating via browser is disabled in your config.php.')); - $eventSource->close(); - exit(); - } - - // if a user is currently logged in, their session must be ignored to - // avoid side effects - \OC_User::setIncognitoMode(true); - - $config = Server::get(IConfig::class); - $updater = Server::get(Updater::class); - $incompatibleApps = []; - $incompatibleOverwrites = $config->getSystemValue('app_install_overwrite', []); - - /** @var IEventDispatcher $dispatcher */ - $dispatcher = Server::get(IEventDispatcher::class); - $dispatcher->addListener( - MigratorExecuteSqlEvent::class, - function (MigratorExecuteSqlEvent $event) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('[%d / %d]: %s', [$event->getCurrentStep(), $event->getMaxStep(), $event->getSql()])); - } - ); - $feedBack = new FeedBackHandler($eventSource, $l); - $dispatcher->addListener(RepairStartEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairAdvanceEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairFinishEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairStepEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairInfoEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairWarningEvent::class, [$feedBack, 'handleRepairFeedback']); - $dispatcher->addListener(RepairErrorEvent::class, [$feedBack, 'handleRepairFeedback']); - - $updater->listen('\OC\Updater', 'maintenanceEnabled', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Turned on maintenance mode')); - }); - $updater->listen('\OC\Updater', 'maintenanceDisabled', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Turned off maintenance mode')); - }); - $updater->listen('\OC\Updater', 'maintenanceActive', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Maintenance mode is kept active')); - }); - $updater->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Updating database schema')); - }); - $updater->listen('\OC\Updater', 'dbUpgrade', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Updated database')); - }); - $updater->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Update app "%s" from App Store', [$app])); - }); - $updater->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Checking whether the database schema for %s can be updated (this can take a long time depending on the database size)', [$app])); - }); - $updater->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Updated "%1$s" to %2$s', [$app, $version])); - }); - $updater->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use (&$incompatibleApps, &$incompatibleOverwrites): void { - if (!in_array($app, $incompatibleOverwrites)) { - $incompatibleApps[] = $app; - } - }); - $updater->listen('\OC\Updater', 'failure', function ($message) use ($eventSource, $config): void { - $eventSource->send('failure', $message); - $config->setSystemValue('maintenance', false); - }); - $updater->listen('\OC\Updater', 'setDebugLogLevel', function ($logLevel, $logLevelName) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Set log level to debug')); - }); - $updater->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Reset log level')); - }); - $updater->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Starting code integrity check')); - }); - $updater->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($eventSource, $l): void { - $eventSource->send('success', $l->t('Finished code integrity check')); - }); - - try { - $updater->upgrade(); - } catch (\Exception $e) { - Server::get(LoggerInterface::class)->error( - $e->getMessage(), - [ - 'exception' => $e, - 'app' => 'update', - ]); - $eventSource->send('failure', get_class($e) . ': ' . $e->getMessage()); - $eventSource->close(); - exit(); - } - - $disabledApps = []; - foreach ($incompatibleApps as $app) { - $disabledApps[$app] = $l->t('%s (incompatible)', [$app]); - } - - if (!empty($disabledApps)) { - $eventSource->send('notice', $l->t('The following apps have been disabled: %s', [implode(', ', $disabledApps)])); - } -} else { - $eventSource->send('notice', $l->t('Already up to date')); -} - -$eventSource->send('done', ''); -$eventSource->close(); diff --git a/core/routes.php b/core/routes.php index 81f84456d528d..910af115c73db 100644 --- a/core/routes.php +++ b/core/routes.php @@ -10,9 +10,4 @@ * SPDX-License-Identifier: AGPL-3.0-only */ /** @var Router $this */ -// Core ajax actions -// Routing -$this->create('core_ajax_update', '/core/ajax/update.php') - ->actionInclude('core/ajax/update.php'); - $this->create('heartbeat', '/heartbeat')->get(); diff --git a/core/src/views/UpdaterAdmin.vue b/core/src/views/UpdaterAdmin.vue index 9197707b0cebb..a5c3d82df8d98 100644 --- a/core/src/views/UpdaterAdmin.vue +++ b/core/src/views/UpdaterAdmin.vue @@ -14,7 +14,7 @@ import { } from '@mdi/js' import { loadState } from '@nextcloud/initial-state' import { t } from '@nextcloud/l10n' -import { generateFilePath } from '@nextcloud/router' +import { generateOcsUrl } from '@nextcloud/router' import { NcButton, NcIconSvgWrapper, NcLoadingIcon } from '@nextcloud/vue' import { computed, onMounted, onUnmounted, ref } from 'vue' import NcGuestContent from '@nextcloud/vue/components/NcGuestContent' @@ -92,7 +92,7 @@ async function onStartUpdate() { } isUpdateRunning.value = true - const eventSource = new OCEventSource(generateFilePath('core', '', 'ajax/update.php')) + const eventSource = new OCEventSource(generateOcsUrl('/core/update')) eventSource.listen('success', (message) => { messages.value.push({ message, type: 'success' }) }) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5b6de5ff356d1..16592577c07a9 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1485,6 +1485,7 @@ 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UnifiedSearchController' => $baseDir . '/core/Controller/UnifiedSearchController.php', 'OC\\Core\\Controller\\UnsupportedBrowserController' => $baseDir . '/core/Controller/UnsupportedBrowserController.php', + 'OC\\Core\\Controller\\UpdateController' => $baseDir . '/core/Controller/UpdateController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index fbee07dafc6b4..46c6a88e5901f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1526,6 +1526,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UnifiedSearchController' => __DIR__ . '/../../..' . '/core/Controller/UnifiedSearchController.php', 'OC\\Core\\Controller\\UnsupportedBrowserController' => __DIR__ . '/../../..' . '/core/Controller/UnsupportedBrowserController.php', + 'OC\\Core\\Controller\\UpdateController' => __DIR__ . '/../../..' . '/core/Controller/UpdateController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php', diff --git a/ocs/v1.php b/ocs/v1.php index e12cd6ddc1147..5c4d125f9eb84 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -28,15 +28,15 @@ use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -if (Util::needUpgrade() - || Server::get(IConfig::class)->getSystemValueBool('maintenance')) { +$request = Server::get(IRequest::class); + +if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') { // since the behavior of apps or remotes are unpredictable during // an upgrade, return a 503 directly ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); exit; } - /* * Try the appframework routes */ @@ -46,16 +46,18 @@ $appManager->loadApps(['authentication']); $appManager->loadApps(['extended_authentication']); - // load all apps to get all api routes properly setup - // FIXME: this should ideally appear after handleLogin but will cause - // side effects in existing apps - $appManager->loadApps(); - - $request = Server::get(IRequest::class); $request->throwDecodingExceptionIfAny(); - if (!Server::get(IUserSession::class)->isLoggedIn()) { - OC::handleLogin($request); + if ($request->getPathInfo() !== '/core/update') { + // load all apps to get all api routes properly setup + // FIXME: this should ideally appear after handleLogin but will cause + // side effects in existing apps + $appManager->loadApps(); + if (!Server::get(IUserSession::class)->isLoggedIn()) { + OC::handleLogin($request); + } + } else { + $appManager->loadApps(['core']); } Server::get(Router::class)->match('/ocsapp' . $request->getRawPathInfo());