Merge pull request #32482 from nextcloud/enh/noid/share-attributes

Add share attributes + prevent download permission
This commit is contained in:
Carl Schwan 2022-08-01 09:44:31 +02:00 committed by GitHub
commit f74e89bde5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1925 additions and 138 deletions

View file

@ -191,6 +191,7 @@ return array(
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => $baseDir . '/../lib/DAV/Sharing/Xml/Invite.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => $baseDir . '/../lib/DAV/Sharing/Xml/ShareRequest.php',
'OCA\\DAV\\DAV\\SystemPrincipalBackend' => $baseDir . '/../lib/DAV/SystemPrincipalBackend.php',
'OCA\\DAV\\DAV\\ViewOnlyPlugin' => $baseDir . '/../lib/DAV/ViewOnlyPlugin.php',
'OCA\\DAV\\Db\\Direct' => $baseDir . '/../lib/Db/Direct.php',
'OCA\\DAV\\Db\\DirectMapper' => $baseDir . '/../lib/Db/DirectMapper.php',
'OCA\\DAV\\Direct\\DirectFile' => $baseDir . '/../lib/Direct/DirectFile.php',

View file

@ -206,6 +206,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/Invite.php',
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/ShareRequest.php',
'OCA\\DAV\\DAV\\SystemPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/SystemPrincipalBackend.php',
'OCA\\DAV\\DAV\\ViewOnlyPlugin' => __DIR__ . '/..' . '/../lib/DAV/ViewOnlyPlugin.php',
'OCA\\DAV\\Db\\Direct' => __DIR__ . '/..' . '/../lib/Db/Direct.php',
'OCA\\DAV\\Db\\DirectMapper' => __DIR__ . '/..' . '/../lib/Db/DirectMapper.php',
'OCA\\DAV\\Direct\\DirectFile' => __DIR__ . '/..' . '/../lib/Direct/DirectFile.php',

View file

@ -65,6 +65,7 @@ class FilesPlugin extends ServerPlugin {
public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions';
public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions';
public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes';
public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL';
public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size';
public const GETETAG_PROPERTYNAME = '{DAV:}getetag';
@ -134,6 +135,7 @@ class FilesPlugin extends ServerPlugin {
$server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME;
$server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME;
$server->protectedProperties[] = self::SIZE_PROPERTYNAME;
$server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME;
$server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME;
@ -321,6 +323,10 @@ class FilesPlugin extends ServerPlugin {
return json_encode($ocmPermissions);
});
$propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) {
return json_encode($node->getShareAttributes());
});
$propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string {
return $node->getETag();
});

View file

@ -38,6 +38,7 @@ namespace OCA\DAV\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\Node\File;
use OC\Files\Node\Folder;
use OC\Files\Storage\Wrapper\Wrapper;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\FileInfo;
@ -322,6 +323,31 @@ abstract class Node implements \Sabre\DAV\INode {
return $permissions;
}
/**
* @return array
*/
public function getShareAttributes(): array {
$attributes = [];
try {
$storage = $this->info->getStorage();
} catch (StorageNotAvailableException $e) {
$storage = null;
}
if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$attributes = $storage->getShare()->getAttributes();
if ($attributes === null) {
return [];
} else {
return $attributes->toArray();
}
}
return $attributes;
}
/**
* @param string $user
* @return string

View file

@ -33,6 +33,7 @@ namespace OCA\DAV\Connector\Sabre;
use OCP\Files\Folder;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCP\Files\Mount\IMountManager;
use OCP\IConfig;
@ -158,6 +159,11 @@ class ServerFactory {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin());
// Allow view-only plugin for webdav requests
$server->addPlugin(new ViewOnlyPlugin(
$this->logger
));
if ($this->userSession->isLoggedIn()) {
$server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin(

View file

@ -31,8 +31,12 @@ use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IRequest;
@ -59,6 +63,8 @@ class DirectController extends OCSController {
/** @var IURLGenerator */
private $urlGenerator;
/** @var IEventDispatcher */
private $eventDispatcher;
public function __construct(string $appName,
IRequest $request,
@ -67,7 +73,8 @@ class DirectController extends OCSController {
DirectMapper $mapper,
ISecureRandom $random,
ITimeFactory $timeFactory,
IURLGenerator $urlGenerator) {
IURLGenerator $urlGenerator,
IEventDispatcher $eventDispatcher) {
parent::__construct($appName, $request);
$this->rootFolder = $rootFolder;
@ -76,6 +83,7 @@ class DirectController extends OCSController {
$this->random = $random;
$this->timeFactory = $timeFactory;
$this->urlGenerator = $urlGenerator;
$this->eventDispatcher = $eventDispatcher;
}
/**
@ -99,6 +107,13 @@ class DirectController extends OCSController {
throw new OCSBadRequestException('Direct download only works for files');
}
$event = new BeforeDirectFileDownloadEvent($userFolder->getRelativePath($file->getPath()));
$this->eventDispatcher->dispatchTyped($event);
if ($event->isSuccessful() === false) {
throw new OCSForbiddenException('Permission denied to download file');
}
//TODO: at some point we should use the directdownlaod function of storages
$direct = new Direct();
$direct->setUserId($this->userId);

View file

@ -0,0 +1,108 @@
<?php
/**
* @author Piotr Mrowczynski piotr@owncloud.com
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @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\DAV;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\File as DavFile;
use OCA\DAV\Meta\MetaFile;
use OCP\Files\FileInfo;
use OCP\Files\NotFoundException;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\DAV\Exception\NotFound;
/**
* Sabre plugin for restricting file share receiver download:
*/
class ViewOnlyPlugin extends ServerPlugin {
private ?Server $server = null;
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
/**
* 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.
*/
public function initialize(Server $server): void {
$this->server = $server;
//priority 90 to make sure the plugin is called before
//Sabre\DAV\CorePlugin::httpGet
$this->server->on('method:GET', [$this, 'checkViewOnly'], 90);
}
/**
* Disallow download via DAV Api in case file being received share
* and having special permission
*
* @throws Forbidden
* @throws NotFoundException
*/
public function checkViewOnly(RequestInterface $request): bool {
$path = $request->getPath();
try {
assert($this->server !== null);
$davNode = $this->server->tree->getNodeForPath($path);
if (!($davNode instanceof DavFile)) {
return true;
}
// Restrict view-only to nodes which are shared
$node = $davNode->getNode();
$storage = $node->getStorage();
if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) {
return true;
}
// Extract extra permissions
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$share = $storage->getShare();
$attributes = $share->getAttributes();
if ($attributes === null) {
return true;
}
// Check if read-only and on whether permission can download is both set and disabled.
$canDownload = $attributes->getAttribute('permissions', 'download');
if ($canDownload !== null && !$canDownload) {
throw new Forbidden('Access to this resource has been denied because it is in view-only mode.');
}
} catch (NotFound $e) {
$this->logger->warning($e->getMessage(), [
'exception' => $e,
]);
}
return true;
}
}

View file

@ -62,6 +62,7 @@ use OCA\DAV\Connector\Sabre\SharesPlugin;
use OCA\DAV\Connector\Sabre\TagsPlugin;
use OCA\DAV\DAV\CustomPropertiesBackend;
use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\Files\BrowserErrorPagePlugin;
use OCA\DAV\Files\LazySearchBackend;
@ -229,6 +230,11 @@ class Server {
$this->server->addPlugin(new FakeLockerPlugin());
}
// Allow view-only plugin for webdav requests
$this->server->addPlugin(new ViewOnlyPlugin(
$logger
));
if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
$this->server->addPlugin(new BrowserErrorPagePlugin());
}

View file

