perf(external-sharing): Port to Entity and SnowflakeId

This removes all the read after write and we don't need to queries all
the time the same share in the same request anymore.

Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
This commit is contained in:
Carl Schwan 2025-10-29 16:50:50 +01:00
parent 93b258317d
commit 3bdb344224
No known key found for this signature in database
GPG key ID: 02325448204E452A
24 changed files with 939 additions and 876 deletions

View file

@ -500,7 +500,6 @@ class RequestHandlerController extends Controller {
* *
* @param IIncomingSignedRequest|null $signedRequest * @param IIncomingSignedRequest|null $signedRequest
* @param string $resourceType * @param string $resourceType
* @param string $sharedSecret
* *
* @throws IncomingRequestException * @throws IncomingRequestException
* @throws BadRequestException * @throws BadRequestException
@ -524,7 +523,7 @@ class RequestHandlerController extends Controller {
return; return;
} }
} catch (\Exception $e) { } catch (\Exception $e) {
throw new IncomingRequestException($e->getMessage()); throw new IncomingRequestException($e->getMessage(), previous: $e);
} }
$this->confirmNotificationEntry($signedRequest, $identity); $this->confirmNotificationEntry($signedRequest, $identity);

View file

@ -752,10 +752,9 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
* Get a share by token * Get a share by token
* *
* @param string $token * @param string $token
* @return IShare
* @throws ShareNotFound * @throws ShareNotFound
*/ */
public function getShareByToken($token) { public function getShareByToken($token): IShare {
$qb = $this->dbConnection->getQueryBuilder(); $qb = $this->dbConnection->getQueryBuilder();
$cursor = $qb->select('*') $cursor = $qb->select('*')
@ -812,9 +811,9 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
* @throws InvalidShare * @throws InvalidShare
* @throws ShareNotFound * @throws ShareNotFound
*/ */
private function createShareObject($data) { private function createShareObject($data): IShare {
$share = new Share($this->rootFolder, $this->userManager); $share = new Share($this->rootFolder, $this->userManager);
$share->setId((int)$data['id']) $share->setId((string)$data['id'])
->setShareType((int)$data['share_type']) ->setShareType((int)$data['share_type'])
->setPermissions((int)$data['permissions']) ->setPermissions((int)$data['permissions'])
->setTarget($data['file_target']) ->setTarget($data['file_target'])

View file

@ -14,8 +14,10 @@ use OCA\FederatedFileSharing\AddressHandler;
use OCA\FederatedFileSharing\FederatedShareProvider; use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Federation\TrustedServers; use OCA\Federation\TrustedServers;
use OCA\Files_Sharing\Activity\Providers\RemoteShares; use OCA\Files_Sharing\Activity\Providers\RemoteShares;
use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\Manager;
use OCA\GlobalSiteSelector\Service\SlaveService; use OCA\GlobalSiteSelector\Service\SlaveService;
use OCA\Polls\Db\Share;
use OCP\Activity\IManager as IActivityManager; use OCP\Activity\IManager as IActivityManager;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\AppFramework\QueryException; use OCP\AppFramework\QueryException;
@ -44,6 +46,7 @@ use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager; use OCP\Share\IManager;
use OCP\Share\IProviderFactory; use OCP\Share\IProviderFactory;
use OCP\Share\IShare; use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use OCP\Util; use OCP\Util;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use SensitiveParameter; use SensitiveParameter;
@ -72,13 +75,11 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
private IFilenameValidator $filenameValidator, private IFilenameValidator $filenameValidator,
private readonly IProviderFactory $shareProviderFactory, private readonly IProviderFactory $shareProviderFactory,
private readonly SetupManager $setupManager, private readonly SetupManager $setupManager,
private readonly IGenerator $snowflakeGenerator,
) { ) {
} }
/** public function getShareType(): string {
* @return string
*/
public function getShareType() {
return 'file'; return 'file';
} }
@ -93,7 +94,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
* @throws HintException * @throws HintException
* @since 14.0.0 * @since 14.0.0
*/ */
public function shareReceived(ICloudFederationShare $share) { public function shareReceived(ICloudFederationShare $share): string {
if (!$this->isS2SEnabled(true)) { if (!$this->isS2SEnabled(true)) {
throw new ProviderCouldNotAddShareException('Server does not support federated cloud sharing', '', Http::STATUS_SERVICE_UNAVAILABLE); throw new ProviderCouldNotAddShareException('Server does not support federated cloud sharing', '', Http::STATUS_SERVICE_UNAVAILABLE);
} }
@ -103,7 +104,8 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
throw new ProviderCouldNotAddShareException('Unsupported protocol for data exchange.', '', Http::STATUS_NOT_IMPLEMENTED); throw new ProviderCouldNotAddShareException('Unsupported protocol for data exchange.', '', Http::STATUS_NOT_IMPLEMENTED);
} }
[$ownerUid, $remote] = $this->addressHandler->splitUserRemote($share->getOwner()); [, $remote] = $this->addressHandler->splitUserRemote($share->getOwner());
// for backward compatibility make sure that the remote url stored in the // for backward compatibility make sure that the remote url stored in the
// database ends with a trailing slash // database ends with a trailing slash
if (!str_ends_with($remote, '/')) { if (!str_ends_with($remote, '/')) {
@ -113,17 +115,15 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
$token = $share->getShareSecret(); $token = $share->getShareSecret();
$name = $share->getResourceName(); $name = $share->getResourceName();
$owner = $share->getOwnerDisplayName() ?: $share->getOwner(); $owner = $share->getOwnerDisplayName() ?: $share->getOwner();
$sharedBy = $share->getSharedByDisplayName();
$shareWith = $share->getShareWith(); $shareWith = $share->getShareWith();
$remoteId = $share->getProviderId(); $remoteId = $share->getProviderId();
$sharedByFederatedId = $share->getSharedBy(); $sharedByFederatedId = $share->getSharedBy();
$ownerFederatedId = $share->getOwner(); $ownerFederatedId = $share->getOwner();
$shareType = $this->mapShareTypeToNextcloud($share->getShareType()); $shareType = $this->mapShareTypeToNextcloud($share->getShareType());
// if no explicit information about the person who created the share was send // if no explicit information about the person who created the share was sent
// we assume that the share comes from the owner // we assume that the share comes from the owner
if ($sharedByFederatedId === null) { if ($sharedByFederatedId === null) {
$sharedBy = $owner;
$sharedByFederatedId = $ownerFederatedId; $sharedByFederatedId = $ownerFederatedId;
} }
@ -159,9 +159,19 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
} }
} }
$externalShare = new ExternalShare();
$externalShare->setId($this->snowflakeGenerator->nextId());
$externalShare->setRemote($remote);
$externalShare->setRemoteId($remoteId);
$externalShare->setShareToken($token);
$externalShare->setPassword('');
$externalShare->setName($name);
$externalShare->setOwner($owner);
$externalShare->setShareType($shareType);
$externalShare->setAccepted(IShare::STATUS_PENDING);
try { try {
$this->externalShareManager->addShare($remote, $token, '', $name, $owner, $shareType, false, $userOrGroup, $remoteId); $this->externalShareManager->addShare($externalShare, $userOrGroup);
$shareId = Server::get(IDBConnection::class)->lastInsertId('*PREFIX*share_external');
// get DisplayName about the owner of the share // get DisplayName about the owner of the share
$ownerDisplayName = $this->getUserDisplayName($ownerFederatedId); $ownerDisplayName = $this->getUserDisplayName($ownerFederatedId);
@ -184,14 +194,14 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
->setType('remote_share') ->setType('remote_share')
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName]) ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
->setAffectedUser($shareWith) ->setAffectedUser($shareWith)
->setObject('remote_share', $shareId, $name); ->setObject('remote_share', $externalShare->getId(), $name);
Server::get(IActivityManager::class)->publish($event); Server::get(IActivityManager::class)->publish($event);
$this->notifyAboutNewShare($shareWith, $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName); $this->notifyAboutNewShare($shareWith, $externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
// If auto-accept is enabled, accept the share // If auto-accept is enabled, accept the share
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) { if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
/** @var IUser $userOrGroup */ /** @var IUser $userOrGroup */
$this->externalShareManager->acceptShare($shareId, $userOrGroup); $this->externalShareManager->acceptShare($externalShare, $userOrGroup);
} }
} else { } else {
/** @var IGroup $userOrGroup */ /** @var IGroup $userOrGroup */
@ -202,18 +212,18 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
->setType('remote_share') ->setType('remote_share')
->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName]) ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/'), $ownerDisplayName])
->setAffectedUser($user->getUID()) ->setAffectedUser($user->getUID())
->setObject('remote_share', $shareId, $name); ->setObject('remote_share', $externalShare->getId(), $name);
Server::get(IActivityManager::class)->publish($event); Server::get(IActivityManager::class)->publish($event);
$this->notifyAboutNewShare($user->getUID(), $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName); $this->notifyAboutNewShare($user->getUID(), $externalShare->getId(), $ownerFederatedId, $sharedByFederatedId, $name, $ownerDisplayName);
// If auto-accept is enabled, accept the share // If auto-accept is enabled, accept the share
if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) { if ($this->federatedShareProvider->isFederatedTrustedShareAutoAccept() && $trustedServers?->isTrustedServer($remote) === true) {
$this->externalShareManager->acceptShare($shareId, $user); $this->externalShareManager->acceptShare($externalShare, $user);
} }
} }
} }
return $shareId; return $externalShare->getId();
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Server can not add remote share.', [ $this->logger->error('Server can not add remote share.', [
'app' => 'files_sharing', 'app' => 'files_sharing',
@ -275,7 +285,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
return $result; return $result;
} }
private function notifyAboutNewShare($shareWith, $shareId, $ownerFederatedId, $sharedByFederatedId, $name, $displayName): void { private function notifyAboutNewShare($shareWith, string $shareId, $ownerFederatedId, $sharedByFederatedId, string $name, string $displayName): void {
$notification = $this->notificationManager->createNotification(); $notification = $this->notificationManager->createNotification();
$notification->setApp('files_sharing') $notification->setApp('files_sharing')
->setUser($shareWith) ->setUser($shareWith)
@ -813,7 +823,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
return ''; return '';
} }
return $share['user'] . '@' . $share['remote']; return $share->getUser() . '@' . $share->getRemote();
} }
// if uid_owner is a local account, the request comes from the recipient // if uid_owner is a local account, the request comes from the recipient

View file

