Skip to content

Commit 1564b81

Browse files
committed
feat: Add setup checks and verify WOPI connectivity
Signed-off-by: Julius Knorr <[email protected]>
1 parent 20e836c commit 1564b81

File tree

10 files changed

+227
-47
lines changed

10 files changed

+227
-47
lines changed

lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
use OCA\Richdocuments\Preview\OpenDocument;
3333
use OCA\Richdocuments\Preview\Pdf;
3434
use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider;
35+
use OCA\Richdocuments\SetupCheck\CollaboraUpdate;
36+
use OCA\Richdocuments\SetupCheck\ConnectivityCheck;
3537
use OCA\Richdocuments\Template\CollaboraTemplateProvider;
3638
use OCA\Viewer\Event\LoadViewer;
3739
use OCP\AppFramework\App;
@@ -84,6 +86,8 @@ public function register(IRegistrationContext $context): void {
8486
$context->registerPreviewProvider(Pdf::class, Pdf::MIMETYPE_REGEX);
8587
$context->registerFileConversionProvider(ConversionProvider::class);
8688
$context->registerNotifierService(Notifier::class);
89+
$context->registerSetupCheck(CollaboraUpdate::class);
90+
$context->registerSetupCheck(ConnectivityCheck::class);
8791
}
8892

8993
public function boot(IBootContext $context): void {

lib/Command/ActivateConfig.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7373
return 1;
7474
}
7575

