diff --git a/lib/AppConfig.php b/lib/AppConfig.php index f3bc849f27..b3711ce314 100644 --- a/lib/AppConfig.php +++ b/lib/AppConfig.php @@ -12,8 +12,10 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\GlobalScale\IConfig as GlobalScaleConfig; use OCP\IConfig; +use OCP\IURLGenerator; class AppConfig { + public const SERVER_MODE = 'server_mode'; // URL that Nextcloud will use to connect to Collabora public const WOPI_URL = 'wopi_url'; // URL that the browser will use to connect to Collabora (inherited from the discovery endpoint of Collabora, @@ -61,6 +63,7 @@ public function __construct( private IAppConfig $appConfig, private IAppManager $appManager, private GlobalScaleConfig $globalScaleConfig, + private IURLGenerator $urlGenerator, ) { } @@ -134,6 +137,46 @@ public function getAppSettings() { return $result; } + /** + * Returns the configured server mode ('builtin', 'custom', 'demo', or ''). + */ + public function getServerMode(): string { + $stored = $this->config->getAppValue(Application::APPNAME, self::SERVER_MODE, ''); + if ($stored !== '') { + return $stored; + } + // Fallback for legacy builtin installs: if the migration step has not yet run (or was + // somehow skipped), detect builtin from the stored wopi_url. + $wopiUrl = $this->config->getAppValue(Application::APPNAME, self::WOPI_URL, ''); + if ($wopiUrl !== '' && str_contains($wopiUrl, 'proxy.php?req=')) { + return 'builtin'; + } + return ''; + } + + public function isBuiltinServer(): bool { + return $this->getServerMode() === 'builtin'; + } + + /** + * Returns the built-in CODE proxy URL derived at runtime from IURLGenerator. + * Never stored - always fresh, so it survives Nextcloud URL/domain changes. + * Returns null if CODE is not installed or not supported on this platform. + */ + public function getBuiltinServerUrl(): ?string { + $arch = php_uname('m'); + $supportedArchs = ['x86_64', 'aarch64']; + if (PHP_OS_FAMILY !== 'Linux' || !in_array($arch, $supportedArchs)) { + return null; + } + $CODEAppID = ($arch === 'aarch64') ? 'richdocumentscode_arm64' : 'richdocumentscode'; + if (!$this->appManager->isInstalled($CODEAppID)) { + return null; + } + $relativeUrl = $this->urlGenerator->linkTo($CODEAppID, '') . 'proxy.php'; + return $this->urlGenerator->getAbsoluteURL($relativeUrl) . '?req='; + } + /** * Returns a list of trusted domains from the gs.trustedHosts config */ @@ -148,12 +191,29 @@ public function isTrustedDomainAllowedForFederation(): bool { return $this->config->getAppValue(Application::APPNAME, self::FEDERATION_USE_TRUSTED_DOMAINS, 'no') === 'yes'; } + /** + * For builtin mode, public_wopi_url is always Nextcloud's own public origin — + * CODE has no separate hostname. Derived from IURLGenerator so it is correct + * in both HTTP and CLI contexts (the latter requires overwrite.cli.url to be set, + * which is a standard Nextcloud prerequisite). + * + * For custom/standalone servers, returns the stored public_wopi_url. + */ public function getCollaboraUrlPublic(): string { - return rtrim($this->config->getAppValue(Application::APPNAME, self::PUBLIC_WOPI_URL, $this->getCollaboraUrlInternal()), '/'); + if ($this->isBuiltinServer()) { + return $this->deriveBuiltinPublicUrl(); + } + return rtrim($this->config->getAppValue(Application::APPNAME, self::PUBLIC_WOPI_URL, + $this->getCollaboraUrlInternal()), '/'); } public function getCollaboraUrlInternal(): string { - return rtrim($this->config->getAppValue(Application::APPNAME, self::WOPI_URL, ''), '/'); + if ($this->isBuiltinServer()) { + // Derives the internal URL at runtime from IURLGenerator. + // This avoids the CLI/browser context mismatch that otherwise arise since built-in uses ProxyPrefix not server_name/etc + return $this->getBuiltinServerUrl() ?? ''; + } + return rtrim($this->config->getAppValue(Application::APPNAME, self::WOPI_URL, ''), '/'); } public function getNextcloudUrl(): string { @@ -256,6 +316,15 @@ private function getGSDomains(): array { return $this->getGlobalScaleTrustedHosts(); } + /** + * Derives the public URL for the built-in CODE server directly from IURLGenerator, + * without requiring server_mode to already be set. Used during initial setup/CLI + * where we want to validate the URL before committing server_mode to config. + */ + public function deriveBuiltinPublicUrl(): string { + return rtrim($this->domainOnly($this->urlGenerator->getAbsoluteURL('/')), '/'); + } + /** * Strips the path and query parameters from the URL. */ diff --git a/lib/Command/ActivateConfig.php b/lib/Command/ActivateConfig.php index 652e9c199e..39ee4a7b76 100644 --- a/lib/Command/ActivateConfig.php +++ b/lib/Command/ActivateConfig.php @@ -5,7 +5,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\Richdocuments\Command; use OCA\Richdocuments\AppConfig; @@ -29,77 +28,23 @@ protected function configure(): void { $this ->setName('richdocuments:activate-config') ->setAliases(['richdocuments:setup']) - ->addOption('wopi-url', 'w', InputOption::VALUE_REQUIRED, 'URL that the Nextcloud server will use to connect to Collabora', null) - ->addOption('callback-url', 'c', InputOption::VALUE_REQUIRED, 'URL that is passed to Collabora to connect back to Nextcloud', null) + ->addOption('wopi-url', 'w', InputOption::VALUE_REQUIRED, + 'URL that Nextcloud will use to connect to Collabora', null) + ->addOption('builtin', null, InputOption::VALUE_NONE, + 'Configure the built-in CODE server (richdocumentscode app must be installed)') + ->addOption('callback-url', 'c', InputOption::VALUE_REQUIRED, + 'URL that is passed to Collabora to connect back to Nextcloud', null) + ->addOption('force', null, InputOption::VALUE_NONE, + 'Persist configuration even if connectivity or sanity checks fail') ->setDescription('Activate config changes'); } protected function execute(InputInterface $input, OutputInterface $output): int { try { - if ($input->getOption('wopi-url') !== null) { - $wopiUrl = $input->getOption('wopi-url'); - $this->appConfig->setAppValue(AppConfig::WOPI_URL, $wopiUrl); - $output->writeln('✓ Set WOPI url to ' . $wopiUrl . ''); - } - - if ($input->getOption('callback-url') !== null) { - $callbackUrl = $input->getOption('callback-url'); - $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, $callbackUrl); - $output->writeln('✓ Set callback url to ' . $callbackUrl . ''); - } else { - $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, ''); - $output->writeln('✓ Reset callback url autodetect'); - } - - $output->writeln('Checking configuration'); - $output->writeln('🛈 Configured WOPI URL: ' . $this->appConfig->getCollaboraUrlInternal()); - $output->writeln('🛈 Configured public WOPI URL: ' . $this->appConfig->getCollaboraUrlPublic()); - $output->writeln('🛈 Configured callback URL: ' . $this->appConfig->getNextcloudUrl()); - $output->writeln(''); - - try { - $this->connectivityService->testDiscovery($output); - } catch (\Throwable $e) { - $output->writeln('Failed to fetch discovery endpoint from ' . $this->appConfig->getCollaboraUrlInternal()); - $output->writeln($e->getMessage()); - return 1; - } - - try { - $this->connectivityService->testCapabilities($output); - } catch (\Throwable $e) { - // FIXME: Optional when allowing generic WOPI servers - $output->writeln('Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint()); - $output->writeln($e->getMessage()); - return 1; + if ($input->getOption('builtin')) { + return $this->executeBuiltin($input, $output); } - - try { - $this->connectivityService->autoConfigurePublicUrl(); - } catch (\Throwable $e) { - $output->writeln('Failed to determine public URL from discovery response'); - $output->writeln($e->getMessage()); - return 1; - } - - // Summarize URLs for easier debugging - - $output->writeln(''); - $output->writeln('Collabora URL (used for Nextcloud to contact the Collabora server):'); - $output->writeln(' ' . $this->appConfig->getCollaboraUrlInternal()); - - $output->writeln('Collabora public URL (used in the browser to open Collabora):'); - $output->writeln(' ' . $this->appConfig->getCollaboraUrlPublic()); - - $output->writeln('Callback URL (used by Collabora to connect back to Nextcloud):'); - $callbackUrl = $this->appConfig->getNextcloudUrl(); - if ($callbackUrl === '') { - $output->writeln(' autodetected (will use the same URL as your user for browsing Nextcloud)'); - } else { - $output->writeln(' ' . $this->appConfig->getNextcloudUrl()); - } - - return 0; + return $this->executeCustom($input, $output); } catch (\Exception $e) { $output->writeln('Failed to activate any config changes'); $output->writeln($e->getMessage()); @@ -107,4 +52,148 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } } + + private function executeBuiltin(InputInterface $input, OutputInterface $output): int { + $force = (bool)$input->getOption('force'); + + // Validate preconditions before writing anything: getBuiltinServerUrl() checks OS/arch/installed + $builtinUrl = $this->appConfig->getBuiltinServerUrl(); + if ($builtinUrl === null) { + $output->writeln('Built-in CODE server is not available.'); + $output->writeln('Check: richdocumentscode (or richdocumentscode_arm64) is installed,' + . ' OS is Linux, arch is x86_64 or aarch64.'); + return 1; + } + + // Validate public URL looks correct before writing anything. + // For builtin, the public URL is always Nextcloud's own origin — derived + // directly from IURLGenerator without requiring server_mode to be set yet. + $publicUrl = $this->appConfig->deriveBuiltinPublicUrl(); + + // If the derived URL looks internal, overwrite.cli.url is probably wrong. + // Fail fast with an actionable message rather than silently storing a broken value. + // Often will be a false positive warning in test environments, but can be forced if necessary still. + $host = parse_url($publicUrl, PHP_URL_HOST); + $looksInternal = $host === 'localhost' || $host === '127.0.0.1' || str_ends_with($host, '.local'); + + if ($looksInternal && !$force) { + $output->writeln('Derived public URL looks internal: ' . $publicUrl . ''); + $output->writeln('"overwrite.cli.url" in config.php must be set to your public Nextcloud URL.'); + $output->writeln('This is required for any occ command that generates absolute URLs.'); + $output->writeln('To override and persist anyway: --force'); + return 1; + } + + if ($looksInternal) { + $output->writeln('⚠ Warning: public URL looks internal (' . $publicUrl . ').' + . ' Proceeding anyway due to --force.'); + } + + // Test connectivity against the derived URL directly; no config mutation needed. + if (!$force) { + try { + $this->connectivityService->testUrl($builtinUrl, $output); + } catch (\Throwable $e) { + // Nothing was writtent o config; no rollback needed + $output->writeln('Failed to reach built-in CODE server: ' . $e->getMessage() . ''); + $output->writeln('To configure without connectivity: --force'); + return 1; + } + } + + // All checks passed (or --force). Now commit atomically. + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, 'builtin'); + $this->appConfig->setAppValue('disable_certificate_verification', 'yes'); + // Explicitly delete any previously stored wopi_url and public_wopi_url to avoid ambiguity. + $this->appConfig->setAppValue(AppConfig::WOPI_URL, ''); + $this->appConfig->setAppValue(AppConfig::PUBLIC_WOPI_URL, ''); + + if ($input->getOption('callback-url') !== null) { + $callbackUrl = $input->getOption('callback-url'); + $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, $callbackUrl); + } else { + $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, ''); + } + + $output->writeln('✓ Built-in CODE server configured successfully.'); + $output->writeln('Built-in CODE URL (Nextcloud → CODE): ' . $this->appConfig->getCollaboraUrlInternal()); + $output->writeln('Public URL (browser → CODE): ' . $this->appConfig->getCollaboraUrlPublic()); + $callbackUrl = $this->appConfig->getNextcloudUrl(); + $output->writeln('Callback URL (Collabora → Nextcloud): ' + . ($callbackUrl === '' ? 'autodetected' : $callbackUrl)); + + return 0; + } + + private function executeCustom(InputInterface $input, OutputInterface $output): int { + $force = (bool)$input->getOption('force'); + + if ($input->getOption('wopi-url') !== null) { + $wopiUrl = $input->getOption('wopi-url'); + $this->appConfig->setAppValue(AppConfig::WOPI_URL, $wopiUrl); + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, 'custom'); + $output->writeln('✓ Set WOPI url to ' . $wopiUrl . ''); + } + + if ($input->getOption('callback-url') !== null) { + $callbackUrl = $input->getOption('callback-url'); + $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, $callbackUrl); + $output->writeln('✓ Set callback url to ' . $callbackUrl . ''); + } else { + $this->appConfig->setAppValue(AppConfig::WOPI_CALLBACK_URL, ''); + $output->writeln('✓ Reset callback url autodetect'); + } + + $output->writeln('Checking configuration'); + $output->writeln('🛈 Configured WOPI URL: ' . $this->appConfig->getCollaboraUrlInternal()); + $output->writeln('🛈 Configured public WOPI URL: ' . $this->appConfig->getCollaboraUrlPublic()); + $output->writeln('🛈 Configured callback URL: ' . $this->appConfig->getNextcloudUrl()); + $output->writeln(''); + + if (!$force) { + try { + $this->connectivityService->testDiscovery($output); + } catch (\Throwable $e) { + $output->writeln('Failed to fetch discovery endpoint from ' + . $this->appConfig->getCollaboraUrlInternal() . ''); + $output->writeln($e->getMessage()); + $output->writeln('To configure without connectivity: --force'); + return 1; + } + + try { + $this->connectivityService->testCapabilities($output); + } catch (\Throwable $e) { + $output->writeln('Failed to fetch capabilities from ' + . $this->capabilitiesService->getCapabilitiesEndpoint() . ''); + $output->writeln($e->getMessage()); + $output->writeln('To configure without connectivity: --force'); + return 1; + } + + try { + $this->connectivityService->autoConfigurePublicUrl(); + } catch (\Throwable $e) { + $output->writeln('Failed to determine public URL from discovery response'); + $output->writeln($e->getMessage()); + $output->writeln('To configure without connectivity: --force'); + return 1; + } + } else { + $output->writeln('⚠ Skipping connectivity checks (--force).'); + } + + $output->writeln(''); + $output->writeln('Collabora URL (used for Nextcloud to contact the Collabora server):'); + $output->writeln(' ' . $this->appConfig->getCollaboraUrlInternal()); + $output->writeln('Collabora public URL (used in the browser to open Collabora):'); + $output->writeln(' ' . $this->appConfig->getCollaboraUrlPublic()); + $output->writeln('Callback URL (used by Collabora to connect back to Nextcloud):'); + $callbackUrl = $this->appConfig->getNextcloudUrl(); + $output->writeln($callbackUrl === '' + ? ' autodetected (will use the same URL as your user for browsing Nextcloud)' + : ' ' . $callbackUrl); + + return 0; + } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index d633f192ae..fdf3fe1026 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -120,12 +120,16 @@ private function getSettingsData(): array { 'esignature_base_url' => $this->appConfig->getAppValue('esignature_base_url'), 'esignature_client_id' => $this->appConfig->getAppValue('esignature_client_id'), 'esignature_secret' => $this->appConfig->getAppValue('esignature_secret'), - 'userId' => $this->userId + 'userId' => $this->userId, + 'server_mode' => $this->appConfig->getServerMode(), + 'builtin_server_url' => $this->appConfig->getBuiltinServerUrl(), ]; } public function setSettings( ?string $wopi_url, + ?string $server_mode, + ?bool $force, // skip connectivity checks ?string $wopi_allowlist, ?bool $disable_certificate_verification, ?string $edit_groups, @@ -137,8 +141,59 @@ public function setSettings( ?string $esignature_client_id, ?string $esignature_secret, ): JSONResponse { + if ($server_mode === 'builtin') { + $builtinUrl = $this->appConfig->getBuiltinServerUrl(); + if ($builtinUrl === null) { + return new JSONResponse([ + 'status' => 'error', + 'data' => ['message' => 'Built-in CODE server is not installed or not supported on this platform.'], + ], Http::STATUS_BAD_REQUEST); + } + + // Capture full previous state before any mutation for symmetric rollback + $snapshot = [ + AppConfig::SERVER_MODE => $this->appConfig->getServerMode(), + AppConfig::WOPI_URL => $this->config->getAppValue('richdocuments', AppConfig::WOPI_URL, ''), + AppConfig::PUBLIC_WOPI_URL => $this->config->getAppValue('richdocuments', AppConfig::PUBLIC_WOPI_URL, ''), + 'disable_certificate_verification' => $this->config->getAppValue( + 'richdocuments', 'disable_certificate_verification', ''), + ]; + + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, 'builtin'); + $this->appConfig->setAppValue('disable_certificate_verification', 'yes'); + $this->config->deleteAppValue('richdocuments', AppConfig::WOPI_URL); + $this->config->deleteAppValue('richdocuments', AppConfig::PUBLIC_WOPI_URL); + + if (!$force) { + try { + $output = new NullOutput(); + $this->connectivityService->testUrl($builtinUrl, $output); + } catch (\Throwable $e) { + foreach ($snapshot as $key => $value) { + if ($value !== '') { + $this->config->setAppValue('richdocuments', $key, $value); + } else { + $this->config->deleteAppValue('richdocuments', $key); + } + } + return new JSONResponse([ + 'status' => 'error', + 'data' => ['message' => 'Failed to connect to built-in CODE server: ' . $e->getMessage()], + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + return new JSONResponse([ + 'status' => 'success', + 'data' => ['message' => $this->l10n->t('Saved'), 'settings' => $this->getSettingsData()], + ]); + } + if ($wopi_url !== null) { - $this->appConfig->setAppValue('wopi_url', $wopi_url); + $this->appConfig->setAppValue(AppConfig::WOPI_URL, $wopi_url); + // Use the provided server_mode if given; default to 'custom' for + // backward-compatible callers that don't send it (e.g. direct API calls). + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, $server_mode ?? 'custom'); } if ($wopi_allowlist !== null) { @@ -146,10 +201,8 @@ public function setSettings( } if ($disable_certificate_verification !== null) { - $this->appConfig->setAppValue( - 'disable_certificate_verification', - $disable_certificate_verification ? 'yes' : '' - ); + $this->appConfig->setAppValue('disable_certificate_verification', + $disable_certificate_verification ? 'yes' : ''); } if ($edit_groups !== null) { @@ -184,27 +237,24 @@ public function setSettings( $this->appConfig->setAppValue('esignature_secret', $esignature_secret); } - try { - $output = new NullOutput(); - $this->connectivityService->testDiscovery($output); - $this->connectivityService->testCapabilities($output); - $this->connectivityService->autoConfigurePublicUrl(); - } catch (\Throwable $e) { - return new JSONResponse([ - 'status' => 'error', - 'data' => ['message' => 'Failed to connect to the remote server: ' . $e->getMessage()] - ], 500); + if (!$force) { + try { + $output = new NullOutput(); + $this->connectivityService->testDiscovery($output); + $this->connectivityService->testCapabilities($output); + $this->connectivityService->autoConfigurePublicUrl(); + } catch (\Throwable $e) { + return new JSONResponse([ + 'status' => 'error', + 'data' => ['message' => 'Failed to connect to the remote server: ' . $e->getMessage()] + ], 500); + } } - $response = [ + return new JSONResponse([ 'status' => 'success', - 'data' => [ - 'message' => $this->l10n->t('Saved'), - 'settings' => $this->getSettingsData(), - ] - ]; - - return new JSONResponse($response); + 'data' => ['message' => $this->l10n->t('Saved'), 'settings' => $this->getSettingsData()], + ]); } public function updateWatermarkSettings($settings = []): JSONResponse { diff --git a/lib/Service/ConnectivityService.php b/lib/Service/ConnectivityService.php index cbe6b9c93e..0ea330e0aa 100644 --- a/lib/Service/ConnectivityService.php +++ b/lib/Service/ConnectivityService.php @@ -49,14 +49,66 @@ public function testCapabilities(OutputInterface $output): void { } /** - * Detect public URL of the WOPI server for setting CSP on Nextcloud + * Test discovery and capabilities reachability against an explicit URL. + * + * This temporarily forces custom mode so AppConfig resolves the provided + * wopi_url directly instead of builtin-mode derivation logic. + * Previous config values are always restored afterwards. + */ + public function testUrl(string $wopiUrl, OutputInterface $output): void { + // Temporarily override the URL for the duration of this test by driving + // DiscoveryService and CapabilitiesService directly with the given URL, + // rather than going through AppConfig. + $previousUrl = $this->appConfig->getAppValue(AppConfig::WOPI_URL, ''); + $previousMode = $this->appConfig->getAppValue(AppConfig::SERVER_MODE, ''); + + // Force explicit-URL resolution through the stored wopi_url path. + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, 'custom'); + $this->appConfig->setAppValue(AppConfig::WOPI_URL, $wopiUrl); + + try { + $this->testDiscovery($output); + $this->testCapabilities($output); + } finally { + // Always restore, whether the test passed or threw + if ($previousMode !== '') { + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, $previousMode); + } else { + // TODO: rename "appConfig" (which isn't; it's not actually the Server AppFramework version I expected)) + $this->appConfig->setAppValue(AppConfig::SERVER_MODE, ''); + } + + if ($previousUrl !== '') { + $this->appConfig->setAppValue(AppConfig::WOPI_URL, $previousUrl); + } else { + $this->appConfig->setAppValue(AppConfig::WOPI_URL, ''); + } + } + } + + /** + * Detect public URL of the WOPI server for setting CSP on Nextcloud. * * This value is not meant to be set manually. If this turns out to be the wrong URL - * it is likely a misconfiguration on your WOPI server. Collabora will inherit the URL to use - * form the request and the ssl.enable/ssl.termination settings and server_name (if configured) + * it is likely a misconfiguration either of the Collabora (i.e. server_name) or + * Nextcloud itself (i.e. overwrite.cli.url). + * + * Skipped for the built-in CODE server: public_wopi_url for builtin is always + * Nextcloud's own public origin, derived directly from IURLGenerator in AppConfig. + * Running discovery-based detection server-side would be redundant and would produce + * incorrect results in CLI context where overwrite.cli.url may differ from the + * public-facing URL that CODE's ProxyPrefix would return to a browser. + * + * For standalone Collabora, server_name in coolwsd.xml makes urlsrc deterministic + * regardless of request context, so server-side detection remains appropriate. */ public function autoConfigurePublicUrl(): void { - $determinedUrl = $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + if ($this->appConfig->isBuiltinServer()) { + return; + } + $determinedUrl = $this->parser->getUrlSrcValue( + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + ); $detectedUrl = $this->appConfig->domainOnly($determinedUrl); $this->appConfig->setAppValue('public_wopi_url', $detectedUrl); } diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 067876f481..78a2ebde1c 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -41,6 +41,8 @@ public function getForm(): TemplateResponse { 'admin', [ 'settings' => [ + 'server_mode' => $this->appConfig->getServerMode(), + 'builtin_server_url' => $this->appConfig->getBuiltinServerUrl(), 'wopi_url' => $this->appConfig->getCollaboraUrlInternal(), 'public_wopi_url' => $this->appConfig->getCollaboraUrlPublic(), 'wopi_callback_url' => $this->appConfig->getNextcloudUrl(), diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 9f8c331ec8..ea63b5595b 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -662,7 +662,6 @@ export default { try { result = await axios.get(generateUrl('/apps/richdocuments/settings/check')) this.serverError = SERVER_STATE_OK - } catch (e) { this.serverError = SERVER_STATE_CONNECTION_ERROR result = e.response @@ -675,17 +674,46 @@ export default { const { settings } = result?.data?.data || {} for (const settingKey in settings) { if (settingKey === 'use_groups' || settingKey === 'edit_groups') { - this.settings[settingKey] = settings[settingKey] ? settings[settingKey].split('|') : [] + this.settings[settingKey] = settings[settingKey] + ? settings[settingKey].split('|') + : [] continue } this.settings[settingKey] = settings[settingKey] } + + this.checkIfDemoServerIsActive() this.checkFrontend() }, async checkFrontend() { try { - await fetch(this.settings.public_wopi_url + '/hosting/discovery', { mode: 'no-cors' }) - await fetch(this.settings.public_wopi_url + '/hosting/capabilities', { mode: 'no-cors' }) + // For builtin: proxy.php is same-origin so the discovery response is fully readable. + if (this.serverMode === 'builtin' && this.settings.wopi_url) { + // Full read (no mode: 'no-cors') because proxy.php is same-origin. + // We parse the response only to surface a clear error if CODE is + // unreachable or returning unexpected content; no writeback needed. + const discoveryRes = await fetch( + this.settings.wopi_url + '/hosting/discovery' + ) + if (!discoveryRes.ok) { + this.serverError = SERVER_STATE_BROWSER_CONNECTION_ERROR + return + } + // Verify capabilities endpoint also reachable from browser + await fetch( + this.settings.wopi_url + '/hosting/capabilities', + { mode: 'no-cors' } + ) + } else { + await fetch( + this.settings.public_wopi_url + '/hosting/discovery', + { mode: 'no-cors' } + ) + await fetch( + this.settings.public_wopi_url + '/hosting/capabilities', + { mode: 'no-cors' } + ) + } } catch (e) { console.error(e) this.serverError = SERVER_STATE_BROWSER_CONNECTION_ERROR @@ -823,15 +851,17 @@ export default { } }, checkIfDemoServerIsActive() { - this.settings.demoUrl = this.demoServers ? this.demoServers.find((server) => server.demo_url === this.settings.wopi_url) : null - this.settings.CODEUrl = this.CODEInstalled ? window.location.protocol + '//' + window.location.host + generateFilePath(this.CODEAppID, '', '') + 'proxy.php?req=' : null + this.settings.demoUrl = this.demoServers + ? this.demoServers.find((server) => server.demo_url === this.settings.wopi_url) + : null + if (this.settings.wopi_url && this.settings.wopi_url !== '') { this.serverMode = 'custom' } if (this.settings.demoUrl) { this.serverMode = 'demo' this.approvedDemoModal = true - } else if (this.settings.CODEUrl && this.settings.CODEUrl === this.settings.wopi_url) { + } else if (this.settings.server_mode === 'builtin') { this.serverMode = 'builtin' } }, @@ -841,12 +871,21 @@ export default { async setDemoServer(server) { this.settings.wopi_url = server.demo_url this.settings.disable_certificate_verification = false - await this.updateServer() + await this.updateSettings({ + server_mode: 'demo', + wopi_url: server.demo_url, + disable_certificate_verification: false, + }) + this.checkIfDemoServerIsActive() }, + // Tell the server to activate builtin mode; it derives wopi_url via IURLGenerator. async setBuiltinServer() { - this.settings.wopi_url = this.settings.CODEUrl - this.settings.disable_certificate_verification = false - await this.updateServer() + await this.updateSettings({ + server_mode: 'builtin', + //disable_certificate_verification: false, + }) + // updateSettings() applies the returned settings (including server_mode, + // wopi_url, builtin_server_url) and calls checkFrontend(); no extra work needed. }, checkUrlProtocol(string) { let url diff --git a/tests/lib/AppConfigTest.php b/tests/lib/AppConfigTest.php index 03fd045093..4565a9c366 100644 --- a/tests/lib/AppConfigTest.php +++ b/tests/lib/AppConfigTest.php @@ -12,6 +12,7 @@ use OCP\AppFramework\Services\IAppConfig; use OCP\GlobalScale\IConfig as IGlobalScaleConfig; use OCP\IConfig; +use OCP\IURLGenerator; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -20,6 +21,7 @@ class AppConfigTest extends TestCase { private $config; /** @var IAppConfig */ private $appConfig; + private IURLGenerator|MockObject $urlGenerator; public function setUp(): void { parent::setUp(); @@ -27,8 +29,9 @@ public function setUp(): void { $this->appManager = $this->createMock(IAppManager::class); $this->appConfig = $this->createMock(IAppConfig::class); $this->gsConfig = $this->createMock(IGlobalScaleConfig::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); - $this->appConfig = new AppConfig($this->config, $this->appConfig, $this->appManager, $this->gsConfig); + $this->appConfig = new AppConfig($this->config, $this->appConfig, $this->appManager, $this->gsConfig, $this->urlGenerator); } public function testGetAppValueArrayWithValues() { diff --git a/tests/lib/Listener/AddContentSecurityPolicyListenerTest.php b/tests/lib/Listener/AddContentSecurityPolicyListenerTest.php index 0de81b8a54..4f920b95ad 100644 --- a/tests/lib/Listener/AddContentSecurityPolicyListenerTest.php +++ b/tests/lib/Listener/AddContentSecurityPolicyListenerTest.php @@ -18,6 +18,7 @@ use OCP\GlobalScale\IConfig as GlobalScaleConfig; use OCP\IConfig; use OCP\IRequest; +use OCP\IURLGenerator; use OCP\Security\CSP\AddContentSecurityPolicyEvent; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -36,6 +37,7 @@ class AddContentSecurityPolicyListenerTest extends TestCase { private $gsConfig; /** @var FederationService|MockObject */ private $federationService; + private IURLGenerator|MockObject $urlGenerator; private CapabilitiesService|MockObject $capabilitiesService; private AddContentSecurityPolicyListener $listener; @@ -45,6 +47,7 @@ public function setUp(): void { $this->appManager = $this->createMock(IAppManager::class); $this->gsConfig = $this->createMock(GlobalScaleConfig::class); $this->federationService = $this->createMock(FederationService::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->overwriteService(FederationService::class, $this->federationService); @@ -58,6 +61,7 @@ public function setUp(): void { $this->createMock(IAppConfig::class), $this->appManager, $this->gsConfig, + $this->urlGenerator, ]) ->onlyMethods(['getCollaboraUrlPublic', 'getGlobalScaleTrustedHosts']) ->getMock();