From 85a6fa289d9f3a1f1d8efb5eb18d5a4f72f7d6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Fri, 20 Sep 2024 12:04:41 +0200 Subject: [PATCH] chore(files_sharing): refactor ShareAPIController shared methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../Controller/DeletedShareAPIController.php | 221 ++----- .../Controller/ExpiredShareAPIController.php | 219 ++----- .../lib/Controller/ShareAPIController.php | 599 +---------------- .../Controller/ShareApiControllerFactory.php | 606 ++++++++++++++++++ .../files_sharing/lib/ResponseDefinitions.php | 23 - apps/files_sharing/openapi.json | 165 ++--- .../Controller/ShareAPIControllerTest.php | 1 - 9 files changed, 778 insertions(+), 1058 deletions(-) create mode 100644 apps/files_sharing/lib/Controller/ShareApiControllerFactory.php diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index cbb73f8147134..93e163a40a528 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -35,6 +35,7 @@ '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 5f1c698f2f7e9..12929cc49c609 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -50,6 +50,7 @@ class ComposerStaticInitFiles_Sharing '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 b61f9995c02f1..4a068375eefd1 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -8,160 +8,64 @@ */ namespace OCA\Files_Sharing\Controller; -use OCA\Files_Sharing\ResponseDefinitions; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; 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\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; -/** - * @psalm-import-type Files_SharingDeletedShare 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 \OCP\Files\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, + ) { + parent::__construct( + $request, $shareManager, + $userId, + $userManager, + $groupManager, + $rootFolder, + $appManager, + $serverContainer, + $userStatusManager, + $previewManager, + $dateTimeZone, + $urlGenerator, + $l + ); } /** * Get a list of all deleted shares * - * @return DataResponse + * @return DataResponse * * 200: Deleted shares returned */ @@ -211,55 +115,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 index 4bfdb1d9dff11..4d569567d3378 100644 --- a/apps/files_sharing/lib/Controller/ExpiredShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ExpiredShareAPIController.php @@ -8,156 +8,60 @@ */ namespace OCA\Files_Sharing\Controller; -use OCA\Files_Sharing\ResponseDefinitions; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\OCSController; -use OCP\AppFramework\QueryException; 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\IManager as ShareManager; use OCP\Share\IShare; +use OCP\UserStatus\IManager as UserStatusManager; +use Psr\Container\ContainerInterface; -/** - * @psalm-import-type Files_SharingDeletedShare from ResponseDefinitions - */ -class ExpiredShareAPIController extends OCSController { - - /** @var ShareManager */ - private $shareManager; - - /** @var string */ - private $userId; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; +class ExpiredShareAPIController extends ShareApiControllerFactory { - /** @var IRootFolder */ - private $rootFolder; - - /** @var IAppManager */ - private $appManager; - - /** @var IServerContainer */ - private $serverContainer; - - public function __construct(string $appName, + 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 \OCP\Files\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, + ) { + parent::__construct( + $request, $shareManager, + $userId, + $userManager, + $groupManager, + $rootFolder, + $appManager, + $serverContainer, + $userStatusManager, + $previewManager, + $dateTimeZone, + $urlGenerator, + $l, + ); } /** * Get a list of all expired shares * - * @return DataResponse + * @return DataResponse * * 200: Deleted shares returned */ @@ -181,55 +85,4 @@ public function index(): DataResponse { return new DataResponse($shares); } - - /** - * 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/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index cf83587467724..07416df3bc337 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -26,7 +26,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\Folder; @@ -45,7 +44,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; @@ -53,7 +51,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; @@ -62,7 +59,7 @@ * * @psalm-import-type Files_SharingShare from ResponseDefinitions */ -class ShareAPIController extends OCSController { +class ShareAPIController extends ShareApiControllerFactory { private ?Node $lockedNode = null; private string $currentUser; @@ -71,379 +68,42 @@ class ShareAPIController extends OCSController { * 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, + 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 = null, ) { - parent::__construct($appName, $request); + parent::__construct( + $request, + $shareManager, + $userId, + $userManager, + $groupManager, + $rootFolder, + $appManager, + $serverContainer, + $userStatusManager, + $previewManager, + $dateTimeZone, + $urlGenerator, + $l, + ); $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(\OCA\GlobalSiteSelector\Service\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; - } - - - /** * Get a specific share by id * @@ -1525,150 +1185,6 @@ protected function canAccessShare(\OCP\Share\IShare $share, bool $checkGroups = 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(\OCP\Share\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(\OCP\Share\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(\OCP\Share\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. @@ -1744,7 +1260,7 @@ private function getShareById(string $id): IShare { * Lock a Node * * @param \OCP\Files\Node $node - * @throws LockedException + * @throws LockedExcprivateeption */ private function lock(\OCP\Files\Node $node) { $node->lock(ILockingProvider::LOCK_SHARED); @@ -1761,57 +1277,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..843013846ce54 --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareApiControllerFactory.php @@ -0,0 +1,606 @@ +currentUser = $userId; + } + + /** + * @suppress PhanUndeclaredClassMethod + * + * @return Files_SharingShare + */ + + /** + * 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->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; + } + + /** + * 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 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 + */ + 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(\OCP\Share\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(\OCP\Share\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(\OCP\Share\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') + || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) { + return []; + } + + try { + $slaveService = Server::get(\OCA\GlobalSiteSelector\Service\SlaveService::class); + } catch (\Throwable $e) { + $this->logger->error( + $e->getMessage(), + ['exception' => $e] + ); + return []; + } + + return $slaveService->getUsersDisplayName($userIds, $cacheOnly); + } +} diff --git a/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php index 774e4c17e001b..0345e44925312 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 362e9aefc3235..a355c9ae375ce 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": [ @@ -2892,7 +2793,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/DeletedShare" + "$ref": "#/components/schemas/Share" } } } @@ -3001,6 +2902,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/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index f741bb64c8186..91e974b447e72 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -103,7 +103,6 @@ protected function setUp(): void { $this->mailer = $this->createMock(IMailer::class); $this->ocs = new ShareAPIController( - $this->appName, $this->request, $this->shareManager, $this->groupManager,