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();