diff --git a/apps/dav/lib/connector/sabre/serverfactory.php b/apps/dav/lib/connector/sabre/serverfactory.php index 6ccec0bc0e..080f889ae7 100644 --- a/apps/dav/lib/connector/sabre/serverfactory.php +++ b/apps/dav/lib/connector/sabre/serverfactory.php @@ -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, diff --git a/apps/dav/lib/connector/sabre/sharesplugin.php b/apps/dav/lib/connector/sabre/sharesplugin.php new file mode 100644 index 0000000000..f75c137871 --- /dev/null +++ b/apps/dav/lib/connector/sabre/sharesplugin.php @@ -0,0 +1,176 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Connector\Sabre; + +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); + }); + } +} diff --git a/apps/dav/lib/connector/sabre/sharetypelist.php b/apps/dav/lib/connector/sabre/sharetypelist.php new file mode 100644 index 0000000000..763586412a --- /dev/null +++ b/apps/dav/lib/connector/sabre/sharetypelist.php @@ -0,0 +1,87 @@ + + * + * @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 + * + */ + +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); + } + } +} diff --git a/apps/dav/tests/unit/connector/sabre/sharesplugin.php b/apps/dav/tests/unit/connector/sabre/sharesplugin.php new file mode 100644 index 0000000000..9a1c6eec50 --- /dev/null +++ b/apps/dav/tests/unit/connector/sabre/sharesplugin.php @@ -0,0 +1,257 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Tests\Unit\Connector\Sabre; + +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]], + ]; + } +} diff --git a/apps/files/appinfo/application.php b/apps/files/appinfo/application.php index bc6dc9fb9e..593e0533c8 100644 --- a/apps/files/appinfo/application.php +++ b/apps/files/appinfo/application.php @@ -40,8 +40,10 @@ class Application extends App { return new ApiController( $c->query('AppName'), $c->query('Request'), + $server->getUserSession(), $c->query('TagService'), - $server->getPreviewManager() + $server->getPreviewManager(), + $server->getShareManager() ); }); diff --git a/apps/files/controller/apicontroller.php b/apps/files/controller/apicontroller.php index c96d92a046..43abf2ff45 100644 --- a/apps/files/controller/apicontroller.php +++ b/apps/files/controller/apicontroller.php @@ -34,6 +34,10 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataDisplayResponse; use OCA\Files\Service\TagService; use OCP\IPreview; +use OCP\Share\IManager; +use OCP\Files\FileInfo; +use OCP\Files\Node; +use OCP\IUserSession; /** * Class ApiController @@ -43,8 +47,12 @@ use OCP\IPreview; class ApiController extends Controller { /** @var TagService */ private $tagService; + /** @var IManager **/ + private $shareManager; /** @var IPreview */ private $previewManager; + /** IUserSession */ + private $userSession; /** * @param string $appName @@ -54,11 +62,15 @@ class ApiController extends Controller { */ public function __construct($appName, IRequest $request, + IUserSession $userSession, TagService $tagService, - IPreview $previewManager){ + IPreview $previewManager, + IManager $shareManager) { parent::__construct($appName, $request); + $this->userSession = $userSession; $this->tagService = $tagService; $this->previewManager = $previewManager; + $this->shareManager = $shareManager; } /** @@ -132,8 +144,10 @@ class ApiController extends Controller { */ public function getFilesByTag($tagName) { $files = array(); - $fileInfos = $this->tagService->getFilesByTag($tagName); - foreach ($fileInfos as &$fileInfo) { + $nodes = $this->tagService->getFilesByTag($tagName); + foreach ($nodes as &$node) { + $shareTypes = $this->getShareTypes($node); + $fileInfo = $node->getFileInfo(); $file = \OCA\Files\Helper::formatFileInfo($fileInfo); $parts = explode('/', dirname($fileInfo->getPath()), 4); if(isset($parts[3])) { @@ -142,9 +156,43 @@ class ApiController extends Controller { $file['path'] = '/'; } $file['tags'] = [$tagName]; + if (!empty($shareTypes)) { + $file['shareTypes'] = $shareTypes; + } $files[] = $file; } return new DataResponse(['files' => $files]); } + /** + * Return a list of share types for outgoing shares + * + * @param Node $node file node + * + * @return int[] array of share types + */ + private function getShareTypes(Node $node) { + $userId = $this->userSession->getUser()->getUID(); + $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( + $userId, + $requestedShareType, + $node, + false, + 1 + ); + if (!empty($shares)) { + $shareTypes[] = $requestedShareType; + } + } + return $shareTypes; + } + } diff --git a/apps/files/service/tagservice.php b/apps/files/service/tagservice.php index e1425c4661..57cad43a53 100644 --- a/apps/files/service/tagservice.php +++ b/apps/files/service/tagservice.php @@ -25,6 +25,7 @@ namespace OCA\Files\Service; use OC\Files\FileInfo; +use OCP\Files\Node; /** * Service class to manage tags on files. @@ -93,7 +94,7 @@ class TagService { * Get all files for the given tag * * @param string $tagName tag name to filter by - * @return FileInfo[] list of matching files + * @return Node[] list of matching files * @throws \Exception if the tag does not exist */ public function getFilesByTag($tagName) { @@ -103,15 +104,11 @@ class TagService { return []; } - $fileInfos = []; + $allNodes = []; foreach ($fileIds as $fileId) { - $nodes = $this->homeFolder->getById((int) $fileId); - foreach ($nodes as $node) { - /** @var \OC\Files\Node\Node $node */ - $fileInfos[] = $node->getFileInfo(); - } + $allNodes = array_merge($allNodes, $this->homeFolder->getById((int) $fileId)); } - return $fileInfos; + return $allNodes; } } diff --git a/apps/files/tests/controller/apicontrollertest.php b/apps/files/tests/controller/apicontrollertest.php index 6fb8340ead..a9b248a36f 100644 --- a/apps/files/tests/controller/apicontrollertest.php +++ b/apps/files/tests/controller/apicontrollertest.php @@ -51,14 +51,27 @@ class ApiControllerTest extends TestCase { private $preview; /** @var ApiController */ private $apiController; + /** @var \OCP\Share\IManager */ + private $shareManager; public function setUp() { $this->request = $this->getMockBuilder('\OCP\IRequest') ->disableOriginalConstructor() ->getMock(); + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('user1')); + $userSession = $this->getMock('\OCP\IUserSession'); + $userSession->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($user)); $this->tagService = $this->getMockBuilder('\OCA\Files\Service\TagService') ->disableOriginalConstructor() ->getMock(); + $this->shareManager = $this->getMockBuilder('\OCP\Share\IManager') + ->disableOriginalConstructor() + ->getMock(); $this->preview = $this->getMockBuilder('\OCP\IPreview') ->disableOriginalConstructor() ->getMock(); @@ -66,8 +79,10 @@ class ApiControllerTest extends TestCase { $this->apiController = new ApiController( $this->appName, $this->request, + $userSession, $this->tagService, - $this->preview + $this->preview, + $this->shareManager ); } @@ -101,10 +116,32 @@ class ApiControllerTest extends TestCase { ->disableOriginalConstructor() ->getMock() ); + $node = $this->getMockBuilder('\OC\Files\Node\File') + ->disableOriginalConstructor() + ->getMock(); + $node->expects($this->once()) + ->method('getFileInfo') + ->will($this->returnValue($fileInfo)); $this->tagService->expects($this->once()) ->method('getFilesByTag') ->with($this->equalTo([$tagName])) - ->will($this->returnValue([$fileInfo])); + ->will($this->returnValue([$node])); + + $this->shareManager->expects($this->any()) + ->method('getSharesBy') + ->with( + $this->equalTo('user1'), + $this->anything(), + $node, + $this->equalTo(false), + $this->equalTo(1) + ) + ->will($this->returnCallback(function($userId, $shareType) { + if ($shareType === \OCP\Share::SHARE_TYPE_USER || $shareType === \OCP\Share::SHARE_TYPE_LINK) { + return ['dummy_share']; + } + return []; + })); $expected = new DataResponse([ 'files' => [ @@ -124,6 +161,7 @@ class ApiControllerTest extends TestCase { 'MyTagName' ] ], + 'shareTypes' => [\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK] ], ], ]); @@ -166,10 +204,22 @@ class ApiControllerTest extends TestCase { ->disableOriginalConstructor() ->getMock() ); + $node1 = $this->getMockBuilder('\OC\Files\Node\File') + ->disableOriginalConstructor() + ->getMock(); + $node1->expects($this->once()) + ->method('getFileInfo') + ->will($this->returnValue($fileInfo1)); + $node2 = $this->getMockBuilder('\OC\Files\Node\File') + ->disableOriginalConstructor() + ->getMock(); + $node2->expects($this->once()) + ->method('getFileInfo') + ->will($this->returnValue($fileInfo2)); $this->tagService->expects($this->once()) ->method('getFilesByTag') ->with($this->equalTo([$tagName])) - ->will($this->returnValue([$fileInfo1, $fileInfo2])); + ->will($this->returnValue([$node1, $node2])); $expected = new DataResponse([ 'files' => [ diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js index 2711b2392e..5ec7824758 100644 --- a/apps/files_sharing/js/share.js +++ b/apps/files_sharing/js/share.js @@ -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; }); diff --git a/apps/files_sharing/js/sharedfilelist.js b/apps/files_sharing/js/sharedfilelist.js index a799d4a94c..da0f957ed9 100644 --- a/apps/files_sharing/js/sharedfilelist.js +++ b/apps/files_sharing/js/sharedfilelist.js @@ -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(); diff --git a/apps/files_sharing/tests/js/shareSpec.js b/apps/files_sharing/tests/js/shareSpec.js index 7607ada50b..c488bd94fa 100644 --- a/apps/files_sharing/tests/js/shareSpec.js +++ b/apps/files_sharing/tests/js/shareSpec.js @@ -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'); diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index 95a05f4ed6..b56a1b7d2f 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -190,7 +190,7 @@ trait WebDav { */ public function theSingleResponseShouldContainAPropertyWithValue($key, $expectedValue) { $keys = $this->response; - if (!isset($keys[$key])) { + if (!array_key_exists($key, $keys)) { throw new \Exception("Cannot find property \"$key\" with \"$expectedValue\""); } @@ -200,6 +200,57 @@ trait WebDav { } } + /** + * @Then the response should contain a share-types property with + */ + public function theResponseShouldContainAShareTypesPropertyWith($table) + { + $keys = $this->response; + if (!array_key_exists('{http://owncloud.org/ns}share-types', $keys)) { + throw new \Exception("Cannot find property \"{http://owncloud.org/ns}share-types\""); + } + + $foundTypes = []; + $data = $keys['{http://owncloud.org/ns}share-types']; + foreach ($data as $item) { + if ($item['name'] !== '{http://owncloud.org/ns}share-type') { + throw new \Exception('Invalid property found: "' . $item['name'] . '"'); + } + + $foundTypes[] = $item['value']; + } + + foreach ($table->getRows() as $row) { + $key = array_search($row[0], $foundTypes); + if ($key === false) { + throw new \Exception('Expected type ' . $row[0] . ' not found'); + } + + unset($foundTypes[$key]); + } + + if ($foundTypes !== []) { + throw new \Exception('Found more share types then specified: ' . $foundTypes); + } + } + + /** + * @Then the response should contain an empty property :property + * @param string $property + * @throws \Exception + */ + public function theResponseShouldContainAnEmptyProperty($property) { + $properties = $this->response; + if (!array_key_exists($property, $properties)) { + throw new \Exception("Cannot find property \"$property\""); + } + + if ($properties[$property] !== null) { + throw new \Exception("Property \"$property\" is not empty"); + } + } + + /*Returns the elements of a propfind, $folderDepth requires 1 to see elements without children*/ public function listFolder($user, $path, $folderDepth, $properties = null){ $fullUrl = substr($this->baseUrl, 0, -4); diff --git a/build/integration/features/webdav-related.feature b/build/integration/features/webdav-related.feature index 019df3436f..ee841f9eb5 100644 --- a/build/integration/features/webdav-related.feature +++ b/build/integration/features/webdav-related.feature @@ -168,3 +168,76 @@ Feature: webdav-related When As an "user0" And Downloading file "/myChunkedFile.txt" Then Downloaded content should be "AAAAABBBBBCCCCC" + + Scenario: A file that is not shared does not have a share-types property + Given user "user0" exists + And user "user0" created a folder "/test" + When as "user0" gets properties of folder "/test" with + |{http://owncloud.org/ns}share-types| + Then the response should contain an empty property "{http://owncloud.org/ns}share-types" + + Scenario: A file that is shared to a user has a share-types property + Given user "user0" exists + And user "user1" exists + And user "user0" created a folder "/test" + And as "user0" creating a share with + | path | test | + | shareType | 0 | + | permissions | 31 | + | shareWith | user1 | + When as "user0" gets properties of folder "/test" with + |{http://owncloud.org/ns}share-types| + Then the response should contain a share-types property with + | 0 | + + Scenario: A file that is shared to a group has a share-types property + Given user "user0" exists + And group "group1" exists + And user "user0" created a folder "/test" + And as "user0" creating a share with + | path | test | + | shareType | 1 | + | permissions | 31 | + | shareWith | group1 | + When as "user0" gets properties of folder "/test" with + |{http://owncloud.org/ns}share-types| + Then the response should contain a share-types property with + | 1 | + + Scenario: A file that is shared by link has a share-types property + Given user "user0" exists + And user "user0" created a folder "/test" + And as "user0" creating a share with + | path | test | + | shareType | 3 | + | permissions | 31 | + When as "user0" gets properties of folder "/test" with + |{http://owncloud.org/ns}share-types| + Then the response should contain a share-types property with + | 3 | + + Scenario: A file that is shared by user,group and link has a share-types property + Given user "user0" exists + And user "user1" exists + And group "group2" exists + And user "user0" created a folder "/test" + And as "user0" creating a share with + | path | test | + | shareType | 0 | + | permissions | 31 | + | shareWith | user1 | + And as "user0" creating a share with + | path | test | + | shareType | 1 | + | permissions | 31 | + | shareWith | group2 | + And as "user0" creating a share with + | path | test | + | shareType | 3 | + | permissions | 31 | + When as "user0" gets properties of folder "/test" with + |{http://owncloud.org/ns}share-types| + Then the response should contain a share-types property with + | 0 | + | 1 | + | 3 |