diff --git a/db_structure.xml b/db_structure.xml
index 1b38a527a129ebc93605690184323b520015b8a0..be7208aa22e401a66e6a85bedff2e101813b0efa 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -1077,6 +1077,130 @@
 
 	</table>
 
+	<table>
+		<!--
+		List of system-wide tags
+		-->
+		<name>*dbprefix*systemtag</name>
+
+		<declaration>
+
+			<field>
+				<name>id</name>
+				<type>integer</type>
+				<default>0</default>
+				<notnull>true</notnull>
+				<autoincrement>1</autoincrement>
+				<unsigned>true</unsigned>
+				<length>4</length>
+			</field>
+
+			<!-- Tag name -->
+			<field>
+				<name>name</name>
+				<type>text</type>
+				<default></default>
+				<notnull>true</notnull>
+				<length>64</length>
+			</field>
+
+			<!-- Visibility: 0 user-not-visible, 1 user-visible -->
+			<field>
+				<name>visibility</name>
+				<type>integer</type>
+				<default>1</default>
+				<notnull>true</notnull>
+				<length>1</length>
+			</field>
+
+			<!-- Editable: 0 user-not-editable, 1 user-editable -->
+			<field>
+				<name>editable</name>
+				<type>integer</type>
+				<default>1</default>
+				<notnull>true</notnull>
+				<length>1</length>
+			</field>
+
+			<index>
+				<name>tag_ident</name>
+				<unique>true</unique>
+				<field>
+					<name>name</name>
+					<sorting>ascending</sorting>
+				</field>
+				<field>
+					<name>visibility</name>
+					<sorting>ascending</sorting>
+				</field>
+				<field>
+					<name>editable</name>
+					<sorting>ascending</sorting>
+				</field>
+			</index>
+
+		</declaration>
+	</table>
+
+	<table>
+
+		<!--
+		System tag to object associations per object type.
+		-->
+		<name>*dbprefix*systemtag_object_mapping</name>
+
+		<declaration>
+
+		<!-- object id (ex: file id for files)-->
+		<field>
+			<name>objectid</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+			<length>4</length>
+		</field>
+
+		<!-- object type (ex: "files")-->
+		<field>
+			<name>objecttype</name>
+			<type>text</type>
+			<default></default>
+			<notnull>true</notnull>
+			<length>64</length>
+		</field>
+
+		<!-- Foreign Key systemtag::id -->
+		<field>
+			<name>systemtagid</name>
+			<type>integer</type>
+			<default>0</default>
+			<notnull>true</notnull>
+			<unsigned>true</unsigned>
+			<length>4</length>
+		</field>
+
+		<index>
+			<unique>true</unique>
+			<name>mapping</name>
+			<field>
+				<name>objecttype</name>
+				<sorting>ascending</sorting>
+			</field>
+			<field>
+				<name>objectid</name>
+				<sorting>ascending</sorting>
+			</field>
+			<field>
+				<name>systemtagid</name>
+				<sorting>ascending</sorting>
+			</field>
+		</index>
+
+		</declaration>
+
+	</table>
+
 	<table>
 
 		<!--
diff --git a/lib/private/server.php b/lib/private/server.php
index 7f3e3af6994527271e6a64ecf2d64718a44b568e..de3324d2cce1290b420aaf40a701aa92642019a9 100644
--- a/lib/private/server.php
+++ b/lib/private/server.php
@@ -138,6 +138,12 @@ class Server extends SimpleContainer implements IServerContainer {
 			$tagMapper = $c->query('TagMapper');
 			return new TagManager($tagMapper, $c->getUserSession());
 		});
