Skip to content

Commit 40c4e16

Browse files
committed
feat: Add setup checks and verify WOPI connectivity
Signed-off-by: Julius Knorr <jus@bitgrid.net>
1 parent ae14806 commit 40c4e16

12 files changed

Lines changed: 233 additions & 28 deletions

File tree

.github/workflows/cypress-e2e.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,10 @@ jobs:
127127
php occ app:enable --force richdocuments
128128
php occ app:list
129129
php occ config:system:set trusted_domains 1 --value="172.17.0.1"
130+
php occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
130131
131-
php occ config:app:set richdocuments wopi_url --value="http://localhost:9980"
132-
php occ config:app:set richdocuments public_wopi_url --value="http://localhost:9980"
133132
php occ config:system:set allow_local_remote_servers --value true --type bool
134-
php occ richdocuments:activate-config
133+
php occ richdocuments:activate-config --wopi-url="http://localhost:9980" --callback-url="http://172.17.0.1:8081"
135134
136135
curl http://admin:admin@localhost:8081/ocs/v1.php/cloud/capabilities\?format\=json -H 'OCS-APIRequest: true'
137136

.github/workflows/integration.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108
mkdir data
109109
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
110110
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
111+
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
111112
./occ app:enable --force ${{ env.APP_NAME }}
112113
113114
- name: Run ${{ matrix.scenarios }} integration tests
@@ -187,6 +188,7 @@ jobs:
187188
mkdir data
188189
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
189190
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
191+
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
190192
./occ app:enable --force ${{ env.APP_NAME }}
191193
192194
- name: Run ${{ matrix.scenarios }} integration tests
@@ -268,6 +270,7 @@ jobs:
268270
mkdir data
269271
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
270272
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
273+
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
271274
./occ app:enable --force ${{ env.APP_NAME }}
272275
273276
- name: Run ${{ matrix.scenarios }} integration tests
@@ -358,6 +361,7 @@ jobs:
358361
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
359362
./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=1521 --database-user=autotest --database-pass=owncloud --admin-user admin --admin-pass password
360363
php -f index.php
364+
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
361365
./occ app:enable --force ${{ env.APP_NAME }}
362366
363367
- name: Run ${{ matrix.scenarios }} integration tests

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: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
use OCP\PreConditionNotMetException;
3434
use OCP\Util;
3535
use Psr\Log\LoggerInterface;
36-
use Symfony\Component\Console\Output\NullOutput;
3736

3837
class SettingsController extends Controller {
3938
// TODO adapt overview generation if we add more font mimetypes
@@ -69,9 +68,7 @@ public function __construct(
6968

7069
public function checkSettings(): DataResponse {
7170
try {
72-
$output = new NullOutput();
73-
$this->connectivityService->testDiscovery($output);
74-
$this->connectivityService->testCapabilities($output);
71+
$this->connectivityService->test();
7572
} catch (\Exception $e) {
7673
$this->logger->error($e->getMessage(), ['exception' => $e]);
7774
return new DataResponse([
@@ -184,9 +181,7 @@ public function setSettings(
184181
}
185182

186183
try {
187-
$output = new NullOutput();
188-
$this->connectivityService->testDiscovery($output);
189-
$this->connectivityService->testCapabilities($output);
184+
$this->connectivityService->test();
190185
$this->connectivityService->autoConfigurePublicUrl();
191186
} catch (\Throwable $e) {
192187
return new JSONResponse([

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: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,94 @@
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+
'SelfSignedCertificate' => $this->l10n->t('The certificate of the response is self-signed and not trusted by the system'),
82+
'ExpiredCertificate' => $this->l10n->t('The certificate of the response is expired'),
83+
'SslHandshakeFail' => $this->l10n->t('Couldn’t establish an SSL/TLS connection'),
84+
'MissingSsl' => $this->l10n->t('The response wasn’t using SSL/TLS contrary to expected'),
85+
'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.'),
86+
'NoScheme' => $this->l10n->t('A scheme (http:// or https://) for the WOPI host URL must be specified'),
87+
'Timeout' => $this->l10n->t('The request didn’t get a response within the time frame allowed'),
88+
default => $this->l10n->t('Unknown error. Check the server logs of Collabora for more details.'),
89+
};
90+
91+
if ($errorMessage) {
92+
throw new \Exception(
93+
$this->l10n->t('The Collabora server could not properly reach the Nextcloud server at %s', [$callbackUrl]) . ' ' . $errorMessage
94+
);
95+
}
96+
97+
$output?->writeln('WOPI access was verified');
4898
}
4999

50100
/**
@@ -59,4 +109,13 @@ public function autoConfigurePublicUrl(): void {
59109
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
60110
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
61111
}
112+
113+
/**
114+
* @throws Exception
115+
*/
116+
public function test(?OutputInterface $output = null): void {
117+
$this->testDiscovery($output);
118+
$this->testCapabilities($output);
119+
$this->testWopiAccess($output);
120+
}
62121
}

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+
}
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+
}

tests/psalm-baseline.xml

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@
4141
<code><![CDATA[$app !== '']]></code>
4242
</RedundantCondition>
4343
</file>
44-
<file src="lib/Controller/SettingsController.php">
45-
<UndefinedClass>
46-
<code><![CDATA[NullOutput]]></code>
47-
<code><![CDATA[NullOutput]]></code>
48-
</UndefinedClass>
49-
</file>
5044
<file src="lib/Controller/TemplatesController.php">
5145
<InvalidReturnStatement>
5246
<code><![CDATA[$this->fetchPreview($template, $x, $y, $a, $forceIcon, $mode)]]></code>
@@ -83,10 +77,13 @@
8377
</MissingDependency>
8478
</file>
8579
<file src="lib/Service/ConnectivityService.php">
86-
<UndefinedClass>
87-
<code><![CDATA[OutputInterface]]></code>
88-
<code><![CDATA[OutputInterface]]></code>
89-
</UndefinedClass>
80+
<InvalidCatch>
81+
<code><![CDATA[try {
82+
$result = $client->post($url, ['body' => json_encode(['callbackUrl' => $callbackUrl . '/status.php']), 'headers' => ['Content-Type' => 'application/json']]);
83+
} catch (ClientException $e) {
84+
$result = $e->getResponse();
85+
}]]></code>
86+
</InvalidCatch>
9087
</file>
9188
<file src="lib/Service/FederationService.php">
9289
<TypeDoesNotContainType>

0 commit comments

Comments
 (0)