Add webdav property for share info in PROPFIND response

This commit is contained in:
Vincent Petry 2016-03-02 18:25:31 +01:00
parent 382b18e85e
commit fb705fa305
7 changed files with 608 additions and 45 deletions

View File

@ -137,6 +137,12 @@ class ServerFactory {
if($this->userSession->isLoggedIn()) {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(
$objectTree,
$this->userSession,
$userFolder,
\OC::$server->getShareManager()
));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin(\OC::$server->getCommentsManager(), $this->userSession));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesReportPlugin(
$objectTree,

View File

@ -0,0 +1,197 @@
<?php
/**
* @author Vincent Petry <pvince81@owncloud.com>
*
* @copyright Copyright (c) 2016, 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 OCA\DAV\Connector\Sabre;
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2016 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/>.
*
*/
use \Sabre\DAV\PropFind;
use \Sabre\DAV\PropPatch;
use OCP\IUserSession;
use OCP\Share\IShare;
use OCA\DAV\Connector\Sabre\ShareTypeList;
/**
* Sabre Plugin to provide share-related properties
*/
class SharesPlugin extends \Sabre\DAV\ServerPlugin {
const NS_OWNCLOUD = 'http://owncloud.org/ns';
const SHARETYPES_PROPERTYNAME = '{http://owncloud.org/ns}share-types';
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @var \OCP\Share\IManager
*/
private $shareManager;
/**
* @var \Sabre\DAV\Tree
*/
private $tree;
/**
* @var string
*/
private $userId;
/**
* @var \OCP\Files\Folder
*/
private $userFolder;
/**
* @var IShare[]
*/
private $cachedShareTypes;
/**
* @param \Sabre\DAV\Tree $tree tree
* @param IUserSession $userSession user session
* @param \OCP\Files\Folder $userFolder user home folder
* @param \OCP\Share\IManager $shareManager share manager
*/
public function __construct(
\Sabre\DAV\Tree $tree,
IUserSession $userSession,
\OCP\Files\Folder $userFolder,
\OCP\Share\IManager $shareManager
) {
$this->tree = $tree;
$this->shareManager = $shareManager;
$this->userFolder = $userFolder;
$this->userId = $userSession->getUser()->getUID();
$this->cachedShareTypes = [];
}
/**
* 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
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->xml->namespacesMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->elementMap[self::SHARETYPES_PROPERTYNAME] = 'OCA\\DAV\\Connector\\Sabre\\ShareTypeList';
$server->protectedProperties[] = self::SHARETYPES_PROPERTYNAME;
$this->server = $server;
$this->server->on('propFind', array($this, 'handleGetProperties'));
}
/**
* Return a list of share types for outgoing shares
*
* @param \OCP\Files\Node $node file node
*
* @return int[] array of share types
*/
private function getShareTypes(\OCP\Files\Node $node) {
$shareTypes = [];
$requestedShareTypes = [
\OCP\Share::SHARE_TYPE_USER,
\OCP\Share::SHARE_TYPE_GROUP,
\OCP\Share::SHARE_TYPE_LINK
];
foreach ($requestedShareTypes as $requestedShareType) {
// one of each type is enough to find out about the types
$shares = $this->shareManager->getSharesBy(
$this->userId,
$requestedShareType,
$node,
false,
1
);
if (!empty($shares)) {
$shareTypes[] = $requestedShareType;
}
}
return $shareTypes;
}
/**
* Adds shares to propfind response
*
* @param PropFind $propFind propfind object
* @param \Sabre\DAV\INode $sabreNode sabre node
*/
public function handleGetProperties(
PropFind $propFind,
\Sabre\DAV\INode $sabreNode
) {
if (!($sabreNode instanceof \OCA\DAV\Connector\Sabre\Node)) {
return;
}
// need prefetch ?
if ($sabreNode instanceof \OCA\DAV\Connector\Sabre\Directory
&& $propFind->getDepth() !== 0
&& !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME))
) {
$folderNode = $this->userFolder->get($propFind->getPath());
$children = $folderNode->getDirectoryListing();
$this->cachedShareTypes[$folderNode->getId()] = $this->getShareTypes($folderNode);
foreach ($children as $childNode) {
$this->cachedShareTypes[$childNode->getId()] = $this->getShareTypes($childNode);
}
}
$propFind->handle(self::SHARETYPES_PROPERTYNAME, function() use ($sabreNode) {
if (isset($this->cachedShareTypes[$sabreNode->getId()])) {
$shareTypes = $this->cachedShareTypes[$sabreNode->getId()];
} else {
$node = $this->userFolder->get($sabreNode->getPath());
$shareTypes = $this->getShareTypes($node);
}
return new ShareTypeList($shareTypes);
});
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* @author Vincent Petry <pvince81@owncloud.com>
*
* @copyright Copyright (c) 2016, 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 OCA\DAV\Connector\Sabre;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* ShareTypeList property
*
* This property contains multiple "share-type" elements, each containing a share type.
*/
class ShareTypeList implements Element {
const NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* Share types
*
* @var int[]
*/
private $shareTypes;
/**
* @param int[] $shareTypes
*/
public function __construct($shareTypes) {
$this->shareTypes = $shareTypes;
}
/**
* Returns the share types
*
* @return int[]
*/
public function getShareTypes() {
return $this->shareTypes;
}
/**
* The deserialize method is called during xml parsing.
*
* @param Reader $reader
* @return mixed
*/
static function xmlDeserialize(Reader $reader) {
$shareTypes = [];
foreach ($reader->parseInnerTree() as $elem) {
if ($elem['name'] === '{' . self::NS_OWNCLOUD . '}share-type') {
$shareTypes[] = (int)$elem['value'];
}
}
return new self($shareTypes);
}
/**
* The xmlSerialize metod is called during xml writing.
*
* @param Writer $writer
* @return void
*/
function xmlSerialize(Writer $writer) {
foreach ($this->shareTypes as $shareType) {
$writer->writeElement('{' . self::NS_OWNCLOUD . '}share-type', $shareType);
}
}
}

View File

@ -0,0 +1,263 @@
<?php
/**
* @author Vincent Petry <pvince81@owncloud.com>
*
* @copyright Copyright (c) 2016, 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 OCA\DAV\Tests\Unit\Connector\Sabre;
/**
* Copyright (c) 2016 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 SharesPlugin extends \Test\TestCase {
const SHARETYPES_PROPERTYNAME = \OCA\DAV\Connector\Sabre\SharesPlugin::SHARETYPES_PROPERTYNAME;
/**
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @var \Sabre\DAV\Tree
*/
private $tree;
/**
* @var \OCP\Share\IManager
*/
private $shareManager;
/**
* @var \OCP\Files\Folder
*/
private $userFolder;
/**
* @var \OCA\DAV\Connector\Sabre\SharesPlugin
*/
private $plugin;
public function setUp() {
parent::setUp();
$this->server = new \Sabre\DAV\Server();
$this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
->disableOriginalConstructor()
->getMock();
$this->shareManager = $this->getMock('\OCP\Share\IManager');
$user = $this->getMock('\OCP\IUser');
$user->expects($this->once())
->method('getUID')
->will($this->returnValue('user1'));
$userSession = $this->getMock('\OCP\IUserSession');
$userSession->expects($this->once())
->method('getUser')
->will($this->returnValue($user));
$this->userFolder = $this->getMock('\OCP\Files\Folder');
$this->plugin = new \OCA\DAV\Connector\Sabre\SharesPlugin(
$this->tree,
$userSession,
$this->userFolder,
$this->shareManager
);
$this->plugin->initialize($this->server);
}
/**
* @dataProvider sharesGetPropertiesDataProvider
*/
public function testGetProperties($shareTypes) {
$sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node')
->disableOriginalConstructor()
->getMock();
$sabreNode->expects($this->any())
->method('getId')
->will($this->returnValue(123));
$sabreNode->expects($this->once())
->method('getPath')
->will($this->returnValue('/subdir'));
// node API nodes
$node = $this->getMock('\OCP\Files\Folder');
$this->userFolder->expects($this->once())
->method('get')
->with('/subdir')
->will($this->returnValue($node));
$this->shareManager->expects($this->any())
->method('getSharesBy')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->anything(),
$this->equalTo(false),
$this->equalTo(1)
)
->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){
if (in_array($requestedShareType, $shareTypes)) {
return ['dummyshare'];
}
return [];
}));
$propFind = new \Sabre\DAV\PropFind(
'/dummyPath',
[self::SHARETYPES_PROPERTYNAME],
0
);
$this->plugin->handleGetProperties(
$propFind,
$sabreNode
);
$result = $propFind->getResultForMultiStatus();
$this->assertEmpty($result[404]);
unset($result[404]);
$this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
}
/**
* @dataProvider sharesGetPropertiesDataProvider
*/
public function testPreloadThenGetProperties($shareTypes) {
$sabreNode1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
->disableOriginalConstructor()
->getMock();
$sabreNode1->expects($this->any())
->method('getId')
->will($this->returnValue(111));
$sabreNode1->expects($this->never())
->method('getPath');
$sabreNode2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')
->disableOriginalConstructor()
->getMock();
$sabreNode2->expects($this->any())
->method('getId')
->will($this->returnValue(222));
$sabreNode2->expects($this->never())
->method('getPath');
$sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory')
->disableOriginalConstructor()
->getMock();
$sabreNode->expects($this->any())
->method('getId')
->will($this->returnValue(123));
// never, because we use getDirectoryListing from the Node API instead
$sabreNode->expects($this->never())
->method('getChildren');
$sabreNode->expects($this->any())
->method('getPath')
->will($this->returnValue('/subdir'));
// node API nodes
$node = $this->getMock('\OCP\Files\Folder');
$node->expects($this->any())
->method('getId')
->will($this->returnValue(123));
$node1 = $this->getMock('\OCP\Files\File');
$node1->expects($this->any())
->method('getId')
->will($this->returnValue(111));
$node2 = $this->getMock('\OCP\Files\File');
$node2->expects($this->any())
->method('getId')
->will($this->returnValue(222));
$node->expects($this->once())
->method('getDirectoryListing')
->will($this->returnValue([$node1, $node2]));
$this->userFolder->expects($this->once())
->method('get')
->with('/subdir')
->will($this->returnValue($node));
$this->shareManager->expects($this->any())
->method('getSharesBy')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->anything(),
$this->equalTo(false),
$this->equalTo(1)
)
->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){
if ($node->getId() === 111 && in_array($requestedShareType, $shareTypes)) {
return ['dummyshare'];
}
return [];
}));
// simulate sabre recursive PROPFIND traversal
$propFindRoot = new \Sabre\DAV\PropFind(
'/subdir',
[self::SHARETYPES_PROPERTYNAME],
1
);
$propFind1 = new \Sabre\DAV\PropFind(
'/subdir/test.txt',
[self::SHARETYPES_PROPERTYNAME],
0
);
$propFind2 = new \Sabre\DAV\PropFind(
'/subdir/test2.txt',
[self::SHARETYPES_PROPERTYNAME],
0
);
$this->plugin->handleGetProperties(
$propFindRoot,
$sabreNode
);
$this->plugin->handleGetProperties(
$propFind1,
$sabreNode1
);
$this->plugin->handleGetProperties(
$propFind2,
$sabreNode2
);
$result = $propFind1->getResultForMultiStatus();
$this->assertEmpty($result[404]);
unset($result[404]);
$this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes());
}
function sharesGetPropertiesDataProvider() {
return [
[[]],
[[\OCP\Share::SHARE_TYPE_USER]],
[[\OCP\Share::SHARE_TYPE_GROUP]],
[[\OCP\Share::SHARE_TYPE_LINK]],
[[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP]],
[[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]],
[[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK]],
[[\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]],
];
}
}

