You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TagsPlugin.php 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
  5. * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
  6. *
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Roeland Jago Douma <roeland@famdouma.nl>
  10. * @author Sergio Bertolín <sbertolin@solidgear.es>
  11. * @author Thomas Citharel <nextcloud@tcit.fr>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. * @author Vincent Petry <vincent@nextcloud.com>
  14. *
  15. * @license AGPL-3.0
  16. *
  17. * This code is free software: you can redistribute it and/or modify
  18. * it under the terms of the GNU Affero General Public License, version 3,
  19. * as published by the Free Software Foundation.
  20. *
  21. * This program is distributed in the hope that it will be useful,
  22. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. * GNU Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public License, version 3,
  27. * along with this program. If not, see <http://www.gnu.org/licenses/>
  28. *
  29. */
  30. namespace OCA\DAV\Connector\Sabre;
  31. /**
  32. * ownCloud
  33. *
  34. * @author Vincent Petry
  35. * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
  36. *
  37. * This library is free software; you can redistribute it and/or
  38. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  39. * License as published by the Free Software Foundation; either
  40. * version 3 of the License, or any later version.
  41. *
  42. * This library is distributed in the hope that it will be useful,
  43. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  44. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  45. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  46. *
  47. * You should have received a copy of the GNU Affero General Public
  48. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  49. *
  50. */
  51. use Sabre\DAV\PropFind;
  52. use Sabre\DAV\PropPatch;
  53. class TagsPlugin extends \Sabre\DAV\ServerPlugin {
  54. // namespace
  55. public const NS_OWNCLOUD = 'http://owncloud.org/ns';
  56. public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags';
  57. public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite';
  58. public const TAG_FAVORITE = '_$!<Favorite>!$_';
  59. /**
  60. * Reference to main server object
  61. *
  62. * @var \Sabre\DAV\Server
  63. */
  64. private $server;
  65. /**
  66. * @var \OCP\ITagManager
  67. */
  68. private $tagManager;
  69. /**
  70. * @var \OCP\ITags
  71. */
  72. private $tagger;
  73. /**
  74. * Array of file id to tags array
  75. * The null value means the cache wasn't initialized.
  76. *
  77. * @var array
  78. */
  79. private $cachedTags;
  80. /**
  81. * @var \Sabre\DAV\Tree
  82. */
  83. private $tree;
  84. /**
  85. * @param \Sabre\DAV\Tree $tree tree
  86. * @param \OCP\ITagManager $tagManager tag manager
  87. */
  88. public function __construct(\Sabre\DAV\Tree $tree, \OCP\ITagManager $tagManager) {
  89. $this->tree = $tree;
  90. $this->tagManager = $tagManager;
  91. $this->tagger = null;
  92. $this->cachedTags = [];
  93. }
  94. /**
  95. * This initializes the plugin.
  96. *
  97. * This function is called by \Sabre\DAV\Server, after
  98. * addPlugin is called.
  99. *
  100. * This method should set up the required event subscriptions.
  101. *
  102. * @param \Sabre\DAV\Server $server
  103. * @return void
  104. */
  105. public function initialize(\Sabre\DAV\Server $server) {
  106. $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
  107. $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class;
  108. $this->server = $server;
  109. $this->server->on('propFind', [$this, 'handleGetProperties']);
  110. $this->server->on('propPatch', [$this, 'handleUpdateProperties']);
  111. }
  112. /**
  113. * Returns the tagger
  114. *
  115. * @return \OCP\ITags tagger
  116. */
  117. private function getTagger() {
  118. if (!$this->tagger) {
  119. $this->tagger = $this->tagManager->load('files');
  120. }
  121. return $this->tagger;
  122. }
  123. /**
  124. * Returns tags and favorites.
  125. *
  126. * @param integer $fileId file id
  127. * @return array list($tags, $favorite) with $tags as tag array
  128. * and $favorite is a boolean whether the file was favorited
  129. */
  130. private function getTagsAndFav($fileId) {
  131. $isFav = false;
  132. $tags = $this->getTags($fileId);
  133. if ($tags) {
  134. $favPos = array_search(self::TAG_FAVORITE, $tags);
  135. if ($favPos !== false) {
  136. $isFav = true;
  137. unset($tags[$favPos]);
  138. }
  139. }
  140. return [$tags, $isFav];
  141. }
  142. /**
  143. * Returns tags for the given file id
  144. *
  145. * @param integer $fileId file id
  146. * @return array list of tags for that file
  147. */
  148. private function getTags($fileId) {
  149. if (isset($this->cachedTags[$fileId])) {
  150. return $this->cachedTags[$fileId];
  151. } else {
  152. $tags = $this->getTagger()->getTagsForObjects([$fileId]);
  153. if ($tags !== false) {
  154. if (empty($tags)) {
  155. return [];
  156. }
  157. return current($tags);
  158. }
  159. }
  160. return null;
  161. }
  162. /**
  163. * Updates the tags of the given file id
  164. *
  165. * @param int $fileId
  166. * @param array $tags array of tag strings
  167. */
  168. private function updateTags($fileId, $tags) {
  169. $tagger = $this->getTagger();
  170. $currentTags = $this->getTags($fileId);
  171. $newTags = array_diff($tags, $currentTags);
  172. foreach ($newTags as $tag) {
  173. if ($tag === self::TAG_FAVORITE) {
  174. continue;
  175. }
  176. $tagger->tagAs($fileId, $tag);
  177. }
  178. $deletedTags = array_diff($currentTags, $tags);
  179. foreach ($deletedTags as $tag) {
  180. if ($tag === self::TAG_FAVORITE) {
  181. continue;
  182. }
  183. $tagger->unTag($fileId, $tag);
  184. }
  185. }
  186. /**
  187. * Adds tags and favorites properties to the response,
  188. * if requested.
  189. *
  190. * @param PropFind $propFind
  191. * @param \Sabre\DAV\INode $node
  192. * @return void
  193. */
  194. public function handleGetProperties(
  195. PropFind $propFind,
  196. \Sabre\DAV\INode $node
  197. ) {
  198. if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
  199. return;
  200. }
  201. // need prefetch ?
  202. if ($node instanceof \OCA\DAV\Connector\Sabre\Directory
  203. && $propFind->getDepth() !== 0
  204. && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME))
  205. || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME))
  206. )) {
  207. // note: pre-fetching only supported for depth <= 1
  208. $folderContent = $node->getChildren();
  209. $fileIds[] = (int)$node->getId();
  210. foreach ($folderContent as $info) {
  211. $fileIds[] = (int)$info->getId();
  212. }
  213. $tags = $this->getTagger()->getTagsForObjects($fileIds);
  214. if ($tags === false) {
  215. // the tags API returns false on error...
  216. $tags = [];
  217. }
  218. $this->cachedTags = $this->cachedTags + $tags;
  219. $emptyFileIds = array_diff($fileIds, array_keys($tags));
  220. // also cache the ones that were not found
  221. foreach ($emptyFileIds as $fileId) {
  222. $this->cachedTags[$fileId] = [];
  223. }
  224. }
  225. $isFav = null;
  226. $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) {
  227. [$tags, $isFav] = $this->getTagsAndFav($node->getId());
  228. return new TagList($tags);
  229. });
  230. $propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) {
  231. if (is_null($isFav)) {
  232. [, $isFav] = $this->getTagsAndFav($node->getId());
  233. }
  234. if ($isFav) {
  235. return 1;
  236. } else {
  237. return 0;
  238. }
  239. });
  240. }
  241. /**
  242. * Updates tags and favorites properties, if applicable.
  243. *
  244. * @param string $path
  245. * @param PropPatch $propPatch
  246. *
  247. * @return void
  248. */
  249. public function handleUpdateProperties($path, PropPatch $propPatch) {
  250. $node = $this->tree->getNodeForPath($path);
  251. if (!($node instanceof \OCA\DAV\Connector\Sabre\Node)) {
  252. return;
  253. }
  254. $propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node) {
  255. $this->updateTags($node->getId(), $tagList->getTags());
  256. return true;
  257. });
  258. $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node) {
  259. if ((int)$favState === 1 || $favState === 'true') {
  260. $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE);
  261. } else {
  262. $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE);
  263. }
  264. if (is_null($favState)) {
  265. // confirm deletion
  266. return 204;
  267. }
  268. return 200;
  269. });
  270. }
  271. }