@ -50,6 +50,8 @@ return array(
'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => $baseDir . '/../lib/Exceptions/SharingRightsException.php', 'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => $baseDir . '/../lib/Exceptions/SharingRightsException.php',
'OCA\\Files_Sharing\\ExpireSharesJob' => $baseDir . '/../lib/ExpireSharesJob.php', 'OCA\\Files_Sharing\\ExpireSharesJob' => $baseDir . '/../lib/ExpireSharesJob.php',
'OCA\\Files_Sharing\\External\\Cache' => $baseDir . '/../lib/External/Cache.php', 'OCA\\Files_Sharing\\External\\Cache' => $baseDir . '/../lib/External/Cache.php',
'OCA\\Files_Sharing\\External\\ExternalShare' => $baseDir . '/../lib/External/ExternalShare.php',
'OCA\\Files_Sharing\\External\\ExternalShareMapper' => $baseDir . '/../lib/External/ExternalShareMapper.php',
'OCA\\Files_Sharing\\External\\Manager' => $baseDir . '/../lib/External/Manager.php', 'OCA\\Files_Sharing\\External\\Manager' => $baseDir . '/../lib/External/Manager.php',
'OCA\\Files_Sharing\\External\\Mount' => $baseDir . '/../lib/External/Mount.php', 'OCA\\Files_Sharing\\External\\Mount' => $baseDir . '/../lib/External/Mount.php',
'OCA\\Files_Sharing\\External\\MountProvider' => $baseDir . '/../lib/External/MountProvider.php', 'OCA\\Files_Sharing\\External\\MountProvider' => $baseDir . '/../lib/External/MountProvider.php',

View file

@ -65,6 +65,8 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => __DIR__ . '/..' . '/../lib/Exceptions/SharingRightsException.php', 'OCA\\Files_Sharing\\Exceptions\\SharingRightsException' => __DIR__ . '/..' . '/../lib/Exceptions/SharingRightsException.php',
'OCA\\Files_Sharing\\ExpireSharesJob' => __DIR__ . '/..' . '/../lib/ExpireSharesJob.php', 'OCA\\Files_Sharing\\ExpireSharesJob' => __DIR__ . '/..' . '/../lib/ExpireSharesJob.php',
'OCA\\Files_Sharing\\External\\Cache' => __DIR__ . '/..' . '/../lib/External/Cache.php', 'OCA\\Files_Sharing\\External\\Cache' => __DIR__ . '/..' . '/../lib/External/Cache.php',
'OCA\\Files_Sharing\\External\\ExternalShare' => __DIR__ . '/..' . '/../lib/External/ExternalShare.php',
'OCA\\Files_Sharing\\External\\ExternalShareMapper' => __DIR__ . '/..' . '/../lib/External/ExternalShareMapper.php',
'OCA\\Files_Sharing\\External\\Manager' => __DIR__ . '/..' . '/../lib/External/Manager.php', 'OCA\\Files_Sharing\\External\\Manager' => __DIR__ . '/..' . '/../lib/External/Manager.php',
'OCA\\Files_Sharing\\External\\Mount' => __DIR__ . '/..' . '/../lib/External/Mount.php', 'OCA\\Files_Sharing\\External\\Mount' => __DIR__ . '/..' . '/../lib/External/Mount.php',
'OCA\\Files_Sharing\\External\\MountProvider' => __DIR__ . '/..' . '/../lib/External/MountProvider.php', 'OCA\\Files_Sharing\\External\\MountProvider' => __DIR__ . '/..' . '/../lib/External/MountProvider.php',

View file

@ -7,6 +7,7 @@
*/ */
namespace OCA\Files_Sharing\Controller; namespace OCA\Files_Sharing\Controller;
use OCA\Files_Sharing\External\Manager;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -21,42 +22,40 @@ class ExternalSharesController extends Controller {
public function __construct( public function __construct(
string $appName, string $appName,
IRequest $request, IRequest $request,
private \OCA\Files_Sharing\External\Manager $externalManager, private readonly Manager $externalManager,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }
/** /**
* @NoOutgoingFederatedSharingRequired * @NoOutgoingFederatedSharingRequired
*
* @return JSONResponse
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function index() { public function index(): JSONResponse {
return new JSONResponse($this->externalManager->getOpenShares()); return new JSONResponse($this->externalManager->getOpenShares());
} }
/** /**
* @NoOutgoingFederatedSharingRequired * @NoOutgoingFederatedSharingRequired
*
* @param int $id
* @return JSONResponse
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function create($id) { public function create(string $id): JSONResponse {
$this->externalManager->acceptShare($id); $externalShare = $this->externalManager->getShare($id);
if ($externalShare !== false) {
$this->externalManager->acceptShare($externalShare);
}
return new JSONResponse(); return new JSONResponse();
} }
/** /**
* @NoOutgoingFederatedSharingRequired * @NoOutgoingFederatedSharingRequired
*
* @param integer $id
* @return JSONResponse
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function destroy($id) { public function destroy(string $id): JSONResponse {
$this->externalManager->declineShare($id); $externalShare = $this->externalManager->getShare($id);
if ($externalShare !== false) {
$this->externalManager->declineShare($externalShare);
}
return new JSONResponse(); return new JSONResponse();
} }
} }

View file

@ -7,7 +7,7 @@
*/ */
namespace OCA\Files_Sharing\Controller; namespace OCA\Files_Sharing\Controller;
use OC\Files\View; use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\Manager;
use OCA\Files_Sharing\ResponseDefinitions; use OCA\Files_Sharing\ResponseDefinitions;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
@ -16,25 +16,27 @@ use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController; use OCP\AppFramework\OCSController;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IRequest; use OCP\IRequest;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
* @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions * @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions
* @api
*/ */
class RemoteController extends OCSController { class RemoteController extends OCSController {
/** /**
* Remote constructor. * Remote controller constructor.
*
* @param string $appName
* @param IRequest $request
* @param Manager $externalManager
*/ */
public function __construct( public function __construct(
$appName, string $appName,
IRequest $request, IRequest $request,
private Manager $externalManager, private readonly Manager $externalManager,
private LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly ?string $userId,
private readonly IRootFolder $rootFolder,
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
} }
@ -47,71 +49,84 @@ class RemoteController extends OCSController {
* 200: Pending remote shares returned * 200: Pending remote shares returned
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function getOpenShares() { public function getOpenShares(): DataResponse {
return new DataResponse($this->externalManager->getOpenShares()); $shares = $this->externalManager->getOpenShares();
$shares = array_map($this->extendShareInfo(...), $shares);
return new DataResponse($shares);
} }
/** /**
* Accept a remote share * Accept a remote share.
* *
* @param int $id ID of the share * @param string $id ID of the share
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSNotFoundException Share not found * @throws OCSNotFoundException Share not found
* *
* 200: Share accepted successfully * 200: Share accepted successfully
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function acceptShare($id) { public function acceptShare(string $id): DataResponse {
if ($this->externalManager->acceptShare($id)) { $externalShare = $this->externalManager->getShare($id);
return new DataResponse(); if ($externalShare === false) {
$this->logger->error('Could not accept federated share with id: ' . $id . ' Share not found.', ['app' => 'files_sharing']);
throw new OCSNotFoundException('Wrong share ID, share does not exist.');
} }
$this->logger->error('Could not accept federated share with id: ' . $id, if (!$this->externalManager->acceptShare($externalShare)) {
['app' => 'files_sharing']); $this->logger->error('Could not accept federated share with id: ' . $id, ['app' => 'files_sharing']);
throw new OCSNotFoundException('Wrong share ID, share does not exist.');
}
throw new OCSNotFoundException('wrong share ID, share does not exist.'); return new DataResponse();
} }
/** /**
* Decline a remote share * Decline a remote share.
* *
* @param int $id ID of the share * @param string $id ID of the share
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSNotFoundException Share not found * @throws OCSNotFoundException Share not found
* *
* 200: Share declined successfully * 200: Share declined successfully
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function declineShare($id) { public function declineShare(string $id): DataResponse {
if ($this->externalManager->declineShare($id)) { $externalShare = $this->externalManager->getShare($id);
return new DataResponse(); if ($externalShare === false) {
$this->logger->error('Could not decline federated share with id: ' . $id . ' Share not found.', ['app' => 'files_sharing']);
throw new OCSNotFoundException('Wrong share ID, share does not exist.');
} }
// Make sure the user has no notification for something that does not exist anymore. if (!$this->externalManager->declineShare($externalShare)) {
$this->externalManager->processNotification($id); $this->logger->error('Could not decline federated share with id: ' . $id, ['app' => 'files_sharing']);
throw new OCSNotFoundException('Wrong share ID, share does not exist.');
}
throw new OCSNotFoundException('wrong share ID, share does not exist.'); return new DataResponse();
} }
/** /**
* @param array $share Share with info from the share_external table * @param ExternalShare $share Share with info from the share_external table
* @return array enriched share info with data from the filecache * @return Files_SharingRemoteShare Enriched share info with data from the filecache
*/ */
private static function extendShareInfo($share) { private function extendShareInfo(ExternalShare $share): array {
$view = new View('/' . \OC_User::getUser() . '/files/'); $userFolder = $this->rootFolder->getUserFolder($this->userId);
$info = $view->getFileInfo($share['mountpoint']);
if ($info === false) { try {
return $share; $mountPointNode = $userFolder->get($share->getMountpoint());
} catch (NotPermittedException|NotFoundException) {
return $share->jsonSerialize();
} }
$share['mimetype'] = $info->getMimetype(); $shareData = $share->jsonSerialize();
$share['mtime'] = $info->getMTime();
$share['permissions'] = $info->getPermissions();
$share['type'] = $info->getType();
$share['file_id'] = $info->getId();
return $share; $shareData['mimetype'] = $mountPointNode->getMimetype();
$shareData['mtime'] = $mountPointNode->getMTime();
$shareData['permissions'] = $mountPointNode->getPermissions();
$shareData['type'] = $mountPointNode->getType();
$shareData['file_id'] = $mountPointNode->getId();
return $shareData;
} }
/** /**
@ -122,30 +137,29 @@ class RemoteController extends OCSController {
* 200: Accepted remote shares returned * 200: Accepted remote shares returned
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function getShares() { public function getShares(): DataResponse {
$shares = $this->externalManager->getAcceptedShares(); $shares = $this->externalManager->getAcceptedShares();
$shares = array_map(self::extendShareInfo(...), $shares); $shares = array_map(fn (ExternalShare $share) => $this->extendShareInfo($share), $shares);
return new DataResponse($shares); return new DataResponse($shares);
} }
/** /**
* Get info of a remote share * Get info of a remote share
* *
* @param int $id ID of the share * @param string $id ID of the share
* @return DataResponse<Http::STATUS_OK, Files_SharingRemoteShare, array{}> * @return DataResponse<Http::STATUS_OK, Files_SharingRemoteShare, array{}>
* @throws OCSNotFoundException Share not found * @throws OCSNotFoundException Share not found
* *
* 200: Share returned * 200: Share returned
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function getShare($id) { public function getShare(string $id): DataResponse {
$shareInfo = $this->externalManager->getShare($id); $shareInfo = $this->externalManager->getShare($id);
if ($shareInfo === false) { if ($shareInfo === false) {
throw new OCSNotFoundException('share does not exist'); throw new OCSNotFoundException('share does not exist');
} else { } else {
$shareInfo = self::extendShareInfo($shareInfo); $shareInfo = $this->extendShareInfo($shareInfo);
return new DataResponse($shareInfo); return new DataResponse($shareInfo);
} }
} }
@ -153,7 +167,7 @@ class RemoteController extends OCSController {
/** /**
* Unshare a remote share * Unshare a remote share
* *
* @param int $id ID of the share * @param string $id ID of the share
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSNotFoundException Share not found * @throws OCSNotFoundException Share not found
* @throws OCSForbiddenException Unsharing is not possible * @throws OCSForbiddenException Unsharing is not possible
@ -161,14 +175,14 @@ class RemoteController extends OCSController {
* 200: Share unshared successfully * 200: Share unshared successfully
*/ */
#[NoAdminRequired] #[NoAdminRequired]
public function unshare($id) { public function unshare(string $id): DataResponse {
$shareInfo = $this->externalManager->getShare($id); $shareInfo = $this->externalManager->getShare($id);
if ($shareInfo === false) { if ($shareInfo === false) {
throw new OCSNotFoundException('Share does not exist'); throw new OCSNotFoundException('Share does not exist');
} }
$mountPoint = '/' . \OC_User::getUser() . '/files' . $shareInfo['mountpoint']; $mountPoint = '/' . $this->userId . '/files' . $shareInfo->getMountPoint();
if ($this->externalManager->removeShare($mountPoint) === true) { if ($this->externalManager->removeShare($mountPoint) === true) {
return new DataResponse(); return new DataResponse();

View file

@ -64,7 +64,7 @@ class ShareInfoController extends ApiController {
try { try {
$share = $this->shareManager->getShareByToken($t); $share = $this->shareManager->getShareByToken($t);
} catch (ShareNotFound $e) { } catch (ShareNotFound $e) {
$response = new JSONResponse([], Http::STATUS_NOT_FOUND); $response = new JSONResponse(["message" => "Not found " . $t . " "], Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $t]); $response->throttle(['token' => $t]);
return $response; return $response;
} }

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Sharing\External;
use OC\Files\Filesystem;
use OCA\Files_Sharing\ResponseDefinitions;
use OCP\AppFramework\Db\Entity;
use OCP\DB\Types;
use OCP\IGroup;
use OCP\IUser;
use OCP\Share\IShare;
/**
* @method string getId()
* @method void setId(string $id)
* @method string getParent()
* @method void setParent(string $parent)
* @method int|null getShareType()
* @method void setShareType(int $shareType)
* @method string getRemote()
* @method void setRemote(string $remote)
* @method string getRemoteId()
* @method void setRemoteId(string $remoteId)
* @method string getShareToken()
* @method void setShareToken(string $shareToken)
* @method string getPassword()
* @method void setPassword(string $password)
* @method string getName()
* @method string getOwner()
* @method void setOwner(string $owner)
* @method string getUser()
* @method void setUser(string $user)
* @method string getMountpoint()
* @method string getMountpointHash()
* @method void setMountpointHash(string $mountPointHash)
* @method int getAccepted()
* @method void setAccepted(int $accepted)
*
* @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions
*/
class ExternalShare extends Entity implements \JsonSerializable {
protected string $parent = '-1';
protected ?int $shareType = null;
protected ?string $remote = null;
protected ?string $remoteId = null;
protected ?string $shareToken = null;
protected ?string $password = null;
protected ?string $name = null;
protected ?string $owner = null;
protected ?string $user = null;
protected ?string $mountpoint = null;
protected ?string $mountpointHash = null;
protected ?int $accepted = null;
public function __construct() {
$this->addType('id', Types::STRING); // Stored as a bigint
$this->addType('parent', Types::STRING); // Stored as a bigint
$this->addType('shareType', Types::INTEGER);
$this->addType('remote', Types::STRING);
$this->addType('remoteId', Types::STRING);
$this->addType('shareToken', Types::STRING);
$this->addType('password', Types::STRING);
$this->addType('name', Types::STRING);
$this->addType('owner', Types::STRING);
$this->addType('user', Types::STRING);
$this->addType('mountpoint', Types::STRING);
$this->addType('mountpointHash', Types::STRING);
$this->addType('accepted', Types::INTEGER);
}
public function setMountpoint(string $mountPoint): void {
$this->setter('mountpoint', [$mountPoint]);
$this->setMountpointHash(md5($mountPoint));
}
public function setName(string $name): void {
$name = Filesystem::normalizePath('/' . $name);
$this->setter('name', [$name]);
}
public function setUserOrGroup(IUser|IGroup|null $userOrGroup): void {
$this->setUser($userOrGroup instanceof IGroup ? $userOrGroup->getGID() : $userOrGroup->getUID());
}
/**
* @return Files_SharingRemoteShare
*/
public function jsonSerialize(): array {
$parent = $this->getParent();
return [
'id' => $this->getId(),
'parent' => $parent === '-1' ? null : $parent,
'share_type' => $this->getShareType() ?? IShare::TYPE_USER, // unfortunately nullable on the DB level, but never null.
'remote' => $this->getRemote(),
'remote_id' => $this->getRemoteId(),
'share_token' => $this->getShareToken(),
'name' => $this->getName(),
'owner' => $this->getOwner(),
'user' => $this->getUser(),
'mountpoint' => $this->getMountpoint(),
'accepted' => $this->getAccepted(),
// Added later on
'file_id' => null,
'mimetype' => null,
'permissions' => null,
'mtime' => null,
'type' => null,
];
}
/**
* @internal For unit tests
* @return ExternalShare
*/
public function clone(): self {
$newShare = new ExternalShare();
$newShare->setParent($this->getParent());
$newShare->setShareType($this->getShareType());
$newShare->setRemote($this->getRemote());
$newShare->setRemoteId($this->getRemoteId());
$newShare->setShareToken($this->getShareToken());
$newShare->setPassword($this->getPassword());
$newShare->setName($this->getName());
$newShare->setOwner($this->getOwner());
$newShare->setMountpoint($this->getMountpoint());
$newShare->setAccepted($this->getAccepted());
$newShare->setPassword($this->getPassword());
return $newShare;
}
}

View file

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Sharing\External;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\Share\IShare;
/**
* @template-extends QBMapper<ExternalShare>
*/
class ExternalShareMapper extends QBMapper {
private const TABLE_NAME = 'share_external';
public function __construct(
IDBConnection $db,
private readonly IGroupManager $groupManager,
) {
parent::__construct($db, self::TABLE_NAME);
}
/**
* @throws DoesNotExistException
*/
public function getById(string $id): ExternalShare {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->setMaxResults(1);
return $this->findEntity($qb);
}
/**
* Get share by token.
*
* @throws DoesNotExistException
*/
public function getShareByToken(string $token): ExternalShare {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->eq('share_token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);
return $this->findEntity($qb);
}
/**
* Get share by parent id and user.
*
* @throws DoesNotExistException
*/
public function getUserShare(ExternalShare $parentShare, IUser $user): ExternalShare {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->andX(
$qb->expr()->eq('parent', $qb->createNamedParameter($parentShare->getId())),
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
));
return $this->findEntity($qb);
}
public function get() {
}
public function getByMountPointAndUser(string $mountPoint, IUser $user) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->andX(
$qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter(md5($mountPoint))),
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
));
return $this->findEntity($qb);
}
/**
* @return \Generator<ExternalShare>
* @throws Exception
*/
public function getUserShares(IUser $user): \Generator {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER, IQueryBuilder::PARAM_INT)));
return $this->yieldEntities($qb);
}
public function deleteUserShares(IUser $user): void {
$qb = $this->db->getQueryBuilder();
$qb->delete(self::TABLE_NAME)
// user field can specify a user or a group
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
->andWhere(
$qb->expr()->orX(
// delete direct shares
$qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_USER)),
// delete sub-shares of group shares for that user
$qb->expr()->andX(
$qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_GROUP)),
$qb->expr()->neq('parent', $qb->expr()->literal(-1)),
)
)
);
$qb->executeStatement();
}
/**
* @throws Exception
*/
public function deleteGroupShares(IGroup $group): void {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from(self::TABLE_NAME)
->where($qb->expr()->eq('user', $qb->createNamedParameter($group->getGID())))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)));
$this->yieldEntities($qb);
$delete = $this->db->getQueryBuilder();
$delete->delete(self::TABLE_NAME)
->where(
$qb->expr()->orX(
$qb->expr()->eq('id', $qb->createParameter('share_id')),
$qb->expr()->eq('parent', $qb->createParameter('share_parent_id'))
)
);
foreach ($this->yieldEntities($qb) as $share) {
$delete->setParameter('share_id', $share->getId());
$delete->setParameter('share_parent_id', $share->getId());
$delete->executeStatement();
}
}
/**
* Return a list of shares for the user.
*
* @psalm-param IShare::STATUS_PENDING|IShare::STATUS_ACCEPTED|null $status Filter share by their status or return all shares of the user if null.
* @return list<ExternalShare> list of open server-to-server shares
* @throws Exception
*/
public function getShares(IUser $user, ?int $status): array {
// Not allowing providing a user here,
// as we only want to retrieve shares for the current user.
$groups = $this->groupManager->getUserGroups($user);
$userGroups = [];
foreach ($groups as $group) {
$userGroups[] = $group->getGID();
}
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('share_external')
->where(
$qb->expr()->orX(
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())),
$qb->expr()->in(
'user',
$qb->createNamedParameter($userGroups, IQueryBuilder::PARAM_STR_ARRAY)
)
)
)
->orderBy('id', 'ASC');
$shares = $this->findEntities($qb);
// remove parent group share entry if we have a specific user share entry for the user
$toRemove = [];
foreach ($shares as $share) {
if ($share->getShareType() === IShare::TYPE_GROUP && $share->getParent() !== '-1') {
$toRemove[] = $share->getParent();
}
}
$shares = array_filter($shares, function (ExternalShare $share) use ($toRemove): bool {
return !in_array($share->getId(), $toRemove, true);
});
if (!is_null($status)) {
$shares = array_filter($shares, function (ExternalShare $share) use ($status): bool {
return $share->getAccepted() === $status;
});
}
return array_values($shares);
}
}

