Преглед изворни кода

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

Add share attributes + prevent download permission
Carl Schwan пре 2 година
родитељ
комит
f74e89bde5
56 измењених фајлова са 1912 додато и 125 уклоњено
  1. 1 0
      apps/dav/composer/composer/autoload_classmap.php
  2. 1 0
      apps/dav/composer/composer/autoload_static.php
  3. 6 0
      apps/dav/lib/Connector/Sabre/FilesPlugin.php
  4. 26 0
      apps/dav/lib/Connector/Sabre/Node.php
  5. 6 0
      apps/dav/lib/Connector/Sabre/ServerFactory.php
  6. 16 1
      apps/dav/lib/Controller/DirectController.php
  7. 108 0
      apps/dav/lib/DAV/ViewOnlyPlugin.php
  8. 6 0
      apps/dav/lib/Server.php
  9. 62 0
      apps/dav/tests/unit/Connector/Sabre/NodeTest.php
  10. 17 9
      apps/dav/tests/unit/Controller/DirectControllerTest.php
  11. 117 0
      apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php
  12. 9 0
      apps/files/js/fileinfomodel.js
  13. 1 0
      apps/files/src/services/FileInfo.js
  14. 1 0
      apps/files_sharing/composer/composer/autoload_classmap.php
  15. 1 0
      apps/files_sharing/composer/composer/autoload_static.php
  16. 67 6
      apps/files_sharing/lib/AppInfo/Application.php
  17. 1 1
      apps/files_sharing/lib/Controller/PublicPreviewController.php
  18. 91 3
      apps/files_sharing/lib/Controller/ShareAPIController.php
  19. 25 5
      apps/files_sharing/lib/MountProvider.php
  20. 121 0
      apps/files_sharing/lib/ViewOnly.php
  21. 72 2
      apps/files_sharing/src/components/SharingEntry.vue
  22. 8 2
      apps/files_sharing/src/components/SharingEntryLink.vue
  23. 1 0
      apps/files_sharing/src/components/SharingInput.vue
  24. 3 2
      apps/files_sharing/src/mixins/ShareRequests.js
  25. 7 1
      apps/files_sharing/src/mixins/SharesMixin.js
  26. 61 0
      apps/files_sharing/src/models/Share.js
  27. 5 0
      apps/files_sharing/src/share.js
  28. 14 2
      apps/files_sharing/tests/ApiTest.php
  29. 236 0
      apps/files_sharing/tests/ApplicationTest.php
  30. 204 48
      apps/files_sharing/tests/Controller/ShareAPIControllerTest.php
  31. 68 34
      apps/files_sharing/tests/MountProviderTest.php
  32. 17 0
      core/src/files/client.js
  33. 16 0
      core/src/files/fileinfo.js
  34. 0 0
      dist/core-files_client.js
  35. 0 0
      dist/core-files_client.js.map
  36. 2 2
      dist/core-files_fileinfo.js
  37. 0 0
      dist/core-files_fileinfo.js.map
  38. 0 0
      dist/files-sidebar.js
  39. 0 0
      dist/files-sidebar.js.map
  40. 0 0
      dist/files_sharing-additionalScripts.js
  41. 0 0
      dist/files_sharing-additionalScripts.js.map
  42. 0 0
      dist/files_sharing-files_sharing_tab.js
  43. 0 0
      dist/files_sharing-files_sharing_tab.js.map
  44. 4 0
      lib/composer/composer/autoload_classmap.php
  45. 4 0
      lib/composer/composer/autoload_static.php
  46. 66 0
      lib/private/Share20/DefaultShareProvider.php
  47. 1 0
      lib/private/Share20/Manager.php
  48. 26 1
      lib/private/Share20/Share.php
  49. 73 0
      lib/private/Share20/ShareAttributes.php
  50. 30 3
      lib/private/legacy/OC_Files.php
  51. 84 0
      lib/public/Files/Events/BeforeDirectFileDownloadEvent.php
  52. 91 0
      lib/public/Files/Events/BeforeZipCreatedEvent.php
  53. 68 0
      lib/public/Share/IAttributes.php
  54. 29 2
      lib/public/Share/IShare.php
  55. 31 0
      tests/lib/Share20/DefaultShareProviderTest.php
  56. 8 1
      tests/lib/Share20/ManagerTest.php

+ 1 - 0
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',

+ 1 - 0
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',

