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 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)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
|
@ -253,12 +271,16 @@ class Server
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['HTTP_CONTENT_ENCODING'])) {
|
||||
if (false !== strpos($_SERVER['HTTP_CONTENT_ENCODING'], 'gzip')) {
|
||||
$content_encoding = $this->getHeader('Content-Encoding');
|
||||
|
||||
if ($content_encoding) {
|
||||
if (false !== strpos($content_encoding, 'gzip')) {
|
||||
// Might be supported later?
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -281,14 +303,14 @@ class Server
|
|||
|
||||
// Support for checksum matching
|
||||
// https://dcache.org/old/manuals/UserGuide-6.0/webdav.shtml#checksums
|
||||
if (!empty($_SERVER['HTTP_CONTENT_MD5'])) {
|
||||
$hash = bin2hex(base64_decode($_SERVER['HTTP_CONTENT_MD5']));
|
||||
if ($hash = $this->getHeader('Content-MD5')) {
|
||||
$hash = bin2hex(base64_decode($hash));
|
||||
$hash_algo = 'MD5';
|
||||
}
|
||||
// Support for ownCloud/NextCloud checksum
|
||||
// https://github.com/owncloud-archive/documentation/issues/2964
|
||||
elseif (!empty($_SERVER['HTTP_OC_CHECKSUM'])
|
||||
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $_SERVER['HTTP_OC_CHECKSUM'], $match)) {
|
||||
elseif (($checksum = $this->getHeader('OC-Checksum'))
|
||||
&& preg_match('/MD5:[a-f0-9]{32}|SHA1:[a-f0-9]{40}/', $checksum, $match)) {
|
||||
$hash_algo = strtok($match[0], ':');
|
||||
$hash = strtok('');
|
||||
}
|
||||
|
@ -297,8 +319,8 @@ class Server
|
|||
|
||||
$this->checkLock($uri);
|
||||
|
||||
if (!empty($_SERVER['HTTP_IF_MATCH'])) {
|
||||
$etag = trim($_SERVER['HTTP_IF_MATCH'], '" ');
|
||||
if ($match = $this->getHeader('If-Match')) {
|
||||
$etag = trim($match, '" ');
|
||||
$prop = $this->storage->propfind($uri, ['DAV::getetag'], 0);
|
||||
|
||||
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
|
||||
// This expects a UNIX timestamp
|
||||
$mtime = (int)($_SERVER['HTTP_X_OC_MTIME'] ?? 0) ?: null;
|
||||
$mtime = intval($this->getHeader('X-OC-MTime')) ?: null;
|
||||
|
||||
$this->extendExecutionTime();
|
||||
|
||||
|
@ -316,7 +348,7 @@ class Server
|
|||
|
||||
// mod_fcgid <= 2.3.9 doesn't handle chunked transfer encoding for PUT requests
|
||||
// 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
|
||||
// see https://github.com/php/php-src/issues/9441
|
||||
$l = strlen(fread($stream, 1));
|
||||
|
|
|
@ -77,6 +77,7 @@ class WOPI
|
|||
const PROP_USER_NAME = self::NS . ':UserFriendlyName';
|
||||
const PROP_USER_ID = self::NS . ':UserId';
|
||||
const PROP_USER_AVATAR = self::NS . ':UserExtraInfo-Avatar';
|
||||
const PROP_LAST_MODIFIED = 'DAV::getlastmodified';
|
||||
|
||||
protected AbstractStorage $storage;
|
||||
protected Server $server;
|
||||
|
@ -140,6 +141,8 @@ class WOPI
|
|||
|
||||
$this->server->log('WOPI: => %s', $uri);
|
||||
|
||||
$return = null;
|
||||
|
||||
try {
|
||||
$auth_token = $this->getAuthToken();
|
||||
|
||||
|
@ -175,26 +178,63 @@ class WOPI
|
|||
}
|
||||
|
||||
$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);
|
||||
|
||||
// In WOPI, HTTP response code 201/204 is not accepted, only 200
|
||||
// or Collabora will fail saving
|
||||
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
|
||||
elseif (!$action && $method == 'GET') {
|
||||
$this->server->log('WOPI: => CheckFileInfo');
|
||||
$this->getInfo($uri, $props);
|
||||
$return = $this->getInfo($uri, $props);
|
||||
}
|
||||
else {
|
||||
throw new Exception('Invalid URI', 404);
|
||||
}
|
||||
}
|
||||
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());
|
||||
$return = ['error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
if ($return) {
|
||||
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;
|
||||
|
@ -203,12 +243,16 @@ class WOPI
|
|||
/**
|
||||
* 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;
|
||||
$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 = [
|
||||
'BaseFileName' => basename($uri),
|
||||
'UserFriendlyName' => $props[self::PROP_USER_NAME] ?? 'User',
|
||||
|
@ -234,14 +278,8 @@ class WOPI
|
|||
$data['LastModifiedTime'] = $modified;
|
||||
}
|
||||
|
||||
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT);
|
||||
$this->server->log('WOPI: => Info: %s', $json);
|
||||
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json', true);
|
||||
echo $json;
|
||||
return true;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -658,11 +658,12 @@ class Storage extends AbstractStorage implements TrashInterface
|
|||
$readonly = !is_writeable($path);
|
||||
|
||||
return [
|
||||
WOPI::PROP_FILE_URI => $uri,
|
||||
WOPI::PROP_READ_ONLY => $readonly,
|
||||
WOPI::PROP_USER_NAME => $user->login,
|
||||
WOPI::PROP_USER_ID => md5($user->login),
|
||||
WOPI::PROP_USER_AVATAR => $user->avatar_url,
|
||||
WOPI::PROP_FILE_URI => $uri,
|
||||
WOPI::PROP_READ_ONLY => $readonly,
|
||||
WOPI::PROP_USER_NAME => $user->login,
|
||||
WOPI::PROP_USER_ID => md5($user->login),
|
||||
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