View file

@ -13,9 +13,8 @@ use OC\Files\SetupManager;
use OC\User\NoUserException; use OC\User\NoUserException;
use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent; use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent;
use OCA\Files_Sharing\Helper; use OCA\Files_Sharing\Helper;
use OCA\Files_Sharing\ResponseDefinitions; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\DB\Exception; use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudFederationProviderManager;
@ -34,29 +33,10 @@ use OCP\IUser;
use OCP\IUserSession; use OCP\IUserSession;
use OCP\Notification\IManager; use OCP\Notification\IManager;
use OCP\OCS\IDiscoveryService; use OCP\OCS\IDiscoveryService;
use OCP\Server;
use OCP\Share;
use OCP\Share\IShare; use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/**
* @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions
* @psalm-type ExternalShare = array{
* id: int,
* remote: string,
* remote_id: string,
* parent: int,
* share_token: string,
* name: string,
* owner: string,
* user: string,
* mountpoint: string,
* accepted: bool,
* share_type:int,
* password: string,
* mountpoint_hash: string
* }
*/
class Manager { class Manager {
public const STORAGE = '\OCA\Files_Sharing\External\Storage'; public const STORAGE = '\OCA\Files_Sharing\External\Storage';
@ -78,6 +58,8 @@ class Manager {
private IRootFolder $rootFolder, private IRootFolder $rootFolder,
private SetupManager $setupManager, private SetupManager $setupManager,
private ICertificateManager $certificateManager, private ICertificateManager $certificateManager,
private ExternalShareMapper $externalShareMapper,
private IGenerator $snowflakeGenerator,
) { ) {
$this->user = $userSession->getUser(); $this->user = $userSession->getUser();
} }
@ -89,181 +71,95 @@ class Manager {
* @throws NotPermittedException * @throws NotPermittedException
* @throws NoUserException * @throws NoUserException
*/ */
public function addShare(string $remote, string $token, string $password, string $name, string $owner, int $shareType, bool $accepted = false, IUser|IGroup|null $userOrGroup = null, string $remoteId = '', int $parent = -1): ?Mount { public function addShare(ExternalShare $shareExternal, IUser|IGroup|null $userOrGroup = null): ?Mount {
$userOrGroup = $userOrGroup ?? $this->user; $userOrGroup = $userOrGroup ?? $this->user;
$accepted = $accepted ? IShare::STATUS_ACCEPTED : IShare::STATUS_PENDING;
$name = Filesystem::normalizePath('/' . $name);
if ($accepted !== IShare::STATUS_ACCEPTED) { if ($shareExternal->getAccepted() !== IShare::STATUS_ACCEPTED) {
// To avoid conflicts with the mount point generation later, // To avoid conflicts with the mount point generation later,
// we only use a temporary mount point name here. The real // we only use a temporary mount point name here. The real
// mount point name will be generated when accepting the share, // mount point name will be generated when accepting the share,
// using the original share item name. // using the original share item name.
$tmpMountPointName = '{{TemporaryMountPointName#' . $name . '}}'; $tmpMountPointName = '{{TemporaryMountPointName#' . $shareExternal->getName() . '}}';
$mountPoint = $tmpMountPointName; $shareExternal->setMountpoint($tmpMountPointName);
$hash = md5($tmpMountPointName); $shareExternal->setUserOrGroup($userOrGroup);
$data = [
'remote' => $remote,
'share_token' => $token,
'password' => $password,
'name' => $name,
'owner' => $owner,
'user' => $userOrGroup instanceof IGroup ? $userOrGroup->getGID() : $userOrGroup->getUID(),
'mountpoint' => $mountPoint,
'mountpoint_hash' => $hash,
'accepted' => $accepted,
'remote_id' => $remoteId,
'share_type' => $shareType,
];
$i = 1; $i = 1;
while (!$this->connection->insertIfNotExist('*PREFIX*share_external', $data, ['user', 'mountpoint_hash'])) { while (true) {
// The external share already exists for the user try {
$data['mountpoint'] = $tmpMountPointName . '-' . $i; $this->externalShareMapper->insert($shareExternal);
$data['mountpoint_hash'] = md5($data['mountpoint']); break;
$i++; } catch (Exception $e) {
if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
$shareExternal->setMountpoint($tmpMountPointName . '-' . $i);
$i++;
} else {
throw $e;
}
}
} }
return null; return null;
} }
$user = $userOrGroup instanceof IUser ? $userOrGroup : $this->user; $user = $userOrGroup instanceof IUser ? $userOrGroup : $this->user;
$userFolder = $this->rootFolder->getUserFolder($user->getUID()); $userFolder = $this->rootFolder->getUserFolder($user->getUID());
$mountPoint = $userFolder->getNonExistingName($name); $mountPoint = $userFolder->getNonExistingName($shareExternal->getName());
$mountPoint = Filesystem::normalizePath('/' . $mountPoint); $mountPoint = Filesystem::normalizePath('/' . $mountPoint);
$hash = md5($mountPoint); $shareExternal->setMountpoint($mountPoint);
$shareExternal->setUserOrGroup($user);
$this->writeShareToDb($remote, $token, $password, $name, $owner, $userOrGroup, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType); $this->externalShareMapper->insert($shareExternal);
$options = [ $options = [
'remote' => $remote, 'remote' => $shareExternal->getRemote(),
'token' => $token, 'token' => $shareExternal->getShareToken(),
'password' => $password, 'password' => $shareExternal->getPassword(),
'mountpoint' => $mountPoint, 'mountpoint' => $shareExternal->getMountpoint(),
'owner' => $owner 'owner' => $shareExternal->getOwner(),
]; ];
return $this->mountShare($options, $user); return $this->mountShare($options, $user);
} }
/** public function getShare(string $id, ?IUser $user = null): ExternalShare|false {
* Write remote share to the database.
*
* @throws Exception
*/
private function writeShareToDb(string $remote, string $token, ?string $password, string $name, string $owner, IUser|IGroup $userOrGroup, string $mountPoint, string $hash, int $accepted, string $remoteId, int $parent, int $shareType): void {
$qb = $this->connection->getQueryBuilder();
$qb->insert('share_external')
->values([
'remote' => $qb->createNamedParameter($remote, IQueryBuilder::PARAM_STR),
'share_token' => $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR),
'password' => $qb->createNamedParameter($password, IQueryBuilder::PARAM_STR),
'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR),
'owner' => $qb->createNamedParameter($owner, IQueryBuilder::PARAM_STR),
'user' => $qb->createNamedParameter($userOrGroup instanceof IGroup ? $userOrGroup->getGID() : $userOrGroup->getUID(), IQueryBuilder::PARAM_STR),
'mountpoint' => $qb->createNamedParameter($mountPoint, IQueryBuilder::PARAM_STR),
'mountpoint_hash' => $qb->createNamedParameter($hash, IQueryBuilder::PARAM_STR),
'accepted' => $qb->createNamedParameter($accepted, IQueryBuilder::PARAM_INT),
'remote_id' => $qb->createNamedParameter($remoteId, IQueryBuilder::PARAM_STR),
'parent' => $qb->createNamedParameter($parent, IQueryBuilder::PARAM_INT),
'share_type' => $qb->createNamedParameter($shareType, IQueryBuilder::PARAM_INT),
])
->executeStatement();
}
/**
* Get share by id.
*
* @return ExternalShare|false
* @throws Exception
*/
private function fetchShare(int $id): array|false {
$qb = $this->connection->getQueryBuilder();
$result = $qb->select('id', 'remote', 'remote_id', 'share_token', 'name', 'owner', 'user', 'mountpoint', 'accepted', 'parent', 'share_type', 'password', 'mountpoint_hash')
->from('share_external')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->executeQuery();
$share = $result->fetchAssociative();
$result->closeCursor();
return $share;
}
/**
* Get share by token.
*
* @return ExternalShare|false
* @throws Exception
*/
public function getShareByToken(string $token): array|false {
$qb = $this->connection->getQueryBuilder();
$result = $qb->select('id', 'remote', 'remote_id', 'share_token', 'name', 'owner', 'user', 'mountpoint', 'accepted', 'parent', 'share_type', 'password', 'mountpoint_hash')
->from('share_external')
->where($qb->expr()->eq('share_token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)))
->executeQuery();
$share = $result->fetchAssociative();
$result->closeCursor();
return $share;
}
/**
* Get share by parent id and user.
*
* @return ExternalShare|false
* @throws Exception
*/
private function getUserShare(int $parentId, IUser $user): array|false {
$qb = $this->connection->getQueryBuilder();
$result = $qb->select('id', 'remote', 'remote_id', 'share_token', 'name', 'owner', 'user', 'mountpoint', 'accepted', 'parent', 'share_type', 'password', 'mountpoint_hash')
->from('share_external')
->where($qb->expr()->andX(
$qb->expr()->eq('parent', $qb->createNamedParameter($parentId, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
))
->executeQuery();
$share = $result->fetchAssociative();
$result->closeCursor();
return $share;
}
public function getShare(int $id, ?IUser $user = null): array|false {
$user = $user ?? $this->user; $user = $user ?? $this->user;
$share = $this->fetchShare($id); try {
if ($share === false) { $externalShare = $this->externalShareMapper->getById($id);
} catch (DoesNotExistException $e) {
return false; return false;
} }
// check if the user is allowed to access it // check if the user is allowed to access it
if ($this->canAccessShare($share, $user)) { if ($this->canAccessShare($externalShare, $user)) {
return $share; return $externalShare;
} }
return false; return false;
} }
private function canAccessShare(array $share, IUser $user): bool { private function canAccessShare(ExternalShare $share, IUser $user): bool {
$validShare = isset($share['share_type']) && isset($share['user']); $isValid = $share->getShareType() === null;
if ($isValid) {
if (!$validShare) { // Invalid share type
return false; return false;
} }
// If the share is a user share, check if the user is the recipient // If the share is a user share, check if the user is the recipient
if ((int)$share['share_type'] === IShare::TYPE_USER if ($share->getShareType() === IShare::TYPE_USER && $share->getUser() === $user->getUID()) {
&& $share['user'] === $user->getUID()) {
return true; return true;
} }
// If the share is a group share, check if the user is in the group // If the share is a group share, check if the user is in the group
if ((int)$share['share_type'] === IShare::TYPE_GROUP) { if ($share->getShareType() === IShare::TYPE_GROUP) {
$parentId = (int)$share['parent']; $parentId = $share->getParent();
if ($parentId !== -1) { if ($parentId !== '-1') {
// we just retrieved a sub-share, switch to the parent entry for verification // we just retrieved a sub-share, switch to the parent entry for verification
$groupShare = $this->fetchShare($parentId); $groupShare = $this->externalShareMapper->getById($parentId);
} else { } else {
$groupShare = $share; $groupShare = $share;
} }
if ($this->groupManager->get($groupShare['user'])->inGroup($user)) { if ($this->groupManager->get($groupShare->getUser())->inGroup($user)) {
return true; return true;
} }
} }
@ -271,17 +167,52 @@ class Manager {
return false; return false;
} }
public function getShareByToken(string $token): ExternalShare|false {
try {
return $this->externalShareMapper->getShareByToken($token);
} catch (DoesNotExistException $e) {
return false;
}
}
/** /**
* Updates accepted flag in the database.
*
* @throws Exception * @throws Exception
*/ */
private function updateAccepted(int $shareId, bool $accepted): void { private function updateSubShare(ExternalShare $externalShare, IUser $user, ?string $mountPoint, int $accepted): ExternalShare {
$qb = $this->connection->getQueryBuilder(); $parentId = $externalShare->getParent();
$qb->update('share_external') if ($parentId !== '-1') {
->set('accepted', $qb->createNamedParameter($accepted ? 1 : 0, IQueryBuilder::PARAM_INT)) // this is the sub-share
->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))) $subShare = $externalShare;
->executeStatement(); } else {
try {
$subShare = $this->externalShareMapper->getUserShare($externalShare, $user);
} catch (DoesNotExistException $e) {
$subShare = new ExternalShare();
$subShare->setId($this->snowflakeGenerator->nextId());
$subShare->setRemote($externalShare->getRemote());
$subShare->setPassword($externalShare->getPassword());
$subShare->setName($externalShare->getName());
$subShare->setOwner($externalShare->getOwner());
$subShare->setUser($user->getUID());
$subShare->setMountpoint($mountPoint ?? $externalShare->getMountpoint());
$subShare->setAccepted($accepted);
$subShare->setRemoteId($externalShare->getRemoteId());
$subShare->setParent($externalShare->getId());
$subShare->setShareType($externalShare->getShareType());
$subShare->setShareToken($externalShare->getShareToken());
$this->externalShareMapper->insert($subShare);
}
}
if ($subShare->getAccepted() !== $accepted) {
$subShare->setAccepted($accepted);
if ($mountPoint !== null) {
$subShare->setMountpoint($mountPoint);
}
$this->externalShareMapper->update($subShare);
}
return $subShare;
} }
/** /**
@ -289,7 +220,7 @@ class Manager {
* *
* @return bool True if the share could be accepted, false otherwise * @return bool True if the share could be accepted, false otherwise
*/ */
public function acceptShare(int $id, ?IUser $user = null): bool { public function acceptShare(ExternalShare $externalShare, ?IUser $user = null): bool {
// If we're auto-accepting a share, we need to know the user id // If we're auto-accepting a share, we need to know the user id
// as there is no session available while processing the share // as there is no session available while processing the share
// from the remote server request. // from the remote server request.
@ -299,91 +230,45 @@ class Manager {
return false; return false;
} }
$share = $this->getShare($id, $user);
$result = false; $result = false;
$this->setupManager->setupForUser($user);
$folder = $this->rootFolder->getUserFolder($user->getUID());
if ($share) { $shareFolder = Helper::getShareFolder(null, $user->getUID());
$this->setupManager->setupForUser($user); $shareFolder = $folder->get($shareFolder);
$folder = $this->rootFolder->getUserFolder($user->getUID()); /** @var Folder $shareFolder */
$mountPoint = $shareFolder->getNonExistingName($externalShare->getName());
$mountPoint = Filesystem::normalizePath($mountPoint);
$userShareAccepted = false;
$shareFolder = Helper::getShareFolder(null, $user->getUID()); if ($externalShare->getShareType() === IShare::TYPE_USER) {
$shareFolder = $folder->get($shareFolder); if ($externalShare->getUser() === $user->getUID()) {
/** @var Folder $shareFolder */ $externalShare->setAccepted(IShare::STATUS_ACCEPTED);
$mountPoint = $shareFolder->getNonExistingName($share['name']); $externalShare->setMountpoint($mountPoint);
$mountPoint = Filesystem::normalizePath($mountPoint); $this->externalShareMapper->update($externalShare);
$hash = md5($mountPoint); $userShareAccepted = true;
$userShareAccepted = false;
if ((int)$share['share_type'] === IShare::TYPE_USER) {
$qb = $this->connection->getQueryBuilder();
$qb->update('share_external')
->set('accepted', $qb->createNamedParameter(1))
->set('mountpoint', $qb->createNamedParameter($mountPoint))
->set('mountpoint_hash', $qb->createNamedParameter($hash))
->where($qb->expr()->andX(
$qb->expr()->eq('id', $qb->createNamedParameter($id)),
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID()))
));
$userShareAccepted = $qb->executeStatement();
} else {
$parentId = (int)$share['parent'];
if ($parentId !== -1) {
// this is the sub-share
$subshare = $share;
} else {
$subshare = $this->getUserShare($id, $user);
}
if ($subshare !== false) {
try {
$qb = $this->connection->getQueryBuilder();
$qb->update('share_external')
->set('accepted', $qb->createNamedParameter(1))
->set('mountpoint', $qb->createNamedParameter($mountPoint))
->set('mountpoint_hash', $qb->createNamedParameter($hash))
->where($qb->expr()->andX(
$qb->expr()->eq('id', $qb->createNamedParameter($subshare['id'])),
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID()))
))
->executeStatement();
$result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not update share', ['exception' => $e]);
$result = false;
}
} else {
try {
$this->writeShareToDb(
$share['remote'],
$share['share_token'],
$share['password'],
$share['name'],
$share['owner'],
$user,
$mountPoint, $hash, 1,
$share['remote_id'],
$id,
$share['share_type']);
$result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not create share', ['exception' => $e]);
$result = false;
}
}
} }
} else {
if ($userShareAccepted !== false) { try {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept'); $this->updateSubShare($externalShare, $user, $mountPoint, IShare::STATUS_ACCEPTED);
$event = new FederatedShareAddedEvent($share['remote']);
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($user));
$result = true; $result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not create sub-share', ['exception' => $e]);
$this->processNotification($externalShare, $user);
return false;
} }
} }
// Make sure the user has no notification for something that does not exist anymore. if ($userShareAccepted !== false) {
$this->processNotification($id, $user); $this->sendFeedbackToRemote($externalShare, 'accept');
$event = new FederatedShareAddedEvent($externalShare->getRemote());
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($user));
$result = true;
}
// Make sure the user has no notification for something that does not exist anymore.
$this->processNotification($externalShare, $user);
return $result; return $result;
} }
@ -392,108 +277,68 @@ class Manager {
* *
* @return bool True if the share could be declined, false otherwise * @return bool True if the share could be declined, false otherwise
*/ */
public function declineShare(int $id, ?Iuser $user = null): bool { public function declineShare(ExternalShare $externalShare, ?Iuser $user = null): bool {
$user = $user ?? $this->user; $user = $user ?? $this->user;
if ($user === null) { if ($user === null) {
$this->logger->error('No user specified for declining share'); $this->logger->error('No user specified for declining share');
return false; return false;
} }
$share = $this->getShare($id, $user);
$result = false; $result = false;
if ($share && (int)$share['share_type'] === IShare::TYPE_USER) { if ($externalShare->getShareType() === IShare::TYPE_USER) {
$qb = $this->connection->getQueryBuilder(); if ($externalShare->getUser() === $user->getUID()) {
$qb->delete('share_external') $this->externalShareMapper->delete($externalShare);
->where($qb->expr()->andX( $this->sendFeedbackToRemote($externalShare, 'decline');
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)), $this->processNotification($externalShare, $user);
$qb->expr()->eq('user', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)) $result = true;
))
->executeStatement();
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
$this->processNotification($id, $user);
$result = true;
} elseif ($share && (int)$share['share_type'] === IShare::TYPE_GROUP) {
$parentId = (int)$share['parent'];
if ($parentId !== -1) {
// this is the sub-share
$subshare = $share;
} else {
$subshare = $this->getUserShare($id, $user);
} }
} elseif ($externalShare->getShareType() === IShare::TYPE_GROUP) {
if ($subshare !== false) { try {
try { $this->updateSubShare($externalShare, $user, null, IShare::STATUS_PENDING);
$this->updateAccepted((int)$subshare['id'], false); $result = true;
$result = true; } catch (Exception $e) {
} catch (Exception $e) { $this->logger->emergency('Could not create sub-share', ['exception' => $e]);
$this->logger->emergency('Could not update share', ['exception' => $e]); $this->processNotification($externalShare, $user);
$result = false; return false;
}
} else {
try {
$this->writeShareToDb(
$share['remote'],
$share['share_token'],
$share['password'],
$share['name'],
$share['owner'],
$user,
$share['mountpoint'],
$share['mountpoint_hash'],
0,
$share['remote_id'],
$id,
$share['share_type']);
$result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not create share', ['exception' => $e]);
$result = false;
}
} }
$this->processNotification($id, $user);
} }
// Make sure the user has no notification for something that does not exist anymore.
$this->processNotification($externalShare, $user);
return $result; return $result;
} }
public function processNotification(int $remoteShare, ?IUser $user = null): void { public function processNotification(ExternalShare $remoteShare, ?IUser $user = null): void {
$user = $user ?? $this->user; $user = $user ?? $this->user;
if ($user === null) { if ($user === null) {
$this->logger->error('No user specified for processing notification'); $this->logger->error('No user specified for processing notification');
return; return;
} }
$share = $this->fetchShare($remoteShare);
if ($share === false) {
return;
}
$filter = $this->notificationManager->createNotification(); $filter = $this->notificationManager->createNotification();
$filter->setApp('files_sharing') $filter->setApp('files_sharing')
->setUser($user->getUID()) ->setUser($user->getUID())
->setObject('remote_share', (string)$remoteShare); ->setObject('remote_share', $remoteShare->getId());
$this->notificationManager->markProcessed($filter); $this->notificationManager->markProcessed($filter);
} }
/** /**
* Inform remote server whether server-to-server share was accepted/declined * Inform remote server whether server-to-server share was accepted/declined
* *
* @param string $remoteId Share id on the remote host
* @param 'accept'|'decline' $feedback * @param 'accept'|'decline' $feedback
*/ */
private function sendFeedbackToRemote(string $remote, string $token, string $remoteId, string $feedback): bool { private function sendFeedbackToRemote(ExternalShare $externalShare, string $feedback): bool {
$result = $this->tryOCMEndPoint($remote, $token, $remoteId, $feedback); $result = $this->tryOCMEndPoint($externalShare, $feedback);
if (is_array($result)) { if (is_array($result)) {
return true; return true;
} }
$federationEndpoints = $this->discoveryService->discover($remote, 'FEDERATED_SHARING'); $federationEndpoints = $this->discoveryService->discover($externalShare->getRemote(), 'FEDERATED_SHARING');
$endpoint = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares'; $endpoint = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares';
$url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=json'; $url = rtrim($externalShare->getRemote(), '/') . $endpoint . '/' . $externalShare->getRemoteId() . '/' . $feedback . '?format=json';
$fields = ['token' => $token]; $fields = ['token' => $externalShare->getShareToken()];
$client = $this->clientService->newClient(); $client = $this->clientService->newClient();
@ -517,37 +362,36 @@ class Manager {
/** /**
* Try to send accept message to ocm end-point * Try to send accept message to ocm end-point
* *
* @param string $remoteId id of the share
* @param 'accept'|'decline' $feedback * @param 'accept'|'decline' $feedback
* @return array|false * @return array|false
*/ */
protected function tryOCMEndPoint(string $remoteDomain, string $token, string $remoteId, string $feedback) { protected function tryOCMEndPoint(ExternalShare $externalShare, string $feedback) {
switch ($feedback) { switch ($feedback) {
case 'accept': case 'accept':
$notification = $this->cloudFederationFactory->getCloudFederationNotification(); $notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage( $notification->setMessage(
'SHARE_ACCEPTED', 'SHARE_ACCEPTED',
'file', 'file',
$remoteId, $externalShare->getRemoteId(),
[ [
'sharedSecret' => $token, 'sharedSecret' => $externalShare->getShareToken(),
'message' => 'Recipient accept the share' 'message' => 'Recipient accept the share'
] ]
); );
return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification); return $this->cloudFederationProviderManager->sendNotification($externalShare->getRemote(), $notification);
case 'decline': case 'decline':
$notification = $this->cloudFederationFactory->getCloudFederationNotification(); $notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage( $notification->setMessage(
'SHARE_DECLINED', 'SHARE_DECLINED',
'file', 'file',
$remoteId, $externalShare->getRemoteId(),
[ [
'sharedSecret' => $token, 'sharedSecret' => $externalShare->getShareToken(),
'message' => 'Recipient declined the share' 'message' => 'Recipient declined the share'
] ]
); );
return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification); return $this->cloudFederationProviderManager->sendNotification($externalShare->getRemote(), $notification);
} }
return false; return false;
} }
@ -560,7 +404,7 @@ class Manager {
return rtrim(substr($path, strlen($prefix)), '/'); return rtrim(substr($path, strlen($prefix)), '/');
} }
public function getMount(array $data, ?IUser $user = null) { public function getMount(array $data, ?IUser $user = null): Mount {
$user = $user ?? $this->user; $user = $user ?? $this->user;
$data['manager'] = $this; $data['manager'] = $this;
$mountPoint = '/' . $user->getUID() . '/files' . $data['mountpoint']; $mountPoint = '/' . $user->getUID() . '/files' . $data['mountpoint'];
@ -599,7 +443,7 @@ class Manager {
return $result; return $result;
} }
public function removeShare($mountPoint): bool { public function removeShare(string $mountPoint): bool {
try { try {
$mountPointObj = $this->mountManager->find($mountPoint); $mountPointObj = $this->mountManager->find($mountPoint);
} catch (NotFoundException $e) { } catch (NotFoundException $e) {
@ -613,31 +457,28 @@ class Manager {
$id = $mountPointObj->getStorage()->getCache()->getId(''); $id = $mountPointObj->getStorage()->getCache()->getId('');
$mountPoint = $this->stripPath($mountPoint); $mountPoint = $this->stripPath($mountPoint);
$hash = md5($mountPoint);
try { try {
$qb = $this->connection->getQueryBuilder(); try {
$qb->select('remote', 'share_token', 'remote_id', 'share_type', 'id') $externalShare = $this->externalShareMapper->getByMountPointAndUser($mountPoint, $this->user);
->from('share_external') } catch (DoesNotExistException $e) {
->where($qb->expr()->eq('mountpoint_hash', $qb->createNamedParameter($hash))) // ignore
->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($this->user->getUID()))); $this->removeReShares((string)$id);
$result = $qb->executeQuery(); return true;
$share = $result->fetchAssociative(); }
$result->closeCursor();
if ($share !== false && (int)$share['share_type'] === IShare::TYPE_USER) { if ($externalShare->getShareType() === IShare::TYPE_USER) {
try { try {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); $this->sendFeedbackToRemote($externalShare, 'decline');
} catch (\Throwable $e) { } catch (\Throwable $e) {
// if we fail to notify the remote (probably cause the remote is down) // if we fail to notify the remote (probably cause the remote is down)
// we still want the share to be gone to prevent undeletable remotes // we still want the share to be gone to prevent undeletable remotes
} }
$qb = $this->connection->getQueryBuilder(); $this->externalShareMapper->delete($externalShare);
$qb->delete('share_external') } elseif ($externalShare->getShareType() === IShare::TYPE_GROUP) {
->where('id', $qb->createNamedParameter((int)$share['id'])) $externalShare->setAccepted(IShare::STATUS_PENDING);
->executeStatement(); $this->externalShareMapper->update($externalShare);
} elseif ($share !== false && (int)$share['share_type'] === IShare::TYPE_GROUP) {
$this->updateAccepted((int)$share['id'], false);
} }
$this->removeReShares((string)$id); $this->removeReShares((string)$id);
@ -670,39 +511,16 @@ class Manager {
} }
/** /**
* remove all shares for user $uid if the user was deleted * Remove all shares for user $uid if the user was deleted.
*/ */
public function removeUserShares(IUser $user): bool { public function removeUserShares(IUser $user): bool {
try { try {
$qb = $this->connection->getQueryBuilder(); $shares = $this->externalShareMapper->getUserShares($user);
$qb->select('id', 'remote', 'share_type', 'share_token', 'remote_id')
->from('share_external')
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)));
$result = $qb->executeQuery();
$shares = $result->fetchAllAssociative();
$result->closeCursor();
foreach ($shares as $share) { foreach ($shares as $share) {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); $this->sendFeedbackToRemote($share, 'decline');
} }
$qb = $this->connection->getQueryBuilder(); $this->externalShareMapper->deleteUserShares($user);
$qb->delete('share_external')
// user field can specify a user or a group
->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
->andWhere(
$qb->expr()->orX(
// delete direct shares
$qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_USER)),
// delete sub-shares of group shares for that user
$qb->expr()->andX(
$qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_GROUP)),
$qb->expr()->neq('parent', $qb->expr()->literal(-1)),
)
)
);
$qb->executeStatement();
} catch (Exception $ex) { } catch (Exception $ex) {
$this->logger->emergency('Could not delete user shares', ['exception' => $ex]); $this->logger->emergency('Could not delete user shares', ['exception' => $ex]);
return false; return false;
@ -711,32 +529,9 @@ class Manager {
return true; return true;
} }
public function removeGroupShares($gid): bool { public function removeGroupShares(IGroup $group): bool {
try { try {
$qb = $this->connection->getQueryBuilder(); $this->externalShareMapper->deleteGroupShares($group);
$qb->select('id', 'remote', 'share_type', 'share_token', 'remote_id')
->from('share_external')
->where($qb->expr()->eq('user', $qb->createNamedParameter($gid)))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)));
$result = $qb->executeQuery();
$shares = $result->fetchAllAssociative();
$result->closeCursor();
$qb = $this->connection->getQueryBuilder();
// delete group share entry and matching sub-entries
$qb->delete('share_external')
->where(
$qb->expr()->orX(
$qb->expr()->eq('id', $qb->createParameter('share_id')),
$qb->expr()->eq('parent', $qb->createParameter('share_parent_id'))
)
);
foreach ($shares as $share) {
$qb->setParameter('share_id', $share['id']);
$qb->setParameter('share_parent_id', $share['id']);
$qb->executeStatement();
}
} catch (Exception $ex) { } catch (Exception $ex) {
$this->logger->emergency('Could not delete user shares', ['exception' => $ex]); $this->logger->emergency('Could not delete user shares', ['exception' => $ex]);
return false; return false;
@ -746,77 +541,27 @@ class Manager {
} }
/** /**
* return a list of shares which are not yet accepted by the user * Return a list of shares which are not yet accepted by the user.
* *
* @return list<Files_SharingRemoteShare> list of open server-to-server shares * @return list<ExternalShare> list of open server-to-server shares
*/ */
public function getOpenShares() { public function getOpenShares(): array {
return $this->getShares(false);
}
/**
* return a list of shares which are accepted by the user
*
* @return list<Files_SharingRemoteShare> list of accepted server-to-server shares
*/
public function getAcceptedShares() {
return $this->getShares(true);
}
/**
* return a list of shares for the user
*
* @param bool|null $accepted True for accepted only,
* false for not accepted,
* null for all shares of the user
* @return list<Files_SharingRemoteShare> list of open server-to-server shares
*/
private function getShares($accepted) {
// Not allowing providing a user here,
// as we only want to retrieve shares for the current user.
$groups = $this->groupManager->getUserGroups($this->user);
$userGroups = [];
foreach ($groups as $group) {
$userGroups[] = $group->getGID();
}
$qb = $this->connection->getQueryBuilder();
$qb->select('id', 'share_type', 'parent', 'remote', 'remote_id', 'share_token', 'name', 'owner', 'user', 'mountpoint', 'accepted')
->from('share_external')
->where(
$qb->expr()->orX(
$qb->expr()->eq('user', $qb->createNamedParameter($this->user->getUID())),
$qb->expr()->in(
'user',
$qb->createNamedParameter($userGroups, IQueryBuilder::PARAM_STR_ARRAY)
)
)
)
->orderBy('id', 'ASC');
try { try {
$result = $qb->executeQuery(); return $this->externalShareMapper->getShares($this->user, IShare::STATUS_PENDING);
/** @var list<Files_SharingRemoteShare> $shares */ } catch (Exception $e) {
$shares = $result->fetchAllAssociative(); $this->logger->emergency('Error when retrieving shares', ['exception' => $e]);
$result->closeCursor(); return [];
}
}
// remove parent group share entry if we have a specific user share entry for the user /**
$toRemove = []; * Return a list of shares which are accepted by the user.
foreach ($shares as $share) { *
if ((int)$share['share_type'] === IShare::TYPE_GROUP && (int)$share['parent'] > 0) { * @return list<ExternalShare> list of accepted server-to-server shares
$toRemove[] = $share['parent']; */
} public function getAcceptedShares(): array {
} try {
$shares = array_filter($shares, function ($share) use ($toRemove) { return $this->externalShareMapper->getShares($this->user, IShare::STATUS_ACCEPTED);
return !in_array($share['id'], $toRemove, true);
});
if (!is_null($accepted)) {
$shares = array_filter($shares, function ($share) use ($accepted) {
return (bool)$share['accepted'] === $accepted;
});
}
return array_values($shares);
} catch (Exception $e) { } catch (Exception $e) {
$this->logger->emergency('Error when retrieving shares', ['exception' => $e]); $this->logger->emergency('Error when retrieving shares', ['exception' => $e]);
return []; return [];

View file

@ -35,9 +35,8 @@ class Mount extends MountPoint implements MoveableMount, ISharedMountPoint {
* Move the mount point to $target * Move the mount point to $target
* *
* @param string $target the target mount point * @param string $target the target mount point
* @return bool
*/ */
public function moveMount($target) { public function moveMount($target): bool {
$result = $this->manager->setMountPoint($this->mountPoint, $target); $result = $this->manager->setMountPoint($this->mountPoint, $target);
$this->setMountPoint($target); $this->setMountPoint($target);
@ -57,7 +56,7 @@ class Mount extends MountPoint implements MoveableMount, ISharedMountPoint {
* *
* @return string * @return string
*/ */
public function getMountType() { public function getMountType(): string {
return 'shared'; return 'shared';
} }
} }

View file

@ -83,13 +83,13 @@ namespace OCA\Files_Sharing;
* @psalm-type Files_SharingRemoteShare = array{ * @psalm-type Files_SharingRemoteShare = array{
* accepted: bool, * accepted: bool,
* file_id: int|null, * file_id: int|null,
* id: int, * id: string,
* mimetype: string|null, * mimetype: string|null,
* mountpoint: string, * mountpoint: string,
* mtime: int|null, * mtime: int|null,
* name: string, * name: string,
* owner: string, * owner: string,
* parent: int|null, * parent: string|null,
* permissions: int|null, * permissions: int|null,
* remote: string, * remote: string,
* remote_id: string, * remote_id: string,

View file

@ -416,8 +416,7 @@
"nullable": true "nullable": true
}, },
"id": { "id": {
"type": "integer", "type": "string"
"format": "int64"
}, },
"mimetype": { "mimetype": {
"type": "string", "type": "string",
@ -438,8 +437,7 @@
"type": "string" "type": "string"
}, },
"parent": { "parent": {
"type": "integer", "type": "string",
"format": "int64",
"nullable": true "nullable": true
}, },
"permissions": { "permissions": {
@ -3932,7 +3930,7 @@
"/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}": { "/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}": {
"post": { "post": {
"operationId": "remote-accept-share", "operationId": "remote-accept-share",
"summary": "Accept a remote share", "summary": "Accept a remote share.",
"tags": [ "tags": [
"remote" "remote"
], ],
@ -3951,8 +3949,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -4055,7 +4052,7 @@
}, },
"delete": { "delete": {
"operationId": "remote-decline-share", "operationId": "remote-decline-share",
"summary": "Decline a remote share", "summary": "Decline a remote share.",
"tags": [ "tags": [
"remote" "remote"
], ],
@ -4074,8 +4071,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -4199,8 +4195,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -4324,8 +4319,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {

View file

@ -8,10 +8,13 @@
namespace OCA\Files_Sharing\Tests\Command; namespace OCA\Files_Sharing\Tests\Command;
use OCA\Files_Sharing\Command\CleanupRemoteStorages; use OCA\Files_Sharing\Command\CleanupRemoteStorages;
use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\ExternalShareMapper;
use OCP\Federation\ICloudId; use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager; use OCP\Federation\ICloudIdManager;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Server; use OCP\Server;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -49,16 +52,6 @@ class CleanupRemoteStoragesTest extends TestCase {
$storageQuery->insert('storages') $storageQuery->insert('storages')
->setValue('id', $storageQuery->createParameter('id')); ->setValue('id', $storageQuery->createParameter('id'));
$shareExternalQuery = Server::get(IDBConnection::class)->getQueryBuilder();
$shareExternalQuery->insert('share_external')
->setValue('share_token', $shareExternalQuery->createParameter('share_token'))
->setValue('remote', $shareExternalQuery->createParameter('remote'))
->setValue('name', $shareExternalQuery->createParameter('name'))
->setValue('owner', $shareExternalQuery->createParameter('owner'))
->setValue('user', $shareExternalQuery->createParameter('user'))
->setValue('mountpoint', $shareExternalQuery->createParameter('mountpoint'))
->setValue('mountpoint_hash', $shareExternalQuery->createParameter('mountpoint_hash'));
$filesQuery = Server::get(IDBConnection::class)->getQueryBuilder(); $filesQuery = Server::get(IDBConnection::class)->getQueryBuilder();
$filesQuery->insert('filecache') $filesQuery->insert('filecache')
->setValue('storage', $filesQuery->createParameter('storage')) ->setValue('storage', $filesQuery->createParameter('storage'))
@ -73,15 +66,15 @@ class CleanupRemoteStoragesTest extends TestCase {
} }
if (isset($storage['share_token'])) { if (isset($storage['share_token'])) {
$shareExternalQuery $externalShare = new ExternalShare();
->setParameter('share_token', $storage['share_token']) $externalShare->setId(Server::get(IGenerator::class)->nextId());
->setParameter('remote', $storage['remote']) $externalShare->setShareToken($storage['share_token']);
->setParameter('name', 'irrelevant') $externalShare->setRemote($storage['remote']);
->setParameter('owner', 'irrelevant') $externalShare->setName('irrelevant');
->setParameter('user', $storage['user']) $externalShare->setOwner('irrelevant');
->setParameter('mountpoint', 'irrelevant') $externalShare->setUser($storage['user']);
->setParameter('mountpoint_hash', 'irrelevant'); $externalShare->setMountpoint('irrelevant');
$shareExternalQuery->executeStatement(); Server::get(ExternalShareMapper::class)->insert($externalShare);
} }
if (isset($storage['files_count'])) { if (isset($storage['files_count'])) {

View file

@ -8,10 +8,9 @@
namespace OCA\Files_Sharing\Tests\Controllers; namespace OCA\Files_Sharing\Tests\Controllers;
use OCA\Files_Sharing\Controller\ExternalSharesController; use OCA\Files_Sharing\Controller\ExternalSharesController;
use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\Manager;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IRequest; use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -21,21 +20,13 @@ use PHPUnit\Framework\MockObject\MockObject;
* @package OCA\Files_Sharing\Controllers * @package OCA\Files_Sharing\Controllers
*/ */
class ExternalShareControllerTest extends \Test\TestCase { class ExternalShareControllerTest extends \Test\TestCase {
/** @var IRequest */ private IRequest&MockObject $request;
private $request; private Manager $externalManager;
/** @var \OCA\Files_Sharing\External\Manager */
private $externalManager;
/** @var IConfig|MockObject */
private $config;
/** @var IClientService */
private $clientService;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
$this->request = $this->createMock(IRequest::class); $this->request = $this->createMock(IRequest::class);
$this->externalManager = $this->createMock(Manager::class); $this->externalManager = $this->createMock(Manager::class);
$this->clientService = $this->createMock(IClientService::class);
$this->config = $this->createMock(IConfig::class);
} }
/** /**
@ -46,8 +37,6 @@ class ExternalShareControllerTest extends \Test\TestCase {
'files_sharing', 'files_sharing',
$this->request, $this->request,
$this->externalManager, $this->externalManager,
$this->clientService,
$this->config,
); );
} }
@ -61,20 +50,32 @@ class ExternalShareControllerTest extends \Test\TestCase {
} }
public function testCreate(): void { public function testCreate(): void {
$share = $this->createMock(ExternalShare::class);
$this->externalManager
->expects($this->once())
->method('getShare')
->with('4')
->willReturn($share);
$this->externalManager $this->externalManager
->expects($this->once()) ->expects($this->once())
->method('acceptShare') ->method('acceptShare')
->with(4); ->with($share);
$this->assertEquals(new JSONResponse(), $this->getExternalShareController()->create(4)); $this->assertEquals(new JSONResponse(), $this->getExternalShareController()->create('4'));
} }
public function testDestroy(): void { public function testDestroy(): void {
$share = $this->createMock(ExternalShare::class);
$this->externalManager
->expects($this->once())
->method('getShare')
->with('4')
->willReturn($share);
$this->externalManager $this->externalManager
->expects($this->once()) ->expects($this->once())
->method('declineShare') ->method('declineShare')
->with(4); ->with($share);
$this->assertEquals(new JSONResponse(), $this->getExternalShareController()->destroy(4)); $this->assertEquals(new JSONResponse(), $this->getExternalShareController()->destroy('4'));
} }
} }

View file

@ -18,6 +18,7 @@ use OCP\Files\Cache\ICacheEntry;
use OCP\ICacheFactory; use OCP\ICacheFactory;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\IUserManager; use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
/** /**
* Class Cache * Class Cache
@ -27,26 +28,11 @@ use OCP\IUserManager;
*/ */
#[\PHPUnit\Framework\Attributes\Group('DB')] #[\PHPUnit\Framework\Attributes\Group('DB')]
class CacheTest extends TestCase { class CacheTest extends TestCase {
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ protected IManager&MockObject $contactsManager;
protected $contactsManager; private Storage&MockObject $storage;
private Cache $cache;
/** private string $remoteUser;
* @var Storage private ICloudIdManager $cloudIdManager;
**/
private $storage;
/**
* @var Cache
*/
private $cache;
/**
* @var string
*/
private $remoteUser;
/** @var ICloudIdManager */
private $cloudIdManager;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -62,7 +48,7 @@ class CacheTest extends TestCase {
); );
$this->remoteUser = $this->getUniqueID('remoteuser'); $this->remoteUser = $this->getUniqueID('remoteuser');
$this->storage = $this->getMockBuilder('\OCA\Files_Sharing\External\Storage') $this->storage = $this->getMockBuilder(\OCA\Files_Sharing\External\Storage::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->storage $this->storage

View file

@ -13,6 +13,8 @@ use OC\Files\SetupManager;
use OC\Files\SetupManagerFactory; use OC\Files\SetupManagerFactory;
use OC\Files\Storage\StorageFactory; use OC\Files\Storage\StorageFactory;
use OC\Files\Storage\Temporary; use OC\Files\Storage\Temporary;
use OCA\Files_Sharing\External\ExternalShare;
use OCA\Files_Sharing\External\ExternalShareMapper;
use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\Manager;
use OCA\Files_Sharing\External\MountProvider; use OCA\Files_Sharing\External\MountProvider;
use OCA\Files_Sharing\Tests\TestCase; use OCA\Files_Sharing\Tests\TestCase;
@ -38,6 +40,7 @@ use OCP\IUserSession;
use OCP\OCS\IDiscoveryService; use OCP\OCS\IDiscoveryService;
use OCP\Server; use OCP\Server;
use OCP\Share\IShare; use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Test\Traits\UserTrait; use Test\Traits\UserTrait;
@ -68,6 +71,7 @@ class ManagerTest extends TestCase {
protected IUserManager&MockObject $userManager; protected IUserManager&MockObject $userManager;
protected SetupManager&MockObject $setupManager; protected SetupManager&MockObject $setupManager;
protected ICertificateManager&MockObject $certificateManager; protected ICertificateManager&MockObject $certificateManager;
private ExternalShareMapper $externalShareMapper;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -93,6 +97,8 @@ class ManagerTest extends TestCase {
return $folder; return $folder;
}); });
$this->externalShareMapper = new ExternalShareMapper(Server::get(IDBConnection::class), $this->groupManager);
$this->contactsManager = $this->createMock(IManager::class); $this->contactsManager = $this->createMock(IManager::class);
// needed for MountProvider() initialization // needed for MountProvider() initialization
$this->contactsManager->expects($this->any()) $this->contactsManager->expects($this->any())
@ -163,6 +169,8 @@ class ManagerTest extends TestCase {
$this->rootFolder, $this->rootFolder,
$this->setupManager, $this->setupManager,
$this->certificateManager, $this->certificateManager,
$this->externalShareMapper,
Server::get(IGenerator::class),
] ]
)->onlyMethods(['tryOCMEndPoint'])->getMock(); )->onlyMethods(['tryOCMEndPoint'])->getMock();
} }
@ -181,39 +189,35 @@ class ManagerTest extends TestCase {
} }
public function testAddUserShare(): void { public function testAddUserShare(): void {
$this->doTestAddShare([ $userShare = new ExternalShare();
'remote' => 'http://localhost', $userShare->setId(Server::get(IGenerator::class)->nextId());
'token' => 'token1', $userShare->setRemote('http://localhost');
'password' => '', $userShare->setShareToken('token1');
'name' => '/SharedFolder', $userShare->setPassword('');
'owner' => 'foobar', $userShare->setName('/SharedFolder');
'shareType' => IShare::TYPE_USER, $userShare->setOwner('foobar');
'accepted' => false, $userShare->setShareType(IShare::TYPE_USER);
'userOrGroup' => $this->user, $userShare->setAccepted(IShare::STATUS_PENDING);
'remoteId' => '2342' $userShare->setRemoteId('2342');
], false);
$this->doTestAddShare($userShare, $this->user);
} }
public function testAddGroupShare(): void { public function testAddGroupShare(): void {
$this->doTestAddShare([ $groupShare = new ExternalShare();
'remote' => 'http://localhost', $groupShare->setId(Server::get(IGenerator::class)->nextId());
'token' => 'token1', $groupShare->setRemote('http://localhost');
'password' => '', $groupShare->setOwner('foobar');
'name' => '/SharedFolder', $groupShare->setShareType(IShare::TYPE_GROUP);
'owner' => 'foobar', $groupShare->setAccepted(IShare::STATUS_PENDING);
'shareType' => IShare::TYPE_GROUP, $groupShare->setRemoteId('2342');
'accepted' => false, $groupShare->setShareToken('token1');
'userOrGroup' => $this->group1, $groupShare->setPassword('');
'remoteId' => '2342' $groupShare->setName('/SharedFolder');
], true); $this->doTestAddShare($groupShare, $this->group1, isGroup: true);
} }
public function doTestAddShare(array $shareData1, bool $isGroup = false): void { public function doTestAddShare(ExternalShare $shareData1, IUser|IGroup $userOrGroup, bool $isGroup = false): void {
$shareData2 = $shareData1;
$shareData2['token'] = 'token2';
$shareData3 = $shareData1;
$shareData3['token'] = 'token3';
if ($isGroup) { if ($isGroup) {
$this->manager->expects($this->never())->method('tryOCMEndPoint')->willReturn(false); $this->manager->expects($this->never())->method('tryOCMEndPoint')->willReturn(false);
} else { } else {
@ -226,27 +230,34 @@ class ManagerTest extends TestCase {
} }
// Add a share for "user" // Add a share for "user"
$this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], $shareData1)); $this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], [$shareData1, $userOrGroup]));
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
$this->assertExternalShareEntry($shareData1, $openShares[0], 1, '{{TemporaryMountPointName#' . $shareData1['name'] . '}}', $shareData1['userOrGroup']); $this->assertExternalShareEntry($shareData1, $openShares[0], 1, '{{TemporaryMountPointName#' . $shareData1->getName() . '}}', $userOrGroup);
$shareData2 = $shareData1->clone();
$shareData2->setShareToken('token2');
$shareData2->setId(\OCP\Server::get(IGenerator::class)->nextId());
$shareData3 = $shareData1->clone();
$shareData3->setShareToken('token3');
$shareData3->setId(\OCP\Server::get(IGenerator::class)->nextId());
$this->setupMounts(); $this->setupMounts();
$this->assertNotMount('SharedFolder'); $this->assertNotMount('SharedFolder');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
// Add a second share for "user" with the same name // Add a second share for "user" with the same name
$this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], $shareData2)); $this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], [$shareData2, $userOrGroup]));
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(2, $openShares); $this->assertCount(2, $openShares);
$this->assertExternalShareEntry($shareData1, $openShares[0], 1, '{{TemporaryMountPointName#' . $shareData1['name'] . '}}', $shareData1['userOrGroup']); $this->assertExternalShareEntry($shareData1, $openShares[0], 1, '{{TemporaryMountPointName#' . $shareData1->getName() . '}}', $userOrGroup);
// New share falls back to "-1" appendix, because the name is already taken // New share falls back to "-1" appendix, because the name is already taken
$this->assertExternalShareEntry($shareData2, $openShares[1], 2, '{{TemporaryMountPointName#' . $shareData2['name'] . '}}-1', $shareData2['userOrGroup']); $this->assertExternalShareEntry($shareData2, $openShares[1], 2, '{{TemporaryMountPointName#' . $shareData2->getName() . '}}-1', $userOrGroup);
$this->setupMounts(); $this->setupMounts();
$this->assertNotMount('SharedFolder'); $this->assertNotMount('SharedFolder');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
$newClientCalls = []; $newClientCalls = [];
$this->clientService $this->clientService
@ -271,44 +282,44 @@ class ManagerTest extends TestCase {
])); ]));
$client->expects($this->once()) $client->expects($this->once())
->method('post') ->method('post')
->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[0]['remote_id']), $this->anything()) ->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[0]->getRemoteId()), $this->anything())
->willReturn($response); ->willReturn($response);
} }
// Accept the first share // Accept the first share
$this->assertTrue($this->manager->acceptShare($openShares[0]['id'])); $this->assertTrue($this->manager->acceptShare($openShares[0]));
// Check remaining shares - Accepted // Check remaining shares - Accepted
$acceptedShares = self::invokePrivate($this->manager, 'getShares', [true]); $acceptedShares = $this->externalShareMapper->getShares($this->user, IShare::STATUS_ACCEPTED);
$this->assertCount(1, $acceptedShares); $this->assertCount(1, $acceptedShares);
$shareData1['accepted'] = true; $shareData1->setAccepted(true);
$this->assertExternalShareEntry($shareData1, $acceptedShares[0], 1, $shareData1['name'], $this->user); $this->assertExternalShareEntry($shareData1, $acceptedShares[0], 1, $shareData1->getName(), $this->user);
// Check remaining shares - Open // Check remaining shares - Open
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
$this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2['name'] . '}}-1', $shareData2['userOrGroup']); $this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2->getName() . '}}-1', $userOrGroup);
$this->setupMounts(); $this->setupMounts();
$this->assertMount($shareData1['name']); $this->assertMount($shareData1->getName());
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
// Add another share for "user" with the same name // Add another share for "user" with the same name
$this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], $shareData3)); $this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], [$shareData3, $userOrGroup]));
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(2, $openShares); $this->assertCount(2, $openShares);
$this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2['name'] . '}}-1', $shareData2['userOrGroup']); $this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2->getName() . '}}-1', $userOrGroup);
if (!$isGroup) { if (!$isGroup) {
// New share falls back to the original name (no "-\d", because the name is not taken) // New share falls back to the original name (no "-\d", because the name is not taken)
$this->assertExternalShareEntry($shareData3, $openShares[1], 3, '{{TemporaryMountPointName#' . $shareData3['name'] . '}}', $shareData3['userOrGroup']); $this->assertExternalShareEntry($shareData3, $openShares[1], 3, '{{TemporaryMountPointName#' . $shareData3->getName() . '}}', $userOrGroup);
} else { } else {
$this->assertExternalShareEntry($shareData3, $openShares[1], 3, '{{TemporaryMountPointName#' . $shareData3['name'] . '}}-2', $shareData3['userOrGroup']); $this->assertExternalShareEntry($shareData3, $openShares[1], 3, '{{TemporaryMountPointName#' . $shareData3->getName() . '}}-2', $userOrGroup);
} }
$this->setupMounts(); $this->setupMounts();
$this->assertMount($shareData1['name']); $this->assertMount($shareData1->getName());
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
if (!$isGroup) { if (!$isGroup) {
$client = $this->createMock(IClient::class); $client = $this->createMock(IClient::class);
@ -324,45 +335,45 @@ class ManagerTest extends TestCase {
])); ]));
$client->expects($this->once()) $client->expects($this->once())
->method('post') ->method('post')
->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[1]['remote_id'] . '/decline'), $this->anything()) ->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[1]->getRemoteId() . '/decline'), $this->anything())
->willReturn($response); ->willReturn($response);
} }
// Decline the third share // Decline the third share
$this->assertTrue($this->manager->declineShare($openShares[1]['id'])); $this->assertTrue($this->manager->declineShare($openShares[1]));
$this->setupMounts(); $this->setupMounts();
$this->assertMount($shareData1['name']); $this->assertMount($shareData1->getName());
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
// Check remaining shares - Accepted // Check remaining shares - Accepted
$acceptedShares = self::invokePrivate($this->manager, 'getShares', [true]); $acceptedShares = $this->externalShareMapper->getShares($this->user, IShare::STATUS_ACCEPTED);
$this->assertCount(1, $acceptedShares); $this->assertCount(1, $acceptedShares);
$shareData1['accepted'] = true; $shareData1->setAccepted(true);
$this->assertExternalShareEntry($shareData1, $acceptedShares[0], 1, $shareData1['name'], $this->user); $this->assertExternalShareEntry($shareData1, $acceptedShares[0], 1, $shareData1->getName(), $this->user);
// Check remaining shares - Open // Check remaining shares - Open
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
if ($isGroup) { if ($isGroup) {
// declining a group share adds it back to pending instead of deleting it // declining a group share adds it back to pending instead of deleting it
$this->assertCount(2, $openShares); $this->assertCount(2, $openShares);
// this is a group share that is still open // this is a group share that is still open
$this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2['name'] . '}}-1', $shareData2['userOrGroup']); $this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2->getName() . '}}-1', $userOrGroup);
// this is the user share sub-entry matching the group share which got declined // this is the user share sub-entry matching the group share which got declined
$this->assertExternalShareEntry($shareData3, $openShares[1], 2, '{{TemporaryMountPointName#' . $shareData3['name'] . '}}-2', $this->user); $this->assertExternalShareEntry($shareData3, $openShares[1], 2, '{{TemporaryMountPointName#' . $shareData3->getName() . '}}-2', $this->user);
} else { } else {
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
$this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2['name'] . '}}-1', $this->user); $this->assertExternalShareEntry($shareData2, $openShares[0], 2, '{{TemporaryMountPointName#' . $shareData2->getName() . '}}-1', $this->user);
} }
$this->setupMounts(); $this->setupMounts();
$this->assertMount($shareData1['name']); $this->assertMount($shareData1->getName());
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
if ($isGroup) { if ($isGroup) {
// no http requests here // no http requests here
$this->manager->removeGroupShares('group1'); $this->manager->removeGroupShares($this->group1);
} else { } else {
$client1 = $this->createMock(IClient::class); $client1 = $this->createMock(IClient::class);
$client2 = $this->createMock(IClient::class); $client2 = $this->createMock(IClient::class);
@ -380,113 +391,119 @@ class ManagerTest extends TestCase {
$client1->expects($this->once()) $client1->expects($this->once())
->method('post') ->method('post')
->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[0]['remote_id'] . '/decline'), $this->anything()) ->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $openShares[0]->getRemoteId() . '/decline'), $this->anything())
->willReturn($response); ->willReturn($response);
$client2->expects($this->once()) $client2->expects($this->once())
->method('post') ->method('post')
->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $acceptedShares[0]['remote_id'] . '/decline'), $this->anything()) ->with($this->stringStartsWith('http://localhost/ocs/v2.php/cloud/shares/' . $acceptedShares[0]->getRemoteId() . '/decline'), $this->anything())
->willReturn($response); ->willReturn($response);
$this->manager->removeUserShares($this->user); $this->manager->removeUserShares($this->user);
} }
$this->assertEmpty(self::invokePrivate($this->manager, 'getShares', [null]), 'Asserting all shares for the user have been deleted'); $this->assertEmpty($this->externalShareMapper->getShares($this->user, null), 'Asserting all shares for the user have been deleted');
$this->clearMounts(); $this->clearMounts();
self::invokePrivate($this->manager, 'setupMounts'); self::invokePrivate($this->manager, 'setupMounts');
$this->assertNotMount($shareData1['name']); $this->assertNotMount($shareData1->getName());
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}');
$this->assertNotMount('{{TemporaryMountPointName#' . $shareData1['name'] . '}}-1'); $this->assertNotMount('{{TemporaryMountPointName#' . $shareData1->getName() . '}}-1');
} }
private function verifyAcceptedGroupShare(array $shareData): void { private function verifyAcceptedGroupShare(ExternalShare $share): void {
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(0, $openShares); $this->assertCount(0, $openShares);
$acceptedShares = self::invokePrivate($this->manager, 'getShares', [true]); $acceptedShares = $this->externalShareMapper->getShares($this->user, IShare::STATUS_ACCEPTED);
$this->assertCount(1, $acceptedShares); $this->assertCount(1, $acceptedShares);
$shareData['accepted'] = true; $share->setAccepted(IShare::STATUS_ACCEPTED);
$this->assertExternalShareEntry($shareData, $acceptedShares[0], 0, $shareData['name'], $this->user); $this->assertExternalShareEntry($share, $acceptedShares[0], 0, $share->getName(), $this->user);
$this->setupMounts(); $this->setupMounts();
$this->assertMount($shareData['name']); $this->assertMount($share->getName());
} }
private function verifyDeclinedGroupShare(array $shareData, ?string $tempMount = null): void { private function verifyDeclinedGroupShare(ExternalShare $share, ?string $tempMount = null): void {
if ($tempMount === null) { if ($tempMount === null) {
$tempMount = '{{TemporaryMountPointName#/SharedFolder}}'; $tempMount = '{{TemporaryMountPointName#/SharedFolder}}';
} }
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
$acceptedShares = self::invokePrivate($this->manager, 'getShares', [true]); $acceptedShares = $this->externalShareMapper->getShares($this->user, IShare::STATUS_ACCEPTED);
$this->assertCount(0, $acceptedShares); $this->assertCount(0, $acceptedShares);
$this->assertExternalShareEntry($shareData, $openShares[0], 0, $tempMount, $this->user); $share->setAccepted(IShare::STATUS_PENDING);
$this->assertExternalShareEntry($share, $openShares[0], 0, $tempMount, $this->user);
$this->setupMounts(); $this->setupMounts();
$this->assertNotMount($shareData['name']); $this->assertNotMount($share->getName());
$this->assertNotMount($tempMount); $this->assertNotMount($tempMount);
} }
private function createTestUserShare(string $userId = 'user1'): array { private function createTestUserShare(string $userId = 'user1'): ExternalShare {
$user = $this->createMock(IUser::class); $user = $this->createMock(IUser::class);
$user->expects($this->any())->method('getUID')->willReturn($userId); $user->expects($this->any())->method('getUID')->willReturn($userId);
$shareData = [ $share = new ExternalShare();
'remote' => 'http://localhost', $share->setId(Server::get(IGenerator::class)->nextId());
'token' => 'token1', $share->setRemote('http://localhost');
'password' => '', $share->setShareToken('token1');
'name' => '/SharedFolder', $share->setPassword('');
'owner' => 'foobar', $share->setName('/SharedFolder');
'shareType' => IShare::TYPE_USER, $share->setOwner('foobar');
'accepted' => false, $share->setShareType(IShare::TYPE_USER);
'userOrGroup' => $user, $share->setAccepted(IShare::STATUS_PENDING);
'remoteId' => '2342' $share->setRemoteId('2346');
];
$this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], $shareData)); $this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], [$share, $user]));
return $shareData; return $share;
} }
/**
* @return array{0: ExternalShare, 1: ExternalShare}
*/
private function createTestGroupShare(string $groupId = 'group1'): array { private function createTestGroupShare(string $groupId = 'group1'): array {
$shareData = [ $share = new ExternalShare();
'remote' => 'http://localhost', $share->setId(Server::get(IGenerator::class)->nextId());
'token' => 'token1', $share->setRemote('http://localhost');
'password' => '', $share->setShareToken('token1');
'name' => '/SharedFolder', $share->setPassword('');
'owner' => 'foobar', $share->setName('/SharedFolder');
'shareType' => IShare::TYPE_GROUP, $share->setOwner('foobar');
'accepted' => false, $share->setShareType(IShare::TYPE_GROUP);
'userOrGroup' => $groupId === 'group1' ? $this->group1 : $this->group2, $share->setAccepted(IShare::STATUS_PENDING);
'remoteId' => '2342' $share->setRemoteId('2342');
];
$this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], $shareData)); $this->assertSame(null, call_user_func_array([$this->manager, 'addShare'], [$share, $groupId === 'group1' ? $this->group1 : $this->group2]));
$allShares = self::invokePrivate($this->manager, 'getShares', [null]); $allShares = $this->externalShareMapper->getShares($this->user, null);
$groupShare = null;
foreach ($allShares as $share) { foreach ($allShares as $share) {
if ($share['user'] === $groupId) { if ($share->getUser() === $groupId) {
// this will hold the main group entry // this will hold the main group entry
$groupShare = $share; $groupShare = $share;
break; break;
} }
} }
return [$shareData, $groupShare]; $this->assertEquals($share->getId(), $groupShare->getId());
return [$share, $groupShare];
} }
public function testAcceptOriginalGroupShare(): void { public function testAcceptOriginalGroupShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// a second time // a second time
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
} }
public function testAcceptGroupShareAgainThroughGroupShare(): void { public function testAcceptGroupShareAgainThroughGroupShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// decline again, this keeps the sub-share // decline again, this keeps the sub-share
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
// this will return sub-entries // this will return sub-entries
@ -494,21 +511,21 @@ class ManagerTest extends TestCase {
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
// accept through group share // accept through group share
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData, '/SharedFolder'); $this->verifyAcceptedGroupShare($shareData, '/SharedFolder');
// accept a second time // accept a second time
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData, '/SharedFolder'); $this->verifyAcceptedGroupShare($shareData, '/SharedFolder');
} }
public function testAcceptGroupShareAgainThroughSubShare(): void { public function testAcceptGroupShareAgainThroughSubShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// decline again, this keeps the sub-share // decline again, this keeps the sub-share
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
// this will return sub-entries // this will return sub-entries
@ -516,152 +533,150 @@ class ManagerTest extends TestCase {
$this->assertCount(1, $openShares); $this->assertCount(1, $openShares);
// accept through sub-share // accept through sub-share
$this->assertTrue($this->manager->acceptShare($openShares[0]['id'])); $this->assertTrue($this->manager->acceptShare($openShares[0]));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// accept a second time // accept a second time
$this->assertTrue($this->manager->acceptShare($openShares[0]['id'])); $this->assertTrue($this->manager->acceptShare($openShares[0]));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
} }
public function testDeclineOriginalGroupShare(): void { public function testDeclineOriginalGroupShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData); $this->verifyDeclinedGroupShare($shareData);
// a second time // a second time
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData); $this->verifyDeclinedGroupShare($shareData);
} }
public function testDeclineGroupShareAgainThroughGroupShare(): void { public function testDeclineGroupShareAgainThroughGroupShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// decline again, this keeps the sub-share // decline again, this keeps the sub-share
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
// a second time // a second time
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
} }
public function testDeclineGroupShareAgainThroughSubshare(): void { public function testDeclineGroupShareAgainThroughSubshare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// this will return sub-entries // this will return sub-entries
$allShares = self::invokePrivate($this->manager, 'getShares', [null]); $allShares = $this->externalShareMapper->getShares($this->user, null);
$this->assertCount(1, $allShares); $this->assertCount(1, $allShares);
// decline again through sub-share // decline again through sub-share
$this->assertTrue($this->manager->declineShare($allShares[0]['id'])); $this->assertTrue($this->manager->declineShare($allShares[0]));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
// a second time // a second time
$this->assertTrue($this->manager->declineShare($allShares[0]['id'])); $this->assertTrue($this->manager->declineShare($allShares[0]));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
} }
public function testDeclineGroupShareAgainThroughMountPoint(): void { public function testDeclineGroupShareAgainThroughMountPoint(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// decline through mount point name // decline through mount point name
$this->assertTrue($this->manager->removeShare($this->user->getUID() . '/files/' . $shareData['name'])); $this->assertTrue($this->manager->removeShare($this->user->getUID() . '/files/' . $shareData->getName()));
$this->verifyDeclinedGroupShare($shareData, '/SharedFolder'); $this->verifyDeclinedGroupShare($shareData, '/SharedFolder');
// second time must fail as the mount point is gone // second time must fail as the mount point is gone
$this->assertFalse($this->manager->removeShare($this->user->getUID() . '/files/' . $shareData['name'])); $this->assertFalse($this->manager->removeShare($this->user->getUID() . '/files/' . $shareData->getName()));
} }
public function testDeclineThenAcceptGroupShareAgainThroughGroupShare(): void { public function testDeclineThenAcceptGroupShareAgainThroughGroupShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
// decline, this creates a declined sub-share // decline, this creates a declined sub-share
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData); $this->verifyDeclinedGroupShare($shareData);
// this will return sub-entries
$openShares = $this->manager->getOpenShares();
// accept through sub-share // accept through sub-share
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData, '/SharedFolder'); $this->verifyAcceptedGroupShare($shareData, '/SharedFolder');
// accept a second time // accept a second time
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$this->verifyAcceptedGroupShare($shareData, '/SharedFolder'); $this->verifyAcceptedGroupShare($shareData, '/SharedFolder');
} }
public function testDeclineThenAcceptGroupShareAgainThroughSubShare(): void { public function testDeclineThenAcceptGroupShareAgainThroughSubShare(): void {
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
// decline, this creates a declined sub-share // decline, this creates a declined sub-share
$this->assertTrue($this->manager->declineShare($groupShare['id'])); $this->assertTrue($this->manager->declineShare($groupShare));
$this->verifyDeclinedGroupShare($shareData); $this->verifyDeclinedGroupShare($shareData);
// this will return sub-entries // this will return sub-entries
$openShares = $this->manager->getOpenShares(); $openShares = $this->manager->getOpenShares();
// accept through sub-share // accept through sub-share
$this->assertTrue($this->manager->acceptShare($openShares[0]['id'])); $this->assertTrue($this->manager->acceptShare($openShares[0]));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
// accept a second time // accept a second time
$this->assertTrue($this->manager->acceptShare($openShares[0]['id'])); $this->assertTrue($this->manager->acceptShare($openShares[0]));
$this->verifyAcceptedGroupShare($shareData); $this->verifyAcceptedGroupShare($shareData);
} }
public function testDeleteUserShares(): void { public function testDeleteUserShares(): void {
// user 1 shares // user 1 shares
$shareData = $this->createTestUserShare($this->user->getUID()); $userShare = $this->createTestUserShare($this->user->getUID());
[$shareData, $groupShare] = $this->createTestGroupShare(); [$shareData, $groupShare] = $this->createTestGroupShare();
$shares = $this->manager->getOpenShares(); $shares = $this->manager->getOpenShares();
$this->assertCount(2, $shares); $this->assertCount(2, $shares);
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$user = $this->createMock(IUser::class); $user2 = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user2'); $user2->method('getUID')->willReturn('user2');
// user 2 shares // user 2 shares
$manager2 = $this->createManagerForUser($user); $manager2 = $this->createManagerForUser($user2);
$shareData2 = [ $share = new ExternalShare();
'remote' => 'http://localhost', $share->setId(Server::get(IGenerator::class)->nextId());
'token' => 'token1', $share->setRemote('http://localhost');
'password' => '', $share->setShareToken('token1');
'name' => '/SharedFolder', $share->setPassword('');
'owner' => 'foobar', $share->setName('/SharedFolder');
'shareType' => IShare::TYPE_USER, $share->setOwner('foobar');
'accepted' => false, $share->setShareType(IShare::TYPE_USER);
'userOrGroup' => $user, $share->setAccepted(IShare::STATUS_PENDING);
'remoteId' => '2342' $share->setRemoteId('2342');
];
$this->assertCount(1, $manager2->getOpenShares()); $this->assertCount(1, $manager2->getOpenShares());
$this->assertSame(null, call_user_func_array([$manager2, 'addShare'], $shareData2)); $this->assertSame(null, call_user_func_array([$manager2, 'addShare'], [$share, $user2]));
$this->assertCount(2, $manager2->getOpenShares()); $this->assertCount(2, $manager2->getOpenShares());
$this->manager->expects($this->once())->method('tryOCMEndPoint')->with('http://localhost', 'token1', '2342', 'decline')->willReturn([]); $userShare = $this->externalShareMapper->getById($userShare->getId()); // Simpler to compare
$this->manager->expects($this->once())->method('tryOCMEndPoint')->with($userShare, 'decline')->willReturn([]);
$this->manager->removeUserShares($this->user); $this->manager->removeUserShares($this->user);
$user1Shares = $this->manager->getOpenShares(); $user1Shares = $this->manager->getOpenShares();
// user share is gone, group is still there // user share is gone, group is still there
$this->assertCount(1, $user1Shares); $this->assertCount(1, $user1Shares);
$this->assertEquals($user1Shares[0]['share_type'], IShare::TYPE_GROUP); $this->assertEquals($user1Shares[0]->getShareType(), IShare::TYPE_GROUP);
// user 2 shares untouched // user 2 shares untouched
$user2Shares = $manager2->getOpenShares(); $user2Shares = $manager2->getOpenShares();
$this->assertCount(2, $user2Shares); $this->assertCount(2, $user2Shares);
$this->assertEquals($user2Shares[0]['share_type'], IShare::TYPE_GROUP); $this->assertEquals($user2Shares[0]->getShareType(), IShare::TYPE_GROUP);
$this->assertEquals($user2Shares[0]['user'], 'group1'); $this->assertEquals($user2Shares[0]->getUser(), 'group1');
$this->assertEquals($user2Shares[1]['share_type'], IShare::TYPE_USER); $this->assertEquals($user2Shares[1]->getShareType(), IShare::TYPE_USER);
$this->assertEquals($user2Shares[1]['user'], 'user2'); $this->assertEquals($user2Shares[1]->getUser(), 'user2');
} }
public function testDeleteGroupShares(): void { public function testDeleteGroupShares(): void {
@ -672,52 +687,52 @@ class ManagerTest extends TestCase {
$shares = $this->manager->getOpenShares(); $shares = $this->manager->getOpenShares();
$this->assertCount(2, $shares); $this->assertCount(2, $shares);
$this->assertTrue($this->manager->acceptShare($groupShare['id'])); $this->assertTrue($this->manager->acceptShare($groupShare));
$user = $this->createMock(IUser::class); $user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user2'); $user->method('getUID')->willReturn('user2');
// user 2 shares // user 2 shares
$manager2 = $this->createManagerForUser($user); $manager2 = $this->createManagerForUser($user);
$shareData2 = [
'remote' => 'http://localhost', $share = new ExternalShare();
'token' => 'token1', $share->setId(Server::get(IGenerator::class)->nextId());
'password' => '', $share->setRemote('http://localhost');
'name' => '/SharedFolder', $share->setShareToken('token1');
'owner' => 'foobar', $share->setPassword('');
'shareType' => IShare::TYPE_USER, $share->setName('/SharedFolder');
'accepted' => false, $share->setOwner('foobar');
'userOrGroup' => $user, $share->setShareType(IShare::TYPE_USER);
'remoteId' => '2342' $share->setAccepted(IShare::STATUS_PENDING);
]; $share->setRemoteId('2343');
$this->assertCount(1, $manager2->getOpenShares()); $this->assertCount(1, $manager2->getOpenShares());
$this->assertSame(null, call_user_func_array([$manager2, 'addShare'], $shareData2)); $this->assertSame(null, call_user_func_array([$manager2, 'addShare'], [$share, $user]));
$this->assertCount(2, $manager2->getOpenShares()); $this->assertCount(2, $manager2->getOpenShares());
$this->manager->expects($this->never())->method('tryOCMEndPoint'); $this->manager->expects($this->never())->method('tryOCMEndPoint');
$this->manager->removeGroupShares('group1'); $this->manager->removeGroupShares($this->group1);
$user1Shares = $this->manager->getOpenShares(); $user1Shares = $this->manager->getOpenShares();
// user share is gone, group is still there // user share is gone, group is still there
$this->assertCount(1, $user1Shares); $this->assertCount(1, $user1Shares);
$this->assertEquals($user1Shares[0]['share_type'], IShare::TYPE_USER); $this->assertEquals($user1Shares[0]->getShareType(), IShare::TYPE_USER);
// user 2 shares untouched // user 2 shares untouched
$user2Shares = $manager2->getOpenShares(); $user2Shares = $manager2->getOpenShares();
$this->assertCount(1, $user2Shares); $this->assertCount(1, $user2Shares);
$this->assertEquals($user2Shares[0]['share_type'], IShare::TYPE_USER); $this->assertEquals($user2Shares[0]->getShareType(), IShare::TYPE_USER);
$this->assertEquals($user2Shares[0]['user'], 'user2'); $this->assertEquals($user2Shares[0]->getUser(), 'user2');
} }
protected function assertExternalShareEntry(array $expected, array $actual, int $share, string $mountPoint, IUser|IGroup $targetEntity): void { protected function assertExternalShareEntry(ExternalShare $expected, ExternalShare $actual, int $share, string $mountPoint, IUser|IGroup $targetEntity): void {
$this->assertEquals($expected['remote'], $actual['remote'], 'Asserting remote of a share #' . $share); $this->assertEquals($expected->getRemote(), $actual->getRemote(), 'Asserting remote of a share #' . $share);
$this->assertEquals($expected['token'], $actual['share_token'], 'Asserting token of a share #' . $share); $this->assertEquals($expected->getShareToken(), $actual->getShareToken(), 'Asserting token of a share #' . $share);
$this->assertEquals($expected['name'], $actual['name'], 'Asserting name of a share #' . $share); $this->assertEquals($expected->getName(), $actual->getName(), 'Asserting name of a share #' . $share);
$this->assertEquals($expected['owner'], $actual['owner'], 'Asserting owner of a share #' . $share); $this->assertEquals($expected->getOwner(), $actual->getOwner(), 'Asserting owner of a share #' . $share);
$this->assertEquals($expected['accepted'], (int)$actual['accepted'], 'Asserting accept of a share #' . $share); $this->assertEquals($expected->getAccepted(), $actual->getAccepted(), 'Asserting accept of a share #' . $share);
$this->assertEquals($targetEntity instanceof IGroup ? $targetEntity->getGID() : $targetEntity->getUID(), $actual['user'], 'Asserting user of a share #' . $share); $this->assertEquals($targetEntity instanceof IGroup ? $targetEntity->getGID() : $targetEntity->getUID(), $actual->getUser(), 'Asserting user of a share #' . $share);
$this->assertEquals($mountPoint, $actual['mountpoint'], 'Asserting mountpoint of a share #' . $share); $this->assertEquals($mountPoint, $actual->getMountpoint(), 'Asserting mountpoint of a share #' . $share);
} }
private function assertMount(string $mountPoint): void { private function assertMount(string $mountPoint): void {

View file

@ -305,6 +305,18 @@ trait Sharing {
} }
} }
public function getFieldValueInResponse($field) {
$data = simplexml_load_string($this->response->getBody())->data[0];
if (count($data->element) > 0) {
foreach ($data as $element) {
return (string)$element->$field;
}
return false;
}
return $data->$field;
}
public function isFieldInResponse($field, $contentExpected) { public function isFieldInResponse($field, $contentExpected) {
$data = simplexml_load_string($this->response->getBody())->data[0]; $data = simplexml_load_string($this->response->getBody())->data[0];
if ((string)$field == 'expiration') { if ((string)$field == 'expiration') {

View file

@ -1305,7 +1305,6 @@
</file> </file>
<file src="apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php"> <file src="apps/federatedfilesharing/lib/OCM/CloudFederationProviderFiles.php">
<DeprecatedClass> <DeprecatedClass>
<code><![CDATA[\OC_Util::setupFS($shareWith)]]></code>
<code><![CDATA[\OC_Util::setupFS($user)]]></code> <code><![CDATA[\OC_Util::setupFS($user)]]></code>
</DeprecatedClass> </DeprecatedClass>
<DeprecatedMethod> <DeprecatedMethod>
@ -1315,7 +1314,6 @@
['uid' => &$shareWith] ['uid' => &$shareWith]
)]]></code> )]]></code>
<code><![CDATA[getAppValue]]></code> <code><![CDATA[getAppValue]]></code>
<code><![CDATA[lastInsertId]]></code>
<code><![CDATA[sendNotification]]></code> <code><![CDATA[sendNotification]]></code>
<code><![CDATA[sendNotification]]></code> <code><![CDATA[sendNotification]]></code>
</DeprecatedMethod> </DeprecatedMethod>
@ -1325,12 +1323,6 @@
<code><![CDATA[$id]]></code> <code><![CDATA[$id]]></code>
<code><![CDATA[$id]]></code> <code><![CDATA[$id]]></code>
</InvalidArgument> </InvalidArgument>
<InvalidReturnStatement>
<code><![CDATA[$shareId]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[string]]></code>
</InvalidReturnType>
</file> </file>
<file src="apps/federation/lib/DbHandler.php"> <file src="apps/federation/lib/DbHandler.php">
<LessSpecificReturnStatement> <LessSpecificReturnStatement>
@ -1626,15 +1618,6 @@
<code><![CDATA[new QueryException()]]></code> <code><![CDATA[new QueryException()]]></code>
</DeprecatedClass> </DeprecatedClass>
</file> </file>
<file src="apps/files_sharing/lib/Controller/RemoteController.php">
<InternalClass>
<code><![CDATA[new View('/' . \OC_User::getUser() . '/files/')]]></code>
</InternalClass>
<InternalMethod>
<code><![CDATA[getFileInfo]]></code>
<code><![CDATA[new View('/' . \OC_User::getUser() . '/files/')]]></code>
</InternalMethod>
</file>
<file src="apps/files_sharing/lib/Controller/SettingsController.php"> <file src="apps/files_sharing/lib/Controller/SettingsController.php">
<DeprecatedMethod> <DeprecatedMethod>
<code><![CDATA[deleteUserValue]]></code> <code><![CDATA[deleteUserValue]]></code>
@ -1682,25 +1665,10 @@
</DeprecatedMethod> </DeprecatedMethod>
</file> </file>
<file src="apps/files_sharing/lib/External/Manager.php"> <file src="apps/files_sharing/lib/External/Manager.php">
<DeprecatedClass>
<code><![CDATA[Files::buildNotExistingFileName($shareFolder, $share['name'])]]></code>
<code><![CDATA[Files::buildNotExistingFileName('/', $name)]]></code>
<code><![CDATA[Share::RESPONSE_FORMAT]]></code>
<code><![CDATA[\OC_Util::setupFS($user)]]></code>
</DeprecatedClass>
<DeprecatedMethod> <DeprecatedMethod>
<code><![CDATA[Files::buildNotExistingFileName($shareFolder, $share['name'])]]></code>
<code><![CDATA[Files::buildNotExistingFileName('/', $name)]]></code>
<code><![CDATA[insertIfNotExist]]></code>
<code><![CDATA[sendNotification]]></code> <code><![CDATA[sendNotification]]></code>
<code><![CDATA[sendNotification]]></code> <code><![CDATA[sendNotification]]></code>
</DeprecatedMethod> </DeprecatedMethod>
<LessSpecificReturnStatement>
<code><![CDATA[$mount]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[Mount]]></code>
</MoreSpecificReturnType>
</file> </file>
<file src="apps/files_sharing/lib/External/Scanner.php"> <file src="apps/files_sharing/lib/External/Scanner.php">
<DeprecatedInterface> <DeprecatedInterface>
@ -4420,14 +4388,8 @@
</file> </file>
<file src="lib/private/legacy/OC_Helper.php"> <file src="lib/private/legacy/OC_Helper.php">
<InternalMethod> <InternalMethod>
<code><![CDATA[file_exists]]></code>
<code><![CDATA[file_exists]]></code>
<code><![CDATA[getAbsolutePath]]></code> <code><![CDATA[getAbsolutePath]]></code>
</InternalMethod> </InternalMethod>
<InvalidArrayOffset>
<code><![CDATA[$matches[0][$last_match]]]></code>
<code><![CDATA[$matches[1][$last_match]]]></code>
</InvalidArrayOffset>
<UndefinedInterfaceMethod> <UndefinedInterfaceMethod>
<code><![CDATA[getQuota]]></code> <code><![CDATA[getQuota]]></code>
</UndefinedInterfaceMethod> </UndefinedInterfaceMethod>

