diff options
Diffstat (limited to 'lib/private/Share20/DefaultShareProvider.php')
-rw-r--r-- | lib/private/Share20/DefaultShareProvider.php | 848 |
1 files changed, 513 insertions, 335 deletions
diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index cf05e9bfbc3..5300e6e1baa 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -1,36 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Jan-Philipp Litza <jplitza@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author phisch <git@philippschaffrath.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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; @@ -39,6 +12,9 @@ use OC\Files\Cache\Cache; use OC\Share20\Exception\BackendError; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; +use OC\User\LazyUser; +use OCA\Files_Sharing\AppInfo\Application; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Defaults; use OCP\Files\Folder; @@ -47,71 +23,42 @@ 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\IShareProvider; +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 - public const SHARE_TYPE_USERGROUP = 2; - - /** @var IDBConnection */ - private $dbConn; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IMailer */ - private $mailer; - - /** @var Defaults */ - private $defaults; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var IConfig */ - private $config; - +class DefaultShareProvider implements IShareProviderWithNotification, IShareProviderSupportsAccept, IShareProviderSupportsAllSharesInFolder { public function __construct( - IDBConnection $connection, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IMailer $mailer, - Defaults $defaults, - IFactory $l10nFactory, - IURLGenerator $urlGenerator, - IConfig $config) { - $this->dbConn = $connection; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; - $this->mailer = $mailer; - $this->defaults = $defaults; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->config = $config; + 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, + ) { } /** @@ -137,22 +84,30 @@ class DefaultShareProvider implements IShareProvider { $qb->insert('share'); $qb->setValue('share_type', $qb->createNamedParameter($share->getShareType())); + $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())); $qb->setValue('accepted', $qb->createNamedParameter(IShare::STATUS_PENDING)); //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')); } + + $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())); //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')); } } elseif ($share->getShareType() === IShare::TYPE_LINK) { //set label for public link @@ -168,13 +123,13 @@ class DefaultShareProvider implements IShareProvider { $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!'); } @@ -194,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())); @@ -203,33 +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'); + $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))); - - $cursor = $qb->execute(); - $data = $cursor->fetch(); - $this->dbConn->commit(); - $cursor->closeCursor(); + // Update mandatory data + $id = $qb->getLastInsertId(); + $share->setId((string)$id); + $share->setProviderId($this->identifier()); - if ($data === false) { - throw new ShareNotFound(); - } + $share->setShareTime(\DateTime::createFromImmutable($shareTime)); $mailSendValue = $share->getMailSend(); - $data['mail_send'] = ($mailSendValue === null) ? true : $mailSendValue; + $share->setMailSend(($mailSendValue === null) ? true : $mailSendValue); - $share = $this->createShare($data); return $share; } @@ -245,6 +200,14 @@ class DefaultShareProvider implements IShareProvider { public function update(\OCP\Share\IShare $share) { $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. @@ -256,12 +219,14 @@ 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())) - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) ->set('note', $qb->createNamedParameter($share->getNote())) ->set('accepted', $qb->createNamedParameter($share->getStatus())) - ->execute(); + ->set('reminder_sent', $qb->createNamedParameter($share->getReminderSent(), IQueryBuilder::PARAM_BOOL)) + ->executeStatement(); } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') @@ -269,11 +234,12 @@ 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())) - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) ->set('note', $qb->createNamedParameter($share->getNote())) - ->execute(); + ->executeStatement(); /* * Update all user defined group shares @@ -286,9 +252,9 @@ class DefaultShareProvider implements IShareProvider { ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->set('expiration', $qb->createNamedParameter($expirationDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)) ->set('note', $qb->createNamedParameter($share->getNote())) - ->execute(); + ->executeStatement(); /* * Now update the permissions for all children that have not set it to 0 @@ -298,7 +264,8 @@ 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(); + ->set('attributes', $qb->createNamedParameter($shareAttributes)) + ->executeStatement(); } elseif ($share->getShareType() === IShare::TYPE_LINK) { $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') @@ -308,14 +275,15 @@ 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())) ->set('token', $qb->createNamedParameter($share->getToken())) - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) + ->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) - ->execute(); + ->set('hide_download', $qb->createNamedParameter($share->getHideDownload() ? 1 : 0, IQueryBuilder::PARAM_INT)) + ->executeStatement(); } if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { @@ -354,11 +322,8 @@ class DefaultShareProvider implements IShareProvider { ->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(); $stmt->closeCursor(); @@ -386,19 +351,12 @@ class DefaultShareProvider implements IShareProvider { $qb->update('share') ->set('accepted', $qb->createNamedParameter(IShare::STATUS_ACCEPTED)) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->execute(); + ->executeStatement(); return $share; } - /** - * Get all children of this share - * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in - * - * @param \OCP\Share\IShare $parent - * @return \OCP\Share\IShare[] - */ - public function getChildren(\OCP\Share\IShare $parent) { + public function getChildren(IShare $parent): array { $children = []; $qb = $this->dbConn->getQueryBuilder(); @@ -415,13 +373,10 @@ class DefaultShareProvider implements IShareProvider { ], 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(); + $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { $children[] = $this->createShare($data); } @@ -448,7 +403,7 @@ class DefaultShareProvider implements IShareProvider { $qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))); } - $qb->execute(); + $qb->executeStatement(); } /** @@ -470,7 +425,8 @@ class DefaultShareProvider implements IShareProvider { } if (!$group->inGroup($user)) { - throw new ProviderException('Recipient not in receiving group'); + // nothing left to do + return; } // Try to fetch user specific share @@ -480,11 +436,8 @@ class DefaultShareProvider implements IShareProvider { ->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(); @@ -506,7 +459,7 @@ class DefaultShareProvider implements IShareProvider { $qb->update('share') ->set('permissions', $qb->createNamedParameter(0)) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->execute(); + ->executeStatement(); } } elseif ($share->getShareType() === IShare::TYPE_USER) { if ($share->getSharedWith() !== $recipient) { @@ -523,6 +476,15 @@ 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([ @@ -534,10 +496,10 @@ class DefaultShareProvider implements IShareProvider { 'item_type' => $qb->createNamedParameter($type), 'item_source' => $qb->createNamedParameter($share->getNodeId()), 'file_source' => $qb->createNamedParameter($share->getNodeId()), - 'file_target' => $qb->createNamedParameter($share->getTarget()), + 'file_target' => $qb->createNamedParameter($target), 'permissions' => $qb->createNamedParameter($share->getPermissions()), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), - ])->execute(); + ])->executeStatement(); return $qb->getLastInsertId(); } @@ -555,7 +517,7 @@ class DefaultShareProvider implements IShareProvider { ->where( $qb->expr()->eq('id', $qb->createNamedParameter($share->getId())) ); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -572,7 +534,7 @@ class DefaultShareProvider implements IShareProvider { $qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)) ); - $qb->execute(); + $qb->executeStatement(); return $this->getShareById($share->getId(), $recipient); } @@ -587,9 +549,8 @@ class DefaultShareProvider implements IShareProvider { $qb->update('share') ->set('file_target', $qb->createNamedParameter($share->getTarget())) ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->execute(); + ->executeStatement(); } elseif ($share->getShareType() === IShare::TYPE_GROUP) { - // Check if there is a usergroup share $qb = $this->dbConn->getQueryBuilder(); $stmt = $qb->select('id') @@ -597,16 +558,17 @@ class DefaultShareProvider implements IShareProvider { ->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(); @@ -622,61 +584,99 @@ class DefaultShareProvider implements IShareProvider { '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) { + 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('*') + $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()->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()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK)) - )); + $qb->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))); - /** - * Reshares for this user are shares where they are the owner. - */ - 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 ($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()))); + // 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'); - $cursor = $qb->execute(); $shares = []; - while ($data = $cursor->fetch()) { - $shares[$data['fileid']][] = $this->createShare($data); + + $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(); } - $cursor->closeCursor(); return $shares; } @@ -688,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))); @@ -722,7 +719,7 @@ class DefaultShareProvider implements IShareProvider { $qb->setFirstResult($offset); $qb->orderBy('id'); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $shares = []; while ($data = $cursor->fetch()) { $shares[] = $this->createShare($data); @@ -751,12 +748,9 @@ class DefaultShareProvider implements IShareProvider { ], 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))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -772,7 +766,7 @@ class DefaultShareProvider implements IShareProvider { // If the recipient is set for a group share resolve to that user if ($recipientId !== null && $share->getShareType() === IShare::TYPE_GROUP) { - $share = $this->resolveGroupShares([$share], $recipientId)[0]; + $share = $this->resolveGroupShares([(int)$share->getId() => $share], $recipientId)[0]; } return $share; @@ -790,17 +784,10 @@ 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(IShare::TYPE_USER)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::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()) { @@ -825,7 +812,12 @@ class DefaultShareProvider implements IShareProvider { $pathSections = explode('/', $data['path'], 2); // FIXME: would not detect rare md5'd home storage case properly if ($pathSections[0] !== 'files' - && (strpos($data['storage_string_id'], 'home::') === 0 || strpos($data['storage_string_id'], 'object::user') === 0)) { + && (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; @@ -862,23 +854,20 @@ class DefaultShareProvider implements IShareProvider { $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()) { if ($data['fileid'] && $data['path'] === null) { - $data['path'] = (string) $data['path']; - $data['name'] = (string) $data['name']; - $data['checksum'] = (string) $data['checksum']; + $data['path'] = (string)$data['path']; + $data['name'] = (string)$data['name']; + $data['checksum'] = (string)$data['checksum']; } if ($this->isAccessibleResult($data)) { $shares[] = $this->createShare($data); @@ -886,16 +875,16 @@ class DefaultShareProvider implements IShareProvider { } $cursor->closeCursor(); } elseif ($shareType === IShare::TYPE_GROUP) { - $user = $this->userManager->get($userId); - $allGroups = ($user instanceof IUser) ? $this->groupManager->getUserGroupIds($user) : []; + $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; + $groups = array_slice($allGroups, $start, 1000); + $start += 1000; if ($groups === []) { break; @@ -923,7 +912,6 @@ class DefaultShareProvider implements IShareProvider { $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); } - $groups = array_filter($groups); $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) @@ -931,12 +919,9 @@ class DefaultShareProvider implements IShareProvider { $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(); + $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { if ($offset > 0) { $offset--; @@ -944,7 +929,8 @@ class DefaultShareProvider implements IShareProvider { } if ($this->isAccessibleResult($data)) { - $shares2[] = $this->createShare($data); + $share = $this->createShare($data); + $shares2[$share->getId()] = $share; } } $cursor->closeCursor(); @@ -976,11 +962,8 @@ class DefaultShareProvider implements IShareProvider { ->from('share') ->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(); @@ -998,7 +981,7 @@ class DefaultShareProvider implements IShareProvider { } /** - * Create a share object from an database row + * Create a share object from a database row * * @param mixed[] $data * @return \OCP\Share\IShare @@ -1013,7 +996,7 @@ class DefaultShareProvider implements IShareProvider { ->setNote((string)$data['note']) ->setMailSend((bool)$data['mail_send']) ->setStatus((int)$data['accepted']) - ->setLabel($data['label']); + ->setLabel($data['label'] ?? ''); $shareTime = new \DateTime(); $shareTime->setTimestamp((int)$data['stime']); @@ -1021,18 +1004,24 @@ class DefaultShareProvider implements IShareProvider { if ($share->getShareType() === IShare::TYPE_USER) { $share->setSharedWith($data['share_with']); - $user = $this->userManager->get($data['share_with']); - if ($user !== null) { - $share->setSharedWithDisplayName($user->getDisplayName()); + $displayName = $this->userManager->getDisplayName($data['share_with']); + if ($displayName !== null) { + $share->setSharedWithDisplayName($displayName); } } elseif ($share->getShareType() === IShare::TYPE_GROUP) { $share->setSharedWith($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']); @@ -1054,66 +1043,46 @@ class DefaultShareProvider implements IShareProvider { $share->setProviderId($this->identifier()); $share->setHideDownload((int)$data['hide_download'] === 1); + $share->setReminderSent((bool)$data['reminder_sent']); return $share; } /** - * @param Share[] $shares + * Update the data from group shares with any per-user modifications + * + * @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($shares, $userId) { - $result = []; - - $start = 0; - while (true) { - /** @var Share[] $shareSlice */ - $shareSlice = array_slice($shares, $start, 100); - $start += 100; - - if ($shareSlice === []) { - break; - } - - /** @var int[] $ids */ - $ids = []; - /** @var Share[] $shareMap */ - $shareMap = []; - - foreach ($shareSlice as $share) { - $ids[] = (int)$share->getId(); - $shareMap[$share->getId()] = $share; - } - - $qb = $this->dbConn->getQueryBuilder(); - - $query = $qb->select('*') - ->from('share') - ->where($qb->expr()->in('parent', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) - ->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')) - )); + 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->execute(); + $stmt = $query->executeQuery(); - while ($data = $stmt->fetch()) { + 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']); } - - $stmt->closeCursor(); - - foreach ($shareMap as $share) { - $result[] = $share; - } } - return $result; + return array_values($shareMap); } /** @@ -1149,10 +1118,7 @@ class DefaultShareProvider implements IShareProvider { */ $qb->where( $qb->expr()->andX( - $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP)) - ), + $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)) ) ); @@ -1177,11 +1143,12 @@ class DefaultShareProvider implements IShareProvider { ) ); } else { - \OC::$server->getLogger()->logException(new \InvalidArgumentException('Default share provider tried to delete all shares for type: ' . $shareType)); + $e = new \InvalidArgumentException('Default share provider tried to delete all shares for type: ' . $shareType); + $this->logger->error($e->getMessage(), ['exception' => $e]); return; } - $qb->execute(); + $qb->executeStatement(); } /** @@ -1200,7 +1167,7 @@ class DefaultShareProvider implements IShareProvider { ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $ids = []; while ($row = $cursor->fetch()) { $ids[] = (int)$row['id']; @@ -1209,11 +1176,15 @@ class DefaultShareProvider implements IShareProvider { 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->delete('share') - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))) - ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); - $qb->execute(); + $qb->setParameter('parents', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + $qb->executeStatement(); } } @@ -1224,7 +1195,7 @@ class DefaultShareProvider implements IShareProvider { $qb->delete('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); - $qb->execute(); + $qb->executeStatement(); } /** @@ -1232,6 +1203,7 @@ class DefaultShareProvider implements IShareProvider { * * @param string $uid * @param string $gid + * @return void */ public function userDeletedFromGroup($uid, $gid) { /* @@ -1243,7 +1215,7 @@ class DefaultShareProvider implements IShareProvider { ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($gid))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $ids = []; while ($row = $cursor->fetch()) { $ids[] = (int)$row['id']; @@ -1252,15 +1224,58 @@ class DefaultShareProvider implements IShareProvider { 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) { - /* - * Delete all special shares wit this users for the found group shares - */ - $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->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); - $qb->execute(); + $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); + } } } } @@ -1276,27 +1291,32 @@ class DefaultShareProvider implements IShareProvider { $qb = $this->dbConn->getQueryBuilder(); - $or = $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USER)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_GROUP)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK)) - ); + $shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]; if ($currentAccess) { - $or->add($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_USERGROUP))); + $shareTypes[] = IShare::TYPE_USERGROUP; } $qb->select('id', 'parent', 'share_type', 'share_with', 'file_source', 'file_target', 'permissions') ->from('share') ->where( - $or + $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()->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))); + + // 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; @@ -1304,7 +1324,7 @@ class DefaultShareProvider implements IShareProvider { $type = (int)$row['share_type']; if ($type === IShare::TYPE_USER) { $uid = $row['share_with']; - $users[$uid] = isset($users[$uid]) ? $users[$uid] : []; + $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } elseif ($type === IShare::TYPE_GROUP) { $gid = $row['share_with']; @@ -1317,14 +1337,14 @@ class DefaultShareProvider implements IShareProvider { $userList = $group->getUsers(); foreach ($userList as $user) { $uid = $user->getUID(); - $users[$uid] = isset($users[$uid]) ? $users[$uid] : []; + $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] = isset($users[$uid]) ? $users[$uid] : []; + $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } } @@ -1348,8 +1368,8 @@ class DefaultShareProvider implements IShareProvider { 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']; + $type = (int)$share['share_type']; + $permissions = (int)$share['permissions']; if ($type === IShare::TYPE_USERGROUP) { unset($shares[$share['parent']]); @@ -1363,7 +1383,7 @@ class DefaultShareProvider implements IShareProvider { $best = []; $bestDepth = 0; foreach ($shares as $id => $share) { - $depth = substr_count($share['file_target'], '/'); + $depth = substr_count(($share['file_target'] ?? ''), '/'); if (empty($best) || $depth < $bestDepth) { $bestDepth = $depth; $best = [ @@ -1393,6 +1413,126 @@ class DefaultShareProvider implements IShareProvider { } } + 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 + */ + 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 * @@ -1429,20 +1569,20 @@ class DefaultShareProvider implements IShareProvider { $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]); + $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->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]), + $l->t('Open %s', [$filename]), $link ); @@ -1480,15 +1620,9 @@ class DefaultShareProvider implements IShareProvider { $qb->select('*') ->from('share') - ->where( - $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_USER)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_GROUP)), - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_LINK)) - ) - ); + ->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { try { $share = $this->createShare($data); @@ -1500,4 +1634,48 @@ class DefaultShareProvider implements IShareProvider { } $cursor->closeCursor(); } + + /** + * 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); + } } |