* * 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 . * */ use OCP\EventDispatcher\IEventDispatcher; use OCP\ITagManager; use OCP\ITags; use OCP\IUserSession; use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; class TagsPlugin extends \Sabre\DAV\ServerPlugin { // namespace public const NS_OWNCLOUD = 'http://owncloud.org/ns'; public const TAGS_PROPERTYNAME = '{http://owncloud.org/ns}tags'; public const FAVORITE_PROPERTYNAME = '{http://owncloud.org/ns}favorite'; public const TAG_FAVORITE = '_$!!$_'; /** * Reference to main server object * * @var \Sabre\DAV\Server */ private $server; /** * @var ITags */ private $tagger; /** * Array of file id to tags array * The null value means the cache wasn't initialized. * * @var array */ private $cachedTags; private array $cachedDirectories; /** * @param \Sabre\DAV\Tree $tree tree * @param ITagManager $tagManager tag manager */ public function __construct( private \Sabre\DAV\Tree $tree, private ITagManager $tagManager, private IEventDispatcher $eventDispatcher, private IUserSession $userSession, ) { $this->tagger = null; $this->cachedTags = []; } /** * 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->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class; $this->server = $server; $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); $this->server->on('propPatch', [$this, 'handleUpdateProperties']); $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']); } /** * Returns the tagger * * @return 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, true); if ($favPos !== false) { $isFav = true; unset($tags[$favPos]); } } return [$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([$fileId]); if ($tags !== false) { if (empty($tags)) { return []; } return current($tags); } } return null; } /** * Prefetches tags for a list of file IDs and caches the results * * @param array $fileIds List of file IDs to prefetch tags for * @return void */ private function prefetchTagsForFileIds(array $fileIds) { $tags = $this->getTagger()->getTagsForObjects($fileIds); if ($tags === false) { // the tags API returns false on error... $tags = []; } foreach ($fileIds as $fileId) { $this->cachedTags[$fileId] = $tags[$fileId] ?? []; } } /** * Updates the tags of the given file id * * @param int $fileId * @param array $tags array of tag strings * @param string $path path of the file */ private function updateTags($fileId, $tags, string $path) { $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, $path); } $deletedTags = array_diff($currentTags, $tags); foreach ($deletedTags as $tag) { if ($tag === self::TAG_FAVORITE) { continue; } $tagger->unTag($fileId, $tag, $path); } } private function preloadCollection(PropFind $propFind, ICollection $collection): void { if (!($collection instanceof Node)) { return; } // need prefetch ? if ($collection instanceof Directory && !isset($this->cachedDirectories[$collection->getPath()]) && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) )) { // note: pre-fetching only supported for depth <= 1 $folderContent = $collection->getChildren(); $fileIds = [(int)$collection->getId()]; foreach ($folderContent as $info) { $fileIds[] = (int)$info->getId(); } $this->prefetchTagsForFileIds($fileIds); $this->cachedDirectories[$collection->getPath()] = true; } } /** * Adds tags and favorites properties to the response, * if requested. * * @param PropFind $propFind * @param \Sabre\DAV\INode $node * @return void */ public function handleGetProperties( PropFind $propFind, \Sabre\DAV\INode $node, ) { if (!($node instanceof Node)) { return; } $isFav = null; $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) { [$tags, $isFav] = $this->getTagsAndFav($node->getId()); return new TagList($tags); }); $propFind->handle(self::FAVORITE_PROPERTYNAME, function () use ($isFav, $node) { if (is_null($isFav)) { [, $isFav] = $this->getTagsAndFav($node->getId()); } if ($isFav) { return 1; } else { return 0; } }); } /** * Updates tags and favorites properties, if applicable. * * @param string $path * @param PropPatch $propPatch * * @return void */ public function handleUpdateProperties($path, PropPatch $propPatch) { $node = $this->tree->getNodeForPath($path); if (!($node instanceof Node)) { return; } $propPatch->handle(self::TAGS_PROPERTYNAME, function ($tagList) use ($node, $path) { $this->updateTags($node->getId(), $tagList->getTags(), $path); return true; }); $propPatch->handle(self::FAVORITE_PROPERTYNAME, function ($favState) use ($node, $path) { if ((int)$favState === 1 || $favState === 'true') { $this->getTagger()->tagAs($node->getId(), self::TAG_FAVORITE, $path); } else { $this->getTagger()->unTag($node->getId(), self::TAG_FAVORITE, $path); } if (is_null($favState)) { // confirm deletion return 204; } return 200; }); } public function handlePreloadProperties(array $nodes, array $requestProperties): void { if ( !in_array(self::FAVORITE_PROPERTYNAME, $requestProperties, true) && !in_array(self::TAGS_PROPERTYNAME, $requestProperties, true) ) { return; } $this->prefetchTagsForFileIds(array_map(fn ($node) => $node->getId(), $nodes)); } }