Files
nextcloud/php/public/index.php
T
Pablo Zmdl fd4e0ddcb2 Store office suite choice in a single config option
Previously it was three boolean options, which were supposed to be mutually
exclusive, which had to be taken care of manually.

Also previously there was only indirect indication of the choice "none", which
is brittle, too.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-07-17 19:24:49 +02:00

309 lines
17 KiB
PHP

<?php
declare(strict_types=1);
// increase memory limit to 2GB
ini_set('memory_limit', '2048M');
// set max execution time to 2h just in case of a very slow internet connection
ini_set('max_execution_time', '7200');
// Log whole log messages
ini_set('log_errors_max_len', '0');
// Path for the Twig compiled-template cache (created at container startup by start.sh)
const TWIG_CACHE_PATH = '/tmp/twig-cache';
use DI\Container;
use DI\NotFoundException;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
require __DIR__ . '/../vendor/autoload.php';
$container = \AIO\DependencyInjection::GetContainer();
$dataConst = $container->get(\AIO\Data\DataConst::class);
// Create app
AppFactory::setContainer($container);
$app = AppFactory::create();
$responseFactory = $app->getResponseFactory();
// Register Middleware On Container
$container->set(Guard::class, function () use ($responseFactory) {
$guard = new Guard($responseFactory);
$guard->setPersistentTokenMode(true);
return $guard;
});
// Register Middleware To Be Executed On All Routes
// Migrate from the old PHPSESSID cookie to the new __Host-Http-PHPSESSID cookie.
// This is needed because the session cookie was renamed in a previous release. Without this,
// users that were logged in before the update would be logged out after the container restarts.
$wasAuthenticated = false;
$oldSessionTimestamp = null;
if (!isset($_COOKIE['__Host-Http-PHPSESSID']) && isset($_COOKIE['PHPSESSID'])) {
session_name('PHPSESSID');
if (session_start(['save_path' => $dataConst->GetSessionDirectory(), 'use_strict_mode' => true])) {
$wasAuthenticated = isset($_SESSION[\AIO\Auth\AuthManager::SESSION_KEY]) && $_SESSION[\AIO\Auth\AuthManager::SESSION_KEY] === true;
$oldSessionTimestamp = isset($_SESSION['date_time']) ? (int)$_SESSION['date_time'] : null;
// Do not destroy the old session: if the response carrying the new __Host-Http-PHPSESSID
// cookie is lost (e.g., due to a 502 during a mastercontainer update), the client can
// retry with the old PHPSESSID cookie and still be authenticated.
session_write_close();
}
}
session_start([
"name" => "__Host-Http-PHPSESSID", // Set cookie prefix to prevent other pages from overwriting this cookie. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie_prefixes
"save_path" => $dataConst->GetSessionDirectory(), // Where to save the session files
"cookie_lifetime" => 0, // Delete the session cookie whenever the browser is closed. See https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime
"gc_maxlifetime" => 86400, // Delete sessions after 24 hours. See https://www.php.net/manual/en/session.configuration.php#ini.session.gc-maxlifetime
"gc_probability" => 0, // Probability that the session cleanup starts. The sessions are cleaned up by a cron job instead, see /cron.sh. See https://www.php.net/manual/en/session.configuration.php#ini.session.gc-probability
"gc_divisor" => 100, // gc_probability/gc_divisor = 0/100 = 0%, meaning that PHP will never run session GC itself (cron.sh handles cleanup instead). See https://www.php.net/manual/en/session.configuration.php#ini.session.gc-divisor
"use_strict_mode" => true, // Only allow initialized session IDs. See https://www.php.net/manual/en/session.configuration.php#ini.session.use-strict-mode
"cookie_secure" => true, // Only send cookies over https (not http). See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure
"cookie_httponly" => true, // Block the cookie from being read with js in the browser, will still be send for fetch request triggered by js. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#httponly
"cookie_samesite" => "Lax", // Send the cookie with same-site requests and top-level cross-site navigations (e.g. redirect after token-based getlogin). "Strict" would block the session cookie on the redirect that follows a cross-site navigation, breaking the getlogin flow from Nextcloud's admin panel. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value
]);
if ($wasAuthenticated) {
if ($oldSessionTimestamp !== null) {
// Use MigrateAuthState to preserve the original login timestamp. This prevents the
// session deduplicator from running and keeps the old PHPSESSID session file alive,
// so the client can retry with the old cookie if the 502 response causes the new
// __Host-Http-PHPSESSID cookie to not be received.
$container->get(\AIO\Auth\AuthManager::class)->MigrateAuthState($oldSessionTimestamp);
} else {
$container->get(\AIO\Auth\AuthManager::class)->SetAuthState(true);
}
}
$app->add(Guard::class);
// Create Twig
$twig = Twig::create(__DIR__ . '/../templates/', ['cache' => TWIG_CACHE_PATH]);
$app->add(TwigMiddleware::create($app, $twig));
$twig->addExtension(new \AIO\Twig\CsrfExtension($container->get(Guard::class)));
// Auth Middleware
$app->add(new \AIO\Middleware\AuthMiddleware($container->get(\AIO\Auth\AuthManager::class)));
// API
$app->post('/api/docker/watchtower', AIO\Controller\DockerController::class . ':StartWatchtowerContainer');
$app->get('/api/docker/getwatchtower', AIO\Controller\DockerController::class . ':StartWatchtowerContainer');
$app->post('/api/docker/start', AIO\Controller\DockerController::class . ':StartContainer');
$app->post('/api/docker/backup', AIO\Controller\DockerController::class . ':StartBackupContainerBackup');
$app->post('/api/docker/backup-check', AIO\Controller\DockerController::class . ':StartBackupContainerCheck');
$app->post('/api/docker/backup-list', AIO\Controller\DockerController::class . ':StartBackupContainerList');
$app->post('/api/docker/backup-check-repair', AIO\Controller\DockerController::class . ':StartBackupContainerCheckRepair');
$app->post('/api/docker/backup-test', AIO\Controller\DockerController::class . ':StartBackupContainerTest');
$app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore');
$app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer');
$app->post('/api/docker/backup-reset-location', AIO\Controller\DockerController::class . ':DeleteBorgBackupConfig');
$app->post('/api/docker/nextcloud-upgrade-to-latest-major', AIO\Controller\DockerController::class . ':RunNextcloudUpgradeToLatestMajor');
$app->post('/api/docker/prune', AIO\Controller\DockerController::class . ':SystemPrune');
$app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs');
$app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin');
$app->get('/api/auth/getlogin', AIO\Controller\LoginController::class . ':GetTryLogin');
$app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout');
$app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig');
$app->post('/api/desec/register', \AIO\Controller\DesecController::class . ':Register');
// Views
$app->get('/containers', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
$view->addExtension(new \AIO\Twig\ClassExtension());
/** @var \AIO\Data\ConfigurationManager $configurationManager */
$configurationManager = $container->get(\AIO\Data\ConfigurationManager::class);
/** @var \AIO\Docker\DockerActionManager $dockerActionManager */
$dockerActionManager = $container->get(\AIO\Docker\DockerActionManager::class);
/** @var \AIO\Controller\DockerController $dockerController */
$dockerController = $container->get(\AIO\Controller\DockerController::class);
$dockerActionManager->ConnectMasterContainerToNetwork();
$dockerController->StartDomaincheckContainer();
// Check if bypass_mastercontainer_update is provided on the URL, a special developer mode to bypass a mastercontainer update and use local image.
$params = $request->getQueryParams();
$bypass_mastercontainer_update = isset($params['bypass_mastercontainer_update']);
$bypass_container_update = isset($params['bypass_container_update']);
$skip_domain_validation = isset($params['skip_domain_validation']);
return $view->render($response, 'containers.twig', [
'domain' => $configurationManager->domain,
'apache_port' => $configurationManager->apachePort,
'borg_backup_host_location' => $configurationManager->borgBackupHostLocation,
'borg_remote_repo' => $configurationManager->borgRemoteRepo,
'borg_public_key' => $configurationManager->getBorgPublicKey(),
'nextcloud_password' => $configurationManager->getAndGenerateSecret('NEXTCLOUD_PASSWORD'),
'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(),
'borgbackup_password' => $configurationManager->getAndGenerateSecret('BORGBACKUP_PASSWORD'),
'is_mastercontainer_update_available' => ( $bypass_mastercontainer_update ? false : $dockerActionManager->IsMastercontainerUpdateAvailable() ),
'has_backup_run_once' => $configurationManager->hasBackupRunOnce(),
'is_backup_container_running' => $dockerActionManager->isBackupContainerRunning(),
'backup_exit_code' => $dockerActionManager->GetBackupcontainerExitCode(),
'is_instance_restore_attempt' => $configurationManager->instanceRestoreAttempt,
'borg_backup_mode' => $configurationManager->backupMode,
'was_start_button_clicked' => $configurationManager->wasStartButtonClicked,
'has_update_available' => $dockerActionManager->isAnyUpdateAvailable(),
'last_backup_time' => $configurationManager->getLastBackupTime(),
'backup_times' => $configurationManager->getBackupTimes(),
'current_channel' => $dockerActionManager->GetCurrentChannel(),
'is_clamav_enabled' => $configurationManager->isClamavEnabled,
'office_suite' => $configurationManager->getOfficeSuiteString(),
'is_talk_enabled' => $configurationManager->isTalkEnabled,
'borg_restore_password' => $configurationManager->borgRestorePassword,
'daily_backup_time' => $configurationManager->getDailyBackupTime(),
'is_daily_backup_running' => $configurationManager->isDailyBackupRunning(),
'timezone' => $configurationManager->timezone,
'skip_domain_validation' => $configurationManager->shouldDomainValidationBeSkipped($skip_domain_validation),
'talk_port' => $configurationManager->talkPort,
'collabora_dictionaries' => $configurationManager->collaboraDictionaries,
'collabora_additional_options' => $configurationManager->collaboraAdditionalOptions,
'automatic_updates' => $configurationManager->areAutomaticUpdatesEnabled(),
'is_backup_section_enabled' => !$configurationManager->disableBackupSection,
'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled,
'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled,
'additional_backup_directories' => $configurationManager->getAdditionalBackupDirectoriesString(),
'nextcloud_datadir' => $configurationManager->nextcloudDatadirMount,
'nextcloud_mount' => $configurationManager->nextcloudMount,
'nextcloud_upload_limit' => $configurationManager->nextcloudUploadLimit,
'nextcloud_max_time' => $configurationManager->nextcloudMaxTime,
'nextcloud_memory_limit' => $configurationManager->nextcloudMemoryLimit,
'is_dri_device_enabled' => $configurationManager->nextcloudEnableDriDevice,
'is_nvidia_gpu_enabled' => $configurationManager->enableNvidiaGpu,
'is_talk_recording_enabled' => $configurationManager->isTalkRecordingEnabled,
'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled,
'is_harp_enabled' => $configurationManager->isHarpEnabled,
'is_whiteboard_enabled' => $configurationManager->isWhiteboardEnabled,
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
'community_containers_enabled' => $configurationManager->aioCommunityContainers,
'bypass_container_update' => $bypass_container_update,
'desec_email' => $configurationManager->desecEmail,
'desec_password' => $configurationManager->desecPassword,
'is_desec_domain' => $configurationManager->isDesecDomain(),
'desec_account_registered' => $configurationManager->isDesecAccountRegistered(),
'desec_awaiting_verification' => $configurationManager->isDesecAwaitingVerification(),
// Do not cache the page as it shows credentials
])->withHeader('Cache-Control', 'no-store');
})->setName('profile');
// Renders only the deSEC registration flow. The containers page opens this in a modal
// iframe so the user can run the multi-step register -> verify -> domain process (adjusting
// the inputs and re-submitting as needed) without reloading the whole page each time. Once a
// deSEC domain is configured the view tells the parent window to reload (see desec-done.js).
$app->get('/desec', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Data\ConfigurationManager $configurationManager */
$configurationManager = $container->get(\AIO\Data\ConfigurationManager::class);
return $view->render($response, 'desec.twig', [
'domain' => $configurationManager->domain,
'desec_email' => $configurationManager->desecEmail,
'desec_password' => $configurationManager->desecPassword,
'desec_slug' => $configurationManager->desecSlug,
'is_desec_domain' => $configurationManager->isDesecDomain(),
'desec_account_registered' => $configurationManager->isDesecAccountRegistered(),
'desec_awaiting_verification' => $configurationManager->isDesecAwaitingVerification(),
// Do not cache the page as it shows credentials
])->withHeader('Cache-Control', 'no-store');
})->setName('desec');
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Docker\DockerActionManager $dockerActionManager */
$dockerActionManager = $container->get(\AIO\Docker\DockerActionManager::class);
return $view->render($response, 'login.twig', [
'is_login_allowed' => $dockerActionManager->isLoginAllowed(),
]);
});
$app->get('/setup', function (Request $request, Response $response, array $args) use ($container) {
$view = Twig::fromRequest($request);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if(!$setup->CanBeInstalled()) {
return $view->render(
$response,
'already-installed.twig'
);
}
return $view->render(
$response,
'setup.twig',
[
'password' => $setup->Setup(),
]
// Do not cache the page as it shows credentials
)->withHeader('Cache-Control', 'no-store');
});
$app->get('/log', function (Request $request, Response $response, array $args) use ($container) {
$params = $request->getQueryParams();
$id = $params['id'] ?? '';
if (!str_starts_with($id, 'nextcloud-aio-')) {
throw new DI\NotFoundException();
}
$view = Twig::fromRequest($request);
return $view->render(
$response, 'log.twig',
[
'id' => $id
]
// Do not cache the page as it might shows credentials
)->withHeader('Cache-Control', 'no-store');
});
// Auth Redirector
$app->get('/', function (\Psr\Http\Message\RequestInterface $request, Response $response, array $args) use ($container) {
/** @var \AIO\Auth\AuthManager $authManager */
$authManager = $container->get(\AIO\Auth\AuthManager::class);
/** @var \AIO\Data\Setup $setup */
$setup = $container->get(\AIO\Data\Setup::class);
if($setup->CanBeInstalled()) {
return $response
->withHeader('Location', 'setup')
->withStatus(302);
}
if($authManager->IsAuthenticated()) {
return $response
->withHeader('Location', 'containers')
->withStatus(302);
} else {
return $response
->withHeader('Location', 'login')
->withStatus(302);
}
});
// Default error handler
$errorMiddleware = $app->addErrorMiddleware(false, true, true);
// Set a custom Not Found handler, which doesn't pollute the app output with 404 errors.
$errorMiddleware->setErrorHandler(
\Slim\Exception\HttpNotFoundException::class,
function (Request $request, Throwable $exception, bool $displayErrorDetails) use ($app) {
$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write('Not Found');
return $response->withStatus(404);
}
);
// Set another custom error handler, which doesn't pollute the app output with 405 errors.
$errorMiddleware->setErrorHandler(
\Slim\Exception\HttpMethodNotAllowedException::class,
function (Request $request, Throwable $exception, bool $displayErrorDetails) use ($app) {
$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write('Method not allowed');
return $response->withStatus(405);
}
);
$app->run();