View file

@ -48,8 +48,7 @@ class Event implements IEvent {
protected $messageRichParameters = []; protected $messageRichParameters = [];
/** @var string */ /** @var string */
protected $objectType = ''; protected $objectType = '';
/** @var int */ protected string|int $objectId = 0;
protected $objectId = 0;
/** @var string */ /** @var string */
protected $objectName = ''; protected $objectName = '';
/** @var string */ /** @var string */
@ -319,7 +318,7 @@ class Event implements IEvent {
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
public function setObject(string $objectType, int $objectId, string $objectName = ''): IEvent { public function setObject(string $objectType, string|int $objectId, string $objectName = ''): IEvent {
if (isset($objectType[255])) { if (isset($objectType[255])) {
throw new InvalidValueException('objectType'); throw new InvalidValueException('objectType');
} }
@ -340,9 +339,9 @@ class Event implements IEvent {
} }
/** /**
* @return int * @return int|string
*/ */
public function getObjectId(): int { public function getObjectId(): string|int {
return $this->objectId; return $this->objectId;
} }

View file

@ -1406,8 +1406,8 @@ class Manager implements IManager {
* *
* @throws ShareNotFound * @throws ShareNotFound
*/ */
public function getShareByToken($token) { public function getShareByToken($token): IShare {
// tokens cannot be valid local user names // tokens cannot be valid local usernames
if ($this->userManager->userExists($token)) { if ($this->userManager->userExists($token)) {
throw new ShareNotFound(); throw new ShareNotFound();
} }
@ -1417,8 +1417,7 @@ class Manager implements IManager {
$provider = $this->factory->getProviderForType(IShare::TYPE_LINK); $provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} }
} catch (ProviderException $e) { } catch (ProviderException|ShareNotFound) {
} catch (ShareNotFound $e) {
} }
@ -1427,8 +1426,7 @@ class Manager implements IManager {
try { try {
$provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE); $provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} catch (ProviderException $e) { } catch (ProviderException|ShareNotFound) {
} catch (ShareNotFound $e) {
} }
} }
@ -1437,8 +1435,7 @@ class Manager implements IManager {
try { try {
$provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL); $provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} catch (ProviderException $e) { } catch (ProviderException|ShareNotFound) {
} catch (ShareNotFound $e) {
} }
} }
@ -1446,8 +1443,7 @@ class Manager implements IManager {
try { try {
$provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE); $provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} catch (ProviderException $e) { } catch (ProviderException|ShareNotFound) {
} catch (ShareNotFound $e) {
} }
} }
@ -1455,8 +1451,7 @@ class Manager implements IManager {
try { try {
$provider = $this->factory->getProviderForType(IShare::TYPE_ROOM); $provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} catch (ProviderException $e) { } catch (ProviderException|ShareNotFound) {
} catch (ShareNotFound $e) {
} }
} }