@ -29,8 +29,11 @@ namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OC\Files\FileInfo;
use OC\Files\View;
use OC\Share20\ShareAttributes;
use OCA\Files_Sharing\SharedStorage;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\Storage;
use OCP\Share\IAttributes;
use OCP\Share\IManager;
use OCP\Share\IShare;
@ -169,6 +172,65 @@ class NodeTest extends \Test\TestCase {
$this->assertEquals($expected, $node->getSharePermissions($user));
}
public function testShareAttributes() {
$storage = $this->getMockBuilder(SharedStorage::class)
->disableOriginalConstructor()
->setMethods(['getShare'])
->getMock();
$shareManager = $this->getMockBuilder(IManager::class)->disableOriginalConstructor()->getMock();
$share = $this->getMockBuilder(IShare::class)->disableOriginalConstructor()->getMock();
$storage->expects($this->once())
->method('getShare')
->willReturn($share);
$attributes = new ShareAttributes();
$attributes->setAttribute('permissions', 'download', false);
$share->expects($this->once())->method('getAttributes')->willReturn($attributes);
$info = $this->getMockBuilder(FileInfo::class)
->disableOriginalConstructor()
->setMethods(['getStorage', 'getType'])
->getMock();
$info->method('getStorage')->willReturn($storage);
$info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
$view = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
$node = new \OCA\DAV\Connector\Sabre\File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
$this->assertEquals($attributes->toArray(), $node->getShareAttributes());
}
public function testShareAttributesNonShare() {
$storage = $this->getMockBuilder(Storage::class)
->disableOriginalConstructor()
->getMock();
$shareManager = $this->getMockBuilder(IManager::class)->disableOriginalConstructor()->getMock();
$info = $this->getMockBuilder(FileInfo::class)
->disableOriginalConstructor()
->setMethods(['getStorage', 'getType'])
->getMock();
$info->method('getStorage')->willReturn($storage);
$info->method('getType')->willReturn(FileInfo::TYPE_FOLDER);
$view = $this->getMockBuilder(View::class)
->disableOriginalConstructor()
->getMock();
$node = new \OCA\DAV\Connector\Sabre\File($view, $info);
$this->invokePrivate($node, 'shareManager', [$shareManager]);
$this->assertEquals([], $node->getShareAttributes());
}
public function sanitizeMtimeProvider() {
return [
[123456789, 123456789],

View file

@ -34,11 +34,12 @@ use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUrlGenerator;
use OCP\Security\ISecureRandom;
use Test\TestCase;
@ -56,11 +57,13 @@ class DirectControllerTest extends TestCase {
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
private $timeFactory;
/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
/** @var IUrlGenerator|\PHPUnit\Framework\MockObject\MockObject */
private $urlGenerator;
/** @var DirectController */
private $controller;
/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
private $eventDispatcher;
private DirectController $controller;
protected function setUp(): void {
parent::setUp();
@ -69,7 +72,8 @@ class DirectControllerTest extends TestCase {
$this->directMapper = $this->createMock(DirectMapper::class);
$this->random = $this->createMock(ISecureRandom::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->urlGenerator = $this->createMock(IUrlGenerator::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->controller = new DirectController(
'dav',
@ -79,11 +83,12 @@ class DirectControllerTest extends TestCase {
$this->directMapper,
$this->random,
$this->timeFactory,
$this->urlGenerator
$this->urlGenerator,
$this->eventDispatcher
);
}
public function testGetUrlNonExistingFileId() {
public function testGetUrlNonExistingFileId(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@ -97,7 +102,7 @@ class DirectControllerTest extends TestCase {
$this->controller->getUrl(101);
}
public function testGetUrlForFolder() {
public function testGetUrlForFolder(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@ -113,7 +118,7 @@ class DirectControllerTest extends TestCase {
$this->controller->getUrl(101);
}
public function testGetUrlValid() {
public function testGetUrlValid(): void {
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with('awesomeUser')
@ -128,6 +133,9 @@ class DirectControllerTest extends TestCase {
->with(101)
->willReturn([$file]);
$userFolder->method('getRelativePath')
->willReturn('/path');
$this->random->method('generate')
->with(
60,

View file

@ -0,0 +1,117 @@
<?php
/**
* @author Piotr Mrowczynski piotr@owncloud.com
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @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\DAV;
use OCA\DAV\DAV\ViewOnlyPlugin;
use OCA\Files_Sharing\SharedStorage;
use OCA\DAV\Connector\Sabre\File as DavFile;
use OCP\Files\File;
use OCP\Files\Storage\IStorage;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\Tree;
use Test\TestCase;
use Sabre\HTTP\RequestInterface;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
class ViewOnlyPluginTest extends TestCase {
private ViewOnlyPlugin $plugin;
/** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
private $tree;
/** @var RequestInterface | \PHPUnit\Framework\MockObject\MockObject */
private $request;
public function setUp(): void {
$this->plugin = new ViewOnlyPlugin(
$this->createMock(LoggerInterface::class)
);
$this->request = $this->createMock(RequestInterface::class);
$this->tree = $this->createMock(Tree::class);
$server = $this->createMock(Server::class);
$server->tree = $this->tree;
$this->plugin->initialize($server);
}
public function testCanGetNonDav(): void {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$this->tree->method('getNodeForPath')->willReturn(null);
$this->assertTrue($this->plugin->checkViewOnly($this->request));
}
public function testCanGetNonShared(): void {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$davNode = $this->createMock(DavFile::class);
$this->tree->method('getNodeForPath')->willReturn($davNode);
$file = $this->createMock(File::class);
$davNode->method('getNode')->willReturn($file);
$storage = $this->createMock(IStorage::class);
$file->method('getStorage')->willReturn($storage);
$storage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(false);
$this->assertTrue($this->plugin->checkViewOnly($this->request));
}
public function providesDataForCanGet(): array {
return [
// has attribute permissions-download enabled - can get file
[ $this->createMock(File::class), true, true],
// has no attribute permissions-download - can get file
[ $this->createMock(File::class), null, true],
// has attribute permissions-download disabled- cannot get the file
[ $this->createMock(File::class), false, false],
];
}
/**
* @dataProvider providesDataForCanGet
*/
public function testCanGet(File $nodeInfo, ?bool $attrEnabled, bool $expectCanDownloadFile): void {
$this->request->expects($this->once())->method('getPath')->willReturn('files/test/target');
$davNode = $this->createMock(DavFile::class);
$this->tree->method('getNodeForPath')->willReturn($davNode);
$davNode->method('getNode')->willReturn($nodeInfo);
$storage = $this->createMock(SharedStorage::class);
$share = $this->createMock(IShare::class);
$nodeInfo->method('getStorage')->willReturn($storage);
$storage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(true);
$storage->method('getShare')->willReturn($share);
$extAttr = $this->createMock(IAttributes::class);
$share->method('getAttributes')->willReturn($extAttr);
$extAttr->method('getAttribute')->with('permissions', 'download')->willReturn($attrEnabled);
if (!$expectCanDownloadFile) {
$this->expectException(Forbidden::class);
}
$this->plugin->checkViewOnly($this->request);
}
}

View file

@ -83,6 +83,15 @@
return OC.joinPaths(this.get('path'), this.get('name'));
},
/**
* Returns the mimetype of the file
*
* @return {string} mimetype
*/
getMimeType: function() {
return this.get('mimetype');
},
/**
* Reloads missing properties from server and set them in the model.
* @param properties array of properties to be reloaded

View file

@ -47,6 +47,7 @@ export default async function(url) {
<nc:mount-type />
<nc:is-encrypted />
<ocs:share-permissions />
<nc:share-attributes />
<oc:tags />
<oc:favorite />
<oc:comments-unread />

View file

@ -80,4 +80,5 @@ return array(
'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php',
'OCA\\Files_Sharing\\Updater' => $baseDir . '/../lib/Updater.php',
'OCA\\Files_Sharing\\ViewOnly' => $baseDir . '/../lib/ViewOnly.php',
);

View file

@ -95,6 +95,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php',
'OCA\\Files_Sharing\\Updater' => __DIR__ . '/..' . '/../lib/Updater.php',
'OCA\\Files_Sharing\\ViewOnly' => __DIR__ . '/..' . '/../lib/ViewOnly.php',
);
public static function getInitializer(ClassLoader $loader)

View file

@ -50,16 +50,22 @@ use OCA\Files_Sharing\Notification\Listener;
use OCA\Files_Sharing\Notification\Notifier;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSidebar;
use OCP\Files\Event\BeforeDirectGetEvent;
use OCA\Files_Sharing\ShareBackend\File;
use OCA\Files_Sharing\ShareBackend\Folder;
use OCA\Files_Sharing\ViewOnly;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\EventDispatcher\GenericEvent;
use OCP\Federation\ICloudIdManager;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\Events\BeforeZipCreatedEvent;
use OCP\Files\IRootFolder;
use OCP\Group\Events\UserAddedEvent;
use OCP\IDBConnection;
use OCP\IGroup;
@ -71,7 +77,7 @@ use OCP\User\Events\UserChangedEvent;
use OCP\Util;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\EventDispatcher\GenericEvent as OldGenericEvent;
class Application extends App implements IBootstrap {
public const APP_ID = 'files_sharing';
@ -107,6 +113,7 @@ class Application extends App implements IBootstrap {
public function boot(IBootContext $context): void {
$context->injectFn([$this, 'registerMountProviders']);
$context->injectFn([$this, 'registerEventsScripts']);
$context->injectFn([$this, 'registerDownloadEvents']);
$context->injectFn([$this, 'setupSharingMenus']);
Helper::registerHooks();
@ -121,12 +128,12 @@ class Application extends App implements IBootstrap {
}
public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider) {
public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider): void {
$mountProviderCollection->registerProvider($mountProvider);
$mountProviderCollection->registerProvider($externalMountProvider);
}
public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatcherInterface $oldDispatcher) {
public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatcherInterface $oldDispatcher): void {
// sidebar and files scripts
$dispatcher->addServiceListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
$dispatcher->addServiceListener(BeforeTemplateRenderedEvent::class, LegacyBeforeTemplateRenderedListener::class);
@ -139,19 +146,73 @@ class Application extends App implements IBootstrap {
});
// notifications api to accept incoming user shares
$oldDispatcher->addListener('OCP\Share::postShare', function (GenericEvent $event) {
$oldDispatcher->addListener('OCP\Share::postShare', function (OldGenericEvent $event) {
/** @var Listener $listener */
$listener = $this->getContainer()->query(Listener::class);
$listener->shareNotification($event);
});
$oldDispatcher->addListener(IGroup::class . '::postAddUser', function (GenericEvent $event) {
$oldDispatcher->addListener(IGroup::class . '::postAddUser', function (OldGenericEvent $event) {
/** @var Listener $listener */
$listener = $this->getContainer()->query(Listener::class);
$listener->userAddedToGroup($event);
});
}
public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession) {
public function registerDownloadEvents(
IEventDispatcher $dispatcher,
IUserSession $userSession,
IRootFolder $rootFolder
): void {
$dispatcher->addListener(
BeforeDirectFileDownloadEvent::class,
function (BeforeDirectFileDownloadEvent $event) use ($userSession, $rootFolder): void {
$pathsToCheck = [$event->getPath()];
// Check only for user/group shares. Don't restrict e.g. share links
$user = $userSession->getUser();
if ($user) {
$viewOnlyHandler = new ViewOnly(
$rootFolder->getUserFolder($user->getUID())
);
if (!$viewOnlyHandler->check($pathsToCheck)) {
$event->setSuccessful(false);
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
}
}
}
);
$dispatcher->addListener(
BeforeZipCreatedEvent::class,
function (BeforeZipCreatedEvent $event) use ($userSession, $rootFolder): void {
$dir = $event->getDirectory();
$files = $event->getFiles();
$pathsToCheck = [];
foreach ($files as $file) {
$pathsToCheck[] = $dir . '/' . $file;
}
// Check only for user/group shares. Don't restrict e.g. share links
$user = $userSession->getUser();
if ($user) {
$viewOnlyHandler = new ViewOnly(
$rootFolder->getUserFolder($user->getUID())
);
if (!$viewOnlyHandler->check($pathsToCheck)) {
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
$event->setSuccessful(false);
} else {
$event->setSuccessful(true);
}
} else {
$event->setSuccessful(true);
}
}
);
}
public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession): void {
if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) {
return;
}

View file

@ -136,7 +136,7 @@ class PublicPreviewController extends PublicShareController {
* @param $token
* @return DataResponse|FileDisplayResponse
*/
public function directLink($token) {
public function directLink(string $token) {
// No token no image
if ($token === '') {
return new DataResponse([], Http::STATUS_BAD_REQUEST);

View file

@ -45,8 +45,10 @@ declare(strict_types=1);
namespace OCA\Files_Sharing\Controller;
use OC\Files\FileInfo;
use OC\Files\Storage\Wrapper\Wrapper;
use OCA\Files_Sharing\Exceptions\SharingRightsException;
use OCA\Files_Sharing\External\Storage;
use OCA\Files_Sharing\SharedStorage;
use OCA\Files\Helper;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\DataResponse;
@ -324,6 +326,11 @@ class ShareAPIController extends OCSController {
$result['mail_send'] = $share->getMailSend() ? 1 : 0;
$result['hide_download'] = $share->getHideDownload() ? 1 : 0;
$result['attributes'] = null;
if ($attributes = $share->getAttributes()) {
$result['attributes'] = \json_encode($attributes->toArray());
}
return $result;
}
@ -436,6 +443,7 @@ class ShareAPIController extends OCSController {
* @param string $sendPasswordByTalk
* @param string $expireDate
* @param string $label
* @param string $attributes
*
* @return DataResponse
* @throws NotFoundException
@ -456,7 +464,8 @@ class ShareAPIController extends OCSController {
string $sendPasswordByTalk = null,
string $expireDate = '',
string $note = '',
string $label = ''
string $label = '',
string $attributes = null
): DataResponse {
$share = $this->shareManager->newShare();
@ -516,6 +525,8 @@ class ShareAPIController extends OCSController {
$permissions &= ~($permissions & ~$node->getPermissions());
}
$this->checkInheritedAttributes($share);
if ($shareType === IShare::TYPE_USER) {
// Valid user is required to share
if ($shareWith === null || !$this->userManager->userExists($shareWith)) {
@ -674,6 +685,10 @@ class ShareAPIController extends OCSController {
$share->setNote($note);
}
if ($attributes !== null) {
$share = $this->setShareAttributes($share, $attributes);
}
try {
$share = $this->shareManager->createShare($share);
} catch (GenericShareException $e) {
@ -1035,6 +1050,7 @@ class ShareAPIController extends OCSController {
* @param string $note
* @param string $label
* @param string $hideDownload
* @param string $attributes
* @return DataResponse
* @throws LockedException
* @throws NotFoundException
@ -1051,7 +1067,8 @@ class ShareAPIController extends OCSController {
string $expireDate = null,
string $note = null,
string $label = null,
string $hideDownload = null
string $hideDownload = null,
string $attributes = null
): DataResponse {
try {
$share = $this->getShareById($id);
@ -1077,7 +1094,8 @@ class ShareAPIController extends OCSController {
$expireDate === null &&
$note === null &&
$label === null &&
$hideDownload === null
$hideDownload === null &&
$attributes === null
) {
throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given'));
}
@ -1086,6 +1104,25 @@ class ShareAPIController extends OCSController {
$share->setNote($note);
}
$userFolder = $this->rootFolder->getUserFolder($this->currentUser);
// get the node with the point of view of the current user
$nodes = $userFolder->getById($share->getNode()->getId());
if (count($nodes) > 0) {
$node = $nodes[0];
$storage = $node->getStorage();
if ($storage && $storage->instanceOfStorage(SharedStorage::class)) {
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$inheritedAttributes = $storage->getShare()->getAttributes();
if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) {
if ($hideDownload === 'false') {
throw new OCSBadRequestException($this->l->t('Cannot increase permissions'));
}
$share->setHideDownload(true);
}
}
}
/**
* expirationdate, password and publicUpload only make sense for link shares
*/
@ -1216,6 +1253,10 @@ class ShareAPIController extends OCSController {
}
}
if ($attributes !== null) {
$share = $this->setShareAttributes($share, $attributes);
}
try {
$share = $this->shareManager->updateShare($share);
} catch (GenericShareException $e) {
@ -1832,4 +1873,51 @@ class ShareAPIController extends OCSController {
}
}
}
/**
* @param IShare $share
* @param string|null $attributesString
* @return IShare modified share
*/
private function setShareAttributes(IShare $share, ?string $attributesString) {
$newShareAttributes = null;
if ($attributesString !== null) {
$newShareAttributes = $this->shareManager->newShare()->newAttributes();
$formattedShareAttributes = \json_decode($attributesString, true);
if (is_array($formattedShareAttributes)) {
foreach ($formattedShareAttributes as $formattedAttr) {
$newShareAttributes->setAttribute(
$formattedAttr['scope'],
$formattedAttr['key'],
is_string($formattedAttr['enabled']) ? (bool) \json_decode($formattedAttr['enabled']) : $formattedAttr['enabled']
);
}
} else {
throw new OCSBadRequestException('Invalid share attributes provided: \"' . $attributesString . '\"');
}
}
$share->setAttributes($newShareAttributes);
return $share;
}
private function checkInheritedAttributes(IShare $share): void {
if ($share->getNode()->getStorage()->instanceOfStorage(SharedStorage::class)) {
$storage = $share->getNode()->getStorage();
if ($storage instanceof Wrapper) {
$storage = $storage->getInstanceOfStorage(SharedStorage::class);
if ($storage === null) {
throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null');
}
} else {
throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper');
}
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$inheritedAttributes = $storage->getShare()->getAttributes();
if ($inheritedAttributes !== null && $inheritedAttributes->getAttribute('permissions', 'download') === false) {
$share->setHideDownload(true);
}
}
}
}

View file

@ -38,6 +38,7 @@ use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\ILogger;
use OCP\IUser;
use OCP\Share\IAttributes;
use OCP\Share\IManager;
use OCP\Share\IShare;
@ -229,14 +230,32 @@ class MountProvider implements IMountProvider {
->setTarget($shares[0]->getTarget());
// use most permissive permissions
$permissions = 0;
// this covers the case where there are multiple shares for the same
// file e.g. from different groups and different permissions
$superPermissions = 0;
$superAttributes = $this->shareManager->newShare()->newAttributes();
$status = IShare::STATUS_PENDING;
foreach ($shares as $share) {
$permissions |= $share->getPermissions();
$superPermissions |= $share->getPermissions();
$status = max($status, $share->getStatus());
// update permissions
$superPermissions |= $share->getPermissions();
// update share permission attributes
$attributes = $share->getAttributes();
if ($attributes !== null) {
foreach ($attributes->toArray() as $attribute) {
if ($superAttributes->getAttribute($attribute['scope'], $attribute['key']) === true) {
// if super share attribute is already enabled, it is most permissive
continue;
}
// update supershare attributes with subshare attribute
$superAttributes->setAttribute($attribute['scope'], $attribute['key'], $attribute['enabled']);
}
}
// adjust target, for database consistency if needed
if ($share->getTarget() !== $superShare->getTarget()) {
// adjust target, for database consistency
$share->setTarget($superShare->getTarget());
try {
$this->shareManager->moveShare($share, $user->getUID());
@ -261,8 +280,9 @@ class MountProvider implements IMountProvider {
}
}
$superShare->setPermissions($permissions)
->setStatus($status);
$superShare->setPermissions($superPermissions);
$superShare->setStatus($status);
$superShare->setAttributes($superAttributes);
$result[] = [$superShare, $shares];
}

View file

@ -0,0 +1,121 @@
<?php
/**
* @author Piotr Mrowczynski piotr@owncloud.com
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @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\Files_Sharing;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
/**
* Handles restricting for download of files
*/
class ViewOnly {
/** @var Folder */
private $userFolder;
public function __construct(Folder $userFolder) {
$this->userFolder = $userFolder;
}
/**
* @param string[] $pathsToCheck
* @return bool
*/
public function check(array $pathsToCheck): bool {
// If any of elements cannot be downloaded, prevent whole download
foreach ($pathsToCheck as $file) {
try {
$info = $this->userFolder->get($file);
if ($info instanceof File) {
// access to filecache is expensive in the loop
if (!$this->checkFileInfo($info)) {
return false;
}
} elseif ($info instanceof Folder) {
// get directory content is rather cheap query
if (!$this->dirRecursiveCheck($info)) {
return false;
}
}
} catch (NotFoundException $e) {
continue;
}
}
return true;
}
/**
* @param Folder $dirInfo
* @return bool
* @throws NotFoundException
*/
private function dirRecursiveCheck(Folder $dirInfo): bool {
if (!$this->checkFileInfo($dirInfo)) {
return false;
}
// If any of elements cannot be downloaded, prevent whole download
$files = $dirInfo->getDirectoryListing();
foreach ($files as $file) {
if ($file instanceof File) {
if (!$this->checkFileInfo($file)) {
return false;
}
} elseif ($file instanceof Folder) {
return $this->dirRecursiveCheck($file);
}
}
return true;
}
/**
* @param Node $fileInfo
* @return bool
* @throws NotFoundException
*/
private function checkFileInfo(Node $fileInfo): bool {
// Restrict view-only to nodes which are shared
$storage = $fileInfo->getStorage();
if (!$storage->instanceOfStorage(SharedStorage::class)) {
return true;
}
// Extract extra permissions
/** @var \OCA\Files_Sharing\SharedStorage $storage */
$share = $storage->getShare();
$canDownload = true;
// Check if read-only and on whether permission can download is both set and disabled.
$attributes = $share->getAttributes();
if ($attributes !== null) {
$canDownload = $attributes->getAttribute('permissions', 'download');
}
if ($canDownload !== null && !$canDownload) {
return false;
}
return true;
}
}

View file

@ -78,6 +78,13 @@
{{ t('files_sharing', 'Allow resharing') }}
</ActionCheckbox>
<ActionCheckbox ref="canDownload"
:checked.sync="canDownload"
v-if="isSetDownloadButtonVisible"
:disabled="saving || !canSetDownload">
{{ allowDownloadText }}
</ActionCheckbox>
<!-- expiration date -->
<ActionCheckbox :checked.sync="hasExpirationDate"
:disabled="config.isDefaultInternalExpireDateEnforced || saving"
@ -271,6 +278,18 @@ export default {
return (this.fileInfo.sharePermissions & OC.PERMISSION_SHARE) || this.canReshare
},
/**
* Can the sharer set whether the sharee can download the file ?
*
* @return {boolean}
*/
canSetDownload() {
// If the owner revoked the permission after the resharer granted it
// the share still has the permission, and the resharer is still
// allowed to revoke it too (but not to grant it again).
return (this.fileInfo.canDownload() || this.canDownload)
},
/**
* Can the sharee edit the shared file ?
*/
@ -319,6 +338,18 @@ export default {
},
},
/**
* Can the sharee download files or only view them ?
*/
canDownload: {
get() {
return this.share.hasDownloadPermission
},
set(checked) {
this.updatePermissions({ isDownloadChecked: checked })
},
},
/**
* Is this share readable
* Needed for some federated shares that might have been added from file drop links
@ -377,10 +408,46 @@ export default {
return (typeof this.share.status === 'object' && !Array.isArray(this.share.status))
},
/**
* @return {string}
*/
allowDownloadText() {
if (this.isFolder) {
return t('files_sharing', 'Allow download of office files')
} else {
return t('files_sharing', 'Allow download')
}
},
/**
* @return {boolean}
*/
isSetDownloadButtonVisible() {
const allowedMimetypes = [
// Office documents
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.presentation',
]
return this.isFolder || allowedMimetypes.includes(this.fileInfo.mimetype)
},
},
methods: {
updatePermissions({ isEditChecked = this.canEdit, isCreateChecked = this.canCreate, isDeleteChecked = this.canDelete, isReshareChecked = this.canReshare } = {}) {
updatePermissions({
isEditChecked = this.canEdit,
isCreateChecked = this.canCreate,
isDeleteChecked = this.canDelete,
isReshareChecked = this.canReshare,
isDownloadChecked = this.canDownload,
} = {}) {
// calc permissions if checked
const permissions = 0
| (this.hasRead ? this.permissionsRead : 0)
@ -390,7 +457,10 @@ export default {
| (isReshareChecked ? this.permissionsShare : 0)
this.share.permissions = permissions
this.queueUpdate('permissions')
if (this.share.hasDownloadPermission !== isDownloadChecked) {
this.share.hasDownloadPermission = isDownloadChecked
}
this.queueUpdate('permissions', 'attributes')
},
/**

View file

@ -159,7 +159,7 @@
<ActionSeparator />
<ActionCheckbox :checked.sync="share.hideDownload"
:disabled="saving"
:disabled="saving || canChangeHideDownload"
@change="queueUpdate('hideDownload')">
{{ t('files_sharing', 'Hide download') }}
</ActionCheckbox>
@ -607,6 +607,12 @@ export default {
isPasswordPolicyEnabled() {
return typeof this.config.passwordPolicy === 'object'
},
canChangeHideDownload() {
const hasDisabledDownload = (shareAttribute) => shareAttribute.key === 'download' && shareAttribute.scope === 'permissions' && shareAttribute.enabled === false
return this.fileInfo.shareAttributes.some(hasDisabledDownload)
},
},
methods: {
@ -697,6 +703,7 @@ export default {
shareType: ShareTypes.SHARE_TYPE_LINK,
password: share.password,
expireDate: share.expireDate,
attributes: JSON.stringify(this.fileInfo.shareAttributes),
// we do not allow setting the publicUpload
// before the share creation.
// Todo: We also need to fix the createShare method in
@ -867,7 +874,6 @@ export default {
this.$emit('remove:share', this.share)
},
},
}
</script>

View file

@ -478,6 +478,7 @@ export default {
shareWith: value.shareWith,
password,
permissions: this.fileInfo.sharePermissions & OC.getCapabilities().files_sharing.default_permissions,
attributes: JSON.stringify(this.fileInfo.shareAttributes),
})
// If we had a password, we need to show it to the user as it was generated

View file

@ -47,12 +47,13 @@ export default {
* @param {boolean} [data.sendPasswordByTalk=false] send the password via a talk conversation
* @param {string} [data.expireDate=''] expire the shareautomatically after
* @param {string} [data.label=''] custom label
* @param {string} [data.attributes=null] Share attributes encoded as json
* @return {Share} the new share
* @throws {Error}
*/
async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label }) {
async createShare({ path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, attributes }) {
try {
const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label })
const request = await axios.post(shareUrl, { path, permissions, shareType, shareWith, publicUpload, password, sendPasswordByTalk, expireDate, label, attributes })
if (!request?.data?.ocs) {
throw request
}

View file

@ -229,7 +229,13 @@ export default {
const properties = {}
// force value to string because that is what our
// share api controller accepts
propertyNames.map(p => (properties[p] = this.share[p].toString()))
propertyNames.forEach(name => {
if ((typeof this.share[name]) === 'object') {
properties[name] = JSON.stringify(this.share[name])
} else {
properties[name] = this.share[name].toString()
}
})
this.updateQueue.add(async () => {
this.saving = true

View file

@ -43,6 +43,15 @@ export default class Share {
ocsData.hide_download = !!ocsData.hide_download
ocsData.mail_send = !!ocsData.mail_send
if (ocsData.attributes) {
try {
ocsData.attributes = JSON.parse(ocsData.attributes)
} catch (e) {
console.warn('Could not parse share attributes returned by server: "' + ocsData.attributes + '"')
}
}
ocsData.attributes = ocsData.attributes ?? []
// store state
this._share = ocsData
}
@ -96,6 +105,17 @@ export default class Share {
return this._share.permissions
}
/**
* Get the share attributes
*
* @return {Array}
* @readonly
* @memberof Share
*/
get attributes() {
return this._share.attributes
}
/**
* Set the share permissions
* See OC.PERMISSION_* variables
@ -527,6 +547,47 @@ export default class Share {
return !!((this.permissions & OC.PERMISSION_SHARE))
}
/**
* Does this share have download permissions
*
* @return {boolean}
* @readonly
* @memberof Share
*/
get hasDownloadPermission() {
for (const i in this._share.attributes) {
const attr = this._share.attributes[i]
if (attr.scope === 'permissions' && attr.key === 'download') {
return attr.enabled
}
}
return true
}
set hasDownloadPermission(enabled) {
this.setAttribute('permissions', 'download', !!enabled)
}
setAttribute(scope, key, enabled) {
const attrUpdate = {
scope,
key,
enabled,
}
// try and replace existing
for (const i in this._share.attributes) {
const attr = this._share.attributes[i]
if (attr.scope === attrUpdate.scope && attr.key === attrUpdate.key) {
this._share.attributes[i] = attrUpdate
return
}
}
this._share.attributes.push(attrUpdate)
}
// PERMISSIONS Shortcuts for the CURRENT USER
// ! the permissions above are the share settings,
// ! meaning the permissions for the recipient

View file

@ -92,7 +92,11 @@ import { getCapabilities } from '@nextcloud/capabilities'
delete fileActions.actions.all.Details
delete fileActions.actions.all.Goto
}
if (_.isFunction(fileData.canDownload) && !fileData.canDownload()) {
delete fileActions.actions.all.Download
}
tr.attr('data-share-permissions', sharePermissions)
tr.attr('data-share-attributes', JSON.stringify(fileData.shareAttributes))
if (fileData.shareOwner) {
tr.attr('data-share-owner', fileData.shareOwner)
tr.attr('data-share-owner-id', fileData.shareOwnerId)
@ -113,6 +117,7 @@ import { getCapabilities } from '@nextcloud/capabilities'
var oldElementToFile = fileList.elementToFile
fileList.elementToFile = function($el) {
var fileInfo = oldElementToFile.apply(this, arguments)
fileInfo.shareAttributes = JSON.parse($el.attr('data-share-attributes') || '[]')
fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined
fileInfo.shareOwner = $el.attr('data-share-owner') || undefined
fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined

View file

@ -948,8 +948,15 @@ class ApiTest extends TestCase {
->setSharedBy(self::TEST_FILES_SHARING_API_USER1)
->setSharedWith(self::TEST_FILES_SHARING_API_USER2)
->setShareType(IShare::TYPE_USER)
->setPermissions(19);
->setPermissions(19)
->setAttributes($this->shareManager->newShare()->newAttributes());
$this->assertNotNull($share1->getAttributes());
$share1 = $this->shareManager->createShare($share1);
$this->assertNull($share1->getAttributes());
$this->assertEquals(19, $share1->getPermissions());
// attributes get cleared when empty
$this->assertNull($share1->getAttributes());
$share2 = $this->shareManager->newShare();
$share2->setNode($node1)
@ -957,14 +964,19 @@ class ApiTest extends TestCase {
->setShareType(IShare::TYPE_LINK)
->setPermissions(1);
$share2 = $this->shareManager->createShare($share2);
$this->assertEquals(1, $share2->getPermissions());
// update permissions
$ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1);
$ocs->updateShare($share1->getId(), 1);
$ocs->updateShare(
$share1->getId(), 1, null, null, null, null, null, null, null,
'[{"scope": "app1", "key": "attr1", "enabled": true}]'
);
$ocs->cleanup();
$share1 = $this->shareManager->getShareById('ocinternal:' . $share1->getId());
$this->assertEquals(1, $share1->getPermissions());
$this->assertEquals(true, $share1->getAttributes()->getAttribute('app1', 'attr1'));
// update password for link share
$this->assertNull($share2->getPassword());

View file

@ -0,0 +1,236 @@
<?php
/**
* @copyright 2022, Vincent Petry <vincent@nextcloud.com>
*
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Files_Sharing\Tests;
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\Events\BeforeZipCreatedEvent;
use Psr\Log\LoggerInterface;
use OC\Share20\LegacyHooks;
use OC\Share20\Manager;
use OC\EventDispatcher\EventDispatcher;
use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\SharedStorage;
use OCP\Constants;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Event\BeforeDirectGetEvent;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Storage\IStorage;
use OCP\IServerContainer;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
use Symfony\Component\EventDispatcher\EventDispatcher as SymfonyDispatcher;
use Test\TestCase;
class ApplicationTest extends TestCase {
private Application $application;
private IEventDispatcher $eventDispatcher;
/** @var IUserSession */
private $userSession;
/** @var IRootFolder */
private $rootFolder;
/** @var Manager */ private $manager;
protected function setUp(): void {
parent::setUp();
$this->application = new Application([]);
$symfonyDispatcher = new SymfonyDispatcher();
$this->eventDispatcher = new EventDispatcher(
$symfonyDispatcher,
$this->createMock(IServerContainer::class),
$this->createMock(LoggerInterface::class)
);
$this->userSession = $this->createMock(IUserSession::class);
$this->rootFolder = $this->createMock(IRootFolder::class);
$this->application->registerDownloadEvents(
$this->eventDispatcher,
$this->userSession,
$this->rootFolder
);
}
public function providesDataForCanGet(): array {
// normal file (sender) - can download directly
$senderFileStorage = $this->createMock(IStorage::class);
$senderFileStorage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(false);
$senderFile = $this->createMock(File::class);
$senderFile->method('getStorage')->willReturn($senderFileStorage);
$senderUserFolder = $this->createMock(Folder::class);
$senderUserFolder->method('get')->willReturn($senderFile);
$result[] = [ '/bar.txt', $senderUserFolder, true ];
// shared file (receiver) with attribute secure-view-enabled set false -
// can download directly
$receiverFileShareAttributes = $this->createMock(IAttributes::class);
$receiverFileShareAttributes->method('getAttribute')->with('permissions', 'download')->willReturn(true);
$receiverFileShare = $this->createMock(IShare::class);
$receiverFileShare->method('getAttributes')->willReturn($receiverFileShareAttributes);
$receiverFileStorage = $this->createMock(SharedStorage::class);
$receiverFileStorage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(true);
$receiverFileStorage->method('getShare')->willReturn($receiverFileShare);
$receiverFile = $this->createMock(File::class);
$receiverFile->method('getStorage')->willReturn($receiverFileStorage);
$receiverUserFolder = $this->createMock(Folder::class);
$receiverUserFolder->method('get')->willReturn($receiverFile);
$result[] = [ '/share-bar.txt', $receiverUserFolder, true ];
// shared file (receiver) with attribute secure-view-enabled set true -
// cannot download directly
$secureReceiverFileShareAttributes = $this->createMock(IAttributes::class);
$secureReceiverFileShareAttributes->method('getAttribute')->with('permissions', 'download')->willReturn(false);
$secureReceiverFileShare = $this->createMock(IShare::class);
$secureReceiverFileShare->method('getAttributes')->willReturn($secureReceiverFileShareAttributes);
$secureReceiverFileStorage = $this->createMock(SharedStorage::class);
$secureReceiverFileStorage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(true);
$secureReceiverFileStorage->method('getShare')->willReturn($secureReceiverFileShare);
$secureReceiverFile = $this->createMock(File::class);
$secureReceiverFile->method('getStorage')->willReturn($secureReceiverFileStorage);
$secureReceiverUserFolder = $this->createMock(Folder::class);
$secureReceiverUserFolder->method('get')->willReturn($secureReceiverFile);
$result[] = [ '/secure-share-bar.txt', $secureReceiverUserFolder, false ];
return $result;
}
/**
* @dataProvider providesDataForCanGet
*/
public function testCheckDirectCanBeDownloaded(string $path, Folder $userFolder, bool $run): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('test');
$this->userSession->method('getUser')->willReturn($user);
$this->userSession->method('isLoggedIn')->willReturn(true);
$this->rootFolder->method('getUserFolder')->willReturn($userFolder);
// Simulate direct download of file
$event = new BeforeDirectFileDownloadEvent($path);
$this->eventDispatcher->dispatchTyped($event);
$this->assertEquals($run, $event->isSuccessful());
}
public function providesDataForCanZip(): array {
// Mock: Normal file/folder storage
$nonSharedStorage = $this->createMock(IStorage::class);
$nonSharedStorage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(false);
// Mock: Secure-view file/folder shared storage
$secureReceiverFileShareAttributes = $this->createMock(IAttributes::class);
$secureReceiverFileShareAttributes->method('getAttribute')->with('permissions', 'download')->willReturn(false);
$secureReceiverFileShare = $this->createMock(IShare::class);
$secureReceiverFileShare->method('getAttributes')->willReturn($secureReceiverFileShareAttributes);
$secureSharedStorage = $this->createMock(SharedStorage::class);
$secureSharedStorage->method('instanceOfStorage')->with(SharedStorage::class)->willReturn(true);
$secureSharedStorage->method('getShare')->willReturn($secureReceiverFileShare);
// 1. can download zipped 2 non-shared files inside non-shared folder
// 2. can download zipped non-shared folder
$sender1File = $this->createMock(File::class);
$sender1File->method('getStorage')->willReturn($nonSharedStorage);
$sender1Folder = $this->createMock(Folder::class);
$sender1Folder->method('getStorage')->willReturn($nonSharedStorage);
$sender1Folder->method('getDirectoryListing')->willReturn([$sender1File, $sender1File]);
$sender1RootFolder = $this->createMock(Folder::class);
$sender1RootFolder->method('getStorage')->willReturn($nonSharedStorage);
$sender1RootFolder->method('getDirectoryListing')->willReturn([$sender1Folder]);
$sender1UserFolder = $this->createMock(Folder::class);
$sender1UserFolder->method('get')->willReturn($sender1RootFolder);
$return[] = [ '/folder', ['bar1.txt', 'bar2.txt'], $sender1UserFolder, true ];
$return[] = [ '/', ['folder'], $sender1UserFolder, true ];
// 3. cannot download zipped 1 non-shared file and 1 secure-shared inside non-shared folder
$receiver1File = $this->createMock(File::class);
$receiver1File->method('getStorage')->willReturn($nonSharedStorage);
$receiver1SecureFile = $this->createMock(File::class);
$receiver1SecureFile->method('getStorage')->willReturn($secureSharedStorage);
$receiver1Folder = $this->createMock(Folder::class);
$receiver1Folder->method('getStorage')->willReturn($nonSharedStorage);
$receiver1Folder->method('getDirectoryListing')->willReturn([$receiver1File, $receiver1SecureFile]);
$receiver1RootFolder = $this->createMock(Folder::class);
$receiver1RootFolder->method('getStorage')->willReturn($nonSharedStorage);
$receiver1RootFolder->method('getDirectoryListing')->willReturn([$receiver1Folder]);
$receiver1UserFolder = $this->createMock(Folder::class);
$receiver1UserFolder->method('get')->willReturn($receiver1RootFolder);
$return[] = [ '/folder', ['secured-bar1.txt', 'bar2.txt'], $receiver1UserFolder, false ];
// 4. cannot download zipped secure-shared folder
$receiver2Folder = $this->createMock(Folder::class);
$receiver2Folder->method('getStorage')->willReturn($secureSharedStorage);
$receiver2RootFolder = $this->createMock(Folder::class);
$receiver2RootFolder->method('getStorage')->willReturn($nonSharedStorage);
$receiver2RootFolder->method('getDirectoryListing')->willReturn([$receiver2Folder]);
$receiver2UserFolder = $this->createMock(Folder::class);
$receiver2UserFolder->method('get')->willReturn($receiver2RootFolder);
$return[] = [ '/', ['secured-folder'], $receiver2UserFolder, false ];
return $return;
}
/**
* @dataProvider providesDataForCanZip
*/
public function testCheckZipCanBeDownloaded(string $dir, array $files, Folder $userFolder, bool $run): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('test');
$this->userSession->method('getUser')->willReturn($user);
$this->userSession->method('isLoggedIn')->willReturn(true);
$this->rootFolder->method('getUserFolder')->with('test')->willReturn($userFolder);
// Simulate zip download of folder folder
$event = new BeforeZipCreatedEvent($dir, $files);
$this->eventDispatcher->dispatchTyped($event);
$this->assertEquals($run, $event->isSuccessful());
$this->assertEquals($run, $event->getErrorMessage() === null);
}
public function testCheckFileUserNotFound(): void {
$this->userSession->method('isLoggedIn')->willReturn(false);
// Simulate zip download of folder folder
$event = new BeforeZipCreatedEvent('/test', ['test.txt']);
$this->eventDispatcher->dispatchTyped($event);
// It should run as this would restrict e.g. share links otherwise
$this->assertTrue($event->isSuccessful());
$this->assertEquals(null, $event->getErrorMessage());
}
}

View file

@ -46,6 +46,7 @@ use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\Storage;
use OCP\Files\Storage\IStorage;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
@ -58,6 +59,7 @@ use OCP\IUser;
use OCP\IUserManager;
use OCP\Lock\LockedException;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\IAttributes as IShareAttributes;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Test\TestCase;
@ -124,7 +126,7 @@ class ShareAPIControllerTest extends TestCase {
->willReturn(true);
$this->shareManager
->expects($this->any())
->method('shareProviderExists')->willReturn(true);
->method('shareProviderExists')->willReturn(true);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->request = $this->createMock(IRequest::class);
@ -194,6 +196,23 @@ class ShareAPIControllerTest extends TestCase {
}
private function mockShareAttributes() {
$formattedShareAttributes = [
[
'scope' => 'permissions',
'key' => 'download',
'enabled' => true
]
];
$shareAttributes = $this->createMock(IShareAttributes::class);
$shareAttributes->method('toArray')->willReturn($formattedShareAttributes);
$shareAttributes->method('getAttribute')->with('permissions', 'download')->willReturn(true);
// send both IShare attributes class and expected json string
return [$shareAttributes, \json_encode($formattedShareAttributes)];
}
public function testDeleteShareShareNotFound() {
$this->expectException(\OCP\AppFramework\OCS\OCSNotFoundException::class);
$this->expectExceptionMessage('Wrong share ID, share does not exist');
@ -505,7 +524,7 @@ class ShareAPIControllerTest extends TestCase {
public function createShare($id, $shareType, $sharedWith, $sharedBy, $shareOwner, $path, $permissions,
$shareTime, $expiration, $parent, $target, $mail_send, $note = '', $token = null,
$password = null, $label = '') {
$password = null, $label = '', $attributes = null) {
$share = $this->getMockBuilder(IShare::class)->getMock();
$share->method('getId')->willReturn($id);
$share->method('getShareType')->willReturn($shareType);
@ -516,6 +535,7 @@ class ShareAPIControllerTest extends TestCase {
$share->method('getPermissions')->willReturn($permissions);
$share->method('getNote')->willReturn($note);
$share->method('getLabel')->willReturn($label);
$share->method('getAttributes')->willReturn($attributes);
$time = new \DateTime();
$time->setTimestamp($shareTime);
$share->method('getShareTime')->willReturn($time);
@ -565,6 +585,8 @@ class ShareAPIControllerTest extends TestCase {
$folder->method('getParent')->willReturn($parentFolder);
$folder->method('getMimeType')->willReturn('myFolderMimeType');
[$shareAttributes, $shareAttributesReturnJson] = $this->mockShareAttributes();
// File shared with user
$share = $this->createShare(
100,
@ -579,7 +601,8 @@ class ShareAPIControllerTest extends TestCase {
6,
'target',
0,
'personal note'
'personal note',
$shareAttributes,
);
$expected = [
'id' => 100,
@ -597,6 +620,7 @@ class ShareAPIControllerTest extends TestCase {
'token' => null,
'expiration' => null,
'permissions' => 4,
'attributes' => $shareAttributesReturnJson,
'stime' => 5,
'parent' => null,
'storage_id' => 'STORAGE',
@ -613,6 +637,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'attributes' => null,
];
$data[] = [$share, $expected];
@ -630,7 +655,8 @@ class ShareAPIControllerTest extends TestCase {
6,
'target',
0,
'personal note'
'personal note',
$shareAttributes,
);
$expected = [
'id' => 101,
@ -647,6 +673,7 @@ class ShareAPIControllerTest extends TestCase {
'token' => null,
'expiration' => null,
'permissions' => 4,
'attributes' => $shareAttributesReturnJson,
'stime' => 5,
'parent' => null,
'storage_id' => 'STORAGE',
@ -662,6 +689,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
];
$data[] = [$share, $expected];
@ -702,6 +730,7 @@ class ShareAPIControllerTest extends TestCase {
'token' => 'token',
'expiration' => '2000-01-02 00:00:00',
'permissions' => 4,
'attributes' => null,
'stime' => 5,
'parent' => null,
'storage_id' => 'STORAGE',
@ -718,6 +747,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
];
$data[] = [$share, $expected];
@ -1646,8 +1676,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1680,8 +1712,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1732,8 +1766,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1788,8 +1824,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1847,8 +1885,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1901,8 +1941,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -1935,8 +1977,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -1956,8 +2000,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -1978,8 +2024,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -1999,8 +2047,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -2035,8 +2085,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -2071,8 +2123,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -2114,8 +2168,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$path->method('getPath')->willReturn('valid-path');
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
@ -2150,8 +2206,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -2193,8 +2251,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$this->rootFolder->method('getUserFolder')->with($this->currentUser)->willReturnSelf();
$this->rootFolder->method('get')->with('valid-path')->willReturn($path);
@ -2241,8 +2301,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -2313,8 +2375,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -2367,8 +2431,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -2451,8 +2517,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$path->method('getPath')->willReturn('valid-path');
$userFolder->expects($this->once())
@ -2494,8 +2562,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(File::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(false);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', false],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$userFolder->expects($this->once())
->method('get')
@ -2575,8 +2645,10 @@ class ShareAPIControllerTest extends TestCase {
$path = $this->getMockBuilder(Folder::class)->getMock();
$storage = $this->createMock(Storage::class);
$storage->method('instanceOfStorage')
->with('OCA\Files_Sharing\External\Storage')
->willReturn(true);
->willReturnMap([
['OCA\Files_Sharing\External\Storage', true],
['OCA\Files_Sharing\SharedStorage', false],
]);
$path->method('getStorage')->willReturn($storage);
$path->method('getPermissions')->willReturn(\OCP\Constants::PERMISSION_READ);
$userFolder->expects($this->once())
@ -2935,8 +3007,17 @@ class ShareAPIControllerTest extends TestCase {
$this->expectExceptionMessage('Invalid date. Format must be YYYY-MM-DD');
$ocs = $this->mockFormatShare();
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$folder = $this->getMockBuilder(Folder::class)->getMock();
$folder->method('getId')
->willReturn(42);
$share = \OC::$server->getShareManager()->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
@ -2974,8 +3055,16 @@ class ShareAPIControllerTest extends TestCase {
$this->expectExceptionMessage('Public upload disabled by the administrator');
$ocs = $this->mockFormatShare();
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$folder = $this->getMockBuilder(Folder::class)->getMock();
$folder->method('getId')->willReturn(42);
$share = \OC::$server->getShareManager()->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
@ -2997,6 +3086,15 @@ class ShareAPIControllerTest extends TestCase {
$ocs = $this->mockFormatShare();
$file = $this->getMockBuilder(File::class)->getMock();
$file->method('getId')
->willReturn(42);
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$share = \OC::$server->getShareManager()->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
@ -3010,13 +3108,21 @@ class ShareAPIControllerTest extends TestCase {
$ocs->updateShare(42, null, 'password', null, 'true', '');
}
public function testUpdateLinkSharePasswordDoesNotChangeOther() {
public function testUpdateLinkSharePasswordDoesNotChangeOther(): void {
$ocs = $this->mockFormatShare();
$date = new \DateTime('2000-01-01');
$date->setTime(0,0,0);
$node = $this->getMockBuilder(File::class)->getMock();
$node->method('getId')->willReturn(42);
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$share = $this->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
->setSharedBy($this->currentUser)
@ -3061,7 +3167,15 @@ class ShareAPIControllerTest extends TestCase {
$date = new \DateTime('2000-01-01');
$date->setTime(0,0,0);
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$node = $this->getMockBuilder(File::class)->getMock();
$node->method('getId')->willReturn(42);
$share = $this->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
->setSharedBy($this->currentUser)
@ -3112,7 +3226,15 @@ class ShareAPIControllerTest extends TestCase {
$date = new \DateTime('2000-01-01');
$date->setTime(0,0,0);
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$node = $this->getMockBuilder(File::class)->getMock();
$node->method('getId')->willReturn(42);
$share = $this->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
->setSharedBy($this->currentUser)
@ -3145,7 +3267,15 @@ class ShareAPIControllerTest extends TestCase {
$date = new \DateTime('2000-01-01');
$date->setTime(0,0,0);
$userFolder = $this->createMock(Folder::class);
$userFolder->method('getById')
->with(42)
->willReturn([]);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$node = $this->getMockBuilder(File::class)->getMock();
$node->method('getId')->willReturn(42);
$share = $this->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
->setSharedBy($this->currentUser)
@ -3725,7 +3855,7 @@ class ShareAPIControllerTest extends TestCase {
$recipient = $this->getMockBuilder(IUser::class)->getMock();
$recipient->method('getDisplayName')->willReturn('recipientDN');
$recipient->method('getSystemEMailAddress')->willReturn('recipient');
[$shareAttributes, $shareAttributesReturnJson] = $this->mockShareAttributes();
$result = [];
@ -3735,6 +3865,7 @@ class ShareAPIControllerTest extends TestCase {
->setSharedBy('initiator')
->setShareOwner('owner')
->setPermissions(\OCP\Constants::PERMISSION_READ)
->setAttributes($shareAttributes)
->setNode($file)
->setShareTime(new \DateTime('2000-01-01T00:01:02'))
->setTarget('myTarget')
@ -3749,6 +3880,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => $shareAttributesReturnJson,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -3775,6 +3907,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'attributes' => '[{"scope":"permissions","key":"download","enabled":true}]',
], $share, [], false
];
// User backend up
@ -3785,6 +3918,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiatorDN',
'permissions' => 1,
'attributes' => $shareAttributesReturnJson,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -3811,6 +3945,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'attributes' => '[{"scope":"permissions","key":"download","enabled":true}]',
], $share, [
['owner', $owner],
['initiator', $initiator],
@ -3837,6 +3972,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => null,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -3863,6 +3999,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'status' => [],
'attributes' => null,
], $share, [], false
];
@ -3885,6 +4022,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => null,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -3911,6 +4049,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => true,
'can_delete' => true,
'status' => [],
'attributes' => null,
], $share, [], false
];
@ -3935,6 +4074,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => null,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -3959,6 +4099,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4005,6 +4146,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4030,6 +4172,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => null,
'stime' => 946684862,
'parent' => null,
'expiration' => '2001-01-02 00:00:00',
@ -4057,6 +4200,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4110,6 +4254,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4157,6 +4302,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4204,6 +4350,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4228,6 +4375,7 @@ class ShareAPIControllerTest extends TestCase {
'uid_owner' => 'initiator',
'displayname_owner' => 'initiator',
'permissions' => 1,
'attributes' => null,
'stime' => 946684862,
'parent' => null,
'expiration' => null,
@ -4253,6 +4401,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4300,6 +4449,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4347,6 +4497,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, [], false
];
@ -4411,6 +4562,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
'attributes' => null,
], $share, [], false
];
@ -4461,6 +4613,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
'attributes' => null,
], $share, [], false
];
@ -4510,6 +4663,7 @@ class ShareAPIControllerTest extends TestCase {
'can_edit' => true,
'can_delete' => true,
'status' => [],
'attributes' => null,
], $share, [], false
];
@ -4661,6 +4815,7 @@ class ShareAPIControllerTest extends TestCase {
'label' => '',
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, false, []
];
@ -4707,6 +4862,7 @@ class ShareAPIControllerTest extends TestCase {
'label' => '',
'can_edit' => false,
'can_delete' => false,
'attributes' => null,
], $share, true, [
'share_with_displayname' => 'recipientRoomName'
]

View file

@ -39,6 +39,7 @@ use OCP\IConfig;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Share\IAttributes as IShareAttributes;
use OCP\Share\IManager;
use OCP\Share\IShare;
@ -81,11 +82,35 @@ class MountProviderTest extends \Test\TestCase {
$this->provider = new MountProvider($this->config, $this->shareManager, $this->logger, $eventDispatcher, $cacheFactory);
}
private function makeMockShare($id, $nodeId, $owner = 'user2', $target = null, $permissions = 31) {
private function makeMockShareAttributes($attrs) {
if ($attrs === null) {
return null;
}
$shareAttributes = $this->createMock(IShareAttributes::class);
$shareAttributes->method('toArray')->willReturn($attrs);
$shareAttributes->method('getAttribute')->will(
$this->returnCallback(function ($scope, $key) use ($attrs) {
$result = null;
foreach ($attrs as $attr) {
if ($attr['key'] === $key && $attr['scope'] === $scope) {
$result = $attr['enabled'];
}
}
return $result;
})
);
return $shareAttributes;
}
private function makeMockShare($id, $nodeId, $owner = 'user2', $target = null, $permissions = 31, $attributes = null) {
$share = $this->createMock(IShare::class);
$share->expects($this->any())
->method('getPermissions')
->willReturn($permissions);
$share->expects($this->any())
->method('getAttributes')
->will($this->returnValue($this->makeMockShareAttributes($attributes)));
$share->expects($this->any())
->method('getShareOwner')
->willReturn($owner);
@ -115,14 +140,16 @@ class MountProviderTest extends \Test\TestCase {
public function testExcludeShares() {
$rootFolder = $this->createMock(IRootFolder::class);
$userManager = $this->createMock(IUserManager::class);
$attr1 = [];
$attr2 = [['scope' => 'permission', 'key' => 'download', 'enabled' => true]];
$userShares = [
$this->makeMockShare(1, 100, 'user2', '/share2', 0),
$this->makeMockShare(2, 100, 'user2', '/share2', 31),
$this->makeMockShare(1, 100, 'user2', '/share2', 0, $attr1),
$this->makeMockShare(2, 100, 'user2', '/share2', 31, $attr2),
];
$groupShares = [
$this->makeMockShare(3, 100, 'user2', '/share2', 0),
$this->makeMockShare(4, 101, 'user2', '/share4', 31),
$this->makeMockShare(5, 100, 'user1', '/share4', 31),
$this->makeMockShare(3, 100, 'user2', '/share2', 0, $attr1),
$this->makeMockShare(4, 101, 'user2', '/share4', 31, $attr2),
$this->makeMockShare(5, 100, 'user1', '/share4', 31, $attr2),
];
$roomShares = [
$this->makeMockShare(6, 102, 'user2', '/share6', 0),
@ -173,12 +200,14 @@ class MountProviderTest extends \Test\TestCase {
$this->assertEquals(100, $mountedShare1->getNodeId());
$this->assertEquals('/share2', $mountedShare1->getTarget());
$this->assertEquals(31, $mountedShare1->getPermissions());
$this->assertEquals(true, $mountedShare1->getAttributes()->getAttribute('permission', 'download'));
$mountedShare2 = $mounts[1]->getShare();
$this->assertEquals('4', $mountedShare2->getId());
$this->assertEquals('user2', $mountedShare2->getShareOwner());
$this->assertEquals(101, $mountedShare2->getNodeId());
$this->assertEquals('/share4', $mountedShare2->getTarget());
$this->assertEquals(31, $mountedShare2->getPermissions());
$this->assertEquals(true, $mountedShare2->getAttributes()->getAttribute('permission', 'download'));
$mountedShare3 = $mounts[2]->getShare();
$this->assertEquals('8', $mountedShare3->getId());
$this->assertEquals('user2', $mountedShare3->getShareOwner());
@ -200,27 +229,27 @@ class MountProviderTest extends \Test\TestCase {
// #0: share as outsider with "group1" and "user1" with same permissions
[
[
[1, 100, 'user2', '/share2', 31],
[1, 100, 'user2', '/share2', 31, null],
],
[
[2, 100, 'user2', '/share2', 31],
[2, 100, 'user2', '/share2', 31, null],
],
[
// combined, user share has higher priority
['1', 100, 'user2', '/share2', 31],
['1', 100, 'user2', '/share2', 31, []],
],
],
// #1: share as outsider with "group1" and "user1" with different permissions
[
[
[1, 100, 'user2', '/share', 31],
[1, 100, 'user2', '/share', 31, [['scope' => 'permission', 'key' => 'download', 'enabled' => true], ['scope' => 'app', 'key' => 'attribute1', 'enabled' => true]]],
],
[
[2, 100, 'user2', '/share', 15],
[2, 100, 'user2', '/share', 15, [['scope' => 'permission', 'key' => 'download', 'enabled' => false], ['scope' => 'app', 'key' => 'attribute2', 'enabled' => false]]],
],
[
// use highest permissions
['1', 100, 'user2', '/share', 31],
['1', 100, 'user2', '/share', 31, [['scope' => 'permission', 'key' => 'download', 'enabled' => true], ['scope' => 'app', 'key' => 'attribute1', 'enabled' => true], ['scope' => 'app', 'key' => 'attribute2', 'enabled' => false]]],
],
],
// #2: share as outsider with "group1" and "group2" with same permissions
@ -228,12 +257,12 @@ class MountProviderTest extends \Test\TestCase {
[
],
[
[1, 100, 'user2', '/share', 31],
[2, 100, 'user2', '/share', 31],
[1, 100, 'user2', '/share', 31, null],
[2, 100, 'user2', '/share', 31, []],
],
[
// combined, first group share has higher priority
['1', 100, 'user2', '/share', 31],
['1', 100, 'user2', '/share', 31, []],
],
],
// #3: share as outsider with "group1" and "group2" with different permissions
@ -241,12 +270,12 @@ class MountProviderTest extends \Test\TestCase {
[
],
[
[1, 100, 'user2', '/share', 31],
[2, 100, 'user2', '/share', 15],
[1, 100, 'user2', '/share', 31, [['scope' => 'permission', 'key' => 'download', 'enabled' => false]]],
[2, 100, 'user2', '/share', 15, [['scope' => 'permission', 'key' => 'download', 'enabled' => true]]],
],
[
// use higher permissions
['1', 100, 'user2', '/share', 31],
// use higher permissions (most permissive)
['1', 100, 'user2', '/share', 31, [['scope' => 'permission', 'key' => 'download', 'enabled' => true]]],
],
],
// #4: share as insider with "group1"
@ -254,7 +283,7 @@ class MountProviderTest extends \Test\TestCase {
[
],
[
[1, 100, 'user1', '/share', 31],
[1, 100, 'user1', '/share', 31, []],
],
[
// no received share since "user1" is the sharer/owner
@ -265,8 +294,8 @@ class MountProviderTest extends \Test\TestCase {
[
],
[
[1, 100, 'user1', '/share', 31],
[2, 100, 'user1', '/share', 15],
[1, 100, 'user1', '/share', 31, [['scope' => 'permission', 'key' => 'download', 'enabled' => true]]],
[2, 100, 'user1', '/share', 15, [['scope' => 'permission', 'key' => 'download', 'enabled' => false]]],
],
[
// no received share since "user1" is the sharer/owner
@ -277,7 +306,7 @@ class MountProviderTest extends \Test\TestCase {
[
],
[
[1, 100, 'user2', '/share', 0],
[1, 100, 'user2', '/share', 0, []],
],
[
// no received share since "user1" opted out
@ -286,40 +315,40 @@ class MountProviderTest extends \Test\TestCase {
// #7: share as outsider with "group1" and "user1" where recipient renamed in between
[
[
[1, 100, 'user2', '/share2-renamed', 31],
[1, 100, 'user2', '/share2-renamed', 31, []],
],
[
[2, 100, 'user2', '/share2', 31],
[2, 100, 'user2', '/share2', 31, []],
],
[
// use target of least recent share
['1', 100, 'user2', '/share2-renamed', 31],
['1', 100, 'user2', '/share2-renamed', 31, []],
],
],
// #8: share as outsider with "group1" and "user1" where recipient renamed in between
[
[
[2, 100, 'user2', '/share2', 31],
[2, 100, 'user2', '/share2', 31, []],
],
[
[1, 100, 'user2', '/share2-renamed', 31],
[1, 100, 'user2', '/share2-renamed', 31, []],
],
[
// use target of least recent share
['1', 100, 'user2', '/share2-renamed', 31],
['1', 100, 'user2', '/share2-renamed', 31, []],
],
],
// #9: share as outsider with "nullgroup" and "user1" where recipient renamed in between
[
[
[2, 100, 'user2', '/share2', 31],
[2, 100, 'user2', '/share2', 31, []],
],
[
[1, 100, 'nullgroup', '/share2-renamed', 31],
[1, 100, 'nullgroup', '/share2-renamed', 31, []],
],
[
// use target of least recent share
['1', 100, 'nullgroup', '/share2-renamed', 31],
['1', 100, 'nullgroup', '/share2-renamed', 31, []],
],
true
],
@ -343,10 +372,10 @@ class MountProviderTest extends \Test\TestCase {
$userManager = $this->createMock(IUserManager::class);
$userShares = array_map(function ($shareSpec) {
return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4]);
return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4], $shareSpec[5]);
}, $userShares);
$groupShares = array_map(function ($shareSpec) {
return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4]);
return $this->makeMockShare($shareSpec[0], $shareSpec[1], $shareSpec[2], $shareSpec[3], $shareSpec[4], $shareSpec[5]);
}, $groupShares);
$this->user->expects($this->any())
@ -400,6 +429,11 @@ class MountProviderTest extends \Test\TestCase {
$this->assertEquals($expectedShare[2], $share->getShareOwner());
$this->assertEquals($expectedShare[3], $share->getTarget());
$this->assertEquals($expectedShare[4], $share->getPermissions());
if ($expectedShare[5] === null) {
$this->assertNull($share->getAttributes());
} else {
$this->assertEquals($expectedShare[5], $share->getAttributes()->toArray());
}
}
}
}

View file

@ -104,6 +104,7 @@ import escapeHTML from 'escape-html'
Client.PROPERTY_GETCONTENTLENGTH = '{' + Client.NS_DAV + '}getcontentlength'
Client.PROPERTY_ISENCRYPTED = '{' + Client.NS_DAV + '}is-encrypted'
Client.PROPERTY_SHARE_PERMISSIONS = '{' + Client.NS_OCS + '}share-permissions'
Client.PROPERTY_SHARE_ATTRIBUTES = '{' + Client.NS_NEXTCLOUD + '}share-attributes'
Client.PROPERTY_QUOTA_AVAILABLE_BYTES = '{' + Client.NS_DAV + '}quota-available-bytes'
Client.PROTOCOL_HTTP = 'http'
@ -160,6 +161,10 @@ import escapeHTML from 'escape-html'
* Share permissions
*/
[Client.NS_OCS, 'share-permissions'],
/**
* Share attributes
*/
[Client.NS_NEXTCLOUD, 'share-attributes'],
]
/**
@ -416,6 +421,18 @@ import escapeHTML from 'escape-html'
data.sharePermissions = parseInt(sharePermissionsProp)
}
const shareAttributesProp = props[Client.PROPERTY_SHARE_ATTRIBUTES]
if (!_.isUndefined(shareAttributesProp)) {
try {
data.shareAttributes = JSON.parse(shareAttributesProp)
} catch (e) {
console.warn('Could not parse share attributes returned by server: "' + shareAttributesProp + '"')
data.shareAttributes = [];
}
} else {
data.shareAttributes = [];
}
const mounTypeProp = props['{' + Client.NS_NEXTCLOUD + '}mount-type']
if (!_.isUndefined(mounTypeProp)) {
data.mountType = mounTypeProp

View file

@ -155,7 +155,23 @@
*/
sharePermissions: null,
/**
* @type Array
*/
shareAttributes: [],
quotaAvailableBytes: -1,
canDownload: function() {
for (const i in this.shareAttributes) {
const attr = this.shareAttributes[i]
if (attr.scope === 'permissions' && attr.key === 'download') {
return attr.enabled
}
}
return true
},
}
if (!OC.Files) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
/*! For license information please see core-files_fileinfo.js.LICENSE.txt */
!function(i){var t=function(i){var t=this;_.each(i,(function(i,e){_.isFunction(i)||(t[e]=i)})),_.isUndefined(this.id)||(this.id=parseInt(i.id,10)),this.path=i.path||"","dir"===this.type?this.mimetype="httpd/unix-directory":this.mimetype=this.mimetype||"application/octet-stream",this.type||("httpd/unix-directory"===this.mimetype?this.type="dir":this.type="file")};t.prototype={id:null,name:null,path:null,mimetype:null,icon:null,type:null,permissions:null,mtime:null,etag:null,mountType:null,hasPreview:!0,sharePermissions:null,quotaAvailableBytes:-1},i.Files||(i.Files={}),i.Files.FileInfo=t}(OC);
//# sourceMappingURL=core-files_fileinfo.js.map?v=d9cc5e8823977a1e870b
!function(t){var i=function(t){var i=this;_.each(t,(function(t,e){_.isFunction(t)||(i[e]=t)})),_.isUndefined(this.id)||(this.id=parseInt(t.id,10)),this.path=t.path||"","dir"===this.type?this.mimetype="httpd/unix-directory":this.mimetype=this.mimetype||"application/octet-stream",this.type||("httpd/unix-directory"===this.mimetype?this.type="dir":this.type="file")};i.prototype={id:null,name:null,path:null,mimetype:null,icon:null,type:null,permissions:null,mtime:null,etag:null,mountType:null,hasPreview:!0,sharePermissions:null,shareAttributes:[],quotaAvailableBytes:-1,canDownload:function(){for(var t in this.shareAttributes){var i=this.shareAttributes[t];if("permissions"===i.scope&&"download"===i.key)return i.enabled}return!0}},t.Files||(t.Files={}),t.Files.FileInfo=i}(OC);
//# sourceMappingURL=core-files_fileinfo.js.map?v=d5c54f8e5b3834c089a0

View file

@ -1 +1 @@
{"version":3,"file":"core-files_fileinfo.js?v=d9cc5e8823977a1e870b","mappings":";CA0BA,SAAUA,GAUT,IAAMC,EAAW,SAASC,GACzB,IAAMC,EAAOC,KACbC,EAAEC,KAAKJ,GAAM,SAASK,EAAOC,GACvBH,EAAEI,WAAWF,KACjBJ,EAAKK,GAAOD,MAITF,EAAEK,YAAYN,KAAKO,MACvBP,KAAKO,GAAKC,SAASV,EAAKS,GAAI,KAI7BP,KAAKS,KAAOX,EAAKW,MAAQ,GAEP,QAAdT,KAAKU,KACRV,KAAKW,SAAW,uBAEhBX,KAAKW,SAAWX,KAAKW,UAAY,2BAG7BX,KAAKU,OACa,yBAAlBV,KAAKW,SACRX,KAAKU,KAAO,MAEZV,KAAKU,KAAO,SAQfb,EAASe,UAAY,CAMpBL,GAAI,KAOJM,KAAM,KAQNJ,KAAM,KAONE,SAAU,KASVG,KAAM,KAQNJ,KAAM,KAQNK,YAAa,KAObC,MAAO,KAOPC,KAAM,KASNC,UAAW,KAKXC,YAAY,EAKZC,iBAAkB,KAElBC,qBAAsB,GAGlBzB,EAAG0B,QACP1B,EAAG0B,MAAQ,IAEZ1B,EAAG0B,MAAMzB,SAAWA,EAzIrB,CA0IGD","sources":["webpack:///nextcloud/core/src/files/fileinfo.js"],"sourcesContent":["/**\n * Copyright (c) 2015\n *\n * @author Julius Härtl <jus@bitgrid.net>\n * @author Robin Appelman <robin@icewind.nl>\n * @author Roeland Jago Douma <roeland@famdouma.nl>\n * @author Vincent Petry <vincent@nextcloud.com>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n/* eslint-disable */\n(function(OC) {\n\n\t/**\n\t * @class OC.Files.FileInfo\n\t * @classdesc File information\n\t *\n\t * @param {Object} data file data, see attributes for details\n\t *\n\t * @since 8.2\n\t */\n\tconst FileInfo = function(data) {\n\t\tconst self = this\n\t\t_.each(data, function(value, key) {\n\t\t\tif (!_.isFunction(value)) {\n\t\t\t\tself[key] = value\n\t\t\t}\n\t\t})\n\n\t\tif (!_.isUndefined(this.id)) {\n\t\t\tthis.id = parseInt(data.id, 10)\n\t\t}\n\n\t\t// TODO: normalize path\n\t\tthis.path = data.path || ''\n\n\t\tif (this.type === 'dir') {\n\t\t\tthis.mimetype = 'httpd/unix-directory'\n\t\t} else {\n\t\t\tthis.mimetype = this.mimetype || 'application/octet-stream'\n\t\t}\n\n\t\tif (!this.type) {\n\t\t\tif (this.mimetype === 'httpd/unix-directory') {\n\t\t\t\tthis.type = 'dir'\n\t\t\t} else {\n\t\t\t\tthis.type = 'file'\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @memberof OC.Files\n\t */\n\tFileInfo.prototype = {\n\t\t/**\n\t\t * File id\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tid: null,\n\n\t\t/**\n\t\t * File name\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tname: null,\n\n\t\t/**\n\t\t * Path leading to the file, without the file name,\n\t\t * and with a leading slash.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tpath: null,\n\n\t\t/**\n\t\t * Mime type\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tmimetype: null,\n\n\t\t/**\n\t\t * Icon URL.\n\t\t *\n\t\t * Can be used to override the mime type icon.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\ticon: null,\n\n\t\t/**\n\t\t * File type. 'file' for files, 'dir' for directories.\n\t\t *\n\t\t * @type String\n\t\t * @deprecated rely on mimetype instead\n\t\t */\n\t\ttype: null,\n\n\t\t/**\n\t\t * Permissions.\n\t\t *\n\t\t * @see OC#PERMISSION_ALL for permissions\n\t\t * @type int\n\t\t */\n\t\tpermissions: null,\n\n\t\t/**\n\t\t * Modification time\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tmtime: null,\n\n\t\t/**\n\t\t * Etag\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tetag: null,\n\n\t\t/**\n\t\t * Mount type.\n\t\t *\n\t\t * One of null, \"external-root\", \"shared\" or \"shared-root\"\n\t\t *\n\t\t * @type string\n\t\t */\n\t\tmountType: null,\n\n\t\t/**\n\t\t * @type boolean\n\t\t */\n\t\thasPreview: true,\n\n\t\t/**\n\t\t * @type int\n\t\t */\n\t\tsharePermissions: null,\n\n\t\tquotaAvailableBytes: -1,\n\t}\n\n\tif (!OC.Files) {\n\t\tOC.Files = {}\n\t}\n\tOC.Files.FileInfo = FileInfo\n})(OC)\n"],"names":["OC","FileInfo","data","self","this","_","each","value","key","isFunction","isUndefined","id","parseInt","path","type","mimetype","prototype","name","icon","permissions","mtime","etag","mountType","hasPreview","sharePermissions","quotaAvailableBytes","Files"],"sourceRoot":""}
{"version":3,"file":"core-files_fileinfo.js?v=d5c54f8e5b3834c089a0","mappings":";CA0BA,SAAUA,GAUT,IAAMC,EAAW,SAASC,GACzB,IAAMC,EAAOC,KACbC,EAAEC,KAAKJ,GAAM,SAASK,EAAOC,GACvBH,EAAEI,WAAWF,KACjBJ,EAAKK,GAAOD,MAITF,EAAEK,YAAYN,KAAKO,MACvBP,KAAKO,GAAKC,SAASV,EAAKS,GAAI,KAI7BP,KAAKS,KAAOX,EAAKW,MAAQ,GAEP,QAAdT,KAAKU,KACRV,KAAKW,SAAW,uBAEhBX,KAAKW,SAAWX,KAAKW,UAAY,2BAG7BX,KAAKU,OACa,yBAAlBV,KAAKW,SACRX,KAAKU,KAAO,MAEZV,KAAKU,KAAO,SAQfb,EAASe,UAAY,CAMpBL,GAAI,KAOJM,KAAM,KAQNJ,KAAM,KAONE,SAAU,KASVG,KAAM,KAQNJ,KAAM,KAQNK,YAAa,KAObC,MAAO,KAOPC,KAAM,KASNC,UAAW,KAKXC,YAAY,EAKZC,iBAAkB,KAKlBC,gBAAiB,GAEjBC,qBAAsB,EAEtBC,YAAa,WACZ,IAAK,IAAMC,KAAKxB,KAAKqB,gBAAiB,CACrC,IAAMI,EAAOzB,KAAKqB,gBAAgBG,GAClC,GAAmB,gBAAfC,EAAKC,OAAwC,aAAbD,EAAKrB,IACxC,OAAOqB,EAAKE,QAId,OAAO,IAIJ/B,EAAGgC,QACPhC,EAAGgC,MAAQ,IAEZhC,EAAGgC,MAAM/B,SAAWA,EAzJrB,CA0JGD","sources":["webpack:///nextcloud/core/src/files/fileinfo.js"],"sourcesContent":["/**\n * Copyright (c) 2015\n *\n * @author Julius Härtl <jus@bitgrid.net>\n * @author Robin Appelman <robin@icewind.nl>\n * @author Roeland Jago Douma <roeland@famdouma.nl>\n * @author Vincent Petry <vincent@nextcloud.com>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n/* eslint-disable */\n(function(OC) {\n\n\t/**\n\t * @class OC.Files.FileInfo\n\t * @classdesc File information\n\t *\n\t * @param {Object} data file data, see attributes for details\n\t *\n\t * @since 8.2\n\t */\n\tconst FileInfo = function(data) {\n\t\tconst self = this\n\t\t_.each(data, function(value, key) {\n\t\t\tif (!_.isFunction(value)) {\n\t\t\t\tself[key] = value\n\t\t\t}\n\t\t})\n\n\t\tif (!_.isUndefined(this.id)) {\n\t\t\tthis.id = parseInt(data.id, 10)\n\t\t}\n\n\t\t// TODO: normalize path\n\t\tthis.path = data.path || ''\n\n\t\tif (this.type === 'dir') {\n\t\t\tthis.mimetype = 'httpd/unix-directory'\n\t\t} else {\n\t\t\tthis.mimetype = this.mimetype || 'application/octet-stream'\n\t\t}\n\n\t\tif (!this.type) {\n\t\t\tif (this.mimetype === 'httpd/unix-directory') {\n\t\t\t\tthis.type = 'dir'\n\t\t\t} else {\n\t\t\t\tthis.type = 'file'\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @memberof OC.Files\n\t */\n\tFileInfo.prototype = {\n\t\t/**\n\t\t * File id\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tid: null,\n\n\t\t/**\n\t\t * File name\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tname: null,\n\n\t\t/**\n\t\t * Path leading to the file, without the file name,\n\t\t * and with a leading slash.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tpath: null,\n\n\t\t/**\n\t\t * Mime type\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tmimetype: null,\n\n\t\t/**\n\t\t * Icon URL.\n\t\t *\n\t\t * Can be used to override the mime type icon.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\ticon: null,\n\n\t\t/**\n\t\t * File type. 'file' for files, 'dir' for directories.\n\t\t *\n\t\t * @type String\n\t\t * @deprecated rely on mimetype instead\n\t\t */\n\t\ttype: null,\n\n\t\t/**\n\t\t * Permissions.\n\t\t *\n\t\t * @see OC#PERMISSION_ALL for permissions\n\t\t * @type int\n\t\t */\n\t\tpermissions: null,\n\n\t\t/**\n\t\t * Modification time\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tmtime: null,\n\n\t\t/**\n\t\t * Etag\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tetag: null,\n\n\t\t/**\n\t\t * Mount type.\n\t\t *\n\t\t * One of null, \"external-root\", \"shared\" or \"shared-root\"\n\t\t *\n\t\t * @type string\n\t\t */\n\t\tmountType: null,\n\n\t\t/**\n\t\t * @type boolean\n\t\t */\n\t\thasPreview: true,\n\n\t\t/**\n\t\t * @type int\n\t\t */\n\t\tsharePermissions: null,\n\n\t\t/**\n\t\t * @type Array\n\t\t */\n\t\tshareAttributes: [],\n\n\t\tquotaAvailableBytes: -1,\n\n\t\tcanDownload: function() {\n\t\t\tfor (const i in this.shareAttributes) {\n\t\t\t\tconst attr = this.shareAttributes[i]\n\t\t\t\tif (attr.scope === 'permissions' && attr.key === 'download') {\n\t\t\t\t\treturn attr.enabled\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t}\n\n\tif (!OC.Files) {\n\t\tOC.Files = {}\n\t}\n\tOC.Files.FileInfo = FileInfo\n})(OC)\n"],"names":["OC","FileInfo","data","self","this","_","each","value","key","isFunction","isUndefined","id","parseInt","path","type","mimetype","prototype","name","icon","permissions","mtime","etag","mountType","hasPreview","sharePermissions","shareAttributes","quotaAvailableBytes","canDownload","i","attr","scope","enabled","Files"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -266,8 +266,10 @@ return array(
'OCP\\Files\\Config\\IUserMountCache' => $baseDir . '/lib/public/Files/Config/IUserMountCache.php',
'OCP\\Files\\EmptyFileNameException' => $baseDir . '/lib/public/Files/EmptyFileNameException.php',
'OCP\\Files\\EntityTooLargeException' => $baseDir . '/lib/public/Files/EntityTooLargeException.php',
'OCP\\Files\\Events\\BeforeDirectFileDownloadEvent' => $baseDir . '/lib/public/Files/Events/BeforeDirectFileDownloadEvent.php',
'OCP\\Files\\Events\\BeforeFileScannedEvent' => $baseDir . '/lib/public/Files/Events/BeforeFileScannedEvent.php',
'OCP\\Files\\Events\\BeforeFolderScannedEvent' => $baseDir . '/lib/public/Files/Events/BeforeFolderScannedEvent.php',
'OCP\\Files\\Events\\BeforeZipCreatedEvent' => $baseDir . '/lib/public/Files/Events/BeforeZipCreatedEvent.php',
'OCP\\Files\\Events\\FileCacheUpdated' => $baseDir . '/lib/public/Files/Events/FileCacheUpdated.php',
'OCP\\Files\\Events\\FileScannedEvent' => $baseDir . '/lib/public/Files/Events/FileScannedEvent.php',
'OCP\\Files\\Events\\FolderScannedEvent' => $baseDir . '/lib/public/Files/Events/FolderScannedEvent.php',
@ -539,6 +541,7 @@ return array(
'OCP\\Share\\Exceptions\\GenericShareException' => $baseDir . '/lib/public/Share/Exceptions/GenericShareException.php',
'OCP\\Share\\Exceptions\\IllegalIDChangeException' => $baseDir . '/lib/public/Share/Exceptions/IllegalIDChangeException.php',
'OCP\\Share\\Exceptions\\ShareNotFound' => $baseDir . '/lib/public/Share/Exceptions/ShareNotFound.php',
'OCP\\Share\\IAttributes' => $baseDir . '/lib/public/Share/IAttributes.php',
'OCP\\Share\\IManager' => $baseDir . '/lib/public/Share/IManager.php',
'OCP\\Share\\IProviderFactory' => $baseDir . '/lib/public/Share/IProviderFactory.php',
'OCP\\Share\\IShare' => $baseDir . '/lib/public/Share/IShare.php',
@ -1512,6 +1515,7 @@ return array(
'OC\\Share20\\Manager' => $baseDir . '/lib/private/Share20/Manager.php',
'OC\\Share20\\ProviderFactory' => $baseDir . '/lib/private/Share20/ProviderFactory.php',
'OC\\Share20\\Share' => $baseDir . '/lib/private/Share20/Share.php',
'OC\\Share20\\ShareAttributes' => $baseDir . '/lib/private/Share20/ShareAttributes.php',
'OC\\Share20\\ShareHelper' => $baseDir . '/lib/private/Share20/ShareHelper.php',
'OC\\Share20\\UserRemovedListener' => $baseDir . '/lib/private/Share20/UserRemovedListener.php',
'OC\\Share\\Constants' => $baseDir . '/lib/private/Share/Constants.php',

View file

@ -299,8 +299,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\Config\\IUserMountCache' => __DIR__ . '/../../..' . '/lib/public/Files/Config/IUserMountCache.php',
'OCP\\Files\\EmptyFileNameException' => __DIR__ . '/../../..' . '/lib/public/Files/EmptyFileNameException.php',
'OCP\\Files\\EntityTooLargeException' => __DIR__ . '/../../..' . '/lib/public/Files/EntityTooLargeException.php',
'OCP\\Files\\Events\\BeforeDirectFileDownloadEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/BeforeDirectFileDownloadEvent.php',
'OCP\\Files\\Events\\BeforeFileScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/BeforeFileScannedEvent.php',
'OCP\\Files\\Events\\BeforeFolderScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/BeforeFolderScannedEvent.php',
'OCP\\Files\\Events\\BeforeZipCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/BeforeZipCreatedEvent.php',
'OCP\\Files\\Events\\FileCacheUpdated' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FileCacheUpdated.php',
'OCP\\Files\\Events\\FileScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FileScannedEvent.php',
'OCP\\Files\\Events\\FolderScannedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/FolderScannedEvent.php',
@ -572,6 +574,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Share\\Exceptions\\GenericShareException' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/GenericShareException.php',
'OCP\\Share\\Exceptions\\IllegalIDChangeException' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/IllegalIDChangeException.php',
'OCP\\Share\\Exceptions\\ShareNotFound' => __DIR__ . '/../../..' . '/lib/public/Share/Exceptions/ShareNotFound.php',
'OCP\\Share\\IAttributes' => __DIR__ . '/../../..' . '/lib/public/Share/IAttributes.php',
'OCP\\Share\\IManager' => __DIR__ . '/../../..' . '/lib/public/Share/IManager.php',
'OCP\\Share\\IProviderFactory' => __DIR__ . '/../../..' . '/lib/public/Share/IProviderFactory.php',
'OCP\\Share\\IShare' => __DIR__ . '/../../..' . '/lib/public/Share/IShare.php',
@ -1545,6 +1548,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Share20\\Manager' => __DIR__ . '/../../..' . '/lib/private/Share20/Manager.php',
'OC\\Share20\\ProviderFactory' => __DIR__ . '/../../..' . '/lib/private/Share20/ProviderFactory.php',
'OC\\Share20\\Share' => __DIR__ . '/../../..' . '/lib/private/Share20/Share.php',
'OC\\Share20\\ShareAttributes' => __DIR__ . '/../../..' . '/lib/private/Share20/ShareAttributes.php',
'OC\\Share20\\ShareHelper' => __DIR__ . '/../../..' . '/lib/private/Share20/ShareHelper.php',
'OC\\Share20\\UserRemovedListener' => __DIR__ . '/../../..' . '/lib/private/Share20/UserRemovedListener.php',
'OC\\Share\\Constants' => __DIR__ . '/../../..' . '/lib/private/Share/Constants.php',

View file

@ -52,6 +52,7 @@ use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
use OCP\Share\IShareProvider;
@ -174,6 +175,8 @@ class DefaultShareProvider implements IShareProvider {
if (method_exists($share, 'getParent')) {
$qb->setValue('parent', $qb->createNamedParameter($share->getParent()));
}
$qb->setValue('hide_download', $qb->createNamedParameter($share->getHideDownload() ? 1 : 0, IQueryBuilder::PARAM_INT));
} else {
throw new \Exception('invalid share type!');
}
@ -193,6 +196,12 @@ class DefaultShareProvider implements IShareProvider {
// set the permissions
$qb->setValue('permissions', $qb->createNamedParameter($share->getPermissions()));
// set share attributes
$shareAttributes = $this->formatShareAttributes(
$share->getAttributes()
);
$qb->setValue('attributes', $qb->createNamedParameter($shareAttributes));
// Set who created this share
$qb->setValue('uid_initiator', $qb->createNamedParameter($share->getSharedBy()));
@ -248,6 +257,8 @@ class DefaultShareProvider implements IShareProvider {
public function update(\OCP\Share\IShare $share) {
$originalShare = $this->getShareById($share->getId());
$shareAttributes = $this->formatShareAttributes($share->getAttributes());
if ($share->getShareType() === IShare::TYPE_USER) {
/*
* We allow updating the recipient on user shares.
@ -259,6 +270,7 @@ class DefaultShareProvider implements IShareProvider {
->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->set('attributes', $qb->createNamedParameter($shareAttributes))
->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
@ -272,6 +284,7 @@ class DefaultShareProvider implements IShareProvider {
->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->set('attributes', $qb->createNamedParameter($shareAttributes))
->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
@ -301,6 +314,7 @@ class DefaultShareProvider implements IShareProvider {
->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0)))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->set('attributes', $qb->createNamedParameter($shareAttributes))
->execute();
} elseif ($share->getShareType() === IShare::TYPE_LINK) {
$qb = $this->dbConn->getQueryBuilder();
@ -311,6 +325,7 @@ class DefaultShareProvider implements IShareProvider {
->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->set('attributes', $qb->createNamedParameter($shareAttributes))
->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
->set('token', $qb->createNamedParameter($share->getToken()))
@ -611,6 +626,10 @@ class DefaultShareProvider implements IShareProvider {
$data = $stmt->fetch();
$stmt->closeCursor();
$shareAttributes = $this->formatShareAttributes(
$share->getAttributes()
);
if ($data === false) {
// No usergroup share yet. Create one.
$qb = $this->dbConn->getQueryBuilder();
@ -626,6 +645,7 @@ class DefaultShareProvider implements IShareProvider {
'file_source' => $qb->createNamedParameter($share->getNodeId()),
'file_target' => $qb->createNamedParameter($share->getTarget()),
'permissions' => $qb->createNamedParameter($share->getPermissions()),
'attributes' => $qb->createNamedParameter($shareAttributes),
'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()),
])->execute();
} else {
@ -1073,6 +1093,8 @@ class DefaultShareProvider implements IShareProvider {
$share->setToken($data['token']);
}
$share = $this->updateShareAttributes($share, $data['attributes']);
$share->setSharedBy($data['uid_initiator']);
$share->setShareOwner($data['uid_owner']);
@ -1540,4 +1562,48 @@ class DefaultShareProvider implements IShareProvider {
}
$cursor->closeCursor();
}
/**
* Load from database format (JSON string) to IAttributes
*
* @return IShare the modified share
*/
private function updateShareAttributes(IShare $share, ?string $data): IShare {
if ($data !== null && $data !== '') {
$attributes = new ShareAttributes();
$compressedAttributes = \json_decode($data, true);
if ($compressedAttributes === false || $compressedAttributes === null) {
return $share;
}
foreach ($compressedAttributes as $compressedAttribute) {
$attributes->setAttribute(
$compressedAttribute[0],
$compressedAttribute[1],
$compressedAttribute[2]
);
}
$share->setAttributes($attributes);
}
return $share;
}
/**
* Format IAttributes to database format (JSON string)
*/
private function formatShareAttributes(?IAttributes $attributes): ?string {
if ($attributes === null || empty($attributes->toArray())) {
return null;
}
$compressedAttributes = [];
foreach ($attributes->toArray() as $attribute) {
$compressedAttributes[] = [
0 => $attribute['scope'],
1 => $attribute['key'],
2 => $attribute['enabled']
];
}
return \json_encode($compressedAttributes);
}
}

View file

@ -1093,6 +1093,7 @@ class Manager implements IManager {
'shareWith' => $share->getSharedWith(),
'uidOwner' => $share->getSharedBy(),
'permissions' => $share->getPermissions(),
'attributes' => $share->getAttributes() !== null ? $share->getAttributes()->toArray() : null,
'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
]);
}

View file

@ -37,6 +37,7 @@ use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IUserManager;
use OCP\Share\Exceptions\IllegalIDChangeException;
use OCP\Share\IAttributes;
use OCP\Share\IShare;
class Share implements IShare {
@ -65,6 +66,8 @@ class Share implements IShare {
private $shareOwner;
/** @var int */
private $permissions;
/** @var IAttributes */
private $attributes;
/** @var int */
private $status;
/** @var string */
@ -332,6 +335,28 @@ class Share implements IShare {
return $this->permissions;
}
/**
* @inheritdoc
*/
public function newAttributes(): IAttributes {
return new ShareAttributes();
}
/**
* @inheritdoc
*/
public function setAttributes(?IAttributes $attributes) {
$this->attributes = $attributes;
return $this;
}
/**
* @inheritdoc
*/
public function getAttributes(): ?IAttributes {
return $this->attributes;
}
/**
* @inheritdoc
*/
@ -511,7 +536,7 @@ class Share implements IShare {
* Set the parent of this share
*
* @param int parent
* @return \OCP\Share\IShare
* @return IShare
* @deprecated The new shares do not have parents. This is just here for legacy reasons.
*/
public function setParent($parent) {

View file

@ -0,0 +1,73 @@
<?php
/**
* @author Piotr Mrowczynski <piotr@owncloud.com>
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @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 OC\Share20;
use OCP\Share\IAttributes;
class ShareAttributes implements IAttributes {
/** @var array */
private $attributes;
public function __construct() {
$this->attributes = [];
}
/**
* @inheritdoc
*/
public function setAttribute($scope, $key, $enabled) {
if (!\array_key_exists($scope, $this->attributes)) {
$this->attributes[$scope] = [];
}
$this->attributes[$scope][$key] = $enabled;
return $this;
}
/**
* @inheritdoc
*/
public function getAttribute($scope, $key) {
if (\array_key_exists($scope, $this->attributes) &&
\array_key_exists($key, $this->attributes[$scope])) {
return $this->attributes[$scope][$key];
}
return null;
}
/**
* @inheritdoc
*/
public function toArray() {
$result = [];
foreach ($this->attributes as $scope => $keys) {
foreach ($keys as $key => $enabled) {
$result[] = [
"scope" => $scope,
"key" => $key,
"enabled" => $enabled
];
}
}
return $result;
}
}

View file

@ -44,10 +44,12 @@ use bantu\IniGetWrapper\IniGetWrapper;
use OC\Files\View;
use OC\Streamer;
use OCP\Lock\ILockingProvider;
use OCP\Files\Events\BeforeZipCreatedEvent;
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\EventDispatcher\IEventDispatcher;
/**
* Class for file server access
*
*/
class OC_Files {
public const FILE = 1;
@ -167,6 +169,14 @@ class OC_Files {
}
}
//Dispatch an event to see if any apps have problem with download
$event = new BeforeZipCreatedEvent($dir, is_array($files) ? $files : [$files]);
$dispatcher = \OCP\Server::get(IEventDispatcher::class);
$dispatcher->dispatchTyped($event);
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
throw new \OC\ForbiddenException($event->getErrorMessage());
}
$streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles);
OC_Util::obEnd();
@ -222,13 +232,16 @@ class OC_Files {
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
OC::$server->getLogger()->logException($ex);
$l = \OC::$server->getL10N('lib');
\OC_Template::printErrorPage($l->t('Cannot read file'), $ex->getMessage(), 200);
\OC_Template::printErrorPage($l->t('Cannot download file'), $ex->getMessage(), 200);
} catch (\Exception $ex) {
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
OC::$server->getLogger()->logException($ex);
$l = \OC::$server->getL10N('lib');
$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
\OC_Template::printErrorPage($l->t('Cannot read file'), $hint, 200);
if ($event && $event->getErrorMessage() !== null) {
$hint .= ' ' . $event->getErrorMessage();
}
\OC_Template::printErrorPage($l->t('Cannot download file'), $hint, 200);
}
}
@ -287,6 +300,7 @@ class OC_Files {
* @param string $name
* @param string $dir
* @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
* @throws \OC\ForbiddenException
*/
private static function getSingleFile($view, $dir, $name, $params) {
$filename = $dir . '/' . $name;
@ -322,6 +336,19 @@ class OC_Files {
$rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize);
}
$dispatcher = \OC::$server->query(IEventDispatcher::class);
$event = new BeforeDirectFileDownloadEvent($filename);
$dispatcher->dispatchTyped($event);
if (!\OC\Files\Filesystem::isReadable($filename) || $event->getErrorMessage()) {
if ($event->getErrorMessage()) {
$msg = $event->getErrorMessage();
} else {
$msg = 'Access denied';
}
throw new \OC\ForbiddenException($msg);
}
self::sendHeaders($filename, $name, $rangeArray);
if (isset($params['head']) && $params['head']) {

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\Files\Events;
use OCP\EventDispatcher\Event;
/**
* This event is triggered when a user tries to download a file
* directly.
*
* @since 25.0.0
*/
class BeforeDirectFileDownloadEvent extends Event {
private string $path;
private bool $successful = true;
private ?string $errorMessage = null;
/**
* @since 25.0.0
*/
public function __construct(string $path) {
parent::__construct();
$this->path = $path;
}
/**
* @since 25.0.0
*/
public function getPath(): string {
return $this->path;
}
/**
* @since 25.0.0
*/
public function isSuccessful(): bool {
return $this->successful;
}
/**
* Set if the event was successful
*
* @since 25.0.0
*/
public function setSuccessful(bool $successful): void {
$this->successful = $successful;
}
/**
* Get the error message, if any
* @since 25.0.0
*/
public function getErrorMessage(): ?string {
return $this->errorMessage;
}
/**
* @since 25.0.0
*/
public function setErrorMessage(string $errorMessage): void {
$this->errorMessage = $errorMessage;
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\Files\Events;
use OCP\EventDispatcher\Event;
/**
* @since 25.0.0
*/
class BeforeZipCreatedEvent extends Event {
private string $directory;
private array $files;
private bool $successful = true;
private ?string $errorMessage = null;
/**
* @since 25.0.0
*/
public function __construct(string $directory, array $files) {
parent::__construct();
$this->directory = $directory;
$this->files = $files;
}
/**
* @since 25.0.0
*/
public function getDirectory(): string {
return $this->directory;
}
/**
* @since 25.0.0
*/
public function getFiles(): array {
return $this->files;
}
/**
* @since 25.0.0
*/
public function isSuccessful(): bool {
return $this->successful;
}
/**
* Set if the event was successful
*
* @since 25.0.0
*/
public function setSuccessful(bool $successful): void {
$this->successful = $successful;
}
/**
* Get the error message, if any
* @since 25.0.0
*/
public function getErrorMessage(): ?string {
return $this->errorMessage;
}
/**
* @since 25.0.0
*/
public function setErrorMessage(string $errorMessage): void {
$this->errorMessage = $errorMessage;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @author Piotr Mrowczynski <piotr@owncloud.com>
*
* @copyright Copyright (c) 2019, ownCloud GmbH
* @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 OCP\Share;
/**
* Interface IAttributes
*
* @package OCP\Share
* @since 25.0.0
*/
interface IAttributes {
/**
* Sets an attribute enabled/disabled. If the key did not exist before it will be created.
*
* @param string $scope scope
* @param string $key key
* @param bool $enabled enabled
* @return IAttributes The modified object
* @since 25.0.0
*/
public function setAttribute($scope, $key, $enabled);
/**
* Returns if attribute is enabled/disabled for given scope id and key.
* If attribute does not exist, returns null
*
* @param string $scope scope
* @param string $key key
* @return bool|null
* @since 25.0.0
*/
public function getAttribute($scope, $key);
/**
* Formats the IAttributes object to array with the following format:
* [
* 0 => [
* "scope" => <string>,
* "key" => <string>,
* "enabled" => <bool>
* ],
* ...
* ]
*
* @return array formatted IAttributes
* @since 25.0.0
*/
public function toArray();
}

View file

@ -36,7 +36,9 @@ use OCP\Files\NotFoundException;
use OCP\Share\Exceptions\IllegalIDChangeException;
/**
* Interface IShare
* This interface allows to represent a share object.
*
* This interface must not be implemented in your application.
*
* @since 9.0.0
*/
@ -300,7 +302,7 @@ interface IShare {
* See \OCP\Constants::PERMISSION_*
*
* @param int $permissions
* @return \OCP\Share\IShare The modified object
* @return IShare The modified object
* @since 9.0.0
*/
public function setPermissions($permissions);
@ -314,6 +316,31 @@ interface IShare {
*/
public function getPermissions();
/**
* Create share attributes object
*
* @since 25.0.0
* @return IAttributes
*/
public function newAttributes(): IAttributes;
/**
* Set share attributes
*
* @param ?IAttributes $attributes
* @since 25.0.0
* @return IShare The modified object
*/
public function setAttributes(?IAttributes $attributes);
/**
* Get share attributes
*
* @since 25.0.0
* @return ?IAttributes
*/
public function getAttributes(): ?IAttributes;
/**
* Set the accepted status
* See self::STATUS_*

View file

@ -23,6 +23,7 @@
namespace Test\Share20;
use OC\Share20\DefaultShareProvider;
use OC\Share20\ShareAttributes;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Defaults;
use OCP\Files\File;
@ -703,6 +704,11 @@ class DefaultShareProviderTest extends \Test\TestCase {
$share->setSharedWithDisplayName('Displayed Name');
$share->setSharedWithAvatar('/path/to/image.svg');
$share->setPermissions(1);
$attrs = new ShareAttributes();
$attrs->setAttribute('permissions', 'download', true);
$share->setAttributes($attrs);
$share->setTarget('/target');
$share2 = $this->provider->create($share);
@ -723,6 +729,17 @@ class DefaultShareProviderTest extends \Test\TestCase {
$this->assertSame('/path/to/image.svg', $share->getSharedWithAvatar());
$this->assertSame(null, $share2->getSharedWithDisplayName());
$this->assertSame(null, $share2->getSharedWithAvatar());
$this->assertSame(
[
[
'scope' => 'permissions',
'key' => 'download',
'enabled' => true
]
],
$share->getAttributes()->toArray()
);
}
public function testCreateGroupShare() {
@ -760,6 +777,9 @@ class DefaultShareProviderTest extends \Test\TestCase {
$share->setSharedWithDisplayName('Displayed Name');
$share->setSharedWithAvatar('/path/to/image.svg');
$share->setTarget('/target');
$attrs = new ShareAttributes();
$attrs->setAttribute('permissions', 'download', true);
$share->setAttributes($attrs);
$share2 = $this->provider->create($share);
@ -779,6 +799,17 @@ class DefaultShareProviderTest extends \Test\TestCase {
$this->assertSame('/path/to/image.svg', $share->getSharedWithAvatar());
$this->assertSame(null, $share2->getSharedWithDisplayName());
$this->assertSame(null, $share2->getSharedWithAvatar());
$this->assertSame(
[
[
'scope' => 'permissions',
'key' => 'download',
'enabled' => true
]
],
$share->getAttributes()->toArray()
);
}
public function testCreateLinkShare() {

View file

@ -593,7 +593,7 @@ class ManagerTest extends \Test\TestCase {
}
public function createShare($id, $type, $path, $sharedWith, $sharedBy, $shareOwner,
$permissions, $expireDate = null, $password = null) {
$permissions, $expireDate = null, $password = null, $attributes = null) {
$share = $this->createMock(IShare::class);
$share->method('getShareType')->willReturn($type);
@ -602,6 +602,7 @@ class ManagerTest extends \Test\TestCase {
$share->method('getShareOwner')->willReturn($shareOwner);
$share->method('getNode')->willReturn($path);
$share->method('getPermissions')->willReturn($permissions);
$share->method('getAttributes')->willReturn($attributes);
$share->method('getExpirationDate')->willReturn($expireDate);
$share->method('getPassword')->willReturn($password);
@ -3039,6 +3040,8 @@ class ManagerTest extends \Test\TestCase {
$manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare);
$share = $this->manager->newShare();
$attrs = $this->manager->newShare()->newAttributes();
$attrs->setAttribute('app1', 'perm1', true);
$share->setProviderId('foo')
->setId('42')
->setShareType(IShare::TYPE_USER);
@ -3129,6 +3132,8 @@ class ManagerTest extends \Test\TestCase {
$manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare);
$share = $this->manager->newShare();
$attrs = $this->manager->newShare()->newAttributes();
$attrs->setAttribute('app1', 'perm1', true);
$share->setProviderId('foo')
->setId('42')
->setShareType(IShare::TYPE_USER)
@ -3136,6 +3141,7 @@ class ManagerTest extends \Test\TestCase {
->setShareOwner('newUser')
->setSharedBy('sharer')
->setPermissions(31)
->setAttributes($attrs)
->setNode($node);
$this->defaultProvider->expects($this->once())
@ -3160,6 +3166,7 @@ class ManagerTest extends \Test\TestCase {
'uidOwner' => 'sharer',
'permissions' => 31,
'path' => '/myPath',
'attributes' => $attrs->toArray(),
]);
$manager->updateShare($share);