Page MenuHomePhorge

D5835.1775187706.diff
No OneTemporary

Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None

D5835.1775187706.diff

diff --git a/src/app/Backends/Storage.php b/src/app/Backends/Storage.php
--- a/src/app/Backends/Storage.php
+++ b/src/app/Backends/Storage.php
@@ -213,6 +213,10 @@
if ($file->type & Item::TYPE_INCOMPLETE) {
$file->type -= Item::TYPE_INCOMPLETE;
$file->save();
+ } else {
+ // Bump last modification time (needed e.g. for proper WebDAV syncronization/ETag)
+ // Note: We don't use touch() directly on $file because it fails when the object has custom properties
+ Item::where('id', $file->id)->touch();
}
// Update the file type and size information
diff --git a/src/app/Http/Controllers/DAVController.php b/src/app/Http/Controllers/DAVController.php
--- a/src/app/Http/Controllers/DAVController.php
+++ b/src/app/Http/Controllers/DAVController.php
@@ -69,6 +69,7 @@
// Register some plugins
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($auth_backend));
+ $server->addPlugin(new DAV\ServerPlugin());
// Unauthenticated access doesn't work for us since we require credentials to get access to the data in the first place.
// $acl_plugin = new \Sabre\DAVACL\Plugin();
diff --git a/src/app/Http/DAV/Collection.php b/src/app/Http/DAV/Collection.php
--- a/src/app/Http/DAV/Collection.php
+++ b/src/app/Http/DAV/Collection.php
@@ -8,15 +8,18 @@
use Sabre\DAV\Exception;
use Sabre\DAV\ICollection;
use Sabre\DAV\ICopyTarget;
+use Sabre\DAV\IExtendedCollection;
use Sabre\DAV\IMoveTarget;
use Sabre\DAV\INode;
use Sabre\DAV\INodeByPath;
use Sabre\DAV\IProperties;
+use Sabre\DAV\MkCol;
+use Sabre\DAV\Xml\Property\ResourceType;
/**
* Sabre DAV Collection interface implemetation
*/
-class Collection extends Node implements ICollection, ICopyTarget, IMoveTarget, INodeByPath, IProperties
+class Collection extends Node implements ICollection, ICopyTarget, IExtendedCollection, IMoveTarget, INodeByPath, IProperties
{
/**
* Checks if a child-node exists.
@@ -76,6 +79,49 @@
return true;
}
+ /**
+ * Creates a new collection.
+ *
+ * This method will receive a MkCol object with all the information about
+ * the new collection that's being created.
+ *
+ * The MkCol object contains information about the resourceType of the new
+ * collection. If you don't support the specified resourceType, you should
+ * throw Exception\InvalidResourceType.
+ *
+ * The object also contains a list of WebDAV properties for the new
+ * collection.
+ *
+ * You should call the handle() method on this object to specify exactly
+ * which properties you are storing. This allows the system to figure out
+ * exactly which properties you didn't store, which in turn allows other
+ * plugins (such as the propertystorage plugin) to handle storing the
+ * property for you.
+ *
+ * @param string $name
+ *
+ * @throws Exception\InvalidResourceType
+ */
+ public function createExtendedCollection($name, MkCol $mkCol)
+ {
+ $types = $mkCol->getResourceType();
+
+ if (count($types) > 1) {
+ // For now we only support use of 'notebook' in the resourcetype (Kolab Notes)
+ if (in_array('{Kolab:}notebook', $types)) {
+ $type = 'notebook';
+ } else {
+ throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
+ }
+ }
+
+ $collection = $this->createDirectory($name);
+
+ if (!empty($type)) {
+ $collection->setProperty('dav:resourcetype', $type);
+ }
+ }
+
/**
* Creates a new file in the directory
*
@@ -160,6 +206,8 @@
}
DB::commit();
+
+ return $collection;
}
/**
@@ -207,9 +255,10 @@
->select('fs_items.*')
->whereNot('type', '&', Item::TYPE_INCOMPLETE);
- foreach (['name', 'size', 'mimetype'] as $key) {
+ foreach (['name', 'size', 'mimetype', 'dav:resourcetype', 'dav:displayname', 'dav:links', 'dav:categories'] as $key) {
+ $alias = str_replace('dav:', '', $key);
$query->selectRaw('(select value from fs_properties where fs_items.id = fs_properties.item_id'
- . " and fs_properties.key = '{$key}') as {$key}");
+ . " and fs_properties.key = '{$key}') as {$alias}");
}
if ($parent = $this->data) {
@@ -309,6 +358,10 @@
$result['{DAV:}creationdate'] = \Sabre\HTTP\toDate($this->data->created_at);
}
+ if (!empty($this->data->resourcetype)) {
+ $result['{DAV:}resourcetype'] = new ResourceType(['{DAV:}collection', "{Kolab:}{$this->data->resourcetype}"]);
+ }
+
return $result;
}
@@ -361,7 +414,6 @@
{
\Log::debug('[DAV] PROP-PATCH: ' . $this->path);
- // not supported
- // FIXME: Should we throw an exception?
+ // Not implemented
}
}
diff --git a/src/app/Http/DAV/File.php b/src/app/Http/DAV/File.php
--- a/src/app/Http/DAV/File.php
+++ b/src/app/Http/DAV/File.php
@@ -3,7 +3,7 @@
namespace App\Http\DAV;
use App\Backends\Storage;
-use Sabre\DAV\Exception;
+use App\Fs\Item;
use Sabre\DAV\IFile;
use Sabre\DAV\IProperties;
@@ -86,6 +86,18 @@
$result['{DAV:}creationdate'] = \Sabre\HTTP\toDate($this->data->created_at);
}
+ if (!empty($this->data->displayname)) {
+ $result['{DAV:}displayname'] = $this->data->displayname;
+ }
+
+ if (isset($this->data->links)) {
+ $result['{Kolab:}links'] = self::propListOutput(\json_decode($this->data->links), 'link');
+ }
+
+ if (isset($this->data->categories)) {
+ $result['{Kolab:}categories'] = self::propListOutput(\json_decode($this->data->categories), 'category');
+ }
+
return $result;
}
@@ -112,8 +124,62 @@
{
\Log::debug('[DAV] PROP-PATCH: ' . $this->path);
- // not supported
- // FIXME: Should we throw an exception?
+ // Note: Here we register handlers that are executed later by Sabre/DAV
+ $propPatch->handle(
+ // Properties used by Kolab Notes
+ ['{DAV:}displayname', '{Kolab:}links', '{Kolab:}categories'],
+ function ($properties) {
+ return $this->propPatchValidateAndSave($properties);
+ }
+ );
+ }
+
+ /**
+ * Validate PROPPATCH properties
+ */
+ protected function propPatchValidateAndSave($properties): array
+ {
+ $result = [];
+ $updated = false;
+
+ foreach ($properties as $key => $value) {
+ $status = true;
+ $prop_name = null;
+
+ switch ($key) {
+ case '{DAV:}displayname':
+ $prop_name = 'dav:displayname';
+ $status = is_string($value);
+ break;
+ case '{Kolab:}categories':
+ case '{Kolab:}links':
+ $prop_name = 'dav:' . str_replace('{Kolab:}', '', $key);
+ $status = is_array($value);
+ break;
+ }
+
+ if ($status && $prop_name) {
+ if ($value === '' || (is_array($value) && empty($value))) {
+ $value = null;
+ }
+ if (is_array($value)) {
+ $value = json_encode($value);
+ }
+
+ $updated = $updated || $value !== ($this->data->{$prop_name} ?? null);
+ $this->data->setProperty($prop_name, $value);
+ }
+
+ $result[$key] = $status ? 200 : 403; // result to SabreDAV
+ }
+
+ // Bump last modification time (needed e.g. for proper WebDAV syncronization/ETag)
+ // Note: We don't use touch() directly on $file because it fails when the object has custom properties
+ if ($updated) {
+ Item::where('id', $this->data->id)->touch();
+ }
+
+ return $result;
}
/**
diff --git a/src/app/Http/DAV/Locks.php b/src/app/Http/DAV/Locks.php
--- a/src/app/Http/DAV/Locks.php
+++ b/src/app/Http/DAV/Locks.php
@@ -33,6 +33,9 @@
{
\Log::debug('[DAV] GET-LOCKS: ' . $uri);
+ // TODO: On a node delete Sabre invokes this method twice (once before and once after)
+ // so there's a place for some optimization.
+
// Note: We're disabling exceptions here, otherwise it has unwanted effects
// in places where Sabre checks locks on non-existing paths
$ids = Node::resolvePath($uri, true);
diff --git a/src/app/Http/DAV/Node.php b/src/app/Http/DAV/Node.php
--- a/src/app/Http/DAV/Node.php
+++ b/src/app/Http/DAV/Node.php
@@ -212,11 +212,18 @@
$item = $query->first();
- // Get file properties
+ // Get file/folder properties
// TODO: In some requests context (e.g. LOCK/UNLOCK) we don't need these extra properties
- if ($item && $item->type == Item::TYPE_FILE) {
- $item->properties()->whereIn('key', ['size', 'mimetype'])->each(function ($prop) use ($item) {
- $item->{$prop->key} = $prop->value;
+ if ($item) {
+ if ($item->isFile()) {
+ $keys = ['size', 'mimetype', 'dav:displayname', 'dav:links', 'dav:categories'];
+ } else {
+ $keys = ['dav:resourcetype'];
+ }
+
+ $item->properties()->whereIn('key', $keys)->each(function ($prop) use ($item) {
+ $key = str_replace('dav:', '', $prop->key);
+ $item->{$key} = $prop->value;
});
}
}
@@ -268,4 +275,20 @@
// that's why we store all lookup results including `false`.
Context::addHidden('fs:' . $path, $item);
}
+
+ /**
+ * Convert an array into XML property understood by the Sabre XML writer
+ */
+ protected static function propListOutput(array $list, string $item_name): array
+ {
+ foreach ($list as $idx => $item) {
+ $list[$idx] = [
+ 'name' => "{Kolab:}{$item_name}",
+ 'value' => $item,
+ 'properties' => [],
+ ];
+ }
+
+ return $list;
+ }
}
diff --git a/src/app/Http/DAV/ServerPlugin.php b/src/app/Http/DAV/ServerPlugin.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/DAV/ServerPlugin.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\DAV;
+
+use Sabre\DAV\Server;
+use Sabre\Xml\Deserializer;
+use Sabre\Xml\Reader;
+
+/**
+ * A plugin covering Kolab XML extensions.
+ *
+ * Plugins can modifies/extends the Sabre server behaviour.
+ */
+class ServerPlugin extends \Sabre\DAV\ServerPlugin
+{
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by Sabre\DAV\Server, after addPlugin is called.
+ */
+ public function initialize(Server $server)
+ {
+ // Tell the XML parser how to handle structured Kolab properties
+ $server->xml->elementMap['{Kolab:}links'] = function (Reader $reader) {
+ return Deserializer\repeatingElements($reader, '{Kolab:}link');
+ };
+
+ $server->xml->elementMap['{Kolab:}categories'] = function (Reader $reader) {
+ return Deserializer\repeatingElements($reader, '{Kolab:}category');
+ };
+ }
+}
diff --git a/src/tests/Feature/Controller/DAVTest.php b/src/tests/Feature/Controller/DAVTest.php
--- a/src/tests/Feature/Controller/DAVTest.php
+++ b/src/tests/Feature/Controller/DAVTest.php
@@ -503,6 +503,23 @@
$this->assertCount(1, $children = $items[0]->children()->get());
$this->assertSame($john->id, $children[0]->user_id);
$this->assertSame('folder2', $children[0]->getProperty('name'));
+
+ // Kolab Notes folder
+ $xml = <<<'EOF'
+ <d:mkcol xmlns:d='DAV:' xmlns:k='Kolab:'>
+ <d:set>
+ <d:prop><d:resourcetype><d:collection/><k:notebook/></d:resourcetype></d:prop>
+ </d:set>
+ </d:mkcol>
+ EOF;
+
+ $response = $this->davRequest('MKCOL', "{$root}/folder1/notes", $xml, $john);
+ $response->assertNoContent(201);
+
+ $this->assertCount(1, $children = $items[0]->children()->whereNot('fs_items.id', $children[0]->id)->get());
+ $this->assertSame($john->id, $children[0]->user_id);
+ $this->assertSame('notes', $children[0]->getProperty('name'));
+ $this->assertSame('notebook', $children[0]->getProperty('dav:resourcetype'));
}
/**
@@ -745,9 +762,25 @@
$this->assertCount(4, $responses = $doc->documentElement->getElementsByTagName('response'));
$this->assertSame("/{$root}/folder1/", $responses[0]->getElementsByTagName('href')->item(0)->textContent);
+ $this->assertSame(1, $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes->count());
+ $this->assertSame('collection', $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes->item(0)->localName);
$this->assertSame("/{$root}/folder1/folder2/", $responses[1]->getElementsByTagName('href')->item(0)->textContent);
$this->assertSame("/{$root}/folder1/test3.txt", $responses[2]->getElementsByTagName('href')->item(0)->textContent);
$this->assertSame("/{$root}/folder1/test4.txt", $responses[3]->getElementsByTagName('href')->item(0)->textContent);
+
+ // Test Kolab Notes folder property
+ $folders[0]->setProperty('dav:resourcetype', 'notebook');
+ $response = $this->davRequest('PROPFIND', "{$root}/folder1", '<d:propfind xmlns:d="DAV:"><d:allprop/></d:propfind>', $john, ['Depth' => 0]);
+ $response->assertStatus(207);
+
+ $doc = $this->responseXML($response);
+ $this->assertSame('multistatus', $doc->documentElement->localName);
+ $this->assertCount(1, $responses = $doc->documentElement->getElementsByTagName('response'));
+
+ $this->assertSame("/{$root}/folder1/", $responses[0]->getElementsByTagName('href')->item(0)->textContent);
+ $this->assertSame(2, $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes->count());
+ $this->assertSame('collection', $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes->item(0)->localName);
+ $this->assertSame('notebook', $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes->item(1)->localName);
}
/**
@@ -855,7 +888,59 @@
$this->assertCount(1, $doc->getElementsByTagName('response'));
$this->assertSame('HTTP/1.1 403 Forbidden', $doc->getElementsByTagName('status')->item(0)->textContent);
- // Note: We don't support any properties in PROPPATCH yet
+ // Test Kolab Notes properties
+ $folder = $this->getTestCollection($john, 'notes');
+ $file = $this->getTestFile($john, 'test.html', '<html>Test con2</html>', ['mimetype' => 'text/html']);
+ $folder->children()->attach($file);
+
+ $xml = <<<'EOF'
+ <d:propertyupdate xmlns:d="DAV:" xmlns:k="Kolab:">
+ <d:set>
+ <d:prop>
+ <d:displayname>test note</d:displayname>
+ <k:categories>
+ <k:category>cat1</k:category>
+ <k:category>cat2</k:category>
+ </k:categories>
+ <k:links>
+ <k:link>imap:///test</k:link>
+ </k:links>
+ </d:prop>
+ </d:set>
+ </d:propertyupdate>
+ EOF;
+
+ $response = $this->davRequest('PROPPATCH', "{$root}/notes/test.html", $xml, $john);
+ $response->assertStatus(207);
+
+ $doc = $this->responseXML($response);
+ $this->assertSame('multistatus', $doc->documentElement->localName);
+
+ // Use PROPFIND to check the properties' values
+ $xml = <<<'EOF'
+ <d:propfind xmlns:d="DAV:" xmlns:k="Kolab:">
+ <d:prop>
+ <d:displayname/>
+ <k:categories/>
+ <k:links/>
+ </d:prop>
+ </d:propfind>
+ EOF;
+
+ $response = $this->davRequest('PROPFIND', "{$root}/notes/test.html", $xml, $john, ['Depth' => '0']);
+ $response->assertStatus(207);
+
+ $doc = $this->responseXML($response);
+ $this->assertSame('multistatus', $doc->documentElement->localName);
+ $this->assertSame("/{$root}/notes/test.html", $doc->getElementsByTagName('href')->item(0)->textContent);
+ $this->assertSame(1, ($links = $doc->getElementsByTagName('links')->item(0))->childNodes->count());
+ $this->assertSame('imap:///test', $links->getElementsByTagName('link')->item(0)->textContent);
+ $this->assertSame(2, ($categories = $doc->getElementsByTagName('categories')->item(0))->childNodes->count());
+ $this->assertSame('cat1', $categories->getElementsByTagName('category')->item(0)->textContent);
+ $this->assertSame('cat2', $categories->getElementsByTagName('category')->item(1)->textContent);
+ $this->assertSame('test note', $doc->getElementsByTagName('displayname')->item(0)->textContent);
+
+ // TODO: Test changing/unsetting above properties
}
/**

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 3:41 AM (14 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822453
Default Alt Text
D5835.1775187706.diff (17 KB)

Event Timeline