Skip to content

Commit

Permalink
Move to more generic way of parsing wopi discovery
Browse files Browse the repository at this point in the history
Signed-off-by: Julius Härtl <[email protected]>
  • Loading branch information
juliusknorr committed Dec 29, 2023
1 parent 85ae148 commit 09b611a
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 18 deletions.
17 changes: 10 additions & 7 deletions lib/Command/ActivateConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ protected function execute(InputInterface $input, OutputInterface $output) {
return 1;
}

try {
$this->connectivityService->testCapabilities($output);
} catch (\Throwable $e) {
// FIXME: Optional when allowing generic WOPI servers
$output->writeln('<error>Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
$output->writeln($e->getMessage());
return 1;
if ($this->connectivityService->hasCapabilities()) {
try {
$this->connectivityService->testCapabilities($output);
} catch (\Throwable $e) {
// FIXME: Optional when allowing generic WOPI servers
// We need this now
$output->writeln('<error>Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
$output->writeln($e->getMessage());
return 1;
}
}

try {
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/CapabilitiesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function getCapabilities() {
$isCODEEnabled = strpos($this->config->getAppValue('richdocuments', 'wopi_url'), 'proxy.php?req=') !== false;
$shouldRecheckCODECapabilities = $isCODEInstalled && $isCODEEnabled && ($this->capabilities === null || count($this->capabilities) === 0);
if ($this->capabilities === null || $shouldRecheckCODECapabilities) {
$this->fetchFromRemote();
// $this->fetchFromRemote();
}

if (!is_array($this->capabilities)) {
Expand Down
17 changes: 15 additions & 2 deletions lib/Service/ConnectivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ public function testDiscovery(OutputInterface $output): void {
$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>');
if ($this->hasCapabilities()) {
$output->writeln('<info>✓ Valid capabilities entry</info>');
}
}

public function hasCapabilities() : bool {
try {
return $this->parser->getUrlSrcValue('Capabilities') !== '';
} catch (\Throwable) {
return false;
}
}

public function testCapabilities(OutputInterface $output): void {
Expand All @@ -73,6 +82,10 @@ public function testCapabilities(OutputInterface $output): void {
public function autoConfigurePublicUrl(): void {
$determinedUrl = $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
if ($detectedUrl === '') {
$determinedUrl = $this->parser->getUrlSrcByExtension('internal-http', 'docx', 'edit');
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
}
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
}
}
2 changes: 1 addition & 1 deletion lib/TokenManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,6 @@ public function updateGuestName(string $accessToken, string $guestName) {
}

public function getUrlSrc(File $file): string {
return $this->wopiParser->getUrlSrcValue($file->getMimeType());
return $this->wopiParser->getUrlSrcForFile($file);
}
}
257 changes: 250 additions & 7 deletions lib/WOPI/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,124 @@
use Exception;
use OCA\Richdocuments\Service\DiscoveryService;
use Psr\Log\LoggerInterface;

use SimpleXMLElement;
use OCP\IL10N;
use OCP\Files\File;
use OCP\IRequest;
class Parser {

public const ACTION_EDIT = 'edit';
public const ACTION_VIEW = 'view';
public const ACTION_EDITNEW = 'editnew';

// https://wopi.readthedocs.io/en/latest/faq/languages.html
public const SUPPORTED_LANGUAGES = [
'af-ZA',
'am-ET',
'ar-SA',
'as-IN',
'az-Latn-AZ',
'be-BY',
'bg-BG',
'bn-BD',
'bn-IN',
'bs-Latn-BA',
'ca-ES',
'ca-ES-valencia',
'chr-Cher-US',
'cs-CZ',
'cy-GB',
'da-DK',
'de-DE',
'el-GR',
'en-gb',
'en-US',
'es-ES',
'es-mx',
'et-EE',
'eu-ES',
'fa-IR',
'fi-FI',
'fil-PH',
'fr-ca',
'fr-FR',
'ga-IE',
'gd-GB',
'gl-ES',
'gu-IN',
'ha-Latn-NG',
'he-IL',
'hi-IN',
'hr-HR',
'hu-HU',
'hy-AM',
'id-ID',
'is-IS',
'it-IT',
'ja-JP',
'ka-GE',
'kk-KZ',
'km-KH',
'kn-IN',
'kok-IN',
'ko-KR',
'ky-KG',
'lb-LU',
'lo-la',
'lt-LT',
'lv-LV',
'mi-NZ',
'mk-MK',
'ml-IN',
'mn-MN',
'mr-IN',
'ms-MY',
'mt-MT',
'nb-NO',
'ne-NP',
'nl-NL',
'nn-NO',
'or-IN',
'pa-IN',
'pl-PL',
'prs-AF',
'pt-BR',
'pt-PT',
'quz-PE',
'ro-Ro',
'ru-Ru',
'sd-Arab-PK',
'si-LK',
'sk-SK',
'sl-SI',
'sq-AL',
'sr-Cyrl-BA',
'sr-Cyrl-RS',
'sr-Latn-RS',
'sv-SE',
'sw-KE',
'ta-IN',
'te-IN',
'th-TH',
'tk-TM',
'tr-TR',
'tt-RU',
'ug-CN',
'uk-UA',
'ur-PK',
'uz-Latn-UZ',
'vi-VN',
'zh-CN',
'zh-TW'
];

private ?SimpleXMLElement $parsed = null;

public function __construct(
private DiscoveryService $discoveryService,
private LoggerInterface $logger
private LoggerInterface $logger,
private IL10N $l10n,
private IRequest $request,
) {
}

Expand All @@ -49,19 +162,149 @@ public function getUrlSrcValue(string $appName): string {
* @throws Exception
*/
private function getUrlSrc(string $mimetype): array {
$discovery = $this->discoveryService->get();
$this->logger->debug('WOPI::getUrlSrc discovery: {discovery}', ['discovery' => $discovery]);
$discoveryParsed = simplexml_load_string($discovery);

$result = $discoveryParsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
$result = $this->getParsed()->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if ($result && count($result) > 0) {
return [
'urlsrc' => (string)$result[0]['urlsrc'],
'action' => (string)$result[0]['name'],
];
}

if ($this->getUrlSrcByExtension('internal-http', 'docx', 'edit')) {
return [
'urlsrc' => (string)$this->getUrlSrcByExtension('external-http', 'docx', 'edit'),
'action' => 'edit',
];
}

$this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response: {discovery}', ['mimetype' => $mimetype, 'discovery' => $discovery]);

Check failure on line 180 in lib/WOPI/Parser.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis

UndefinedVariable

lib/WOPI/Parser.php:180:157: UndefinedVariable: Cannot find referenced variable $discovery (see https://psalm.dev/024)
throw new Exception('Could not find urlsrc for ' . $mimetype . ' in WOPI discovery response');
}

/**
* @return SimpleXMLElement|bool
* @throws \Exception
*/
public function getParsed() {
if (!empty($this->parsed)) {
return $this->parsed;
}
$discovery = $this->discoveryService->get();
// In PHP 8.0 and later, PHP uses libxml versions from 2.9.0, which disabled XXE by default. libxml_disable_entity_loader() is now deprecated.
// Ref.: https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
if (\PHP_VERSION_ID < 80000) {
$loadEntities = libxml_disable_entity_loader(true);
$discoveryParsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
} else {
$discoveryParsed = simplexml_load_string($discovery);
}
$this->parsed = $discoveryParsed;
return $discoveryParsed;
}

public function getUrlSrcForFile(File $file, bool $edit = true): string {
$protocol = $this->request->getServerProtocol();
$fallbackProtocol = $protocol === 'https' ? 'http' : 'https';

$netZones = [
'external-' . $protocol,
'internal-' . $protocol,
'external-' . $fallbackProtocol,
'internal-' . $fallbackProtocol,
];

$actions = [
$edit && $file->getSize() === 0 ? self::ACTION_EDITNEW : null,
$edit ? self::ACTION_EDIT : null,
self::ACTION_VIEW,
];
$actions = array_filter($actions);

foreach ($netZones as $netZone) {
foreach ($actions as $action) {
$result = $this->getUrlSrcByExtension($netZone, $file->getExtension(), $action);
if ($result) {
return $this->replaceUrlSrcParams($result);
}
}
}

foreach ($netZones as $netZone) {
$result = $this->getUrlSrcByMimetype($netZone, $file->getMimeType());
if ($result) {
return $this->replaceUrlSrcParams($result);
}
}

throw new \Exception('Could not find urlsrc in WOPI');
}

public function getUrlSrcByExtension(string $netZoneName, string $actionExt, $actionName): ?string {
$result = $this->getParsed()->xpath(sprintf(
'/wopi-discovery/net-zone[@name=\'%s\']/app/action[@ext=\'%s\' and @name=\'%s\']',
$netZoneName, $actionExt, $actionName
));

if (!$result) {
return null;
}

return (string)current($result)->attributes()['urlsrc'];
}

private function getUrlSrcByMimetype(string $netZoneName, string $mimetype): ?string {
$result = $this->getParsed()->xpath(sprintf(
'/wopi-discovery/net-zone[@name=\'%s\']/app[@name=\'%s\']/action',
$netZoneName, $mimetype
));

if (!$result) {
return null;
}

return (string)current($result)->attributes()['urlsrc'];
}

private function replaceUrlSrcParams(string $urlSrc): string {
if (strpos($urlSrc, 'UI_LLCC') === false) {
return $urlSrc;
}

$urlSrc = preg_replace('/<ui=UI_LLCC&>/', 'ui=' . $this->getLanguageCode() . '&', $urlSrc);
return preg_replace('/<.+>/', '', $urlSrc);
}

private function getLanguageCode(): string {
$languageCode = $this->l10n->getLanguageCode();
$localeCode = $this->l10n->getLocaleCode();
$splitLocale = explode('_', $localeCode);
if (count($splitLocale) > 1) {
$localeCode = $splitLocale[1];
}

$languageMatches = array_filter(self::SUPPORTED_LANGUAGES, function ($language) use ($languageCode, $localeCode) {
return stripos($language, $languageCode) === 0;
});

// Unique match on the language
if (count($languageMatches) === 1) {
return array_shift($languageMatches);
}
$localeMatches = array_filter($languageMatches, function ($language) use ($languageCode, $localeCode) {
return stripos($language, $languageCode . '-' . $localeCode) === 0;
});

// Matches with language and locale with region
if (count($localeMatches) >= 1) {
return array_shift($localeMatches);
}

// Fallback to first language match if multiple found and no fitting region is available
if (count($languageMatches) > 1) {
return array_shift($languageMatches);
}

return 'en-US';
}
}
2 changes: 2 additions & 0 deletions src/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import '../css/filetypes.scss'
import Office from './view/Office.vue'
import { getCapabilities } from '@nextcloud/capabilities'


Check failure on line 30 in src/viewer.js

View workflow job for this annotation

GitHub Actions / NPM lint

More than 1 blank line not allowed

const supportedMimes = getCapabilities().richdocuments.mimetypes

if (OCA.Viewer) {
Expand Down

0 comments on commit 09b611a

Please sign in to comment.