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 Aug 29, 2024
1 parent 4c37f2a commit b89aa9f
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 17 deletions.
17 changes: 10 additions & 7 deletions lib/Command/ActivateConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
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
17 changes: 15 additions & 2 deletions lib/Service/ConnectivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,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 @@ -57,6 +66,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 @@ -304,6 +304,6 @@ public function setGuestName(Wopi $wopi, ?string $guestName = null): Wopi {
}

public function getUrlSrc(File $file): string {
return $this->wopiParser->getUrlSrcValue($file->getMimeType());
return $this->wopiParser->getUrlSrcForFile($file);
}
}
253 changes: 246 additions & 7 deletions lib/WOPI/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,126 @@

use Exception;
use OCA\Richdocuments\Service\DiscoveryService;
use OCP\Files\File;
use OCP\IL10N;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
use SimpleXMLElement;

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 @@ -34,19 +148,144 @@ 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'],
];
}

$this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response: {discovery}', ['mimetype' => $mimetype, 'discovery' => $discovery]);
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', ['mimetype' => $mimetype]);
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();
$discoveryParsed = simplexml_load_string($discovery);
if ($discoveryParsed === false) {
throw new Exception('Discovery response is not valid XML');
}
$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';
}
}

0 comments on commit b89aa9f

Please sign in to comment.