diff --git a/lib/OC.php b/lib/OC.php
index c7072df64b88d..c1810d8fcb56d 100644
--- a/lib/OC.php
+++ b/lib/OC.php
@@ -1128,8 +1128,17 @@ public static function handleRequest(): void {
if ($requestPath === '/heartbeat') {
return;
}
+ $serveAppApiDuringMaintenance = false;
if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade
- self::checkMaintenanceMode($systemConfig);
+ if (((bool)$systemConfig->getValue('maintenance', false)) && !\OCP\Util::needUpgrade()
+ && ($requestPath === '/apps/app_api' || str_starts_with($requestPath, '/apps/app_api/'))
+ && Server::get(\OCP\App\IAppManager::class)->isEnabledForAnyone('app_api')) {
+ // Keep serving ExApp traffic (HaRP metadata) while the instance is in maintenance mode
+ $serveAppApiDuringMaintenance = true;
+ }
+ if (!$serveAppApiDuringMaintenance) {
+ self::checkMaintenanceMode($systemConfig);
+ }
if (\OCP\Util::needUpgrade()) {
if (function_exists('opcache_reset')) {
@@ -1193,6 +1202,10 @@ public static function handleRequest(): void {
$appManager->loadApps(['filesystem', 'logging']);
$appManager->loadApps();
}
+ if ($serveAppApiDuringMaintenance) {
+ // loadApps() above is a no-op during maintenance, load app_api explicitly
+ $appManager->loadApp('app_api');
+ }
Server::get(\OC\Route\Router::class)->match($request->getRawPathInfo());
return;
} catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php
index 2aaa6f21baf13..e18dcc5ff8ef0 100644
--- a/lib/private/Console/Application.php
+++ b/lib/private/Console/Application.php
@@ -87,6 +87,22 @@ public function loadCommands(
if (Util::needUpgrade()) {
throw new NeedsUpdateException();
} elseif ($this->config->getSystemValueBool('maintenance')) {
+ if ($this->appManager->isEnabledForAnyone('app_api')) {
+ // AppAPI must stay usable during maintenance mode;
+ // loading commands from register_command.php is intentionally skipped.
+ $this->appManager->loadApp('app_api');
+ $info = $this->appManager->getAppInfo('app_api');
+ if (isset($info['commands'])) {
+ try {
+ $this->loadCommandsFromInfoXml($info['commands']);
+ } catch (\Throwable $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ $this->logger->error($e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ }
+ }
+ }
$this->writeMaintenanceModeInfo($input, $output);
} else {
$this->appManager->loadApps();
@@ -161,8 +177,13 @@ private function writeMaintenanceModeInfo(InputInterface $input, ConsoleOutputIn
&& $input->getArgument('command') !== 'maintenance:mode'
&& $input->getArgument('command') !== 'status') {
$errOutput = $output->getErrorOutput();
- $errOutput->writeln('Nextcloud is in maintenance mode, no apps are loaded.');
- $errOutput->writeln('Commands provided by apps are unavailable.');
+ if ($this->appManager->isEnabledForAnyone('app_api')) {
+ $errOutput->writeln('Nextcloud is in maintenance mode, only AppAPI commands are loaded.');
+ $errOutput->writeln('Commands provided by other apps are unavailable.');
+ } else {
+ $errOutput->writeln('Nextcloud is in maintenance mode, no apps are loaded.');
+ $errOutput->writeln('Commands provided by apps are unavailable.');
+ }
}
}
diff --git a/ocs/v1.php b/ocs/v1.php
index 5c4d125f9eb84..f97e69980f5c1 100644
--- a/ocs/v1.php
+++ b/ocs/v1.php
@@ -30,7 +30,18 @@
$request = Server::get(IRequest::class);
-if ((Util::needUpgrade() || Server::get(IConfig::class)->getSystemValueBool('maintenance')) && $request->getPathInfo() !== '/core/update') {
+$serveAppApiDuringMaintenance = false;
+if (!Util::needUpgrade() && Server::get(IConfig::class)->getSystemValueBool('maintenance')) {
+ $pathInfo = $request->getPathInfo();
+ // AppAPI must keep serving HaRP traffic (signed OCS calls and ExApp callbacks)
+ $serveAppApiDuringMaintenance
+ = ($pathInfo === '/apps/app_api' || str_starts_with($pathInfo, '/apps/app_api/'))
+ && Server::get(IAppManager::class)->isEnabledForAnyone('app_api');
+}
+
+if ((Util::needUpgrade()
+ || (Server::get(IConfig::class)->getSystemValueBool('maintenance') && !$serveAppApiDuringMaintenance))
+ && $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);
@@ -49,6 +60,10 @@
$request->throwDecodingExceptionIfAny();
if ($request->getPathInfo() !== '/core/update') {
+ if ($serveAppApiDuringMaintenance) {
+ // loadApps() below is a no-op during maintenance, load app_api explicitly
+ $appManager->loadApp('app_api');
+ }
// 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