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.6KB

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