Skip to content

Commit 09b611a

Browse files
committed
Move to more generic way of parsing wopi discovery
Signed-off-by: Julius Härtl <[email protected]>
1 parent 85ae148 commit 09b611a

File tree

6 files changed

+279
-18
lines changed

6 files changed

+279
-18
lines changed

lib/Command/ActivateConfig.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,16 @@ protected function execute(InputInterface $input, OutputInterface $output) {
8181
return 1;
8282
}
8383

84-
try {
85-
$this->connectivityService->testCapabilities($output);
86-
} catch (\Throwable $e) {
87-
// FIXME: Optional when allowing generic WOPI servers
88-
$output->writeln('<error>Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
89-
$output->writeln($e->getMessage());
90-
return 1;
84+
if ($this->connectivityService->hasCapabilities()) {
85+
try {
86+
$this->connectivityService->testCapabilities($output);
87+
} catch (\Throwable $e) {
88+
// FIXME: Optional when allowing generic WOPI servers
89+
// We need this now
90+
$output->writeln('<error>Failed to fetch capabilities endpoint from ' . $this->capabilitiesService->getCapabilitiesEndpoint());
91+
$output->writeln($e->getMessage());
92+
return 1;
93+
}
9194
}
9295

9396
try {

lib/Service/CapabilitiesService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function getCapabilities() {
7070
$isCODEEnabled = strpos($this->config->getAppValue('richdocuments', 'wopi_url'), 'proxy.php?req=') !== false;
7171
$shouldRecheckCODECapabilities = $isCODEInstalled && $isCODEEnabled && ($this->capabilities === null || count($this->capabilities) === 0);
7272
if ($this->capabilities === null || $shouldRecheckCODECapabilities) {
73-
$this->fetchFromRemote();
73+
// $this->fetchFromRemote();
7474
}
7575

7676
if (!is_array($this->capabilities)) {

lib/Service/ConnectivityService.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,17 @@ public function testDiscovery(OutputInterface $output): void {
4848
$output->writeln('<info>✓ Valid mimetype response</info>');
4949

5050
// FIXME: Optional when allowing generic WOPI servers
51-
$this->parser->getUrlSrcValue('Capabilities');
52-
$output->writeln('<info>✓ Valid capabilities entry</info>');
51+
if ($this->hasCapabilities()) {
52+
$output->writeln('<info>✓ Valid capabilities entry</info>');
53+
}
54+
}
55+
56+
public function hasCapabilities() : bool {
57+
try {
58+
return $this->parser->getUrlSrcValue('Capabilities') !== '';
59+
} catch (\Throwable) {
60+
return false;
61+
}
5362
}
5463

5564
public function testCapabilities(OutputInterface $output): void {
@@ -73,6 +82,10 @@ public function testCapabilities(OutputInterface $output): void {
7382
public function autoConfigurePublicUrl(): void {
7483
$determinedUrl = $this->parser->getUrlSrcValue('application/vnd.openxmlformats-officedocument.wordprocessingml.document');
7584
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
85+
if ($detectedUrl === '') {
86+
$determinedUrl = $this->parser->getUrlSrcByExtension('internal-http', 'docx', 'edit');
87+
$detectedUrl = $this->appConfig->domainOnly($determinedUrl);
88+
}
7689
$this->appConfig->setAppValue('public_wopi_url', $detectedUrl);
7790
}
7891
}

lib/TokenManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,6 @@ public function updateGuestName(string $accessToken, string $guestName) {
313313
}
314314

315315
public function getUrlSrc(File $file): string {
316-
return $this->wopiParser->getUrlSrcValue($file->getMimeType());
316+
return $this->wopiParser->getUrlSrcForFile($file);
317317
}
318318
}

lib/WOPI/Parser.php

Lines changed: 250 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,124 @@
2424
use Exception;
2525
use OCA\Richdocuments\Service\DiscoveryService;
2626
use Psr\Log\LoggerInterface;
27-
27+
use SimpleXMLElement;
28+
use OCP\IL10N;
29+
use OCP\Files\File;
30+
use OCP\IRequest;
2831
class Parser {
32+
33+
public const ACTION_EDIT = 'edit';
34+
public const ACTION_VIEW = 'view';
35+
public const ACTION_EDITNEW = 'editnew';
36+
37+
// https://wopi.readthedocs.io/en/latest/faq/languages.html
38+
public const SUPPORTED_LANGUAGES = [
39+
'af-ZA',
40+
'am-ET',
41+
'ar-SA',
42+
'as-IN',
43+
'az-Latn-AZ',
44+
'be-BY',
45+
'bg-BG',
46+
'bn-BD',
47+
'bn-IN',
48+
'bs-Latn-BA',
49+
'ca-ES',
50+
'ca-ES-valencia',
51+
'chr-Cher-US',
52+
'cs-CZ',
53+
'cy-GB',
54+
'da-DK',
55+
'de-DE',
56+
'el-GR',
57+
'en-gb',
58+
'en-US',
59+
'es-ES',
60+
'es-mx',
61+
'et-EE',
62+
'eu-ES',
63+
'fa-IR',
64+
'fi-FI',
65+
'fil-PH',
66+
'fr-ca',
67+
'fr-FR',
68+
'ga-IE',
69+
'gd-GB',
70+
'gl-ES',
71+
'gu-IN',
72+
'ha-Latn-NG',
73+
'he-IL',
74+
'hi-IN',
75+
'hr-HR',
76+
'hu-HU',
77+
'hy-AM',
78+
'id-ID',
79+
'is-IS',
80+
'it-IT',
81+
'ja-JP',
82+
'ka-GE',
83+
'kk-KZ',
84+
'km-KH',
85+
'kn-IN',
86+
'kok-IN',
87+
'ko-KR',
88+
'ky-KG',
89+
'lb-LU',
90+
'lo-la',
91+
'lt-LT',
92+
'lv-LV',
93+
'mi-NZ',
94+
'mk-MK',
95+
'ml-IN',
96+
'mn-MN',
97+
'mr-IN',
98+
'ms-MY',
99+
'mt-MT',
100+
'nb-NO',
101+
'ne-NP',
102+
'nl-NL',
103+
'nn-NO',
104+
'or-IN',
105+
'pa-IN',
106+
'pl-PL',
107+
'prs-AF',
108+
'pt-BR',
109+
'pt-PT',
110+
'quz-PE',
111+
'ro-Ro',
112+
'ru-Ru',
113+
'sd-Arab-PK',
114+
'si-LK',
115+
'sk-SK',
116+
'sl-SI',
117+
'sq-AL',
118+
'sr-Cyrl-BA',
119+
'sr-Cyrl-RS',
120+
'sr-Latn-RS',
121+
'sv-SE',
122+
'sw-KE',
123+
'ta-IN',
124+
'te-IN',
125+
'th-TH',
126+
'tk-TM',
127+
'tr-TR',
128+
'tt-RU',
129+
'ug-CN',
130+
'uk-UA',
131+
'ur-PK',
132+
'uz-Latn-UZ',
133+
'vi-VN',
134+
'zh-CN',
135+
'zh-TW'
136+
];
137+
138+
private ?SimpleXMLElement $parsed = null;
139+
29140
public function __construct(
30141
private DiscoveryService $discoveryService,
31-
private LoggerInterface $logger
142+
private LoggerInterface $logger,
143+
private IL10N $l10n,
144+
private IRequest $request,
32145
) {
33146
}
34147

@@ -49,19 +162,149 @@ public function getUrlSrcValue(string $appName): string {
49162
* @throws Exception
50163
*/
51164
private function getUrlSrc(string $mimetype): array {
52-
$discovery = $this->discoveryService->get();
53-
$this->logger->debug('WOPI::getUrlSrc discovery: {discovery}', ['discovery' => $discovery]);
54-
$discoveryParsed = simplexml_load_string($discovery);
55-
56-
$result = $discoveryParsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
165+
$result = $this->getParsed()->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
57166
if ($result && count($result) > 0) {
58167
return [
59168
'urlsrc' => (string)$result[0]['urlsrc'],
60169
'action' => (string)$result[0]['name'],
61170
];
62171
}
63172

173+
if ($this->getUrlSrcByExtension('internal-http', 'docx', 'edit')) {
174+
return [
175+
'urlsrc' => (string)$this->getUrlSrcByExtension('external-http', 'docx', 'edit'),
176+
'action' => 'edit',
177+
];
178+
}
179+
64180
$this->logger->error('Didn\'t find urlsrc for mimetype {mimetype} in this WOPI discovery response: {discovery}', ['mimetype' => $mimetype, 'discovery' => $discovery]);
65181
throw new Exception('Could not find urlsrc for ' . $mimetype . ' in WOPI discovery response');
66182
}
183+
184+
/**
185+
* @return SimpleXMLElement|bool
186+
* @throws \Exception
187+
*/
188+
public function getParsed() {
189+
if (!empty($this->parsed)) {
190+
return $this->parsed;
191+
}
192+
$discovery = $this->discoveryService->get();
193+
// 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.
194+
// Ref.: https://php.watch/versions/8.0/libxml_disable_entity_loader-deprecation
195+
if (\PHP_VERSION_ID < 80000) {
196+
$loadEntities = libxml_disable_entity_loader(true);
197+
$discoveryParsed = simplexml_load_string($discovery);
198+
libxml_disable_entity_loader($loadEntities);
199+
} else {
200+
$discoveryParsed = simplexml_load_string($discovery);
201+
}
202+
$this->parsed = $discoveryParsed;
203+
return $discoveryParsed;
204+
}
205+
206+
public function getUrlSrcForFile(File $file, bool $edit = true): string {
207+
$protocol = $this->request->getServerProtocol();
208+
$fallbackProtocol = $protocol === 'https' ? 'http' : 'https';
209+
210+
$netZones = [
211+
'external-' . $protocol,
212+
'internal-' . $protocol,
213+
'external-' . $fallbackProtocol,
214+
'internal-' . $fallbackProtocol,
215+
];
216+
217+
$actions = [
218+
$edit && $file->getSize() === 0 ? self::ACTION_EDITNEW : null,
219+
$edit ? self::ACTION_EDIT : null,
220+
self::ACTION_VIEW,
221+
];
222+
$actions = array_filter($actions);
223+
224+
foreach ($netZones as $netZone) {
225+
foreach ($actions as $action) {
226+
$result = $this->getUrlSrcByExtension($netZone, $file->getExtension(), $action);
227+
if ($result) {
228+
return $this->replaceUrlSrcParams($result);
229+
}
230+
}
231+
}
232+
233+
foreach ($netZones as $netZone) {
234+
$result = $this->getUrlSrcByMimetype($netZone, $file->getMimeType());
235+
if ($result) {
236+
return $this->replaceUrlSrcParams($result);
237+
}
238+
}
239+
240+
throw new \Exception('Could not find urlsrc in WOPI');
241+
}
242+
243+
public function getUrlSrcByExtension(string $netZoneName, string $actionExt, $actionName): ?string {
244+
$result = $this->getParsed()->xpath(sprintf(
245+
'/wopi-discovery/net-zone[@name=\'%s\']/app/action[@ext=\'%s\' and @name=\'%s\']',
246+
$netZoneName, $actionExt, $actionName
247+
));
248+
249+
if (!$result) {
250+
return null;
251+
}
252+
253+
return (string)current($result)->attributes()['urlsrc'];
254+
}
255+
256+
private function getUrlSrcByMimetype(string $netZoneName, string $mimetype): ?string {
257+
$result = $this->getParsed()->xpath(sprintf(
258+
'/wopi-discovery/net-zone[@name=\'%s\']/app[@name=\'%s\']/action',
259+
$netZoneName, $mimetype
260+
));
261+
262+
if (!$result) {
263+
return null;
264+
}
265+
266+
return (string)current($result)->attributes()['urlsrc'];
267+
}
268+
269+
private function replaceUrlSrcParams(string $urlSrc): string {
270+
if (strpos($urlSrc, 'UI_LLCC') === false) {
271+
return $urlSrc;
272+
}
273+
274+
$urlSrc = preg_replace('/<ui=UI_LLCC&>/', 'ui=' . $this->getLanguageCode() . '&', $urlSrc);
275+
return preg_replace('/<.+>/', '', $urlSrc);
276+
}
277+
278+
private function getLanguageCode(): string {
279+
$languageCode = $this->l10n->getLanguageCode();
280+
$localeCode = $this->l10n->getLocaleCode();
281+
$splitLocale = explode('_', $localeCode);
282+
if (count($splitLocale) > 1) {
283+
$localeCode = $splitLocale[1];
284+
}
285+
286+
$languageMatches = array_filter(self::SUPPORTED_LANGUAGES, function ($language) use ($languageCode, $localeCode) {
287+
return stripos($language, $languageCode) === 0;
288+
});
289+
290+
// Unique match on the language
291+
if (count($languageMatches) === 1) {
292+
return array_shift($languageMatches);
293+
}
294+
$localeMatches = array_filter($languageMatches, function ($language) use ($languageCode, $localeCode) {
295+
return stripos($language, $languageCode . '-' . $localeCode) === 0;
296+
});
297+
298+
// Matches with language and locale with region
299+
if (count($localeMatches) >= 1) {
300+
return array_shift($localeMatches);
301+
}
302+
303+
// Fallback to first language match if multiple found and no fitting region is available
304+
if (count($languageMatches) > 1) {
305+
return array_shift($languageMatches);
306+
}
307+
308+
return 'en-US';
309+
}
67310
}

src/viewer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import '../css/filetypes.scss'
2727
import Office from './view/Office.vue'
2828
import { getCapabilities } from '@nextcloud/capabilities'
2929

30+
31+
3032
const supportedMimes = getCapabilities().richdocuments.mimetypes
3133

3234
if (OCA.Viewer) {

0 commit comments

Comments
 (0)