Commit 39d6ddd3 authored by Morris Jobke's avatar Morris Jobke
Browse files

Merge pull request #12865 from owncloud/files-tags-webdav

Returns tags through WebDAV
parents 028b0efd 6224e29f
......@@ -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
......
......@@ -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;
}
/**
......
<?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
*/
......
......@@ -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);
......
<?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);
}
}
<?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;
}
}
......@@ -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)
);
}
}
<?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);
}