View file

@ -211,14 +211,14 @@ interface IEvent {
* Set the object of the activity * Set the object of the activity
* *
* @param string $objectType * @param string $objectType
* @param int $objectId * @param string|int $objectId
* @param string $objectName * @param string $objectName
* @return IEvent * @return IEvent
* @throws InvalidValueException if the object is invalid * @throws InvalidValueException if the object is invalid
* @since 8.2.0 * @since 8.2.0
* @since 30.0.0 throws {@see InvalidValueException} instead of \InvalidArgumentException * @since 30.0.0 throws {@see InvalidValueException} instead of \InvalidArgumentException
*/ */
public function setObject(string $objectType, int $objectId, string $objectName = ''): self; public function setObject(string $objectType, string|int $objectId, string $objectName = ''): self;
/** /**
* Set the link of the activity * Set the link of the activity
@ -292,10 +292,10 @@ interface IEvent {
public function getObjectType(): string; public function getObjectType(): string;
/** /**
* @return int * @return string|int
* @since 8.2.0 * @since 8.2.0
*/ */
public function getObjectId(): int; public function getObjectId(): string|int;
/** /**
* @return string * @return string

View file

@ -2523,8 +2523,7 @@
"nullable": true "nullable": true
}, },
"id": { "id": {
"type": "integer", "type": "string"
"format": "int64"
}, },
"mimetype": { "mimetype": {
"type": "string", "type": "string",
@ -2545,8 +2544,7 @@
"type": "string" "type": "string"
}, },
"parent": { "parent": {
"type": "integer", "type": "string",
"format": "int64",
"nullable": true "nullable": true
}, },
"permissions": { "permissions": {
@ -25527,7 +25525,7 @@
"/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}": { "/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}": {
"post": { "post": {
"operationId": "files_sharing-remote-accept-share", "operationId": "files_sharing-remote-accept-share",
"summary": "Accept a remote share", "summary": "Accept a remote share.",
"tags": [ "tags": [
"files_sharing/remote" "files_sharing/remote"
], ],
@ -25546,8 +25544,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -25650,7 +25647,7 @@
}, },
"delete": { "delete": {
"operationId": "files_sharing-remote-decline-share", "operationId": "files_sharing-remote-decline-share",
"summary": "Decline a remote share", "summary": "Decline a remote share.",
"tags": [ "tags": [
"files_sharing/remote" "files_sharing/remote"
], ],
@ -25669,8 +25666,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -25794,8 +25790,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {
@ -25919,8 +25914,7 @@
"description": "ID of the share", "description": "ID of the share",
"required": true, "required": true,
"schema": { "schema": {
"type": "integer", "type": "string"
"format": "int64"
} }
}, },
{ {