From 8d2a4910897e4ff22e96c62e77b8713982cbec23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Fri, 9 Dec 2022 11:25:07 +0100 Subject: [PATCH] Move to more generic way of parsing wopi discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/TokenManager.php | 4 +- lib/WOPI/Parser.php | 255 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 243 insertions(+), 16 deletions(-) diff --git a/lib/TokenManager.php b/lib/TokenManager.php index aa47bcbb09..c2bdfeb698 100644 --- a/lib/TokenManager.php +++ b/lib/TokenManager.php @@ -224,7 +224,7 @@ public function getToken($fileId, $shareToken = null, $editoruid = null, $direct $wopi = $this->wopiMapper->generateFileToken($fileId, $owneruid, $editoruid, $version, $updatable, $serverHost, $guestName, 0, $hideDownload, $direct, 0, $shareToken); return [ - $this->wopiParser->getUrlSrc($file->getMimeType())['urlsrc'], // url src might not be found ehre + $this->wopiParser->getUrlSrcForFile($file, $updatable), $wopi->getToken(), $wopi ]; @@ -308,7 +308,7 @@ public function getTokenForTemplate(File $templateFile, $userId, $targetFileId, } return [ - $this->wopiParser->getUrlSrc($templateFile->getMimeType())['urlsrc'], + $this->wopiParser->getUrlSrcForFile($targetFile), $wopi ]; } diff --git a/lib/WOPI/Parser.php b/lib/WOPI/Parser.php index 2a99252ffa..979de55f2d 100644 --- a/lib/WOPI/Parser.php +++ b/lib/WOPI/Parser.php @@ -21,24 +21,155 @@ namespace OCA\Richdocuments\WOPI; +use OCP\Files\File; +use OCP\IL10N; +use OCP\IRequest; +use SimpleXMLElement; + class Parser { - /** @var DiscoveryManager */ - private $discoveryManager; + 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 DiscoveryManager $discoveryManager; + private IRequest $request; + private IL10N $l10n; + + private ?SimpleXMLElement $parsed = null; + + public function __construct(DiscoveryManager $discoveryManager, IRequest $request, IL10N $l10n) { + $this->discoveryManager = $discoveryManager; + $this->request = $request; + $this->l10n = $l10n; + } /** - * @param DiscoveryManager $discoveryManager + * @throws \Exception */ - public function __construct(DiscoveryManager $discoveryManager) { - $this->discoveryManager = $discoveryManager; + public function getUrlSrc(string $mimetype): array { + $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'], + ]; + } + + throw new \Exception('Could not find urlsrc in WOPI'); } /** - * @param $mimetype - * @return array + * @return SimpleXMLElement|bool * @throws \Exception */ - public function getUrlSrc($mimetype) { + public function getParsed() { + if (!empty($this->parsed)) { + return $this->parsed; + } $discovery = $this->discoveryManager->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); @@ -46,16 +177,112 @@ public function getUrlSrc($mimetype) { } 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'; - $result = $discoveryParsed->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'], - ]; + $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'); } + + private 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 || count($result) === 0) { + 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 || count($result) === 0) { + return null; + } + + return (string)current($result)->attributes()['urlsrc']; + } + + private function replaceUrlSrcParams(string $urlSrc): string { + if (!str_contains($urlSrc, 'UI_LLCC')) { + return $urlSrc; + } + + $urlSrc = preg_replace('//', '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'; + } }