+ 6 - 0
apps/dav/lib/Connector/Sabre/FilesPlugin.php

@@ -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();
 			});

+ 26 - 0
apps/dav/lib/Connector/Sabre/Node.php

@@ -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

+ 6 - 0
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(
+				$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(

+ 16 - 1
apps/dav/lib/Controller/DirectController.php

@@ -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);

+ 108 - 0
apps/dav/lib/DAV/ViewOnlyPlugin.php

@@ -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;
+	}
+}

+ 6 - 0
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(
+			$logger
+		));
+
 		if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
 			$this->server->addPlugin(new BrowserErrorPagePlugin());
 		}

+ 62 - 0
apps/dav/tests/unit/Connector/Sabre/NodeTest.php

@@ -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],

+ 17 - 9
apps/dav/tests/unit/Controller/DirectControllerTest.php

@@ -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,

+ 117 - 0
apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php

@@ -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);
+	}
+}

+ 9 - 0
apps/files/js/fileinfomodel.js

@@ -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

+ 1 - 0
apps/files/src/services/FileInfo.js

@@ -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 />

+ 1 - 0
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',
 );

+ 1 - 0
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)

+ 67 - 6
apps/files_sharing/lib/AppInfo/Application.php

@@ -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;
 		}

+ 1 - 1
apps/files_sharing/lib/Controller/PublicPreviewController.php

@@ -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);

+ 91 - 3
apps/files_sharing/lib/Controller/ShareAPIController.php

@@ -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);
+			}
+		}
+
+	}
 }

+ 25 - 5
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,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];
 		}

+ 121 - 0
apps/files_sharing/lib/ViewOnly.php

@@ -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;
+	}
+}

+ 72 - 2
apps/files_sharing/src/components/SharingEntry.vue

@@ -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')
 		},
 
 		/**

+ 8 - 2
apps/files_sharing/src/components/SharingEntryLink.vue

@@ -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>
 

+ 1 - 0
apps/files_sharing/src/components/SharingInput.vue

@@ -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

+ 3 - 2
apps/files_sharing/src/mixins/ShareRequests.js

@@ -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
 				}

+ 7 - 1
apps/files_sharing/src/mixins/SharesMixin.js

@@ -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

+ 61 - 0
apps/files_sharing/src/models/Share.js

@@ -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

+ 5 - 0
apps/files_sharing/src/share.js

@@ -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

+ 14 - 2
apps/files_sharing/tests/ApiTest.php

@@ -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());

+ 236 - 0
apps/files_sharing/tests/ApplicationTest.php

@@ -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());
+	}
+}

+ 204 - 48
apps/files_sharing/tests/Controller/ShareAPIControllerTest.php

@@ -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'
 			]

+ 68 - 34
apps/files_sharing/tests/MountProviderTest.php

@@ -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());
+			}
 		}
 	}
 }

+ 17 - 0
core/src/files/client.js

@@ -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

+ 16 - 0
core/src/files/fileinfo.js

@@ -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) {

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/core-files_client.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/core-files_client.js.map


+ 2 - 2
dist/core-files_fileinfo.js

@@ -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

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/core-files_fileinfo.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files-sidebar.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files-sidebar.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files_sharing-additionalScripts.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files_sharing-additionalScripts.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files_sharing-files_sharing_tab.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/files_sharing-files_sharing_tab.js.map


+ 4 - 0
lib/composer/composer/autoload_classmap.php

@@ -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',

+ 4 - 0
lib/composer/composer/autoload_static.php

@@ -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',

+ 66 - 0
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;
 
@@ -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);
+	}
 }

+ 1 - 0
lib/private/Share20/Manager.php

@@ -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()),
 			]);
 		}

+ 26 - 1
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 */
@@ -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) {

+ 73 - 0
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;
+	}
+}

+ 30 - 3
lib/private/legacy/OC_Files.php

@@ -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']) {

+ 84 - 0
lib/public/Files/Events/BeforeDirectFileDownloadEvent.php

@@ -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;
+	}
+}

+ 91 - 0
lib/public/Files/Events/BeforeZipCreatedEvent.php

@@ -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;
+	}
+}

+ 68 - 0
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 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();
+}

+ 29 - 2
lib/public/Share/IShare.php

@@ -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_*

+ 31 - 0
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() {

+ 8 - 1
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);
@@ -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);

Неке датотеке нису приказане због велике количине промена