diff options
author | Vincent Petry <vincent@nextcloud.com> | 2022-05-18 14:54:27 +0200 |
---|---|---|
committer | Carl Schwan <carl@carlschwan.eu> | 2022-07-28 16:53:22 +0200 |
commit | a95c19e14b5a371240392de480278ee97c01ab12 (patch) | |
tree | c96d6efaa88d234cdc3393e5004fd27cfc174ebe | |
parent | ee23f41abe2fd53d00f44d9c16ebd722ac93e9a3 (diff) | |
download | nextcloud-server-a95c19e14b5a371240392de480278ee97c01ab12.tar.gz nextcloud-server-a95c19e14b5a371240392de480278ee97c01ab12.zip |
Add share attributes + prevent download permission
Makes it possible to store download permission
Signed-off-by: Vincent Petry <vincent@nextcloud.com>
28 files changed, 1189 insertions, 51 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index b01ae68e43a..d3290c4e792 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 4c9a1dcc793..4d425f70f3b 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -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', diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 8f1f710ca5e..5b9471d537c 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -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( + \OC::$server->getLogger() + )); + if ($this->userSession->isLoggedIn()) { $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php new file mode 100644 index 00000000000..b6cd85a69a0 --- /dev/null +++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php @@ -0,0 +1,113 @@ +<?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 OCP\ILogger; +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 { + + /** @var Server $server */ + private $server; + + /** @var ILogger $logger */ + private $logger; + + /** + * @param ILogger $logger + */ + public function __construct(ILogger $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. + * + * @param Server $server + * @return void + */ + public function initialize(Server $server) { + $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 + * + * @param RequestInterface $request request object + * @return boolean + * @throws Forbidden + * @throws NotFoundException + */ + public function checkViewOnly( + RequestInterface $request + ) { + $path = $request->getPath(); + + try { + $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(); + // using string as we have no guarantee that "files_sharing" app is loaded + if (!$storage->instanceOfStorage('OCA\Files_Sharing\SharedStorage')) { + return true; + } + // Extract extra permissions + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $share = $storage->getShare(); + + // Check if read-only and on whether permission can download is both set and disabled. + $canDownload = $share->getAttributes()->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()); + } + + return true; + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 5b532465aba..5c83e5c2963 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -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( + \OC::$server->getLogger() + )); + if (BrowserErrorPagePlugin::isBrowserRequest($request)) { $this->server->addPlugin(new BrowserErrorPagePlugin()); } diff --git a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php new file mode 100644 index 00000000000..59a557d7006 --- /dev/null +++ b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php @@ -0,0 +1,118 @@ +<?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\ILogger; +use OCP\Share\IAttributes; +use OCP\Share\IShare; +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 { + + /** @var ViewOnlyPlugin */ + private $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(ILogger::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() { + $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() { + $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() { + 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($nodeInfo, $attrEnabled, $expectCanDownloadFile) { + $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); + } +} diff --git a/apps/files/js/fileinfomodel.js b/apps/files/js/fileinfomodel.js index 8e7b399544c..83a8c62592b 100644 --- a/apps/files/js/fileinfomodel.js +++ b/apps/files/js/fileinfomodel.js @@ -84,6 +84,15 @@ }, /** + * 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 * @return ajax call object diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index 2810910c8c9..e4a493cadfb 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -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', ); diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 70149b1cdc0..3c92a46fc82 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -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) diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 6f1d72f9115..fef579a11c0 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -52,14 +52,17 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadSidebar; 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\IRootFolder; use OCP\Group\Events\UserAddedEvent; use OCP\IDBConnection; use OCP\IGroup; @@ -71,7 +74,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 +110,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(); @@ -139,18 +143,76 @@ 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 registerDownloadEvents( + IEventDispatcher $dispatcher, + ?IUserSession $userSession, + IRootFolder $rootFolder + ) { + + $dispatcher->addListener( + 'file.beforeGetDirect', + function (GenericEvent $event) use ($userSession, $rootFolder) { + $pathsToCheck[] = $event->getArgument('path'); + + // Check only for user/group shares. Don't restrict e.g. share links + if ($userSession && $userSession->isLoggedIn()) { + $uid = $userSession->getUser()->getUID(); + $viewOnlyHandler = new ViewOnly( + $rootFolder->getUserFolder($uid) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setArgument('errorMessage', 'Access to this resource or one of its sub-items has been denied.'); + } + } + } + ); + + $dispatcher->addListener( + 'file.beforeCreateZip', + function (GenericEvent $event) use ($userSession, $rootFolder) { + $dir = $event->getArgument('dir'); + $files = $event->getArgument('files'); + + $pathsToCheck = []; + if (\is_array($files)) { + foreach ($files as $file) { + $pathsToCheck[] = $dir . '/' . $file; + } + } elseif (\is_string($files)) { + $pathsToCheck[] = $dir . '/' . $files; + } + + // Check only for user/group shares. Don't restrict e.g. share links + if ($userSession && $userSession->isLoggedIn()) { + $uid = $userSession->getUser()->getUID(); + $viewOnlyHandler = new ViewOnly( + $rootFolder->getUserFolder($uid) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setArgument('errorMessage', 'Access to this resource or one of its sub-items has been denied.'); + $event->setArgument('run', false); + } else { + $event->setArgument('run', true); + } + } else { + $event->setArgument('run', true); + } + } + ); + } + public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession) { if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) { return; diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index fafdb1a64cd..e40aed0da70 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -45,6 +45,7 @@ declare(strict_types=1); namespace OCA\Files_Sharing\Controller; use OC\Files\FileInfo; +use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; use OCA\Files\Helper; @@ -324,6 +325,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; } @@ -674,6 +680,8 @@ class ShareAPIController extends OCSController { $share->setNote($note); } + $share = $this->setShareAttributes($share, $this->request->getParam('attributes', null)); + try { $share = $this->shareManager->createShare($share); } catch (GenericShareException $e) { @@ -1216,6 +1224,8 @@ class ShareAPIController extends OCSController { } } + $share = $this->setShareAttributes($share, $this->request->getParam('attributes', null)); + try { $share = $this->shareManager->updateShare($share); } catch (GenericShareException $e) { @@ -1832,4 +1842,25 @@ class ShareAPIController extends OCSController { } } } + + /** + * @param IShare $share + * @param string[][]|null $formattedShareAttributes + * @return IShare modified share + */ + private function setShareAttributes(IShare $share, $formattedShareAttributes) { + $newShareAttributes = $this->shareManager->newShare()->newAttributes(); + if ($formattedShareAttributes !== null) { + foreach ($formattedShareAttributes as $formattedAttr) { + $newShareAttributes->setAttribute( + $formattedAttr['scope'], + $formattedAttr['key'], + (bool) \json_decode($formattedAttr['enabled']) + ); + } + } + $share->setAttributes($newShareAttributes); + + return $share; + } } diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index 5817ece6809..30e398d663b 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -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,31 @@ 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 + if ($share->getAttributes() !== null) { + foreach ($share->getAttributes()->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 +279,9 @@ class MountProvider implements IMountProvider { } } - $superShare->setPermissions($permissions) - ->setStatus($status); + $superShare->setPermissions($superPermissions); + $superShare->setStatus($status); + $superShare->setAttributes($superAttributes); $result[] = [$superShare, $shares]; } diff --git a/apps/files_sharing/lib/ViewOnly.php b/apps/files_sharing/lib/ViewOnly.php new file mode 100644 index 00000000000..58ff2e7f2b4 --- /dev/null +++ b/apps/files_sharing/lib/ViewOnly.php @@ -0,0 +1,116 @@ +<?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($pathsToCheck) { + // 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) { + 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) { + // 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(); + + // Check if read-only and on whether permission can download is both set and disabled. + + $canDownload = $share->getAttributes()->getAttribute('permissions', 'download'); + if ($canDownload !== null && !$canDownload) { + return false; + } + return true; + } +} diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index a16a1aaf383..50869b7a9f8 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -948,8 +948,11 @@ 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()); $share1 = $this->shareManager->createShare($share1); + $this->assertEquals(19, $share1->getPermissions()); + $this->assertEquals(null, $share1->getAttributes()); $share2 = $this->shareManager->newShare(); $share2->setNode($node1) @@ -957,6 +960,7 @@ 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); @@ -965,6 +969,7 @@ class ApiTest extends TestCase { $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()); diff --git a/apps/files_sharing/tests/ApplicationTest.php b/apps/files_sharing/tests/ApplicationTest.php new file mode 100644 index 00000000000..3f3164b233e --- /dev/null +++ b/apps/files_sharing/tests/ApplicationTest.php @@ -0,0 +1,238 @@ +<?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 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\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 { + + /** @var Application */ + private $application; + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IUserSession */ + private $userSession; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var Manager */ private $manager; + + protected function setUp(): void { + parent::setUp(); + + $this->application = new Application([]); + + // FIXME: how to mock this one ?? + $symfonyDispatcher = $this->createMock(SymfonyDispatcher::class); + $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() { + // 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($path, $userFolder, $run) { + $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 GenericEvent(null, [ 'path' => $path ]); + $this->eventDispatcher->dispatch('file.beforeGetDirect', $event); + + $this->assertEquals($run, !$event->hasArgument('errorMessage')); + } + + public function providesDataForCanZip() { + // 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($dir, $files, $userFolder, $run) { + $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 GenericEvent(null, ['dir' => $dir, 'files' => $files, 'run' => true]); + $this->eventDispatcher->dispatch('file.beforeCreateZip', $event); + + $this->assertEquals($run, $event->getArgument('run')); + $this->assertEquals($run, !$event->hasArgument('errorMessage')); + } + + public function testCheckFileUserNotFound() { + $this->userSession->method("isLoggedIn")->willReturn(false); + + // Simulate zip download of folder folder + $event = new GenericEvent(null, ['dir' => '/test', 'files' => ['test.txt'], 'run' => true]); + $this->eventDispatcher->dispatch('file.beforeCreateZip', $event); + + // It should run as this would restrict e.g. share links otherwise + $this->assertTrue($event->getArgument('run')); + $this->assertFalse($event->hasArgument('errorMessage')); + } +} diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index fd5580f19a7..f6568415727 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -124,7 +124,11 @@ class ShareAPIControllerTest extends TestCase { ->willReturn(true); $this->shareManager ->expects($this->any()) - ->method('shareProviderExists')->willReturn(true); + ->method('shareProviderExists')->willReturn(true); + $this->shareManager + ->expects($this->any()) + ->method('newShare') + ->willReturn($this->newShare()); $this->groupManager = $this->createMock(IGroupManager::class); $this->userManager = $this->createMock(IUserManager::class); $this->request = $this->createMock(IRequest::class); @@ -194,6 +198,25 @@ 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 +528,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 +539,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 +589,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 +605,8 @@ class ShareAPIControllerTest extends TestCase { 6, 'target', 0, - 'personal note' + 'personal note', + $shareAttributes, ); $expected = [ 'id' => 100, @@ -597,6 +624,7 @@ class ShareAPIControllerTest extends TestCase { 'token' => null, 'expiration' => null, 'permissions' => 4, + 'attributes' => $shareAttributesReturnJson, 'stime' => 5, 'parent' => null, 'storage_id' => 'STORAGE', @@ -630,7 +658,8 @@ class ShareAPIControllerTest extends TestCase { 6, 'target', 0, - 'personal note' + 'personal note', + $shareAttributes, ); $expected = [ 'id' => 101, @@ -647,6 +676,7 @@ class ShareAPIControllerTest extends TestCase { 'token' => null, 'expiration' => null, 'permissions' => 4, + 'attributes' => $shareAttributesReturnJson, 'stime' => 5, 'parent' => null, 'storage_id' => 'STORAGE', @@ -702,6 +732,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', @@ -3725,7 +3756,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 +3766,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 +3781,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiator', 'permissions' => 1, + 'attributes' => $shareAttributesReturnJson, 'stime' => 946684862, 'parent' => null, 'expiration' => null, @@ -3785,6 +3818,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiatorDN', 'permissions' => 1, + 'attributes' => $shareAttributesReturnJson, 'stime' => 946684862, 'parent' => null, 'expiration' => null, @@ -3837,6 +3871,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiator', 'permissions' => 1, + 'attributes' => null, 'stime' => 946684862, 'parent' => null, 'expiration' => null, @@ -3885,6 +3920,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiator', 'permissions' => 1, + 'attributes' => null, 'stime' => 946684862, 'parent' => null, 'expiration' => null, @@ -3935,6 +3971,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiator', 'permissions' => 1, + 'attributes' => null, 'stime' => 946684862, 'parent' => null, 'expiration' => null, @@ -4030,6 +4067,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', @@ -4228,6 +4266,7 @@ class ShareAPIControllerTest extends TestCase { 'uid_owner' => 'initiator', 'displayname_owner' => 'initiator', 'permissions' => 1, + 'attributes' => null, 'stime' => 946684862, 'parent' => null, 'expiration' => null, diff --git a/apps/files_sharing/tests/MountProviderTest.php b/apps/files_sharing/tests/MountProviderTest.php index 53bea929def..740c7c89eb7 100644 --- a/apps/files_sharing/tests/MountProviderTest.php +++ b/apps/files_sharing/tests/MountProviderTest.php @@ -81,12 +81,36 @@ 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) { $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); $share->expects($this->any()) @@ -115,14 +139,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 +199,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 +228,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 +256,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 +269,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 +282,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 +293,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 +305,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 +314,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 ], @@ -400,6 +428,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()); + } } } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8853c1f17f4..e17bbc96423 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -539,6 +539,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 +1513,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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 5617430958d..dcca888aeb0 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -572,6 +572,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 +1546,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', diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index e4cf0415202..6f9f62dec03 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -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; @@ -193,6 +194,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 +255,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 +268,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 +282,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 +312,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 +323,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 +624,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 +643,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 +1091,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 +1560,50 @@ class DefaultShareProvider implements IShareProvider { } $cursor->closeCursor(); } + + /** + * Load from database format (JSON string) to IAttributes + * + * @param IShare $share + * @param string|null $data + * @return IShare modified share + */ + private function updateShareAttributes(IShare $share, $data) { + if ($data !== null) { + $attributes = new ShareAttributes(); + $compressedAttributes = \json_decode($data, true); + foreach ($compressedAttributes as $compressedAttribute) { + $attributes->setAttribute( + $compressedAttribute[0], + $compressedAttribute[1], + $compressedAttribute[2] + ); + } + $share->setAttributes($attributes); + } + + return $share; + } + + /** + * Format IAttributes to database format (JSON string) + * + * @param IAttributes|null $attributes + * @return string|null + */ + private function formatShareAttributes($attributes) { + 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); + } } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 25511491a24..d0120a299be 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -70,6 +70,7 @@ use OCP\Share; use OCP\Share\Exceptions\AlreadySharedException; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IAttributes; use OCP\Share\IManager; use OCP\Share\IProviderFactory; use OCP\Share\IShare; @@ -1093,6 +1094,7 @@ class Manager implements IManager { 'shareWith' => $share->getSharedWith(), 'uidOwner' => $share->getSharedBy(), 'permissions' => $share->getPermissions(), + 'attributes' => $share->getAttributes(), 'path' => $userFolder->getRelativePath($share->getNode()->getPath()), ]); } @@ -2087,4 +2089,16 @@ class Manager implements IManager { yield from $provider->getAllShares(); } } + + /** + * @param IAttributes|null $perms + * @return string + */ + private function hashAttributes($perms) { + if ($perms === null || empty($perms->toArray())) { + return ""; + } + + return \md5(\json_encode($perms->toArray())); + } } diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index e21564563a2..1aeb79d4d31 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -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 */ @@ -335,6 +338,28 @@ class Share implements IShare { /** * @inheritdoc */ + public function newAttributes() { + return new ShareAttributes(); + } + + /** + * @inheritdoc + */ + public function setAttributes(IAttributes $attributes) { + $this->attributes = $attributes; + return $this; + } + + /** + * @inheritdoc + */ + public function getAttributes() { + return $this->attributes; + } + + /** + * @inheritdoc + */ public function setStatus(int $status): IShare { $this->status = $status; return $this; @@ -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) { diff --git a/lib/private/Share20/ShareAttributes.php b/lib/private/Share20/ShareAttributes.php new file mode 100644 index 00000000000..92f034e6783 --- /dev/null +++ b/lib/private/Share20/ShareAttributes.php @@ -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; + } +} diff --git a/lib/private/legacy/OC_Files.php b/lib/private/legacy/OC_Files.php index 02e15fd08d5..220fa0e3429 100644 --- a/lib/private/legacy/OC_Files.php +++ b/lib/private/legacy/OC_Files.php @@ -44,6 +44,8 @@ use bantu\IniGetWrapper\IniGetWrapper; use OC\Files\View; use OC\Streamer; use OCP\Lock\ILockingProvider; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; /** * Class for file server access @@ -167,6 +169,14 @@ class OC_Files { } } + //Dispatch an event to see if any apps have problem with download + $event = new GenericEvent(null, ['dir' => $dir, 'files' => $files, 'run' => true]); + $dispatcher = \OC::$server->query(IEventDispatcher::class); + $dispatcher->dispatch('file.beforeCreateZip', $event); + if (($event->getArgument('run') === false) or ($event->hasArgument('errorMessage'))) { + throw new \OC\ForbiddenException($event->getArgument('errorMessage')); + } + $streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles); OC_Util::obEnd(); @@ -212,6 +222,8 @@ class OC_Files { $streamer->finalize(); set_time_limit($executionTime); self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); + $event = new GenericEvent(null, ['result' => 'success', 'dir' => $dir, 'files' => $files]); + $dispatcher->dispatch('file.afterCreateZip', $event); } catch (\OCP\Lock\LockedException $ex) { self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); OC::$server->getLogger()->logException($ex); @@ -222,13 +234,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->hasArgument('message')) { + $hint .= ' ' . $event->getArgument('message'); + } + \OC_Template::printErrorPage($l->t('Cannot download file'), $hint, 200); } } @@ -287,6 +302,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 +338,19 @@ class OC_Files { $rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize); } + $dispatcher = \OC::$server->query(IEventDispatcher::class); + $event = new GenericEvent(null, ['path' => $filename]); + $dispatcher->dispatch('file.beforeGetDirect', $event); + + if (!\OC\Files\Filesystem::isReadable($filename) || $event->hasArgument('errorMessage')) { + if (!$event->hasArgument('errorMessage')) { + $msg = $event->getArgument('errorMessage'); + } else { + $msg = 'Access denied'; + } + throw new \OC\ForbiddenException($msg); + } + self::sendHeaders($filename, $name, $rangeArray); if (isset($params['head']) && $params['head']) { diff --git a/lib/public/Share/IAttributes.php b/lib/public/Share/IAttributes.php new file mode 100644 index 00000000000..9f2556e4005 --- /dev/null +++ b/lib/public/Share/IAttributes.php @@ -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 10.2.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 10.2.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 10.2.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 10.2.0 + */ + public function toArray(); +} diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index 1d3cf9bbbdf..d81f263b464 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -300,7 +300,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); @@ -315,6 +315,31 @@ interface IShare { public function getPermissions(); /** + * Create share attributes object + * + * @since 25.0.0 + * @return IAttributes + */ + public function newAttributes(); + + /** + * 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(); + + /** * Set the accepted status * See self::STATUS_* * diff --git a/tests/lib/Share20/DefaultShareProviderTest.php b/tests/lib/Share20/DefaultShareProviderTest.php index 03e1bdb4346..ed2bc2a4eda 100644 --- a/tests/lib/Share20/DefaultShareProviderTest.php +++ b/tests/lib/Share20/DefaultShareProviderTest.php @@ -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() { diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 2ed99519df6..797a5ebf683 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -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); @@ -3136,6 +3139,7 @@ class ManagerTest extends \Test\TestCase { ->setShareOwner('newUser') ->setSharedBy('sharer') ->setPermissions(31) + ->setAttributes($attrs) ->setNode($node); $this->defaultProvider->expects($this->once()) |