+		$this->registerService('SystemTagManager', function (Server $c) {
+			return new SystemTag\SystemTagManager($c->getDatabaseConnection());
+		});
+		$this->registerService('SystemTagObjectMapper', function (Server $c) {
+			return new SystemTag\SystemTagObjectMapper($c->getDatabaseConnection(), $c->getSystemTagManager());
+		});
 		$this->registerService('RootFolder', function (Server $c) {
 			// TODO: get user and user manager from container as well
 			$user = \OC_User::getUser();
@@ -582,6 +588,29 @@ class Server extends SimpleContainer implements IServerContainer {
 		return $this->query('TagManager');
 	}
 
+	/**
+	 * Returns the system-tag manager
+	 *
+	 * @return \OCP\SystemTag\ISystemTagManager
+	 *
+	 * @since 9.0.0
+	 */
+	public function getSystemTagManager() {
+		return $this->query('SystemTagManager');
+	}
+
+	/**
+	 * Returns the system-tag object mapper
+	 *
+	 * @return \OCP\SystemTag\ISystemTagObjectMapper
+	 *
+	 * @since 9.0.0
+	 */
+	public function getSystemTagObjectMapper() {
+		return $this->query('SystemTagObjectMapper');
+	}
+
+
 	/**
 	 * Returns the avatar manager, used for avatar functionality
 	 *
diff --git a/lib/private/systemtag/systemtag.php b/lib/private/systemtag/systemtag.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f4f7090b21d74a91e4327ac9158d1146b5f502f
--- /dev/null
+++ b/lib/private/systemtag/systemtag.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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, version 3,
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\SystemTag;
+
+use OCP\SystemTag\ISystemTag;
+
+class SystemTag implements ISystemTag {
+
+	/**
+	 * @var string
+	 */
+	private $id;
+
+	/**
+	 * @var string
+	 */
+	private $name;
+
+	/**
+	 * @var bool
+	 */
+	private $userVisible;
+
+	/**
+	 * @var bool
+	 */
+	private $userAssignable;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param string $id tag id
+	 * @param string $name tag name
+	 * @param bool $userVisible whether the tag is user visible
+	 * @param bool $userAssignable whether the tag is user assignable
+	 */
+	public function __construct($id, $name, $userVisible, $userAssignable) {
+		$this->id = $id;
+		$this->name = $name;
+		$this->userVisible = $userVisible;
+		$this->userAssignable = $userAssignable;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getId() {
+		return $this->id;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getName() {
+		return $this->name;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function isUserVisible() {
+		return $this->userVisible;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function isUserAssignable() {
+		return $this->userAssignable;
+	}
+}
diff --git a/lib/private/systemtag/systemtagmanager.php b/lib/private/systemtag/systemtagmanager.php
new file mode 100644
index 0000000000000000000000000000000000000000..95b9a61ca38ccd7f29ae03e959982d653526b631
--- /dev/null
+++ b/lib/private/systemtag/systemtagmanager.php
@@ -0,0 +1,265 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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, version 3,
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\SystemTag;
+
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OCP\IDBConnection;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\TagAlreadyExistsException;
+use OCP\SystemTag\TagNotFoundException;
+
+class SystemTagManager implements ISystemTagManager {
+
+	const TAG_TABLE = 'systemtag';
+
+	/**
+	 * @var IDBConnection
+	 */
+	private $connection;
+
+	/**
+	 * Prepared query for selecting tags directly
+	 *
+	 * @var \OCP\DB\QueryBuilder\IQueryBuilder
+	 */
+	private $selectTagQuery;
+
+	/**
+	* Constructor.
+	*
+	* @param IDBConnection $connection database connection
+	*/
+	public function __construct(IDBConnection $connection) {
+		$this->connection = $connection;
+
+		$query = $this->connection->getQueryBuilder();
+		$this->selectTagQuery = $query->select('*')
+			->from(self::TAG_TABLE)
+			->where($query->expr()->eq('name', $query->createParameter('name')))
+			->andWhere($query->expr()->eq('visibility', $query->createParameter('visibility')))
+			->andWhere($query->expr()->eq('editable', $query->createParameter('editable')));
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getTagsById($tagIds) {
+		if (!is_array($tagIds)) {
+			$tagIds = [$tagIds];
+		}
+
+		$tags = [];
+
+		// note: not all databases will fail if it's a string or starts with a number
+		foreach ($tagIds as $tagId) {
+			if (!is_numeric($tagId)) {
+				throw new \InvalidArgumentException('Tag id must be integer');
+			}
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select('*')
+			->from(self::TAG_TABLE)
+			->where($query->expr()->in('id', $query->createParameter('tagids')))
+			->addOrderBy('name', 'ASC')
+			->addOrderBy('visibility', 'ASC')
+			->addOrderBy('editable', 'ASC')
+			->setParameter('tagids', $tagIds, Connection::PARAM_INT_ARRAY);
+
+		$result = $query->execute();
+		while ($row = $result->fetch()) {
+			$tags[$row['id']] = $this->createSystemTagFromRow($row);
+		}
+
+		$result->closeCursor();
+
+		if (count($tags) !== count($tagIds)) {
+			throw new TagNotFoundException(
+				'Tag(s) with id(s) ' . json_encode(array_diff($tagIds, array_keys($tags))) . ' not found'
+			);
+		}
+
+		return $tags;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getAllTags($visibilityFilter = null, $nameSearchPattern = null) {
+		$tags = [];
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select('*')
+			->from(self::TAG_TABLE);
+
+		if (!is_null($visibilityFilter)) {
+			$query->andWhere($query->expr()->eq('visibility', $query->createNamedParameter((int)$visibilityFilter)));
+		}
+
+		if (!empty($nameSearchPattern)) {
+			$query->andWhere(
+				$query->expr()->like(
+					'name',
+					$query->expr()->literal('%' . $this->connection->escapeLikeParameter($nameSearchPattern). '%')
+				)
+			);
+		}
+
+		$query
+			->addOrderBy('name', 'ASC')
+			->addOrderBy('visibility', 'ASC')
+			->addOrderBy('editable', 'ASC');
+
+		$result = $query->execute();
+		while ($row = $result->fetch()) {
+			$tags[$row['id']] = $this->createSystemTagFromRow($row);
+		}
+
+		$result->closeCursor();
+
+		return $tags;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getTag($tagName, $userVisible, $userAssignable) {
+		$userVisible = (int)$userVisible;
+		$userAssignable = (int)$userAssignable;
+
+		$result = $this->selectTagQuery
+			->setParameter('name', $tagName)
+			->setParameter('visibility', $userVisible)
+			->setParameter('editable', $userAssignable)
+			->execute();
+
+		$row = $result->fetch();
+		$result->closeCursor();
+		if (!$row) {
+			throw new TagNotFoundException(
+				'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') does not exist'
+			);
+		}
+
+		return $this->createSystemTagFromRow($row);
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function createTag($tagName, $userVisible, $userAssignable) {
+		$userVisible = (int)$userVisible;
+		$userAssignable = (int)$userAssignable;
+
+		$query = $this->connection->getQueryBuilder();
+		$query->insert(self::TAG_TABLE)
+			->values([
+				'name' => $query->createNamedParameter($tagName),
+				'visibility' => $query->createNamedParameter($userVisible),
+				'editable' => $query->createNamedParameter($userAssignable),
+			]);
+
+		try {
+			$query->execute();
+		} catch (UniqueConstraintViolationException $e) {
+			throw new TagAlreadyExistsException(
+				'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists',
+				0,
+				$e
+			);
+		}
+
+		$tagId = $this->connection->lastInsertId('*PREFIX*' . self::TAG_TABLE);
+
+		return new SystemTag(
+			(int)$tagId,
+			$tagName,
+			(bool)$userVisible,
+			(bool)$userAssignable
+		);
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function updateTag($tagId, $tagName, $userVisible, $userAssignable) {
+		$userVisible = (int)$userVisible;
+		$userAssignable = (int)$userAssignable;
+
+		$query = $this->connection->getQueryBuilder();
+		$query->update(self::TAG_TABLE)
+			->set('name', $query->createParameter('name'))
+			->set('visibility', $query->createParameter('visibility'))
+			->set('editable', $query->createParameter('editable'))
+			->where($query->expr()->eq('id', $query->createParameter('tagid')))
+			->setParameter('name', $tagName)
+			->setParameter('visibility', $userVisible)
+			->setParameter('editable', $userAssignable)
+			->setParameter('tagid', $tagId);
+
+		try {
+			if ($query->execute() === 0) {
+				throw new TagNotFoundException(
+					'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') does not exist'
+				);
+			}
+		} catch (UniqueConstraintViolationException $e) {
+			throw new TagAlreadyExistsException(
+				'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists',
+				0,
+				$e
+			);
+		}
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function deleteTags($tagIds) {
+		if (!is_array($tagIds)) {
+			$tagIds = [$tagIds];
+		}
+
+		// delete relationships first
+		$query = $this->connection->getQueryBuilder();
+		$query->delete(SystemTagObjectMapper::RELATION_TABLE)
+			->where($query->expr()->in('systemtagid', $query->createParameter('tagids')))
+			->setParameter('tagids', $tagIds, Connection::PARAM_INT_ARRAY)
+			->execute();
+
+		$query = $this->connection->getQueryBuilder();
+		$query->delete(self::TAG_TABLE)
+			->where($query->expr()->in('id', $query->createParameter('tagids')))
+			->setParameter('tagids', $tagIds, Connection::PARAM_INT_ARRAY);
+
+		if ($query->execute() === 0) {
+			throw new TagNotFoundException(
+				'Tag does not exist'
+			);
+		}
+	}
+
+	private function createSystemTagFromRow($row) {
+		return new SystemTag((int)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable']);
+	}
+}
diff --git a/lib/private/systemtag/systemtagobjectmapper.php b/lib/private/systemtag/systemtagobjectmapper.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8ff069910d32c41f97def555296ef9453c3bd3c
--- /dev/null
+++ b/lib/private/systemtag/systemtagobjectmapper.php
@@ -0,0 +1,225 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program 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, version 3,
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\SystemTag;
+
+use Doctrine\DBAL\Connection;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OCP\IDBConnection;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\ISystemTagObjectMapper;
+use OCP\SystemTag\TagNotFoundException;
+
+class SystemTagObjectMapper implements ISystemTagObjectMapper {
+
+	const RELATION_TABLE = 'systemtag_object_mapping';
+
+	/**
+	 * @var ISystemTagManager
+	 */
+	private $tagManager;
+
+	/**
+	 * @var IDBConnection
+	 */
+	private $connection;
+
+	/**
+	* Constructor.
+	*
+	* @param IDBConnection $connection database connection
+	* @param ISystemTagManager $tagManager system tag manager
+	*/
+	public function __construct(IDBConnection $connection, ISystemTagManager $tagManager) {
+		$this->connection = $connection;
+		$this->tagManager = $tagManager;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getTagIdsForObjects($objIds, $objectType) {
+		if (!is_array($objIds)) {
+			$objIds = [$objIds];
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select(['systemtagid', 'objectid'])
+			->from(self::RELATION_TABLE)
+			->where($query->expr()->in('objectid', $query->createParameter('objectids')))
+			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
+			->setParameter('objectids', $objIds, Connection::PARAM_INT_ARRAY)
+			->setParameter('objecttype', $objectType)
+			->addOrderBy('objectid', 'ASC')
+			->addOrderBy('systemtagid', 'ASC');
+
+		$mapping = [];
+		foreach ($objIds as $objId) {
+			$mapping[$objId] = [];
+		}
+
+		$result = $query->execute();
+		while ($row = $result->fetch()) {
+			$objectId = $row['objectid'];
+			$mapping[$objectId][] = $row['systemtagid'];
+		}
+
+		$result->closeCursor();
+
+		return $mapping;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getObjectIdsForTags($tagIds, $objectType) {
+		if (!is_array($tagIds)) {
+			$tagIds = [$tagIds];
+		}
+
+		$this->assertTagsExist($tagIds);
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select($query->createFunction('DISTINCT(`objectid`)'))
+			->from(self::RELATION_TABLE)
+			->where($query->expr()->in('systemtagid', $query->createParameter('tagids')))
+			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
+			->setParameter('tagids', $tagIds, Connection::PARAM_INT_ARRAY)
+			->setParameter('objecttype', $objectType);
+
+		$objectIds = [];
+
+		$result = $query->execute();
+		while ($row = $result->fetch()) {
+			$objectIds[] = $row['objectid'];
+		}
+
+		return $objectIds;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function assignTags($objId, $objectType, $tagIds) {
+		if (!is_array($tagIds)) {
+			$tagIds = [$tagIds];
+		}
+
+		$this->assertTagsExist($tagIds);
+
+		$query = $this->connection->getQueryBuilder();
+		$query->insert(self::RELATION_TABLE)
+			->values([
+				'objectid' => $query->createNamedParameter($objId),
+				'objecttype' => $query->createNamedParameter($objectType),
+				'systemtagid' => $query->createParameter('tagid'),
+			]);
+
+		foreach ($tagIds as $tagId) {
+			try {
+				$query->setParameter('tagid', $tagId);
+				$query->execute();
+			} catch (UniqueConstraintViolationException $e) {
+				// ignore existing relations
+			}
+		}
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function unassignTags($objId, $objectType, $tagIds) {
+		if (!is_array($tagIds)) {
+			$tagIds = [$tagIds];
+		}
+
+		$this->assertTagsExist($tagIds);
+
+		$query = $this->connection->getQueryBuilder();
+		$query->delete(self::RELATION_TABLE)
+			->where($query->expr()->eq('objectid', $query->createParameter('objectid')))
+			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
+			->andWhere($query->expr()->in('systemtagid', $query->createParameter('tagids')))
+			->setParameter('objectid', $objId)
+			->setParameter('objecttype', $objectType)
+			->setParameter('tagids', $tagIds, Connection::PARAM_INT_ARRAY)
+			->execute();
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function haveTag($objIds, $objectType, $tagId, $all = true) {
+		$this->assertTagsExist([$tagId]);
+
+		$query = $this->connection->getQueryBuilder();
+
+		if (!$all) {
+			// If we only need one entry, we make the query lighter, by not
+			// counting the elements
+			$query->select('*')
+				->setMaxResults(1);
+		} else {
+			$query->select($query->createFunction('COUNT(1)'));
+		}
+
+		$query->from(self::RELATION_TABLE)
+			->where($query->expr()->in('objectid', $query->createParameter('objectids')))
+			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
+			->andWhere($query->expr()->eq('systemtagid', $query->createParameter('tagid')))
+			->setParameter('objectids', $objIds, Connection::PARAM_INT_ARRAY)
+			->setParameter('tagid', $tagId)
+			->setParameter('objecttype', $objectType);
+
+		$result = $query->execute();
+		$row = $result->fetch(\PDO::FETCH_NUM);
+		$result->closeCursor();
+
+		if ($all) {
+			return ((int)$row[0] === count($objIds));
+		} else {
+			return (bool) $row;
+		}
+	}
+
+	/**
+	 * Asserts that all the given tag ids exist.
+	 *
+	 * @param string[] $tagIds tag ids to check
+	 *
+	 * @throws \OCP\SystemTag\TagNotFoundException if at least one tag did not exist
+	 */
+	private function assertTagsExist($tagIds) {
+		$tags = $this->tagManager->getTagsById($tagIds);
+		if (count($tags) !== count($tagIds)) {
+			// at least one tag missing, bail out
+			$foundTagIds = array_map(
+				function(ISystemTag $tag) {
+					return $tag->getId();
+				},
+				$tags
+			);
+			$missingTagIds = array_diff($tagIds, $foundTagIds);
+			throw new TagNotFoundException('Tags ' . json_encode($missingTagIds) . ' do not exist');
+		}
+	}
+}
diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php
index d85f812b2e7ba9394670ab4c38842241a969f79d..7cb2672254b18782a8ac29f7550a75f0e7e24b0c 100644
--- a/lib/public/iservercontainer.php
+++ b/lib/public/iservercontainer.php
@@ -470,4 +470,22 @@ interface IServerContainer {
 	 * @since 8.2.0
 	 */
 	public function getNotificationManager();
+
+	/**
+	 * Returns the system-tag manager
+	 *
+	 * @return \OCP\SystemTag\ISystemTagManager
+	 *
+	 * @since 9.0.0
+	 */
+	public function getSystemTagManager();
+
+	/**
+	 * Returns the system-tag object mapper
+	 *
+	 * @return \OCP\SystemTag\ISystemTagObjectMapper
+	 *
+	 * @since 9.0.0
+	 */
+	public function getSystemTagObjectMapper();
 }
diff --git a/lib/public/systemtag/isystemtag.php b/lib/public/systemtag/isystemtag.php
index 76a812f38dc7c8c33cd2f6833ee150c4b19a69d6..26609fd8af72aca09ac507a8f95023f47f759bc6 100644
--- a/lib/public/systemtag/isystemtag.php
+++ b/lib/public/systemtag/isystemtag.php
@@ -62,7 +62,7 @@ interface ISystemTag {
 	 *
 	 * @since 9.0.0
 	 */
-	public function isUserAsssignable();
+	public function isUserAssignable();
 
 }
 
diff --git a/lib/public/systemtag/isystemtagsmanager.php b/lib/public/systemtag/isystemtagmanager.php
similarity index 88%
rename from lib/public/systemtag/isystemtagsmanager.php
rename to lib/public/systemtag/isystemtagmanager.php
index df59cc48d52e72977c226374b5acd2c7c3a7929f..2020ec5290052a651a8fcd38afd689d89b29a89b 100644
--- a/lib/public/systemtag/isystemtagsmanager.php
+++ b/lib/public/systemtag/isystemtagmanager.php
@@ -31,9 +31,11 @@ interface ISystemTagManager {
 	/**
 	 * Returns the tag objects matching the given tag ids.
 	 *
-	 * @param array|string $tagIds The ID or array of IDs of the tags to retrieve
+	 * @param array|string $tagIds id or array of unique ids of the tag to retrieve
 	 *
-	 * @return \OCP\SystemTag\ISystemTag[] array of system tags or empty array if none found
+	 * @return \OCP\SystemTag\ISystemTag[] array of system tags with tag id as key
+	 *
+	 * @throws \OCP\SystemTag\TagNotFoundException if at least one given tag id did no exist
 	 *
 	 * @since 9.0.0
 	 */
@@ -72,14 +74,14 @@ interface ISystemTagManager {
 	/**
 	 * Returns all known tags, optionally filtered by visibility.
 	 *
-	 * @param bool $visibleOnly whether to only return user visible tags
+	 * @param bool|null $visibilityFilter filter by visibility if non-null
 	 * @param string $nameSearchPattern optional search pattern for the tag name
 	 *
 	 * @return \OCP\SystemTag\ISystemTag[] array of system tags or empty array if none found
 	 *
 	 * @since 9.0.0
 	 */
-	public function getAllTags($visibleOnly = false, $nameSearchPattern = null);
+	public function getAllTags($visibilityFilter = null, $nameSearchPattern = null);
 
 	/**
 	 * Updates the given tag
diff --git a/lib/public/systemtag/isystemtagobjectmapper.php b/lib/public/systemtag/isystemtagobjectmapper.php
index 8c6c27c4846cd613da1f448a1aa9fdbed7c32130..e2ac1fab1241eef00fcc9d7927489482d96c77eb 100644
--- a/lib/public/systemtag/isystemtagobjectmapper.php
+++ b/lib/public/systemtag/isystemtagobjectmapper.php
@@ -69,6 +69,11 @@ interface ISystemTagObjectMapper {
 	/**
 	 * Assign the given tags to the given object.
 	 *
+	 * If at least one of the given tag ids doesn't exist, none of the tags
+	 * will be assigned.
+	 *
+	 * If the relationship already existed, fail silently.
+	 *
 	 * @param string $objId object id
 	 * @param string $objectType object type
 	 * @param string|array $tagIds tag id or array of tag ids to assign
@@ -83,6 +88,11 @@ interface ISystemTagObjectMapper {
 	/**
 	 * Unassign the given tags from the given object.
 	 *
+	 * If at least one of the given tag ids doesn't exist, none of the tags
+	 * will be unassigned.
+	 *
+	 * If the relationship did not exist in the first place, fail silently.
+	 *
 	 * @param string $objId object id
 	 * @param string $objectType object type
 	 * @param string|array $tagIds tag id or array of tag ids to unassign
diff --git a/tests/lib/systemtag/systemtagmanagertest.php b/tests/lib/systemtag/systemtagmanagertest.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a192f01f414d3287223add62ec8c59c60635ce0
--- /dev/null
+++ b/tests/lib/systemtag/systemtagmanagertest.php
@@ -0,0 +1,405 @@
+<?php
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ *
+*/
+
+namespace Test\SystemTag;
+
+use OC\SystemTag\SystemTagManager;
+use OC\SystemTag\SystemTagObjectMapper;
+use OCP\IDBConnection;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use Test\TestCase;
+
+/**
+ * Class TestSystemTagManager
+ *
+ * @group DB
+ * @package Test\SystemTag
+ */
+class SystemTagManagerTest extends TestCase {
+
+	/**
+	 * @var ISystemTagManager
+	 **/
+	private $tagManager;
+
+	/**
+	 * @var IDBConnection
+	 */
+	private $connection;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->connection = \OC::$server->getDatabaseConnection();
+		$this->tagManager = new SystemTagManager($this->connection);
+	} 
+
+	public function tearDown() {
+		$query = $this->connection->getQueryBuilder();
+		$query->delete(SystemTagObjectMapper::RELATION_TABLE)->execute();
+		$query->delete(SystemTagManager::TAG_TABLE)->execute();
+	}
+
+	public function getAllTagsDataProvider() {
+		return [
+			[
+				// no tags at all
+				[]
+			],
+			[
+				// simple
+				[
+					['one', false, false],
+					['two', false, false],
+				]
+			],
+			[
+				// duplicate names, different flags
+				[
+					['one', false, false],
+					['one', true, false],
+					['one', false, true],
+					['one', true, true],
+					['two', false, false],
+					['two', false, true],
+				]
+			]
+		];
+	}
+
+	/**
+	 * @dataProvider getAllTagsDataProvider
+	 */
+	public function testGetAllTags($testTags) {
+		$testTagsById = [];
+		foreach ($testTags as $testTag) {
+			$tag = $this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]);
+			$testTagsById[$tag->getId()] = $tag;
+		}
+
+		$tagList = $this->tagManager->getAllTags();
+
+		$this->assertCount(count($testTags), $tagList);
+
+		foreach ($testTagsById as $testTagId => $testTag) {
+			$this->assertTrue(isset($tagList[$testTagId]));
+			$this->assertSameTag($tagList[$testTagId], $testTag);
+		}
+	}
+
+	public function getAllTagsFilteredDataProvider() {
+		return [
+			[
+				[
+					// no tags at all
+				],
+				null,
+				null,
+				[]
+			],
+			// filter by visibile only
+			[
+				// none visible
+				[
+					['one', false, false],
+					['two', false, false],
+				],
+				true,
+				null,
+				[]
+			],
+			[
+				// one visible
+				[
+					['one', true, false],
+					['two', false, false],
+				],
+				true,
+				null,
+				[
+					['one', true, false],
+				]
+			],
+			[
+				// one invisible
+				[
+					['one', true, false],
+					['two', false, false],
+				],
+				false,
+				null,
+				[
+					['two', false, false],
+				]
+			],
+			// filter by name pattern
+			[
+				[
+					['one', true, false],
+					['one', false, false],
+					['two', true, false],
+				],
+				null,
+				'on',
+				[
+					['one', true, false],
+					['one', false, false],
+				]
+			],
+			// filter by name pattern and visibility
+			[
+				// one visible
+				[
+					['one', true, false],
+					['two', true, false],
+					['one', false, false],
+				],
+				true,
+				'on',
+				[
+					['one', true, false],
+				]
+			],
+			// filter by name pattern in the middle
+			[
+				// one visible
+				[
+					['abcdefghi', true, false],
+					['two', true, false],
+				],
+				null,
+				'def',
+				[
+					['abcdefghi', true, false],
+				]
+			]
+		];
+	}
+
+	/**
+	 * @dataProvider getAllTagsFilteredDataProvider
+	 */
+	public function testGetAllTagsFiltered($testTags, $visibilityFilter, $nameSearch, $expectedResults) {
+		foreach ($testTags as $testTag) {
+			$this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]);
+		}
+
+		$testTagsById = [];
+		foreach ($expectedResults as $expectedTag) {
+			$tag = $this->tagManager->getTag($expectedTag[0], $expectedTag[1], $expectedTag[2]);
+			$testTagsById[$tag->getId()] = $tag;
+		}
+
+		$tagList = $this->tagManager->getAllTags($visibilityFilter, $nameSearch);
+
+		$this->assertCount(count($testTagsById), $tagList);
+
+		foreach ($testTagsById as $testTagId => $testTag) {
+			$this->assertTrue(isset($tagList[$testTagId]));
+			$this->assertSameTag($tagList[$testTagId], $testTag);
+		}
+	}
+
+	public function oneTagMultipleFlagsProvider() {
+		return [
+			['one', false, false],
+			['one', true, false],
+			['one', false, true],
+			['one', true, true],
+		];
+	}
+
+	/**
+	 * @dataProvider oneTagMultipleFlagsProvider
+	 * @expectedException \OCP\SystemTag\TagAlreadyExistsException
+	 */
+	public function testCreateDuplicate($name, $userVisible, $userAssignable) {
+		try {
+			$this->tagManager->createTag($name, $userVisible, $userAssignable);
+		} catch (\Exception $e) {
+			$this->assertTrue(false, 'No exception thrown for the first create call');
+		}
+		$this->tagManager->createTag($name, $userVisible, $userAssignable);
+	}
+
+	/**
+	 * @dataProvider oneTagMultipleFlagsProvider
+	 */
+	public function testGetExistingTag($name, $userVisible, $userAssignable) {
+		$tag1 = $this->tagManager->createTag($name, $userVisible, $userAssignable);
+		$tag2 = $this->tagManager->getTag($name, $userVisible, $userAssignable);
+
+		$this->assertSameTag($tag1, $tag2);
+	}
+
+	public function testGetExistingTagById() {
+		$tag1 = $this->tagManager->createTag('one', true, false);
+		$tag2 = $this->tagManager->createTag('two', false, true);
+
+		$tagList = $this->tagManager->getTagsById([$tag1->getId(), $tag2->getId()]);
+
+		$this->assertCount(2, $tagList);
+
+		$this->assertSameTag($tag1, $tagList[$tag1->getId()]);
+		$this->assertSameTag($tag2, $tagList[$tag2->getId()]);
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testGetNonExistingTag() {
+		$this->tagManager->getTag('nonexist', false, false);
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testGetNonExistingTagsById() {
+		$tag1 = $this->tagManager->createTag('one', true, false);
+		$this->tagManager->getTagsById([$tag1->getId(), 100, 101]);
+	}
+
+	/**
+	 * @expectedException \InvalidArgumentException
+	 */
+	public function testGetInvalidTagIdFormat() {
+		$tag1 = $this->tagManager->createTag('one', true, false);
+		$this->tagManager->getTagsById([$tag1->getId() . 'suffix']);
+	}
+
+	public function updateTagProvider() {
+		return [
+			[
+				// update name
+				['one', true, true],
+				['two', true, true]
+			],
+			[
+				// update one flag
+				['one', false, true],
+				['one', true, true]
+			],
+			[
+				// update all flags
+				['one', false, false],
+				['one', true, true]
+			],
+			[
+				// update all
+				['one', false, false],
+				['two', true, true]
+			],
+		];
+	}
+
+	/**
+	 * @dataProvider updateTagProvider
+	 */
+	public function testUpdateTag($tagCreate, $tagUpdated) {
+		$tag1 = $this->tagManager->createTag(
+			$tagCreate[0],
+			$tagCreate[1],
+			$tagCreate[2]
+		);
+		$this->tagManager->updateTag(
+			$tag1->getId(),
+			$tagUpdated[0],
+			$tagUpdated[1],
+			$tagUpdated[2]
+		);
+		$tag2 = $this->tagManager->getTag(
+			$tagUpdated[0],
+			$tagUpdated[1],
+			$tagUpdated[2]
+		);
+
+		$this->assertEquals($tag2->getId(), $tag1->getId());
+		$this->assertEquals($tag2->getName(), $tagUpdated[0]);
+		$this->assertEquals($tag2->isUserVisible(), $tagUpdated[1]);
+		$this->assertEquals($tag2->isUserAssignable(), $tagUpdated[2]);
+	}
+
+	/**
+	 * @dataProvider updateTagProvider
+	 * @expectedException \OCP\SystemTag\TagAlreadyExistsException
+	 */
+	public function testUpdateTagDuplicate($tagCreate, $tagUpdated) {
+		$this->tagManager->createTag(
+			$tagCreate[0],
+			$tagCreate[1],
+			$tagCreate[2]
+		);
+		$tag2 = $this->tagManager->createTag(
+			$tagUpdated[0],
+			$tagUpdated[1],
+			$tagUpdated[2]
+		);
+
+		// update to match the first tag
+		$this->tagManager->updateTag(
+			$tag2->getId(),
+			$tagCreate[0],
+			$tagCreate[1],
+			$tagCreate[2]
+		);
+	}
+
+	public function testDeleteTags() {
+		$tag1 = $this->tagManager->createTag('one', true, false);
+		$tag2 = $this->tagManager->createTag('two', false, true);
+
+		$this->tagManager->deleteTags([$tag1->getId(), $tag2->getId()]);
+
+		$this->assertEmpty($this->tagManager->getAllTags());
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testDeleteNonExistingTag() {
+		$this->tagManager->deleteTags([100]);
+	}
+
+	public function testDeleteTagRemovesRelations() {
+		$tag1 = $this->tagManager->createTag('one', true, false);
+		$tag2 = $this->tagManager->createTag('two', true, true);
+
+		$tagMapper = new SystemTagObjectMapper($this->connection, $this->tagManager);
+
+		$tagMapper->assignTags(1, 'testtype', $tag1->getId());
+		$tagMapper->assignTags(1, 'testtype', $tag2->getId());
+		$tagMapper->assignTags(2, 'testtype', $tag1->getId());
+
+		$this->tagManager->deleteTags($tag1->getId());
+
+		$tagIdMapping = $tagMapper->getTagIdsForObjects(
+			[1, 2],
+			'testtype'
+		);
+
+		$this->assertEquals([
+			1 => [$tag2->getId()],
+			2 => [],
+		], $tagIdMapping);
+	}
+
+	/**
+	 * @param ISystemTag $tag1
+	 * @param ISystemTag $tag2
+	 */
+	private function assertSameTag($tag1, $tag2) {
+		$this->assertEquals($tag1->getId(), $tag2->getId());
+		$this->assertEquals($tag1->getName(), $tag2->getName());
+		$this->assertEquals($tag1->isUserVisible(), $tag2->isUserVisible());
+		$this->assertEquals($tag1->isUserAssignable(), $tag2->isUserAssignable());
+	}
+
+}
diff --git a/tests/lib/systemtag/systemtagobjectmappertest.php b/tests/lib/systemtag/systemtagobjectmappertest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a4312fa722d0b64863b3e42ddbf9de5fb04bea35
--- /dev/null
+++ b/tests/lib/systemtag/systemtagobjectmappertest.php
@@ -0,0 +1,335 @@
+<?php
+
+/**
+ * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ *
+*/
+
+namespace Test\SystemTag;
+
+use OC\SystemTag\SystemTagManager;
+use OC\SystemTag\SystemTagObjectMapper;
+use \OCP\SystemTag\ISystemTag;
+use \OCP\SystemTag\ISystemTagManager;
+use \OCP\SystemTag\ISystemTagObjectMapper;
+use \OCP\SystemTag\TagNotFoundException;
+use \OCP\IDBConnection;
+use \OC\SystemTag\SystemTag;
+use Test\TestCase;
+
+/**
+ * Class TestSystemTagObjectMapper
+ *
+ * @group DB
+ * @package Test\SystemTag
+ */
+class SystemTagObjectMapperTest extends TestCase {
+
+	/**
+	 * @var ISystemTagManager
+	 **/
+	private $tagManager;
+
+	/**
+	 * @var ISystemTagObjectMapper
+	 **/
+	private $tagMapper;
+
+	/**
+	 * @var IDBConnection
+	 */
+	private $connection;
+
+	/**
+	 * @var ISystemTag
+	 */
+	private $tag1;
+
+	/**
+	 * @var ISystemTag
+	 */
+	private $tag2;
+
+	/**
+	 * @var ISystemTag
+	 */
+	private $tag3;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->connection = \OC::$server->getDatabaseConnection();
+
+		$this->tagManager = $this->getMockBuilder('OCP\SystemTag\ISystemTagManager')
+			->getMock();
+
+		$this->tagMapper = new SystemTagObjectMapper($this->connection, $this->tagManager);
+
+		$this->tag1 = new SystemTag(1, 'testtag1', false, false);
+		$this->tag2 = new SystemTag(2, 'testtag2', true, false);
+		$this->tag3 = new SystemTag(3, 'testtag3', false, false);
+
+		$this->tagManager->expects($this->any())
+			->method('getTagsById')
+			->will($this->returnCallback(function($tagIds) {
+				$result = [];
+				if (in_array(1, $tagIds)) {
+					$result[1] = $this->tag1;
+				}
+				if (in_array(2, $tagIds)) {
+					$result[2] = $this->tag2;
+				}
+				if (in_array(3, $tagIds)) {
+					$result[3] = $this->tag3;
+				}
+				return $result;
+			}));
+
+		$this->tagMapper->assignTags(1, 'testtype', $this->tag1->getId());
+		$this->tagMapper->assignTags(1, 'testtype', $this->tag2->getId());
+		$this->tagMapper->assignTags(2, 'testtype', $this->tag1->getId());
+		$this->tagMapper->assignTags(3, 'anothertype', $this->tag1->getId());
+	} 
+
+	public function tearDown() {
+		$query = $this->connection->getQueryBuilder();
+		$query->delete(SystemTagObjectMapper::RELATION_TABLE)->execute();
+		$query->delete(SystemTagManager::TAG_TABLE)->execute();
+	}
+
+	public function testGetTagsForObjects() {
+		$tagIdMapping = $this->tagMapper->getTagIdsForObjects(
+			[1, 2, 3, 4],
+			'testtype'
+		);
+
+		$this->assertEquals([
+			1 => [$this->tag1->getId(), $this->tag2->getId()],
+			2 => [$this->tag1->getId()],
+			3 => [],
+			4 => [],
+		], $tagIdMapping);
+	}
+
+	public function testGetObjectsForTags() {
+		$objectIds = $this->tagMapper->getObjectIdsForTags(
+			[$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()],
+			'testtype'
+		);
+
+		$this->assertEquals([
+			1,
+			2,
+		], $objectIds);
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testGetObjectsForNonExistingTag() {
+		$this->tagMapper->getObjectIdsForTags(
+			[100],
+			'testtype'
+		);
+	}
+
+	public function testAssignUnassignTags() {
+		$this->tagMapper->unassignTags(1, 'testtype', [$this->tag1->getId()]);
+
+		$tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype');
+		$this->assertEquals([
+			1 => [$this->tag2->getId()],
+		], $tagIdMapping);
+
+		$this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]);
+		$this->tagMapper->assignTags(1, 'testtype', $this->tag3->getId());
+
+		$tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype');
+
+		$this->assertEquals([
+			1 => [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()],
+		], $tagIdMapping);
+	}
+
+	public function testReAssignUnassignTags() {
+		// reassign tag1
+		$this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]);
+
+		// tag 3 was never assigned
+		$this->tagMapper->unassignTags(1, 'testtype', [$this->tag3->getId()]);
+
+		$this->assertTrue(true, 'No error when reassigning/unassigning');
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testAssignNonExistingTags() {
+		$this->tagMapper->assignTags(1, 'testtype', [100]);
+	}
+
+	public function testAssignNonExistingTagInArray() {
+		$caught = false;
+		try {
+			$this->tagMapper->assignTags(1, 'testtype', [100, $this->tag3->getId()]);
+		} catch (TagNotFoundException $e) {
+			$caught = true;
+		}
+
+		$this->assertTrue($caught, 'Exception thrown');
+
+		$tagIdMapping = $this->tagMapper->getTagIdsForObjects(
+			[1],
+			'testtype'
+		);
+
+		$this->assertEquals([
+			1 => [$this->tag1->getId(), $this->tag2->getId()],
+		], $tagIdMapping, 'None of the tags got assigned');
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testUnassignNonExistingTags() {
+		$this->tagMapper->unassignTags(1, 'testtype', [100]);
+	}
+
+	public function testUnassignNonExistingTagsInArray() {
+		$caught = false;
+		try {
+			$this->tagMapper->unassignTags(1, 'testtype', [100, $this->tag1->getId()]);
+		} catch (TagNotFoundException $e) {
+			$caught = true;
+		}
+
+		$this->assertTrue($caught, 'Exception thrown');
+
+		$tagIdMapping = $this->tagMapper->getTagIdsForObjects(
+			[1],
+			'testtype'
+		);
+
+		$this->assertEquals([
+			1 => [$this->tag1->getId(), $this->tag2->getId()],
+		], $tagIdMapping, 'None of the tags got unassigned');
+	}
+
+	public function testHaveTagAllMatches() {
+		$this->assertTrue(
+			$this->tagMapper->haveTag(
+				[1],
+				'testtype',
+				$this->tag1->getId(),
+				true
+			),
+			'object 1 has the tag tag1'
+		);
+
+		$this->assertTrue(
+			$this->tagMapper->haveTag(
+				[1, 2],
+				'testtype',
+				$this->tag1->getId(),
+				true
+			),
+			'object 1 and object 2 ALL have the tag tag1'
+		);
+
+		$this->assertFalse(
+			$this->tagMapper->haveTag(
+				[1, 2],
+				'testtype',
+				$this->tag2->getId(),
+				true
+			),
+			'object 1 has tag2 but not object 2, so not ALL of them'
+		);
+
+		$this->assertFalse(
+			$this->tagMapper->haveTag(
+				[2],
+				'testtype',
+				$this->tag2->getId(),
+				true
+			),
+			'object 2 does not have tag2'
+		);
+
+		$this->assertFalse(
+			$this->tagMapper->haveTag(
+				[3],
+				'testtype',
+				$this->tag2->getId(),
+				true
+			),
+			'object 3 does not have tag1 due to different type'
+		);
+	}
+
+	public function testHaveTagAtLeastOneMatch() {
+		$this->assertTrue(
+			$this->tagMapper->haveTag(
+				[1],
+				'testtype',
+				$this->tag1->getId(),
+				false
+			),
+			'object1 has the tag tag1'
+		);
+
+		$this->assertTrue(
+			$this->tagMapper->haveTag(
+				[1, 2],
+				'testtype',
+				$this->tag1->getId(),
+				false
+			),
+			'object 1  and object 2 both the tag tag1'
+		);
+
+		$this->assertTrue(
+			$this->tagMapper->haveTag(
+				[1, 2],
+				'testtype',
+				$this->tag2->getId(),
+				false
+			),
+			'at least object 1 has the tag tag2'
+		);
+
+		$this->assertFalse(
+			$this->tagMapper->haveTag(
+				[2],
+				'testtype',
+				$this->tag2->getId(),
+				false
+			),
+			'object 2 does not have tag2'
+		);
+
+		$this->assertFalse(
+			$this->tagMapper->haveTag(
+				[3],
+				'testtype',
+				$this->tag2->getId(),
+				false
+			),
+			'object 3 does not have tag1 due to different type'
+		);
+	}
+
+	/**
+	 * @expectedException \OCP\SystemTag\TagNotFoundException
+	 */
+	public function testHaveTagNonExisting() {
+		$this->tagMapper->haveTag(
+			[1],
+			'testtype',
+			100
+		);
+	}
+}