Add WOPI support, tested with Collabora

This commit is contained in:
bohwaz 2022-10-15 02:15:39 +02:00
parent b8fd51c750
commit 07c3711d65
6 changed files with 174 additions and 4 deletions

86
COLLABORA.md Normal file
View file

@ -0,0 +1,86 @@
# Setting up Collabora with KaraDAV
This is entirely optional, but will allow you to edit office documents directly from the browser.
Note that Collabora has a soft limit of 20
## With Docker
First install docker and docker-compose, then run `docker pull collabora/code` to fetch the docker image.
Now you will have to create a `docker-compose.yml` file containing:
```
collabora:
image: collabora/code
container_name: collabora
environment:
domain: "karadav.localhost"
extra_params: "--o:ssl.enable=false --o:ssl.termination=false -o:net.frame_ancestors=karadav.localhost:*"
expose:
- 9980
ports:
- "9980:9980"
extra_hosts:
- "karadav.localhost:0.0.0.0"
```
This setup is for a localhost test environment, where `karadav.localhost` is hosting your WebDAV server, and `docs.karadav.localhost` will host the Collabora server. You will have to replace `0.0.0.0` with your computer IP.
Then create a new Apache virtual host:
```
<VirtualHost *:80>
ServerName docs.karadav.localhost
AllowEncodedSlashes NoDecode
ProxyPreserveHost On
<Location />
<Limit OPTIONS>
Header always set Access-Control-Allow-Origin "*"
</Limit>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
</Location>
# static html, js, images, etc. served from coolwsd
# browser is the client part of Collabora Online
ProxyPass /browser http://127.0.0.1:9980/browser retry=0
ProxyPassReverse /browser http://127.0.0.1:9980/browser
# WOPI discovery URL
ProxyPass /hosting/discovery http://127.0.0.1:9980/hosting/discovery retry=0
ProxyPassReverse /hosting/discovery http://127.0.0.1:9980/hosting/discovery
# Capabilities
ProxyPass /hosting/capabilities http://127.0.0.1:9980/hosting/capabilities retry=0
ProxyPassReverse /hosting/capabilities http://127.0.0.1:9980/hosting/capabilities
# Main websocket
ProxyPassMatch "/cool/(.*)/ws$" ws://127.0.0.1:9980/cool/$1/ws nocanon
# Admin Console websocket
ProxyPass /cool/adminws ws://127.0.0.1:9980/cool/adminws
# Download as, Fullscreen presentation and Image upload operations
ProxyPass /cool http://127.0.0.1:9980/cool
ProxyPassReverse /cool http://127.0.0.1:9980/cool
# Compatibility with integrations that use the /lool/convert-to endpoint
ProxyPass /lool http://127.0.0.1:9980/cool
ProxyPassReverse /lool http://127.0.0.1:9980/cool
</VirtualHost>
```
Reload the apache configuration, and launch `docker-compose up`.
Lastly, in KaraDAV's `config.local.php` set `WOPI_DISCOVERY_URL` to `http://docs.karadav.localhost/hosting/discovery`.
Now you should be able to edit ODS/ODT/etc. files from the web UI using Collabora.

View file

