diff --git a/apps/files/appinfo/remote.php b/apps/files/appinfo/remote.php
index 26bef966f798e7d7e32ddf5f0a29229ff20cdeb8..c622f08395891c4f8652d1d288c036a824bb5bdd 100644
--- a/apps/files/appinfo/remote.php
+++ b/apps/files/appinfo/remote.php
@@ -53,6 +53,7 @@ $server->subscribeEvent('beforeMethod', function () use ($server, $objectTree) {
 	$rootDir = new OC_Connector_Sabre_Directory($view, $rootInfo);
 	$objectTree->init($rootDir, $view, $mountManager);
 
+	$server->addPlugin(new \OC\Connector\Sabre\TagsPlugin($objectTree, \OC::$server->getTagManager()));
 	$server->addPlugin(new OC_Connector_Sabre_QuotaPlugin($view));
 }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request
 
diff --git a/lib/private/connector/sabre/directory.php b/lib/private/connector/sabre/directory.php
index bbe0f3452a732945238b85177b463f5c33967c4f..c878e5ee4b49574e450e0fd6d07dd2edf259e2e1 100644
--- a/lib/private/connector/sabre/directory.php
+++ b/lib/private/connector/sabre/directory.php
@@ -24,6 +24,13 @@
 class OC_Connector_Sabre_Directory extends OC_Connector_Sabre_Node
 	implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota {
 
+	/**
+	 * Cached directory content
+	 *
+	 * @var \OCP\Files\FileInfo[]
+	 */
+	private $dirContent;
+
 	/**
 	 * Creates a new file in the directory
 	 *
@@ -138,13 +145,20 @@ class OC_Connector_Sabre_Directory extends OC_Connector_Sabre_Node
 	 * @return \Sabre\DAV\INode[]
 	 */
 	public function getChildren() {
+		if (!is_null($this->dirContent)) {
+			return $this->dirContent;
+		}
+		$folderContent = $this->fileView->getDirectoryContent($this->path);
 
-		$folder_content = $this->fileView->getDirectoryContent($this->path);
+		$properties = array();
 		$paths = array();
-		foreach($folder_content as $info) {
-			$paths[] = $this->path.'/'.$info['name'];
-			$properties[$this->path.'/'.$info['name']][self::GETETAG_PROPERTYNAME] = '"' . $info['etag'] . '"';
+		foreach($folderContent as $info) {
+			$name = $info->getName();
+			$paths[] = $this->path . '/' . $name;
+			$properties[$this->path.'/' . $name][self::GETETAG_PROPERTYNAME] = '"' . $info->getEtag() . '"';
 		}
+		// TODO: move this to a beforeGetPropertiesForPath event to pre-cache properties
+		// TODO: only fetch the requested properties
 		if(count($paths)>0) {
 			//
 			// the number of arguments within IN conditions are limited in most databases
@@ -169,12 +183,13 @@ class OC_Connector_Sabre_Directory extends OC_Connector_Sabre_Node
 		}
 
 		$nodes = array();
-		foreach($folder_content as $info) {
+		foreach($folderContent as $info) {
 			$node = $this->getChild($info->getName(), $info);
-			$node->setPropertyCache($properties[$this->path.'/'.$info['name']]);
+			$node->setPropertyCache($properties[$this->path . '/' . $info->getName()]);
 			$nodes[] = $node;
 		}
-		return $nodes;
+		$this->dirContent = $nodes;
+		return $this->dirContent;
 	}
 
 	/**
diff --git a/lib/private/connector/sabre/node.php b/lib/private/connector/sabre/node.php
index a22dc9c5fbea9177e67b42512a79b0dd3e57fc5a..3173ab8a30fb03b71148c0d941ee2c9327473de3 100644
--- a/lib/private/connector/sabre/node.php
+++ b/lib/private/connector/sabre/node.php
@@ -1,5 +1,7 @@
 <?php
+
 use Sabre\DAV\URLUtil;
+use OC\Connector\Sabre\TagList;
 
 /**
  * ownCloud
@@ -226,6 +228,15 @@ abstract class OC_Connector_Sabre_Node implements \Sabre\DAV\INode, \Sabre\DAV\I
 		return $props;
 	}
 
+	/**
+	 * Returns the cache's file id
+	 *
+	 * @return int
+	 */
+	public function getId() {
+		return $this->info->getId();
+	}
+
 	/**
 	 * @return string|null
 	 */
diff --git a/lib/private/connector/sabre/server.php b/lib/private/connector/sabre/server.php
index 137082eea6719885c6deb68f3cf8bbf29228b81b..a836af2a0b898da97d8c836e773fd60233cdaa3a 100644
--- a/lib/private/connector/sabre/server.php
+++ b/lib/private/connector/sabre/server.php
@@ -144,6 +144,13 @@ class OC_Connector_Sabre_Server extends Sabre\DAV\Server {
 
 		$path = rtrim($path,'/');
 
+		// This event allows people to intercept these requests early on in the
+		// process.
+		//
+		// We're not doing anything with the result, but this can be helpful to
+		// pre-fetch certain expensive live properties.
+		$this->broadCastEvent('beforeGetPropertiesForPath', array($path, $propertyNames, $depth));
+
 		$returnPropertyList = array();
 
 		$parentNode = $this->tree->getNodeForPath($path);
diff --git a/lib/private/connector/sabre/taglist.php b/lib/private/connector/sabre/taglist.php
new file mode 100644
index 0000000000000000000000000000000000000000..56cab393feae10283d49fc3761c4cc34d4617775
--- /dev/null
+++ b/lib/private/connector/sabre/taglist.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Connector\Sabre;
+
+use Sabre\DAV;
+
+/**
+ * TagList property
+ *
+ * This property contains multiple "tag" elements, each containing a tag name.
+ */
+class TagList extends DAV\Property {
+	const NS_OWNCLOUD = 'http://owncloud.org/ns';
+
+    /**
+     * tags
+     *
+     * @var array
+     */
+    private $tags;
+
+    /**
+     * @param array $tags
+     */
+    public function __construct(array $tags) {
+        $this->tags = $tags;
+    }
+
+    /**
+     * Returns the tags
+     *
+     * @return array
+     */
+    public function getTags() {
+
+        return $this->tags;
+
+    }
+
+    /**
+     * Serializes this property.
+     *
+     * @param DAV\Server $server
+     * @param \DOMElement $dom
+     * @return void
+     */
+    public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+        $prefix = $server->xmlNamespaces[self::NS_OWNCLOUD];
+
+        foreach($this->tags as $tag) {
+
+            $elem = $dom->ownerDocument->createElement($prefix . ':tag');
+            $elem->appendChild($dom->ownerDocument->createTextNode($tag));
+
+            $dom->appendChild($elem);
+        }
+
+    }
+
+    /**
+     * Unserializes this property from a DOM Element
+     *
+     * This method returns an instance of this class.
+     * It will only decode tag values.
+     *
+     * @param \DOMElement $dom
+     * @return \OC\Connector\Sabre\TagList
+     */
+    static function unserialize(\DOMElement $dom) {
+
+        $tags = array();
+        foreach($dom->childNodes as $child) {
+            if (DAV\XMLUtil::toClarkNotation($child)==='{' . self::NS_OWNCLOUD . '}tag') {
+                $tags[] = $child->textContent;
+            }
+        }
+        return new self($tags);
+
+    }
+
+}
diff --git a/lib/private/connector/sabre/tagsplugin.php b/lib/private/connector/sabre/tagsplugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..dd0b5172bd6a86955432cf63e8bc026ee99a8255
--- /dev/null
+++ b/lib/private/connector/sabre/tagsplugin.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace OC\Connector\Sabre;
+
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+class TagsPlugin extends \Sabre\DAV\ServerPlugin
+{
+
+	// namespace
+	const NS_OWNCLOUD = 'http://owncloud.org/ns';
+	const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
+	const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
+	const TAG_FAVORITE = '_$!<Favorite>!$_';
+
+	/**
+	 * Reference to main server object
+	 *
+	 * @var \Sabre\DAV\Server
+	 */
+	private $server;
+
+	/**
+	 * @var \OCP\ITagManager
+	 */
+	private $tagManager;
+
+	/**
+	 * @var \OCP\ITags
+	 */
+	private $tagger;
+
+	/**
+	 * Array of file id to tags array
+	 * The null value means the cache wasn't initialized.
+	 *
+	 * @var array
+	 */
+	private $cachedTags;
+
+	/**
+	 * @param \OCP\ITagManager $tagManager tag manager
+	 */
+	public function __construct(\Sabre\DAV\ObjectTree $objectTree, \OCP\ITagManager $tagManager) {
+		$this->objectTree = $objectTree;
+		$this->tagManager = $tagManager;
+		$this->tagger = null;
+		$this->cachedTags = null;
+	}
+
+	/**
+	 * This initializes the plugin.
+	 *
+	 * This function is called by \Sabre\DAV\Server, after
+	 * addPlugin is called.
+	 *
+	 * This method should set up the required event subscriptions.
+	 *
+	 * @param \Sabre\DAV\Server $server
+	 * @return void
+	 */
+	public function initialize(\Sabre\DAV\Server $server) {
+
+		$server->xmlNamespaces[self::NS_OWNCLOUD] = 'oc';
+		$server->propertyMap[self::TAGS_PROPERTYNAME] = 'OC\\Connector\\Sabre\\TagList';
+
+		$this->server = $server;
+		$this->server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
+		$this->server->subscribeEvent('beforeGetPropertiesForPath', array($this, 'beforeGetPropertiesForPath'));
+		$this->server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
+	}
+
+	/**
+	 * Searches and removes a value from the given array
+	 *
+	 * @param array $requestedProps
+	 * @param string $propName to remove
+	 * @return boolean true if the property was present, false otherwise
+	 */
+	private function findAndRemoveProperty(&$requestedProps, $propName) {
+		$index = array_search($propName, $requestedProps);
+		if ($index !== false) {
+			unset($requestedProps[$index]);
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Returns the tagger
+	 *
+	 * @return \OCP\ITags tagger
+	 */
+	private function getTagger() {
+		if (!$this->tagger) {
+			$this->tagger = $this->tagManager->load('files');
+		}
+		return $this->tagger;
+	}
+
+	/**
+	 * Returns tags and favorites.
+	 *
+	 * @param integer $fileId file id
+	 * @return array list($tags, $favorite) with $tags as tag array
+	 * and $favorite is a boolean whether the file was favorited
+	 */
+	private function getTagsAndFav($fileId) {
+		$isFav = false;
+		$tags = $this->getTags($fileId);
+		if ($tags) {
+			$favPos = array_search(self::TAG_FAVORITE, $tags);
+			if ($favPos !== false) {
+				$isFav = true;
+				unset($tags[$favPos]);
+			}
+		}
+		return array($tags, $isFav);
+	}
+
+	/**
+	 * Returns tags for the given file id
+	 *
+	 * @param integer $fileId file id
+	 * @return array list of tags for that file
+	 */
+	private function getTags($fileId) {
+		if (isset($this->cachedTags[$fileId])) {
+			return $this->cachedTags[$fileId];
+		} else {
+			$tags = $this->getTagger()->getTagsForObjects(array($fileId));
+			if ($tags) {
+				return current($tags);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Updates the tags of the given file id
+	 *
+	 * @param int $fileId
+	 * @param array $tags array of tag strings
+	 */
+	private function updateTags($fileId, $tags) {
+		$tagger = $this->getTagger();
+		$currentTags = $this->getTags($fileId);
+
+		$newTags = array_diff($tags, $currentTags);
+		foreach ($newTags as $tag) {
+			if ($tag === self::TAG_FAVORITE) {
+				continue;
+			}
+			$tagger->tagAs($fileId, $tag);
+		}
+		$deletedTags = array_diff($currentTags, $tags);
+		foreach ($deletedTags as $tag) {
+			if ($tag === self::TAG_FAVORITE) {
+				continue;
+			}
+			$tagger->unTag($fileId, $tag);
+		}
+	}
+
+	/**
+	 * Pre-fetch tags info
+	 *
+	 * @param string $path
+	 * @param array $requestedProperties
+	 * @param integer $depth
+	 * @return void
+	 */
+	public function beforeGetPropertiesForPath(
+		$path,
+		array $requestedProperties,
+		$depth
+	) {
+		$node = $this->objectTree->getNodeForPath($path);
+		if (!($node instanceof \OC_Connector_Sabre_Directory)) {
+			return;
+		}
+
+		if ($this->findAndRemoveProperty($requestedProperties, self::TAGS_PROPERTYNAME)
+			|| $this->findAndRemoveProperty($requestedProperties, self::FAVORITE_PROPERTYNAME)
+		) {
+			$fileIds = array();
+			// note: pre-fetching only supported for depth <= 1
+			$folderContent = $node->getChildren();
+			// TODO: refactor somehow with the similar array that is created
+			// in getChildren()
+			foreach ($folderContent as $info) {
+				$fileIds[] = $info->getId();
+			}
+			$tags = $this->getTagger()->getTagsForObjects($fileIds);
+			if ($tags) {
+				$this->cachedTags = $tags;
+			}
+		}
+	}
+
+	/**
+	 * Adds tags and favorites properties to the response,
+	 * if requested.
+	 *
+	 * @param string $path
+	 * @param \Sabre\DAV\INode $node
+	 * @param array $requestedProperties
+	 * @param array $returnedProperties
+	 * @return void
+	 */
+	public function beforeGetProperties(
+		$path,
+		\Sabre\DAV\INode $node,
+		array &$requestedProperties,
+		array &$returnedProperties
+	) {
+		if (!($node instanceof \OC_Connector_Sabre_Node)) {
+			return;
+		}
+
+		$tags = null;
+		$isFav = null;
+		if ($this->findAndRemoveProperty($requestedProperties, self::TAGS_PROPERTYNAME)) {
+			list($tags, $isFav) = $this->getTagsAndFav($node->getId());
+			$returnedProperties[200][self::TAGS_PROPERTYNAME] = new TagList($tags);
+		}
+		if ($this->findAndRemoveProperty($requestedProperties, self::FAVORITE_PROPERTYNAME)) {
+			if (is_null($tags)) {
+				list($tags, $isFav) = $this->getTagsAndFav($node->getId());
+			}
+			$returnedProperties[200][self::FAVORITE_PROPERTYNAME] = $isFav;
+		}
+	}
+
+	/**
+	 * Updates tags and favorites properties, if applicable.
+	 *
+	 * @param string $path
+	 * @param \Sabre\DAV\INode $node
+	 * @param array $requestedProperties
+	 * @param array $returnedProperties
+	 * @return bool success status
+	 */
+	public function updateProperties(array &$properties, array &$result, \Sabre\DAV\INode $node) {
+		if (!($node instanceof \OC_Connector_Sabre_Node)) {
+			return;
+		}
+
+		$fileId = $node->getId();
+		if (isset($properties[self::TAGS_PROPERTYNAME])) {
+			$tagsProp = $properties[self::TAGS_PROPERTYNAME];
+			unset($properties[self::TAGS_PROPERTYNAME]);
+			$this->updateTags($fileId, $tagsProp->getTags());
+			$result[200][self::TAGS_PROPERTYNAME] = new TagList($tagsProp->getTags());
+		}
+		if (isset($properties[self::FAVORITE_PROPERTYNAME])) {
+			$favState = $properties[self::FAVORITE_PROPERTYNAME];
+			unset($properties[self::FAVORITE_PROPERTYNAME]);
+			if ((int)$favState === 1 || $favState === 'true') {
+				$favState = true;
+				$this->getTagger()->tagAs($fileId, self::TAG_FAVORITE);
+			} else {
+				$favState = false;
+				$this->getTagger()->unTag($fileId, self::TAG_FAVORITE);
+			}
+			$result[200][self::FAVORITE_PROPERTYNAME] = $favState;
+		}
+		return true;
+	}
+}
diff --git a/tests/lib/connector/sabre/directory.php b/tests/lib/connector/sabre/directory.php
index d8dca35cd71d997eeb557d50811a01a6d0570383..e9bfea81b779f7f985a0db607dabde7b4b89b787 100644
--- a/tests/lib/connector/sabre/directory.php
+++ b/tests/lib/connector/sabre/directory.php
@@ -101,4 +101,58 @@ class Test_OC_Connector_Sabre_Directory extends \Test\TestCase {
 		$dir = $this->getRootDir();
 		$dir->delete();
 	}
+
+	public function testGetChildren() {
+		$info1 = $this->getMockBuilder('OC\Files\FileInfo')
+			->disableOriginalConstructor()
+			->getMock();
+		$info2 = $this->getMockBuilder('OC\Files\FileInfo')
+			->disableOriginalConstructor()
+			->getMock();
+		$info1->expects($this->any())
+			->method('getName')
+			->will($this->returnValue('first'));
+		$info1->expects($this->any())
+			->method('getEtag')
+			->will($this->returnValue('abc'));
+		$info2->expects($this->any())
+			->method('getName')
+			->will($this->returnValue('second'));
+		$info2->expects($this->any())
+			->method('getEtag')
+			->will($this->returnValue('def'));
+
+		$this->view->expects($this->once())
+			->method('getDirectoryContent')
+			->with('')
+			->will($this->returnValue(array($info1, $info2)));
+
+		$this->view->expects($this->any())
+			->method('getRelativePath')
+			->will($this->returnValue(''));
+
+		$dir = new OC_Connector_Sabre_Directory($this->view, $this->info);
+		$nodes = $dir->getChildren();
+
+		$this->assertEquals(2, count($nodes));
+
+		// calling a second time just returns the cached values,
+		// does not call getDirectoryContents again
+		$nodes = $dir->getChildren();
+
+		$properties = array('testprop', OC_Connector_Sabre_Node::GETETAG_PROPERTYNAME);
+		$this->assertEquals(2, count($nodes));
+		$this->assertEquals(
+			array(
+				OC_Connector_Sabre_Node::GETETAG_PROPERTYNAME => '"abc"'
+			),
+			$nodes[0]->getProperties($properties)
+		);
+		$this->assertEquals(
+			array(
+				OC_Connector_Sabre_Node::GETETAG_PROPERTYNAME => '"def"'
+			),
+			$nodes[1]->getProperties($properties)
+		);
+	}
 }
diff --git a/tests/lib/connector/sabre/tagsplugin.php b/tests/lib/connector/sabre/tagsplugin.php
new file mode 100644
index 0000000000000000000000000000000000000000..2afea061ec3f360f66293cb226cae8fa2486fe7b
--- /dev/null
+++ b/tests/lib/connector/sabre/tagsplugin.php
@@ -0,0 +1,314 @@
+<?php
+
+namespace Tests\Connector\Sabre;
+
+/**
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+class TagsPlugin extends \Test\TestCase {
+
+	const TAGS_PROPERTYNAME = \OC\Connector\Sabre\TagsPlugin::TAGS_PROPERTYNAME;
+	const FAVORITE_PROPERTYNAME = \OC\Connector\Sabre\TagsPlugin::FAVORITE_PROPERTYNAME;
+	const TAG_FAVORITE = \OC\Connector\Sabre\TagsPlugin::TAG_FAVORITE;
+
+	/**
+	 * @var \Sabre\DAV\Server
+	 */
+	private $server;
+
+	/**
+	 * @var \Sabre\DAV\ObjectTree
+	 */
+	private $tree;
+
+	/**
+	 * @var \OCP\ITagManager
+	 */
+	private $tagManager;
+
+	/**
+	 * @var \OCP\ITags
+	 */
+	private $tagger;
+
+	/**
+	 * @var \OC\Connector\Sabre\TagsPlugin
+	 */
+	private $plugin;
+
+	public function setUp() {
+		parent::setUp();
+		$this->server = new \Sabre\DAV\Server();
+		$this->tree = $this->getMockBuilder('\Sabre\DAV\ObjectTree')
+			->disableOriginalConstructor()
+			->getMock();
+		$this->tagger = $this->getMock('\OCP\ITags');
+		$this->tagManager = $this->getMock('\OCP\ITagManager');
+		$this->tagManager->expects($this->any())
+			->method('load')
+			->with('files')
+			->will($this->returnValue($this->tagger));
+		$this->plugin = new \OC\Connector\Sabre\TagsPlugin($this->tree, $this->tagManager);
+		$this->plugin->initialize($this->server);
+	}
+
+	/**
+	 * @dataProvider tagsGetPropertiesDataProvider
+	 */
+	public function testGetProperties($tags, $requestedProperties, $expectedProperties) {
+		$node = $this->getMockBuilder('\OC_Connector_Sabre_Node')
+			->disableOriginalConstructor()
+			->getMock();
+		$node->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(123));
+
+		$expectedCallCount = 0;
+		if (count($requestedProperties) > 0) {
+			$expectedCallCount = 1;
+		}
+
+		$this->tagger->expects($this->exactly($expectedCallCount))
+			->method('getTagsForObjects')
+			->with($this->equalTo(array(123)))
+			->will($this->returnValue(array(123 => $tags)));
+
+		$returnedProperties = array();
+
+		$this->plugin->beforeGetProperties(
+			'',
+			$node,
+			$requestedProperties,
+			$returnedProperties
+		);
+
+		$this->assertEquals($expectedProperties, $returnedProperties);
+	}
+
+	/**
+	 * @dataProvider tagsGetPropertiesDataProvider
+	 */
+	public function testPreloadThenGetProperties($tags, $requestedProperties, $expectedProperties) {
+		$node1 = $this->getMockBuilder('\OC_Connector_Sabre_File')
+			->disableOriginalConstructor()
+			->getMock();
+		$node1->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(111));
+		$node2 = $this->getMockBuilder('\OC_Connector_Sabre_File')
+			->disableOriginalConstructor()
+			->getMock();
+		$node2->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(222));
+
+		$expectedCallCount = 0;
+		if (count($requestedProperties) > 0) {
+			// this guarantees that getTagsForObjects
+			// is only called once and then the tags
+			// are cached
+			$expectedCallCount = 1;
+		}
+
+		$node = $this->getMockBuilder('\OC_Connector_Sabre_Directory')
+			->disableOriginalConstructor()
+			->getMock();
+		$node->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(123));
+		$node->expects($this->exactly($expectedCallCount))
+			->method('getChildren')
+			->will($this->returnValue(array($node1, $node2)));
+
+		$this->tree->expects($this->once())
+			->method('getNodeForPath')
+			->with('/subdir')
+			->will($this->returnValue($node));
+
+		$this->tagger->expects($this->exactly($expectedCallCount))
+			->method('getTagsForObjects')
+			->with($this->equalTo(array(111, 222)))
+			->will($this->returnValue(
+				array(
+					111 => $tags,
+					123 => $tags
+				)
+			));
+
+		$returnedProperties = array();
+
+		$this->plugin->beforeGetPropertiesForPath(
+			'/subdir',
+			$requestedProperties,
+			1
+		);
+
+		$this->plugin->beforeGetProperties(
+			'/subdir/test.txt',
+			$node1,
+			$requestedProperties,
+			$returnedProperties
+		);
+
+		$this->assertEquals($expectedProperties, $returnedProperties);
+	}
+
+	function tagsGetPropertiesDataProvider() {
+		return array(
+			// request both, receive both
+			array(
+				array('tag1', 'tag2', self::TAG_FAVORITE),
+				array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
+				array(
+					200 => array(
+						self::TAGS_PROPERTYNAME => new \OC\Connector\Sabre\TagList(array('tag1', 'tag2')),
+						self::FAVORITE_PROPERTYNAME => true,
+					)
+				)
+			),
+			// request tags alone
+			array(
+				array('tag1', 'tag2', self::TAG_FAVORITE),
+				array(self::TAGS_PROPERTYNAME),
+				array(
+					200 => array(
+						self::TAGS_PROPERTYNAME => new \OC\Connector\Sabre\TagList(array('tag1', 'tag2')),
+					)
+				)
+			),
+			// request fav alone
+			array(
+				array('tag1', 'tag2', self::TAG_FAVORITE),
+				array(self::FAVORITE_PROPERTYNAME),
+				array(
+					200 => array(
+						self::FAVORITE_PROPERTYNAME => true,
+					)
+				)
+			),
+			// request none
+			array(
+				array('tag1', 'tag2', self::TAG_FAVORITE),
+				array(),
+				array(),
+			),
+			// request both with none set, receive both
+			array(
+				array(),
+				array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME),
+				array(
+					200 => array(
+						self::TAGS_PROPERTYNAME => new \OC\Connector\Sabre\TagList(array()),
+						self::FAVORITE_PROPERTYNAME => false,
+					)
+				)
+			),
+		);
+	}
+
+	public function testUpdateTags() {
+		// this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+		// and keep "tagkeep"
+		$node = $this->getMockBuilder('\OC_Connector_Sabre_Node')
+			->disableOriginalConstructor()
+			->getMock();
+		$node->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(123));
+
+		$this->tagger->expects($this->at(0))
+			->method('getTagsForObjects')
+			->with($this->equalTo(array(123)))
+			->will($this->returnValue(array(123 => array('tagkeep', 'tagremove', self::TAG_FAVORITE))));
+
+		// then tag as tag1 and tag2
+		$this->tagger->expects($this->at(1))
+			->method('tagAs')
+			->with(123, 'tag1');
+		$this->tagger->expects($this->at(2))
+			->method('tagAs')
+			->with(123, 'tag2');
+
+		// it will untag tag3
+		$this->tagger->expects($this->at(3))
+			->method('unTag')
+			->with(123, 'tagremove');
+
+		// properties to set
+		$properties = array(
+			self::TAGS_PROPERTYNAME => new \OC\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep'))
+		);
+		$result = array();
+
+		$this->plugin->updateProperties(
+			$properties,
+			$result,
+			$node
+		);
+
+		// all requested properties removed, as they were processed already
+		$this->assertEmpty($properties);
+
+		$this->assertEquals(
+			new \OC\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep')),
+			$result[200][self::TAGS_PROPERTYNAME]
+		);
+		$this->assertFalse(isset($result[200][self::FAVORITE_PROPERTYNAME]));
+	}
+
+	public function testUpdateFav() {
+		// this test will replace the existing tags "tagremove" with "tag1" and "tag2"
+		// and keep "tagkeep"
+		$node = $this->getMockBuilder('\OC_Connector_Sabre_Node')
+			->disableOriginalConstructor()
+			->getMock();
+		$node->expects($this->any())
+			->method('getId')
+			->will($this->returnValue(123));
+
+		// set favorite tag
+		$this->tagger->expects($this->once())
+			->method('tagAs')
+			->with(123, self::TAG_FAVORITE);
+
+		// properties to set
+		$properties = array(
+			self::FAVORITE_PROPERTYNAME => true
+		);
+		$result = array();
+		$this->plugin->updateProperties(
+			$properties,
+			$result,
+			$node
+		);
+
+		// all requested properties removed, as they were processed already
+		$this->assertEmpty($properties);
+
+		$this->assertTrue($result[200][self::FAVORITE_PROPERTYNAME]);
+		$this->assertFalse(isset($result[200][self::TAGS_PROPERTYNAME]));
+
+		// unfavorite now
+		// set favorite tag
+		$this->tagger->expects($this->once())
+			->method('unTag')
+			->with(123, self::TAG_FAVORITE);
+
+		$properties = array(
+			self::FAVORITE_PROPERTYNAME => false
+		);
+		$result = array();
+		$this->plugin->updateProperties(
+			$properties,
+			$result,
+			$node
+		);
+
+		$this->assertFalse($result[200][self::FAVORITE_PROPERTYNAME]);
+		$this->assertFalse(isset($result[200][self::TAGS_PROPERTYNAME]));
+	}
+
+}