View File

@ -60,6 +60,9 @@
if (fileData.recipientsDisplayName) {
tr.attr('data-share-recipients', fileData.recipientsDisplayName);
}
if (fileData.shareTypes) {
tr.attr('data-share-types', fileData.shareTypes.join(','));
}
return tr;
};
@ -77,6 +80,7 @@
fileList._getWebdavProperties = function() {
var props = oldGetWebdavProperties.apply(this, arguments);
props.push('{' + NS_OC + '}owner-display-name');
props.push('{' + NS_OC + '}share-types');
return props;
};
@ -88,40 +92,45 @@
if (permissionsProp && permissionsProp.indexOf('S') >= 0) {
data.shareOwner = props['{' + NS_OC + '}owner-display-name'];
}
var shareTypesProp = props['{' + NS_OC + '}share-types'];
if (shareTypesProp) {
data.shareTypes = _.chain(shareTypesProp).filter(function(xmlvalue) {
return (xmlvalue.namespaceURI === NS_OC && xmlvalue.nodeName.split(':')[1] === 'share-type');
}).map(function(xmlvalue) {
return parseInt(xmlvalue.textContent || xmlvalue.text, 10);
}).value();
}
return data;
});
// use delegate to catch the case with multiple file lists
fileList.$el.on('fileActionsReady', function(ev){
var fileList = ev.fileList;
var $files = ev.$files;
function updateIcons($files) {
if (!$files) {
// if none specified, update all
$files = fileList.$fileList.find('tr');
_.each($files, function(file) {
var $tr = $(file);
var shareTypes = $tr.attr('data-share-types');
if (shareTypes) {
var hasLink = false;
var hasShares = false;
_.each(shareTypes.split(',') || [], function(shareType) {
shareType = parseInt(shareType, 10);
if (shareType === OC.Share.SHARE_TYPE_LINK) {
hasLink = true;
} else if (shareType === OC.Share.SHARE_TYPE_USER) {
hasShares = true;
} else if (shareType === OC.Share.SHARE_TYPE_GROUP) {
hasShares = true;
}
});
OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink);
}
_.each($files, function(file) {
var $tr = $(file);
var shareStatus = OC.Share.statuses[$tr.data('id')];
OCA.Sharing.Util._updateFileActionIcon($tr, !!shareStatus, shareStatus && shareStatus.link);
});
}
if (!OCA.Sharing.sharesLoaded){
OC.Share.loadIcons('file', fileList, function() {
// since we don't know which files are affected, just refresh them all
updateIcons();
});
// assume that we got all shares, so switching directories
// will not invalidate that list
OCA.Sharing.sharesLoaded = true;
}
else{
updateIcons($files);
}
});
});
fileList.$el.on('changeDirectory', function() {
OCA.Sharing.sharesLoaded = false;
});

