Skip to content

Commit

Permalink
feat: Add setup checks and verify WOPI connectivity
Browse files Browse the repository at this point in the history
Signed-off-by: Julius Knorr <[email protected]>
  • Loading branch information
juliusknorr committed Feb 14, 2025
1 parent 20e836c commit 1564b81
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 47 deletions.
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
use OCA\Richdocuments\Preview\OpenDocument;
use OCA\Richdocuments\Preview\Pdf;
use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider;
use OCA\Richdocuments\SetupCheck\CollaboraUpdate;
use OCA\Richdocuments\SetupCheck\ConnectivityCheck;
use OCA\Richdocuments\Template\CollaboraTemplateProvider;
use OCA\Viewer\Event\LoadViewer;
use OCP\AppFramework\App;
Expand Down Expand Up @@ -84,6 +86,8 @@ public function register(IRegistrationContext $context): void {
$context->registerPreviewProvider(Pdf::class, Pdf::MIMETYPE_REGEX);
$context->registerFileConversionProvider(ConversionProvider::class);
$context->registerNotifierService(Notifier::class);
$context->registerSetupCheck(CollaboraUpdate::class);
$context->registerSetupCheck(ConnectivityCheck::class);
}

public function boot(IBootContext $context): void {
Expand Down
8 changes: 8 additions & 0 deletions lib/Command/ActivateConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 1;
}

try {
$this->connectivityService->testWopiAccess($output);
} catch (\Throwable $e) {
$output->writeln('<error>Failed to verify WOPI connectivity');
$output->writeln($e->getMessage());
return 1;
}

