From f7dad729e4b776ec0ea7171d7e9163045764b060 Mon Sep 17 00:00:00 2001
From: Ferdinand Thiessen
Date: Thu, 15 Jan 2026 00:28:07 +0100
Subject: [PATCH] refactor(core): migrate web updater to Vue
Signed-off-by: Ferdinand Thiessen
---
build/frontend-legacy/webpack.modules.cjs | 1 +
core/ajax/update.php | 1 -
core/js/update.js | 160 -----------
core/src/update.ts | 20 ++
core/src/views/UpdaterAdmin.vue | 317 ++++++++++++++++++++++
core/src/views/UpdaterAdminCli.vue | 53 ++++
core/templates/update.admin.php | 60 ----
core/templates/update.php | 8 +
core/templates/update.use-cli.php | 32 ---
lib/base.php | 77 ++++--
10 files changed, 452 insertions(+), 277 deletions(-)
delete mode 100644 core/js/update.js
create mode 100644 core/src/update.ts
create mode 100644 core/src/views/UpdaterAdmin.vue
create mode 100644 core/src/views/UpdaterAdminCli.vue
delete mode 100644 core/templates/update.admin.php
create mode 100644 core/templates/update.php
delete mode 100644 core/templates/update.use-cli.php
diff --git a/build/frontend-legacy/webpack.modules.cjs b/build/frontend-legacy/webpack.modules.cjs
index 43405d97de0..a68a7a0e2da 100644
--- a/build/frontend-legacy/webpack.modules.cjs
+++ b/build/frontend-legacy/webpack.modules.cjs
@@ -29,6 +29,7 @@ module.exports = {
public: path.join(__dirname, 'core/src', 'public.ts'),
public_share_auth: path.join(__dirname, 'core/src', 'public-share-auth.ts'),
'twofactor-request-token': path.join(__dirname, 'core/src', 'twofactor-request-token.ts'),
+ update: path.join(__dirname, 'core/src', 'update.ts'),
},
dashboard: {
main: path.join(__dirname, 'apps/dashboard/src', 'main.js'),
diff --git a/core/ajax/update.php b/core/ajax/update.php
index 22bbdcff3e0..0a882929537 100644
--- a/core/ajax/update.php
+++ b/core/ajax/update.php
@@ -106,7 +106,6 @@ if (Util::needUpgrade()) {
});
$updater->listen('\OC\Updater', 'failure', function ($message) use ($eventSource, $config): void {
$eventSource->send('failure', $message);
- $eventSource->close();
$config->setSystemValue('maintenance', false);
});
$updater->listen('\OC\Updater', 'setDebugLogLevel', function ($logLevel, $logLevelName) use ($eventSource, $l): void {
diff --git a/core/js/update.js b/core/js/update.js
deleted file mode 100644
index 382ad8414ac..00000000000
--- a/core/js/update.js
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2014 ownCloud Inc.
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-(function() {
- OC.Update = {
- _started: false,
- options: {},
-
- /**
- * Start the update process.
- *
- * @param $el progress list element
- * @param options
- */
- start: function($el, options) {
- if (this._started) {
- return
- }
-
- this.options = options
- let hasWarnings = false
-
- this.$el = $el
-
- this._started = true
-
- const self = this
-
- $(window).on('beforeunload.inprogress', function() {
- return t('core', 'The update is in progress, leaving this page might interrupt the process in some environments.')
- })
-
- $('#update-progress-title').html(t(
- 'core',
- 'Update to {version}',
- {
- version: options.version,
- },
- ))
-
- const updateEventSource = new OC.EventSource(OC.getRootPath() + '/core/ajax/update.php')
- updateEventSource.listen('success', function(message) {
- self.setMessage(message)
- })
- updateEventSource.listen('notice', function(message) {
- self.setPermanentMessage(message)
- hasWarnings = true
- })
- updateEventSource.listen('error', function(message) {
- $('#update-progress-message').hide()
- $('#update-progress-icon')
- .addClass('icon-error-white')
- .removeClass('icon-loading-dark')
- message = message || t('core', 'An error occurred.')
- $(window).off('beforeunload.inprogress')
- self.setErrorMessage(message)
- message = t('core', 'Please reload the page.')
- $('').append('' + message + ' ').appendTo($el)
- updateEventSource.close()
- })
- updateEventSource.listen('failure', function(message) {
- $(window).off('beforeunload.inprogress')
- $('#update-progress-message').hide()
- $('#update-progress-icon')
- .addClass('icon-error-white')
- .removeClass('icon-loading-dark')
-
- self.setErrorMessage(message)
- const updateUnsuccessful = $('
')
- if (message === 'Exception: Updates between multiple major versions and downgrades are unsupported.') {
- updateUnsuccessful.append(t('core', 'The update was unsuccessful. For more information check our forum post covering this issue.', { url: 'https://help.nextcloud.com/t/updates-between-multiple-major-versions-are-unsupported/7094' }))
- } else if (OC.Update.options.productName === 'Nextcloud') {
- updateUnsuccessful.append(t('core', 'The update was unsuccessful. '
- + 'Please report this issue to the '
- + 'Nextcloud community .'))
- }
- updateUnsuccessful.appendTo($el)
- })
- updateEventSource.listen('done', function() {
- $(window).off('beforeunload.inprogress')
-
- $('#update-progress-message').hide()
-
- $('#update-progress-icon')
- .addClass('icon-checkmark-white')
- .removeClass('icon-loading-dark')
-
- if (hasWarnings) {
- $el.find('.update-show-detailed').before($(' ').on('click', function() {
- window.location.reload()
- }))
- } else {
- $el.find('.update-show-detailed').before($('
'))
-
- for (let i = 0; i <= 4; i++) {
- self.updateCountdown(i, 4)
- }
-
- setTimeout(function() {
- window.location = window.location.href
- window.location.reload()
- }, 3000)
- }
- })
- },
-
- updateCountdown: function(i, total) {
- setTimeout(function() {
- $('#redirect-countdown').text(n('core', 'The update was successful. Redirecting you to {productName} in %n second.', 'The update was successful. Redirecting you to {productName} in %n seconds.', i, OC.Update.options))
- }, (total - i) * 1000)
- },
-
- setMessage: function(message) {
- $('#update-progress-message').html(message)
- $('#update-progress-detailed')
- .append('' + message + '
')
- },
-
- setPermanentMessage: function(message) {
- $('#update-progress-message').html(message)
- $('#update-progress-message-warnings')
- .show()
- .append($('').append(message))
- $('#update-progress-detailed')
- .append('' + message + '
')
- },
-
- setErrorMessage: function(message) {
- $('#update-progress-message-error')
- .show()
- .html(message)
- $('#update-progress-detailed')
- .append('' + message + '
')
- },
- }
-})()
-
-window.addEventListener('DOMContentLoaded', function() {
- $('.updateButton').on('click', function() {
- const $updateEl = $('.update')
- const $progressEl = $('.update-progress')
- $progressEl.removeClass('hidden')
- $('.updateOverview').addClass('hidden')
- $('#update-progress-message-error').hide()
- $('#update-progress-message-warnings').hide()
- OC.Update.start($progressEl, {
- productName: $updateEl.attr('data-productname'),
- version: $updateEl.attr('data-version'),
- })
- return false
- })
-
- $('.update-show-detailed').on('click', function() {
- $('#update-progress-detailed').toggleClass('hidden')
- return false
- })
-})
diff --git a/core/src/update.ts b/core/src/update.ts
new file mode 100644
index 00000000000..a3f95431984
--- /dev/null
+++ b/core/src/update.ts
@@ -0,0 +1,20 @@
+/*!
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCSPNonce } from '@nextcloud/auth'
+import { loadState } from '@nextcloud/initial-state'
+import Vue, { defineAsyncComponent } from 'vue'
+
+__webpack_nonce__ = getCSPNonce()
+
+const UpdaterAdmin = defineAsyncComponent(() => import('./views/UpdaterAdmin.vue'))
+const UpdaterAdminCli = defineAsyncComponent(() => import('./views/UpdaterAdminCli.vue'))
+
+const view = loadState('core', 'updaterView')
+const app = new Vue({
+ name: 'NextcloudUpdater',
+ render: (h) => view === 'adminCli' ? h(UpdaterAdminCli) : h(UpdaterAdmin),
+})
+app.$mount('#core-updater')
diff --git a/core/src/views/UpdaterAdmin.vue b/core/src/views/UpdaterAdmin.vue
new file mode 100644
index 00000000000..9197707b0ce
--- /dev/null
+++ b/core/src/views/UpdaterAdmin.vue
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+ {{ updateInfo.isAppsOnlyUpgrade
+ ? t('core', 'App update required')
+ : t('core', '{productName} will be updated to version {version}', { productName: updateInfo.productName, version: updateInfo.version }) }}
+
+
+
+ {{ t('core', 'The theme {oldTheme} has been disabled.', { oldTheme: updateInfo.oldTheme }) }}
+
+
+
+ {{ t('core', 'These incompatible apps will be disabled:') }}
+
+
+ {{ app.name }} ({{ app.id }})
+
+
+
+
+
+ {{ t('core', 'These apps will be updated:') }}
+
+
+ {{ t('core', '{app} from {oldVersion} to {version}', { app: `${app.name} (${app.id})`, oldVersion: app.oldVersion, version: app.version }) }}
+
+
+
+
+
+ {{ t('core', 'Please make sure that the database, the config folder and the data folder have been backed up before proceeding.') }}
+
+ {{ t('core', 'To avoid timeouts with larger installations, you can instead run the following command from your installation directory:') }}
+
./occ upgrade
+
+
+
+ {{ t('core', 'Start update') }}
+
+
+ {{ t('core', 'Continue to {productName}', { productName: updateInfo.productName }) }}
+
+
+
+
{{ t('core', 'Update to {version}', { version: updateInfo.version }) }}
+
+
+
+
+ {{ statusMessage }}
+ {{ redirectMessage }}
+
+
+
+
+
+
+ {{ isShowingDetails ? t('core', 'Hide details') : t('core', 'Show details') }}
+
+
+
+
+
+
+
+
+
diff --git a/core/src/views/UpdaterAdminCli.vue b/core/src/views/UpdaterAdminCli.vue
new file mode 100644
index 00000000000..092f4b5377d
--- /dev/null
+++ b/core/src/views/UpdaterAdminCli.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ {{ t('core', 'Update needed') }}
+
+ {{ updateInfo.tooBig
+ ? t('core', 'Please use the command line updater because you have a big instance with more than 50 accounts.')
+ : t('core', 'Please use the command line updater because updating via browser is disabled in your config.php.') }}
+
+
+
+
+
+ {{ t('core', 'Documentation') }}
+
+
+
+
+ {{ t('core', 'I know that if I continue doing the update via web UI has the risk, that the request runs into a timeout and could cause data loss, but I have a backup and know how to restore my instance in case of a failure.') }}
+
+ {{ t('core', 'Upgrade via web on my own risk') }}
+
+
+
+
+
+
diff --git a/core/templates/update.admin.php b/core/templates/update.admin.php
deleted file mode 100644
index 504071451a9..00000000000
--- a/core/templates/update.admin.php
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
t('App update required')); ?>
-
-
t('%1$s will be updated to version %2$s',
- [$_['productName'], $_['version']])); ?>
-
-
-
-
t('The following apps will be updated:')); ?>
-
-
-
-
-
-
t('These incompatible apps will be disabled:')) ?>
-
-
-
-
-
- t('The theme %s has been disabled.', [$_['oldTheme']])) ?>
-
-
-
- t('Please make sure that the database, the config folder and the data folder have been backed up before proceeding.')) ?>
-
-
-
- t('To avoid timeouts with larger installations, you can instead run the following command from your installation directory:')) ?>
-
./occ upgrade
-
-
-
-
-
diff --git a/core/templates/update.php b/core/templates/update.php
new file mode 100644
index 00000000000..2e9faab3fed
--- /dev/null
+++ b/core/templates/update.php
@@ -0,0 +1,8 @@
+
+
+
diff --git a/core/templates/update.use-cli.php b/core/templates/update.use-cli.php
deleted file mode 100644
index a13dd2f51f1..00000000000
--- a/core/templates/update.use-cli.php
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
t('Update needed')) ?>
-
- t('Please use the command line updater because you have a big instance with more than 50 accounts.'));
- } else {
- p($l->t('Please use the command line updater because updating via browser is disabled in your config.php.'));
- } ?>
- t('For help, see the
documentation .', [$cliUpgradeLink])); ?>
-
-
-
-
-
-
t('I know that if I continue doing the update via web UI has the risk, that the request runs into a timeout and could cause data loss, but I have a backup and know how to restore my instance in case of a failure.')); ?>
-
t('Upgrade via web on my own risk')); ?>
-
-
-
diff --git a/lib/base.php b/lib/base.php
index 632d19968a6..e323e6d2138 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -17,7 +17,9 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\BeforeFileSystemSetupEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserRemovedEvent;
+use OCP\IAppConfig;
use OCP\IConfig;
+use OCP\IInitialStateService;
use OCP\ILogger;
use OCP\IRequest;
use OCP\IURLGenerator;
@@ -290,47 +292,49 @@ class OC {
$ignoreTooBigWarning = isset($_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'])
&& $_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'] === 'IAmSuperSureToDoThis';
+ Util::addTranslations('core');
+ Util::addScript('core', 'common');
+ Util::addScript('core', 'main');
+ Util::addScript('core', 'update');
+
+ $initialState = Server::get(IInitialStateService::class);
+ $serverVersion = \OCP\Server::get(\OCP\ServerVersion::class);
if ($disableWebUpdater || ($tooBig && !$ignoreTooBigWarning)) {
// send http status 503
http_response_code(503);
header('Retry-After: 120');
- $serverVersion = \OCP\Server::get(\OCP\ServerVersion::class);
+ $urlGenerator = Server::get(IURLGenerator::class);
+ $initialState->provideInitialState('core', 'updaterView', 'adminCli');
+ $initialState->provideInitialState('core', 'updateInfo', [
+ 'cliUpgradeLink' => $cliUpgradeLink ?: $urlGenerator->linkToDocs('admin-cli-upgrade'),
+ 'productName' => self::getProductName(),
+ 'version' => $serverVersion->getVersionString(),
+ 'tooBig' => $tooBig,
+ ]);
// render error page
- $template = Server::get(ITemplateManager::class)->getTemplate('', 'update.use-cli', 'guest');
- $template->assign('productName', 'nextcloud'); // for now
- $template->assign('version', $serverVersion->getVersionString());
- $template->assign('tooBig', $tooBig);
- $template->assign('cliUpgradeLink', $cliUpgradeLink);
-
- $template->printPage();
+ Server::get(ITemplateManager::class)
+ ->getTemplate('', 'update', 'guest')
+ ->printPage();
die();
}
// check whether this is a core update or apps update
$installedVersion = $systemConfig->getValue('version', '0.0.0');
- $currentVersion = implode('.', \OCP\Util::getVersion());
+ $currentVersion = implode('.', $serverVersion->getVersion());
// if not a core upgrade, then it's apps upgrade
$isAppsOnlyUpgrade = version_compare($currentVersion, $installedVersion, '=');
$oldTheme = $systemConfig->getValue('theme');
$systemConfig->setValue('theme', '');
- \OCP\Util::addScript('core', 'common');
- \OCP\Util::addScript('core', 'main');
- \OCP\Util::addTranslations('core');
- \OCP\Util::addScript('core', 'update');
/** @var \OC\App\AppManager $appManager */
$appManager = Server::get(\OCP\App\IAppManager::class);
- $tmpl = Server::get(ITemplateManager::class)->getTemplate('', 'update.admin', 'guest');
- $tmpl->assign('version', \OCP\Server::get(\OCP\ServerVersion::class)->getVersionString());
- $tmpl->assign('isAppsOnlyUpgrade', $isAppsOnlyUpgrade);
-
// get third party apps
- $ocVersion = \OCP\Util::getVersion();
+ $ocVersion = $serverVersion->getVersion();
$ocVersion = implode('.', $ocVersion);
$incompatibleApps = $appManager->getIncompatibleApps($ocVersion);
$incompatibleOverwrites = $systemConfig->getValue('app_install_overwrite', []);
@@ -351,16 +355,41 @@ class OC {
throw new \OCP\HintException('Application ' . implode(', ', $incompatibleShippedApps) . ' is not present or has a non-compatible version with this server. Please check the apps directory.', $hint);
}
- $tmpl->assign('appsToUpgrade', $appManager->getAppsNeedingUpgrade($ocVersion));
- $tmpl->assign('incompatibleAppsList', $incompatibleDisabledApps);
+ $appConfig = Server::get(IAppConfig::class);
+ $appsToUpgrade = array_map(function ($app) use (&$appConfig) {
+ return [
+ 'id' => $app['id'],
+ 'name' => $app['name'],
+ 'version' => $app['version'],
+ 'oldVersion' => $appConfig->getValueString($app['id'], 'installed_version'),
+ ];
+ }, $appManager->getAppsNeedingUpgrade($ocVersion));
+
+ $params = [
+ 'appsToUpgrade' => $appsToUpgrade,
+ 'incompatibleAppsList' => $incompatibleDisabledApps,
+ 'isAppsOnlyUpgrade' => $isAppsOnlyUpgrade,
+ 'oldTheme' => $oldTheme,
+ 'productName' => self::getProductName(),
+ 'version' => $serverVersion->getVersionString(),
+ ];
+
+ $initialState->provideInitialState('core', 'updaterView', 'admin');
+ $initialState->provideInitialState('core', 'updateInfo', $params);
+ Server::get(ITemplateManager::class)
+ ->getTemplate('', 'update', 'guest')
+ ->printPage();
+ }
+
+ private static function getProductName(): string {
+ $productName = 'Nextcloud';
try {
$defaults = new \OC_Defaults();
- $tmpl->assign('productName', $defaults->getName());
+ $productName = $defaults->getName();
} catch (Throwable $error) {
- $tmpl->assign('productName', 'Nextcloud');
+ // ignore
}
- $tmpl->assign('oldTheme', $oldTheme);
- $tmpl->printPage();
+ return $productName;
}
public static function initSession(): void {