Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add setup checks and verify WOPI connectivity #4470

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading