diff options
Diffstat (limited to 'lib/private/Share20/DefaultShareProvider.php')
-rw-r--r-- | lib/private/Share20/DefaultShareProvider.php | 1422 |
1 files changed, 1148 insertions, 274 deletions
diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index f6171f87992..5300e6e1baa 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -1,80 +1,64 @@ <?php + /** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Share20; -use OCP\Files\File; -use OCP\Share\IShareProvider; +use OC\Files\Cache\Cache; +use OC\Share20\Exception\BackendError; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; -use OCP\Share\Exceptions\ShareNotFound; -use OC\Share20\Exception\BackendError; +use OC\User\LazyUser; +use OCA\Files_Sharing\AppInfo\Application; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Files\NotFoundException; -use OCP\IGroup; -use OCP\IGroupManager; -use OCP\IUserManager; +use OCP\Defaults; +use OCP\Files\Folder; use OCP\Files\IRootFolder; -use OCP\IDBConnection; use OCP\Files\Node; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IAttributes; +use OCP\Share\IManager; +use OCP\Share\IShare; +use OCP\Share\IShareProviderSupportsAccept; +use OCP\Share\IShareProviderSupportsAllSharesInFolder; +use OCP\Share\IShareProviderWithNotification; +use Psr\Log\LoggerInterface; +use function str_starts_with; /** * Class DefaultShareProvider * * @package OC\Share20 */ -class DefaultShareProvider implements IShareProvider { - - // Special share type for user modified group shares - const SHARE_TYPE_USERGROUP = 2; - - /** @var IDBConnection */ - private $dbConn; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** - * DefaultShareProvider constructor. - * - * @param IDBConnection $connection - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param IRootFolder $rootFolder - */ +class DefaultShareProvider implements IShareProviderWithNotification, IShareProviderSupportsAccept, IShareProviderSupportsAllSharesInFolder { public function __construct( - IDBConnection $connection, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder) { - $this->dbConn = $connection; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; + private IDBConnection $dbConn, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IRootFolder $rootFolder, + private IMailer $mailer, + private Defaults $defaults, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + private LoggerInterface $logger, + private IManager $shareManager, + private IConfig $config, + ) { } /** @@ -100,29 +84,52 @@ class DefaultShareProvider implements IShareProvider { $qb->insert('share'); $qb->setValue('share_type', $qb->createNamedParameter($share->getShareType())); - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + $expirationDate = $share->getExpirationDate(); + if ($expirationDate !== null) { + $expirationDate = clone $expirationDate; + $expirationDate->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } + + if ($share->getShareType() === IShare::TYPE_USER) { //Set the UID of the user we share with $qb->setValue('share_with', $qb->createNamedParameter($share->getSharedWith())); - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + $qb->setValue('accepted', $qb->createNamedParameter(IShare::STATUS_PENDING)); + + //If an expiration date is set store it + if ($expirationDate !== null) { + $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); + } + + $qb->setValue('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL)); + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { //Set the GID of the group we share with $qb->setValue('share_with', $qb->createNamedParameter($share->getSharedWith())); - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { + + //If an expiration date is set store it + if ($expirationDate !== null) { + $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); + } + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + //set label for public link + $qb->setValue('label', $qb->createNamedParameter($share->getLabel())); //Set the token of the share $qb->setValue('token', $qb->createNamedParameter($share->getToken())); //If a password is set store it if ($share->getPassword() !== null) { - $qb->setValue('share_with', $qb->createNamedParameter($share->getPassword())); + $qb->setValue('password', $qb->createNamedParameter($share->getPassword())); } + $qb->setValue('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)); + //If an expiration date is set store it - if ($share->getExpirationDate() !== null) { - $qb->setValue('expiration', $qb->createNamedParameter($share->getExpirationDate(), 'datetime')); + if ($expirationDate !== null) { + $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); } - if (method_exists($share, 'getParent')) { - $qb->setValue('parent', $qb->createNamedParameter($share->getParent())); - } + $qb->setValue('parent', $qb->createNamedParameter($share->getParent())); + + $qb->setValue('hide_download', $qb->createNamedParameter($share->getHideDownload() ? 1 : 0, IQueryBuilder::PARAM_INT)); } else { throw new \Exception('invalid share type!'); } @@ -142,6 +149,12 @@ class DefaultShareProvider implements IShareProvider { // set the permissions $qb->setValue('permissions', $qb->createNamedParameter($share->getPermissions())); + // set share attributes + $shareAttributes = $this->formatShareAttributes( + $share->getAttributes() + ); + $qb->setValue('attributes', $qb->createNamedParameter($shareAttributes)); + // Set who created this share $qb->setValue('uid_initiator', $qb->createNamedParameter($share->getSharedBy())); @@ -151,30 +164,27 @@ class DefaultShareProvider implements IShareProvider { // Set the file target $qb->setValue('file_target', $qb->createNamedParameter($share->getTarget())); + if ($share->getNote() !== '') { + $qb->setValue('note', $qb->createNamedParameter($share->getNote())); + } + // Set the time this share was created - $qb->setValue('stime', $qb->createNamedParameter(time())); + $shareTime = $this->timeFactory->now(); + $qb->setValue('stime', $qb->createNamedParameter($shareTime->getTimestamp())); // insert the data and fetch the id of the share - $this->dbConn->beginTransaction(); - $qb->execute(); - $id = $this->dbConn->lastInsertId('*PREFIX*share'); - $this->dbConn->commit(); + $qb->executeStatement(); - // Now fetch the inserted share and create a complete share object - $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + // Update mandatory data + $id = $qb->getLastInsertId(); + $share->setId((string)$id); + $share->setProviderId($this->identifier()); - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $cursor->closeCursor(); + $share->setShareTime(\DateTime::createFromImmutable($shareTime)); - if ($data === false) { - throw new ShareNotFound(); - } + $mailSendValue = $share->getMailSend(); + $share->setMailSend(($mailSendValue === null) ? true : $mailSendValue); - $share = $this->createShare($data); return $share; } @@ -183,9 +193,22 @@ class DefaultShareProvider implements IShareProvider { * * @param \OCP\Share\IShare $share * @return \OCP\Share\IShare The share object + * @throws ShareNotFound + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotFoundException */ public function update(\OCP\Share\IShare $share) { - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + $originalShare = $this->getShareById($share->getId()); + + $shareAttributes = $this->formatShareAttributes($share->getAttributes()); + + $expirationDate = $share->getExpirationDate(); + if ($expirationDate !== null) { + $expirationDate = clone $expirationDate; + $expirationDate->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } + + if ($share->getShareType() === IShare::TYPE_USER) { /* * We allow updating the recipient on user shares. */ @@ -196,19 +219,27 @@ class DefaultShareProvider implements IShareProvider { ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) - ->execute(); - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->set('note', $qb->createNamedParameter($share->getNote())) + ->set('accepted', $qb->createNamedParameter($share->getStatus())) + ->set('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL)) + ->executeStatement(); + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) - ->execute(); + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->set('note', $qb->createNamedParameter($share->getNote())) + ->executeStatement(); /* * Update all user defined group shares @@ -216,11 +247,14 @@ class DefaultShareProvider implements IShareProvider { $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) - ->execute(); + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->set('note', $qb->createNamedParameter($share->getNote())) + ->executeStatement(); /* * Now update the permissions for all children that have not set it to 0 @@ -230,34 +264,99 @@ class DefaultShareProvider implements IShareProvider { ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0))) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) - ->execute(); - - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { + ->set('attributes', $qb->createNamedParameter($shareAttributes)) + ->executeStatement(); + } elseif ($share->getShareType() === IShare::TYPE_LINK) { $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->set('share_with', $qb->createNamedParameter($share->getPassword())) + ->set('password', $qb->createNamedParameter($share->getPassword())) + ->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('token', $qb->createNamedParameter($share->getToken())) - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) - ->execute(); + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) + ->set('note', $qb->createNamedParameter($share->getNote())) + ->set('label', $qb->createNamedParameter($share->getLabel())) + ->set('hide_download', $qb->createNamedParameter($share->getHideDownload() ? 1 : 0, IQueryBuilder::PARAM_INT)) + ->executeStatement(); } + if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { + $this->propagateNote($share); + } + + return $share; } /** - * Get all children of this share - * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in + * Accept a share. * - * @param \OCP\Share\IShare $parent - * @return \OCP\Share\IShare[] + * @param IShare $share + * @param string $recipient + * @return IShare The share object + * @since 9.0.0 */ - public function getChildren(\OCP\Share\IShare $parent) { + public function acceptShare(IShare $share, string $recipient): IShare { + if ($share->getShareType() === IShare::TYPE_GROUP) { + $group = $this->groupManager->get($share->getSharedWith()); + $user = $this->userManager->get($recipient); + + if (is_null($group)) { + throw new ProviderException('Group "' . $share->getSharedWith() . '" does not exist'); + } + + if (!$group->inGroup($user)) { + throw new ProviderException('Recipient not in receiving group'); + } + + // Try to fetch user specific share + $qb = $this->dbConn->getQueryBuilder(); + $stmt = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) + ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->executeQuery(); + + $data = $stmt->fetch(); + $stmt->closeCursor(); + + /* + * Check if there already is a user specific group share. + * If there is update it (if required). + */ + if ($data === false) { + $id = $this->createUserSpecificGroupShare($share, $recipient); + } else { + $id = $data['id']; + } + } elseif ($share->getShareType() === IShare::TYPE_USER) { + if ($share->getSharedWith() !== $recipient) { + throw new ProviderException('Recipient does not match'); + } + + $id = $share->getId(); + } else { + throw new ProviderException('Invalid shareType'); + } + + $qb = $this->dbConn->getQueryBuilder(); + $qb->update('share') + ->set('accepted', $qb->createNamedParameter(IShare::STATUS_ACCEPTED)) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->executeStatement(); + + return $share; + } + + public function getChildren(IShare $parent): array { $children = []; $qb = $this->dbConn->getQueryBuilder(); @@ -268,20 +367,17 @@ class DefaultShareProvider implements IShareProvider { $qb->expr()->in( 'share_type', $qb->createNamedParameter([ - \OCP\Share::SHARE_TYPE_USER, - \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK, + IShare::TYPE_USER, + IShare::TYPE_GROUP, + IShare::TYPE_LINK, ], IQueryBuilder::PARAM_INT_ARRAY) ) ) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) ->orderBy('id'); - $cursor = $qb->execute(); - while($data = $cursor->fetch()) { + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { $children[] = $this->createShare($data); } $cursor->closeCursor(); @@ -303,44 +399,45 @@ class DefaultShareProvider implements IShareProvider { * If the share is a group share delete all possible * user defined groups shares. */ - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + if ($share->getShareType() === IShare::TYPE_GROUP) { $qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))); } - $qb->execute(); + $qb->executeStatement(); } /** * Unshare a share from the recipient. If this is a group share * this means we need a special entry in the share db. * - * @param \OCP\Share\IShare $share + * @param IShare $share * @param string $recipient UserId of recipient * @throws BackendError * @throws ProviderException */ - public function deleteFromSelf(\OCP\Share\IShare $share, $recipient) { - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { - + public function deleteFromSelf(IShare $share, $recipient) { + if ($share->getShareType() === IShare::TYPE_GROUP) { $group = $this->groupManager->get($share->getSharedWith()); $user = $this->userManager->get($recipient); + if (is_null($group)) { + throw new ProviderException('Group "' . $share->getSharedWith() . '" does not exist'); + } + if (!$group->inGroup($user)) { - throw new ProviderException('Recipient not in receiving group'); + // nothing left to do + return; } // Try to fetch user specific share $qb = $this->dbConn->getQueryBuilder(); $stmt = $qb->select('*') ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERGROUP))) + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) - ->execute(); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->executeQuery(); $data = $stmt->fetch(); @@ -349,38 +446,22 @@ class DefaultShareProvider implements IShareProvider { * If there is update it (if required). */ if ($data === false) { - $qb = $this->dbConn->getQueryBuilder(); - - $type = $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder'; - - //Insert new share - $qb->insert('share') - ->values([ - 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERGROUP), - 'share_with' => $qb->createNamedParameter($recipient), - 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), - 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), - 'parent' => $qb->createNamedParameter($share->getId()), - 'item_type' => $qb->createNamedParameter($type), - 'item_source' => $qb->createNamedParameter($share->getNode()->getId()), - 'file_source' => $qb->createNamedParameter($share->getNode()->getId()), - 'file_target' => $qb->createNamedParameter($share->getTarget()), - 'permissions' => $qb->createNamedParameter(0), - 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), - ])->execute(); - - } else if ($data['permissions'] !== 0) { + $id = $this->createUserSpecificGroupShare($share, $recipient); + $permissions = $share->getPermissions(); + } else { + $permissions = $data['permissions']; + $id = $data['id']; + } + if ($permissions !== 0) { // Update existing usergroup share $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->set('permissions', $qb->createNamedParameter(0)) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) - ->execute(); + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->executeStatement(); } - - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { - + } elseif ($share->getShareType() === IShare::TYPE_USER) { if ($share->getSharedWith() !== $recipient) { throw new ProviderException('Recipient does not match'); } @@ -392,67 +473,214 @@ class DefaultShareProvider implements IShareProvider { } } + protected function createUserSpecificGroupShare(IShare $share, string $recipient): int { + $type = $share->getNodeType(); + + $shareFolder = $this->config->getSystemValue('share_folder', '/'); + $allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true); + if ($allowCustomShareFolder) { + $shareFolder = $this->config->getUserValue($recipient, Application::APP_ID, 'share_folder', $shareFolder); + } + + $target = $shareFolder . '/' . $share->getNode()->getName(); + $target = \OC\Files\Filesystem::normalizePath($target); + + $qb = $this->dbConn->getQueryBuilder(); + $qb->insert('share') + ->values([ + 'share_type' => $qb->createNamedParameter(IShare::TYPE_USERGROUP), + 'share_with' => $qb->createNamedParameter($recipient), + 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), + 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), + 'parent' => $qb->createNamedParameter($share->getId()), + 'item_type' => $qb->createNamedParameter($type), + 'item_source' => $qb->createNamedParameter($share->getNodeId()), + 'file_source' => $qb->createNamedParameter($share->getNodeId()), + 'file_target' => $qb->createNamedParameter($target), + 'permissions' => $qb->createNamedParameter($share->getPermissions()), + 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), + ])->executeStatement(); + + return $qb->getLastInsertId(); + } + + /** + * @inheritdoc + * + * For now this only works for group shares + * If this gets implemented for normal shares we have to extend it + */ + public function restore(IShare $share, string $recipient): IShare { + $qb = $this->dbConn->getQueryBuilder(); + $qb->select('permissions') + ->from('share') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($share->getId())) + ); + $cursor = $qb->executeQuery(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + $originalPermission = $data['permissions']; + + $qb = $this->dbConn->getQueryBuilder(); + $qb->update('share') + ->set('permissions', $qb->createNamedParameter($originalPermission)) + ->where( + $qb->expr()->eq('parent', $qb->createNamedParameter($share->getParent())) + )->andWhere( + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)) + )->andWhere( + $qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)) + ); + + $qb->executeStatement(); + + return $this->getShareById($share->getId(), $recipient); + } + /** * @inheritdoc */ public function move(\OCP\Share\IShare $share, $recipient) { - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + if ($share->getShareType() === IShare::TYPE_USER) { // Just update the target $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->set('file_target', $qb->createNamedParameter($share->getTarget())) ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->execute(); - - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { - + ->executeStatement(); + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { // Check if there is a usergroup share $qb = $this->dbConn->getQueryBuilder(); $stmt = $qb->select('id') ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERGROUP))) + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) ->setMaxResults(1) - ->execute(); + ->executeQuery(); $data = $stmt->fetch(); $stmt->closeCursor(); + $shareAttributes = $this->formatShareAttributes( + $share->getAttributes() + ); + if ($data === false) { // No usergroup share yet. Create one. $qb = $this->dbConn->getQueryBuilder(); $qb->insert('share') ->values([ - 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERGROUP), + 'share_type' => $qb->createNamedParameter(IShare::TYPE_USERGROUP), 'share_with' => $qb->createNamedParameter($recipient), 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 'parent' => $qb->createNamedParameter($share->getId()), - 'item_type' => $qb->createNamedParameter($share->getNode() instanceof File ? 'file' : 'folder'), - 'item_source' => $qb->createNamedParameter($share->getNode()->getId()), - 'file_source' => $qb->createNamedParameter($share->getNode()->getId()), + 'item_type' => $qb->createNamedParameter($share->getNodeType()), + 'item_source' => $qb->createNamedParameter($share->getNodeId()), + 'file_source' => $qb->createNamedParameter($share->getNodeId()), 'file_target' => $qb->createNamedParameter($share->getTarget()), 'permissions' => $qb->createNamedParameter($share->getPermissions()), + 'attributes' => $qb->createNamedParameter($shareAttributes), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), - ])->execute(); + ])->executeStatement(); } else { // Already a usergroup share. Update it. $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->set('file_target', $qb->createNamedParameter($share->getTarget())) ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) - ->execute(); + ->executeStatement(); } } return $share; } + 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->dbConn->getQueryBuilder(); + $qb->select('s.*', + 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', + 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', + 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum') + ->from('share', 's') + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + + $qb->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))); + + 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)) + ) + ); + } + } + + // todo? maybe get these from the oc_mounts table + $childMountNodes = array_filter($node->getDirectoryListing(), function (Node $node): bool { + return $node->getInternalPath() === ''; + }); + $childMountRootIds = array_map(function (Node $node): int { + return $node->getId(); + }, $childMountNodes); + + $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())), + $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) + ) + ); + + $qb->orderBy('id'); + + $shares = []; + + $chunks = array_chunk($childMountRootIds, 1000); + + // Force the request to be run when there is 0 mount. + if (count($chunks) === 0) { + $chunks = [[]]; + } + + foreach ($chunks as $chunk) { + $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { + $shares[$data['fileid']][] = $this->createShare($data); + } + $cursor->closeCursor(); + } + + return $shares; + } + /** * @inheritdoc */ @@ -460,10 +688,7 @@ class DefaultShareProvider implements IShareProvider { $qb = $this->dbConn->getQueryBuilder(); $qb->select('*') ->from('share') - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType))); @@ -473,12 +698,14 @@ class DefaultShareProvider implements IShareProvider { if ($reshares === false) { $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)) - ) - ); + if ($node === null) { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } } if ($node !== null) { @@ -492,9 +719,9 @@ class DefaultShareProvider implements IShareProvider { $qb->setFirstResult($offset); $qb->orderBy('id'); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $shares = []; - while($data = $cursor->fetch()) { + while ($data = $cursor->fetch()) { $shares[] = $this->createShare($data); } $cursor->closeCursor(); @@ -515,18 +742,15 @@ class DefaultShareProvider implements IShareProvider { $qb->expr()->in( 'share_type', $qb->createNamedParameter([ - \OCP\Share::SHARE_TYPE_USER, - \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK, + IShare::TYPE_USER, + IShare::TYPE_GROUP, + IShare::TYPE_LINK, ], IQueryBuilder::PARAM_INT_ARRAY) ) ) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )); - - $cursor = $qb->execute(); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -541,8 +765,8 @@ class DefaultShareProvider implements IShareProvider { } // If the recipient is set for a group share resolve to that user - if ($recipientId !== null && $share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { - $share = $this->resolveGroupShare($share, $recipientId); + if ($recipientId !== null && $share->getShareType() === IShare::TYPE_GROUP) { + $share = $this->resolveGroupShares([(int)$share->getId() => $share], $recipientId)[0]; } return $share; @@ -560,20 +784,13 @@ class DefaultShareProvider implements IShareProvider { $cursor = $qb->select('*') ->from('share') ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) - ->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_USER)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_GROUP)) - ) - ) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) - ->execute(); + ->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->orderBy('id', 'ASC') + ->executeQuery(); $shares = []; - while($data = $cursor->fetch()) { + while ($data = $cursor->fetch()) { $shares[] = $this->createShare($data); } $cursor->closeCursor(); @@ -582,20 +799,52 @@ class DefaultShareProvider implements IShareProvider { } /** + * Returns whether the given database result can be interpreted as + * a share with accessible file (not trashed, not deleted) + */ + private function isAccessibleResult($data) { + // exclude shares leading to deleted file entries + if ($data['fileid'] === null || $data['path'] === null) { + return false; + } + + // exclude shares leading to trashbin on home storages + $pathSections = explode('/', $data['path'], 2); + // FIXME: would not detect rare md5'd home storage case properly + if ($pathSections[0] !== 'files' + && (str_starts_with($data['storage_string_id'], 'home::') || str_starts_with($data['storage_string_id'], 'object::user'))) { + return false; + } elseif ($pathSections[0] === '__groupfolders' + && str_starts_with($pathSections[1], 'trash/') + ) { + // exclude shares leading to trashbin on group folders storages + return false; + } + return true; + } + + /** * @inheritdoc */ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { /** @var Share[] $shares */ $shares = []; - if ($shareType === \OCP\Share::SHARE_TYPE_USER) { + if ($shareType === IShare::TYPE_USER) { //Get shares directly with this user $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share'); + $qb->select('s.*', + 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', + 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', + 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' + ) + ->selectAlias('st.id', 'storage_string_id') + ->from('share', 's') + ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) + ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')); // Order by id - $qb->orderBy('id'); + $qb->orderBy('s.id'); // Set limit and offset if ($limit !== -1) { @@ -603,45 +852,55 @@ class DefaultShareProvider implements IShareProvider { } $qb->setFirstResult($offset); - $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_USER))) + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); // Filter by node if provided if ($node !== null) { $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); } - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); - while($data = $cursor->fetch()) { - $shares[] = $this->createShare($data); + while ($data = $cursor->fetch()) { + if ($data['fileid'] && $data['path'] === null) { + $data['path'] = (string)$data['path']; + $data['name'] = (string)$data['name']; + $data['checksum'] = (string)$data['checksum']; + } + if ($this->isAccessibleResult($data)) { + $shares[] = $this->createShare($data); + } } $cursor->closeCursor(); - - } else if ($shareType === \OCP\Share::SHARE_TYPE_GROUP) { - $user = $this->userManager->get($userId); - $allGroups = $this->groupManager->getUserGroups($user); + } elseif ($shareType === IShare::TYPE_GROUP) { + $user = new LazyUser($userId, $this->userManager); + $allGroups = $this->groupManager->getUserGroupIds($user); /** @var Share[] $shares2 */ $shares2 = []; $start = 0; - while(true) { - $groups = array_slice($allGroups, $start, 100); - $start += 100; + while (true) { + $groups = array_slice($allGroups, $start, 1000); + $start += 1000; if ($groups === []) { break; } $qb = $this->dbConn->getQueryBuilder(); - $qb->select('*') - ->from('share') - ->orderBy('id') + $qb->select('s.*', + 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', + 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', + 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' + ) + ->selectAlias('st.id', 'storage_string_id') + ->from('share', 's') + ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) + ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) + ->orderBy('s.id') ->setFirstResult(0); if ($limit !== -1) { @@ -653,36 +912,34 @@ class DefaultShareProvider implements IShareProvider { $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); } - $groups = array_map(function(IGroup $group) { return $group->getGID(); }, $groups); + $groups = array_filter($groups); - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_GROUP))) + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter( $groups, IQueryBuilder::PARAM_STR_ARRAY ))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); - $cursor = $qb->execute(); - while($data = $cursor->fetch()) { + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { if ($offset > 0) { $offset--; continue; } - $shares2[] = $this->createShare($data); + + if ($this->isAccessibleResult($data)) { + $share = $this->createShare($data); + $shares2[$share->getId()] = $share; + } } $cursor->closeCursor(); } /* - * Resolve all group shares to user specific shares - * TODO: Optmize this! - */ - foreach($shares2 as $share) { - $shares[] = $this->resolveGroupShare($share, $userId); - } + * Resolve all group shares to user specific shares + */ + $shares = $this->resolveGroupShares($shares2, $userId); } else { throw new BackendError('Invalid backend'); } @@ -703,13 +960,10 @@ class DefaultShareProvider implements IShareProvider { $cursor = $qb->select('*') ->from('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_LINK))) + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK))) ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) - ->execute(); + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))) + ->executeQuery(); $data = $cursor->fetch(); @@ -725,35 +979,49 @@ class DefaultShareProvider implements IShareProvider { return $share; } - + /** - * Create a share object from an database row + * Create a share object from a database row * * @param mixed[] $data * @return \OCP\Share\IShare * @throws InvalidShare */ private function createShare($data) { - $share = new Share($this->rootFolder); + $share = new Share($this->rootFolder, $this->userManager); $share->setId((int)$data['id']) ->setShareType((int)$data['share_type']) ->setPermissions((int)$data['permissions']) ->setTarget($data['file_target']) - ->setMailSend((bool)$data['mail_send']); + ->setNote((string)$data['note']) + ->setMailSend((bool)$data['mail_send']) + ->setStatus((int)$data['accepted']) + ->setLabel($data['label'] ?? ''); $shareTime = new \DateTime(); $shareTime->setTimestamp((int)$data['stime']); $share->setShareTime($shareTime); - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) { + if ($share->getShareType() === IShare::TYPE_USER) { $share->setSharedWith($data['share_with']); - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) { + $displayName = $this->userManager->getDisplayName($data['share_with']); + if ($displayName !== null) { + $share->setSharedWithDisplayName($displayName); + } + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $share->setSharedWith($data['share_with']); - } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { - $share->setPassword($data['share_with']); + $group = $this->groupManager->get($data['share_with']); + if ($group !== null) { + $share->setSharedWithDisplayName($group->getDisplayName()); + } + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + $share->setPassword($data['password']); + $share->setSendPasswordByTalk((bool)$data['password_by_talk']); $share->setToken($data['token']); } + $share = $this->updateShareAttributes($share, $data['attributes']); + $share->setSharedBy($data['uid_initiator']); $share->setShareOwner($data['uid_owner']); @@ -765,43 +1033,649 @@ class DefaultShareProvider implements IShareProvider { $share->setExpirationDate($expiration); } + if (isset($data['f_permissions'])) { + $entryData = $data; + $entryData['permissions'] = $entryData['f_permissions']; + $entryData['parent'] = $entryData['f_parent']; + $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, + \OC::$server->getMimeTypeLoader())); + } + $share->setProviderId($this->identifier()); + $share->setHideDownload((int)$data['hide_download'] === 1); + $share->setReminderSent((bool)$data['reminder_sent']); return $share; } /** - * Resolve a group share to a user specific share - * Thus if the user moved their group share make sure this is properly reflected here. + * Update the data from group shares with any per-user modifications * - * @param \OCP\Share\IShare $share - * @param string $userId - * @return Share Returns the updated share if one was found else return the original share. + * @param array<int, Share> $shareMap shares indexed by share id + * @param $userId + * @return Share[] The updates shares if no update is found for a share return the original + */ + private function resolveGroupShares($shareMap, $userId) { + $qb = $this->dbConn->getQueryBuilder(); + $query = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + + // this is called with either all group shares or one group share. + // for all shares it's easier to just only search by share_with, + // for a single share it's efficient to filter by parent + if (count($shareMap) === 1) { + $share = reset($shareMap); + $query->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))); + } + + $stmt = $query->executeQuery(); + + while ($data = $stmt->fetch()) { + if (array_key_exists($data['parent'], $shareMap)) { + $shareMap[$data['parent']]->setPermissions((int)$data['permissions']); + $shareMap[$data['parent']]->setStatus((int)$data['accepted']); + $shareMap[$data['parent']]->setTarget($data['file_target']); + $shareMap[$data['parent']]->setParent($data['parent']); + } + } + + return array_values($shareMap); + } + + /** + * A user is deleted from the system + * So clean up the relevant shares. + * + * @param string $uid + * @param int $shareType + */ + public function userDeleted($uid, $shareType) { + $qb = $this->dbConn->getQueryBuilder(); + + $qb->delete('share'); + + if ($shareType === IShare::TYPE_USER) { + /* + * Delete all user shares that are owned by this user + * or that are received by this user + */ + + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER))); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)), + $qb->expr()->eq('share_with', $qb->createNamedParameter($uid)) + ) + ); + } elseif ($shareType === IShare::TYPE_GROUP) { + /* + * Delete all group shares that are owned by this user + * Or special user group shares that are received by this user + */ + $qb->where( + $qb->expr()->andX( + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_GROUP, IShare::TYPE_USERGROUP], IQueryBuilder::PARAM_INT_ARRAY)), + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)) + ) + ); + + $qb->orWhere( + $qb->expr()->andX( + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)), + $qb->expr()->eq('share_with', $qb->createNamedParameter($uid)) + ) + ); + } elseif ($shareType === IShare::TYPE_LINK) { + /* + * Delete all link shares owned by this user. + * And all link shares initiated by this user (until #22327 is in) + */ + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK))); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($uid)) + ) + ); + } else { + $e = new \InvalidArgumentException('Default share provider tried to delete all shares for type: ' . $shareType); + $this->logger->error($e->getMessage(), ['exception' => $e]); + return; + } + + $qb->executeStatement(); + } + + /** + * Delete all shares received by this group. As well as any custom group + * shares for group members. + * + * @param string $gid + */ + public function groupDeleted($gid) { + /* + * First delete all custom group shares for group members + */ + $qb = $this->dbConn->getQueryBuilder(); + $qb->select('id') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); + + $cursor = $qb->executeQuery(); + $ids = []; + while ($row = $cursor->fetch()) { + $ids[] = (int)$row['id']; + } + $cursor->closeCursor(); + + if (!empty($ids)) { + $chunks = array_chunk($ids, 100); + + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents'))); + + foreach ($chunks as $chunk) { + $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); + } + } + + /* + * Now delete all the group shares + */ + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); + $qb->executeStatement(); + } + + /** + * Delete custom group shares to this group for this user + * + * @param string $uid + * @param string $gid + * @return void + */ + public function userDeletedFromGroup($uid, $gid) { + /* + * Get all group shares + */ + $qb = $this->dbConn->getQueryBuilder(); + $qb->select('id') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); + + $cursor = $qb->executeQuery(); + $ids = []; + while ($row = $cursor->fetch()) { + $ids[] = (int)$row['id']; + } + $cursor->closeCursor(); + + if (!empty($ids)) { + $chunks = array_chunk($ids, 100); + + /* + * Delete all special shares with this user for the found group shares + */ + $qb = $this->dbConn->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) + ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->in('parent', $qb->createParameter('parents'))); + + foreach ($chunks as $chunk) { + $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); + } + } + + if ($this->shareManager->shareWithGroupMembersOnly()) { + $user = $this->userManager->get($uid); + if ($user === null) { + return; + } + $userGroups = $this->groupManager->getUserGroupIds($user); + $userGroups = array_diff($userGroups, $this->shareManager->shareWithGroupMembersOnlyExcludeGroupsList()); + + // Delete user shares received by the user from users in the group. + $userReceivedShares = $this->shareManager->getSharedWith($uid, IShare::TYPE_USER, null, -1); + foreach ($userReceivedShares as $share) { + $owner = $this->userManager->get($share->getSharedBy()); + if ($owner === null) { + continue; + } + $ownerGroups = $this->groupManager->getUserGroupIds($owner); + $mutualGroups = array_intersect($userGroups, $ownerGroups); + + if (count($mutualGroups) === 0) { + $this->shareManager->deleteShare($share); + } + } + + // Delete user shares from the user to users in the group. + $userEmittedShares = $this->shareManager->getSharesBy($uid, IShare::TYPE_USER, null, true, -1); + foreach ($userEmittedShares as $share) { + $recipient = $this->userManager->get($share->getSharedWith()); + if ($recipient === null) { + continue; + } + $recipientGroups = $this->groupManager->getUserGroupIds($recipient); + $mutualGroups = array_intersect($userGroups, $recipientGroups); + + if (count($mutualGroups) === 0) { + $this->shareManager->deleteShare($share); + } + } + } + } + + /** + * @inheritdoc + */ + public function getAccessList($nodes, $currentAccess) { + $ids = []; + foreach ($nodes as $node) { + $ids[] = $node->getId(); + } + + $qb = $this->dbConn->getQueryBuilder(); + + $shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]; + + if ($currentAccess) { + $shareTypes[] = IShare::TYPE_USERGROUP; + } + + $qb->select('id', 'parent', 'share_type', 'share_with', 'file_source', 'file_target', 'permissions') + ->from('share') + ->where( + $qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)) + ) + ->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))); + + // Ensure accepted is true for user and usergroup type + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)), + $qb->expr()->neq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)), + ), + $qb->expr()->eq('accepted', $qb->createNamedParameter(IShare::STATUS_ACCEPTED, IQueryBuilder::PARAM_INT)), + ), + ); + + $cursor = $qb->executeQuery(); + + $users = []; + $link = false; + while ($row = $cursor->fetch()) { + $type = (int)$row['share_type']; + if ($type === IShare::TYPE_USER) { + $uid = $row['share_with']; + $users[$uid] = $users[$uid] ?? []; + $users[$uid][$row['id']] = $row; + } elseif ($type === IShare::TYPE_GROUP) { + $gid = $row['share_with']; + $group = $this->groupManager->get($gid); + + if ($group === null) { + continue; + } + + $userList = $group->getUsers(); + foreach ($userList as $user) { + $uid = $user->getUID(); + $users[$uid] = $users[$uid] ?? []; + $users[$uid][$row['id']] = $row; + } + } elseif ($type === IShare::TYPE_LINK) { + $link = true; + } elseif ($type === IShare::TYPE_USERGROUP && $currentAccess === true) { + $uid = $row['share_with']; + $users[$uid] = $users[$uid] ?? []; + $users[$uid][$row['id']] = $row; + } + } + $cursor->closeCursor(); + + if ($currentAccess === true) { + $users = array_map([$this, 'filterSharesOfUser'], $users); + $users = array_filter($users); + } else { + $users = array_keys($users); + } + + return ['users' => $users, 'public' => $link]; + } + + /** + * For each user the path with the fewest slashes is returned + * @param array $shares + * @return array + */ + protected function filterSharesOfUser(array $shares) { + // Group shares when the user has a share exception + foreach ($shares as $id => $share) { + $type = (int)$share['share_type']; + $permissions = (int)$share['permissions']; + + if ($type === IShare::TYPE_USERGROUP) { + unset($shares[$share['parent']]); + + if ($permissions === 0) { + unset($shares[$id]); + } + } + } + + $best = []; + $bestDepth = 0; + foreach ($shares as $id => $share) { + $depth = substr_count(($share['file_target'] ?? ''), '/'); + if (empty($best) || $depth < $bestDepth) { + $bestDepth = $depth; + $best = [ + 'node_id' => $share['file_source'], + 'node_path' => $share['file_target'], + ]; + } + } + + return $best; + } + + /** + * propagate notes to the recipients + * + * @param IShare $share + * @throws \OCP\Files\NotFoundException + */ + private function propagateNote(IShare $share) { + if ($share->getShareType() === IShare::TYPE_USER) { + $user = $this->userManager->get($share->getSharedWith()); + $this->sendNote([$user], $share); + } elseif ($share->getShareType() === IShare::TYPE_GROUP) { + $group = $this->groupManager->get($share->getSharedWith()); + $groupMembers = $group->getUsers(); + $this->sendNote($groupMembers, $share); + } + } + + public function sendMailNotification(IShare $share): bool { + try { + // Check user + $user = $this->userManager->get($share->getSharedWith()); + if ($user === null) { + $this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because user could not be found.', ['app' => 'share']); + return false; + } + + // Handle user shares + if ($share->getShareType() === IShare::TYPE_USER) { + // Check email address + $emailAddress = $user->getEMailAddress(); + if ($emailAddress === null || $emailAddress === '') { + $this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because email address is not set.', ['app' => 'share']); + return false; + } + + $userLang = $this->l10nFactory->getUserLanguage($user); + $l = $this->l10nFactory->get('lib', $userLang); + $this->sendUserShareMail( + $l, + $share->getNode()->getName(), + $this->urlGenerator->linkToRouteAbsolute('files_sharing.Accept.accept', ['shareId' => $share->getFullId()]), + $share->getSharedBy(), + $emailAddress, + $share->getExpirationDate(), + $share->getNote() + ); + $this->logger->debug('Sent share notification to ' . $emailAddress . ' for share with ID ' . $share->getId() . '.', ['app' => 'share']); + return true; + } + } catch (\Exception $e) { + $this->logger->error('Share notification mail could not be sent.', ['exception' => $e]); + } + + return false; + } + + /** + * Send mail notifications for the user share type + * + * @param IL10N $l Language of the recipient + * @param string $filename file/folder name + * @param string $link link to the file/folder + * @param string $initiator user ID of share sender + * @param string $shareWith email address of share receiver + * @param \DateTime|null $expiration + * @param string $note + * @throws \Exception */ - private function resolveGroupShare(\OCP\Share\IShare $share, $userId) { + protected function sendUserShareMail( + IL10N $l, + $filename, + $link, + $initiator, + $shareWith, + ?\DateTime $expiration = null, + $note = '') { + $initiatorUser = $this->userManager->get($initiator); + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; + + $message = $this->mailer->createMessage(); + + $emailTemplate = $this->mailer->createEMailTemplate('files_sharing.RecipientNotification', [ + 'filename' => $filename, + 'link' => $link, + 'initiator' => $initiatorDisplayName, + 'expiration' => $expiration, + 'shareWith' => $shareWith, + ]); + + $emailTemplate->setSubject($l->t('%1$s shared %2$s with you', [$initiatorDisplayName, $filename])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($l->t('%1$s shared %2$s with you', [$initiatorDisplayName, $filename]), false); + + if ($note !== '') { + $emailTemplate->addBodyText(htmlspecialchars($note), $note); + } + + $emailTemplate->addBodyButton( + $l->t('Open %s', [$filename]), + $link + ); + + $message->setTo([$shareWith]); + + // The "From" contains the sharers name + $instanceName = $this->defaults->getName(); + $senderName = $l->t( + '%1$s via %2$s', + [ + $initiatorDisplayName, + $instanceName, + ] + ); + $message->setFrom([\OCP\Util::getDefaultEmailAddress('noreply') => $senderName]); + + // The "Reply-To" is set to the sharer if an mail address is configured + // also the default footer contains a "Do not reply" which needs to be adjusted. + if ($initiatorUser) { + $initiatorEmail = $initiatorUser->getEMailAddress(); + if ($initiatorEmail !== null) { + $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]); + $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')); + } else { + $emailTemplate->addFooter(); + } + } else { + $emailTemplate->addFooter(); + } + + $message->useTemplate($emailTemplate); + $failedRecipients = $this->mailer->send($message); + if (!empty($failedRecipients)) { + $this->logger->error('Share notification mail could not be sent to: ' . implode(', ', $failedRecipients)); + return; + } + } + + /** + * send note by mail + * + * @param array $recipients + * @param IShare $share + * @throws \OCP\Files\NotFoundException + */ + private function sendNote(array $recipients, IShare $share) { + $toListByLanguage = []; + + foreach ($recipients as $recipient) { + /** @var IUser $recipient */ + $email = $recipient->getEMailAddress(); + if ($email) { + $language = $this->l10nFactory->getUserLanguage($recipient); + if (!isset($toListByLanguage[$language])) { + $toListByLanguage[$language] = []; + } + $toListByLanguage[$language][$email] = $recipient->getDisplayName(); + } + } + + if (empty($toListByLanguage)) { + return; + } + + foreach ($toListByLanguage as $l10n => $toList) { + $filename = $share->getNode()->getName(); + $initiator = $share->getSharedBy(); + $note = $share->getNote(); + + $l = $this->l10nFactory->get('lib', $l10n); + + $initiatorUser = $this->userManager->get($initiator); + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; + $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; + $plainHeading = $l->t('%1$s shared %2$s with you and wants to add:', [$initiatorDisplayName, $filename]); + $htmlHeading = $l->t('%1$s shared %2$s with you and wants to add', [$initiatorDisplayName, $filename]); + $message = $this->mailer->createMessage(); + + $emailTemplate = $this->mailer->createEMailTemplate('defaultShareProvider.sendNote'); + + $emailTemplate->setSubject($l->t('%s added a note to a file shared with you', [$initiatorDisplayName])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($htmlHeading, $plainHeading); + $emailTemplate->addBodyText(htmlspecialchars($note), $note); + + $link = $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $share->getNode()->getId()]); + $emailTemplate->addBodyButton( + $l->t('Open %s', [$filename]), + $link + ); + + + // The "From" contains the sharers name + $instanceName = $this->defaults->getName(); + $senderName = $l->t( + '%1$s via %2$s', + [ + $initiatorDisplayName, + $instanceName + ] + ); + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); + if ($initiatorEmailAddress !== null) { + $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); + $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); + } else { + $emailTemplate->addFooter(); + } + + if (count($toList) === 1) { + $message->setTo($toList); + } else { + $message->setTo([]); + $message->setBcc($toList); + } + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + } + } + + public function getAllShares(): iterable { $qb = $this->dbConn->getQueryBuilder(); - $stmt = $qb->select('*') + $qb->select('*') ->from('share') - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERGROUP))) - ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))) - ->andWhere($qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) - )) - ->setMaxResults(1) - ->execute(); + ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))); + + $cursor = $qb->executeQuery(); + while ($data = $cursor->fetch()) { + try { + $share = $this->createShare($data); + } catch (InvalidShare $e) { + continue; + } - $data = $stmt->fetch(); - $stmt->closeCursor(); + yield $share; + } + $cursor->closeCursor(); + } - if ($data !== false) { - $share->setPermissions((int)$data['permissions']); - $share->setTarget($data['file_target']); + /** + * Load from database format (JSON string) to IAttributes + * + * @return IShare the modified share + */ + protected function updateShareAttributes(IShare $share, ?string $data): IShare { + if ($data !== null && $data !== '') { + $attributes = new ShareAttributes(); + $compressedAttributes = \json_decode($data, true); + if ($compressedAttributes === false || $compressedAttributes === null) { + return $share; + } + foreach ($compressedAttributes as $compressedAttribute) { + $attributes->setAttribute( + $compressedAttribute[0], + $compressedAttribute[1], + $compressedAttribute[2] + ); + } + $share->setAttributes($attributes); } return $share; } + /** + * Format IAttributes to database format (JSON string) + */ + protected function formatShareAttributes(?IAttributes $attributes): ?string { + if ($attributes === null || empty($attributes->toArray())) { + return null; + } + + $compressedAttributes = []; + foreach ($attributes->toArray() as $attribute) { + $compressedAttributes[] = [ + 0 => $attribute['scope'], + 1 => $attribute['key'], + 2 => $attribute['value'] + ]; + } + return \json_encode($compressedAttributes); + } } |