76+
try {
77+
$this->connectivityService->testWopiAccess($output);
78+
} catch (\Throwable $e) {
79+
$output->writeln('<error>Failed to verify WOPI connectivity');
80+
$output->writeln($e->getMessage());
81+
return 1;
82+
}
83+
7684
try {
7785
$this->connectivityService->autoConfigurePublicUrl();
7886
} catch (\Throwable $e) {

lib/Controller/SettingsController.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
use OCP\PreConditionNotMetException;
3333
use OCP\Util;
3434
use Psr\Log\LoggerInterface;
35-
use Symfony\Component\Console\Output\NullOutput;
3635

3736
class SettingsController extends Controller {
3837
// TODO adapt overview generation if we add more font mimetypes
@@ -67,9 +66,7 @@ public function __construct(
6766

6867
public function checkSettings(): DataResponse {
6968
try {
70-
$output = new NullOutput();
71-
$this->connectivityService->testDiscovery($output);
72-
$this->connectivityService->testCapabilities($output);
69+
$this->connectivityService->test();
7370
} catch (\Exception $e) {
7471
$this->logger->error($e->getMessage(), ['exception' => $e]);
7572
return new DataResponse([
@@ -182,9 +179,7 @@ public function setSettings(
182179
}
183180

184181
try {
185-
$output = new NullOutput();
186-
$this->connectivityService->testDiscovery($output);
187-
$this->connectivityService->testCapabilities($output);
182+
$this->connectivityService->test();
188183
$this->connectivityService->autoConfigurePublicUrl();
189184
} catch (\Throwable $e) {
190185
return new JSONResponse([
@@ -504,7 +499,7 @@ public function getSettingsFile(string $type, string $token, string $category, s
504499
return new DataDisplayResponse('Something went wrong', 500);
505500
}
506501
}
507-
502+
508503

509504
/**
510505
* @param string $key

lib/Service/CapabilitiesService.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ public function getProductName(): string {
112112
return $this->l10n->t('Nextcloud Office');
113113
}
114114

115+
public function isEnterprise(): bool {
116+
return ($this->getCapabilities()['productName'] ?? false) === 'Collabora Online';
117+
}
118+
119+
public function isCode(): bool {
120+
return ($this->getCapabilities()['productName'] ?? false) === 'Collabora Online Development Edition';
121+
}
122+
123+
public function hasWopiAccessCheck(): bool {
124+
return $this->getCapabilities()['hasWopiAccessCheck'] ?? false;
125+
}
126+
115127
public function hasOtherOOXMLApps(): bool {
116128
if ($this->appManager->isEnabledForUser('officeonline')) {
117129
return true;

lib/Service/ConnectivityService.php

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,92 @@
77
namespace OCA\Richdocuments\Service;
88

99
use Exception;
10+
use GuzzleHttp\Exception\ClientException;
1011
use OCA\Richdocuments\AppConfig;
1112
use OCA\Richdocuments\WOPI\Parser;
13+
use OCP\Http\Client\IClientService;
14+
use OCP\IL10N;
15+
use OCP\IURLGenerator;
1216
use Symfony\Component\Console\Output\OutputInterface;
1317

1418
class ConnectivityService {
1519
public function __construct(
1620
private AppConfig $appConfig,
1721
private DiscoveryService $discoveryService,
1822
private CapabilitiesService $capabilitiesService,
23+
private IClientService $clientService,
24+
private IURLGenerator $urlGenerator,
1925
private Parser $parser,
26+
private IL10N $l10n,
2027
) {
2128
}
2229

2330
/**
2431
* @throws Exception
2532
*/
26-
public function testDiscovery(OutputInterface $output): void {
33+
public function testDiscovery(?OutputInterface $output = null): void {
2734
$this->discoveryService->resetCache();
2835
$this->discoveryService->fetch();
29-
$output->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');
36+
$output?->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');
3037

3138
$this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
32-
$output->writeln('<info>✓ Valid mimetype response</info>');
39+
$output?->writeln('<info>✓ Valid mimetype response</info>');
3340

3441
// FIXME: Optional when allowing generic WOPI servers
3542
$this->parser->getUrlSrcValue('Capabilities');
36-
$output->writeln('<info>✓ Valid capabilities entry</info>');
43+
$output?->writeln('<info>✓ Valid capabilities entry</info>');
3744
}
3845

39-
public function testCapabilities(OutputInterface $output): void {
46+
public function testCapabilities(?OutputInterface $output = null): void {
4047
$this->capabilitiesService->resetCache();
4148
$this->capabilitiesService->fetch();
42-
$output->writeln('<info>✓ Fetched /hosting/capabilities endpoint</info>');
49+
$output?->writeln('<info>✓ Fetched /hosting/capabilities endpoint</info>');
4350

4451
if ($this->capabilitiesService->getCapabilities() === []) {
4552
throw new \Exception('Empty capabilities, unexpected result from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
4653
}
47-
$output->writeln('<info>✓ Detected WOPI server: ' . $this->capabilitiesService->getServerProductName() . ' ' . $this->capabilitiesService->getProductVersion() . '</info>');
54+
$output?->writeln('<info>✓ Detected WOPI server: ' . $this->capabilitiesService->getServerProductName() . ' ' . $this->capabilitiesService->getProductVersion() . '</info>');
55+
}
56+
57+
public function testWopiAccess(?OutputInterface $output = null): void {
58+
$client = $this->clientService->newClient();
59+
60+
if (!$this->capabilitiesService->hasWopiAccessCheck()) {
61+
return;
62+
}
63+
64+
$url = str_replace('/hosting/capabilities', '/hosting/wopiAccessCheck', $this->capabilitiesService->getCapabilitiesEndpoint());
65+
$callbackUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');
66+
67+
try {
68+
$result = $client->post($url, ['body' => json_encode(['callbackUrl' => $callbackUrl . '/status.php']), 'headers' => ['Content-Type' => 'application/json']]);
69+
} catch (ClientException $e) {
70+
$result = $e->getResponse();
71+
}
72+
$response = json_decode($result->getBody(), true);
73+
74+
$errorMessage = match ($response['status']) {
75+
'Ok' => null,
76+
'NotHttpSuccess' => $this->l10n->t('The connection was successful but the response to the request was not 200'),
77+
'HostNotFound' => $this->l10n->t('DNS error, the host is not known by the Collabora Online server'),
78+
'WopiHostNotAllowed' => $this->l10n->t('The host for this request is not allowed to be used as a WOPI Host, this is likely a configuration issue in coolwsd.xml'),
79+
'ConnectionAborted' => $this->l10n->t('The connection was aborted by the destination server'),
80+
'CertificateValidation' => $this->l10n->t('The certificate of the response is invalid or otherwise not accepted'),
81+
'SslHandshakeFail' => $this->l10n->t('Couldn’t establish an SSL/TLS connection'),
82+
'MissingSsl' => $this->l10n->t('The response wasn’t using SSL/TLS contrary to expected'),
83+
'NotHttps' => $this->l10n->t('HTTPS is expected to connect to Collabora Online as the WOPI host uses it. This is necessary to prevent mixed content errors.'),
84+
'NoScheme' => $this->l10n->t('A scheme (http:// or https://) for the WOPI host URL must be specified'),
85+
'Timeout' => $this->l10n->t('The request didn’t get a response within the time frame allowed'),
86+
default => $this->l10n->t('Unknown error. Check the server logs of Collabora for more details.'),
87+
};
88+
89+
if ($errorMessage) {
90+
throw new \Exception(
91+
$this->l10n->t('The Collabora server could not properly reach the Nextcloud server.') . ' ' . $errorMessage
92+
);
93+
}
94+
95+
$output?->writeln('WOPI access was verified');
4896
}
4997

5098
/**
@@ -59,4 +107,13 @@ public function autoConfigurePublicUrl(): void {
59107
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
60108
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
61109
}
110+
111+
/**
112+
* @throws Exception
113+
*/
114+
public function test(?OutputInterface $output = null): void {
115+
$this->testDiscovery($output);
116+
$this->testCapabilities($output);
117+
$this->testWopiAccess($output);
118+
}
62119
}

lib/SetupCheck/CollaboraUpdate.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\Richdocuments\SetupCheck;
10+
11+
use OCA\Richdocuments\Service\CapabilitiesService;
12+
use OCP\Http\Client\IClientService;
13+
use OCP\IL10N;
14+
use OCP\IURLGenerator;
15+
use OCP\SetupCheck\ISetupCheck;
16+
use OCP\SetupCheck\SetupResult;
17+
use Psr\Log\LoggerInterface;
18+
19+
class CollaboraUpdate implements ISetupCheck {
20+
21+
public function __construct(
22+
protected IL10N $l10n,
23+
protected CapabilitiesService $capabilitiesService,
24+
protected IURLGenerator $urlGenerator,
25+
protected IClientService $clientService,
26+
protected LoggerInterface $logger,
27+
) {
28+
}
29+
30+
public function getCategory(): string {
31+
return 'office';
32+
}
33+
34+
public function getName(): string {
35+
return $this->l10n->t('Collabora server version check');
36+
}
37+
38+
public function run(): SetupResult {
39+
$client = $this->clientService->newClient();
40+
41+
$url = null;
42+
if ($this->capabilitiesService->isCode()) {
43+
$url = 'https://rating.collaboraonline.com/UpdateCheck';
44+
}
45+
46+
if ($this->capabilitiesService->isEnterprise()) {
47+
$url = 'https://rating.collaboraonline.com/UpdateCheck?product=cool';
48+
}
49+
50+
if ($url === null) {
51+
return SetupResult::success();
52+
}
53+
54+
// FIXME internet conection check config
55+
56+
$response = $client->get($url, ['timeout' => 5]);
57+
$response = json_decode($response->getBody(), true);
58+
$latestVersion = $response['coolwsd_version'] ?? null;
59+
60+
$installedVersion = $this->capabilitiesService->getProductVersion();
61+
62+
if ($latestVersion !== null && version_compare($latestVersion, $installedVersion, '>')) {
63+
return SetupResult::warning($this->l10n->t('Collabora server version is out of date. Currently using %s, new version is available: %s', [$installedVersion, $latestVersion]));
64+
}
65+
66+
return SetupResult::success();
67+
}
68+
}

lib/SetupCheck/ConnectivityCheck.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OCA\Richdocuments\SetupCheck;
10+
11+
use OCA\Richdocuments\Service\ConnectivityService;
12+
use OCP\Http\Client\IClientService;
13+
use OCP\IL10N;
14+
use OCP\IURLGenerator;
15+
use OCP\SetupCheck\ISetupCheck;
16+
use OCP\SetupCheck\SetupResult;
17+
use Psr\Log\LoggerInterface;
18+
19+
class ConnectivityCheck implements ISetupCheck {
20+
21+
public function __construct(
22+
protected IL10N $l10n,
23+
protected ConnectivityService $connectivityService,
24+
protected IURLGenerator $urlGenerator,
25+
protected IClientService $clientService,
26+
protected LoggerInterface $logger,
27+
) {
28+
}
29+
30+
public function getCategory(): string {
31+
return 'office';
32+
}
33+
34+
public function getName(): string {
35+
return $this->l10n->t('Collabora server connectivity check');
36+
}
37+
38+
public function run(): SetupResult {
39+
try {
40+
$this->connectivityService->test();
41+
} catch (\Exception $e) {
42+
return SetupResult::error($this->l10n->t('Collabora is not configured properly:') . ' ' . $e->getMessage());
43+
}
44+
45+
return SetupResult::success();
46+
}
47+
}

0 commit comments

Comments
 (0)