diff --git a/apps/files_sharing/appinfo/routes.php b/apps/files_sharing/appinfo/routes.php index 91f8271c143bb..b741e9e98c91f 100644 --- a/apps/files_sharing/appinfo/routes.php +++ b/apps/files_sharing/appinfo/routes.php @@ -139,6 +139,14 @@ 'url' => '/api/v1/deletedshares/{id}', 'verb' => 'POST', ], + /* + * Expired Shares + */ + [ + 'name' => 'ExpiredShareAPI#index', + 'url' => '/api/v1/expiredshares', + 'verb' => 'GET', + ], /* * OCS Sharee API */ diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index e3c94a7ac1a94..93e163a40a528 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -29,11 +29,13 @@ 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php', + 'OCA\\Files_Sharing\\Controller\\ExpiredShareAPIController' => $baseDir . '/../lib/Controller/ExpiredShareAPIController.php', 'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php', 'OCA\\Files_Sharing\\Controller\\PublicPreviewController' => $baseDir . '/../lib/Controller/PublicPreviewController.php', 'OCA\\Files_Sharing\\Controller\\RemoteController' => $baseDir . '/../lib/Controller/RemoteController.php', 'OCA\\Files_Sharing\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php', 'OCA\\Files_Sharing\\Controller\\ShareAPIController' => $baseDir . '/../lib/Controller/ShareAPIController.php', + 'OCA\\Files_Sharing\\Controller\\ShareApiControllerFactory' => $baseDir . '/../lib/Controller/ShareApiControllerFactory.php', 'OCA\\Files_Sharing\\Controller\\ShareController' => $baseDir . '/../lib/Controller/ShareController.php', 'OCA\\Files_Sharing\\Controller\\ShareInfoController' => $baseDir . '/../lib/Controller/ShareInfoController.php', 'OCA\\Files_Sharing\\Controller\\ShareesAPIController' => $baseDir . '/../lib/Controller/ShareesAPIController.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 597e65d96a261..12929cc49c609 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -44,11 +44,13 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php', + 'OCA\\Files_Sharing\\Controller\\ExpiredShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/ExpiredShareAPIController.php', 'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php', 'OCA\\Files_Sharing\\Controller\\PublicPreviewController' => __DIR__ . '/..' . '/../lib/Controller/PublicPreviewController.php', 'OCA\\Files_Sharing\\Controller\\RemoteController' => __DIR__ . '/..' . '/../lib/Controller/RemoteController.php', 'OCA\\Files_Sharing\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php', 'OCA\\Files_Sharing\\Controller\\ShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/ShareAPIController.php', + 'OCA\\Files_Sharing\\Controller\\ShareApiControllerFactory' => __DIR__ . '/..' . '/../lib/Controller/ShareApiControllerFactory.php', 'OCA\\Files_Sharing\\Controller\\ShareController' => __DIR__ . '/..' . '/../lib/Controller/ShareController.php', 'OCA\\Files_Sharing\\Controller\\ShareInfoController' => __DIR__ . '/..' . '/../lib/Controller/ShareInfoController.php', 'OCA\\Files_Sharing\\Controller\\ShareesAPIController' => __DIR__ . '/..' . '/../lib/Controller/ShareesAPIController.php', diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php index ce30cf373ec0c..3d3a1cd527010 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -15,154 +15,66 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSNotFoundException; -use OCP\AppFramework\OCSController; -use OCP\AppFramework\QueryException; -use OCP\Files\Folder; use OCP\Files\IRootFolder; -use OCP\Files\NotFoundException; +use OCP\IDateTimeZone; use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IPreview; use OCP\IRequest; -use OCP\IServerContainer; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; +use OCP\UserStatus\IManager as UserStatusManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; /** - * @psalm-import-type Files_SharingDeletedShare from ResponseDefinitions + * @psalm-import-type Files_SharingShare from ResponseDefinitions */ -class DeletedShareAPIController extends OCSController { - - /** @var ShareManager */ - private $shareManager; - - /** @var string */ - private $userId; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IAppManager */ - private $appManager; - - /** @var IServerContainer */ - private $serverContainer; - - public function __construct(string $appName, +class DeletedShareAPIController extends ShareApiControllerFactory { + + public function __construct( IRequest $request, - ShareManager $shareManager, - string $UserId, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IAppManager $appManager, - IServerContainer $serverContainer) { - parent::__construct($appName, $request); - - $this->shareManager = $shareManager; - $this->userId = $UserId; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; - $this->appManager = $appManager; - $this->serverContainer = $serverContainer; - } - - /** - * @suppress PhanUndeclaredClassMethod - * - * @return Files_SharingDeletedShare - */ - private function formatShare(IShare $share): array { - $result = [ - 'id' => $share->getFullId(), - 'share_type' => $share->getShareType(), - 'uid_owner' => $share->getSharedBy(), - 'displayname_owner' => $this->userManager->get($share->getSharedBy())->getDisplayName(), - 'permissions' => 0, - 'stime' => $share->getShareTime()->getTimestamp(), - 'parent' => null, - 'expiration' => null, - 'token' => null, - 'uid_file_owner' => $share->getShareOwner(), - 'displayname_file_owner' => $this->userManager->get($share->getShareOwner())->getDisplayName(), - 'path' => $share->getTarget(), - ]; - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $node = $userFolder->getFirstNodeById($share->getNodeId()); - if (!$node) { - // fallback to guessing the path - $node = $userFolder->get($share->getTarget()); - if ($node === null || $share->getTarget() === '') { - throw new NotFoundException(); - } - } - - $result['path'] = $userFolder->getRelativePath($node->getPath()); - if ($node instanceof Folder) { - $result['item_type'] = 'folder'; - } else { - $result['item_type'] = 'file'; - } - $result['mimetype'] = $node->getMimetype(); - $result['storage_id'] = $node->getStorage()->getId(); - $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId(); - $result['item_source'] = $node->getId(); - $result['file_source'] = $node->getId(); - $result['file_parent'] = $node->getParent()->getId(); - $result['file_target'] = $share->getTarget(); - $result['item_size'] = $node->getSize(); - $result['item_mtime'] = $node->getMTime(); - - $expiration = $share->getExpirationDate(); - if ($expiration !== null) { - $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); - } - - if ($share->getShareType() === IShare::TYPE_GROUP) { - $group = $this->groupManager->get($share->getSharedWith()); - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith(); - } elseif ($share->getShareType() === IShare::TYPE_ROOM) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - $result = array_merge($result, $this->getRoomShareHelper()->formatShare($share)); - } catch (QueryException $e) { - } - } elseif ($share->getShareType() === IShare::TYPE_DECK) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - $result = array_merge($result, $this->getDeckShareHelper()->formatShare($share)); - } catch (QueryException $e) { - } - } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - $result = array_merge($result, $this->getSciencemeshShareHelper()->formatShare($share)); - } catch (QueryException $e) { - } - } - - return $result; + protected ShareManager $shareManager, + protected ?string $userId, + protected IUserManager $userManager, + protected IGroupManager $groupManager, + protected IRootFolder $rootFolder, + protected IAppManager $appManager, + protected ContainerInterface $serverContainer, + protected UserStatusManager $userStatusManager, + protected IPreview $previewManager, + protected IDateTimeZone $dateTimeZone, + protected IURLGenerator $urlGenerator, + protected IL10N $l, + protected LoggerInterface $logger, + ) { + parent::__construct( + $request, + $shareManager, + $userId, + $userManager, + $groupManager, + $rootFolder, + $appManager, + $serverContainer, + $userStatusManager, + $previewManager, + $dateTimeZone, + $urlGenerator, + $l, + $logger, + ); + $this->isDeletedShareController = true; } /** * Get a list of all deleted shares * - * @return DataResponse + * @return DataResponse * * 200: Deleted shares returned */ @@ -212,55 +124,4 @@ public function undelete(string $id): DataResponse { return new DataResponse([]); } - - /** - * Returns the helper of DeletedShareAPIController for room shares. - * - * If the Talk application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Talk\Share\Helper\DeletedShareAPIController - * @throws QueryException - */ - private function getRoomShareHelper() { - if (!$this->appManager->isEnabledForUser('spreed')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); - } - - /** - * Returns the helper of DeletedShareAPIHelper for deck shares. - * - * If the Deck application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException - */ - private function getDeckShareHelper() { - if (!$this->appManager->isEnabledForUser('deck')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper'); - } - - /** - * Returns the helper of DeletedShareAPIHelper for sciencemesh shares. - * - * If the sciencemesh application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException - */ - private function getSciencemeshShareHelper() { - if (!$this->appManager->isEnabledForUser('sciencemesh')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); - } } diff --git a/apps/files_sharing/lib/Controller/ExpiredShareAPIController.php b/apps/files_sharing/lib/Controller/ExpiredShareAPIController.php new file mode 100644 index 0000000000000..d50e98089dadb --- /dev/null +++ b/apps/files_sharing/lib/Controller/ExpiredShareAPIController.php @@ -0,0 +1,96 @@ + + * + * 200: Deleted shares returned + */ + #[NoAdminRequired] + public function index(): DataResponse { + $groupShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_GROUP, null, -1, 0); + $roomShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_ROOM, null, -1, 0); + $deckShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_DECK, null, -1, 0); + $sciencemeshShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_SCIENCEMESH, null, -1, 0); + $linkShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_LINK, null, -1, 0); + $userShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_USER, null, -1, 0); + $emailsShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_EMAIL, null, -1, 0); + $circlesShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_CIRCLE, null, -1, 0); + $remoteShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_REMOTE, null, -1, 0); + + $shares = array_merge($groupShares, $roomShares, $deckShares, $sciencemeshShares, $linkShares, $userShares, $emailsShares, $circlesShares, $remoteShares); + + $shares = array_map(function (IShare $share) { + return $this->formatShare($share); + }, $shares); + + return new DataResponse($shares); + } +} diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index 4cff998724207..53a719cab737f 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -18,7 +18,6 @@ use OCA\Files_Sharing\External\Storage; use OCA\Files_Sharing\ResponseDefinitions; use OCA\Files_Sharing\SharedStorage; -use OCA\GlobalSiteSelector\Service\SlaveService; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -28,7 +27,6 @@ use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSNotFoundException; -use OCP\AppFramework\OCSController; use OCP\AppFramework\QueryException; use OCP\Constants; use OCP\Files\File; @@ -48,7 +46,6 @@ use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Mail\IMailer; -use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; @@ -56,7 +53,6 @@ use OCP\Share\IShare; use OCP\Share\IShareProviderWithNotification; use OCP\UserStatus\IManager as IUserStatusManager; -use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -65,388 +61,47 @@ * * @psalm-import-type Files_SharingShare from ResponseDefinitions */ -class ShareAPIController extends OCSController { +class ShareAPIController extends ShareApiControllerFactory { private ?Node $lockedNode = null; - private string $currentUser; - /** - * Share20OCS constructor. - */ public function __construct( - string $appName, IRequest $request, - private IManager $shareManager, - private IGroupManager $groupManager, - private IUserManager $userManager, - private IRootFolder $rootFolder, - private IURLGenerator $urlGenerator, - private IL10N $l, - private IConfig $config, - private IAppManager $appManager, - private ContainerInterface $serverContainer, - private IUserStatusManager $userStatusManager, - private IPreview $previewManager, - private IDateTimeZone $dateTimeZone, - private LoggerInterface $logger, - private IProviderFactory $factory, - private IMailer $mailer, - ?string $userId = null, + protected IManager $shareManager, + protected IGroupManager $groupManager, + protected IUserManager $userManager, + protected IRootFolder $rootFolder, + protected IURLGenerator $urlGenerator, + protected IL10N $l, + protected IConfig $config, + protected IAppManager $appManager, + protected ContainerInterface $serverContainer, + protected IUserStatusManager $userStatusManager, + protected IPreview $previewManager, + protected IDateTimeZone $dateTimeZone, + protected LoggerInterface $logger, + protected IProviderFactory $factory, + protected IMailer $mailer, + string $userId, ) { - parent::__construct($appName, $request); - $this->currentUser = $userId; - } - - /** - * Convert an IShare to an array for OCS output - * - * @param \OCP\Share\IShare $share - * @param Node|null $recipientNode - * @return Files_SharingShare - * @throws NotFoundException In case the node can't be resolved. - * - * @suppress PhanUndeclaredClassMethod - */ - protected function formatShare(IShare $share, ?Node $recipientNode = null): array { - $sharedBy = $this->userManager->get($share->getSharedBy()); - $shareOwner = $this->userManager->get($share->getShareOwner()); - - $isOwnShare = false; - if ($shareOwner !== null) { - $isOwnShare = $shareOwner->getUID() === $this->currentUser; - } - - $result = [ - 'id' => $share->getId(), - 'share_type' => $share->getShareType(), - 'uid_owner' => $share->getSharedBy(), - 'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(), - // recipient permissions - 'permissions' => $share->getPermissions(), - // current user permissions on this share - 'can_edit' => $this->canEditShare($share), - 'can_delete' => $this->canDeleteShare($share), - 'stime' => $share->getShareTime()->getTimestamp(), - 'parent' => null, - 'expiration' => null, - 'token' => null, - 'uid_file_owner' => $share->getShareOwner(), - 'note' => $share->getNote(), - 'label' => $share->getLabel(), - 'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(), - ]; - - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); - if ($recipientNode) { - $node = $recipientNode; - } else { - $node = $userFolder->getFirstNodeById($share->getNodeId()); - if (!$node) { - // fallback to guessing the path - $node = $userFolder->get($share->getTarget()); - if ($node === null || $share->getTarget() === '') { - throw new NotFoundException(); - } - } - } - - $result['path'] = $userFolder->getRelativePath($node->getPath()); - if ($node instanceof Folder) { - $result['item_type'] = 'folder'; - } else { - $result['item_type'] = 'file'; - } - - // Get the original node permission if the share owner is the current user - if ($isOwnShare) { - $result['item_permissions'] = $node->getPermissions(); - } - - // If we're on the recipient side, the node permissions - // are bound to the share permissions. So we need to - // adjust the permissions to the share permissions if necessary. - if (!$isOwnShare) { - $result['item_permissions'] = $share->getPermissions(); - - // For some reason, single files share are forbidden to have the delete permission - // since we have custom methods to check those, let's adjust straight away. - // DAV permissions does not have that issue though. - if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) { - $result['item_permissions'] |= Constants::PERMISSION_DELETE; - } - if ($this->canEditShare($share)) { - $result['item_permissions'] |= Constants::PERMISSION_UPDATE; - } - } - - // See MOUNT_ROOT_PROPERTYNAME dav property - $result['is-mount-root'] = $node->getInternalPath() === ''; - $result['mount-type'] = $node->getMountPoint()->getMountType(); - - $result['mimetype'] = $node->getMimetype(); - $result['has_preview'] = $this->previewManager->isAvailable($node); - $result['storage_id'] = $node->getStorage()->getId(); - $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId(); - $result['item_source'] = $node->getId(); - $result['file_source'] = $node->getId(); - $result['file_parent'] = $node->getParent()->getId(); - $result['file_target'] = $share->getTarget(); - $result['item_size'] = $node->getSize(); - $result['item_mtime'] = $node->getMTime(); - - $expiration = $share->getExpirationDate(); - if ($expiration !== null) { - $expiration->setTimezone($this->dateTimeZone->getTimeZone()); - $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); - } - - if ($share->getShareType() === IShare::TYPE_USER) { - $sharedWith = $this->userManager->get($share->getSharedWith()); - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith(); - $result['share_with_displayname_unique'] = $sharedWith !== null ? ( - !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID() - ) : $share->getSharedWith(); - - $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]); - $userStatus = array_shift($userStatuses); - if ($userStatus) { - $result['status'] = [ - 'status' => $userStatus->getStatus(), - 'message' => $userStatus->getMessage(), - 'icon' => $userStatus->getIcon(), - 'clearAt' => $userStatus->getClearAt() - ? (int)$userStatus->getClearAt()->format('U') - : null, - ]; - } - } elseif ($share->getShareType() === IShare::TYPE_GROUP) { - $group = $this->groupManager->get($share->getSharedWith()); - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith(); - } elseif ($share->getShareType() === IShare::TYPE_LINK) { - - // "share_with" and "share_with_displayname" for passwords of link - // shares was deprecated in Nextcloud 15, use "password" instead. - $result['share_with'] = $share->getPassword(); - $result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')'; - - $result['password'] = $share->getPassword(); - - $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); - - $result['token'] = $share->getToken(); - $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]); - } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith()); - $result['token'] = $share->getToken(); - } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD'); - $result['token'] = $share->getToken(); - } elseif ($share->getShareType() === IShare::TYPE_EMAIL) { - $result['share_with'] = $share->getSharedWith(); - $result['password'] = $share->getPassword(); - $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null; - $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); - $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL'); - $result['token'] = $share->getToken(); - } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) { - // getSharedWith() returns either "name (type, owner)" or - // "name (type, owner) [id]", depending on the Teams app version. - $hasCircleId = (substr($share->getSharedWith(), -1) === ']'); - - $result['share_with_displayname'] = $share->getSharedWithDisplayName(); - if (empty($result['share_with_displayname'])) { - $displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith())); - $result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength); - } - - $result['share_with_avatar'] = $share->getSharedWithAvatar(); - - $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0); - $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' ')); - if ($shareWithLength === false) { - $result['share_with'] = substr($share->getSharedWith(), $shareWithStart); - } else { - $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength); - } - } elseif ($share->getShareType() === IShare::TYPE_ROOM) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */ - $roomShare = $this->getRoomShareHelper()->formatShare($share); - $result = array_merge($result, $roomShare); - } catch (QueryException $e) { - } - } elseif ($share->getShareType() === IShare::TYPE_DECK) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */ - $deckShare = $this->getDeckShareHelper()->formatShare($share); - $result = array_merge($result, $deckShare); - } catch (QueryException $e) { - } - } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { - $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = ''; - - try { - /** @var array{share_with: string, share_with_displayname: string, token: string} $scienceMeshShare */ - $scienceMeshShare = $this->getSciencemeshShareHelper()->formatShare($share); - $result = array_merge($result, $scienceMeshShare); - } catch (QueryException $e) { - } - } - - - $result['mail_send'] = $share->getMailSend() ? 1 : 0; - $result['hide_download'] = $share->getHideDownload() ? 1 : 0; - - $result['attributes'] = null; - if ($attributes = $share->getAttributes()) { - $result['attributes'] = (string)\json_encode($attributes->toArray()); - } - - return $result; - } - - /** - * Check if one of the users address books knows the exact property, if - * not we return the full name. - * - * @param string $query - * @param string $property - * @return string - */ - private function getDisplayNameFromAddressBook(string $query, string $property): string { - // FIXME: If we inject the contacts manager it gets initialized before any address books are registered - try { - $result = \OC::$server->getContactsManager()->search($query, [$property], [ - 'limit' => 1, - 'enumeration' => false, - 'strict_search' => true, - ]); - } catch (Exception $e) { - $this->logger->error( - $e->getMessage(), - ['exception' => $e] - ); - return $query; - } - - foreach ($result as $r) { - foreach ($r[$property] as $value) { - if ($value === $query && $r['FN']) { - return $r['FN']; - } - } - } - - return $query; - } - - - /** - * @param array $shares - * @param array|null $updatedDisplayName - * - * @return array - */ - private function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array { - $userIds = $updated = []; - foreach ($shares as $share) { - // share is federated and share have no display name yet - if ($share['share_type'] === IShare::TYPE_REMOTE - && ($share['share_with'] ?? '') !== '' - && ($share['share_with_displayname'] ?? '') === '') { - $userIds[] = $userId = $share['share_with']; - - if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) { - $share['share_with_displayname'] = $updatedDisplayName[$userId]; - } - } - - // prepping userIds with displayName to be updated - $updated[] = $share; - } - - // if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares - if ($updatedDisplayName !== null) { - return $updated; - } - - // get displayName for the generated list of userId with no displayName - $displayNames = $this->retrieveFederatedDisplayName($userIds); - - // if no displayName are updated, we exit - if (empty($displayNames)) { - return $updated; - } - - // let's fix missing display name and returns all shares - return $this->fixMissingDisplayName($shares, $displayNames); - } - - - /** - * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app. - * returns an array with userIds as keys and displayName as values. - * - * @param array $userIds - * @param bool $cacheOnly - do not reach LUS, get data from cache. - * - * @return array - * @throws ContainerExceptionInterface - */ - private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array { - // check if gss is enabled and available - if (count($userIds) === 0 - || !$this->appManager->isInstalled('globalsiteselector') - || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) { - return []; - } - - try { - $slaveService = Server::get(SlaveService::class); - } catch (\Throwable $e) { - $this->logger->error( - $e->getMessage(), - ['exception' => $e] - ); - return []; - } - - return $slaveService->getUsersDisplayName($userIds, $cacheOnly); - } - - - /** - * retrieve displayName from cache if available (should be used on federated shares) - * if not available in cache/lus, try for get from address-book, else returns empty string. - * - * @param string $userId - * @param bool $cacheOnly if true will not reach the lus but will only get data from cache - * - * @return string - */ - private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string { - $details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly); - if (array_key_exists($userId, $details)) { - return $details[$userId]; - } - - $displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD'); - return ($displayName === $userId) ? '' : $displayName; + parent::__construct( + $request, + $shareManager, + $userId, + $userManager, + $groupManager, + $rootFolder, + $appManager, + $serverContainer, + $userStatusManager, + $previewManager, + $dateTimeZone, + $urlGenerator, + $l, + $logger, + ); } - - /** * Get a specific share by id * @@ -1528,150 +1183,6 @@ protected function canAccessShare(IShare $share, bool $checkGroups = true): bool return false; } - /** - * Does the user have edit permission on the share - * - * @param \OCP\Share\IShare $share the share to check - * @return boolean - */ - protected function canEditShare(IShare $share): bool { - // A file with permissions 0 can't be accessed by us. So Don't show it - if ($share->getPermissions() === 0) { - return false; - } - - // The owner of the file and the creator of the share - // can always edit the share - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser - ) { - return true; - } - - //! we do NOT support some kind of `admin` in groups. - //! You cannot edit shares shared to a group you're - //! a member of if you're not the share owner or the file owner! - - return false; - } - - /** - * Does the user have delete permission on the share - * - * @param \OCP\Share\IShare $share the share to check - * @return boolean - */ - protected function canDeleteShare(IShare $share): bool { - // A file with permissions 0 can't be accessed by us. So Don't show it - if ($share->getPermissions() === 0) { - return false; - } - - // if the user is the recipient, i can unshare - // the share with self - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $this->currentUser - ) { - return true; - } - - // The owner of the file and the creator of the share - // can always delete the share - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser - ) { - return true; - } - - return false; - } - - /** - * Does the user have delete permission on the share - * This differs from the canDeleteShare function as it only - * remove the share for the current user. It does NOT - * completely delete the share but only the mount point. - * It can then be restored from the deleted shares section. - * - * @param \OCP\Share\IShare $share the share to check - * @return boolean - * - * @suppress PhanUndeclaredClassMethod - */ - protected function canDeleteShareFromSelf(IShare $share): bool { - if ($share->getShareType() !== IShare::TYPE_GROUP && - $share->getShareType() !== IShare::TYPE_ROOM && - $share->getShareType() !== IShare::TYPE_DECK && - $share->getShareType() !== IShare::TYPE_SCIENCEMESH - ) { - return false; - } - - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser - ) { - // Delete the whole share, not just for self - return false; - } - - // If in the recipient group, you can delete the share from self - if ($share->getShareType() === IShare::TYPE_GROUP) { - $sharedWith = $this->groupManager->get($share->getSharedWith()); - $user = $this->userManager->get($this->currentUser); - if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { - return true; - } - } - - if ($share->getShareType() === IShare::TYPE_ROOM) { - try { - return $this->getRoomShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { - return false; - } - } - - if ($share->getShareType() === IShare::TYPE_DECK) { - try { - return $this->getDeckShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { - return false; - } - } - - if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { - try { - return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { - return false; - } - } - - return false; - } - - /** - * Make sure that the passed date is valid ISO 8601 - * So YYYY-MM-DD - * If not throw an exception - * - * @param string $expireDate - * - * @throws \Exception - * @return \DateTime - */ - private function parseDate(string $expireDate): \DateTime { - try { - $date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone()); - // Make sure it expires at midnight in owner timezone - $date->setTime(0, 0, 0); - } catch (\Exception $e) { - throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD')); - } - - return $date; - } - /** * Since we have multiple providers but the OCS Share API v1 does * not support this we need to check all backends. @@ -1764,57 +1275,6 @@ public function cleanup() { } } - /** - * Returns the helper of ShareAPIController for room shares. - * - * If the Talk application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Talk\Share\Helper\ShareAPIController - * @throws QueryException - */ - private function getRoomShareHelper() { - if (!$this->appManager->isEnabledForUser('spreed')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController'); - } - - /** - * Returns the helper of ShareAPIHelper for deck shares. - * - * If the Deck application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException - */ - private function getDeckShareHelper() { - if (!$this->appManager->isEnabledForUser('deck')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper'); - } - - /** - * Returns the helper of ShareAPIHelper for sciencemesh shares. - * - * If the sciencemesh application is not enabled or the helper is not available - * a QueryException is thrown instead. - * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException - */ - private function getSciencemeshShareHelper() { - if (!$this->appManager->isEnabledForUser('sciencemesh')) { - throw new QueryException(); - } - - return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); - } - /** * @param string $viewer * @param Node $node diff --git a/apps/files_sharing/lib/Controller/ShareApiControllerFactory.php b/apps/files_sharing/lib/Controller/ShareApiControllerFactory.php new file mode 100644 index 0000000000000..a29ab8c1d20ce --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareApiControllerFactory.php @@ -0,0 +1,628 @@ +currentUser = $userId; + } + + /** + * Convert an IShare to an array for OCS output + * + * @param \OCP\Share\IShare $share + * @param Node|null $recipientNode + * @return Files_SharingShare + * @throws NotFoundException In case the node can't be resolved. + * + * @suppress PhanUndeclaredClassMethod + */ + public function formatShare(IShare $share, ?Node $recipientNode = null): array { + $sharedBy = $this->userManager->get($share->getSharedBy()); + $shareOwner = $this->userManager->get($share->getShareOwner()); + + $isOwnShare = false; + if ($shareOwner !== null) { + $isOwnShare = $shareOwner->getUID() === $this->currentUser; + } + + $result = [ + 'id' => $share->getId(), + 'share_type' => $share->getShareType(), + 'uid_owner' => $share->getSharedBy(), + 'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(), + // recipient permissions + 'permissions' => $share->getPermissions(), + // current user permissions on this share + 'can_edit' => $this->canEditShare($share), + 'can_delete' => $this->canDeleteShare($share), + 'stime' => $share->getShareTime()->getTimestamp(), + 'parent' => null, + 'expiration' => null, + 'token' => null, + 'uid_file_owner' => $share->getShareOwner(), + 'note' => $share->getNote(), + 'label' => $share->getLabel(), + 'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(), + ]; + + $userFolder = $this->rootFolder->getUserFolder($this->isDeletedShareController ? $share->getSharedBy() : $this->currentUser); + if ($recipientNode) { + $node = $recipientNode; + } else { + $node = $userFolder->getFirstNodeById($share->getNodeId()); + if (!$node) { + // fallback to guessing the path + $node = $userFolder->get($share->getTarget()); + if ($node === null || $share->getTarget() === '') { + throw new NotFoundException(); + } + } + } + + $result['path'] = $userFolder->getRelativePath($node->getPath()); + if ($node instanceof Folder) { + $result['item_type'] = 'folder'; + } else { + $result['item_type'] = 'file'; + } + + // Get the original node permission if the share owner is the current user + if ($isOwnShare) { + $result['item_permissions'] = $node->getPermissions(); + } + + // If we're on the recipient side, the node permissions + // are bound to the share permissions. So we need to + // adjust the permissions to the share permissions if necessary. + if (!$isOwnShare) { + $result['item_permissions'] = $share->getPermissions(); + + // For some reason, single files share are forbidden to have the delete permission + // since we have custom methods to check those, let's adjust straight away. + // DAV permissions does not have that issue though. + if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) { + $result['item_permissions'] |= Constants::PERMISSION_DELETE; + } + if ($this->canEditShare($share)) { + $result['item_permissions'] |= Constants::PERMISSION_UPDATE; + } + } + + // See MOUNT_ROOT_PROPERTYNAME dav property + $result['is-mount-root'] = $node->getInternalPath() === ''; + $result['mount-type'] = $node->getMountPoint()->getMountType(); + + $result['mimetype'] = $node->getMimetype(); + $result['has_preview'] = $this->previewManager->isAvailable($node); + $result['storage_id'] = $node->getStorage()->getId(); + $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId(); + $result['item_source'] = $node->getId(); + $result['file_source'] = $node->getId(); + $result['file_parent'] = $node->getParent()->getId(); + $result['file_target'] = $share->getTarget(); + $result['item_size'] = $node->getSize(); + $result['item_mtime'] = $node->getMTime(); + + $expiration = $share->getExpirationDate(); + if ($expiration !== null) { + $expiration->setTimezone($this->dateTimeZone->getTimeZone()); + $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); + } + + if ($share->getShareType() === IShare::TYPE_USER) { + $sharedWith = $this->userManager->get($share->getSharedWith()); + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith(); + $result['share_with_displayname_unique'] = $sharedWith !== null ? ( + !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID() + ) : $share->getSharedWith(); + + $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]); + $userStatus = array_shift($userStatuses); + if ($userStatus) { + $result['status'] = [ + 'status' => $userStatus->getStatus(), + 'message' => $userStatus->getMessage(), + 'icon' => $userStatus->getIcon(), + 'clearAt' => $userStatus->getClearAt() + ? (int)$userStatus->getClearAt()->format('U') + : null, + ]; + } + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { + $group = $this->groupManager->get($share->getSharedWith()); + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith(); + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + + // "share_with" and "share_with_displayname" for passwords of link + // shares was deprecated in Nextcloud 15, use "password" instead. + $result['share_with'] = $share->getPassword(); + $result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')'; + + $result['password'] = $share->getPassword(); + + $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); + + $result['token'] = $share->getToken(); + $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]); + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith()); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD'); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_EMAIL) { + $result['share_with'] = $share->getSharedWith(); + $result['password'] = $share->getPassword(); + $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null; + $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL'); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) { + // getSharedWith() returns either "name (type, owner)" or + // "name (type, owner) [id]", depending on the Teams app version. + $hasCircleId = (substr($share->getSharedWith(), -1) === ']'); + + $result['share_with_displayname'] = $share->getSharedWithDisplayName(); + if (empty($result['share_with_displayname'])) { + $displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith())); + $result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength); + } + + $result['share_with_avatar'] = $share->getSharedWithAvatar(); + + $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0); + $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' ')); + if ($shareWithLength === false) { + $result['share_with'] = substr($share->getSharedWith(), $shareWithStart); + } else { + $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength); + } + } elseif ($share->getShareType() === IShare::TYPE_ROOM) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = ''; + + try { + $roomShareHelper = $this->isDeletedShareController + ? $this->getDeletedRoomShareHelper() + : $this->getRoomShareHelper(); + + /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */ + $roomShare = $roomShareHelper->formatShare($share); + $result = array_merge($result, $roomShare); + } catch (QueryException $e) { + } + } elseif ($share->getShareType() === IShare::TYPE_DECK) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = ''; + + try { + /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */ + $deckShare = $this->getDeckShareHelper()->formatShare($share); + $result = array_merge($result, $deckShare); + } catch (QueryException $e) { + } + } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = ''; + + try { + /** @var array{share_with: string, share_with_displayname: string, token: string} $scienceMeshShare */ + $scienceMeshShare = $this->getSciencemeshShareHelper()->formatShare($share); + $result = array_merge($result, $scienceMeshShare); + } catch (QueryException $e) { + } + } + + + $result['mail_send'] = $share->getMailSend() ? 1 : 0; + $result['hide_download'] = $share->getHideDownload() ? 1 : 0; + + $result['attributes'] = null; + if ($attributes = $share->getAttributes()) { + $result['attributes'] = (string)\json_encode($attributes->toArray()); + } + + return $result; + } + + /** + * Returns the helper of ShareAPIController for room shares. + * + * If the Talk application is not enabled or the helper is not available + * a QueryException is thrown instead. + * + * @return \OCA\Talk\Share\Helper\ShareAPIController + * @throws QueryException + */ + public function getRoomShareHelper() { + if (!$this->appManager->isEnabledForUser('spreed')) { + throw new QueryException(); + } + + return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController'); + } + + /** + * Returns the helper of DeletedShareAPIController for room shares. + * + * If the Talk application is not enabled or the helper is not available + * a QueryException is thrown instead. + * + * @return \OCA\Talk\Share\Helper\DeletedShareAPIController + * @throws QueryException + */ + public function getDeletedRoomShareHelper() { + if (!$this->appManager->isEnabledForUser('spreed')) { + throw new QueryException(); + } + + return $this->serverContainer->get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); + } + + /** + * Returns the helper of DeletedShareAPIHelper for deck shares. + * + * If the Deck application is not enabled or the helper is not available + * a QueryException is thrown instead. + * + * @return \OCA\Deck\Sharing\ShareAPIHelper + * @throws QueryException + */ + public function getDeckShareHelper() { + if (!$this->appManager->isEnabledForUser('deck')) { + throw new QueryException(); + } + + return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper'); + } + + /** + * Returns the helper of DeletedShareAPIHelper for sciencemesh shares. + * + * If the sciencemesh application is not enabled or the helper is not available + * a QueryException is thrown instead. + * + * @return \OCA\Deck\Sharing\ShareAPIHelper + * @throws QueryException + */ + public function getSciencemeshShareHelper() { + if (!$this->appManager->isEnabledForUser('sciencemesh')) { + throw new QueryException(); + } + + return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); + } + + /** + * Does the user have edit permission on the share + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + */ + public function canEditShare(IShare $share): bool { + // A file with permissions 0 can't be accessed by us. So Don't show it + if ($share->getPermissions() === 0) { + return false; + } + + // The owner of the file and the creator of the share + // can always edit the share + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + return true; + } + + //! we do NOT support some kind of `admin` in groups. + //! You cannot edit shares shared to a group you're + //! a member of if you're not the share owner or the file owner! + + return false; + } + + /** + * Does the user have delete permission on the share + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + */ + public function canDeleteShare(IShare $share): bool { + // A file with permissions 0 can't be accessed by us. So Don't show it + if ($share->getPermissions() === 0) { + return false; + } + + // if the user is the recipient, i can unshare + // the share with self + if ($share->getShareType() === IShare::TYPE_USER && + $share->getSharedWith() === $this->currentUser + ) { + return true; + } + + // The owner of the file and the creator of the share + // can always delete the share + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + return true; + } + + return false; + } + + /** + * Does the user have delete permission on the share + * This differs from the canDeleteShare function as it only + * remove the share for the current user. It does NOT + * completely delete the share but only the mount point. + * It can then be restored from the deleted shares section. + * + * @param \OCP\Share\IShare $share the share to check + * @return boolean + * + * @suppress PhanUndeclaredClassMethod + */ + public function canDeleteShareFromSelf(IShare $share): bool { + if ($share->getShareType() !== IShare::TYPE_GROUP && + $share->getShareType() !== IShare::TYPE_ROOM && + $share->getShareType() !== IShare::TYPE_DECK && + $share->getShareType() !== IShare::TYPE_SCIENCEMESH + ) { + return false; + } + + if ($share->getShareOwner() === $this->currentUser || + $share->getSharedBy() === $this->currentUser + ) { + // Delete the whole share, not just for self + return false; + } + + // If in the recipient group, you can delete the share from self + if ($share->getShareType() === IShare::TYPE_GROUP) { + $sharedWith = $this->groupManager->get($share->getSharedWith()); + $user = $this->userManager->get($this->currentUser); + if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { + return true; + } + } + + if ($share->getShareType() === IShare::TYPE_ROOM) { + try { + return $this->getRoomShareHelper()->canAccessShare($share, $this->currentUser); + } catch (QueryException $e) { + return false; + } + } + + if ($share->getShareType() === IShare::TYPE_DECK) { + try { + return $this->getDeckShareHelper()->canAccessShare($share, $this->currentUser); + } catch (QueryException $e) { + return false; + } + } + + if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + try { + return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->currentUser); + } catch (QueryException $e) { + return false; + } + } + + return false; + } + + /** + * Make sure that the passed date is valid ISO 8601 + * So YYYY-MM-DD + * If not throw an exception + * + * @param string $expireDate + * + * @throws \Exception + * @return \DateTime + */ + public function parseDate(string $expireDate): \DateTime { + try { + $date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone()); + // Make sure it expires at midnight in owner timezone + $date->setTime(0, 0, 0); + } catch (\Exception $e) { + throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD')); + } + + return $date; + } + + /** + * retrieve displayName from cache if available (should be used on federated shares) + * if not available in cache/lus, try for get from address-book, else returns empty string. + * + * @param string $userId + * @param bool $cacheOnly if true will not reach the lus but will only get data from cache + * + * @return string + */ + public function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string { + $details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly); + if (array_key_exists($userId, $details)) { + return $details[$userId]; + } + + $displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD'); + return ($displayName === $userId) ? '' : $displayName; + } + + /** + * Check if one of the users address books knows the exact property, if + * not we return the full name. + * + * @param string $query + * @param string $property + * @return string + */ + public function getDisplayNameFromAddressBook(string $query, string $property): string { + // FIXME: If we inject the contacts manager it gets initialized before any address books are registered + try { + $result = \OC::$server->getContactsManager()->search($query, [$property], [ + 'limit' => 1, + 'enumeration' => false, + 'strict_search' => true, + ]); + } catch (\Exception $e) { + $this->logger->error( + $e->getMessage(), + ['exception' => $e] + ); + return $query; + } + + foreach ($result as $r) { + foreach ($r[$property] as $value) { + if ($value === $query && $r['FN']) { + return $r['FN']; + } + } + } + + return $query; + } + + + /** + * @param array $shares + * @param array|null $updatedDisplayName + * + * @return array + */ + public function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array { + $userIds = $updated = []; + foreach ($shares as $share) { + // share is federated and share have no display name yet + if ($share['share_type'] === IShare::TYPE_REMOTE + && ($share['share_with'] ?? '') !== '' + && ($share['share_with_displayname'] ?? '') === '') { + $userIds[] = $userId = $share['share_with']; + + if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) { + $share['share_with_displayname'] = $updatedDisplayName[$userId]; + } + } + + // prepping userIds with displayName to be updated + $updated[] = $share; + } + + // if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares + if ($updatedDisplayName !== null) { + return $updated; + } + + // get displayName for the generated list of userId with no displayName + $displayNames = $this->retrieveFederatedDisplayName($userIds); + + // if no displayName are updated, we exit + if (empty($displayNames)) { + return $updated; + } + + // let's fix missing display name and returns all shares + return $this->fixMissingDisplayName($shares, $displayNames); + } + + + /** + * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app. + * returns an array with userIds as keys and displayName as values. + * + * @param array $userIds + * @param bool $cacheOnly - do not reach LUS, get data from cache. + * + * @return array + * @throws ContainerExceptionInterface + */ + public function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array { + // check if gss is enabled and available + if (count($userIds) === 0 + || !$this->appManager->isInstalled('globalsiteselector') + || !$this->appManager->isEnabledForUser('globalsiteselector') + || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) { + return []; + } + + try { + /** @var \OCA\GlobalSiteSelector\Service\SlaveService $slaveService */ + $slaveService = $this->serverContainer->get('\OCA\GlobalSiteSelector\Service\SlaveService'); + } catch (\Throwable $e) { + $this->logger->error( + $e->getMessage(), + ['exception' => $e] + ); + return []; + } + + return $slaveService->getUsersDisplayName($userIds, $cacheOnly); + } +} diff --git a/apps/files_sharing/lib/ExpireSharesJob.php b/apps/files_sharing/lib/ExpireSharesJob.php index 3900225092455..1d511f813e9e3 100644 --- a/apps/files_sharing/lib/ExpireSharesJob.php +++ b/apps/files_sharing/lib/ExpireSharesJob.php @@ -8,25 +8,25 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; +use OCP\IAppConfig; use OCP\IDBConnection; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; /** * Delete all shares that are expired */ class ExpireSharesJob extends TimedJob { - /** @var IManager */ - private $shareManager; - - /** @var IDBConnection */ - private $db; - - public function __construct(ITimeFactory $time, IManager $shareManager, IDBConnection $db) { - $this->shareManager = $shareManager; - $this->db = $db; + public function __construct( + ITimeFactory $time, + private IManager $shareManager, + private IDBConnection $db, + private IAppConfig $appConfig, + private LoggerInterface $logger, + ) { parent::__construct($time); @@ -42,13 +42,16 @@ public function __construct(ITimeFactory $time, IManager $shareManager, IDBConne * @param array $argument unused argument */ public function run($argument) { - //Current time + if ($this->appConfig->getValueString('core', 'shareapi_delete_on_expire', 'yes') === 'no') { + $this->logger->info('Share deletion on expiration is disabled'); + return; + } + + // Current time $now = new \DateTime(); $now = $now->format('Y-m-d H:i:s'); - /* - * Expire file link shares only (for now) - */ + // Expire file link shares only (for now) $qb = $this->db->getQueryBuilder(); $qb->select('id', 'share_type') ->from('share') diff --git a/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php index d412b93a1355d..267394f6faba6 100644 --- a/apps/files_sharing/lib/ResponseDefinitions.php +++ b/apps/files_sharing/lib/ResponseDefinitions.php @@ -56,29 +56,6 @@ * url?: string, * } * - * @psalm-type Files_SharingDeletedShare = array{ - * id: string, - * share_type: int, - * uid_owner: string, - * displayname_owner: string, - * permissions: int, - * stime: int, - * uid_file_owner: string, - * displayname_file_owner: string, - * path: string, - * item_type: string, - * mimetype: string, - * storage: int, - * item_source: int, - * file_source: int, - * file_parent: int, - * file_target: int, - * expiration: string|null, - * share_with: string|null, - * share_with_displayname: string|null, - * share_with_link: string|null, - * } - * * @psalm-type Files_SharingRemoteShare = array{ * accepted: bool, * file_id: int|null, diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index 9f38223bca39e..cc4ee55b79c0c 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -244,105 +244,6 @@ } } }, - "DeletedShare": { - "type": "object", - "required": [ - "id", - "share_type", - "uid_owner", - "displayname_owner", - "permissions", - "stime", - "uid_file_owner", - "displayname_file_owner", - "path", - "item_type", - "mimetype", - "storage", - "item_source", - "file_source", - "file_parent", - "file_target", - "expiration", - "share_with", - "share_with_displayname", - "share_with_link" - ], - "properties": { - "id": { - "type": "string" - }, - "share_type": { - "type": "integer", - "format": "int64" - }, - "uid_owner": { - "type": "string" - }, - "displayname_owner": { - "type": "string" - }, - "permissions": { - "type": "integer", - "format": "int64" - }, - "stime": { - "type": "integer", - "format": "int64" - }, - "uid_file_owner": { - "type": "string" - }, - "displayname_file_owner": { - "type": "string" - }, - "path": { - "type": "string" - }, - "item_type": { - "type": "string" - }, - "mimetype": { - "type": "string" - }, - "storage": { - "type": "integer", - "format": "int64" - }, - "item_source": { - "type": "integer", - "format": "int64" - }, - "file_source": { - "type": "integer", - "format": "int64" - }, - "file_parent": { - "type": "integer", - "format": "int64" - }, - "file_target": { - "type": "integer", - "format": "int64" - }, - "expiration": { - "type": "string", - "nullable": true - }, - "share_with": { - "type": "string", - "nullable": true - }, - "share_with_displayname": { - "type": "string", - "nullable": true - }, - "share_with_link": { - "type": "string", - "nullable": true - } - } - }, "Lookup": { "type": "object", "required": [ @@ -2891,7 +2792,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/DeletedShare" + "$ref": "#/components/schemas/Share" } } } @@ -3000,6 +2901,70 @@ } } }, + "/ocs/v2.php/apps/files_sharing/api/v1/expiredshares": { + "get": { + "operationId": "expired_shareapi-index", + "summary": "Get a list of all expired shares", + "tags": [ + "expired_shareapi" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Deleted shares returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Share" + } + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/files_sharing/api/v1/sharees": { "get": { "operationId": "shareesapi-search", diff --git a/apps/files_sharing/src/files_actions/sharingStatusAction.ts b/apps/files_sharing/src/files_actions/sharingStatusAction.ts index bd9448689a2be..9edc45da7fe78 100644 --- a/apps/files_sharing/src/files_actions/sharingStatusAction.ts +++ b/apps/files_sharing/src/files_actions/sharingStatusAction.ts @@ -14,6 +14,7 @@ import LinkSvg from '@mdi/svg/svg/link.svg?raw' import CircleSvg from '../../../../core/img/apps/circles.svg?raw' import { action as sidebarAction } from '../../../files/src/actions/sidebarAction' +import { expiredSharesViewId } from '../files_views/shares' import { generateAvatarSvg } from '../utils/AccountIcon' import './sharingStatusAction.scss' @@ -24,10 +25,14 @@ const isExternal = (node: Node) => { export const action = new FileAction({ id: 'sharing-status', - displayName(nodes: Node[]) { + displayName(nodes: Node[], view: View) { const node = nodes[0] const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[] + if (view.id === expiredSharesViewId) { + return t('files_sharing', 'Expired') + } + if (shareTypes.length > 0 || (node.owner !== getCurrentUser()?.uid || isExternal(node))) { return t('files_sharing', 'Shared') diff --git a/apps/files_sharing/src/files_views/shares.spec.ts b/apps/files_sharing/src/files_views/shares.spec.ts index 153057bc0ad69..54662f5ad2387 100644 --- a/apps/files_sharing/src/files_views/shares.spec.ts +++ b/apps/files_sharing/src/files_views/shares.spec.ts @@ -34,12 +34,12 @@ describe('Sharing views definition', () => { const shareOverviewView = Navigation.views.find(view => view.id === 'shareoverview') as View const sharesChildViews = Navigation.views.filter(view => view.parent === 'shareoverview') as View[] - expect(Navigation.register).toHaveBeenCalledTimes(7) + expect(Navigation.register).toHaveBeenCalledTimes(8) // one main view and no children - expect(Navigation.views.length).toBe(7) + expect(Navigation.views.length).toBe(8) expect(shareOverviewView).toBeDefined() - expect(sharesChildViews.length).toBe(6) + expect(sharesChildViews.length).toBe(7) expect(shareOverviewView?.id).toBe('shareoverview') expect(shareOverviewView?.name).toBe('Shares') @@ -55,6 +55,7 @@ describe('Sharing views definition', () => { { id: 'sharinglinks', name: 'Shared by link' }, { id: 'filerequest', name: 'File requests' }, { id: 'deletedshares', name: 'Deleted shares' }, + { id: 'expiredshares', name: 'Expired shares', columns: 1 }, { id: 'pendingshares', name: 'Pending shares' }, ] @@ -67,7 +68,7 @@ describe('Sharing views definition', () => { expect(view?.emptyCaption).toBeDefined() expect(view?.icon).match(//) expect(view?.order).toBe(index + 1) - expect(view?.columns).toStrictEqual([]) + expect(view?.columns).toHaveLength(dataProvider[index].columns || 0) expect(view?.getContents).toBeDefined() }) }) @@ -98,7 +99,7 @@ describe('Sharing views contents', () => { }) registerSharingViews() - expect(Navigation.views.length).toBe(7) + expect(Navigation.views.length).toBe(8) Navigation.views.forEach(async (view: View) => { const content = await view.getContents('/') expect(content.contents).toStrictEqual([]) diff --git a/apps/files_sharing/src/files_views/shares.ts b/apps/files_sharing/src/files_views/shares.ts index 7aec0dbeafb70..3ff04b7708cfd 100644 --- a/apps/files_sharing/src/files_views/shares.ts +++ b/apps/files_sharing/src/files_views/shares.ts @@ -3,8 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { translate as t } from '@nextcloud/l10n' -import { View, getNavigation } from '@nextcloud/files' +import { Column, View, getNavigation } from '@nextcloud/files' import { ShareType } from '@nextcloud/sharing' +import moment from '@nextcloud/moment' + +import AccountArrowLeftSvg from '@mdi/svg/svg/account-arrow-left.svg?raw' import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw' import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw' import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw' @@ -22,6 +25,7 @@ export const sharingByLinksViewId = 'sharinglinks' export const deletedSharesViewId = 'deletedshares' export const pendingSharesViewId = 'pendingshares' export const fileRequestViewId = 'filerequest' +export const expiredSharesViewId = 'expiredshares' export default () => { const Navigation = getNavigation() @@ -89,7 +93,7 @@ export default () => { columns: [], - getContents: () => getContents(false, true, false, false, [ShareType.Link]), + getContents: () => getContents(false, true, false, false, false, [ShareType.Link]), })) Navigation.register(new View({ @@ -106,7 +110,7 @@ export default () => { columns: [], - getContents: () => getContents(false, true, false, false, [ShareType.Link, ShareType.Email]) + getContents: () => getContents(false, true, false, false, false, [ShareType.Link, ShareType.Email]) .then(({ folder, contents }) => { return { folder, @@ -132,6 +136,46 @@ export default () => { getContents: () => getContents(false, false, false, true), })) + Navigation.register(new View({ + id: expiredSharesViewId, + name: t('files_sharing', 'Expired shares'), + caption: t('files_sharing', 'List of shares that expired.'), + + emptyTitle: t('files_sharing', 'No expired shares'), + emptyCaption: t('files_sharing', 'Shares that have expired will show up here'), + + icon: AccountClockSvg, + order: 6, + parent: sharesViewId, + + columns: [ + new Column({ + id: 'expired', + title: t('files_sharing', 'Expired'), + render(node) { + const expirationTime = node.attributes?.expiration + const span = document.createElement('span') + if (expirationTime) { + span.title = moment.unix(expirationTime).format('LLL') + span.textContent = moment.unix(expirationTime).fromNow() + return span + } + + // Unknown expiration time + span.textContent = t('files_sharing', 'A long time ago') + return span + }, + sort(nodeA, nodeB) { + const expirationTimeA = nodeA.attributes?.expiration || 0 + const expirationTimeB = nodeB.attributes?.expiration || 0 + return expirationTimeB - expirationTimeA + }, + }), + ], + + getContents: () => getContents(false, false, false, false, true), + })) + Navigation.register(new View({ id: pendingSharesViewId, name: t('files_sharing', 'Pending shares'), @@ -140,8 +184,8 @@ export default () => { emptyTitle: t('files_sharing', 'No pending shares'), emptyCaption: t('files_sharing', 'Shares you have received but not approved will show up here'), - icon: AccountClockSvg, - order: 6, + icon: AccountArrowLeftSvg, + order: 7, parent: sharesViewId, columns: [], diff --git a/apps/files_sharing/src/services/SharingService.spec.ts b/apps/files_sharing/src/services/SharingService.spec.ts index daba81bd4f2f3..332c35b84b824 100644 --- a/apps/files_sharing/src/services/SharingService.spec.ts +++ b/apps/files_sharing/src/services/SharingService.spec.ts @@ -46,7 +46,7 @@ describe('SharingService methods definitions', () => { }) test('Shared with you', async () => { - await getContents(true, false, false, false, []) + await getContents(true, false, false, false, false, []) expect(axios.get).toHaveBeenCalledTimes(2) expect(axios.get).toHaveBeenNthCalledWith(1, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { @@ -69,7 +69,7 @@ describe('SharingService methods definitions', () => { }) test('Shared with others', async () => { - await getContents(false, true, false, false, []) + await getContents(false, true, false, false, false, []) expect(axios.get).toHaveBeenCalledTimes(1) expect(axios.get).toHaveBeenCalledWith('http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { @@ -84,7 +84,7 @@ describe('SharingService methods definitions', () => { }) test('Pending shares', async () => { - await getContents(false, false, true, false, []) + await getContents(false, false, true, false, false, []) expect(axios.get).toHaveBeenCalledTimes(2) expect(axios.get).toHaveBeenNthCalledWith(1, 'http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares/pending', { @@ -106,7 +106,7 @@ describe('SharingService methods definitions', () => { }) test('Deleted shares', async () => { - await getContents(false, true, false, false, []) + await getContents(false, true, false, false, false, []) expect(axios.get).toHaveBeenCalledTimes(1) expect(axios.get).toHaveBeenCalledWith('http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/shares', { @@ -120,9 +120,23 @@ describe('SharingService methods definitions', () => { }) }) + test('Expired shares', async () => { + await getContents(false, false, false, false, true, []) + + expect(axios.get).toHaveBeenCalledTimes(1) + expect(axios.get).toHaveBeenCalledWith('http://nextcloud.local/ocs/v2.php/apps/files_sharing/api/v1/expiredshares', { + headers: { + 'Content-Type': 'application/json', + }, + params: { + include_tags: true, + }, + }) + }) + test('Unknown owner', async () => { vi.spyOn(auth, 'getCurrentUser').mockReturnValue(null) - const results = await getContents(false, true, false, false, []) + const results = await getContents(false, true, false, false, false, []) expect(results.folder.owner).toEqual(null) }) @@ -170,7 +184,7 @@ describe('SharingService filtering', () => { }) test('Shared with others filtering', async () => { - const shares = await getContents(false, true, false, false, [ShareType.User]) + const shares = await getContents(false, true, false, false, false, [ShareType.User]) expect(axios.get).toHaveBeenCalledTimes(1) expect(shares.contents).toHaveLength(1) @@ -179,7 +193,7 @@ describe('SharingService filtering', () => { }) test('Shared with others filtering empty', async () => { - const shares = await getContents(false, true, false, false, [ShareType.Link]) + const shares = await getContents(false, true, false, false, false, [ShareType.Link]) expect(axios.get).toHaveBeenCalledTimes(1) expect(shares.contents).toHaveLength(0) diff --git a/apps/files_sharing/src/services/SharingService.ts b/apps/files_sharing/src/services/SharingService.ts index 2f8144e216e1d..db18c9e5ee097 100644 --- a/apps/files_sharing/src/services/SharingService.ts +++ b/apps/files_sharing/src/services/SharingService.ts @@ -59,11 +59,16 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise (ocsEntry?.item_mtime || 0)) { mtime = new Date((ocsEntry.stime) * 1000) } + const expiration = ocsEntry?.expiration + ? new Date(ocsEntry?.expiration)?.getTime() / 1000 + : undefined + return new Node({ id: fileid, source, @@ -75,6 +80,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise> { }) } +const getExpiredShares = function(): AxiosPromise> { + const url = generateOcsUrl('apps/files_sharing/api/v1/expiredshares') + return axios.get(url, { + headers, + params: { + include_tags: true, + }, + }) +} + /** * Check if a file request is enabled * @param attributes the share attributes json-encoded array @@ -180,7 +196,7 @@ const groupBy = function(nodes: (Folder | File)[], key: string) { }, {})) as (Folder | File)[][] } -export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise => { +export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, expiredShares = false, filterTypes: number[] = []): Promise => { const promises = [] as AxiosPromise>[] if (sharedWithYou) { @@ -195,6 +211,9 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true, if (deletedshares) { promises.push(getDeletedShares()) } + if (expiredShares) { + promises.push(getExpiredShares()) + } const responses = await Promise.all(promises) const data = responses.map((response) => response.data.ocs.data).flat() diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index d549d441df4ec..b92cb109872af 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -108,7 +108,6 @@ private function createOCS($userId) { $dateTimeZone->method('getTimeZone')->willReturn(new \DateTimeZone(date_default_timezone_get())); return new ShareAPIController( - self::APP_NAME, $this->getMockBuilder(IRequest::class)->getMock(), $this->shareManager, \OC::$server->getGroupManager(), diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 84234660665f8..314c90408a7cd 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -108,7 +108,6 @@ protected function setUp(): void { $this->mailer = $this->createMock(IMailer::class); $this->ocs = new ShareAPIController( - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -135,7 +134,6 @@ protected function setUp(): void { private function mockFormatShare() { return $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -752,7 +750,6 @@ public function testGetShare(IShare $share, array $result): void { /** @var ShareAPIController|\PHPUnit\Framework\MockObject\MockObject $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -1386,7 +1383,6 @@ public function testGetShares(array $getSharesParameters, array $shares, array $ /** @var ShareAPIController $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -1729,7 +1725,6 @@ public function testCreateShareUser(): void { /** @var ShareAPIController $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -1827,7 +1822,6 @@ public function testCreateShareGroup(): void { /** @var ShareAPIController|\PHPUnit\Framework\MockObject\MockObject $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -2249,7 +2243,6 @@ public function testCreateShareRemote(): void { /** @var ShareAPIController $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -2319,7 +2312,6 @@ public function testCreateShareRemoteGroup(): void { /** @var ShareAPIController $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, @@ -2562,7 +2554,6 @@ public function testCreateReshareOfFederatedMountNoDeletePermissions(): void { /** @var ShareAPIController|\PHPUnit\Framework\MockObject\MockObject $ocs */ $ocs = $this->getMockBuilder(ShareAPIController::class) ->setConstructorArgs([ - $this->appName, $this->request, $this->shareManager, $this->groupManager, diff --git a/apps/files_sharing/tests/ExpireSharesJobTest.php b/apps/files_sharing/tests/ExpireSharesJobTest.php index c3b81591ebc5d..6094285f8940c 100644 --- a/apps/files_sharing/tests/ExpireSharesJobTest.php +++ b/apps/files_sharing/tests/ExpireSharesJobTest.php @@ -9,8 +9,10 @@ use OCA\Files_Sharing\ExpireSharesJob; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Constants; +use OCP\IAppConfig; use OCP\Share\IManager; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; /** * Class ExpireSharesJobTest @@ -49,7 +51,14 @@ protected function setUp(): void { \OC::registerShareHooks(\OC::$server->getSystemConfig()); - $this->job = new ExpireSharesJob(\OC::$server->get(ITimeFactory::class), \OC::$server->get(IManager::class), $this->connection); + $this->job = new ExpireSharesJob( + \OC::$server->get(ITimeFactory::class), + \OC::$server->get(IManager::class), + $this->connection, + \OC::$server->get(IAppConfig::class), + \OC::$server->get(LoggerInterface::class), + + ); } protected function tearDown(): void { diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index c9ad58250c9ac..85fa4c19d12de 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -71,6 +71,7 @@ public function getForm() { 'defaultRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_remote_expire_date'), 'remoteExpireAfterNDays' => $this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7'), 'enforceRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_remote_expire_date'), + 'deleteOnExpire' => $this->getHumanBooleanConfig('core', 'shareapi_delete_on_expire', true), ]; $this->initialState->provideInitialState('sharingAppEnabled', $this->appManager->isEnabledForUser('files_sharing')); @@ -82,10 +83,10 @@ public function getForm() { } /** - * Helper function to retrive boolean values from human readable strings ('yes' / 'no') + * Helper function to retrieve boolean values from human readable strings ('yes' / 'no') */ private function getHumanBooleanConfig(string $app, string $key, bool $default = false): bool { - return $this->config->getAppValue($app, $key, $default ? 'yes' : 'no') === 'yes'; + return $this->config->getAppValue($app, $key, $default ? 'yes' : 'no') !== 'no'; } /** diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue index 93f30b2262c9f..c3aebee91680d 100644 --- a/apps/settings/src/components/AdminSettingsSharingForm.vue +++ b/apps/settings/src/components/AdminSettingsSharingForm.vue @@ -141,6 +141,12 @@ :placeholder="t('settings', 'Expire shares after x days')" :value.sync="settings.expireAfterNDays" /> + + + {{ t('settings', 'Delete shares on expiration') }} +