@ -25,6 +25,7 @@ This server features:
* Support for `Content-MD5` with `PUT` requests, see [dCache documentation for details](https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums) * Support for `Content-MD5` with `PUT` requests, see [dCache documentation for details](https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums)
* Support for some of the [Microsoft proprietary properties](https://greenbytes.de/tech/webdav/webdavfaq.html) * Support for some of the [Microsoft proprietary properties](https://greenbytes.de/tech/webdav/webdavfaq.html)
* Passes most of the [Litmus compliance tests](https://github.com/tolsen/litmus) * Passes most of the [Litmus compliance tests](https://github.com/tolsen/litmus)
* Supports WOPI, for editing and viewing of documents using OnlyOffice, Collabora Online or MS Office.
## NextCloud/ownCloud compatibility ## NextCloud/ownCloud compatibility
@ -52,6 +53,9 @@ The following NextCloud specific features are supported:
* [FUSE webdavfs](https://github.com/miquels/webdavfs) is recommended for Linux * [FUSE webdavfs](https://github.com/miquels/webdavfs) is recommended for Linux
* davfs2 is NOT recommended: it is very slow, and it is using a local cache, meaning changing a file locally may not be synced to the server for a few minutes, leading to things getting out of sync. If you have to use it, at least disable locks, by setting `use_locks=0` in the config. * davfs2 is NOT recommended: it is very slow, and it is using a local cache, meaning changing a file locally may not be synced to the server for a few minutes, leading to things getting out of sync. If you have to use it, at least disable locks, by setting `use_locks=0` in the config.
## WOPI clients compatibility
* Tested successfully with Collabora Development Edition (see [COLLABORA.md](COLLABORA.md))
## Future development ## Future development
This might get supported in future (maybe): This might get supported in future (maybe):

View file

@ -32,3 +32,10 @@ $port = !in_array($_SERVER['SERVER_PORT'], [80, 443]) ? ':' . $_SERVER['SERVER_P
$root = '/'; $root = '/';
define('KaraDAV\WWW_URL', sprintf('http%s://%s%s%s', $https, $name, $port, $root)); define('KaraDAV\WWW_URL', sprintf('http%s://%s%s%s', $https, $name, $port, $root));
/**
* WOPI client discovery URL
* eg. http://onlyoffice.domain.tld/hosting/discovery for OnlyOffice
* If set to NULL, WOPI support is disabled
*/
const WOPI_DISCOVERY_URL = null;

View file

@ -2,6 +2,8 @@
namespace KaraDAV; namespace KaraDAV;
use KD2\WebDAV\WOPI;
class Server class Server
{ {
public Users $users; public Users $users;
@ -26,6 +28,15 @@ class Server
return true; return true;
} }
if (WOPI_DISCOVERY_URL) {
$wopi = new WOPI;
$wopi->setServer($this->dav);
if ($wopi->route($uri)) {
return true;
}
}
$nc = new NextCloud($this->dav, $this->users); $nc = new NextCloud($this->dav, $this->users);
if ($r = $nc->route($uri)) { if ($r = $nc->route($uri)) {

View file

@ -4,6 +4,7 @@ namespace KaraDAV;
use KD2\WebDAV\AbstractStorage; use KD2\WebDAV\AbstractStorage;
use KD2\WebDAV\Server as WebDAV_Server; use KD2\WebDAV\Server as WebDAV_Server;
use KD2\WebDAV\WOPI;
use KD2\WebDAV\Exception as WebDAV_Exception; use KD2\WebDAV\Exception as WebDAV_Exception;
class Storage extends AbstractStorage class Storage extends AbstractStorage
@ -142,8 +143,8 @@ class Storage extends AbstractStorage
return md5_file($target); return md5_file($target);
// NextCloud stuff // NextCloud stuff
case Nextcloud::PROP_NC_HAS_PREVIEW: case NextCloud::PROP_NC_HAS_PREVIEW:
case Nextcloud::PROP_NC_IS_ENCRYPTED: case NextCloud::PROP_NC_IS_ENCRYPTED:
return 'false'; return 'false';
case NextCloud::PROP_OC_SHARETYPES: case NextCloud::PROP_OC_SHARETYPES:
return WebDAV::EMPTY_PROP_VALUE; return WebDAV::EMPTY_PROP_VALUE;
@ -161,10 +162,10 @@ class Storage extends AbstractStorage
} }
return ''; return '';
case Nextcloud::PROP_OC_ID: case NextCloud::PROP_OC_ID:
$username = $this->users->current()->login; $username = $this->users->current()->login;
return NextCloud::getDirectID($username, $uri); return NextCloud::getDirectID($username, $uri);
case Nextcloud::PROP_OC_PERMISSIONS: case NextCloud::PROP_OC_PERMISSIONS:
return implode('', [NextCloud::PERM_READ, NextCloud::PERM_WRITE, NextCloud::PERM_CREATE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME_MOVE]); return implode('', [NextCloud::PERM_READ, NextCloud::PERM_WRITE, NextCloud::PERM_CREATE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME_MOVE]);
case 'DAV::quota-available-bytes': case 'DAV::quota-available-bytes':
return null; return null;
@ -178,6 +179,31 @@ class Storage extends AbstractStorage
else { else {
return filesize($target); return filesize($target);
} }
case WOPI::PROP_FILE_URL:
$id = gzcompress($uri);
$id = WOPI::base64_encode_url_safe($id);
return WWW_URL . 'wopi/files/' . $id;
case WOPI::PROP_TOKEN:
$p = $this->getResourceProperties($uri);
$token = $p->get($name)['xml'] ?? null;
// Check if token has expired, if so, then renew it
if ($token) {
$expiry = $p->get(WOPI::PROP_TOKEN_TTL);
if ($expiry < time() * 1000) {
$token = null;
}
}
// Create token and store it
if (!$token) {
$token = $this->createWopiToken($uri);
$p->set(WOPI::PROP_TOKEN, null, $token);
$p->set(WOPI::PROP_TOKEN_TTL, null, (time()+3600)*1000);
}
return $token;
default: default:
break; break;
} }
@ -189,6 +215,13 @@ class Storage extends AbstractStorage
return $this->getResourceProperties($uri)->get($name); return $this->getResourceProperties($uri)->get($name);
} }
protected function createWopiToken(string $uri)
{
$login = $this->users->current()->login;
$bytes = substr(md5(random_bytes(10)), 0, 10);
return WOPI::base64_encode_url_safe(sprintf('%s:%s', sha1($login . $uri . $bytes), $bytes));
}
public function properties(string $uri, ?array $properties, int $depth): ?array public function properties(string $uri, ?array $properties, int $depth): ?array
{ {
$target = $this->users->current()->path . $uri; $target = $this->users->current()->path . $uri;
@ -471,4 +504,29 @@ class Storage extends AbstractStorage
return $last; return $last;
} }
public function getWopiURI(string $id, string $token): ?string
{
$id = WOPI::base64_decode_url_safe($id);
$uri = gzuncompress($id);
$token_decode = WOPI::base64_decode_url_safe($token);
$hash = strtok($token_decode, ':');
$bytes = strtok(false);
$r = DB::getInstance()->first('SELECT user, uri FROM properties WHERE name = ? AND xml = ?;', WOPI::PROP_TOKEN, $token);
if (!$r) {
return null;
}
if (!hash_equals(sha1($r->user . $r->uri . $bytes), $hash)) {
return null;
}
if (!$this->users->setCurrent($r->user)) {
return null;
}
return $r->uri;
}
} }

View file

@ -11,6 +11,10 @@ class WebDAV extends WebDAV_Server
$out = parent::html_directory($uri, $list); $out = parent::html_directory($uri, $list);
if (null !== $out) { if (null !== $out) {
if (WOPI_DISCOVERY_URL) {
$out = str_replace('<html', sprintf('<html data-wopi-discovery-url="%s" data-wopi-host-url="%s"', WOPI_DISCOVERY_URL, WWW_URL . 'wopi/'), $out);
}
$out = str_replace('<body>', sprintf('<body style="opacity: 0"><script type="text/javascript" src="%swebdav.js"></script>', WWW_URL), $out); $out = str_replace('<body>', sprintf('<body style="opacity: 0"><script type="text/javascript" src="%swebdav.js"></script>', WWW_URL), $out);
} }