Make sure we return last modified time to Collabora so that it handles correctly race conditions
This commit is contained in:
parent
ac3d269a79
commit
7559286d38
|
@ -120,6 +120,24 @@ class Server
|
||||||
|
|
||||||
protected AbstractStorage $storage;
|
protected AbstractStorage $storage;
|
||||||
|
|
||||||
|
protected array $headers;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->headers = apache_request_headers();
|
||||||
|
$this->headers = array_change_key_case($this->headers, \CASE_LOWER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeader(string $name): ?string
|
||||||
|
{
|
||||||
|
return $this->headers[strtolower($name)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeader(string $name, string $value): void
|
||||||
|
{
|
||||||
|
$this->headers[strtolower($name)] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
public function setStorage(AbstractStorage $storage)
|
public function setStorage(AbstractStorage $storage)
|
||||||
{
|
{
|
||||||
$this->storage = $storage;
|
$this->storage = $storage;
|
||||||
|
@ -253,12 +271,16 @@ class Server
|
||||||
|
|
||||||
public function http_put(string $uri): ?string
|
public function http_put(string $uri): ?string
|
||||||
{
|
{
|
||||||
if (!empty($_SERVER['HTTP_CONTENT_TYPE']) && !strncmp($_SERVER['HTTP_CONTENT_TYPE'], 'multipart/', 10)) {
|
$content_type = $this->getHeader('Content-Type');
|
||||||
|
|
||||||
|
if ($content_type && !strncmp($content_type, 'multipart/', 10)) {
|
||||||
throw new Exception('Multipart PUT requests are not supported', 501);
|
throw new Exception('Multipart PUT requests are not supported', 501);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SERVER['HTTP_CONTENT_ENCODING'])) {
|
$content_encoding = $this->getHeader('Content-Encoding');
|
||||||
if (false !== strpos($_SERVER['HTTP_CONTENT_ENCODING'], 'gzip')) {
|
|
||||||
|
if ($content_encoding) {
|
||||||
|
if (false !== strpos($content_encoding, 'gzip')) {
|
||||||
// Might be supported later?
|
// Might be supported later?
|
||||||
throw new Exception('Content Encoding is not supported', 501);
|
throw new Exception('Content Encoding is not supported', 501);
|
||||||
}
|
}
|
||||||
|
@ -267,12 +289,12 @@ class Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($_SERVER['HTTP_CONTENT_RANGE'])) {
|
if ($this->getHeader('Content-Range')) {
|
||||||
throw new Exception('Content Range is not supported', 501);
|
throw new Exception('Content Range is not supported', 501);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See SabreDAV CorePlugin for reason why OS/X Finder is buggy
|
// See SabreDAV CorePlugin for reason why OS/X Finder is buggy
|
||||||
if (isset($_SERVER['HTTP_X_EXPECTED_ENTITY_LENGTH'])) {
|
if ($this->getHeader('X-Expected-Entity-Length')) {
|
||||||
throw new Exception('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.', 403);
|
throw new Exception('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,14 +303,14 @@ class Server
|
||||||
|
|
||||||
// Support for checksum matching
|
// Support for checksum matching
|
||||||
// https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums
|
// https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums
|
||||||
if (!empty($_SERVER['HTTP_CONTENT_MD5'])) {
|
if ($hash = $this->getHeader('Content-MD5')) {
|
||||||
$hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5']));
|
$hash = bin2hex(base64_decode($hash));
|
||||||
$hash_algo = 'MD5';
|
$hash_algo = 'MD5';
|
||||||
}
|
}
|
||||||
// Support for ownCloud/NextCloud checksum
|
// Support for ownCloud/NextCloud checksum
|
||||||
// https://github.com/owncloud-archive/documentation/issues/2964
|
// https://github.com/owncloud-archive/documentation/issues/2964
|
||||||
elseif (!empty($_SERVER['HTTP_OC_CHECKSUM'])
|
elseif (($checksum = $this->getHeader('OC-Checksum'))
|
||||||
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) {
|
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $checksum, $match)) {
|
||||||
$hash_algo = strtok($match[0], ':');
|
$hash_algo = strtok($match[0], ':');
|
||||||
$hash = strtok('');
|
$hash = strtok('');
|
||||||
}
|
}
|
||||||
|
@ -297,8 +319,8 @@ class Server
|
||||||
|
|
||||||
$this->checkLock($uri);
|
$this->checkLock($uri);
|
||||||
|
|
||||||
if (!empty($_SERVER['HTTP_IF_MATCH'])) {
|
if ($match = $this->getHeader('If-Match')) {
|
||||||
$etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
|
$etag = trim($match, '" ');
|
||||||
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
|
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
|
||||||
|
|
||||||
if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
|
if (!empty($prop['DAV::getetag']) && $prop['DAV::getetag'] != $etag) {
|
||||||
|
@ -306,9 +328,19 @@ class Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($date = $this->getHeader('If-Unmodified-Since')) {
|
||||||
|
$date = \DateTime::createFromFormat(\DateTime::RFC7231, $date);
|
||||||
|
$prop = $this->storage->propfind($uri, ['DAV::getlastmodified'], 0);
|
||||||
|
if ($date && $prop && $prop instanceof \DateTimeInterface) {
|
||||||
|
if ($date != $prop) {
|
||||||
|
throw new Exception('File was modified since "If-Unmodified-Since" condition', 412);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Specific to NextCloud/ownCloud, to allow setting file mtime
|
// Specific to NextCloud/ownCloud, to allow setting file mtime
|
||||||
// This expects a UNIX timestamp
|
// This expects a UNIX timestamp
|
||||||
$mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
|
$mtime = intval($this->getHeader('X-OC-MTime')) ?: null;
|
||||||
|
|
||||||
$this->extendExecutionTime();
|
$this->extendExecutionTime();
|
||||||
|
|
||||||
|
@ -316,7 +348,7 @@ class Server
|
||||||
|
|
||||||
// mod_fcgid <= 2.3.9 doesn't handle chunked transfer encoding for PUT requests
|
// mod_fcgid <= 2.3.9 doesn't handle chunked transfer encoding for PUT requests
|
||||||
// see https://github.com/kd2org/picodav/issues/6
|
// see https://github.com/kd2org/picodav/issues/6
|
||||||
if (strstr($_SERVER['HTTP_TRANSFER_ENCODING'] ?? '', 'chunked') && PHP_SAPI == 'fpm-fcgi') {
|
if (strstr($this->getHeader('Transfer-Encoding') ?? '', 'chunked') && PHP_SAPI == 'fpm-fcgi') {
|
||||||
// We can't seek here
|
// We can't seek here
|
||||||
// see https://github.com/php/php-src/issues/9441
|
// see https://github.com/php/php-src/issues/9441
|
||||||
$l = strlen(fread($stream, 1));
|
$l = strlen(fread($stream, 1));
|
||||||
|
|
|
@ -77,6 +77,7 @@ class WOPI
|
||||||
const PROP_USER_NAME = self::NS . ':UserFriendlyName';
|
const PROP_USER_NAME = self::NS . ':UserFriendlyName';
|
||||||
const PROP_USER_ID = self::NS . ':UserId';
|
const PROP_USER_ID = self::NS . ':UserId';
|
||||||
const PROP_USER_AVATAR = self::NS . ':UserExtraInfo-Avatar';
|
const PROP_USER_AVATAR = self::NS . ':UserExtraInfo-Avatar';
|
||||||
|
const PROP_LAST_MODIFIED = 'DAV::getlastmodified';
|
||||||
|
|
||||||
protected AbstractStorage $storage;
|
protected AbstractStorage $storage;
|
||||||
protected Server $server;
|
protected Server $server;
|
||||||
|
@ -140,6 +141,8 @@ class WOPI
|
||||||
|
|
||||||
$this->server->log('WOPI: => %s', $uri);
|
$this->server->log('WOPI: => %s', $uri);
|
||||||
|
|
||||||
|
$return = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$auth_token = $this->getAuthToken();
|
$auth_token = $this->getAuthToken();
|
||||||
|
|
||||||
|
@ -175,26 +178,63 @@ class WOPI
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->server->log('WOPI: => PutFile');
|
$this->server->log('WOPI: => PutFile');
|
||||||
|
|
||||||
|
$lastmodified = $props[self::PROP_LAST_MODIFIED];
|
||||||
|
$collabora_timestamp = $this->server->getHeader('X-COOL-WOPI-Timestamp');
|
||||||
|
|
||||||
|
// Collabora doesn't use WOPI lock/unlock
|
||||||
|
// See https://sdk.collaboraonline.com/docs/How_to_integrate.html#further-differences-to-wopi
|
||||||
|
if ($collabora_timestamp
|
||||||
|
&& ($date = \DateTime::createFromFormat(\DateTime::ISO8601, $collabora_timestamp))
|
||||||
|
&& $lastmodified
|
||||||
|
&& $lastmodified instanceof \DateTime
|
||||||
|
&& $date->format('YmdHis') != $lastmodified->format('YmdHis'))
|
||||||
|
{
|
||||||
|
$this->server->log('WOPI: <= 409 (File was modified: client = %s, server = %s)',
|
||||||
|
$date->format('Y-m-d H:i:s'),
|
||||||
|
$lastmodified->format('Y-m-d H:i:s'));
|
||||||
|
http_response_code(409);
|
||||||
|
header('Content-Type: application/json', true);
|
||||||
|
echo '{"COOLStatusCode": 1010}';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$this->server->http_put($uri);
|
$this->server->http_put($uri);
|
||||||
|
|
||||||
// In WOPI, HTTP response code 201/204 is not accepted, only 200
|
// In WOPI, HTTP response code 201/204 is not accepted, only 200
|
||||||
// or Collabora will fail saving
|
// or Collabora will fail saving
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
|
|
||||||
|
// Useful for Collabora
|
||||||
|
if ($lastmodified) {
|
||||||
|
// Update last-modified time
|
||||||
|
$lastmodified = $this->storage->propfind($uri, ['DAV::getlastmodified'], 0)[self::PROP_LAST_MODIFIED] ?? null;
|
||||||
|
|
||||||
|
if ($lastmodified && $lastmodified instanceof \DateTime) {
|
||||||
|
$return = ['LastModifiedTime' => $lastmodified->format(\DateTime::ISO8601)];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// CheckFileInfo
|
// CheckFileInfo
|
||||||
elseif (!$action && $method == 'GET') {
|
elseif (!$action && $method == 'GET') {
|
||||||
$this->server->log('WOPI: => CheckFileInfo');
|
$this->server->log('WOPI: => CheckFileInfo');
|
||||||
$this->getInfo($uri, $props);
|
$return = $this->getInfo($uri, $props);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Exception('Invalid URI', 404);
|
throw new Exception('Invalid URI', 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception $e) {
|
catch (Exception $e) {
|
||||||
$this->server->log('WOPI: => %d: %s', $e->getCode(), $e->getMessage());
|
$this->server->log('WOPI: <= %d: %s', $e->getCode(), $e->getMessage());
|
||||||
http_response_code($e->getCode());
|
http_response_code($e->getCode());
|
||||||
|
$return = ['error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($return) {
|
||||||
header('Content-Type: application/json', true);
|
header('Content-Type: application/json', true);
|
||||||
echo json_encode(['error' => $e->getMessage()]);
|
$return = json_encode($return, JSON_PRETTY_PRINT);
|
||||||
|
echo $return;
|
||||||
|
$this->server->log('WOPI: <= %s', $return);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -203,12 +243,16 @@ class WOPI
|
||||||
/**
|
/**
|
||||||
* Output file informations in JSON
|
* Output file informations in JSON
|
||||||
*/
|
*/
|
||||||
protected function getInfo(string $uri, array $props): bool
|
protected function getInfo(string $uri, array $props): array
|
||||||
{
|
{
|
||||||
$modified = !empty($props['DAV::getlastmodified']) ? $props['DAV::getlastmodified']->format(DATE_ISO8601) : null;
|
$modified = null;
|
||||||
$size = $props['DAV::getcontentlength'] ?? null;
|
$size = $props['DAV::getcontentlength'] ?? null;
|
||||||
$readonly = (bool) $props[self::PROP_READ_ONLY];
|
$readonly = (bool) $props[self::PROP_READ_ONLY];
|
||||||
|
|
||||||
|
if (isset($props[self::PROP_LAST_MODIFIED]) && $props[self::PROP_LAST_MODIFIED] instanceof \DateTimeInterface) {
|
||||||
|
$modified = $props[self::PROP_LAST_MODIFIED]->format(DATE_ISO8601);
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'BaseFileName' => basename($uri),
|
'BaseFileName' => basename($uri),
|
||||||
'UserFriendlyName' => $props[self::PROP_USER_NAME] ?? 'User',
|
'UserFriendlyName' => $props[self::PROP_USER_NAME] ?? 'User',
|
||||||
|
@ -234,14 +278,8 @@ class WOPI
|
||||||
$data['LastModifiedTime'] = $modified;
|
$data['LastModifiedTime'] = $modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$json = json_encode($data, JSON_PRETTY_PRINT);
|
|
||||||
$this->server->log('WOPI: => Info: %s', $json);
|
|
||||||
|
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
header('Content-Type: application/json', true);
|
return $data;
|
||||||
echo $json;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -658,11 +658,12 @@ class Storage extends AbstractStorage implements TrashInterface
|
||||||
$readonly = !is_writeable($path);
|
$readonly = !is_writeable($path);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
WOPI::PROP_FILE_URI => $uri,
|
WOPI::PROP_FILE_URI => $uri,
|
||||||
WOPI::PROP_READ_ONLY => $readonly,
|
WOPI::PROP_READ_ONLY => $readonly,
|
||||||
WOPI::PROP_USER_NAME => $user->login,
|
WOPI::PROP_USER_NAME => $user->login,
|
||||||
WOPI::PROP_USER_ID => md5($user->login),
|
WOPI::PROP_USER_ID => md5($user->login),
|
||||||
WOPI::PROP_USER_AVATAR => $user->avatar_url,
|
WOPI::PROP_USER_AVATAR => $user->avatar_url,
|
||||||
|
WOPI::PROP_LAST_MODIFIED => $this->get_file_property($uri, WOPI::PROP_LAST_MODIFIED, 0),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue