Skip to content
Merged
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
11 changes: 7 additions & 4 deletions .github/workflows/cypress-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
image: ${{ matrix.code-image == 'release' && 'collabora/code:latest' || 'juliushaertl/nc-code-nightly:latest' }} # zizmor: ignore[unpinned-images]
env:
extra_params: '--o:ssl.enable=false --o:home_mode.enable=true'
aliasgroup1: 'http://172.17.0.1'
aliasgroup1: 'http://172.17.0.1:8081'
ports:
- "9980:9980"
options: --cap-add SYS_ADMIN --cap-add SYS_CHROOT
Expand Down Expand Up @@ -153,11 +153,10 @@ jobs:
php occ app:enable --force spreed
php occ app:list
php occ config:system:set trusted_domains 1 --value="172.17.0.1"
php occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"

php occ config:app:set richdocuments wopi_url --value="http://localhost:9980"
php occ config:app:set richdocuments public_wopi_url --value="http://localhost:9980"
php occ config:system:set allow_local_remote_servers --value true --type bool
php occ richdocuments:activate-config
php occ richdocuments:activate-config --wopi-url="http://localhost:9980" --callback-url="http://172.17.0.1:8081"

curl http://admin:admin@localhost:8081/ocs/v1.php/cloud/capabilities\?format\=json -H 'OCS-APIRequest: true'

Expand Down Expand Up @@ -228,6 +227,10 @@ jobs:
path: data/nextcloud.log
retention-days: 5

- name: Dump docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@2741064ab9d7af54b0b1ffb6076cf64c16f0220e # v2.2.2

summary:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
image: ${{ matrix.code-image == 'release' && 'collabora/code:latest' || 'juliushaertl/nc-code-nightly:latest' }} # zizmor: ignore[unpinned-images]
env:
extra_params: '--o:ssl.enable=false'
aliasgroup1: 'http://nextcloud'
aliasgroup1: 'http://172.17.0.1:8081'
ports:
- "9980:9980"

Expand Down Expand Up @@ -109,6 +109,7 @@ jobs:
mkdir data
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
./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
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
./occ app:enable --force ${{ env.APP_NAME }}

- name: Run ${{ matrix.scenarios }} integration tests
Expand Down Expand Up @@ -144,7 +145,7 @@ jobs:
image: ghcr.io/juliusknorr/nextcloud-dev-code:latest # zizmor: ignore[unpinned-images]
env:
extra_params: '--o:ssl.enable=false'
aliasgroup1: 'http://nextcloud'
aliasgroup1: 'http://172.17.0.1:8081'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is available in this configuration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is within the github action setup that the tests run

ports:
- "9980:9980"

Expand Down Expand Up @@ -184,6 +185,7 @@ jobs:
mkdir data
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
./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
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
./occ app:enable --force ${{ env.APP_NAME }}

- name: Run ${{ matrix.scenarios }} integration tests
Expand Down Expand Up @@ -221,7 +223,7 @@ jobs:
image: ghcr.io/juliusknorr/nextcloud-dev-code:latest # zizmor: ignore[unpinned-images]
env:
extra_params: '--o:ssl.enable=false'
aliasgroup1: 'http://nextcloud'
aliasgroup1: 'http://172.17.0.1:8081'
ports:
- "9980:9980"

Expand Down Expand Up @@ -261,6 +263,7 @@ jobs:
mkdir data
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
./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
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
./occ app:enable --force ${{ env.APP_NAME }}

- name: Run ${{ matrix.scenarios }} integration tests
Expand Down Expand Up @@ -306,7 +309,7 @@ jobs:
image: ghcr.io/juliusknorr/nextcloud-dev-code:latest # zizmor: ignore[unpinned-images]
env:
extra_params: '--o:ssl.enable=false'
aliasgroup1: 'http://nextcloud'
aliasgroup1: 'http://172.17.0.1:8081'
ports:
- "9980:9980"

Expand Down Expand Up @@ -345,6 +348,7 @@ jobs:
echo '<?php $CONFIG=["memcache.local"=>"\OC\Memcache\APCu","hashing_default_password"=>true];' > config/config.php
./occ maintenance:install --verbose --database=oci --database-name=XE --database-host=127.0.0.1 --database-port=1521 --database-user=system --database-pass=rootpassword --admin-user admin --admin-pass password
php -f index.php
./occ config:system:set overwrite.cli.url --value="http://172.17.0.1:8081"
./occ app:enable --force ${{ env.APP_NAME }}

