nextcloud/apps/federatedfilesharing/lib/FederatedShareProvider.php
Daniel Kesselberg da7250f05d
fix(sharing): do not notify remote if owner and sharee are local users
If both the owner and the sharee are local users, no remote notification is required.
If either the owner or the sharee is not a local user, the change must be propagated.

Based on https://github.com/owncloud/core/pull/37534

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Co-authored-by: Victor Dubiniuk <victor.dubiniuk@gmail.com>
2026-03-03 23:08:52 +01:00

1072 lines
33 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\FederatedFileSharing;
use OC\Share20\Exception\InvalidShare;
use OC\Share20\Share;
use OCP\Constants;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\HintException;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUserManager;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IShare;
use OCP\Share\IShareProvider;
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
use Override;
use Psr\Log\LoggerInterface;
/**
* Class FederatedShareProvider
*
* @package OCA\FederatedFileSharing
*/
class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder {
public const SHARE_TYPE_REMOTE = 6;
/** @var string */
private $externalShareTable = 'share_external';
/** @var array list of supported share types */
private $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE];
/**
* DefaultShareProvider constructor.
*/
public function __construct(
private IDBConnection $dbConnection,
private AddressHandler $addressHandler,
private Notifications $notifications,
private TokenHandler $tokenHandler,
private IL10N $l,
private IRootFolder $rootFolder,
private IConfig $config,
private IUserManager $userManager,
private ICloudIdManager $cloudIdManager,
private \OCP\GlobalScale\IConfig $gsConfig,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private LoggerInterface $logger,
) {
}
#[Override]
public function identifier(): string {
return 'ocFederatedSharing';
}
/**
* Share a path
*
* @throws ShareNotFound
* @throws \Exception
*/
#[Override]
public function create(IShare $share): IShare {
$shareWith = $share->getSharedWith();
$itemSource = $share->getNodeId();
$itemType = $share->getNodeType();
$permissions = $share->getPermissions();
$sharedBy = $share->getSharedBy();
$shareType = $share->getShareType();
$expirationDate = $share->getExpirationDate();
if ($shareType === IShare::TYPE_REMOTE_GROUP
&& !$this->isOutgoingServer2serverGroupShareEnabled()
) {
$message = 'It is not allowed to send federated group shares from this server.';
$message_t = $this->l->t('It is not allowed to send federated group shares from this server.');
$this->logger->debug($message, ['app' => 'Federated File Sharing']);
throw new \Exception($message_t);
}
/*
* Check if file is not already shared with the remote user
*/
$alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_REMOTE, $share->getNode(), 1, 0);
$alreadySharedGroup = $this->getSharedWith($shareWith, IShare::TYPE_REMOTE_GROUP, $share->getNode(), 1, 0);
if (!empty($alreadyShared) || !empty($alreadySharedGroup)) {
$message = 'Sharing %1$s failed, because this item is already shared with %2$s';
$message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with the account %2$s', [$share->getNode()->getName(), $shareWith]);
$this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
throw new \Exception($message_t);
}
// don't allow federated shares if source and target server are the same
$cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
$currentServer = $this->addressHandler->generateRemoteURL();
$currentUser = $sharedBy;
if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) {
$message = 'Not allowed to create a federated share to the same account.';
$message_t = $this->l->t('Not allowed to create a federated share to the same account');
$this->logger->debug($message, ['app' => 'Federated File Sharing']);
throw new \Exception($message_t);
}
// Federated shares always have read permissions
if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) {
$message = 'Federated shares require read permissions';
$message_t = $this->l->t('Federated shares require read permissions');
$this->logger->debug($message, ['app' => 'Federated File Sharing']);
throw new \Exception($message_t);
}
$share->setSharedWith($cloudId->getId());
try {
$remoteShare = $this->getShareFromExternalShareTable($share);
} catch (ShareNotFound $e) {
$remoteShare = null;
}
if ($remoteShare) {
try {
$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
$shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time(), $shareType, $expirationDate);
$share->setId($shareId);
[$token, $remoteId] = $this->askOwnerToReShare($shareWith, $share, $shareId);
// remote share was create successfully if we get a valid token as return
$send = is_string($token) && $token !== '';
} catch (\Exception $e) {
// fall back to old re-share behavior if the remote server
// doesn't support flat re-shares (was introduced with Nextcloud 9.1)
$this->removeShareFromTable($share);
$shareId = $this->createFederatedShare($share);
}
if ($send) {
$this->updateSuccessfulReshare($shareId, $token);
$this->storeRemoteId($shareId, $remoteId);
} else {
$this->removeShareFromTable($share);
$message_t = $this->l->t('File is already shared with %s', [$shareWith]);
throw new \Exception($message_t);
}
} else {
$shareId = $this->createFederatedShare($share);
}
$data = $this->getRawShare($shareId);
return $this->createShareObject($data);
}
/**
* Create federated share and inform the recipient.
*
* @throws ShareNotFound
* @throws \Exception
*/
protected function createFederatedShare(IShare $share): string {
$token = $this->tokenHandler->generateToken();
$shareId = $this->addShareToDB(
$share->getNodeId(),
$share->getNodeType(),
$share->getSharedWith(),
$share->getSharedBy(),
$share->getShareOwner(),
$share->getPermissions(),
$token,
$share->getShareType(),
$share->getExpirationDate()
);
$failure = false;
try {
$sharedByFederatedId = $share->getSharedBy();
if ($this->userManager->userExists($sharedByFederatedId)) {
$cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL());
$sharedByFederatedId = $cloudId->getId();
}
$ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
$send = $this->notifications->sendRemoteShare(
$token,
$share->getSharedWith(),
$share->getNode()->getName(),
$shareId,
$share->getShareOwner(),
$ownerCloudId->getId(),
$share->getSharedBy(),
$sharedByFederatedId,
$share->getShareType()
);
if ($send === false) {
$failure = true;
}
} catch (\Exception $e) {
$this->logger->error('Failed to notify remote server of federated share, removing share.', [
'app' => 'federatedfilesharing',
'exception' => $e,
]);
$failure = true;
}
if ($failure) {
$this->removeShareFromTableById($shareId);
$message_t = $this->l->t('Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable or uses a self-signed certificate.',
[$share->getNode()->getName(), $share->getSharedWith()]);
throw new \Exception($message_t);
}
return $shareId;
}
/**
* @param string $shareWith
* @param IShare $share
* @param string $shareId internal share Id
* @return array
* @throws \Exception
*/
protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
$remoteShare = $this->getShareFromExternalShareTable($share);
$token = $remoteShare['share_token'];
$remoteId = $remoteShare['remote_id'];
$remote = $remoteShare['remote'];
[$token, $remoteId] = $this->notifications->requestReShare(
$token,
$remoteId,
$shareId,
$remote,
$shareWith,
$share->getPermissions(),
$share->getNode()->getName(),
$share->getShareType(),
);
return [$token, $remoteId];
}
/**
* get federated share from the share_external table but exclude mounted link shares
*
* @param IShare $share
* @return array
* @throws ShareNotFound
*/
protected function getShareFromExternalShareTable(IShare $share) {
$query = $this->dbConnection->getQueryBuilder();
$query->select('*')->from($this->externalShareTable)
->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
$qResult = $query->executeQuery();
$result = $qResult->fetchAllAssociative();
$qResult->closeCursor();
if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) {
return $result[0];
}
throw new ShareNotFound('share not found in share_external table');
}
/**
* add share to the database and return the ID
*
* @param int $itemSource
* @param string $itemType
* @param string $shareWith
* @param string $sharedBy
* @param string $uidOwner
* @param int $permissions
* @param string $token
* @param int $shareType
* @param \DateTime $expirationDate
*/
private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType, $expirationDate): string {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert('share')
->setValue('share_type', $qb->createNamedParameter($shareType))
->setValue('item_type', $qb->createNamedParameter($itemType))
->setValue('item_source', $qb->createNamedParameter($itemSource))
->setValue('file_source', $qb->createNamedParameter($itemSource))
->setValue('share_with', $qb->createNamedParameter($shareWith))
->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
->setValue('permissions', $qb->createNamedParameter($permissions))
->setValue('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE))
->setValue('token', $qb->createNamedParameter($token))
->setValue('stime', $qb->createNamedParameter(time()));
/*
* Added to fix https://github.com/owncloud/core/issues/22215
* Can be removed once we get rid of ajax/share.php
*/
$qb->setValue('file_target', $qb->createNamedParameter(''));
$qb->executeStatement();
return (string)$qb->getLastInsertId();
}
/**
* Update a share.
*/
public function update(IShare $share): IShare {
/*
* We allow updating the permissions of federated shares
*/
$qb = $this->dbConnection->getQueryBuilder();
$qb->update('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
->set('permissions', $qb->createNamedParameter($share->getPermissions()))
->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATETIME_MUTABLE))
->executeStatement();
/*
* If the share owner and share initiator are on the same instance,
* then we're done here as the share was just updated above.
*
* However, if the share owner/sharee is on a remote instance (and thus we're dealing with a federated share),
* then we are supposed to let the share owner/ sharee on the remote instance know.
*/
if ($this->shouldNotifyRemote($share)) {
$this->sendPermissionUpdate($share);
}
return $share;
}
/**
* Notify owner/sharee if they are not the same and ANY of them is a remote user.
*/
protected function shouldNotifyRemote(IShare $share): bool {
$ownerOrSharerIsRemoteUser = !$this->userManager->userExists($share->getShareOwner())
|| !$this->userManager->userExists($share->getSharedBy());
return $ownerOrSharerIsRemoteUser && $share->getShareOwner() !== $share->getSharedBy();
}
/**
* Send the updated permission to the owner/initiator, if they are not the same.
*
* @throws ShareNotFound
* @throws HintException
*/
protected function sendPermissionUpdate(IShare $share): void {
$remoteId = $this->getRemoteId($share);
// if the local user is the owner we send the permission change to the initiator
if ($this->userManager->userExists($share->getShareOwner())) {
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
} else { // ... if not we send the permission change to the owner
[, $remote] = $this->addressHandler->splitUserRemote($share->getShareOwner());
}
$this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions());
}
/**
* Update successful reShare with the correct token.
*/
protected function updateSuccessfulReShare(string $shareId, string $token): void {
$query = $this->dbConnection->getQueryBuilder();
$query->update('share')
->where($query->expr()->eq('id', $query->createNamedParameter($shareId)))
->set('token', $query->createNamedParameter($token))
->executeStatement();
}
/**
* Store remote ID in federated reShare table.
*/
public function storeRemoteId(string $shareId, string $remoteId): void {
$query = $this->dbConnection->getQueryBuilder();
$query->insert('federated_reshares')
->values(
[
'share_id' => $query->createNamedParameter($shareId),
'remote_id' => $query->createNamedParameter($remoteId),
]
);
$query->executeStatement();
}
/**
* Get share ID on remote server for federated re-shares.
*
* @throws ShareNotFound
*/
public function getRemoteId(IShare $share): string {
$query = $this->dbConnection->getQueryBuilder();
$query->select('remote_id')->from('federated_reshares')
->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId())));
$result = $query->executeQuery();
$data = $result->fetchAssociative();
$result->closeCursor();
if (!is_array($data) || !isset($data['remote_id'])) {
throw new ShareNotFound();
}
return (string)$data['remote_id'];
}
/**
* @inheritdoc
*/
public function move(IShare $share, $recipient) {
/*
* This function does nothing yet as it is just for outgoing
* federated shares.
*/
return $share;
}
public function getChildren(IShare $parent): array {
$children = [];
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)))
->orderBy('id');
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
$children[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $children;
}
/**
* Delete a share (owner unShares the file)
*
* @param IShare $share
* @throws ShareNotFound
* @throws HintException
*/
public function delete(IShare $share) {
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedWith());
// if the local user is the owner we can send the unShare request directly...
if ($this->userManager->userExists($share->getShareOwner())) {
$this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken());
$this->revokeShare($share, true);
} else { // ... if not we need to correct ID for the unShare request
$remoteId = $this->getRemoteId($share);
$this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken());
$this->revokeShare($share, false);
}
// only remove the share when all messages are send to not lose information
// about the share to early
$this->removeShareFromTable($share);
}
/**
* in case of a re-share we need to send the other use (initiator or owner)
* a message that the file was unshared
*
* @param IShare $share
* @param bool $isOwner the user can either be the owner or the user who re-sahred it
* @throws ShareNotFound
* @throws HintException
*/
protected function revokeShare($share, $isOwner) {
// also send a unShare request to the initiator, if this is a different user than the owner
if ($this->shouldNotifyRemote($share)) {
if ($isOwner) {
[, $remote] = $this->addressHandler->splitUserRemote($share->getSharedBy());
} else {
[, $remote] = $this->addressHandler->splitUserRemote($share->getShareOwner());
}
$remoteId = $this->getRemoteId($share);
$this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
}
}
/**
* remove share from table
*
* @param IShare $share
*/
public function removeShareFromTable(IShare $share) {
$this->removeShareFromTableById($share->getId());
}
/**
* Remove share from table.
*/
private function removeShareFromTableById(string $shareId): void {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)))
->andWhere($qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)));
$qb->executeStatement();
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete('federated_reshares')
->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId)));
$qb->executeStatement();
}
/**
* @inheritdoc
*/
public function deleteFromSelf(IShare $share, $recipient) {
// nothing to do here. Technically deleteFromSelf in the context of federated
// shares is a umount of an external storage. This is handled here
// apps/files_sharing/lib/external/manager.php
// TODO move this code over to this app
}
public function restore(IShare $share, string $recipient): IShare {
throw new GenericShareException('not implemented');
}
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) {
if (!$shallow) {
throw new \Exception('non-shallow getSharesInFolder is no longer supported');
}
return $this->getSharesInFolderInternal($userId, $node, $reshares);
}
public function getAllSharesInFolder(Folder $node): array {
return $this->getSharesInFolderInternal(null, $node, null);
}
/**
* @return array<int, list<IShare>>
*/
private function getSharesInFolderInternal(?string $userId, Folder $node, ?bool $reshares): array {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share', 's')
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
->andWhere(
$qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE))
);
if ($userId !== null) {
/**
* Reshares for this user are shares where they are the owner.
*/
if ($reshares !== true) {
$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
} else {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
)
);
}
}
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[$data['fileid']][] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
/**
* @inheritdoc
*/
public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share');
$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType)));
/**
* Reshares for this user are shares where they are the owner.
*/
if ($reshares === false) {
//Special case for old shares created via the web UI
$or1 = $qb->expr()->andX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->isNull('uid_initiator')
);
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
$or1
)
);
} else {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
)
);
}
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
if ($limit !== -1) {
$qb->setMaxResults($limit);
}
$qb->setFirstResult($offset);
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
/**
* @inheritdoc
*/
public function getShareById($id, $recipientId = null) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
$data = $cursor->fetchAssociative();
$cursor->closeCursor();
if ($data === false) {
throw new ShareNotFound('Can not find share with ID: ' . $id);
}
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound();
}
return $share;
}
/**
* Get shares for a given path
*
* @param Node $path
* @return IShare[]
*/
public function getSharesByPath(Node $path) {
$qb = $this->dbConnection->getQueryBuilder();
// get federated user shares
$cursor = $qb->select('*')
->from('share')
->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)))
->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
/**
* @inheritdoc
*/
public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
/** @var IShare[] $shares */
$shares = [];
//Get shares directly with this user
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share');
// Order by id
$qb->orderBy('id');
// Set limit and offset
if ($limit !== -1) {
$qb->setMaxResults($limit);
}
$qb->setFirstResult($offset);
$qb->where($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)));
$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
// Filter by node if provided
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
#[Override]
public function getShareByToken(string $token): IShare {
$qb = $this->dbConnection->getQueryBuilder();
$cursor = $qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
->executeQuery();
$data = $cursor->fetchAssociative();
if ($data === false) {
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
return $share;
}
/**
* get database row of a give share
*
* @param $id
* @return array
* @throws ShareNotFound
*/
private function getRawShare($id) {
// Now fetch the inserted share and create a complete share object
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
$cursor = $qb->executeQuery();
$data = $cursor->fetchAssociative();
$cursor->closeCursor();
if ($data === false) {
throw new ShareNotFound;
}
return $data;
}
/**
* Create a share object from an database row
*
* @param array $data
* @return IShare
* @throws InvalidShare
* @throws ShareNotFound
*/
private function createShareObject($data): IShare {
$share = new Share($this->rootFolder, $this->userManager);
$share->setId((string)$data['id'])
->setShareType((int)$data['share_type'])
->setPermissions((int)$data['permissions'])
->setTarget($data['file_target'])
->setMailSend((bool)$data['mail_send'])
->setStatus((int)$data['accepted'])
->setToken($data['token']);
$shareTime = new \DateTime();
$shareTime->setTimestamp((int)$data['stime']);
$share->setShareTime($shareTime);
$share->setSharedWith($data['share_with']);
if ($data['uid_initiator'] !== null) {
$share->setShareOwner($data['uid_owner']);
$share->setSharedBy($data['uid_initiator']);
} else {
//OLD SHARE
$share->setSharedBy($data['uid_owner']);
$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
$owner = $path->getOwner();
$share->setShareOwner($owner->getUID());
}
$share->setNodeId((int)$data['file_source']);
$share->setNodeType($data['item_type']);
$share->setProviderId($this->identifier());
if ($data['expiration'] !== null) {
$expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
$share->setExpirationDate($expiration);
}
return $share;
}
/**
* Get the node with file $id for $user
*
* @param string $userId
* @param int $id
* @return Node
* @throws InvalidShare
*/
private function getNode($userId, $id) {
try {
$userFolder = $this->rootFolder->getUserFolder($userId);
} catch (NotFoundException $e) {
throw new InvalidShare();
}
$node = $userFolder->getFirstNodeById($id);
if (!$node) {
throw new InvalidShare();
}
return $node;
}
/**
* A user is deleted from the system
* So clean up the relevant shares.
*
* @param string $uid
* @param int $shareType
*/
public function userDeleted($uid, $shareType) {
//TODO: probably a good idea to send unshare info to remote servers
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE)))
->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
->executeStatement();
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete('share_external')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)))
->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($uid)))
->executeStatement();
}
public function groupDeleted($gid) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('id')
->from('share_external')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)))
// This is not a typo, the group ID is really stored in the 'user' column
->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($gid)));
$cursor = $qb->executeQuery();
$parentShareIds = $cursor->fetchFirstColumn();
$cursor->closeCursor();
if ($parentShareIds === []) {
return;
}
$qb = $this->dbConnection->getQueryBuilder();
$parentShareIdsParam = $qb->createNamedParameter($parentShareIds, IQueryBuilder::PARAM_INT_ARRAY);
$qb->delete('share_external')
->where($qb->expr()->in('id', $parentShareIdsParam))
->orWhere($qb->expr()->in('parent', $parentShareIdsParam))
->executeStatement();
}
public function userDeletedFromGroup($uid, $gid) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('id')
->from('share_external')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)))
// This is not a typo, the group ID is really stored in the 'user' column
->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($gid)));
$cursor = $qb->executeQuery();
$parentShareIds = $cursor->fetchFirstColumn();
$cursor->closeCursor();
if ($parentShareIds === []) {
return;
}
$qb = $this->dbConnection->getQueryBuilder();
$parentShareIdsParam = $qb->createNamedParameter($parentShareIds, IQueryBuilder::PARAM_INT_ARRAY);
$qb->delete('share_external')
->where($qb->expr()->in('parent', $parentShareIdsParam))
->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($uid)))
->executeStatement();
}
/**
* Check if users from other Nextcloud instances are allowed to mount public links share by this instance
*/
public function isOutgoingServer2serverShareEnabled(): bool {
if ($this->gsConfig->onlyInternalFederation()) {
return false;
}
$result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
return $result === 'yes';
}
/**
* Check if users are allowed to mount public links from other Nextclouds
*/
public function isIncomingServer2serverShareEnabled(): bool {
if ($this->gsConfig->onlyInternalFederation()) {
return false;
}
$result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes');
return $result === 'yes';
}
/**
* Check if users from other Nextcloud instances are allowed to send federated group shares
*/
public function isOutgoingServer2serverGroupShareEnabled(): bool {
if ($this->gsConfig->onlyInternalFederation()) {
return false;
}
$result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no');
return $result === 'yes';
}
/**
* Check if users are allowed to receive federated group shares
*/
public function isIncomingServer2serverGroupShareEnabled(): bool {
if ($this->gsConfig->onlyInternalFederation()) {
return false;
}
$result = $this->config->getAppValue('files_sharing', 'incoming_server2server_group_share_enabled', 'no');
return $result === 'yes';
}
/**
* Check if federated group sharing is supported, therefore the OCM API need to be enabled
*/
public function isFederatedGroupSharingSupported(): bool {
return $this->cloudFederationProviderManager->isReady();
}
/**
* Check if querying sharees on the lookup server is enabled
*/
public function isLookupServerQueriesEnabled(): bool {
// in a global scale setup we should always query the lookup server
if ($this->gsConfig->isGlobalScaleEnabled()) {
return true;
}
$result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no') === 'yes';
// TODO: Reenable if lookup server is used again
// return $result;
return false;
}
/**
* Check if it is allowed to publish user specific data to the lookup server
*/
public function isLookupServerUploadEnabled(): bool {
// in a global scale setup the admin is responsible to keep the lookup server up-to-date
if ($this->gsConfig->isGlobalScaleEnabled()) {
return false;
}
$result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'no') === 'yes';
// TODO: Reenable if lookup server is used again
// return $result;
return false;
}
/**
* Check if auto accepting incoming shares from trusted servers is enabled
*/
public function isFederatedTrustedShareAutoAccept(): bool {
$result = $this->config->getAppValue('files_sharing', 'federatedTrustedShareAutoAccept', 'yes');
return $result === 'yes';
}
public function getAccessList($nodes, $currentAccess) {
$ids = [];
foreach ($nodes as $node) {
$ids[] = $node->getId();
}
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('share_with', 'token', 'file_source')
->from('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_REMOTE)))
->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)));
$cursor = $qb->executeQuery();
if ($currentAccess === false) {
$remote = $cursor->fetchAssociative() !== false;
$cursor->closeCursor();
return ['remote' => $remote];
}
$remote = [];
while ($row = $cursor->fetchAssociative()) {
$remote[$row['share_with']] = [
'node_id' => $row['file_source'],
'token' => $row['token'],
];
}
$cursor->closeCursor();
return ['remote' => $remote];
}
public function getAllShares(): iterable {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE], IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
continue;
} catch (ShareNotFound $e) {
continue;
}
yield $share;
}
$cursor->closeCursor();
}
}