View File

@ -286,6 +286,8 @@
// using a hash to make them unique,
// this is only a list to be displayed
data.recipients = {};
// share types
data.shareTypes = {};
// counter is cheaper than calling _.keys().length
data.recipientsCount = 0;
data.mtime = file.share.stime;
@ -308,6 +310,8 @@
data.recipientsCount++;
}
data.shareTypes[file.share.type] = true;
delete file.share;
return memo;
}, {})
@ -324,6 +328,12 @@
data.recipientsCount
);
delete data.recipientsCount;
if (self._sharedWithUser) {
// only for outgoing shres
delete data.shareTypes;
} else {
data.shareTypes = _.keys(data.shareTypes);
}
})
// Finish the chain by getting the result
.value();

View File

@ -53,35 +53,21 @@ describe('OCA.Sharing.Util tests', function() {
permissions: OC.PERMISSION_ALL,
etag: 'abc',
shareOwner: 'User One',
isShareMountPoint: false
isShareMountPoint: false,
shareTypes: [OC.Share.SHARE_TYPE_USER]
}];
OCA.Sharing.sharesLoaded = true;
OC.Share.statuses = {
1: {link: false, path: '/subdir'}
};
});
afterEach(function() {
delete OCA.Sharing.sharesLoaded;
delete OC.Share.droppedDown;
fileList.destroy();
fileList = null;
OC.Share.statuses = {};
OC.Share.currentShares = {};
});
describe('Sharing data in table row', function() {
// TODO: test data-permissions, data-share-owner, etc
});
describe('Share action icon', function() {
beforeEach(function() {
OC.Share.statuses = {1: {link: false, path: '/subdir'}};
OCA.Sharing.sharesLoaded = true;
});
afterEach(function() {
OC.Share.statuses = {};
OCA.Sharing.sharesLoaded = false;
});
it('do not shows share text when not shared', function() {
var $action, $tr;
OC.Share.statuses = {};
@ -93,7 +79,8 @@ describe('OCA.Sharing.Util tests', function() {
mimetype: 'httpd/unix-directory',
size: 12,
permissions: OC.PERMISSION_ALL,
etag: 'abc'
etag: 'abc',
shareTypes: []
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
@ -111,7 +98,8 @@ describe('OCA.Sharing.Util tests', function() {
mimetype: 'text/plain',
size: 12,
permissions: OC.PERMISSION_ALL,
etag: 'abc'
etag: 'abc',
shareTypes: [OC.Share.SHARE_TYPE_USER]
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
@ -131,7 +119,8 @@ describe('OCA.Sharing.Util tests', function() {
mimetype: 'text/plain',
size: 12,
permissions: OC.PERMISSION_ALL,
etag: 'abc'
etag: 'abc',
shareTypes: [OC.Share.SHARE_TYPE_LINK]
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
@ -151,7 +140,8 @@ describe('OCA.Sharing.Util tests', function() {
size: 12,
permissions: OC.PERMISSION_ALL,
shareOwner: 'User One',
etag: 'abc'
etag: 'abc',
shareTypes: [OC.Share.SHARE_TYPE_USER]
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
@ -171,7 +161,8 @@ describe('OCA.Sharing.Util tests', function() {
size: 12,
permissions: OC.PERMISSION_ALL,
recipientsDisplayName: 'User One, User Two',
etag: 'abc'
etag: 'abc',
shareTypes: [OC.Share.SHARE_TYPE_USER]
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');