diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 9936d3543d934..14bec18db5a93 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -3085,6 +3085,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
session->get(self::WEBAUTHN_LOGIN))]]>
@@ -3117,24 +3135,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/core/Controller/UpdateController.php b/core/Controller/UpdateController.php
new file mode 100644
index 0000000000000..a394178d41428
--- /dev/null
+++ b/core/Controller/UpdateController.php
@@ -0,0 +1,178 @@
+
+ *
+ * 200: Success
+ */
+ #[ApiRoute(verb: 'GET', url: '/update', root: '/core')]
+ #[PublicPage]
+ public function update(): DataResponse {
+ if (!str_contains(@ini_get('disable_functions'), 'set_time_limit')) {
+ @set_time_limit(0);
+ }
+
+ \OC_User::setIncognitoMode(true);
+
+ $eventSource = $this->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(null);
+ }
+
+ 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(null);
+ }
+
+ // 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(null);
+ }
+
+ $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(null);
+ }
+}
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/openapi-full.json b/core/openapi-full.json
index de4c07ec3910c..9627fdfd7f45c 100644
--- a/core/openapi-full.json
+++ b/core/openapi-full.json
@@ -9002,6 +9002,68 @@
}
}
},
+ "/ocs/v2.php/core/update": {
+ "get": {
+ "operationId": "update-update",
+ "summary": "Update the server via the web interface",
+ "tags": [
+ "update"
+ ],
+ "security": [
+ {},
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/index.php/avatar/{userId}/{size}/dark": {
"get": {
"operationId": "avatar-get-avatar-dark",
diff --git a/core/openapi.json b/core/openapi.json
index 145894f33a0b5..401069f39b110 100644
--- a/core/openapi.json
+++ b/core/openapi.json
@@ -9002,6 +9002,68 @@
}
}
},
+ "/ocs/v2.php/core/update": {
+ "get": {
+ "operationId": "update-update",
+ "summary": "Update the server via the web interface",
+ "tags": [
+ "update"
+ ],
+ "security": [
+ {},
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/index.php/avatar/{userId}/{size}/dark": {
"get": {
"operationId": "avatar-get-avatar-dark",
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());
diff --git a/openapi.json b/openapi.json
index ca5b2b566f709..af7cd3aa1a6e4 100644
--- a/openapi.json
+++ b/openapi.json
@@ -12650,6 +12650,68 @@
}
}
},
+ "/ocs/v2.php/core/update": {
+ "get": {
+ "operationId": "core-update-update",
+ "summary": "Update the server via the web interface",
+ "tags": [
+ "core/update"
+ ],
+ "security": [
+ {},
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "ocs"
+ ],
+ "properties": {
+ "ocs": {
+ "type": "object",
+ "required": [
+ "meta",
+ "data"
+ ],
+ "properties": {
+ "meta": {
+ "$ref": "#/components/schemas/OCSMeta"
+ },
+ "data": {
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/index.php/avatar/{userId}/{size}/dark": {
"get": {
"operationId": "core-avatar-get-avatar-dark",