try {
$this->connectivityService->autoConfigurePublicUrl();
} catch (\Throwable $e) {
Expand Down
11 changes: 3 additions & 8 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
use OCP\PreConditionNotMetException;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\NullOutput;

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

public function checkSettings(): DataResponse {
try {
$output = new NullOutput();
$this->connectivityService->testDiscovery($output);
$this->connectivityService->testCapabilities($output);
$this->connectivityService->test();
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([
Expand Down Expand Up @@ -182,9 +179,7 @@ public function setSettings(
}

try {
$output = new NullOutput();
$this->connectivityService->testDiscovery($output);
$this->connectivityService->testCapabilities($output);
$this->connectivityService->test();
$this->connectivityService->autoConfigurePublicUrl();
} catch (\Throwable $e) {
return new JSONResponse([
Expand Down Expand Up @@ -504,7 +499,7 @@ public function getSettingsFile(string $type, string $token, string $category, s
return new DataDisplayResponse('Something went wrong', 500);
}
}


/**
* @param string $key
Expand Down
12 changes: 12 additions & 0 deletions lib/Service/CapabilitiesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ public function getProductName(): string {
return $this->l10n->t('Nextcloud Office');
}

public function isEnterprise(): bool {
return ($this->getCapabilities()['productName'] ?? false) === 'Collabora Online';
}

public function isCode(): bool {
return ($this->getCapabilities()['productName'] ?? false) === 'Collabora Online Development Edition';
}

public function hasWopiAccessCheck(): bool {
return $this->getCapabilities()['hasWopiAccessCheck'] ?? false;
}

public function hasOtherOOXMLApps(): bool {
if ($this->appManager->isEnabledForUser('officeonline')) {
return true;
Expand Down
71 changes: 64 additions & 7 deletions lib/Service/ConnectivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,92 @@
namespace OCA\Richdocuments\Service;

use Exception;
use GuzzleHttp\Exception\ClientException;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\WOPI\Parser;
use OCP\Http\Client\IClientService;
use OCP\IL10N;
use OCP\IURLGenerator;
use Symfony\Component\Console\Output\OutputInterface;

class ConnectivityService {
public function __construct(
private AppConfig $appConfig,
private DiscoveryService $discoveryService,
private CapabilitiesService $capabilitiesService,
private IClientService $clientService,
private IURLGenerator $urlGenerator,
private Parser $parser,
private IL10N $l10n,
) {
}

/**
* @throws Exception
*/
public function testDiscovery(OutputInterface $output): void {
public function testDiscovery(?OutputInterface $output = null): void {
$this->discoveryService->resetCache();
$this->discoveryService->fetch();
$output->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');
$output?->writeln('<info>✓ Fetched /hosting/discovery endpoint</info>');

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

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

public function testCapabilities(OutputInterface $output): void {
public function testCapabilities(?OutputInterface $output = null): void {
$this->capabilitiesService->resetCache();
$this->capabilitiesService->fetch();
$output->writeln('<info>✓ Fetched /hosting/capabilities endpoint</info>');
$output?->writeln('<info>✓ Fetched /hosting/capabilities endpoint</info>');

if ($this->capabilitiesService->getCapabilities() === []) {
throw new \Exception('Empty capabilities, unexpected result from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
}
$output->writeln('<info>✓ Detected WOPI server: ' . $this->capabilitiesService->getServerProductName() . ' ' . $this->capabilitiesService->getProductVersion() . '</info>');
$output?->writeln('<info>✓ Detected WOPI server: ' . $this->capabilitiesService->getServerProductName() . ' ' . $this->capabilitiesService->getProductVersion() . '</info>');
}

public function testWopiAccess(?OutputInterface $output = null): void {
$client = $this->clientService->newClient();

if (!$this->capabilitiesService->hasWopiAccessCheck()) {
return;
}

$url = str_replace('/hosting/capabilities', '/hosting/wopiAccessCheck', $this->capabilitiesService->getCapabilitiesEndpoint());
$callbackUrl = $this->appConfig->getNextcloudUrl() ?: trim($this->urlGenerator->getAbsoluteURL(''), '/');

try {
$result = $client->post($url, ['body' => json_encode(['callbackUrl' => $callbackUrl . '/status.php']), 'headers' => ['Content-Type' => 'application/json']]);
} catch (ClientException $e) {
$result = $e->getResponse();
}
$response = json_decode($result->getBody(), true);

$errorMessage = match ($response['status']) {
'Ok' => null,
'NotHttpSuccess' => $this->l10n->t('The connection was successful but the response to the request was not 200'),
'HostNotFound' => $this->l10n->t('DNS error, the host is not known by the Collabora Online server'),
'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'),
'ConnectionAborted' => $this->l10n->t('The connection was aborted by the destination server'),
'CertificateValidation' => $this->l10n->t('The certificate of the response is invalid or otherwise not accepted'),
'SslHandshakeFail' => $this->l10n->t('Couldn’t establish an SSL/TLS connection'),
'MissingSsl' => $this->l10n->t('The response wasn’t using SSL/TLS contrary to expected'),
'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.'),
'NoScheme' => $this->l10n->t('A scheme (http:// or https://) for the WOPI host URL must be specified'),
'Timeout' => $this->l10n->t('The request didn’t get a response within the time frame allowed'),
default => $this->l10n->t('Unknown error. Check the server logs of Collabora for more details.'),
};

if ($errorMessage) {
throw new \Exception(
$this->l10n->t('The Collabora server could not properly reach the Nextcloud server.') . ' ' . $errorMessage
);
}

$output?->writeln('WOPI access was verified');
}

/**
Expand All @@ -59,4 +107,13 @@ public function autoConfigurePublicUrl(): void {
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
}

/**
* @throws Exception
*/
public function test(?OutputInterface $output = null): void {
$this->testDiscovery($output);
$this->testCapabilities($output);
$this->testWopiAccess($output);
}
}
68 changes: 68 additions & 0 deletions lib/SetupCheck/CollaboraUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Richdocuments\SetupCheck;

use OCA\Richdocuments\Service\CapabilitiesService;
use OCP\Http\Client\IClientService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
use Psr\Log\LoggerInterface;

class CollaboraUpdate implements ISetupCheck {

public function __construct(
protected IL10N $l10n,
protected CapabilitiesService $capabilitiesService,
protected IURLGenerator $urlGenerator,
protected IClientService $clientService,
protected LoggerInterface $logger,
) {
}

public function getCategory(): string {
return 'office';
}

public function getName(): string {
return $this->l10n->t('Collabora server version check');
}

public function run(): SetupResult {
$client = $this->clientService->newClient();

$url = null;
if ($this->capabilitiesService->isCode()) {
$url = 'https://rating.collaboraonline.com/UpdateCheck';
}

if ($this->capabilitiesService->isEnterprise()) {
$url = 'https://rating.collaboraonline.com/UpdateCheck?product=cool';
}

if ($url === null) {
return SetupResult::success();
}

// FIXME internet conection check config

$response = $client->get($url, ['timeout' => 5]);
$response = json_decode($response->getBody(), true);
$latestVersion = $response['coolwsd_version'] ?? null;

$installedVersion = $this->capabilitiesService->getProductVersion();

if ($latestVersion !== null && version_compare($latestVersion, $installedVersion, '>')) {
return SetupResult::warning($this->l10n->t('Collabora server version is out of date. Currently using %s, new version is available: %s', [$installedVersion, $latestVersion]));
}

return SetupResult::success();
}
}
47 changes: 47 additions & 0 deletions lib/SetupCheck/ConnectivityCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Richdocuments\SetupCheck;

use OCA\Richdocuments\Service\ConnectivityService;
use OCP\Http\Client\IClientService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
use Psr\Log\LoggerInterface;

class ConnectivityCheck implements ISetupCheck {

public function __construct(
protected IL10N $l10n,
protected ConnectivityService $connectivityService,
protected IURLGenerator $urlGenerator,
protected IClientService $clientService,
protected LoggerInterface $logger,
) {
}

public function getCategory(): string {
return 'office';
}

public function getName(): string {
return $this->l10n->t('Collabora server connectivity check');
}

public function run(): SetupResult {
try {
$this->connectivityService->test();
} catch (\Exception $e) {
return SetupResult::error($this->l10n->t('Collabora is not configured properly:') . ' ' . $e->getMessage());
}

return SetupResult::success();
}
}
Loading

0 comments on commit 1564b81

Please sign in to comment.