- name: Run ${{ matrix.scenarios }} integration tests
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
['name' => 'settings#getSettings', 'url' => 'ajax/settings.php', 'verb' => 'GET'],
['name' => 'settings#updateWatermarkSettings', 'url' => 'settings/watermark', 'verb' => 'POST'],
['name' => 'settings#checkSettings', 'url' => 'settings/check', 'verb' => 'GET'],
['name' => 'settings#accessCheck', 'url' => 'settings/access-check', 'verb' => 'GET'],
['name' => 'settings#demoServers', 'url' => 'settings/demo', 'verb' => 'GET'],
['name' => 'settings#getFontNames', 'url' => 'settings/fonts', 'verb' => 'GET'],
['name' => 'settings#getJsonFontList', 'url' => 'settings/fonts.json', 'verb' => 'GET'],
Expand Down
7 changes: 7 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
use OCA\Richdocuments\Preview\Pdf;
use OCA\Richdocuments\Preview\Rtf;
use OCA\Richdocuments\Reference\OfficeTargetReferenceProvider;
use OCA\Richdocuments\SetupCheck\CollaboraUpdate;
use OCA\Richdocuments\SetupCheck\ConnectivityCheck;
use OCA\Richdocuments\SetupCheck\ProofKeyCheck;
use OCA\Richdocuments\Storage\SecureViewWrapper;
use OCA\Richdocuments\TaskProcessing\SlideDeckGenerationProvider;
use OCA\Richdocuments\TaskProcessing\SlideDeckGenerationTaskType;
Expand Down Expand Up @@ -122,6 +125,10 @@ public function register(IRegistrationContext $context): void {
$context->registerTaskProcessingProvider(TextToSpreadsheetProvider::class);
$context->registerTaskProcessingProvider(SlideDeckGenerationProvider::class);
$context->registerTaskProcessingTaskType(SlideDeckGenerationTaskType::class);

$context->registerSetupCheck(CollaboraUpdate::class);
$context->registerSetupCheck(ConnectivityCheck::class);
$context->registerSetupCheck(ProofKeyCheck::class);
}

#[\Override]
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
26 changes: 19 additions & 7 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
Expand All @@ -38,7 +40,6 @@
use OCP\PreConditionNotMetException;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\NullOutput;

class SettingsController extends Controller {
/** Settings file URLs are generated with a settings token and carry no document context. */
Expand Down Expand Up @@ -76,11 +77,20 @@ public function __construct(
parent::__construct($appName, $request);
}

/**
* Callback for the Collabora WOPI access check. Collabora requires a 200 response and cannot
* handle a body without Content-Length, which the PHP built-in webserver does not add.
*/
#[PublicPage]
#[NoCSRFRequired]
public function accessCheck(): DataDisplayResponse {
$body = 'OK';
return new DataDisplayResponse($body, Http::STATUS_OK, ['Content-Type' => 'text/plain', 'Content-Length' => (string)strlen($body)]);
}

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 @@ -192,9 +202,11 @@ public function setSettings(
}

try {
$output = new NullOutput();
$this->connectivityService->testDiscovery($output);
$this->connectivityService->testCapabilities($output);
$this->connectivityService->testDiscovery();
$this->connectivityService->testCapabilities();
if ($wopi_url !== null) {
$this->connectivityService->testWopiAccess();
}
$this->connectivityService->autoConfigurePublicUrl();
} catch (\Throwable $e) {
return new JSONResponse([
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/CachedRequestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function getLastUpdate(): ?int {
* @return string
* @throws \Exception
*/
final public function fetch(): string {
public function fetch(): string {
$cache = $this->cacheFactory->createDistributed('richdocuments');
$client = $this->clientService->newClient();

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

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 All @@ -130,6 +142,12 @@ public function getCapabilitiesEndpoint(): ?string {
return rtrim($remoteHost, '/') . '/hosting/capabilities';
}

#[\Override]
public function fetch(): string {
$this->capabilities = null;
return parent::fetch();
}

#[\Override]
protected function sendRequest(IClient $client): string {
$response = $client->get($this->getCapabilitiesEndpoint(), $this->getDefaultRequestOptions());
Expand Down
78 changes: 69 additions & 9 deletions lib/Service/ConnectivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,95 @@
namespace OCA\Richdocuments\Service;

use Exception;
use GuzzleHttp\Exception\BadResponseException;
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 {
$this->discoveryService->resetCache();
public function testDiscovery(?OutputInterface $output = null): void {
$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 {
$this->capabilitiesService->resetCache();
public function testCapabilities(?OutputInterface $output = null): void {
$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 {
if (!$this->capabilitiesService->hasWopiAccessCheck()) {
return;
}

$client = $this->clientService->newClient();

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

try {
$result = $client->post($url, array_merge(RemoteOptionsService::getDefaultOptions(), [
'body' => json_encode(['callbackUrl' => $callbackUrl . '/index.php/apps/richdocuments/settings/access-check']), 'headers' => ['Content-Type' => 'application/json']
]));
} catch (BadResponseException $e) {
$result = $e->getResponse();
}
$status = json_decode((string)$result->getBody(), true)['status'] ?? throw new \Exception('Unexpected response from ' . $url);

$errorMessage = match ($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'),
Comment thread
juliusknorr marked this conversation as resolved.
'SelfSignedCertificate' => $this->l10n->t('The certificate of the response is self-signed and not trusted by the system'),
'ExpiredCertificate' => $this->l10n->t('The certificate of the response is expired'),
'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'),
'UnspecifiedError' => $this->l10n->t('The response could not be read, for example because the connection was closed before the full response was received. Check the server logs of Collabora for more details.'),
default => $this->l10n->t('Unknown error "%s". Check the server logs of Collabora for more details.', [$status]),
};

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

$output?->writeln('<info>✓ Verified WOPI access from Collabora to ' . $callbackUrl . '</info>');
}

/**
Expand All @@ -60,4 +111,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);
Comment thread
elzody marked this conversation as resolved.
$this->testCapabilities($output);
$this->testWopiAccess($output);
}
}
Loading
Loading