Merge pull request #15952 from nextcloud/feature/noid/inherited-shares

Inherited Shares
This commit is contained in:
Roeland Jago Douma 2019-12-03 10:13:55 +01:00 committed by GitHub
commit 109aee525d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 853 additions and 217 deletions

View File

@ -53,6 +53,11 @@ return [
'url' => '/api/v1/shares',
'verb' => 'GET',
],
[
'name' => 'ShareAPI#getInheritedShares',
'url' => '/api/v1/shares/inherited',
'verb' => 'GET',
],
[
'name' => 'ShareAPI#createShare',
'url' => '/api/v1/shares',

View File

@ -34,6 +34,7 @@ return array(
'OCA\\Files_Sharing\\DeleteOrphanedSharesJob' => $baseDir . '/../lib/DeleteOrphanedSharesJob.php',
'OCA\\Files_Sharing\\Exceptions\\BrokenPath' => $baseDir . '/../lib/Exceptions/BrokenPath.php',
'OCA\\Files_Sharing\\Exceptions\\S2SException' => $baseDir . '/../lib/Exceptions/S2SException.php',
'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => $baseDir . '/../lib/Exceptions/SharingRightsException.php',
'OCA\\Files_Sharing\\ExpireSharesJob' => $baseDir . '/../lib/ExpireSharesJob.php',
'OCA\\Files_Sharing\\External\\Cache' => $baseDir . '/../lib/External/Cache.php',
'OCA\\Files_Sharing\\External\\Manager' => $baseDir . '/../lib/External/Manager.php',

View File

@ -49,6 +49,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\DeleteOrphanedSharesJob' => __DIR__ . '/..' . '/../lib/DeleteOrphanedSharesJob.php',
'OCA\\Files_Sharing\\Exceptions\\BrokenPath' => __DIR__ . '/..' . '/../lib/Exceptions/BrokenPath.php',
'OCA\\Files_Sharing\\Exceptions\\S2SException' => __DIR__ . '/..' . '/../lib/Exceptions/S2SException.php',
'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => __DIR__ . '/..' . '/../lib/Exceptions/SharingRightsException.php',
'OCA\\Files_Sharing\\ExpireSharesJob' => __DIR__ . '/..' . '/../lib/ExpireSharesJob.php',
'OCA\\Files_Sharing\\External\\Cache' => __DIR__ . '/..' . '/../lib/External/Cache.php',
'OCA\\Files_Sharing\\External\\Manager' => __DIR__ . '/..' . '/../lib/External/Manager.php',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=65)}({65:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=68)}({68:function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}});
//# sourceMappingURL=collaboration.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="/js/",r(r.s=343)}({343:function(e,t){
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="/js/",r(r.s=349)}({349:function(e,t){
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <pvince81@owncloud.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @license AGPL-3.0
*
@ -31,8 +32,9 @@ declare(strict_types=1);
namespace OCA\Files_Sharing\Controller;
use OCA\Files\Helper;
use OCA\Files_Sharing\Exceptions\SharingRightsException;
use OCA\Files_Sharing\External\Storage;
use OCA\Files\Helper;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
@ -42,6 +44,7 @@ use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\QueryException;
use OCP\Constants;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
@ -383,7 +386,7 @@ class ShareAPIController extends OCSController {
* @throws OCSException
* @throws OCSForbiddenException
* @throws OCSNotFoundException
* @throws \OCP\Files\InvalidPathException
* @throws InvalidPathException
* @suppress PhanUndeclaredClassMethod
*/
public function createShare(
@ -579,11 +582,12 @@ class ShareAPIController extends OCSController {
}
/**
* @param \OCP\Files\File|\OCP\Files\Folder $node
* @param null|Node $node
* @param boolean $includeTags
* @return DataResponse
*
* @return array
*/
private function getSharedWithMe($node = null, bool $includeTags): DataResponse {
private function getSharedWithMe($node, bool $includeTags): array {
$userShares = $this->shareManager->getSharedWith($this->currentUser, Share::SHARE_TYPE_USER, $node, -1, 0);
$groupShares = $this->shareManager->getSharedWith($this->currentUser, Share::SHARE_TYPE_GROUP, $node, -1, 0);
@ -592,7 +596,7 @@ class ShareAPIController extends OCSController {
$shares = array_merge($userShares, $groupShares, $circleShares, $roomShares);
$shares = array_filter($shares, function (IShare $share) {
$shares = array_filter($shares, function(IShare $share) {
return $share->getShareOwner() !== $this->currentUser;
});
@ -611,13 +615,15 @@ class ShareAPIController extends OCSController {
$formatted = Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager());
}
return new DataResponse($formatted);
return $formatted;
}
/**
* @param \OCP\Files\Folder $folder
* @param \OCP\Files\Node $folder
*
* @return array
* @throws OCSBadRequestException
* @throws NotFoundException
*/
private function getSharesInDir(Node $folder): array {
if (!($folder instanceof \OCP\Files\Folder)) {
@ -634,13 +640,37 @@ class ShareAPIController extends OCSController {
// filter out duplicate shares
$known = [];
return array_filter($shares, function($share) use (&$known) {
if (in_array($share->getId(), $known)) {
return false;
$formatted = $miniFormatted = [];
$resharingRight = false;
$known = [];
foreach ($shares as $share) {
if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->currentUser) {
continue;
}
$known[] = $share->getId();
return true;
});
try {
$format = $this->formatShare($share);
$known[] = $share->getId();
$formatted[] = $format;
if ($share->getSharedBy() === $this->currentUser) {
$miniFormatted[] = $format;
}
if (!$resharingRight && $this->shareProviderResharingRights($this->currentUser, $share, $folder)) {
$resharingRight = true;
}
} catch (\Exception $e) {
//Ignore this share
}
}
if (!$resharingRight) {
$formatted = $miniFormatted;
}
return $formatted;
}
/**
@ -659,64 +689,87 @@ class ShareAPIController extends OCSController {
* - Get shares for a specific path (?path=...)
* - Get all shares in a folder (?subfiles=true&path=..)
*
* @param string $include_tags
*
* @return DataResponse
* @throws NotFoundException
* @throws OCSBadRequestException
* @throws OCSNotFoundException
*/
public function getShares(
string $shared_with_me = 'false',
string $reshares = 'false',
string $subfiles = 'false',
string $path = null,
string $path = '',
string $include_tags = 'false'
): DataResponse {
if ($path !== null) {
$node = null;
if ($path !== '') {
$userFolder = $this->rootFolder->getUserFolder($this->currentUser);
try {
$path = $userFolder->get($path);
$this->lock($path);
} catch (\OCP\Files\NotFoundException $e) {
throw new OCSNotFoundException($this->l->t('Wrong path, file/folder doesn\'t exist'));
$node = $userFolder->get($path);
$this->lock($node);
} catch (NotFoundException $e) {
throw new OCSNotFoundException(
$this->l->t('Wrong path, file/folder doesn\'t exist')
);
} catch (LockedException $e) {
throw new OCSNotFoundException($this->l->t('Could not lock path'));
throw new OCSNotFoundException($this->l->t('Could not lock node'));
}
}
$include_tags = $include_tags === 'true';
$shares = $this->getFormattedShares(
$this->currentUser,
$node,
($shared_with_me === 'true'),
($reshares === 'true'),
($subfiles === 'true'),
($include_tags === 'true')
);
if ($shared_with_me === 'true') {
$result = $this->getSharedWithMe($path, $include_tags);
return $result;
return new DataResponse($shares);
}
/**
* @param string $viewer
* @param Node $node
* @param bool $sharedWithMe
* @param bool $reShares
* @param bool $subFiles
* @param bool $includeTags
*
* @return array
* @throws NotFoundException
* @throws OCSBadRequestException
*/
private function getFormattedShares(
string $viewer, $node = null, bool $sharedWithMe = false, bool $reShares = false,
bool $subFiles = false, bool $includeTags = false
): array {
if ($sharedWithMe) {
return $this->getSharedWithMe($node, $includeTags);
}
if ($reshares === 'true') {
$reshares = true;
} else {
$reshares = false;
if ($subFiles) {
return $this->getSharesInDir($node);
}
if ($subfiles === 'true') {
$shares = $this->getSharesInDir($path);
$recipientNode = null;
} else {
// get all shares
$shares = $this->getAllShares($path, $reshares);
$recipientNode = $path;
}
$shares = $this->getSharesFromNode($viewer, $node, $reShares);
// process all shares
$formatted = $miniFormatted = [];
$known = $formatted = $miniFormatted = [];
$resharingRight = false;
foreach ($shares as $share) {
/** @var IShare $share */
// do not list the shares of the current user
if ($share->getSharedWith() === $this->currentUser) {
if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->currentUser) {
continue;
}
$known[] = $share->getId();
try {
$format = $this->formatShare($share, $recipientNode);
/** @var IShare $share */
$format = $this->formatShare($share, $node);
$formatted[] = $format;
// let's also build a list of shares created
@ -728,11 +781,10 @@ class ShareAPIController extends OCSController {
// check if one of those share is shared with me
// and if I have resharing rights on it
if (!$resharingRight && $this->shareProviderResharingRights($this->currentUser, $share, $path)) {
if (!$resharingRight && $this->shareProviderResharingRights($this->currentUser, $share, $node)) {
$resharingRight = true;
}
} catch (\Exception $e) {
//Ignore share
} catch (InvalidPathException | NotFoundException $e) {
}
}
@ -740,13 +792,89 @@ class ShareAPIController extends OCSController {
$formatted = $miniFormatted;
}
if ($include_tags) {
$formatted = Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager());
if ($includeTags) {
$formatted =
Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager());
}
return new DataResponse($formatted);
return $formatted;
}
/**
* The getInheritedShares function.
* returns all shares relative to a file, including parent folders shares rights.
*
* @NoAdminRequired
*
* @param string $path
*
* - Get shares by the current user
* - Get shares by the current user and reshares (?reshares=true)
* - Get shares with the current user (?shared_with_me=true)
* - Get shares for a specific path (?path=...)
* - Get all shares in a folder (?subfiles=true&path=..)
*
* @return DataResponse
* @throws InvalidPathException
* @throws NotFoundException
* @throws OCSNotFoundException
* @throws OCSBadRequestException
* @throws SharingRightsException
*/
public function getInheritedShares(string $path): DataResponse {
// get Node from (string) path.
$userFolder = $this->rootFolder->getUserFolder($this->currentUser);
try {
$node = $userFolder->get($path);
$this->lock($node);
} catch (\OCP\Files\NotFoundException $e) {
throw new OCSNotFoundException($this->l->t('Wrong path, file/folder doesn\'t exist'));
} catch (LockedException $e) {
throw new OCSNotFoundException($this->l->t('Could not lock path'));
}
// current User has resharing rights ?
$this->confirmSharingRights($node);
// initiate real owner.
$owner = $node->getOwner()
->getUID();
if (!$this->userManager->userExists($owner)) {
return new DataResponse([]);
}
// get node based on the owner, fix owner in case of external storage
$userFolder = $this->rootFolder->getUserFolder($owner);
if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) {
$owner = $node->getOwner()
->getUID();
$userFolder = $this->rootFolder->getUserFolder($owner);
$nodes = $userFolder->getById($node->getId());
$node = array_shift($nodes);
}
$basePath = $userFolder->getPath();
// generate node list for each parent folders
/** @var Node[] $nodes */
$nodes = [];
while ($node->getPath() !== $basePath) {
$nodes[] = $node;
$node = $node->getParent();
}
// for each nodes, retrieve shares.
$shares = [];
foreach ($nodes as $node) {
$getShares = $this->getFormattedShares($owner, $node, false, true);
$this->mergeFormattedShares($shares, $getShares);
}
return new DataResponse(array_values($shares));
}
/**
* @NoAdminRequired
*
@ -1261,6 +1389,90 @@ class ShareAPIController extends OCSController {
}
/**
* @param string $viewer
* @param Node $node
* @param bool $reShares
*
* @return IShare[]
*/
private function getSharesFromNode(string $viewer, $node, bool $reShares): array {
$providers = [
Share::SHARE_TYPE_USER,
Share::SHARE_TYPE_GROUP,
Share::SHARE_TYPE_LINK,
Share::SHARE_TYPE_EMAIL,
Share::SHARE_TYPE_EMAIL,
Share::SHARE_TYPE_CIRCLE,
Share::SHARE_TYPE_ROOM
];
// Should we assume that the (currentUser) viewer is the owner of the node !?
$shares = [];
foreach ($providers as $provider) {
if (!$this->shareManager->shareProviderExists($provider)) {
continue;
}
$providerShares =
$this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0);
$shares = array_merge($shares, $providerShares);
}
if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
$federatedShares = $this->shareManager->getSharesBy(
$this->currentUser, Share::SHARE_TYPE_REMOTE, $node, $reShares, -1, 0
);
$shares = array_merge($shares, $federatedShares);
}
if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
$federatedShares = $this->shareManager->getSharesBy(
$this->currentUser, Share::SHARE_TYPE_REMOTE_GROUP, $node, $reShares, -1, 0
);
$shares = array_merge($shares, $federatedShares);
}
return $shares;
}
/**
* @param Node $node
*
* @throws SharingRightsException
*/
private function confirmSharingRights(Node $node): void {
if (!$this->hasResharingRights($this->currentUser, $node)) {
throw new SharingRightsException('no sharing rights on this item');
}
}
/**
* @param string $viewer
* @param Node $node
*
* @return bool
*/
private function hasResharingRights($viewer, $node): bool {
foreach ([$node, $node->getParent()] as $node) {
$shares = $this->getSharesFromNode($viewer, $node, true);
foreach ($shares as $share) {
try {
if ($this->shareProviderResharingRights($viewer, $share, $node)) {
return true;
}
} catch (InvalidPathException | NotFoundException $e) {
}
}
}
return false;
}
/**
* Returns if we can find resharing rights in an IShare object for a specific user.
*
@ -1269,18 +1481,18 @@ class ShareAPIController extends OCSController {
* @param string $userId
* @param IShare $share
* @param Node $node
*
* @return bool
* @throws NotFoundException
* @throws \OCP\Files\InvalidPathException
* @throws InvalidPathException
*/
private function shareProviderResharingRights(string $userId, IShare $share, $node): bool {
if ($share->getShareOwner() === $userId) {
return true;
}
// we check that current user have parent resharing rights on the current file
if ($node !== null && ($node->getPermissions() & \OCP\Constants::PERMISSION_SHARE) !== 0) {
if ($node !== null && ($node->getPermissions() & Constants::PERMISSION_SHARE) !== 0) {
return true;
}
@ -1357,4 +1569,21 @@ class ShareAPIController extends OCSController {
return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $federatedShares, $federatedGroupShares);
}
/**
* merging already formatted shares.
* We'll make an associative array to easily detect duplicate Ids.
* Keys _needs_ to be removed after all shares are retrieved and merged.
*
* @param array $shares
* @param array $newShares
*/
private function mergeFormattedShares(array &$shares, array $newShares) {
foreach ($newShares as $newShare) {
if (!array_key_exists($newShare['id'], $shares)) {
$shares[$newShare['id']] = $newShare;
}
}
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2019, Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.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\Exceptions;
use Exception;
/**
* Sharing and Resharing rights.
*
* Class SharingRightsException
*
* @package OCA\Files_Sharing\Exceptions
*/
class SharingRightsException extends Exception {
}

View File

@ -119,8 +119,6 @@ import ActionInput from 'nextcloud-vue/dist/Components/ActionInput'
import ActionTextEditable from 'nextcloud-vue/dist/Components/ActionTextEditable'
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
// eslint-disable-next-line no-unused-vars
import Share from '../models/Share'
import SharesMixin from '../mixins/SharesMixin'
export default {

View File

@ -0,0 +1,112 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.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/>.
-
-->
<template>
<SharingEntrySimple
:key="share.id"
class="sharing-entry__inherited"
:title="share.shareWithDisplayName">
<template #avatar>
<Avatar
:user="share.shareWith"
:display-name="share.shareWithDisplayName"
class="sharing-entry__avatar"
tooltip-message="" />
</template>
<ActionText icon="icon-user">
{{ t('files_sharing', 'Invited by {initiator}', { initiator: share.ownerDisplayName }) }}
</ActionText>
<ActionLink v-if="share.fileSource"
icon="icon-folder"
:href="fileTargetUrl">
{{ t('files_sharing', 'Open folder') }}
</ActionLink>
<ActionButton v-if="share.canDelete"
icon="icon-delete"
@click.prevent="onDelete">
{{ t('files_sharing', 'Delete share') }}
</actionbutton>
</SharingEntrySimple>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
import ActionLink from 'nextcloud-vue/dist/Components/ActionLink'
import ActionText from 'nextcloud-vue/dist/Components/ActionText'
// eslint-disable-next-line no-unused-vars
import Share from '../models/Share'
import SharesMixin from '../mixins/SharesMixin'
import SharingEntrySimple from '../components/SharingEntrySimple'
export default {
name: 'SharingEntryInherited',
components: {
ActionButton,
ActionLink,
ActionText,
Avatar,
SharingEntrySimple
},
mixins: [SharesMixin],
props: {
share: {
type: Share,
required: true
}
},
computed: {
fileTargetUrl() {
return generateUrl('/f/{fileid}', {
fileid: this.share.fileSource
})
}
}
}
</script>
<style lang="scss" scoped>
.sharing-entry {
display: flex;
align-items: center;
height: 44px;
&__desc {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px;
line-height: 1.2em;
p {
color: var(--color-text-maxcontrast);
}
}
&__actions {
margin-left: auto;
}
}
</style>

View File

@ -0,0 +1,164 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.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/>.
-
-->
<template>
<ul id="sharing-inherited-shares">
<!-- Main collapsible entry -->
<SharingEntrySimple
class="sharing-entry__inherited"
:title="mainTitle">
<template #avatar>
<div class="avatar-shared icon-more-white" />
</template>
<ActionButton :icon="showInheritedSharesIcon" @click.prevent.stop="toggleInheritedShares">
{{ toggleTooltip }}
</ActionButton>
</SharingEntrySimple>
<!-- Inherited shares list -->
<SharingEntryInherited v-for="share in shares"
:key="share.id"
:share="share" />
</ul>
</template>
<script>
import { generateOcsUrl } from '@nextcloud/router'
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
import axios from '@nextcloud/axios'
import Share from '../models/Share'
import SharingEntryInherited from '../components/SharingEntryInherited'
import SharingEntrySimple from '../components/SharingEntrySimple'
export default {
name: 'SharingInherited',
components: {
ActionButton,
SharingEntryInherited,
SharingEntrySimple
},
props: {
fileInfo: {
type: Object,
default: () => {},
required: true
}
},
data() {
return {
loaded: false,
loading: false,
showInheritedShares: false,
shares: []
}
},
computed: {
showInheritedSharesIcon() {
if (this.loading) {
return 'icon-loading-small'
}
if (this.showInheritedShares) {
return 'icon-triangle-n'
}
return 'icon-triangle-s'
},
mainTitle() {
return t('files_sharing', 'Others with access {count}', {
count: this.loaded ? `: ${this.shares.length}` : ''
})
},
toggleTooltip() {
return this.fileInfo.type === 'dir'
? t('files_sharing', 'Toggle list of others with access to this directory')
: t('files_sharing', 'Toggle list of others with access to this file')
},
fullPath() {
const path = `${this.fileInfo.path}/${this.fileInfo.name}`
return path.replace('//', '/')
}
},
watch: {
fileInfo() {
this.resetState()
}
},
methods: {
/**
* Toggle the list view and fetch/reset the state
*/
toggleInheritedShares() {
this.showInheritedShares = !this.showInheritedShares
if (this.showInheritedShares) {
this.fetchInheritedShares()
} else {
this.resetState()
}
},
/**
* Fetch the Inherited Shares array
*/
async fetchInheritedShares() {
this.loading = true
try {
const url = generateOcsUrl(`apps/files_sharing/api/v1/shares/inherited?format=json&path=${this.fullPath}`, 2)
const shares = await axios.get(url.replace(/\/$/, ''))
this.shares = shares.data.ocs.data
.map(share => new Share(share))
.sort((a, b) => b.createdTime - a.createdTime)
console.info(this.shares)
this.loaded = true
} catch (error) {
OC.Notification.showTemporary(t('files_sharing', 'Unable to fetch inherited shares'), { type: 'error' })
} finally {
this.loading = false
}
},
/**
* Reset current component state
*/
resetState() {
this.loaded = false
this.loading = false
this.showInheritedShares = false
this.shares = []
}
}
}
</script>
<style lang="scss" scoped>
.sharing-entry__inherited {
.avatar-shared {
width: 32px;
height: 32px;
line-height: 32px;
font-size: 18px;
background-color: var(--color-text-maxcontrast);
border-radius: 50%;
flex-shrink: 0;
}
}
</style>

View File

@ -33,7 +33,7 @@
<!-- shared with me information -->
<SharingEntrySimple v-if="isSharedWithMe" v-bind="sharedWithMe" class="sharing-entry__reshare">
<template #avatar>
<Avatar #avatar
<Avatar
:user="sharedWithMe.user"
:display-name="sharedWithMe.displayName"
class="sharing-entry__avatar"
@ -61,6 +61,9 @@
:shares="shares"
:file-info="fileInfo" />
<!-- inherited shares -->
<SharingInherited v-if="!loading" :file-info="fileInfo" />
<!-- internal link copy -->
<SharingEntryInternal :file-info="fileInfo" />
@ -82,11 +85,11 @@
</template>
<script>
import { CollectionList } from 'nextcloud-vue-collections'
import { generateOcsUrl } from '@nextcloud/router'
import Tab from 'nextcloud-vue/dist/Components/AppSidebarTab'
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
import axios from '@nextcloud/axios'
import { CollectionList } from 'nextcloud-vue-collections'
import Tab from 'nextcloud-vue/dist/Components/AppSidebarTab'
import { shareWithTitle } from '../utils/SharedWithMe'
import Share from '../models/Share'
@ -95,6 +98,7 @@ import SharingEntryInternal from '../components/SharingEntryInternal'
import SharingEntrySimple from '../components/SharingEntrySimple'
import SharingInput from '../components/SharingInput'
import SharingInherited from './SharingInherited'
import SharingLinkList from './SharingLinkList'
import SharingList from './SharingList'
@ -106,6 +110,7 @@ export default {
CollectionList,
SharingEntryInternal,
SharingEntrySimple,
SharingInherited,
SharingInput,
SharingLinkList,
SharingList,

View File

@ -106,6 +106,9 @@ class ShareAPIControllerTest extends TestCase {
->expects($this->any())
->method('shareApiEnabled')
->willReturn(true);
$this->shareManager
->expects($this->any())
->method('shareProviderExists')->willReturn(true);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->request = $this->createMock(IRequest::class);
@ -170,10 +173,10 @@ class ShareAPIControllerTest extends TestCase {
$this->expectExceptionMessage('Wrong share ID, share doesn\'t exist');
$this->shareManager
->expects($this->exactly(3))
->expects($this->exactly(5))
->method('getShareById')
->will($this->returnCallback(function($id) {
if ($id === 'ocinternal:42' || $id === 'ocRoomShare:42' || $id === 'ocFederatedSharing:42') {
if ($id === 'ocinternal:42' || $id === 'ocRoomShare:42' || $id === 'ocFederatedSharing:42' || $id === 'ocCircleShare:42' || $id === 'ocMailShare:42') {
throw new \OCP\Share\Exceptions\ShareNotFound();
} else {
throw new \Exception();
@ -1008,9 +1011,7 @@ class ShareAPIControllerTest extends TestCase {
[
],
[
$file1UserShareOwnerExpected,
$file1UserShareOwnerExpected,
$file1UserShareOwnerExpected,
$file1UserShareOwnerExpected
]
],
[

View File

@ -322,4 +322,84 @@ Feature: sharing
And User "user2" should be included in the response
And User "user3" should not be included in the response
Scenario: getting inherited shares of a file
Given user "user0" exists
And user "user1" exists
And user "user2" exists
And user "user3" exists
# will be shared with user1
And User "user0" created a folder "/first"
# will be shared with user1, user2
And User "user0" created a folder "/first/second"
# will be shared with user1, user3
And User "user0" uploads file "data/textfile.txt" to "/first/test1.txt"
# will be shared with user1, user2, user3
And User "user0" uploads file "data/textfile.txt" to "/first/second/test2.txt"
And As an "user0"
And creating a share with
| path | /first |
| shareType | 0 |
| shareWith | user1 |
| permissions | 16 |
And As an "user1"
And accepting last share
# And folder "first" of user "user0" is shared with user "user1"
# And creating a share with
# | path | /first/second |
# | shareType | 0 |
# | shareWith | user2 |
# | permissions | 16 |
And folder "first/second" of user "user0" is shared with user "user2"
# And As an "user1"
# And creating a share with
# | path | /first/test1.txt |
# | shareType | 0 |
# | shareWith | user3 |
# | permissions | 8 |
And file "first/test1.txt" of user "user0" is shared with user "user3"
# And As an "user2"
# And creating a share with
# | path | /second/test2.txt |
# | shareType | 0 |
# | shareWith | user3 |
# | permissions | 8 |
And file "first/second/test2.txt" of user "user0" is shared with user "user3"
# get inherited shares from the owner PoV
And As an "user0"
When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/second/test2.txt"
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And User "user0" should not be included in the response
And User "user1" should be included in the response
And User "user2" should be included in the response
And User "user3" should be included in the response
When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt"
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And User "user0" should not be included in the response
And User "user1" should be included in the response
And User "user2" should not be included in the response
And User "user3" should be included in the response
# get inherited shares from the a user with no shares rights
And As an "user2"
When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt"
Then the OCS status code should be "404"
And the HTTP status code should be "200"
# get inherited shares from the PoV of a user with resharing rights (user1)
And As an "user1"
When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/second/test2.txt"
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And User "user0" should not be included in the response
And User "user1" should not be included in the response
And User "user2" should be included in the response
And User "user3" should be included in the response
When sending "GET" to "/apps/files_sharing/api/v1/shares/inherited?path=first/test1.txt"
Then the OCS status code should be "100"
And the HTTP status code should be "200"
And User "user0" should not be included in the response
And User "user1" should not be included in the response
And User "user2" should not be included in the response
And User "user3" should be included in the response
# See sharing-v1-part2.feature