diff options
Diffstat (limited to 'apps/files_sharing/lib')
89 files changed, 4841 insertions, 4237 deletions
diff --git a/apps/files_sharing/lib/Activity/Filter.php b/apps/files_sharing/lib/Activity/Filter.php index 0ac64301ec8..4f3c4a7c914 100644 --- a/apps/files_sharing/lib/Activity/Filter.php +++ b/apps/files_sharing/lib/Activity/Filter.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity; @@ -31,15 +14,10 @@ class Filter implements IFilter { public const TYPE_REMOTE_SHARE = 'remote_share'; public const TYPE_SHARED = 'shared'; - /** @var IL10N */ - protected $l; - - /** @var IURLGenerator */ - protected $url; - - public function __construct(IL10N $l, IURLGenerator $url) { - $this->l = $l; - $this->url = $url; + public function __construct( + protected IL10N $l, + protected IURLGenerator $url, + ) { } /** diff --git a/apps/files_sharing/lib/Activity/Providers/Base.php b/apps/files_sharing/lib/Activity/Providers/Base.php index cbd16c134cc..7428af382fc 100644 --- a/apps/files_sharing/lib/Activity/Providers/Base.php +++ b/apps/files_sharing/lib/Activity/Providers/Base.php @@ -1,28 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; +use OCP\Activity\Exceptions\UnknownActivityException; use OCP\Activity\IEvent; use OCP\Activity\IEventMerger; use OCP\Activity\IManager; @@ -31,53 +15,25 @@ use OCP\Contacts\IManager as IContactsManager; use OCP\Federation\ICloudIdManager; use OCP\IL10N; use OCP\IURLGenerator; -use OCP\IUser; use OCP\IUserManager; use OCP\L10N\IFactory; abstract class Base implements IProvider { - - /** @var IFactory */ - protected $languageFactory; - /** @var IL10N */ protected $l; - /** @var IURLGenerator */ - protected $url; - - /** @var IManager */ - protected $activityManager; - - /** @var IUserManager */ - protected $userManager; - - /** @var IEventMerger */ - protected $eventMerger; - - /** @var IContactsManager */ - protected $contactsManager; - - /** @var ICloudIdManager */ - protected $cloudIdManager; - /** @var array */ protected $displayNames = []; - public function __construct(IFactory $languageFactory, - IURLGenerator $url, - IManager $activityManager, - IUserManager $userManager, - ICloudIdManager $cloudIdManager, - IContactsManager $contactsManager, - IEventMerger $eventMerger) { - $this->languageFactory = $languageFactory; - $this->url = $url; - $this->activityManager = $activityManager; - $this->userManager = $userManager; - $this->cloudIdManager = $cloudIdManager; - $this->contactsManager = $contactsManager; - $this->eventMerger = $eventMerger; + public function __construct( + protected IFactory $languageFactory, + protected IURLGenerator $url, + protected IManager $activityManager, + protected IUserManager $userManager, + protected ICloudIdManager $cloudIdManager, + protected IContactsManager $contactsManager, + protected IEventMerger $eventMerger, + ) { } /** @@ -85,12 +41,12 @@ abstract class Base implements IProvider { * @param IEvent $event * @param IEvent|null $previousEvent * @return IEvent - * @throws \InvalidArgumentException + * @throws UnknownActivityException * @since 11.0.0 */ - public function parse($language, IEvent $event, IEvent $previousEvent = null) { + public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { if ($event->getApp() !== 'files_sharing') { - throw new \InvalidArgumentException(); + throw new UnknownActivityException(); } $this->l = $this->languageFactory->get('files_sharing', $language); @@ -121,27 +77,13 @@ abstract class Base implements IProvider { * @throws \InvalidArgumentException * @since 11.0.0 */ - abstract protected function parseLongVersion(IEvent $event, IEvent $previousEvent = null); + abstract protected function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null); /** - * @param IEvent $event - * @param string $subject - * @param array $parameters * @throws \InvalidArgumentException */ - protected function setSubjects(IEvent $event, $subject, array $parameters) { - $placeholders = $replacements = []; - foreach ($parameters as $placeholder => $parameter) { - $placeholders[] = '{' . $placeholder . '}'; - if ($parameter['type'] === 'file') { - $replacements[] = $parameter['path']; - } else { - $replacements[] = $parameter['name']; - } - } - - $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) - ->setRichSubject($subject, $parameters); + protected function setSubjects(IEvent $event, string $subject, array $parameters): void { + $event->setRichSubject($subject, $parameters); } /** @@ -150,14 +92,13 @@ abstract class Base implements IProvider { * @return array * @throws \InvalidArgumentException */ - protected function getFile($parameter, IEvent $event = null) { + protected function getFile($parameter, ?IEvent $event = null) { if (is_array($parameter)) { $path = reset($parameter); - $id = (string) key($parameter); + $id = (string)key($parameter); } elseif ($event !== null) { - // Legacy from before ownCloud 8.2 $path = $parameter; - $id = $event->getObjectId(); + $id = (string)$event->getObjectId(); } else { throw new \InvalidArgumentException('Could not generate file parameter'); } @@ -173,16 +114,18 @@ abstract class Base implements IProvider { /** * @param string $uid + * @param string $overwriteDisplayName - overwrite display name, only if user is not local + * * @return array */ - protected function getUser($uid) { + protected function getUser(string $uid, string $overwriteDisplayName = '') { // First try local user - $user = $this->userManager->get($uid); - if ($user instanceof IUser) { + $displayName = $this->userManager->getDisplayName($uid); + if ($displayName !== null) { return [ 'type' => 'user', - 'id' => $user->getUID(), - 'name' => $user->getDisplayName(), + 'id' => $uid, + 'name' => $displayName, ]; } @@ -192,7 +135,7 @@ abstract class Base implements IProvider { return [ 'type' => 'user', 'id' => $cloudId->getUser(), - 'name' => $this->getDisplayNameFromAddressBook($cloudId->getDisplayId()), + 'name' => (($overwriteDisplayName !== '') ? $overwriteDisplayName : $this->getDisplayNameFromAddressBook($cloudId->getDisplayId())), 'server' => $cloudId->getRemote(), ]; } @@ -201,7 +144,7 @@ abstract class Base implements IProvider { return [ 'type' => 'user', 'id' => $uid, - 'name' => $uid, + 'name' => (($overwriteDisplayName !== '') ? $overwriteDisplayName : $uid), ]; } diff --git a/apps/files_sharing/lib/Activity/Providers/Downloads.php b/apps/files_sharing/lib/Activity/Providers/Downloads.php index 8152e0b0885..bddf2d30f73 100644 --- a/apps/files_sharing/lib/Activity/Providers/Downloads.php +++ b/apps/files_sharing/lib/Activity/Providers/Downloads.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; @@ -41,11 +24,11 @@ class Downloads extends Base { public function parseShortVersion(IEvent $event) { $parsedParameters = $this->getParsedParameters($event); - if ($event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED || - $event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED) { + if ($event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED + || $event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED) { $subject = $this->l->t('Downloaded via public link'); - } elseif ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED || - $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED) { + } elseif ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED + || $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED) { $subject = $this->l->t('Downloaded by {email}'); } else { throw new \InvalidArgumentException(); @@ -68,11 +51,11 @@ class Downloads extends Base { * @throws \InvalidArgumentException * @since 11.0.0 */ - public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { $parsedParameters = $this->getParsedParameters($event); - if ($event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED || - $event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED) { + if ($event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED + || $event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED) { if (!isset($parsedParameters['remote-address-hash']['type'])) { $subject = $this->l->t('{file} downloaded via public link'); $this->setSubjects($event, $subject, $parsedParameters); @@ -81,8 +64,8 @@ class Downloads extends Base { $this->setSubjects($event, $subject, $parsedParameters); $event = $this->eventMerger->mergeEvents('file', $event, $previousEvent); } - } elseif ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED || - $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED) { + } elseif ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED + || $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED) { $subject = $this->l->t('{email} downloaded {file}'); $this->setSubjects($event, $subject, $parsedParameters); } else { diff --git a/apps/files_sharing/lib/Activity/Providers/Groups.php b/apps/files_sharing/lib/Activity/Providers/Groups.php index b9cc2e6b579..d0086c05ced 100644 --- a/apps/files_sharing/lib/Activity/Providers/Groups.php +++ b/apps/files_sharing/lib/Activity/Providers/Groups.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; @@ -44,22 +26,20 @@ class Groups extends Base { public const SUBJECT_EXPIRED_GROUP = 'expired_group'; - /** @var IGroupManager */ - protected $groupManager; - /** @var string[] */ protected $groupDisplayNames = []; - public function __construct(IFactory $languageFactory, - IURLGenerator $url, - IManager $activityManager, - IUserManager $userManager, - ICloudIdManager $cloudIdManager, - IContactsManager $contactsManager, - IEventMerger $eventMerger, - IGroupManager $groupManager) { + public function __construct( + IFactory $languageFactory, + IURLGenerator $url, + IManager $activityManager, + IUserManager $userManager, + ICloudIdManager $cloudIdManager, + IContactsManager $contactsManager, + IEventMerger $eventMerger, + protected IGroupManager $groupManager, + ) { parent::__construct($languageFactory, $url, $activityManager, $userManager, $cloudIdManager, $contactsManager, $eventMerger); - $this->groupManager = $groupManager; } /** @@ -102,7 +82,7 @@ class Groups extends Base { * @throws \InvalidArgumentException * @since 11.0.0 */ - public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { $parsedParameters = $this->getParsedParameters($event); if ($event->getSubject() === self::SUBJECT_SHARED_GROUP_SELF) { diff --git a/apps/files_sharing/lib/Activity/Providers/PublicLinks.php b/apps/files_sharing/lib/Activity/Providers/PublicLinks.php index c09b9baa951..15ffaf2cdb0 100644 --- a/apps/files_sharing/lib/Activity/Providers/PublicLinks.php +++ b/apps/files_sharing/lib/Activity/Providers/PublicLinks.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; @@ -75,7 +58,7 @@ class PublicLinks extends Base { * @throws \InvalidArgumentException * @since 11.0.0 */ - public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { $parsedParameters = $this->getParsedParameters($event); if ($event->getSubject() === self::SUBJECT_SHARED_LINK_SELF) { diff --git a/apps/files_sharing/lib/Activity/Providers/RemoteShares.php b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php index f1cc90f5e65..750d0747b62 100644 --- a/apps/files_sharing/lib/Activity/Providers/RemoteShares.php +++ b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; @@ -40,12 +22,12 @@ class RemoteShares extends Base { public const SUBJECT_REMOTE_SHARE_UNSHARED = 'remote_share_unshared'; public function __construct(IFactory $languageFactory, - IURLGenerator $url, - IManager $activityManager, - IUserManager $userManager, - ICloudIdManager $cloudIdManager, - IContactsManager $contactsManager, - IEventMerger $eventMerger) { + IURLGenerator $url, + IManager $activityManager, + IUserManager $userManager, + ICloudIdManager $cloudIdManager, + IContactsManager $contactsManager, + IEventMerger $eventMerger) { parent::__construct($languageFactory, $url, $activityManager, $userManager, $cloudIdManager, $contactsManager, $eventMerger); } @@ -83,7 +65,7 @@ class RemoteShares extends Base { * @throws \InvalidArgumentException * @since 11.0.0 */ - public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { $parsedParameters = $this->getParsedParameters($event); if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_RECEIVED) { @@ -115,13 +97,14 @@ class RemoteShares extends Base { switch ($subject) { case self::SUBJECT_REMOTE_SHARE_RECEIVED: case self::SUBJECT_REMOTE_SHARE_UNSHARED: + $displayName = (count($parameters) > 2) ? $parameters[2] : ''; return [ 'file' => [ 'type' => 'pending-federated-share', 'id' => $parameters[1], 'name' => $parameters[1], ], - 'user' => $this->getUser($parameters[0]), + 'user' => $this->getUser($parameters[0], $displayName) ]; case self::SUBJECT_REMOTE_SHARE_ACCEPTED: case self::SUBJECT_REMOTE_SHARE_DECLINED: diff --git a/apps/files_sharing/lib/Activity/Providers/Users.php b/apps/files_sharing/lib/Activity/Providers/Users.php index ce873eb5f77..5c833ffae93 100644 --- a/apps/files_sharing/lib/Activity/Providers/Users.php +++ b/apps/files_sharing/lib/Activity/Providers/Users.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Kevin Ndung'u <kevgathuku@gmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Providers; @@ -91,7 +72,7 @@ class Users extends Base { * @throws \InvalidArgumentException * @since 11.0.0 */ - public function parseLongVersion(IEvent $event, IEvent $previousEvent = null) { + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { $parsedParameters = $this->getParsedParameters($event); if ($event->getSubject() === self::SUBJECT_SHARED_USER_SELF) { diff --git a/apps/files_sharing/lib/Activity/Settings/PublicLinks.php b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php index 6d91bf16ce2..0d3d00d2a7b 100644 --- a/apps/files_sharing/lib/Activity/Settings/PublicLinks.php +++ b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Settings; @@ -42,8 +25,8 @@ class PublicLinks extends ShareActivitySettings { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/files_sharing/lib/Activity/Settings/PublicLinksUpload.php b/apps/files_sharing/lib/Activity/Settings/PublicLinksUpload.php new file mode 100644 index 00000000000..fd55752632d --- /dev/null +++ b/apps/files_sharing/lib/Activity/Settings/PublicLinksUpload.php @@ -0,0 +1,67 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Activity\Settings; + +class PublicLinksUpload extends ShareActivitySettings { + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'public_links_upload'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('Files have been <strong>uploaded</strong> to a folder shared by mail or by public link'); + } + + /** + * @return int whether the filter should be rather on the top or bottom of + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * @since 11.0.0 + */ + public function getPriority() { + return 20; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function canChangeStream() { + return true; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function isDefaultEnabledStream() { + return true; + } + + /** + * @return bool True when the option can be changed for the mail + * @since 11.0.0 + */ + public function canChangeMail() { + return true; + } + + /** + * @return bool True when the option can be changed for the stream + * @since 11.0.0 + */ + public function isDefaultEnabledMail() { + return false; + } +} diff --git a/apps/files_sharing/lib/Activity/Settings/RemoteShare.php b/apps/files_sharing/lib/Activity/Settings/RemoteShare.php index 4a796132612..c04364bef20 100644 --- a/apps/files_sharing/lib/Activity/Settings/RemoteShare.php +++ b/apps/files_sharing/lib/Activity/Settings/RemoteShare.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Settings; @@ -43,8 +25,8 @@ class RemoteShare extends ShareActivitySettings { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php b/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php index e7de3dff8ee..4d8d8278433 100644 --- a/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php +++ b/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Settings; @@ -29,21 +12,19 @@ use OCP\Activity\ActivitySettings; use OCP\IL10N; abstract class ShareActivitySettings extends ActivitySettings { - /** @var IL10N */ - protected $l; - /** * @param IL10N $l */ - public function __construct(IL10N $l) { - $this->l = $l; + public function __construct( + protected IL10N $l, + ) { } public function getGroupIdentifier() { - return 'files'; + return 'sharing'; } public function getGroupName() { - return $this->l->t('Files'); + return $this->l->t('Sharing'); } } diff --git a/apps/files_sharing/lib/Activity/Settings/Shared.php b/apps/files_sharing/lib/Activity/Settings/Shared.php index eaef23c1c92..3717512eebd 100644 --- a/apps/files_sharing/lib/Activity/Settings/Shared.php +++ b/apps/files_sharing/lib/Activity/Settings/Shared.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Activity\Settings; @@ -43,8 +25,8 @@ class Shared extends ShareActivitySettings { /** * @return int whether the filter should be rather on the top or bottom of - * the admin section. The filters are arranged in ascending order of the - * priority values. It is required to return a value between 0 and 100. + * the admin section. The filters are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. * @since 11.0.0 */ public function getPriority() { diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php index 2539247b561..8ddb3afaf33 100644 --- a/apps/files_sharing/lib/AppInfo/Application.php +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -1,43 +1,27 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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 OCA\Files_Sharing\AppInfo; +use OC\Group\DisplayNameCache as GroupDisplayNameCache; use OC\Share\Share; use OC\User\DisplayNameCache; +use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCA\Files\Event\LoadSidebar; use OCA\Files_Sharing\Capabilities; -use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; +use OCA\Files_Sharing\Config\ConfigLexicon; use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\MountProvider as ExternalMountProvider; use OCA\Files_Sharing\Helper; -use OCA\Files_Sharing\Listener\LegacyBeforeTemplateRenderedListener; +use OCA\Files_Sharing\Listener\BeforeDirectFileDownloadListener; +use OCA\Files_Sharing\Listener\BeforeNodeReadListener; +use OCA\Files_Sharing\Listener\BeforeZipCreatedListener; use OCA\Files_Sharing\Listener\LoadAdditionalListener; +use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener; use OCA\Files_Sharing\Listener\LoadSidebarListener; use OCA\Files_Sharing\Listener\ShareInteractionListener; use OCA\Files_Sharing\Listener\UserAddedToGroupListener; @@ -48,29 +32,31 @@ use OCA\Files_Sharing\Middleware\SharingCheckMiddleware; use OCA\Files_Sharing\MountProvider; use OCA\Files_Sharing\Notification\Listener; use OCA\Files_Sharing\Notification\Notifier; -use OCA\Files\Event\LoadAdditionalScriptsEvent; -use OCA\Files\Event\LoadSidebar; use OCA\Files_Sharing\ShareBackend\File; use OCA\Files_Sharing\ShareBackend\Folder; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudIdManager; use OCP\Files\Config\IMountProviderCollection; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\Events\Node\BeforeNodeReadEvent; +use OCP\Group\Events\GroupChangedEvent; +use OCP\Group\Events\GroupDeletedEvent; use OCP\Group\Events\UserAddedEvent; use OCP\IDBConnection; use OCP\IGroup; -use OCP\IUserSession; -use OCP\L10N\IFactory; use OCP\Share\Events\ShareCreatedEvent; -use OCP\Share\IManager; use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserDeletedEvent; use OCP\Util; use Psr\Container\ContainerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\EventDispatcher\GenericEvent as OldGenericEvent; class Application extends App implements IBootstrap { public const APP_ID = 'files_sharing'; @@ -101,126 +87,71 @@ class Application extends App implements IBootstrap { $context->registerNotifierService(Notifier::class); $context->registerEventListener(UserChangedEvent::class, DisplayNameCache::class); + $context->registerEventListener(UserDeletedEvent::class, DisplayNameCache::class); + $context->registerEventListener(GroupChangedEvent::class, GroupDisplayNameCache::class); + $context->registerEventListener(GroupDeletedEvent::class, GroupDisplayNameCache::class); + + // Sidebar and files scripts + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); + $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); + $context->registerEventListener(ShareCreatedEvent::class, ShareInteractionListener::class); + $context->registerEventListener(ShareCreatedEvent::class, UserShareAcceptanceListener::class); + $context->registerEventListener(UserAddedEvent::class, UserAddedToGroupListener::class); + + // Publish activity for public download + $context->registerEventListener(BeforeNodeReadEvent::class, BeforeNodeReadListener::class); + $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeNodeReadListener::class); + + // Handle download events for view only checks. Priority higher than 0 to run early. + $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeZipCreatedListener::class, 5); + $context->registerEventListener(BeforeDirectFileDownloadEvent::class, BeforeDirectFileDownloadListener::class, 5); + + // File request auth + $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class); + + $context->registerConfigLexicon(ConfigLexicon::class); } public function boot(IBootContext $context): void { $context->injectFn([$this, 'registerMountProviders']); $context->injectFn([$this, 'registerEventsScripts']); - $context->injectFn([$this, 'setupSharingMenus']); Helper::registerHooks(); Share::registerBackend('file', File::class); Share::registerBackend('folder', Folder::class, 'file'); - - /** - * Always add main sharing script - */ - Util::addScript(self::APP_ID, 'main'); } - public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider) { + public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider): void { $mountProviderCollection->registerProvider($mountProvider); $mountProviderCollection->registerProvider($externalMountProvider); } - public function registerEventsScripts(IEventDispatcher $dispatcher, EventDispatcherInterface $oldDispatcher) { - // sidebar and files scripts - $dispatcher->addServiceListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); - $dispatcher->addServiceListener(BeforeTemplateRenderedEvent::class, LegacyBeforeTemplateRenderedListener::class); - $dispatcher->addServiceListener(LoadSidebar::class, LoadSidebarListener::class); - $dispatcher->addServiceListener(ShareCreatedEvent::class, ShareInteractionListener::class); - $dispatcher->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () { - \OCP\Util::addScript('files_sharing', 'collaboration'); + public function registerEventsScripts(IEventDispatcher $dispatcher): void { + $dispatcher->addListener(ResourcesLoadAdditionalScriptsEvent::class, function (): void { + Util::addScript('files_sharing', 'collaboration'); + }); + $dispatcher->addListener(BeforeTemplateRenderedEvent::class, function (): void { + /** + * Always add main sharing script + */ + Util::addScript(self::APP_ID, 'main'); }); - $dispatcher->addServiceListener(ShareCreatedEvent::class, UserShareAcceptanceListener::class); - $dispatcher->addServiceListener(UserAddedEvent::class, UserAddedToGroupListener::class); // notifications api to accept incoming user shares - $oldDispatcher->addListener('OCP\Share::postShare', function (GenericEvent $event) { + $dispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event): void { /** @var Listener $listener */ $listener = $this->getContainer()->query(Listener::class); $listener->shareNotification($event); }); - $oldDispatcher->addListener(IGroup::class . '::postAddUser', function (GenericEvent $event) { + $dispatcher->addListener(IGroup::class . '::postAddUser', function ($event): void { + if (!$event instanceof OldGenericEvent) { + return; + } /** @var Listener $listener */ $listener = $this->getContainer()->query(Listener::class); $listener->userAddedToGroup($event); }); } - - public function setupSharingMenus(IManager $shareManager, IFactory $l10nFactory, IUserSession $userSession) { - if (!$shareManager->shareApiEnabled() || !class_exists('\OCA\Files\App')) { - return; - } - - $navigationManager = \OCA\Files\App::getNavigationManager(); - // show_Quick_Access stored as string - $navigationManager->add(function () use ($shareManager, $l10nFactory, $userSession) { - $l = $l10nFactory->get('files_sharing'); - $user = $userSession->getUser(); - $userId = $user ? $user->getUID() : null; - - $sharingSublistArray = []; - - if ($shareManager->sharingDisabledForUser($userId) === false) { - $sharingSublistArray[] = [ - 'id' => 'sharingout', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 16, - 'name' => $l->t('Shared with others'), - ]; - } - - $sharingSublistArray[] = [ - 'id' => 'sharingin', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 15, - 'name' => $l->t('Shared with you'), - ]; - - if ($shareManager->sharingDisabledForUser($userId) === false) { - // Check if sharing by link is enabled - if ($shareManager->shareApiAllowLinks()) { - $sharingSublistArray[] = [ - 'id' => 'sharinglinks', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 17, - 'name' => $l->t('Shared by link'), - ]; - } - } - - $sharingSublistArray[] = [ - 'id' => 'deletedshares', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 19, - 'name' => $l->t('Deleted shares'), - ]; - - $sharingSublistArray[] = [ - 'id' => 'pendingshares', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 19, - 'name' => $l->t('Pending shares'), - ]; - - return [ - 'id' => 'shareoverview', - 'appname' => 'files_sharing', - 'script' => 'list.php', - 'order' => 18, - 'name' => $l->t('Shares'), - 'classes' => 'collapsible', - 'sublist' => $sharingSublistArray, - 'expandedState' => 'show_sharing_menu' - ]; - }); - } } diff --git a/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php b/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php index d35f35d20f2..ca4c82c03d7 100644 --- a/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php +++ b/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php @@ -3,45 +3,31 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\BackgroundJob; -use OC\BackgroundJob\TimedJob; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; use OCP\IDBConnection; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMDiscoveryService; use OCP\OCS\IDiscoveryService; +use Psr\Log\LoggerInterface; class FederatedSharesDiscoverJob extends TimedJob { - /** @var IDBConnection */ - private $connection; - /** @var IDiscoveryService */ - private $discoveryService; - public function __construct(IDBConnection $connection, - IDiscoveryService $discoveryService) { - $this->connection = $connection; - $this->discoveryService = $discoveryService; - - $this->setInterval(86400); + public function __construct( + ITimeFactory $time, + private IDBConnection $connection, + private IDiscoveryService $discoveryService, + private IOCMDiscoveryService $ocmDiscoveryService, + private LoggerInterface $logger, + ) { + parent::__construct($time); + $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } public function run($argument) { @@ -50,9 +36,14 @@ class FederatedSharesDiscoverJob extends TimedJob { $qb->selectDistinct('remote') ->from('share_external'); - $result = $qb->execute(); + $result = $qb->executeQuery(); while ($row = $result->fetch()) { $this->discoveryService->discover($row['remote'], 'FEDERATED_SHARING', true); + try { + $this->ocmDiscoveryService->discover($row['remote'], true); + } catch (OCMProviderException $e) { + $this->logger->info('exception while running files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob', ['exception' => $e]); + } } $result->closeCursor(); } diff --git a/apps/files_sharing/lib/Cache.php b/apps/files_sharing/lib/Cache.php index 9f11431008f..f9042fc0765 100644 --- a/apps/files_sharing/lib/Cache.php +++ b/apps/files_sharing/lib/Cache.php @@ -1,46 +1,26 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 OCA\Files_Sharing; +use OC\Files\Cache\CacheDependencies; use OC\Files\Cache\FailedCache; use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; use OC\Files\Storage\Wrapper\Jail; use OC\User\DisplayNameCache; +use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOperator; use OCP\Files\StorageNotAvailableException; -use OCP\ICacheFactory; -use OCP\IUserManager; +use OCP\Share\IShare; /** * Metadata cache for shared files @@ -48,9 +28,6 @@ use OCP\IUserManager; * don't use this class directly if you need to get metadata, use \OC\Files\Filesystem::getFileInfo instead */ class Cache extends CacheJail { - /** @var SharedStorage */ - private $storage; - private ICacheEntry $sourceRootInfo; private bool $rootUnchanged = true; private ?string $ownerDisplayName = null; private $numericId; @@ -59,15 +36,19 @@ class Cache extends CacheJail { /** * @param SharedStorage $storage */ - public function __construct($storage, ICacheEntry $sourceRootInfo, DisplayNameCache $displayNameCache) { - $this->storage = $storage; - $this->sourceRootInfo = $sourceRootInfo; - $this->numericId = $sourceRootInfo->getStorageId(); - $this->displayNameCache = $displayNameCache; + public function __construct( + private $storage, + private ICacheEntry $sourceRootInfo, + CacheDependencies $dependencies, + private IShare $share, + ) { + $this->numericId = $this->sourceRootInfo->getStorageId(); + $this->displayNameCache = $dependencies->getDisplayNameCache(); parent::__construct( null, - '' + '', + $dependencies, ); } @@ -77,22 +58,22 @@ class Cache extends CacheJail { // the sourceRootInfo path is the absolute path of the folder in the "real" storage // in the case where a folder is shared from a Jail we need to ensure that the share Jail - // has it's root set relative to the source Jail + // has its root set relative to the source Jail $currentStorage = $this->storage->getSourceStorage(); if ($currentStorage->instanceOfStorage(Jail::class)) { /** @var Jail $currentStorage */ $absoluteRoot = $currentStorage->getJailedPath($absoluteRoot); } - $this->root = $absoluteRoot; + $this->root = $absoluteRoot ?? ''; } return $this->root; } - protected function getGetUnjailedRoot() { + public function getGetUnjailedRoot(): string { return $this->sourceRootInfo->getPath(); } - public function getCache() { + public function getCache(): ICache { if (is_null($this->cache)) { $sourceStorage = $this->storage->getSourceStorage(); if ($sourceStorage) { @@ -109,7 +90,7 @@ class Cache extends CacheJail { if (isset($this->numericId)) { return $this->numericId; } else { - return false; + return -1; } } @@ -135,7 +116,7 @@ class Cache extends CacheJail { parent::remove($file); } - public function moveFromCache(\OCP\Files\Cache\ICache $sourceCache, $sourcePath, $targetPath) { + public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) { $this->rootUnchanged = false; return parent::moveFromCache($sourceCache, $sourcePath, $targetPath); } @@ -150,16 +131,20 @@ class Cache extends CacheJail { try { if (isset($entry['permissions'])) { - $entry['permissions'] &= $this->storage->getShare()->getPermissions(); + $entry['permissions'] &= $this->share->getPermissions(); } else { $entry['permissions'] = $this->storage->getPermissions($entry['path']); } + + if ($this->share->getNodeId() === $entry['fileid']) { + $entry['name'] = basename($this->share->getTarget()); + } } catch (StorageNotAvailableException $e) { // thrown by FailedStorage e.g. when the sharer does not exist anymore // (IDE may say the exception is never thrown – false negative) $sharePermissions = 0; } - $entry['uid_owner'] = $this->storage->getOwner(''); + $entry['uid_owner'] = $this->share->getShareOwner(); $entry['displayname_owner'] = $this->getOwnerDisplayName(); if ($path === '') { $entry['is_share_mount_point'] = true; @@ -169,8 +154,8 @@ class Cache extends CacheJail { private function getOwnerDisplayName() { if (!$this->ownerDisplayName) { - $uid = $this->storage->getOwner(''); - $this->ownerDisplayName = $this->displayNameCache->getDisplayName($uid); + $uid = $this->share->getShareOwner(); + $this->ownerDisplayName = $this->displayNameCache->getDisplayName($uid) ?? $uid; } return $this->ownerDisplayName; } @@ -183,18 +168,32 @@ class Cache extends CacheJail { } public function getQueryFilterForStorage(): ISearchOperator { + $storageFilter = \OC\Files\Cache\Cache::getQueryFilterForStorage(); + // Do the normal jail behavior for non files if ($this->storage->getItemType() !== 'file') { - return parent::getQueryFilterForStorage(); + return $this->addJailFilterQuery($storageFilter); } // for single file shares we don't need to do the LIKE return new SearchBinaryOperator( ISearchBinaryOperator::OPERATOR_AND, [ - \OC\Files\Cache\Cache::getQueryFilterForStorage(), + $storageFilter, new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', $this->getGetUnjailedRoot()), ] ); } + + public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry { + if ($rawEntry->getStorageId() === $this->getNumericStorageId()) { + return parent::getCacheEntryFromSearchResult($rawEntry); + } else { + return null; + } + } + + public function markRootChanged(): void { + $this->rootUnchanged = false; + } } diff --git a/apps/files_sharing/lib/Capabilities.php b/apps/files_sharing/lib/Capabilities.php index c5421fe779a..06aa1271c8f 100644 --- a/apps/files_sharing/lib/Capabilities.php +++ b/apps/files_sharing/lib/Capabilities.php @@ -1,33 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Julius Härtl <jus@bitgrid.net> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * @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 OCA\Files_Sharing; +use OC\Core\AppInfo\ConfigLexicon; +use OCP\App\IAppManager; use OCP\Capabilities\ICapability; use OCP\Constants; +use OCP\IAppConfig; use OCP\IConfig; use OCP\Share\IManager; @@ -37,21 +21,78 @@ use OCP\Share\IManager; * @package OCA\Files_Sharing */ class Capabilities implements ICapability { - - /** @var IConfig */ - private $config; - /** @var IManager */ - private $shareManager; - - public function __construct(IConfig $config, IManager $shareManager) { - $this->config = $config; - $this->shareManager = $shareManager; + public function __construct( + private IConfig $config, + private readonly IAppConfig $appConfig, + private IManager $shareManager, + private IAppManager $appManager, + ) { } /** * Return this classes capabilities * - * @return array + * @return array{ + * files_sharing: array{ + * api_enabled: bool, + * public: array{ + * enabled: bool, + * password?: array{ + * enforced: bool, + * askForOptionalPassword: bool + * }, + * multiple_links?: bool, + * expire_date?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * expire_date_internal?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * expire_date_remote?: array{ + * enabled: bool, + * days?: int, + * enforced?: bool, + * }, + * send_mail?: bool, + * upload?: bool, + * upload_files_drop?: bool, + * custom_tokens?: bool, + * }, + * user: array{ + * send_mail: bool, + * expire_date?: array{ + * enabled: bool, + * }, + * }, + * resharing: bool, + * group_sharing?: bool, + * group?: array{ + * enabled: bool, + * expire_date?: array{ + * enabled: bool, + * }, + * }, + * default_permissions?: int, + * federation: array{ + * outgoing: bool, + * incoming: bool, + * expire_date: array{ + * enabled: bool, + * }, + * expire_date_supported: array{ + * enabled: bool, + * }, + * }, + * sharee: array{ + * query_lookup_default: bool, + * always_show_unique: bool, + * }, + * }, + * } */ public function getCapabilities() { $res = []; @@ -73,7 +114,7 @@ class Capabilities implements ICapability { if ($public['password']['enforced']) { $public['password']['askForOptionalPassword'] = false; } else { - $public['password']['askForOptionalPassword'] = ($this->config->getAppValue('core', 'shareapi_enable_link_password_by_default', 'no') === 'yes'); + $public['password']['askForOptionalPassword'] = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT); } $public['expire_date'] = []; @@ -101,6 +142,7 @@ class Capabilities implements ICapability { $public['send_mail'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no') === 'yes'; $public['upload'] = $this->shareManager->shareApiLinkAllowPublicUpload(); $public['upload_files_drop'] = $public['upload']; + $public['custom_tokens'] = $this->shareManager->allowCustomTokens(); } $res['public'] = $public; @@ -116,18 +158,27 @@ class Capabilities implements ICapability { $res['group'] = []; $res['group']['enabled'] = $this->shareManager->allowGroupSharing(); $res['group']['expire_date']['enabled'] = true; - $res['default_permissions'] = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', Constants::PERMISSION_ALL); + $res['default_permissions'] = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL); } //Federated sharing - $res['federation'] = [ - 'outgoing' => $this->shareManager->outgoingServer2ServerSharesAllowed(), - 'incoming' => $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'yes', - // old bogus one, expire_date was not working before, keeping for compatibility - 'expire_date' => ['enabled' => true], - // the real deal, signifies that expiration date can be set on federated shares - 'expire_date_supported' => ['enabled' => true], - ]; + if ($this->appManager->isEnabledForAnyone('federation')) { + $res['federation'] = [ + 'outgoing' => $this->shareManager->outgoingServer2ServerSharesAllowed(), + 'incoming' => $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'yes', + // old bogus one, expire_date was not working before, keeping for compatibility + 'expire_date' => ['enabled' => true], + // the real deal, signifies that expiration date can be set on federated shares + 'expire_date_supported' => ['enabled' => true], + ]; + } else { + $res['federation'] = [ + 'outgoing' => false, + 'incoming' => false, + 'expire_date' => ['enabled' => false], + 'expire_date_supported' => ['enabled' => false], + ]; + } // Sharee searches $res['sharee'] = [ diff --git a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php index 28d0d26c5be..803dfd6325f 100644 --- a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php +++ b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php @@ -1,30 +1,12 @@ <?php + /** - * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Collaboration; use OCP\Collaboration\AutoComplete\ISorter; -use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\IUserSession; @@ -32,20 +14,14 @@ use OCP\Share\IManager; class ShareRecipientSorter implements ISorter { - /** @var IManager */ - private $shareManager; - /** @var Folder */ - private $rootFolder; - /** @var IUserSession */ - private $userSession; - - public function __construct(IManager $shareManager, IRootFolder $rootFolder, IUserSession $userSession) { - $this->shareManager = $shareManager; - $this->rootFolder = $rootFolder; - $this->userSession = $userSession; + public function __construct( + private IManager $shareManager, + private IRootFolder $rootFolder, + private IUserSession $userSession, + ) { } - public function getId() { + public function getId(): string { return 'share-recipients'; } @@ -60,11 +36,11 @@ class ShareRecipientSorter implements ISorter { } $userFolder = $this->rootFolder->getUserFolder($user->getUID()); /** @var Node[] $nodes */ - $nodes = $userFolder->getById((int)$context['itemId']); - if (count($nodes) === 0) { + $node = $userFolder->getFirstNodeById((int)$context['itemId']); + if (!$node) { return; } - $al = $this->shareManager->getAccessList($nodes[0]); + $al = $this->shareManager->getAccessList($node); foreach ($sortArray as $type => &$byType) { if (!isset($al[$type]) || !is_array($al[$type])) { diff --git a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php index 3816a2a5124..809481e5c0f 100644 --- a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php +++ b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Command; @@ -39,19 +21,10 @@ use Symfony\Component\Console\Output\OutputInterface; */ class CleanupRemoteStorages extends Command { - /** - * @var IDBConnection - */ - protected $connection; - - /** - * @var ICloudIdManager - */ - private $cloudIdManager; - - public function __construct(IDBConnection $connection, ICloudIdManager $cloudIdManager) { - $this->connection = $connection; - $this->cloudIdManager = $cloudIdManager; + public function __construct( + protected IDBConnection $connection, + private ICloudIdManager $cloudIdManager, + ) { parent::__construct(); } @@ -113,8 +86,9 @@ class CleanupRemoteStorages extends Command { $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR) ); - $result = $queryBuilder->execute(); + $result = $queryBuilder->executeQuery(); $count = $result->fetchOne(); + $result->closeCursor(); $output->writeln("$count files can be deleted for storage $numericId"); } @@ -127,7 +101,7 @@ class CleanupRemoteStorages extends Command { IQueryBuilder::PARAM_STR) ); $output->write("deleting $id [$numericId] ... "); - $count = $queryBuilder->execute(); + $count = $queryBuilder->executeStatement(); $output->writeln("deleted $count storage"); $this->deleteFiles($numericId, $output); } @@ -141,7 +115,7 @@ class CleanupRemoteStorages extends Command { IQueryBuilder::PARAM_STR) ); $output->write("deleting files for storage $numericId ... "); - $count = $queryBuilder->execute(); + $count = $queryBuilder->executeStatement(); $output->writeln("deleted $count files"); } @@ -160,14 +134,16 @@ class CleanupRemoteStorages extends Command { // but not the ones starting with a '/', they are for normal shares $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::/') . '%'), IQueryBuilder::PARAM_STR) - )->orderBy('numeric_id'); - $query = $queryBuilder->execute(); + ) + ->orderBy('numeric_id'); + $result = $queryBuilder->executeQuery(); $remoteStorages = []; - while ($row = $query->fetch()) { + while ($row = $result->fetch()) { $remoteStorages[$row['id']] = $row['numeric_id']; } + $result->closeCursor(); return $remoteStorages; } @@ -176,16 +152,17 @@ class CleanupRemoteStorages extends Command { $queryBuilder = $this->connection->getQueryBuilder(); $queryBuilder->select(['id', 'share_token', 'owner', 'remote']) ->from('share_external'); - $query = $queryBuilder->execute(); + $result = $queryBuilder->executeQuery(); $remoteShareIds = []; - while ($row = $query->fetch()) { + while ($row = $result->fetch()) { $cloudId = $this->cloudIdManager->getCloudId($row['owner'], $row['remote']); $remote = $cloudId->getRemote(); $remoteShareIds[$row['id']] = 'shared::' . md5($row['share_token'] . '@' . $remote); } + $result->closeCursor(); return $remoteShareIds; } diff --git a/apps/files_sharing/lib/Command/DeleteOrphanShares.php b/apps/files_sharing/lib/Command/DeleteOrphanShares.php new file mode 100644 index 00000000000..a7e96387d60 --- /dev/null +++ b/apps/files_sharing/lib/Command/DeleteOrphanShares.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +use OC\Core\Command\Base; +use OCA\Files_Sharing\OrphanHelper; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class DeleteOrphanShares extends Base { + public function __construct( + private OrphanHelper $orphanHelper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('sharing:delete-orphan-shares') + ->setDescription('Delete shares where the owner no longer has access to the file') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'delete the shares without asking' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $force = $input->getOption('force'); + $shares = $this->orphanHelper->getAllShares(); + + $orphans = []; + foreach ($shares as $share) { + if (!$this->orphanHelper->isShareValid($share['owner'], $share['fileid'])) { + $orphans[] = $share['id']; + $exists = $this->orphanHelper->fileExists($share['fileid']); + $output->writeln("<info>{$share['target']}</info> owned by <info>{$share['owner']}</info>"); + if ($exists) { + $output->writeln(" file still exists but the share owner lost access to it, run <info>occ info:file {$share['fileid']}</info> for more information about the file"); + } else { + $output->writeln(' file no longer exists'); + } + } + } + + $count = count($orphans); + + if ($count === 0) { + $output->writeln('No orphan shares detected'); + return 0; + } + + if ($force) { + $doDelete = true; + } else { + $output->writeln(''); + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion("Delete <info>$count</info> orphan shares? [y/N] ", false); + $doDelete = $helper->ask($input, $output, $question); + } + + if ($doDelete) { + $this->orphanHelper->deleteShares($orphans); + } + + return 0; + } +} diff --git a/apps/files_sharing/lib/Command/ExiprationNotification.php b/apps/files_sharing/lib/Command/ExiprationNotification.php index e77b41b1835..b7ea5c5f14e 100644 --- a/apps/files_sharing/lib/Command/ExiprationNotification.php +++ b/apps/files_sharing/lib/Command/ExiprationNotification.php @@ -3,29 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Command; +use OCA\Files_Sharing\OrphanHelper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IDBConnection; use OCP\Notification\IManager as NotificationManager; @@ -36,25 +19,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ExiprationNotification extends Command { - /** @var NotificationManager */ - private $notificationManager; - /** @var IDBConnection */ - private $connection; - /** @var ITimeFactory */ - private $time; - /** @var ShareManager */ - private $shareManager; - - public function __construct(ITimeFactory $time, - NotificationManager $notificationManager, - IDBConnection $connection, - ShareManager $shareManager) { + public function __construct( + private ITimeFactory $time, + private NotificationManager $notificationManager, + private IDBConnection $connection, + private ShareManager $shareManager, + private OrphanHelper $orphanHelper, + ) { parent::__construct(); - - $this->notificationManager = $notificationManager; - $this->connection = $connection; - $this->time = $time; - $this->shareManager = $shareManager; } protected function configure() { @@ -67,7 +39,7 @@ class ExiprationNotification extends Command { //Current time $minTime = $this->time->getDateTime(); $minTime->add(new \DateInterval('P1D')); - $minTime->setTime(0,0,0); + $minTime->setTime(0, 0, 0); $maxTime = clone $minTime; $maxTime->setTime(23, 59, 59); @@ -80,7 +52,8 @@ class ExiprationNotification extends Command { foreach ($shares as $share) { if ($share->getExpirationDate() === null || $share->getExpirationDate()->getTimestamp() < $minTime->getTimestamp() - || $share->getExpirationDate()->getTimestamp() > $maxTime->getTimestamp()) { + || $share->getExpirationDate()->getTimestamp() > $maxTime->getTimestamp() + || !$this->orphanHelper->isShareValid($share->getSharedBy(), $share->getNodeId())) { continue; } diff --git a/apps/files_sharing/lib/Command/FixShareOwners.php b/apps/files_sharing/lib/Command/FixShareOwners.php new file mode 100644 index 00000000000..1cf5f82f5a8 --- /dev/null +++ b/apps/files_sharing/lib/Command/FixShareOwners.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +use OC\Core\Command\Base; +use OCA\Files_Sharing\OrphanHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class FixShareOwners extends Base { + public function __construct( + private readonly OrphanHelper $orphanHelper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('sharing:fix-share-owners') + ->setDescription('Fix owner of broken shares after transfer ownership on old versions') + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'only show which shares would be updated' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $shares = $this->orphanHelper->getAllShares(); + $dryRun = $input->getOption('dry-run'); + $count = 0; + + foreach ($shares as $share) { + if ($this->orphanHelper->isShareValid($share['owner'], $share['fileid']) || !$this->orphanHelper->fileExists($share['fileid'])) { + continue; + } + + $owner = $this->orphanHelper->findOwner($share['fileid']); + + if ($owner !== null) { + if ($dryRun) { + $output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) can be updated to owner <info>$owner</info>"); + } else { + $this->orphanHelper->updateShareOwner($share['id'], $owner); + $output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) updated to owner <info>$owner</info>"); + } + $count++; + } + } + + if ($count === 0) { + $output->writeln('No broken shares detected'); + } + + return static::SUCCESS; + } +} diff --git a/apps/files_sharing/lib/Command/ListShares.php b/apps/files_sharing/lib/Command/ListShares.php new file mode 100644 index 00000000000..2d5cdbf7812 --- /dev/null +++ b/apps/files_sharing/lib/Command/ListShares.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +use OC\Core\Command\Base; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListShares extends Base { + /** @var array<string, Node> */ + private array $fileCache = []; + + private const SHARE_TYPE_NAMES = [ + IShare::TYPE_USER => 'user', + IShare::TYPE_GROUP => 'group', + IShare::TYPE_LINK => 'link', + IShare::TYPE_EMAIL => 'email', + IShare::TYPE_REMOTE => 'remote', + IShare::TYPE_REMOTE_GROUP => 'group', + IShare::TYPE_ROOM => 'room', + IShare::TYPE_DECK => 'deck', + ]; + + public function __construct( + private readonly IManager $shareManager, + private readonly IRootFolder $rootFolder, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this + ->setName('share:list') + ->setDescription('List available shares') + ->addOption('owner', null, InputOption::VALUE_REQUIRED, 'only show shares owned by a specific user') + ->addOption('recipient', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific recipient') + ->addOption('by', null, InputOption::VALUE_REQUIRED, 'only show shares with by as specific user') + ->addOption('file', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific file') + ->addOption('parent', null, InputOption::VALUE_REQUIRED, 'only show shares of files inside a specific folder') + ->addOption('recursive', null, InputOption::VALUE_NONE, 'also show shares nested deep inside the specified parent folder') + ->addOption('type', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific type') + ->addOption('status', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific status'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + if ($input->getOption('recursive') && !$input->getOption('parent')) { + $output->writeln("<error>recursive option can't be used without parent option</error>"); + return 1; + } + + // todo: do some pre-filtering instead of first querying all shares + /** @var \Iterator<IShare> $allShares */ + $allShares = $this->shareManager->getAllShares(); + $shares = new \CallbackFilterIterator($allShares, function (IShare $share) use ($input) { + return $this->shouldShowShare($input, $share); + }); + $shares = iterator_to_array($shares); + $data = array_map(function (IShare $share) { + return [ + 'id' => $share->getId(), + 'file' => $share->getNodeId(), + 'target-path' => $share->getTarget(), + 'source-path' => $share->getNode()->getPath(), + 'owner' => $share->getShareOwner(), + 'recipient' => $share->getSharedWith(), + 'by' => $share->getSharedBy(), + 'type' => self::SHARE_TYPE_NAMES[$share->getShareType()] ?? 'unknown', + ]; + }, $shares); + + $this->writeTableInOutputFormat($input, $output, $data); + return 0; + } + + private function getFileId(string $file): int { + if (is_numeric($file)) { + return (int)$file; + } + return $this->getFile($file)->getId(); + } + + private function getFile(string $file): Node { + if (isset($this->fileCache[$file])) { + return $this->fileCache[$file]; + } + + if (is_numeric($file)) { + $node = $this->rootFolder->getFirstNodeById((int)$file); + if (!$node) { + throw new NotFoundException("File with id $file not found"); + } + } else { + $node = $this->rootFolder->get($file); + } + $this->fileCache[$file] = $node; + return $node; + } + + private function getShareType(string $type): int { + foreach (self::SHARE_TYPE_NAMES as $shareType => $shareTypeName) { + if ($shareTypeName === $type) { + return $shareType; + } + } + throw new \Exception("Unknown share type $type"); + } + + private function shouldShowShare(InputInterface $input, IShare $share): bool { + if ($input->getOption('owner') && $share->getShareOwner() !== $input->getOption('owner')) { + return false; + } + if ($input->getOption('recipient') && $share->getSharedWith() !== $input->getOption('recipient')) { + return false; + } + if ($input->getOption('by') && $share->getSharedBy() !== $input->getOption('by')) { + return false; + } + if ($input->getOption('file') && $share->getNodeId() !== $this->getFileId($input->getOption('file'))) { + return false; + } + if ($input->getOption('parent')) { + $parent = $this->getFile($input->getOption('parent')); + if (!$parent instanceof Folder) { + throw new \Exception("Parent {$parent->getPath()} is not a folder"); + } + $recursive = $input->getOption('recursive'); + if (!$recursive) { + $shareCacheEntry = $share->getNodeCacheEntry(); + if (!$shareCacheEntry) { + $shareCacheEntry = $share->getNode(); + } + if ($shareCacheEntry->getParentId() !== $parent->getId()) { + return false; + } + } else { + $shareNode = $share->getNode(); + if ($parent->getRelativePath($shareNode->getPath()) === null) { + return false; + } + } + } + if ($input->getOption('type') && $share->getShareType() !== $this->getShareType($input->getOption('type'))) { + return false; + } + return true; + } +} diff --git a/apps/files_sharing/lib/Config/ConfigLexicon.php b/apps/files_sharing/lib/Config/ConfigLexicon.php new file mode 100644 index 00000000000..c2743a2c4ce --- /dev/null +++ b/apps/files_sharing/lib/Config/ConfigLexicon.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Config; + +use OCP\Config\Lexicon\Entry; +use OCP\Config\Lexicon\ILexicon; +use OCP\Config\Lexicon\Strictness; +use OCP\Config\ValueType; + +/** + * Config Lexicon for files_sharing. + * + * Please Add & Manage your Config Keys in that file and keep the Lexicon up to date! + * + * {@see ILexicon} + */ +class ConfigLexicon implements ILexicon { + public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal'; + public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal'; + + public function getStrictness(): Strictness { + return Strictness::IGNORE; + } + + public function getAppConfigs(): array { + return [ + new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true), + new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true), + ]; + } + + public function getUserConfigs(): array { + return []; + } +} diff --git a/apps/files_sharing/lib/Controller/AcceptController.php b/apps/files_sharing/lib/Controller/AcceptController.php index c97780d6f0c..721ddec7d2b 100644 --- a/apps/files_sharing/lib/Controller/AcceptController.php +++ b/apps/files_sharing/lib/Controller/AcceptController.php @@ -3,30 +3,16 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Controller; use OCA\Files_Sharing\AppInfo\Application; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\Response; @@ -36,29 +22,20 @@ use OCP\IUserSession; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class AcceptController extends Controller { - /** @var ShareManager */ - private $shareManager; - - /** @var IUserSession */ - private $userSession; - - /** @var IURLGenerator */ - private $urlGenerator; - - public function __construct(IRequest $request, ShareManager $shareManager, IUserSession $userSession, IURLGenerator $urlGenerator) { + public function __construct( + IRequest $request, + private ShareManager $shareManager, + private IUserSession $userSession, + private IURLGenerator $urlGenerator, + ) { parent::__construct(Application::APP_ID, $request); - - $this->shareManager = $shareManager; - $this->userSession = $userSession; - $this->urlGenerator = $urlGenerator; } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ + #[NoAdminRequired] + #[NoCSRFRequired] public function accept(string $shareId): Response { try { $share = $this->shareManager->getShareById($shareId); diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php index 1d625b35322..2fa4d7c668f 100644 --- a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php +++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php @@ -3,95 +3,55 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Controller; +use OCA\Deck\Sharing\ShareAPIHelper; +use OCA\Files_Sharing\ResponseDefinitions; use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\AppFramework\QueryException; +use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\IGroupManager; use OCP\IRequest; -use OCP\IServerContainer; use OCP\IUserManager; +use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; +/** + * @psalm-import-type Files_SharingDeletedShare from ResponseDefinitions + */ class DeletedShareAPIController extends OCSController { - /** @var ShareManager */ - private $shareManager; - - /** @var string */ - private $userId; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IAppManager */ - private $appManager; - - /** @var IServerContainer */ - private $serverContainer; - - public function __construct(string $appName, - IRequest $request, - ShareManager $shareManager, - string $UserId, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IAppManager $appManager, - IServerContainer $serverContainer) { + public function __construct( + string $appName, + IRequest $request, + private ShareManager $shareManager, + private ?string $userId, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IRootFolder $rootFolder, + private IAppManager $appManager, + ) { parent::__construct($appName, $request); - - $this->shareManager = $shareManager; - $this->userId = $UserId; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; - $this->appManager = $appManager; - $this->serverContainer = $serverContainer; } /** * @suppress PhanUndeclaredClassMethod + * + * @return Files_SharingDeletedShare */ private function formatShare(IShare $share): array { $result = [ @@ -109,19 +69,17 @@ class DeletedShareAPIController extends OCSController { 'path' => $share->getTarget(), ]; $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $nodes = $userFolder->getById($share->getNodeId()); - if (empty($nodes)) { + $node = $userFolder->getFirstNodeById($share->getNodeId()); + if (!$node) { // fallback to guessing the path $node = $userFolder->get($share->getTarget()); if ($node === null || $share->getTarget() === '') { throw new NotFoundException(); } - } else { - $node = $nodes[0]; } $result['path'] = $userFolder->getRelativePath($node->getPath()); - if ($node instanceof \OCP\Files\Folder) { + if ($node instanceof Folder) { $result['item_type'] = 'folder'; } else { $result['item_type'] = 'file'; @@ -133,6 +91,8 @@ class DeletedShareAPIController extends OCSController { $result['file_source'] = $node->getId(); $result['file_parent'] = $node->getParent()->getId(); $result['file_target'] = $share->getTarget(); + $result['item_size'] = $node->getSize(); + $result['item_mtime'] = $node->getMTime(); $expiration = $share->getExpirationDate(); if ($expiration !== null) { @@ -159,33 +119,54 @@ class DeletedShareAPIController extends OCSController { $result = array_merge($result, $this->getDeckShareHelper()->formatShare($share)); } catch (QueryException $e) { } + } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = ''; + + try { + $result = array_merge($result, $this->getSciencemeshShareHelper()->formatShare($share)); + } catch (QueryException $e) { + } } return $result; } /** - * @NoAdminRequired + * Get a list of all deleted shares + * + * @return DataResponse<Http::STATUS_OK, list<Files_SharingDeletedShare>, array{}> + * + * 200: Deleted shares returned */ + #[NoAdminRequired] public function index(): DataResponse { $groupShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_GROUP, null, -1, 0); + $teamShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_CIRCLE, null, -1, 0); $roomShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_ROOM, null, -1, 0); $deckShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_DECK, null, -1, 0); + $sciencemeshShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_SCIENCEMESH, null, -1, 0); - $shares = array_merge($groupShares, $roomShares, $deckShares); + $shares = array_merge($groupShares, $teamShares, $roomShares, $deckShares, $sciencemeshShares); - $shares = array_map(function (IShare $share) { + $shares = array_values(array_map(function (IShare $share) { return $this->formatShare($share); - }, $shares); + }, $shares)); return new DataResponse($shares); } /** - * @NoAdminRequired + * Undelete a deleted share * + * @param string $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> * @throws OCSException + * @throws OCSNotFoundException Share not found + * + * 200: Share undeleted successfully */ + #[NoAdminRequired] public function undelete(string $id): DataResponse { try { $share = $this->shareManager->getShareById($id, $this->userId); @@ -220,16 +201,16 @@ class DeletedShareAPIController extends OCSController { throw new QueryException(); } - return $this->serverContainer->get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); + return Server::get('\OCA\Talk\Share\Helper\DeletedShareAPIController'); } /** - * Returns the helper of ShareAPIHelper for deck shares. + * Returns the helper of DeletedShareAPIHelper for deck shares. * * If the Deck application is not enabled or the helper is not available * a QueryException is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper + * @return ShareAPIHelper * @throws QueryException */ private function getDeckShareHelper() { @@ -237,6 +218,23 @@ class DeletedShareAPIController extends OCSController { throw new QueryException(); } - return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper'); + return Server::get('\OCA\Deck\Sharing\ShareAPIHelper'); + } + + /** + * Returns the helper of DeletedShareAPIHelper for sciencemesh shares. + * + * If the sciencemesh application is not enabled or the helper is not available + * a QueryException is thrown instead. + * + * @return ShareAPIHelper + * @throws QueryException + */ + private function getSciencemeshShareHelper() { + if (!$this->appManager->isEnabledForUser('sciencemesh')) { + throw new QueryException(); + } + + return Server::get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); } } diff --git a/apps/files_sharing/lib/Controller/ExternalSharesController.php b/apps/files_sharing/lib/Controller/ExternalSharesController.php index 9fab8d4e1a0..fa828a9d2c2 100644 --- a/apps/files_sharing/lib/Controller/ExternalSharesController.php +++ b/apps/files_sharing/lib/Controller/ExternalSharesController.php @@ -1,34 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Controller; use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\JSONResponse; -use OCP\Http\Client\IClientService; use OCP\IRequest; /** @@ -37,116 +18,45 @@ use OCP\IRequest; * @package OCA\Files_Sharing\Controller */ class ExternalSharesController extends Controller { - - /** @var \OCA\Files_Sharing\External\Manager */ - private $externalManager; - /** @var IClientService */ - private $clientService; - - /** - * @param string $appName - * @param IRequest $request - * @param \OCA\Files_Sharing\External\Manager $externalManager - * @param IClientService $clientService - */ - public function __construct($appName, - IRequest $request, - \OCA\Files_Sharing\External\Manager $externalManager, - IClientService $clientService) { + public function __construct( + string $appName, + IRequest $request, + private \OCA\Files_Sharing\External\Manager $externalManager, + ) { parent::__construct($appName, $request); - $this->externalManager = $externalManager; - $this->clientService = $clientService; } /** - * @NoAdminRequired * @NoOutgoingFederatedSharingRequired * * @return JSONResponse */ + #[NoAdminRequired] public function index() { return new JSONResponse($this->externalManager->getOpenShares()); } /** - * @NoAdminRequired * @NoOutgoingFederatedSharingRequired * * @param int $id * @return JSONResponse */ + #[NoAdminRequired] public function create($id) { $this->externalManager->acceptShare($id); return new JSONResponse(); } /** - * @NoAdminRequired * @NoOutgoingFederatedSharingRequired * * @param integer $id * @return JSONResponse */ + #[NoAdminRequired] public function destroy($id) { $this->externalManager->declineShare($id); return new JSONResponse(); } - - /** - * Test whether the specified remote is accessible - * - * @param string $remote - * @param bool $checkVersion - * @return bool - */ - protected function testUrl($remote, $checkVersion = false) { - try { - $client = $this->clientService->newClient(); - $response = json_decode($client->get( - $remote, - [ - 'timeout' => 3, - 'connect_timeout' => 3, - ] - )->getBody()); - - if ($checkVersion) { - return !empty($response->version) && version_compare($response->version, '7.0.0', '>='); - } else { - return is_object($response); - } - } catch (\Exception $e) { - return false; - } - } - - /** - * @PublicPage - * @NoOutgoingFederatedSharingRequired - * @NoIncomingFederatedSharingRequired - * - * @param string $remote - * @return DataResponse - */ - public function testRemote($remote) { - if (strpos($remote, '#') !== false || strpos($remote, '?') !== false || strpos($remote, ';') !== false) { - return new DataResponse(false); - } - - if ( - $this->testUrl('https://' . $remote . '/ocs-provider/') || - $this->testUrl('https://' . $remote . '/ocs-provider/index.php') || - $this->testUrl('https://' . $remote . '/status.php', true) - ) { - return new DataResponse('https'); - } elseif ( - $this->testUrl('http://' . $remote . '/ocs-provider/') || - $this->testUrl('http://' . $remote . '/ocs-provider/index.php') || - $this->testUrl('http://' . $remote . '/status.php', true) - ) { - return new DataResponse('http'); - } else { - return new DataResponse(false); - } - } } diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php index 4a16afa7ac0..d917f6e0ebb 100644 --- a/apps/files_sharing/lib/Controller/PublicPreviewController.php +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -1,32 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\PublicShareController; use OCP\Constants; use OCP\Files\Folder; @@ -34,33 +20,28 @@ use OCP\Files\NotFoundException; use OCP\IPreview; use OCP\IRequest; use OCP\ISession; +use OCP\Preview\IMimeIconProvider; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; class PublicPreviewController extends PublicShareController { - /** @var ShareManager */ - private $shareManager; - - /** @var IPreview */ - private $previewManager; - /** @var IShare */ private $share; - public function __construct(string $appName, - IRequest $request, - ShareManager $shareManger, - ISession $session, - IPreview $previewManager) { + public function __construct( + string $appName, + IRequest $request, + private ShareManager $shareManager, + ISession $session, + private IPreview $previewManager, + private IMimeIconProvider $mimeIconProvider, + ) { parent::__construct($appName, $request, $session); - - $this->shareManager = $shareManger; - $this->previewManager = $previewManager; } - protected function getPasswordHash(): string { + protected function getPasswordHash(): ?string { return $this->share->getPassword(); } @@ -79,22 +60,35 @@ class PublicPreviewController extends PublicShareController { /** - * @PublicPage - * @NoCSRFRequired + * Get a preview for a shared file * - * @param string $file - * @param int $x - * @param int $y - * @param bool $a - * @return DataResponse|FileDisplayResponse + * @param string $token Token of the share + * @param string $file File in the share + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param bool $a Whether to not crop the preview + * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * + * 200: Preview returned + * 303: Redirect to the mime icon url if mimeFallback is true + * 400: Getting preview is not possible + * 403: Getting preview is not allowed + * 404: Share or preview not found */ + #[PublicPage] + #[NoCSRFRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getPreview( string $token, string $file = '', int $x = 32, int $y = 32, - $a = false + $a = false, + bool $mimeFallback = false, ) { + $cacheForSeconds = 60 * 60 * 24; // 1 day + if ($token === '' || $x === 0 || $y === 0) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -109,6 +103,21 @@ class PublicPreviewController extends PublicShareController { return new DataResponse([], Http::STATUS_FORBIDDEN); } + // Only explicitly set to false will forbid the download! + $downloadForbidden = !$share->canSeeContent(); + + // Is this header is set it means our UI is doing a preview for no-download shares + // we check a header so we at least prevent people from using the link directly (obfuscation) + $isPublicPreview = $this->request->getHeader('x-nc-preview') === 'true'; + + if ($isPublicPreview && $downloadForbidden) { + // Only cache for 15 minutes on public preview requests to quickly remove from cache + $cacheForSeconds = 15 * 60; + } elseif ($downloadForbidden) { + // This is not a public share preview so we only allow a preview if download permissions are granted + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + try { $node = $share->getNode(); if ($node instanceof Folder) { @@ -119,9 +128,15 @@ class PublicPreviewController extends PublicShareController { $f = $this->previewManager->getPreview($file, $x, $y, !$a); $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]); - $response->cacheFor(3600 * 24); + $response->cacheFor($cacheForSeconds); return $response; } catch (NotFoundException $e) { + // If we have no preview enabled, we can redirect to the mime icon if any + if ($mimeFallback) { + if ($url = $this->mimeIconProvider->getMimeIconUrl($file->getMimeType())) { + return new RedirectResponse($url); + } + } return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -129,14 +144,22 @@ class PublicPreviewController extends PublicShareController { } /** - * @PublicPage - * @NoCSRFRequired * @NoSameSiteCookieRequired * - * @param $token - * @return DataResponse|FileDisplayResponse + * Get a direct link preview for a shared file + * + * @param string $token Token of the share + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}> + * + * 200: Preview returned + * 400: Getting preview is not possible + * 403: Getting preview is not allowed + * 404: Share or preview not found */ - public function directLink($token) { + #[PublicPage] + #[NoCSRFRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function directLink(string $token) { // No token no image if ($token === '') { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -159,6 +182,10 @@ class PublicPreviewController extends PublicShareController { return new DataResponse([], Http::STATUS_FORBIDDEN); } + if (!$share->canSeeContent()) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + try { $node = $share->getNode(); if ($node instanceof Folder) { diff --git a/apps/files_sharing/lib/Controller/RemoteController.php b/apps/files_sharing/lib/Controller/RemoteController.php index 75684220c52..8c15cd8463e 100644 --- a/apps/files_sharing/lib/Controller/RemoteController.php +++ b/apps/files_sharing/lib/Controller/RemoteController.php @@ -1,83 +1,66 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 OCA\Files_Sharing\Controller; +use OC\Files\View; use OCA\Files_Sharing\External\Manager; +use OCA\Files_Sharing\ResponseDefinitions; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; -use OCP\ILogger; use OCP\IRequest; +use Psr\Log\LoggerInterface; +/** + * @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions + */ class RemoteController extends OCSController { - - /** @var Manager */ - private $externalManager; - - /** @var ILogger */ - private $logger; - /** - * @NoAdminRequired - * * Remote constructor. * * @param string $appName * @param IRequest $request * @param Manager $externalManager */ - public function __construct($appName, - IRequest $request, - Manager $externalManager, - ILogger $logger) { + public function __construct( + $appName, + IRequest $request, + private Manager $externalManager, + private LoggerInterface $logger, + ) { parent::__construct($appName, $request); - - $this->externalManager = $externalManager; - $this->logger = $logger; } /** - * @NoAdminRequired - * * Get list of pending remote shares * - * @return DataResponse + * @return DataResponse<Http::STATUS_OK, list<Files_SharingRemoteShare>, array{}> + * + * 200: Pending remote shares returned */ + #[NoAdminRequired] public function getOpenShares() { return new DataResponse($this->externalManager->getOpenShares()); } /** - * @NoAdminRequired - * * Accept a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSNotFoundException Share not found + * + * 200: Share accepted successfully */ + #[NoAdminRequired] public function acceptShare($id) { if ($this->externalManager->acceptShare($id)) { return new DataResponse(); @@ -86,18 +69,19 @@ class RemoteController extends OCSController { $this->logger->error('Could not accept federated share with id: ' . $id, ['app' => 'files_sharing']); - throw new OCSNotFoundException('wrong share ID, share doesn\'t exist.'); + throw new OCSNotFoundException('wrong share ID, share does not exist.'); } /** - * @NoAdminRequired - * * Decline a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSNotFoundException Share not found + * + * 200: Share declined successfully */ + #[NoAdminRequired] public function declineShare($id) { if ($this->externalManager->declineShare($id)) { return new DataResponse(); @@ -106,7 +90,7 @@ class RemoteController extends OCSController { // Make sure the user has no notification for something that does not exist anymore. $this->externalManager->processNotification($id); - throw new OCSNotFoundException('wrong share ID, share doesn\'t exist.'); + throw new OCSNotFoundException('wrong share ID, share does not exist.'); } /** @@ -114,7 +98,7 @@ class RemoteController extends OCSController { * @return array enriched share info with data from the filecache */ private static function extendShareInfo($share) { - $view = new \OC\Files\View('/' . \OC_User::getUser() . '/files/'); + $view = new View('/' . \OC_User::getUser() . '/files/'); $info = $view->getFileInfo($share['mountpoint']); if ($info === false) { @@ -131,28 +115,30 @@ class RemoteController extends OCSController { } /** - * @NoAdminRequired + * Get a list of accepted remote shares * - * List accepted remote shares + * @return DataResponse<Http::STATUS_OK, list<Files_SharingRemoteShare>, array{}> * - * @return DataResponse + * 200: Accepted remote shares returned */ + #[NoAdminRequired] public function getShares() { $shares = $this->externalManager->getAcceptedShares(); - $shares = array_map('self::extendShareInfo', $shares); + $shares = array_map(self::extendShareInfo(...), $shares); return new DataResponse($shares); } /** - * @NoAdminRequired - * * Get info of a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException + * @param int $id ID of the share + * @return DataResponse<Http::STATUS_OK, Files_SharingRemoteShare, array{}> + * @throws OCSNotFoundException Share not found + * + * 200: Share returned */ + #[NoAdminRequired] public function getShare($id) { $shareInfo = $this->externalManager->getShare($id); @@ -165,15 +151,16 @@ class RemoteController extends OCSController { } /** - * @NoAdminRequired - * * Unshare a remote share * - * @param int $id - * @return DataResponse - * @throws OCSNotFoundException - * @throws OCSForbiddenException + * @param int $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSNotFoundException Share not found + * @throws OCSForbiddenException Unsharing is not possible + * + * 200: Share unshared successfully */ + #[NoAdminRequired] public function unshare($id) { $shareInfo = $this->externalManager->getShare($id); diff --git a/apps/files_sharing/lib/Controller/SettingsController.php b/apps/files_sharing/lib/Controller/SettingsController.php index 00d627095b8..67d9193be78 100644 --- a/apps/files_sharing/lib/Controller/SettingsController.php +++ b/apps/files_sharing/lib/Controller/SettingsController.php @@ -3,71 +3,41 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Hinrich Mahler <nextcloud@mahlerhome.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Controller; use OCA\Files_Sharing\AppInfo\Application; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\JSONResponse; use OCP\IConfig; use OCP\IRequest; class SettingsController extends Controller { - /** @var IConfig */ - private $config; - - /** @var string */ - private $userId; - - public function __construct(IRequest $request, - IConfig $config, - string $userId) { + public function __construct( + IRequest $request, + private IConfig $config, + private string $userId, + ) { parent::__construct(Application::APP_ID, $request); - - $this->config = $config; - $this->userId = $userId; } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] public function setDefaultAccept(bool $accept): JSONResponse { $this->config->setUserValue($this->userId, Application::APP_ID, 'default_accept', $accept ? 'yes' : 'no'); return new JSONResponse(); } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] public function setUserShareFolder(string $shareFolder): JSONResponse { $this->config->setUserValue($this->userId, Application::APP_ID, 'share_folder', $shareFolder); return new JSONResponse(); } - /** - * @NoAdminRequired - */ + #[NoAdminRequired] public function resetUserShareFolder(): JSONResponse { $this->config->deleteUserValue($this->userId, Application::APP_ID, 'share_folder'); return new JSONResponse(); diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index c0441485132..095a8a75963 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -1,54 +1,32 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author castillo92 <37965565+castillo92@users.noreply.github.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Gary Kim <gary@garykim.dev> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Maxence Lange <maxence@nextcloud.com> - * @author Michael Jobst <mjobst+github@tecratech.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * @author Vincent Petry <vincent@nextcloud.com> - * @author waleczny <michal@walczak.xyz> - * - * @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 OCA\Files_Sharing\Controller; +use Exception; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\FileInfo; +use OC\Files\Storage\Wrapper\Wrapper; +use OCA\Circles\Api\v1\Circles; +use OCA\Deck\Sharing\ShareAPIHelper; +use OCA\Federation\TrustedServers; +use OCA\Files\Helper; use OCA\Files_Sharing\Exceptions\SharingRightsException; use OCA\Files_Sharing\External\Storage; -use OCA\Files\Helper; +use OCA\Files_Sharing\ResponseDefinitions; +use OCA\Files_Sharing\SharedStorage; +use OCA\GlobalSiteSelector\Service\SlaveService; use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSException; @@ -57,126 +35,98 @@ use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\AppFramework\QueryException; use OCP\Constants; +use OCP\Files\File; +use OCP\Files\Folder; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; -use OCP\Files\Folder; +use OCP\Files\Mount\IShareOwnerlessMount; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\HintException; +use OCP\IAppConfig; use OCP\IConfig; +use OCP\IDateTimeZone; use OCP\IGroupManager; use OCP\IL10N; use OCP\IPreview; use OCP\IRequest; -use OCP\IServerContainer; +use OCP\ITagManager; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; -use OCP\Share; +use OCP\Mail\IMailer; +use OCP\Server; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\Exceptions\ShareTokenException; use OCP\Share\IManager; +use OCP\Share\IProviderFactory; use OCP\Share\IShare; +use OCP\Share\IShareProviderWithNotification; use OCP\UserStatus\IManager as IUserStatusManager; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; /** - * Class Share20OCS - * * @package OCA\Files_Sharing\API + * + * @psalm-import-type Files_SharingShare from ResponseDefinitions */ class ShareAPIController extends OCSController { - /** @var IManager */ - private $shareManager; - /** @var IGroupManager */ - private $groupManager; - /** @var IUserManager */ - private $userManager; - /** @var IRootFolder */ - private $rootFolder; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var string */ - private $currentUser; - /** @var IL10N */ - private $l; - /** @var \OCP\Files\Node */ - private $lockedNode; - /** @var IConfig */ - private $config; - /** @var IAppManager */ - private $appManager; - /** @var IServerContainer */ - private $serverContainer; - /** @var IUserStatusManager */ - private $userStatusManager; - /** @var IPreview */ - private $previewManager; + private ?Node $lockedNode = null; + private array $trustedServerCache = []; /** * Share20OCS constructor. - * - * @param string $appName - * @param IRequest $request - * @param IManager $shareManager - * @param IGroupManager $groupManager - * @param IUserManager $userManager - * @param IRootFolder $rootFolder - * @param IURLGenerator $urlGenerator - * @param string $userId - * @param IL10N $l10n - * @param IConfig $config - * @param IAppManager $appManager - * @param IServerContainer $serverContainer - * @param IUserStatusManager $userStatusManager */ public function __construct( string $appName, IRequest $request, - IManager $shareManager, - IGroupManager $groupManager, - IUserManager $userManager, - IRootFolder $rootFolder, - IURLGenerator $urlGenerator, - string $userId = null, - IL10N $l10n, - IConfig $config, - IAppManager $appManager, - IServerContainer $serverContainer, - IUserStatusManager $userStatusManager, - IPreview $previewManager + private IManager $shareManager, + private IGroupManager $groupManager, + private IUserManager $userManager, + private IRootFolder $rootFolder, + private IURLGenerator $urlGenerator, + private IL10N $l, + private IConfig $config, + private IAppConfig $appConfig, + private IAppManager $appManager, + private ContainerInterface $serverContainer, + private IUserStatusManager $userStatusManager, + private IPreview $previewManager, + private IDateTimeZone $dateTimeZone, + private LoggerInterface $logger, + private IProviderFactory $factory, + private IMailer $mailer, + private ITagManager $tagManager, + private ?TrustedServers $trustedServers, + private ?string $userId = null, ) { parent::__construct($appName, $request); - - $this->shareManager = $shareManager; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->request = $request; - $this->rootFolder = $rootFolder; - $this->urlGenerator = $urlGenerator; - $this->currentUser = $userId; - $this->l = $l10n; - $this->config = $config; - $this->appManager = $appManager; - $this->serverContainer = $serverContainer; - $this->userStatusManager = $userStatusManager; - $this->previewManager = $previewManager; } /** * Convert an IShare to an array for OCS output * - * @param \OCP\Share\IShare $share + * @param IShare $share * @param Node|null $recipientNode - * @return array + * @return Files_SharingShare * @throws NotFoundException In case the node can't be resolved. * * @suppress PhanUndeclaredClassMethod */ - protected function formatShare(IShare $share, Node $recipientNode = null): array { + protected function formatShare(IShare $share, ?Node $recipientNode = null): array { $sharedBy = $this->userManager->get($share->getSharedBy()); $shareOwner = $this->userManager->get($share->getShareOwner()); + $isOwnShare = false; + if ($shareOwner !== null) { + $isOwnShare = $shareOwner->getUID() === $this->userId; + } + $result = [ 'id' => $share->getId(), 'share_type' => $share->getShareType(), @@ -197,19 +147,17 @@ class ShareAPIController extends OCSController { 'displayname_file_owner' => $shareOwner !== null ? $shareOwner->getDisplayName() : $share->getShareOwner(), ]; - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + $userFolder = $this->rootFolder->getUserFolder($this->userId); if ($recipientNode) { $node = $recipientNode; } else { - $nodes = $userFolder->getById($share->getNodeId()); - if (empty($nodes)) { + $node = $userFolder->getFirstNodeById($share->getNodeId()); + if (!$node) { // fallback to guessing the path $node = $userFolder->get($share->getTarget()); if ($node === null || $share->getTarget() === '') { throw new NotFoundException(); } - } else { - $node = reset($nodes); } } @@ -220,6 +168,32 @@ class ShareAPIController extends OCSController { $result['item_type'] = 'file'; } + // Get the original node permission if the share owner is the current user + if ($isOwnShare) { + $result['item_permissions'] = $node->getPermissions(); + } + + // If we're on the recipient side, the node permissions + // are bound to the share permissions. So we need to + // adjust the permissions to the share permissions if necessary. + if (!$isOwnShare) { + $result['item_permissions'] = $share->getPermissions(); + + // For some reason, single files share are forbidden to have the delete permission + // since we have custom methods to check those, let's adjust straight away. + // DAV permissions does not have that issue though. + if ($this->canDeleteShare($share) || $this->canDeleteShareFromSelf($share)) { + $result['item_permissions'] |= Constants::PERMISSION_DELETE; + } + if ($this->canEditShare($share)) { + $result['item_permissions'] |= Constants::PERMISSION_UPDATE; + } + } + + // See MOUNT_ROOT_PROPERTYNAME dav property + $result['is-mount-root'] = $node->getInternalPath() === ''; + $result['mount-type'] = $node->getMountPoint()->getMountType(); + $result['mimetype'] = $node->getMimetype(); $result['has_preview'] = $this->previewManager->isAvailable($node); $result['storage_id'] = $node->getStorage()->getId(); @@ -228,9 +202,38 @@ class ShareAPIController extends OCSController { $result['file_source'] = $node->getId(); $result['file_parent'] = $node->getParent()->getId(); $result['file_target'] = $share->getTarget(); + $result['item_size'] = $node->getSize(); + $result['item_mtime'] = $node->getMTime(); + + if ($this->trustedServers !== null && in_array($share->getShareType(), [IShare::TYPE_REMOTE, IShare::TYPE_REMOTE_GROUP], true)) { + $result['is_trusted_server'] = false; + $sharedWith = $share->getSharedWith(); + $remoteIdentifier = is_string($sharedWith) ? strrchr($sharedWith, '@') : false; + if ($remoteIdentifier !== false) { + $remote = substr($remoteIdentifier, 1); + + if (isset($this->trustedServerCache[$remote])) { + $result['is_trusted_server'] = $this->trustedServerCache[$remote]; + } else { + try { + $isTrusted = $this->trustedServers->isTrustedServer($remote); + $this->trustedServerCache[$remote] = $isTrusted; + $result['is_trusted_server'] = $isTrusted; + } catch (\Exception $e) { + // Server not found or other issue, we consider it not trusted + $this->trustedServerCache[$remote] = false; + $this->logger->error( + 'Error checking if remote server is trusted (treating as untrusted): ' . $e->getMessage(), + ['exception' => $e] + ); + } + } + } + } $expiration = $share->getExpirationDate(); if ($expiration !== null) { + $expiration->setTimezone($this->dateTimeZone->getTimeZone()); $result['expiration'] = $expiration->format('Y-m-d 00:00:00'); } @@ -239,9 +242,8 @@ class ShareAPIController extends OCSController { $result['share_with'] = $share->getSharedWith(); $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith(); $result['share_with_displayname_unique'] = $sharedWith !== null ? ( - !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID() + !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID() ) : $share->getSharedWith(); - $result['status'] = []; $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]); $userStatus = array_shift($userStatuses); @@ -272,20 +274,24 @@ class ShareAPIController extends OCSController { $result['token'] = $share->getToken(); $result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]); - } elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) { + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith()); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) { $result['share_with'] = $share->getSharedWith(); $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD'); $result['token'] = $share->getToken(); } elseif ($share->getShareType() === IShare::TYPE_EMAIL) { $result['share_with'] = $share->getSharedWith(); $result['password'] = $share->getPassword(); - $result['password_expiration_time'] = $share->getPasswordExpirationTime(); + $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null; $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL'); $result['token'] = $share->getToken(); } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) { // getSharedWith() returns either "name (type, owner)" or - // "name (type, owner) [id]", depending on the Circles app version. + // "name (type, owner) [id]", depending on the Teams app version. $hasCircleId = (substr($share->getSharedWith(), -1) === ']'); $result['share_with_displayname'] = $share->getSharedWithDisplayName(); @@ -298,25 +304,40 @@ class ShareAPIController extends OCSController { $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0); $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' ')); - if (is_bool($shareWithLength)) { - $shareWithLength = -1; + if ($shareWithLength === false) { + $result['share_with'] = substr($share->getSharedWith(), $shareWithStart); + } else { + $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength); } - $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength); } elseif ($share->getShareType() === IShare::TYPE_ROOM) { $result['share_with'] = $share->getSharedWith(); $result['share_with_displayname'] = ''; try { - $result = array_merge($result, $this->getRoomShareHelper()->formatShare($share)); - } catch (QueryException $e) { + /** @var array{share_with_displayname: string, share_with_link: string, share_with?: string, token?: string} $roomShare */ + $roomShare = $this->getRoomShareHelper()->formatShare($share); + $result = array_merge($result, $roomShare); + } catch (ContainerExceptionInterface $e) { } } elseif ($share->getShareType() === IShare::TYPE_DECK) { $result['share_with'] = $share->getSharedWith(); $result['share_with_displayname'] = ''; try { - $result = array_merge($result, $this->getDeckShareHelper()->formatShare($share)); - } catch (QueryException $e) { + /** @var array{share_with: string, share_with_displayname: string, share_with_link: string} $deckShare */ + $deckShare = $this->getDeckShareHelper()->formatShare($share); + $result = array_merge($result, $deckShare); + } catch (ContainerExceptionInterface $e) { + } + } elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = ''; + + try { + /** @var array{share_with: string, share_with_displayname: string, token: string} $scienceMeshShare */ + $scienceMeshShare = $this->getSciencemeshShareHelper()->formatShare($share); + $result = array_merge($result, $scienceMeshShare); + } catch (ContainerExceptionInterface $e) { } } @@ -324,12 +345,17 @@ class ShareAPIController extends OCSController { $result['mail_send'] = $share->getMailSend() ? 1 : 0; $result['hide_download'] = $share->getHideDownload() ? 1 : 0; + $result['attributes'] = null; + if ($attributes = $share->getAttributes()) { + $result['attributes'] = (string)\json_encode($attributes->toArray()); + } + return $result; } /** * Check if one of the users address books knows the exact property, if - * yes we return the full name. + * not we return the full name. * * @param string $query * @param string $property @@ -337,11 +363,20 @@ class ShareAPIController extends OCSController { */ private function getDisplayNameFromAddressBook(string $query, string $property): string { // FIXME: If we inject the contacts manager it gets initialized before any address books are registered - $result = \OC::$server->getContactsManager()->search($query, [$property], [ - 'limit' => 1, - 'enumeration' => false, - 'strict_search' => true, - ]); + try { + $result = Server::get(\OCP\Contacts\IManager::class)->search($query, [$property], [ + 'limit' => 1, + 'enumeration' => false, + 'strict_search' => true, + ]); + } catch (Exception $e) { + $this->logger->error( + $e->getMessage(), + ['exception' => $e] + ); + return $query; + } + foreach ($result as $r) { foreach ($r[$property] as $value) { if ($value === $query && $r['FN']) { @@ -353,48 +388,155 @@ class ShareAPIController extends OCSController { return $query; } + + /** + * @param list<Files_SharingShare> $shares + * @param array<string, string>|null $updatedDisplayName + * + * @return list<Files_SharingShare> + */ + private function fixMissingDisplayName(array $shares, ?array $updatedDisplayName = null): array { + $userIds = $updated = []; + foreach ($shares as $share) { + // share is federated and share have no display name yet + if ($share['share_type'] === IShare::TYPE_REMOTE + && ($share['share_with'] ?? '') !== '' + && ($share['share_with_displayname'] ?? '') === '') { + $userIds[] = $userId = $share['share_with']; + + if ($updatedDisplayName !== null && array_key_exists($userId, $updatedDisplayName)) { + $share['share_with_displayname'] = $updatedDisplayName[$userId]; + } + } + + // prepping userIds with displayName to be updated + $updated[] = $share; + } + + // if $updatedDisplayName is not null, it means we should have already fixed displayNames of the shares + if ($updatedDisplayName !== null) { + return $updated; + } + + // get displayName for the generated list of userId with no displayName + $displayNames = $this->retrieveFederatedDisplayName($userIds); + + // if no displayName are updated, we exit + if (empty($displayNames)) { + return $updated; + } + + // let's fix missing display name and returns all shares + return $this->fixMissingDisplayName($shares, $displayNames); + } + + + /** + * get displayName of a list of userIds from the lookup-server; through the globalsiteselector app. + * returns an array with userIds as keys and displayName as values. + * + * @param array $userIds + * @param bool $cacheOnly - do not reach LUS, get data from cache. + * + * @return array + * @throws ContainerExceptionInterface + */ + private function retrieveFederatedDisplayName(array $userIds, bool $cacheOnly = false): array { + // check if gss is enabled and available + if (count($userIds) === 0 + || !$this->appManager->isEnabledForAnyone('globalsiteselector') + || !class_exists('\OCA\GlobalSiteSelector\Service\SlaveService')) { + return []; + } + + try { + $slaveService = Server::get(SlaveService::class); + } catch (\Throwable $e) { + $this->logger->error( + $e->getMessage(), + ['exception' => $e] + ); + return []; + } + + return $slaveService->getUsersDisplayName($userIds, $cacheOnly); + } + + + /** + * retrieve displayName from cache if available (should be used on federated shares) + * if not available in cache/lus, try for get from address-book, else returns empty string. + * + * @param string $userId + * @param bool $cacheOnly if true will not reach the lus but will only get data from cache + * + * @return string + */ + private function getCachedFederatedDisplayName(string $userId, bool $cacheOnly = true): string { + $details = $this->retrieveFederatedDisplayName([$userId], $cacheOnly); + if (array_key_exists($userId, $details)) { + return $details[$userId]; + } + + $displayName = $this->getDisplayNameFromAddressBook($userId, 'CLOUD'); + return ($displayName === $userId) ? '' : $displayName; + } + + + /** * Get a specific share by id * - * @NoAdminRequired + * @param string $id ID of the share + * @param bool $include_tags Include tags in the share + * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}> + * @throws OCSNotFoundException Share not found * - * @param string $id - * @return DataResponse - * @throws OCSNotFoundException + * 200: Share returned */ - public function getShare(string $id): DataResponse { + #[NoAdminRequired] + public function getShare(string $id, bool $include_tags = false): DataResponse { try { $share = $this->getShareById($id); } catch (ShareNotFound $e) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } try { if ($this->canAccessShare($share)) { $share = $this->formatShare($share); - return new DataResponse([$share]); + + if ($include_tags) { + $share = $this->populateTags([$share]); + } else { + $share = [$share]; + } + + return new DataResponse($share); } } catch (NotFoundException $e) { - // Fall trough + // Fall through } - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } /** * Delete a share * - * @NoAdminRequired + * @param string $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSNotFoundException Share not found + * @throws OCSForbiddenException Missing permissions to delete the share * - * @param string $id - * @return DataResponse - * @throws OCSNotFoundException + * 200: Share deleted successfully */ + #[NoAdminRequired] public function deleteShare(string $id): DataResponse { try { $share = $this->getShareById($id); } catch (ShareNotFound $e) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } try { @@ -404,7 +546,7 @@ class ShareAPIController extends OCSController { } if (!$this->canAccessShare($share)) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } // if it's a group share or a room share @@ -412,7 +554,7 @@ class ShareAPIController extends OCSController { // mount point. Allowing it to be restored // from the deleted shares if ($this->canDeleteShareFromSelf($share)) { - $this->shareManager->deleteFromSelf($share, $this->currentUser); + $this->shareManager->deleteFromSelf($share, $this->userId); } else { if (!$this->canDeleteShare($share)) { throw new OCSForbiddenException($this->l->t('Could not delete share')); @@ -425,51 +567,58 @@ class ShareAPIController extends OCSController { } /** - * @NoAdminRequired + * Create a share * - * @param string $path - * @param int $permissions - * @param int $shareType - * @param string $shareWith - * @param string $publicUpload - * @param string $password - * @param string $sendPasswordByTalk - * @param string $expireDate - * @param string $label + * @param string|null $path Path of the share + * @param int|null $permissions Permissions for the share + * @param int $shareType Type of the share + * @param ?string $shareWith The entity this should be shared with + * @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated) + * @param string $password Password for the share + * @param string|null $sendPasswordByTalk Send the password for the share over Talk + * @param ?string $expireDate The expiry date of the share in the user's timezone at 00:00. + * If $expireDate is not supplied or set to `null`, the system default will be used. + * @param string $note Note for the share + * @param string $label Label for the share (only used in link and email) + * @param string|null $attributes Additional attributes for the share + * @param 'false'|'true'|null $sendMail Send a mail to the recipient * - * @return DataResponse - * @throws NotFoundException - * @throws OCSBadRequestException + * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}> + * @throws OCSBadRequestException Unknown share type * @throws OCSException - * @throws OCSForbiddenException - * @throws OCSNotFoundException - * @throws InvalidPathException + * @throws OCSForbiddenException Creating the share is not allowed + * @throws OCSNotFoundException Creating the share failed * @suppress PhanUndeclaredClassMethod + * + * 200: Share created */ + #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 600)] public function createShare( - string $path = null, - int $permissions = null, + ?string $path = null, + ?int $permissions = null, int $shareType = -1, - string $shareWith = null, - string $publicUpload = 'false', + ?string $shareWith = null, + ?string $publicUpload = null, string $password = '', - string $sendPasswordByTalk = null, - string $expireDate = '', + ?string $sendPasswordByTalk = null, + ?string $expireDate = null, string $note = '', - string $label = '' + string $label = '', + ?string $attributes = null, + ?string $sendMail = null, ): DataResponse { - $share = $this->shareManager->newShare(); + assert($this->userId !== null); - if ($permissions === null) { - $permissions = $this->config->getAppValue('core', 'shareapi_default_permissions', Constants::PERMISSION_ALL); - } + $share = $this->shareManager->newShare(); + $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload); // Verify path if ($path === null) { throw new OCSNotFoundException($this->l->t('Please specify a file or folder path')); } - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + $userFolder = $this->rootFolder->getUserFolder($this->userId); try { /** @var \OC\Files\Node\Node $node */ $node = $userFolder->get($path); @@ -494,17 +643,23 @@ class ShareAPIController extends OCSController { throw new OCSNotFoundException($this->l->t('Could not create share')); } - if ($permissions < 0 || $permissions > Constants::PERMISSION_ALL) { - throw new OCSNotFoundException($this->l->t('Invalid permissions')); + // Set permissions + if ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) { + $permissions = $this->getLinkSharePermissions($permissions, $hasPublicUpload); + $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload); + } else { + // Use default permissions only for non-link shares to keep legacy behavior + if ($permissions === null) { + $permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL); + } + // Non-link shares always require read permissions (link shares could be file drop) + $permissions |= Constants::PERMISSION_READ; } - // Shares always require read permissions - $permissions |= Constants::PERMISSION_READ; - - if ($node instanceof \OCP\Files\File) { - // Single file shares should never have delete or create permissions - $permissions &= ~Constants::PERMISSION_DELETE; - $permissions &= ~Constants::PERMISSION_CREATE; + // For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk) + if ($node instanceof File) { + // if this is a single file share we remove the DELETE and CREATE permissions + $permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE); } /** @@ -516,10 +671,48 @@ class ShareAPIController extends OCSController { $permissions &= ~($permissions & ~$node->getPermissions()); } + if ($attributes !== null) { + $share = $this->setShareAttributes($share, $attributes); + } + + // Expire date checks + // Normally, null means no expiration date but we still set the default for backwards compatibility + // If the client sends an empty string, we set noExpirationDate to true + if ($expireDate !== null) { + if ($expireDate !== '') { + try { + $expireDateTime = $this->parseDate($expireDate); + $share->setExpirationDate($expireDateTime); + } catch (\Exception $e) { + throw new OCSNotFoundException($e->getMessage(), $e); + } + } else { + // Client sent empty string for expire date. + // Set noExpirationDate to true so overwrite is prevented. + $share->setNoExpirationDate(true); + } + } + + $share->setSharedBy($this->userId); + + // Handle mail send + if (is_null($sendMail)) { + $allowSendMail = $this->config->getSystemValueBool('sharing.enable_share_mail', true); + if ($allowSendMail !== true || $shareType === IShare::TYPE_EMAIL) { + // Define a default behavior when sendMail is not provided + // For email shares with a valid recipient, the default is to send the mail + // For all other share types, the default is to not send the mail + $allowSendMail = ($shareType === IShare::TYPE_EMAIL && $shareWith !== null && $shareWith !== ''); + } + $share->setMailSend($allowSendMail); + } else { + $share->setMailSend($sendMail === 'true'); + } + if ($shareType === IShare::TYPE_USER) { // Valid user is required to share if ($shareWith === null || !$this->userManager->userExists($shareWith)) { - throw new OCSNotFoundException($this->l->t('Please specify a valid user')); + throw new OCSNotFoundException($this->l->t('Please specify a valid account to share with')); } $share->setSharedWith($shareWith); $share->setPermissions($permissions); @@ -542,30 +735,7 @@ class ShareAPIController extends OCSController { throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator')); } - if ($publicUpload === 'true') { - // Check if public upload is allowed - if (!$this->shareManager->shareApiLinkAllowPublicUpload()) { - throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator')); - } - - // Public upload can only be set for folders - if ($node instanceof \OCP\Files\File) { - throw new OCSNotFoundException($this->l->t('Public upload is only possible for publicly shared folders')); - } - - $permissions = Constants::PERMISSION_READ | - Constants::PERMISSION_CREATE | - Constants::PERMISSION_UPDATE | - Constants::PERMISSION_DELETE; - } else { - $permissions = Constants::PERMISSION_READ; - } - - // TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones - if (($permissions & Constants::PERMISSION_READ) && $this->shareManager->outgoingServer2ServerSharesAllowed()) { - $permissions |= Constants::PERMISSION_SHARE; - } - + $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload); $share->setPermissions($permissions); // Set password @@ -575,11 +745,18 @@ class ShareAPIController extends OCSController { // Only share by mail have a recipient if (is_string($shareWith) && $shareType === IShare::TYPE_EMAIL) { + // If sending a mail have been requested, validate the mail address + if ($share->getMailSend() && !$this->mailer->validateMailAddress($shareWith)) { + throw new OCSNotFoundException($this->l->t('Please specify a valid email address')); + } $share->setSharedWith($shareWith); } // If we have a label, use it - if (!empty($label)) { + if ($label !== '') { + if (strlen($label) > 255) { + throw new OCSBadRequestException('Maximum label length is 255'); + } $share->setLabel($label); } @@ -590,35 +767,18 @@ class ShareAPIController extends OCSController { $share->setSendPasswordByTalk(true); } - - //Expire date - if ($expireDate !== '') { - try { - $expireDate = $this->parseDate($expireDate); - $share->setExpirationDate($expireDate); - } catch (\Exception $e) { - throw new OCSNotFoundException($this->l->t('Invalid date, date format must be YYYY-MM-DD')); - } - } } elseif ($shareType === IShare::TYPE_REMOTE) { if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType])); } if ($shareWith === null) { - throw new OCSNotFoundException($this->l->t('Please specify a valid federated user ID')); + throw new OCSNotFoundException($this->l->t('Please specify a valid federated account ID')); } $share->setSharedWith($shareWith); $share->setPermissions($permissions); - if ($expireDate !== '') { - try { - $expireDate = $this->parseDate($expireDate); - $share->setExpirationDate($expireDate); - } catch (\Exception $e) { - throw new OCSNotFoundException($this->l->t('Invalid date, date format must be YYYY-MM-DD')); - } - } + $share->setSharedWithDisplayName($this->getCachedFederatedDisplayName($shareWith, false)); } elseif ($shareType === IShare::TYPE_REMOTE_GROUP) { if (!$this->shareManager->outgoingServer2ServerGroupSharesAllowed()) { throw new OCSForbiddenException($this->l->t('Sharing %1$s failed because the back end does not allow shares from type %2$s', [$node->getPath(), $shareType])); @@ -630,45 +790,43 @@ class ShareAPIController extends OCSController { $share->setSharedWith($shareWith); $share->setPermissions($permissions); - if ($expireDate !== '') { - try { - $expireDate = $this->parseDate($expireDate); - $share->setExpirationDate($expireDate); - } catch (\Exception $e) { - throw new OCSNotFoundException($this->l->t('Invalid date, date format must be YYYY-MM-DD')); - } - } } elseif ($shareType === IShare::TYPE_CIRCLE) { - if (!\OC::$server->getAppManager()->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) { - throw new OCSNotFoundException($this->l->t('You cannot share to a Circle if the app is not enabled')); + if (!Server::get(IAppManager::class)->isEnabledForUser('circles') || !class_exists('\OCA\Circles\ShareByCircleProvider')) { + throw new OCSNotFoundException($this->l->t('You cannot share to a Team if the app is not enabled')); } - $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($shareWith); + $circle = Circles::detailsCircle($shareWith); - // Valid circle is required to share + // Valid team is required to share if ($circle === null) { - throw new OCSNotFoundException($this->l->t('Please specify a valid circle')); + throw new OCSNotFoundException($this->l->t('Please specify a valid team')); } $share->setSharedWith($shareWith); $share->setPermissions($permissions); } elseif ($shareType === IShare::TYPE_ROOM) { try { - $this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate); - } catch (QueryException $e) { + $this->getRoomShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); + } catch (ContainerExceptionInterface $e) { throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()])); } } elseif ($shareType === IShare::TYPE_DECK) { try { - $this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate); - } catch (QueryException $e) { + $this->getDeckShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); + } catch (ContainerExceptionInterface $e) { throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support room shares', [$node->getPath()])); } + } elseif ($shareType === IShare::TYPE_SCIENCEMESH) { + try { + $this->getSciencemeshShareHelper()->createShare($share, $shareWith, $permissions, $expireDate ?? ''); + } catch (ContainerExceptionInterface $e) { + throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not support ScienceMesh shares', [$node->getPath()])); + } } else { throw new OCSBadRequestException($this->l->t('Unknown share type')); } $share->setShareType($shareType); - $share->setSharedBy($this->currentUser); + $this->checkInheritedAttributes($share); if ($note !== '') { $share->setNote($note); @@ -676,13 +834,15 @@ class ShareAPIController extends OCSController { try { $share = $this->shareManager->createShare($share); - } catch (GenericShareException $e) { - \OC::$server->getLogger()->logException($e); + } catch (HintException $e) { $code = $e->getCode() === 0 ? 403 : $e->getCode(); throw new OCSException($e->getHint(), $code); - } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e); + } catch (GenericShareException|\InvalidArgumentException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); throw new OCSForbiddenException($e->getMessage(), $e); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSForbiddenException('Failed to create share.', $e); } $output = $this->formatShare($share); @@ -694,19 +854,20 @@ class ShareAPIController extends OCSController { * @param null|Node $node * @param boolean $includeTags * - * @return array + * @return list<Files_SharingShare> */ private function getSharedWithMe($node, bool $includeTags): array { - $userShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_USER, $node, -1, 0); - $groupShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_GROUP, $node, -1, 0); - $circleShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_CIRCLE, $node, -1, 0); - $roomShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_ROOM, $node, -1, 0); - $deckShares = $this->shareManager->getSharedWith($this->currentUser, IShare::TYPE_DECK, $node, -1, 0); + $userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0); + $groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0); + $circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0); + $roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0); + $deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0); + $sciencemeshShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_SCIENCEMESH, $node, -1, 0); - $shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares); + $shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares, $sciencemeshShares); $filteredShares = array_filter($shares, function (IShare $share) { - return $share->getShareOwner() !== $this->currentUser; + return $share->getShareOwner() !== $this->userId; }); $formatted = []; @@ -721,27 +882,27 @@ class ShareAPIController extends OCSController { } if ($includeTags) { - $formatted = Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager()); + $formatted = $this->populateTags($formatted); } return $formatted; } /** - * @param \OCP\Files\Node $folder + * @param Node $folder * - * @return array + * @return list<Files_SharingShare> * @throws OCSBadRequestException * @throws NotFoundException */ private function getSharesInDir(Node $folder): array { - if (!($folder instanceof \OCP\Files\Folder)) { + if (!($folder instanceof Folder)) { throw new OCSBadRequestException($this->l->t('Not a directory')); } $nodes = $folder->getDirectoryListing(); - /** @var \OCP\Share\IShare[] $shares */ + /** @var IShare[] $shares */ $shares = array_reduce($nodes, function ($carry, $node) { $carry = array_merge($carry, $this->getAllShares($node, true)); return $carry; @@ -750,12 +911,11 @@ class ShareAPIController extends OCSController { // filter out duplicate shares $known = []; - $formatted = $miniFormatted = []; $resharingRight = false; $known = []; foreach ($shares as $share) { - if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->currentUser) { + if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) { continue; } @@ -764,10 +924,10 @@ class ShareAPIController extends OCSController { $known[] = $share->getId(); $formatted[] = $format; - if ($share->getSharedBy() === $this->currentUser) { + if ($share->getSharedBy() === $this->userId) { $miniFormatted[] = $format; } - if (!$resharingRight && $this->shareProviderResharingRights($this->currentUser, $share, $folder)) { + if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) { $resharingRight = true; } } catch (\Exception $e) { @@ -783,38 +943,30 @@ class ShareAPIController extends OCSController { } /** - * The getShares function. + * Get shares of the current user * - * @NoAdminRequired + * @param string $shared_with_me Only get shares with the current user + * @param string $reshares Only get shares by the current user and reshares + * @param string $subfiles Only get all shares in a folder + * @param string $path Get shares for a specific path + * @param string $include_tags Include tags in the share * - * @param string $shared_with_me - * @param string $reshares - * @param string $subfiles - * @param string $path + * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}> + * @throws OCSNotFoundException The folder was not found or is inaccessible * - * - Get shares by the current user - * - Get shares by the current user and reshares (?reshares=true) - * - Get shares with the current user (?shared_with_me=true) - * - Get shares for a specific path (?path=...) - * - Get all shares in a folder (?subfiles=true&path=..) - * - * @param string $include_tags - * - * @return DataResponse - * @throws NotFoundException - * @throws OCSBadRequestException - * @throws OCSNotFoundException + * 200: Shares returned */ + #[NoAdminRequired] public function getShares( string $shared_with_me = 'false', string $reshares = 'false', string $subfiles = 'false', string $path = '', - string $include_tags = 'false' + string $include_tags = 'false', ): DataResponse { $node = null; if ($path !== '') { - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + $userFolder = $this->rootFolder->getUserFolder($this->userId); try { $node = $userFolder->get($path); $this->lock($node); @@ -828,7 +980,7 @@ class ShareAPIController extends OCSController { } $shares = $this->getFormattedShares( - $this->currentUser, + $this->userId, $node, ($shared_with_me === 'true'), ($reshares === 'true'), @@ -839,6 +991,71 @@ class ShareAPIController extends OCSController { return new DataResponse($shares); } + private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int { + $permissions = $permissions ?? Constants::PERMISSION_READ; + + // Legacy option handling + if ($legacyPublicUpload !== null) { + $permissions = $legacyPublicUpload + ? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE) + : Constants::PERMISSION_READ; + } + + if ($this->hasPermission($permissions, Constants::PERMISSION_READ) + && $this->shareManager->outgoingServer2ServerSharesAllowed() + && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) { + $permissions |= Constants::PERMISSION_SHARE; + } + + return $permissions; + } + + /** + * Helper to check for legacy "publicUpload" handling. + * If the value is set to `true` or `false` then true or false are returned. + * Otherwise null is returned to indicate that the option was not (or wrong) set. + * + * @param null|string $legacyPublicUpload The value of `publicUpload` + */ + private function getLegacyPublicUpload(?string $legacyPublicUpload): ?bool { + if ($legacyPublicUpload === 'true') { + return true; + } elseif ($legacyPublicUpload === 'false') { + return false; + } + // Not set at all + return null; + } + + /** + * For link and email shares validate that only allowed combinations are set. + * + * @throw OCSBadRequestException If permission combination is invalid. + * @throw OCSForbiddenException If public upload was forbidden by the administrator. + */ + private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void { + if ($legacyPublicUpload && ($node instanceof File)) { + throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders')); + } + + // We need at least READ or CREATE (file drop) + if (!$this->hasPermission($permissions, Constants::PERMISSION_READ) + && !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) { + throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions')); + } + + // UPDATE and DELETE require a READ permission + if (!$this->hasPermission($permissions, Constants::PERMISSION_READ) + && ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) { + throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set')); + } + + // Check if public uploading was disabled + if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE) + && !$this->shareManager->shareApiLinkAllowPublicUpload()) { + throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator')); + } + } /** * @param string $viewer @@ -848,7 +1065,7 @@ class ShareAPIController extends OCSController { * @param bool $subFiles * @param bool $includeTags * - * @return array + * @return list<Files_SharingShare> * @throws NotFoundException * @throws OCSBadRequestException */ @@ -858,7 +1075,7 @@ class ShareAPIController extends OCSController { bool $sharedWithMe = false, bool $reShares = false, bool $subFiles = false, - bool $includeTags = false + bool $includeTags = false, ): array { if ($sharedWithMe) { return $this->getSharedWithMe($node, $includeTags); @@ -884,7 +1101,7 @@ class ShareAPIController extends OCSController { } if (in_array($share->getId(), $known) - || ($share->getSharedWith() === $this->currentUser && $share->getShareType() === IShare::TYPE_USER)) { + || ($share->getSharedWith() === $this->userId && $share->getShareType() === IShare::TYPE_USER)) { continue; } @@ -897,16 +1114,16 @@ class ShareAPIController extends OCSController { // let's also build a list of shares created // by the current user only, in case // there is no resharing rights - if ($share->getSharedBy() === $this->currentUser) { + if ($share->getSharedBy() === $this->userId) { $miniFormatted[] = $format; } // check if one of those share is shared with me // and if I have resharing rights on it - if (!$resharingRight && $this->shareProviderResharingRights($this->currentUser, $share, $node)) { + if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $node)) { $resharingRight = true; } - } catch (InvalidPathException | NotFoundException $e) { + } catch (InvalidPathException|NotFoundException $e) { } } @@ -914,9 +1131,11 @@ class ShareAPIController extends OCSController { $formatted = $miniFormatted; } + // fix eventual missing display name from federated shares + $formatted = $this->fixMissingDisplayName($formatted); + if ($includeTags) { - $formatted = - Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager()); + $formatted = $this->populateTags($formatted); } return $formatted; @@ -924,41 +1143,33 @@ class ShareAPIController extends OCSController { /** - * The getInheritedShares function. - * returns all shares relative to a file, including parent folders shares rights. - * - * @NoAdminRequired - * - * @param string $path + * Get all shares relative to a file, including parent folders shares rights * - * - Get shares by the current user - * - Get shares by the current user and reshares (?reshares=true) - * - Get shares with the current user (?shared_with_me=true) - * - Get shares for a specific path (?path=...) - * - Get all shares in a folder (?subfiles=true&path=..) + * @param string $path Path all shares will be relative to * - * @return DataResponse + * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}> * @throws InvalidPathException * @throws NotFoundException - * @throws OCSNotFoundException - * @throws OCSBadRequestException + * @throws OCSNotFoundException The given path is invalid * @throws SharingRightsException + * + * 200: Shares returned */ + #[NoAdminRequired] public function getInheritedShares(string $path): DataResponse { - // get Node from (string) path. - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); + $userFolder = $this->rootFolder->getUserFolder($this->userId); try { $node = $userFolder->get($path); $this->lock($node); - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist')); } catch (LockedException $e) { throw new OCSNotFoundException($this->l->t('Could not lock path')); } if (!($node->getPermissions() & Constants::PERMISSION_SHARE)) { - throw new SharingRightsException('no sharing rights on this item'); + throw new SharingRightsException($this->l->t('no sharing rights on this item')); } // The current top parent we have access to @@ -966,7 +1177,7 @@ class ShareAPIController extends OCSController { // initiate real owner. $owner = $node->getOwner() - ->getUID(); + ->getUID(); if (!$this->userManager->userExists($owner)) { return new DataResponse([]); } @@ -975,23 +1186,25 @@ class ShareAPIController extends OCSController { $userFolder = $this->rootFolder->getUserFolder($owner); if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) { $owner = $node->getOwner() - ->getUID(); + ->getUID(); $userFolder = $this->rootFolder->getUserFolder($owner); - $nodes = $userFolder->getById($node->getId()); - $node = array_shift($nodes); + $node = $userFolder->getFirstNodeById($node->getId()); } $basePath = $userFolder->getPath(); // generate node list for each parent folders /** @var Node[] $nodes */ $nodes = []; - while ($node->getPath() !== $basePath) { + while (true) { $node = $node->getParent(); + if ($node->getPath() === $basePath) { + break; + } $nodes[] = $node; } // The user that is requesting this list - $currentUserFolder = $this->rootFolder->getUserFolder($this->currentUser); + $currentUserFolder = $this->rootFolder->getUserFolder($this->userId); // for each nodes, retrieve shares. $shares = []; @@ -999,9 +1212,9 @@ class ShareAPIController extends OCSController { foreach ($nodes as $node) { $getShares = $this->getFormattedShares($owner, $node, false, true); - $currentUserNodes = $currentUserFolder->getById($node->getId()); - if (!empty($currentUserNodes)) { - $parent = array_pop($currentUserNodes); + $currentUserNode = $currentUserFolder->getFirstNodeById($node->getId()); + if ($currentUserNode) { + $parent = $currentUserNode; } $subPath = $currentUserFolder->getRelativePath($parent->getPath()); @@ -1022,62 +1235,74 @@ class ShareAPIController extends OCSController { return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck; } - /** - * @NoAdminRequired + * Update a share * - * @param string $id - * @param int $permissions - * @param string $password - * @param string $sendPasswordByTalk - * @param string $publicUpload - * @param string $expireDate - * @param string $note - * @param string $label - * @param string $hideDownload - * @return DataResponse - * @throws LockedException - * @throws NotFoundException - * @throws OCSBadRequestException - * @throws OCSForbiddenException - * @throws OCSNotFoundException + * @param string $id ID of the share + * @param int|null $permissions New permissions + * @param string|null $password New password + * @param string|null $sendPasswordByTalk New condition if the password should be send over Talk + * @param string|null $publicUpload New condition if public uploading is allowed + * @param string|null $expireDate New expiry date + * @param string|null $note New note + * @param string|null $label New label + * @param string|null $hideDownload New condition if the download should be hidden + * @param string|null $attributes New additional attributes + * @param string|null $sendMail if the share should be send by mail. + * Considering the share already exists, no mail will be send after the share is updated. + * You will have to use the sendMail action to send the mail. + * @param string|null $shareWith New recipient for email shares + * @param string|null $token New token + * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}> + * @throws OCSBadRequestException Share could not be updated because the requested changes are invalid + * @throws OCSForbiddenException Missing permissions to update the share + * @throws OCSNotFoundException Share not found + * + * 200: Share updated successfully */ + #[NoAdminRequired] public function updateShare( string $id, - int $permissions = null, - string $password = null, - string $sendPasswordByTalk = null, - string $publicUpload = null, - string $expireDate = null, - string $note = null, - string $label = null, - string $hideDownload = null + ?int $permissions = null, + ?string $password = null, + ?string $sendPasswordByTalk = null, + ?string $publicUpload = null, + ?string $expireDate = null, + ?string $note = null, + ?string $label = null, + ?string $hideDownload = null, + ?string $attributes = null, + ?string $sendMail = null, + ?string $token = null, ): DataResponse { try { $share = $this->getShareById($id); } catch (ShareNotFound $e) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } $this->lock($share->getNode()); if (!$this->canAccessShare($share, false)) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } if (!$this->canEditShare($share)) { - throw new OCSForbiddenException('You are not allowed to edit incoming shares'); + throw new OCSForbiddenException($this->l->t('You are not allowed to edit incoming shares')); } if ( - $permissions === null && - $password === null && - $sendPasswordByTalk === null && - $publicUpload === null && - $expireDate === null && - $note === null && - $label === null && - $hideDownload === null + $permissions === null + && $password === null + && $sendPasswordByTalk === null + && $publicUpload === null + && $expireDate === null + && $note === null + && $label === null + && $hideDownload === null + && $attributes === null + && $sendMail === null + && $token === null ) { throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given')); } @@ -1086,24 +1311,21 @@ class ShareAPIController extends OCSController { $share->setNote($note); } + if ($attributes !== null) { + $share = $this->setShareAttributes($share, $attributes); + } + + // Handle mail send + if ($sendMail === 'true' || $sendMail === 'false') { + $share->setMailSend($sendMail === 'true'); + } + /** - * expirationdate, password and publicUpload only make sense for link shares + * expiration date, password and publicUpload only make sense for link shares */ if ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) { - /** - * We do not allow editing link shares that the current user - * doesn't own. This is confusing and lead to errors when - * someone else edit a password or expiration date without - * the share owner knowing about it. - * We only allow deletion - */ - - if ($share->getSharedBy() !== $this->currentUser) { - throw new OCSForbiddenException('You are not allowed to edit link shares that you don\'t own'); - } - // Update hide download state if ($hideDownload === 'true') { $share->setHideDownload(true); @@ -1111,67 +1333,13 @@ class ShareAPIController extends OCSController { $share->setHideDownload(false); } - $newPermissions = null; - if ($publicUpload === 'true') { - $newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE; - } elseif ($publicUpload === 'false') { - $newPermissions = Constants::PERMISSION_READ; - } - - if ($permissions !== null) { - $newPermissions = $permissions; - $newPermissions = $newPermissions & ~Constants::PERMISSION_SHARE; - } - - if ($newPermissions !== null) { - if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && !$this->hasPermission($newPermissions, Constants::PERMISSION_CREATE)) { - throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions')); - } - - if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && ( - $this->hasPermission($newPermissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($newPermissions, Constants::PERMISSION_DELETE) - )) { - throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set')); - } - } - - if ( - // legacy - $newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE) || - // correct - $newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE) - ) { - if (!$this->shareManager->shareApiLinkAllowPublicUpload()) { - throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator')); - } - - if (!($share->getNode() instanceof \OCP\Files\Folder)) { - throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders')); - } - - // normalize to correct public upload permissions - $newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE; - } - - if ($newPermissions !== null) { - // TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones - if (($newPermissions & Constants::PERMISSION_READ) && $this->shareManager->outgoingServer2ServerSharesAllowed()) { - $newPermissions |= Constants::PERMISSION_SHARE; - } - - $share->setPermissions($newPermissions); - $permissions = $newPermissions; - } - - if ($expireDate === '') { - $share->setExpirationDate(null); - } elseif ($expireDate !== null) { - try { - $expireDate = $this->parseDate($expireDate); - } catch (\Exception $e) { - throw new OCSBadRequestException($e->getMessage(), $e); - } - $share->setExpirationDate($expireDate); + // If either manual permissions are specified or publicUpload + // then we need to also update the permissions of the share + if ($permissions !== null || $publicUpload !== null) { + $hasPublicUpload = $this->getLegacyPublicUpload($publicUpload); + $permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload); + $this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload); + $share->setPermissions($permissions); } if ($password === '') { @@ -1182,7 +1350,7 @@ class ShareAPIController extends OCSController { if ($label !== null) { if (strlen($label) > 255) { - throw new OCSBadRequestException("Maxmimum label length is 255"); + throw new OCSBadRequestException('Maximum label length is 255'); } $share->setLabel($label); } @@ -1196,6 +1364,16 @@ class ShareAPIController extends OCSController { } elseif ($sendPasswordByTalk !== null) { $share->setSendPasswordByTalk(false); } + + if ($token !== null) { + if (!$this->shareManager->allowCustomTokens()) { + throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator')); + } + if (!$this->validateToken($token)) { + throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen')); + } + $share->setToken($token); + } } // NOT A LINK SHARE @@ -1203,34 +1381,51 @@ class ShareAPIController extends OCSController { if ($permissions !== null) { $share->setPermissions($permissions); } + } - if ($expireDate === '') { - $share->setExpirationDate(null); - } elseif ($expireDate !== null) { - try { - $expireDate = $this->parseDate($expireDate); - } catch (\Exception $e) { - throw new OCSBadRequestException($e->getMessage(), $e); - } - $share->setExpirationDate($expireDate); + if ($expireDate === '') { + $share->setExpirationDate(null); + } elseif ($expireDate !== null) { + try { + $expireDateTime = $this->parseDate($expireDate); + $share->setExpirationDate($expireDateTime); + } catch (\Exception $e) { + throw new OCSBadRequestException($e->getMessage(), $e); } } try { + $this->checkInheritedAttributes($share); $share = $this->shareManager->updateShare($share); - } catch (GenericShareException $e) { + } catch (HintException $e) { $code = $e->getCode() === 0 ? 403 : $e->getCode(); - throw new OCSException($e->getHint(), $code); + throw new OCSException($e->getHint(), (int)$code); } catch (\Exception $e) { - throw new OCSBadRequestException($e->getMessage(), $e); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSBadRequestException('Failed to update share.', $e); } return new DataResponse($this->formatShare($share)); } + private function validateToken(string $token): bool { + if (mb_strlen($token) === 0) { + return false; + } + if (!preg_match('/^[a-z0-9-]+$/i', $token)) { + return false; + } + return true; + } + /** - * @NoAdminRequired + * Get all shares that are still pending + * + * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}> + * + * 200: Pending shares returned */ + #[NoAdminRequired] public function pendingShares(): DataResponse { $pendingShares = []; @@ -1240,7 +1435,7 @@ class ShareAPIController extends OCSController { ]; foreach ($shareTypes as $shareType) { - $shares = $this->shareManager->getSharedWith($this->currentUser, $shareType, null, -1, 0); + $shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0); foreach ($shares as $share) { if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) { @@ -1249,23 +1444,20 @@ class ShareAPIController extends OCSController { } } - $result = array_filter(array_map(function (IShare $share) { + $result = array_values(array_filter(array_map(function (IShare $share) { $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $nodes = $userFolder->getById($share->getNodeId()); - if (empty($nodes)) { + $node = $userFolder->getFirstNodeById($share->getNodeId()); + if (!$node) { // fallback to guessing the path $node = $userFolder->get($share->getTarget()); if ($node === null || $share->getTarget() === '') { return null; } - } else { - $node = $nodes[0]; } try { $formattedShare = $this->formatShare($share, $node); - $formattedShare['status'] = $share->getStatus(); - $formattedShare['path'] = $share->getNode()->getName(); + $formattedShare['path'] = '/' . $share->getNode()->getName(); $formattedShare['permissions'] = 0; return $formattedShare; } catch (NotFoundException $e) { @@ -1273,38 +1465,42 @@ class ShareAPIController extends OCSController { } }, $pendingShares), function ($entry) { return $entry !== null; - }); + })); return new DataResponse($result); } /** - * @NoAdminRequired + * Accept a share * - * @param string $id - * @return DataResponse - * @throws OCSNotFoundException + * @param string $id ID of the share + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * @throws OCSNotFoundException Share not found * @throws OCSException - * @throws OCSBadRequestException + * @throws OCSBadRequestException Share could not be accepted + * + * 200: Share accepted successfully */ + #[NoAdminRequired] public function acceptShare(string $id): DataResponse { try { $share = $this->getShareById($id); } catch (ShareNotFound $e) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } if (!$this->canAccessShare($share)) { - throw new OCSNotFoundException($this->l->t('Wrong share ID, share doesn\'t exist')); + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); } try { - $this->shareManager->acceptShare($share, $this->currentUser); - } catch (GenericShareException $e) { + $this->shareManager->acceptShare($share, $this->userId); + } catch (HintException $e) { $code = $e->getCode() === 0 ? 403 : $e->getCode(); - throw new OCSException($e->getHint(), $code); + throw new OCSException($e->getHint(), (int)$code); } catch (\Exception $e) { - throw new OCSBadRequestException($e->getMessage(), $e); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSBadRequestException('Failed to accept share.', $e); } return new DataResponse(); @@ -1313,43 +1509,43 @@ class ShareAPIController extends OCSController { /** * Does the user have read permission on the share * - * @param \OCP\Share\IShare $share the share to check + * @param IShare $share the share to check * @param boolean $checkGroups check groups as well? * @return boolean * @throws NotFoundException * * @suppress PhanUndeclaredClassMethod */ - protected function canAccessShare(\OCP\Share\IShare $share, bool $checkGroups = true): bool { + protected function canAccessShare(IShare $share, bool $checkGroups = true): bool { // A file with permissions 0 can't be accessed by us. So Don't show it if ($share->getPermissions() === 0) { return false; } // Owner of the file and the sharer of the file can always get share - if ($share->getShareOwner() === $this->currentUser - || $share->getSharedBy() === $this->currentUser) { + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId) { return true; } // If the share is shared with you, you can access it! if ($share->getShareType() === IShare::TYPE_USER - && $share->getSharedWith() === $this->currentUser) { + && $share->getSharedWith() === $this->userId) { return true; } // Have reshare rights on the shared file/folder ? // Does the currentUser have access to the shared file? - $userFolder = $this->rootFolder->getUserFolder($this->currentUser); - $files = $userFolder->getById($share->getNodeId()); - if (!empty($files) && $this->shareProviderResharingRights($this->currentUser, $share, $files[0])) { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $file = $userFolder->getFirstNodeById($share->getNodeId()); + if ($file && $this->shareProviderResharingRights($this->userId, $share, $file)) { return true; } // If in the recipient group, you can see the share if ($checkGroups && $share->getShareType() === IShare::TYPE_GROUP) { $sharedWith = $this->groupManager->get($share->getSharedWith()); - $user = $this->userManager->get($this->currentUser); + $user = $this->userManager->get($this->userId); if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { return true; } @@ -1362,16 +1558,24 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_ROOM) { try { - return $this->getRoomShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { + return $this->getRoomShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { return false; } } if ($share->getShareType() === IShare::TYPE_DECK) { try { - return $this->getDeckShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { + return $this->getDeckShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { + return false; + } + } + + if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + try { + return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1382,10 +1586,10 @@ class ShareAPIController extends OCSController { /** * Does the user have edit permission on the share * - * @param \OCP\Share\IShare $share the share to check + * @param IShare $share the share to check * @return boolean */ - protected function canEditShare(\OCP\Share\IShare $share): bool { + protected function canEditShare(IShare $share): bool { // A file with permissions 0 can't be accessed by us. So Don't show it if ($share->getPermissions() === 0) { return false; @@ -1393,12 +1597,18 @@ class ShareAPIController extends OCSController { // The owner of the file and the creator of the share // can always edit the share - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { return true; } + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $file = $userFolder->getFirstNodeById($share->getNodeId()); + if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) { + return true; + } + //! we do NOT support some kind of `admin` in groups. //! You cannot edit shares shared to a group you're //! a member of if you're not the share owner or the file owner! @@ -1409,10 +1619,10 @@ class ShareAPIController extends OCSController { /** * Does the user have delete permission on the share * - * @param \OCP\Share\IShare $share the share to check + * @param IShare $share the share to check * @return boolean */ - protected function canDeleteShare(\OCP\Share\IShare $share): bool { + protected function canDeleteShare(IShare $share): bool { // A file with permissions 0 can't be accessed by us. So Don't show it if ($share->getPermissions() === 0) { return false; @@ -1420,20 +1630,26 @@ class ShareAPIController extends OCSController { // if the user is the recipient, i can unshare // the share with self - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $this->currentUser + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $this->userId ) { return true; } // The owner of the file and the creator of the share // can always delete the share - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { return true; } + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $file = $userFolder->getFirstNodeById($share->getNodeId()); + if ($file?->getMountPoint() instanceof IShareOwnerlessMount && $this->shareProviderResharingRights($this->userId, $share, $file)) { + return true; + } + return false; } @@ -1444,21 +1660,22 @@ class ShareAPIController extends OCSController { * completely delete the share but only the mount point. * It can then be restored from the deleted shares section. * - * @param \OCP\Share\IShare $share the share to check + * @param IShare $share the share to check * @return boolean * * @suppress PhanUndeclaredClassMethod */ - protected function canDeleteShareFromSelf(\OCP\Share\IShare $share): bool { - if ($share->getShareType() !== IShare::TYPE_GROUP && - $share->getShareType() !== IShare::TYPE_ROOM && - $share->getShareType() !== IShare::TYPE_DECK + protected function canDeleteShareFromSelf(IShare $share): bool { + if ($share->getShareType() !== IShare::TYPE_GROUP + && $share->getShareType() !== IShare::TYPE_ROOM + && $share->getShareType() !== IShare::TYPE_DECK + && $share->getShareType() !== IShare::TYPE_SCIENCEMESH ) { return false; } - if ($share->getShareOwner() === $this->currentUser || - $share->getSharedBy() === $this->currentUser + if ($share->getShareOwner() === $this->userId + || $share->getSharedBy() === $this->userId ) { // Delete the whole share, not just for self return false; @@ -1467,7 +1684,7 @@ class ShareAPIController extends OCSController { // If in the recipient group, you can delete the share from self if ($share->getShareType() === IShare::TYPE_GROUP) { $sharedWith = $this->groupManager->get($share->getSharedWith()); - $user = $this->userManager->get($this->currentUser); + $user = $this->userManager->get($this->userId); if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { return true; } @@ -1475,16 +1692,24 @@ class ShareAPIController extends OCSController { if ($share->getShareType() === IShare::TYPE_ROOM) { try { - return $this->getRoomShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { + return $this->getRoomShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { return false; } } if ($share->getShareType() === IShare::TYPE_DECK) { try { - return $this->getDeckShareHelper()->canAccessShare($share, $this->currentUser); - } catch (QueryException $e) { + return $this->getDeckShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { + return false; + } + } + + if ($share->getShareType() === IShare::TYPE_SCIENCEMESH) { + try { + return $this->getSciencemeshShareHelper()->canAccessShare($share, $this->userId); + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1504,13 +1729,13 @@ class ShareAPIController extends OCSController { */ private function parseDate(string $expireDate): \DateTime { try { - $date = new \DateTime($expireDate); + $date = new \DateTime(trim($expireDate, '"'), $this->dateTimeZone->getTimeZone()); + // Make sure it expires at midnight in owner timezone + $date->setTime(0, 0, 0); } catch (\Exception $e) { - throw new \Exception('Invalid date. Format must be YYYY-MM-DD'); + throw new \Exception($this->l->t('Invalid date. Format must be YYYY-MM-DD')); } - $date->setTime(0, 0, 0); - return $date; } @@ -1519,7 +1744,7 @@ class ShareAPIController extends OCSController { * not support this we need to check all backends. * * @param string $id - * @return \OCP\Share\IShare + * @return IShare * @throws ShareNotFound */ private function getShareById(string $id): IShare { @@ -1527,7 +1752,7 @@ class ShareAPIController extends OCSController { // First check if it is an internal share. try { - $share = $this->shareManager->getShareById('ocinternal:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('ocinternal:' . $id, $this->userId); return $share; } catch (ShareNotFound $e) { // Do nothing, just try the other share type @@ -1536,7 +1761,7 @@ class ShareAPIController extends OCSController { try { if ($this->shareManager->shareProviderExists(IShare::TYPE_CIRCLE)) { - $share = $this->shareManager->getShareById('ocCircleShare:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('ocCircleShare:' . $id, $this->userId); return $share; } } catch (ShareNotFound $e) { @@ -1545,7 +1770,7 @@ class ShareAPIController extends OCSController { try { if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) { - $share = $this->shareManager->getShareById('ocMailShare:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('ocMailShare:' . $id, $this->userId); return $share; } } catch (ShareNotFound $e) { @@ -1553,7 +1778,7 @@ class ShareAPIController extends OCSController { } try { - $share = $this->shareManager->getShareById('ocRoomShare:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('ocRoomShare:' . $id, $this->userId); return $share; } catch (ShareNotFound $e) { // Do nothing, just try the other share type @@ -1561,7 +1786,16 @@ class ShareAPIController extends OCSController { try { if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) { - $share = $this->shareManager->getShareById('deck:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('deck:' . $id, $this->userId); + return $share; + } + } catch (ShareNotFound $e) { + // Do nothing, just try the other share type + } + + try { + if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) { + $share = $this->shareManager->getShareById('sciencemesh:' . $id, $this->userId); return $share; } } catch (ShareNotFound $e) { @@ -1571,7 +1805,7 @@ class ShareAPIController extends OCSController { if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { throw new ShareNotFound(); } - $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id, $this->currentUser); + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id, $this->userId); return $share; } @@ -1579,10 +1813,10 @@ class ShareAPIController extends OCSController { /** * Lock a Node * - * @param \OCP\Files\Node $node + * @param Node $node * @throws LockedException */ - private function lock(\OCP\Files\Node $node) { + private function lock(Node $node) { $node->lock(ILockingProvider::LOCK_SHARED); $this->lockedNode = $node; } @@ -1601,10 +1835,10 @@ class ShareAPIController extends OCSController { * Returns the helper of ShareAPIController for room shares. * * If the Talk application is not enabled or the helper is not available - * a QueryException is thrown instead. + * a ContainerExceptionInterface is thrown instead. * * @return \OCA\Talk\Share\Helper\ShareAPIController - * @throws QueryException + * @throws ContainerExceptionInterface */ private function getRoomShareHelper() { if (!$this->appManager->isEnabledForUser('spreed')) { @@ -1618,10 +1852,10 @@ class ShareAPIController extends OCSController { * Returns the helper of ShareAPIHelper for deck shares. * * If the Deck application is not enabled or the helper is not available - * a QueryException is thrown instead. + * a ContainerExceptionInterface is thrown instead. * - * @return \OCA\Deck\Sharing\ShareAPIHelper - * @throws QueryException + * @return ShareAPIHelper + * @throws ContainerExceptionInterface */ private function getDeckShareHelper() { if (!$this->appManager->isEnabledForUser('deck')) { @@ -1632,6 +1866,23 @@ class ShareAPIController extends OCSController { } /** + * Returns the helper of ShareAPIHelper for sciencemesh shares. + * + * If the sciencemesh application is not enabled or the helper is not available + * a ContainerExceptionInterface is thrown instead. + * + * @return ShareAPIHelper + * @throws ContainerExceptionInterface + */ + private function getSciencemeshShareHelper() { + if (!$this->appManager->isEnabledForUser('sciencemesh')) { + throw new QueryException(); + } + + return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper'); + } + + /** * @param string $viewer * @param Node $node * @param bool $reShares @@ -1646,7 +1897,8 @@ class ShareAPIController extends OCSController { IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, - IShare::TYPE_DECK + IShare::TYPE_DECK, + IShare::TYPE_SCIENCEMESH ]; // Should we assume that the (currentUser) viewer is the owner of the node !? @@ -1656,21 +1908,21 @@ class ShareAPIController extends OCSController { continue; } - $providerShares = - $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0); + $providerShares + = $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0); $shares = array_merge($shares, $providerShares); } if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { $federatedShares = $this->shareManager->getSharesBy( - $this->currentUser, IShare::TYPE_REMOTE, $node, $reShares, -1, 0 + $this->userId, IShare::TYPE_REMOTE, $node, $reShares, -1, 0 ); $shares = array_merge($shares, $federatedShares); } if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) { $federatedShares = $this->shareManager->getSharesBy( - $this->currentUser, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0 + $this->userId, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0 ); $shares = array_merge($shares, $federatedShares); } @@ -1685,8 +1937,8 @@ class ShareAPIController extends OCSController { * @throws SharingRightsException */ private function confirmSharingRights(Node $node): void { - if (!$this->hasResharingRights($this->currentUser, $node)) { - throw new SharingRightsException('no sharing rights on this item'); + if (!$this->hasResharingRights($this->userId, $node)) { + throw new SharingRightsException($this->l->t('No sharing rights on this item')); } } @@ -1709,7 +1961,7 @@ class ShareAPIController extends OCSController { if ($this->shareProviderResharingRights($viewer, $share, $node)) { return true; } - } catch (InvalidPathException | NotFoundException $e) { + } catch (InvalidPathException|NotFoundException $e) { } } } @@ -1741,7 +1993,7 @@ class ShareAPIController extends OCSController { return true; } - if ((\OCP\Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) { + if ((Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) { return false; } @@ -1753,9 +2005,9 @@ class ShareAPIController extends OCSController { return true; } - if ($share->getShareType() === IShare::TYPE_CIRCLE && \OC::$server->getAppManager()->isEnabledForUser('circles') + if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\Api\v1\Circles')) { - $hasCircleId = (substr($share->getSharedWith(), -1) === ']'); + $hasCircleId = (str_ends_with($share->getSharedWith(), ']')); $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0); $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' ')); if ($shareWithLength === false) { @@ -1764,12 +2016,12 @@ class ShareAPIController extends OCSController { $sharedWith = substr($share->getSharedWith(), $shareWithStart, $shareWithLength); } try { - $member = \OCA\Circles\Api\v1\Circles::getMember($sharedWith, $userId, 1); + $member = Circles::getMember($sharedWith, $userId, 1); if ($member->getLevel() >= 4) { return true; } return false; - } catch (QueryException $e) { + } catch (ContainerExceptionInterface $e) { return false; } } @@ -1786,34 +2038,38 @@ class ShareAPIController extends OCSController { */ private function getAllShares(?Node $path = null, bool $reshares = false) { // Get all shares - $userShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_USER, $path, $reshares, -1, 0); - $groupShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_GROUP, $path, $reshares, -1, 0); - $linkShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_LINK, $path, $reshares, -1, 0); + $userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0); + $groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0); + $linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0); // EMAIL SHARES - $mailShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_EMAIL, $path, $reshares, -1, 0); + $mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0); - // CIRCLE SHARES - $circleShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0); + // TEAM SHARES + $circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0); // TALK SHARES - $roomShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_ROOM, $path, $reshares, -1, 0); + $roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0); + + // DECK SHARES + $deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0); - $deckShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_DECK, $path, $reshares, -1, 0); + // SCIENCEMESH SHARES + $sciencemeshShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_SCIENCEMESH, $path, $reshares, -1, 0); // FEDERATION if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { - $federatedShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_REMOTE, $path, $reshares, -1, 0); + $federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0); } else { $federatedShares = []; } if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) { - $federatedGroupShares = $this->shareManager->getSharesBy($this->currentUser, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0); + $federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0); } else { $federatedGroupShares = []; } - return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $federatedShares, $federatedGroupShares); + return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $sciencemeshShares, $federatedShares, $federatedGroupShares); } @@ -1832,4 +2088,208 @@ class ShareAPIController extends OCSController { } } } + + /** + * @param IShare $share + * @param string|null $attributesString + * @return IShare modified share + */ + private function setShareAttributes(IShare $share, ?string $attributesString) { + $newShareAttributes = null; + if ($attributesString !== null) { + $newShareAttributes = $this->shareManager->newShare()->newAttributes(); + $formattedShareAttributes = \json_decode($attributesString, true); + if (is_array($formattedShareAttributes)) { + foreach ($formattedShareAttributes as $formattedAttr) { + $newShareAttributes->setAttribute( + $formattedAttr['scope'], + $formattedAttr['key'], + $formattedAttr['value'], + ); + } + } else { + throw new OCSBadRequestException($this->l->t('Invalid share attributes provided: "%s"', [$attributesString])); + } + } + $share->setAttributes($newShareAttributes); + + return $share; + } + + private function checkInheritedAttributes(IShare $share): void { + if (!$share->getSharedBy()) { + return; // Probably in a test + } + + $canDownload = false; + $hideDownload = true; + + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); + $nodes = $userFolder->getById($share->getNodeId()); + foreach ($nodes as $node) { + // Owner always can download it - so allow it and break + if ($node->getOwner()?->getUID() === $share->getSharedBy()) { + $canDownload = true; + $hideDownload = false; + break; + } + + if ($node->getStorage()->instanceOfStorage(SharedStorage::class)) { + $storage = $node->getStorage(); + if ($storage instanceof Wrapper) { + $storage = $storage->getInstanceOfStorage(SharedStorage::class); + if ($storage === null) { + throw new \RuntimeException('Should not happen, instanceOfStorage but getInstanceOfStorage return null'); + } + } else { + throw new \RuntimeException('Should not happen, instanceOfStorage but not a wrapper'); + } + + /** @var SharedStorage $storage */ + $originalShare = $storage->getShare(); + $inheritedAttributes = $originalShare->getAttributes(); + // hide if hidden and also the current share enforces hide (can only be false if one share is false or user is owner) + $hideDownload = $hideDownload && $originalShare->getHideDownload(); + // allow download if already allowed by previous share or when the current share allows downloading + $canDownload = $canDownload || $inheritedAttributes === null || $inheritedAttributes->getAttribute('permissions', 'download') !== false; + } elseif ($node->getStorage()->instanceOfStorage(Storage::class)) { + $canDownload = true; // in case of federation storage, we can expect the download to be activated by default + } + } + + if ($hideDownload || !$canDownload) { + $share->setHideDownload(true); + + if (!$canDownload) { + $attributes = $share->getAttributes() ?? $share->newAttributes(); + $attributes->setAttribute('permissions', 'download', false); + $share->setAttributes($attributes); + } + } + } + + /** + * Send a mail notification again for a share. + * The mail_send option must be enabled for the given share. + * @param string $id the share ID + * @param string $password the password to check against. Necessary for password protected shares. + * @throws OCSNotFoundException Share not found + * @throws OCSForbiddenException You are not allowed to send mail notifications + * @throws OCSBadRequestException Invalid request or wrong password + * @throws OCSException Error while sending mail notification + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * + * 200: The email notification was sent successfully + */ + #[NoAdminRequired] + #[UserRateLimit(limit: 10, period: 600)] + public function sendShareEmail(string $id, $password = ''): DataResponse { + try { + $share = $this->getShareById($id); + + if (!$this->canAccessShare($share, false)) { + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); + } + + if (!$this->canEditShare($share)) { + throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications')); + } + + // For mail and link shares, the user must be + // the owner of the share, not only the file owner. + if ($share->getShareType() === IShare::TYPE_EMAIL + || $share->getShareType() === IShare::TYPE_LINK) { + if ($share->getSharedBy() !== $this->userId) { + throw new OCSForbiddenException($this->l->t('You are not allowed to send mail notifications')); + } + } + + try { + $provider = $this->factory->getProviderForType($share->getShareType()); + if (!($provider instanceof IShareProviderWithNotification)) { + throw new OCSBadRequestException($this->l->t('No mail notification configured for this share type')); + } + + // Circumvent the password encrypted data by + // setting the password clear. We're not storing + // the password clear, it is just a temporary + // object manipulation. The password will stay + // encrypted in the database. + if ($share->getPassword() !== null && $share->getPassword() !== $password) { + if (!$this->shareManager->checkPassword($share, $password)) { + throw new OCSBadRequestException($this->l->t('Wrong password')); + } + $share = $share->setPassword($password); + } + + $provider->sendMailNotification($share); + return new DataResponse(); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSException($this->l->t('Error while sending mail notification')); + } + + } catch (ShareNotFound $e) { + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); + } + } + + /** + * Get a unique share token + * + * @throws OCSException Failed to generate a unique token + * + * @return DataResponse<Http::STATUS_OK, array{token: string}, array{}> + * + * 200: Token generated successfully + */ + #[ApiRoute(verb: 'GET', url: '/api/v1/token')] + #[NoAdminRequired] + public function generateToken(): DataResponse { + try { + $token = $this->shareManager->generateToken(); + return new DataResponse([ + 'token' => $token, + ]); + } catch (ShareTokenException $e) { + throw new OCSException($this->l->t('Failed to generate a unique token')); + } + } + + /** + * Populate the result set with file tags + * + * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>} + * @param list<T> $fileList + * @return list<T> file list populated with tags + */ + private function populateTags(array $fileList): array { + $tagger = $this->tagManager->load('files'); + $tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList)); + + if (!is_array($tags)) { + throw new \UnexpectedValueException('$tags must be an array'); + } + + // Set empty tag array + foreach ($fileList as &$fileData) { + $fileData['tags'] = []; + } + unset($fileData); + + if (!empty($tags)) { + foreach ($tags as $fileId => $fileTags) { + foreach ($fileList as &$fileData) { + if ($fileId !== $fileData['file_source']) { + continue; + } + + $fileData['tags'] = $fileTags; + } + unset($fileData); + } + } + + return $fileList; + } } diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index a12878e6de2..5a776379fce 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -1,150 +1,90 @@ <?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 Georg Ehrke <oc.list@georgehrke.com> - * @author j3l11234 <297259024@qq.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jonas Sulzer <jonas@violoncello.ch> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author MartB <mart.b@outlook.de> - * @author Maxence Lange <maxence@pontapreta.net> - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Piotr Filiciak <piotr@filiciak.pl> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sascha Sambale <mastixmc@gmail.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @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 OCA\Files_Sharing\Controller; use OC\Security\CSP\ContentSecurityPolicy; -use OC_Files; -use OC_Util; +use OCA\DAV\Connector\Sabre\PublicAuth; use OCA\FederatedFileSharing\FederatedShareProvider; -use OCA\Files_Sharing\Activity\Providers\Downloads; use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; use OCA\Files_Sharing\Event\ShareLinkAccessedEvent; -use OCA\Viewer\Event\LoadViewer; use OCP\Accounts\IAccountManager; use OCP\AppFramework\AuthPublicShareController; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\NotFoundResponse; -use OCP\AppFramework\Http\Template\ExternalShareMenuAction; -use OCP\AppFramework\Http\Template\LinkMenuAction; -use OCP\AppFramework\Http\Template\PublicTemplateResponse; -use OCP\AppFramework\Http\Template\SimpleMenuAction; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; +use OCP\Constants; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\HintException; use OCP\IConfig; use OCP\IL10N; -use OCP\ILogger; use OCP\IPreview; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; -use OCP\IUser; use OCP\IUserManager; +use OCP\Security\Events\GenerateSecurePasswordEvent; use OCP\Security\ISecureRandom; +use OCP\Security\PasswordContext; use OCP\Share; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as ShareManager; +use OCP\Share\IPublicShareTemplateFactory; use OCP\Share\IShare; -use OCP\Template; /** - * Class ShareController - * * @package OCA\Files_Sharing\Controllers */ +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class ShareController extends AuthPublicShareController { - protected IConfig $config; - protected IUserManager $userManager; - protected ILogger $logger; - protected \OCP\Activity\IManager $activityManager; - protected IPreview $previewManager; - protected IRootFolder $rootFolder; - protected FederatedShareProvider $federatedShareProvider; - protected IAccountManager $accountManager; - protected IEventDispatcher $eventDispatcher; - protected IL10N $l10n; - protected Defaults $defaults; - protected ShareManager $shareManager; - protected ISecureRandom $secureRandom; - protected ?Share\IShare $share = null; - - public function __construct(string $appName, - IRequest $request, - IConfig $config, - IURLGenerator $urlGenerator, - IUserManager $userManager, - ILogger $logger, - \OCP\Activity\IManager $activityManager, - ShareManager $shareManager, - ISession $session, - IPreview $previewManager, - IRootFolder $rootFolder, - FederatedShareProvider $federatedShareProvider, - IAccountManager $accountManager, - IEventDispatcher $eventDispatcher, - IL10N $l10n, - ISecureRandom $secureRandom, - Defaults $defaults) { - parent::__construct($appName, $request, $session, $urlGenerator); - - $this->config = $config; - $this->userManager = $userManager; - $this->logger = $logger; - $this->activityManager = $activityManager; - $this->previewManager = $previewManager; - $this->rootFolder = $rootFolder; - $this->federatedShareProvider = $federatedShareProvider; - $this->accountManager = $accountManager; - $this->eventDispatcher = $eventDispatcher; - $this->l10n = $l10n; - $this->secureRandom = $secureRandom; - $this->defaults = $defaults; - $this->shareManager = $shareManager; - } + protected ?IShare $share = null; public const SHARE_ACCESS = 'access'; public const SHARE_AUTH = 'auth'; public const SHARE_DOWNLOAD = 'download'; + public function __construct( + string $appName, + IRequest $request, + protected IConfig $config, + IURLGenerator $urlGenerator, + protected IUserManager $userManager, + protected \OCP\Activity\IManager $activityManager, + protected ShareManager $shareManager, + ISession $session, + protected IPreview $previewManager, + protected IRootFolder $rootFolder, + protected FederatedShareProvider $federatedShareProvider, + protected IAccountManager $accountManager, + protected IEventDispatcher $eventDispatcher, + protected IL10N $l10n, + protected ISecureRandom $secureRandom, + protected Defaults $defaults, + private IPublicShareTemplateFactory $publicShareTemplateFactory, + ) { + parent::__construct($appName, $request, $session, $urlGenerator); + } + /** - * @PublicPage - * @NoCSRFRequired - * * Show the authentication page * The form has to submit to the authenticate method route */ + #[PublicPage] + #[NoCSRFRequired] public function showAuthenticate(): TemplateResponse { $templateParameters = ['share' => $this->share]; @@ -206,7 +146,6 @@ class ShareController extends AuthPublicShareController { * @return bool */ protected function validateIdentity(?string $identityToken = null): bool { - if ($this->share->getShareType() !== IShare::TYPE_EMAIL) { return false; } @@ -222,7 +161,7 @@ class ShareController extends AuthPublicShareController { * Generates a password for the share, respecting any password policy defined */ protected function generatePassword(): void { - $event = new \OCP\Security\Events\GenerateSecurePasswordEvent(); + $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING); $this->eventDispatcher->dispatchTyped($event); $password = $event->getPassword() ?? $this->secureRandom->generate(20); @@ -234,7 +173,7 @@ class ShareController extends AuthPublicShareController { return $this->shareManager->checkPassword($this->share, $password); } - protected function getPasswordHash(): string { + protected function getPasswordHash(): ?string { return $this->share->getPassword(); } @@ -253,8 +192,12 @@ class ShareController extends AuthPublicShareController { } protected function authSucceeded() { + if ($this->share === null) { + throw new NotFoundException(); + } + // For share this was always set so it is still used in other apps - $this->session->set('public_link_authenticated', (string)$this->share->getId()); + $this->session->set(PublicAuth::DAV_AUTHENTICATED, $this->share->getId()); } protected function authFailed() { @@ -265,12 +208,12 @@ class ShareController extends AuthPublicShareController { /** * throws hooks when a share is attempted to be accessed * - * @param \OCP\Share\IShare|string $share the Share instance if available, - * otherwise token + * @param IShare|string $share the Share instance if available, + * otherwise token * @param int $errorCode * @param string $errorMessage * - * @throws \OCP\HintException + * @throws HintException * @throws \OC\ServerNotAvailableException * * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent @@ -279,7 +222,7 @@ class ShareController extends AuthPublicShareController { $itemType = $itemSource = $uidOwner = ''; $token = $share; $exception = null; - if ($share instanceof \OCP\Share\IShare) { + if ($share instanceof IShare) { try { $token = $share->getToken(); $uidOwner = $share->getSharedBy(); @@ -309,9 +252,9 @@ class ShareController extends AuthPublicShareController { * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth... */ protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void { - if ($step !== self::SHARE_ACCESS && - $step !== self::SHARE_AUTH && - $step !== self::SHARE_DOWNLOAD) { + if ($step !== self::SHARE_ACCESS + && $step !== self::SHARE_AUTH + && $step !== self::SHARE_DOWNLOAD) { return; } $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage)); @@ -323,8 +266,8 @@ class ShareController extends AuthPublicShareController { * @param Share\IShare $share * @return bool */ - private function validateShare(\OCP\Share\IShare $share) { - // If the owner is disabled no access to the linke is granted + private function validateShare(IShare $share) { + // If the owner is disabled no access to the link is granted $owner = $this->userManager->get($share->getShareOwner()); if ($owner === null || !$owner->isEnabled()) { return false; @@ -340,15 +283,13 @@ class ShareController extends AuthPublicShareController { } /** - * @PublicPage - * @NoCSRFRequired - * - * * @param string $path * @return TemplateResponse * @throws NotFoundException * @throws \Exception */ + #[PublicPage] + #[NoCSRFRequired] public function showShare($path = ''): TemplateResponse { \OC_User::setIncognitoMode(true); @@ -358,21 +299,30 @@ class ShareController extends AuthPublicShareController { } catch (ShareNotFound $e) { // The share does not exists, we do not emit an ShareLinkAccessedEvent $this->emitAccessShareHook($this->getToken(), 404, 'Share not found'); - throw new NotFoundException(); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); } if (!$this->validateShare($share)) { - throw new NotFoundException(); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); } $shareNode = $share->getNode(); + try { + $templateProvider = $this->publicShareTemplateFactory->getProvider($share); + $response = $templateProvider->renderPage($share, $this->getToken(), $path); + } catch (NotFoundException $e) { + $this->emitAccessShareHook($share, 404, 'Share not found'); + $this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found'); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); + } + // We can't get the path of a file share try { - if ($shareNode instanceof \OCP\Files\File && $path !== '') { + if ($shareNode instanceof File && $path !== '') { $this->emitAccessShareHook($share, 404, 'Share not found'); $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found'); - throw new NotFoundException(); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); } } catch (\Exception $e) { $this->emitAccessShareHook($share, 404, 'Share not found'); @@ -380,206 +330,6 @@ class ShareController extends AuthPublicShareController { throw $e; } - $shareTmpl = []; - $shareTmpl['owner'] = ''; - $shareTmpl['shareOwner'] = ''; - - $owner = $this->userManager->get($share->getShareOwner()); - if ($owner instanceof IUser) { - $ownerAccount = $this->accountManager->getAccount($owner); - - $ownerName = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); - if ($ownerName->getScope() === IAccountManager::SCOPE_PUBLISHED) { - $shareTmpl['owner'] = $owner->getUID(); - $shareTmpl['shareOwner'] = $owner->getDisplayName(); - } - } - - $shareTmpl['filename'] = $shareNode->getName(); - $shareTmpl['directory_path'] = $share->getTarget(); - $shareTmpl['note'] = $share->getNote(); - $shareTmpl['mimetype'] = $shareNode->getMimetype(); - $shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($shareNode->getMimetype()); - $shareTmpl['dirToken'] = $this->getToken(); - $shareTmpl['sharingToken'] = $this->getToken(); - $shareTmpl['server2serversharing'] = $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); - $shareTmpl['protected'] = $share->getPassword() !== null ? 'true' : 'false'; - $shareTmpl['dir'] = ''; - $shareTmpl['nonHumanFileSize'] = $shareNode->getSize(); - $shareTmpl['fileSize'] = \OCP\Util::humanFileSize($shareNode->getSize()); - $shareTmpl['hideDownload'] = $share->getHideDownload(); - - $hideFileList = false; - - if ($shareNode instanceof \OCP\Files\Folder) { - $shareIsFolder = true; - - try { - $folderNode = $shareNode->get($path); - } catch (\OCP\Files\NotFoundException $e) { - $this->emitAccessShareHook($share, 404, 'Share not found'); - $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found'); - throw new NotFoundException(); - } - - $shareTmpl['dir'] = $shareNode->getRelativePath($folderNode->getPath()); - - /* - * The OC_Util methods require a view. This just uses the node API - */ - $freeSpace = $share->getNode()->getStorage()->free_space($share->getNode()->getInternalPath()); - if ($freeSpace < \OCP\Files\FileInfo::SPACE_UNLIMITED) { - $freeSpace = max($freeSpace, 0); - } else { - $freeSpace = (INF > 0) ? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 - } - - $hideFileList = !($share->getPermissions() & \OCP\Constants::PERMISSION_READ); - $maxUploadFilesize = $freeSpace; - - $folder = new Template('files', 'list', ''); - - $folder->assign('dir', $shareNode->getRelativePath($folderNode->getPath())); - $folder->assign('dirToken', $this->getToken()); - $folder->assign('permissions', \OCP\Constants::PERMISSION_READ); - $folder->assign('isPublic', true); - $folder->assign('hideFileList', $hideFileList); - $folder->assign('publicUploadEnabled', 'no'); - // default to list view - $folder->assign('showgridview', false); - $folder->assign('uploadMaxFilesize', $maxUploadFilesize); - $folder->assign('uploadMaxHumanFilesize', \OCP\Util::humanFileSize($maxUploadFilesize)); - $folder->assign('freeSpace', $freeSpace); - $folder->assign('usedSpacePercent', 0); - $folder->assign('trash', false); - $shareTmpl['folder'] = $folder->fetchPage(); - } else { - $shareIsFolder = false; - } - - // default to list view - $shareTmpl['showgridview'] = false; - - $shareTmpl['hideFileList'] = $hideFileList; - $shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', [ - 'token' => $this->getToken(), - 'filename' => $shareIsFolder ? null : $shareNode->getName() - ]); - $shareTmpl['shareUrl'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $this->getToken()]); - $shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10); - $shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true); - $shareTmpl['previewMaxX'] = $this->config->getSystemValue('preview_max_x', 1024); - $shareTmpl['previewMaxY'] = $this->config->getSystemValue('preview_max_y', 1024); - $shareTmpl['disclaimer'] = $this->config->getAppValue('core', 'shareapi_public_link_disclaimertext', null); - $shareTmpl['previewURL'] = $shareTmpl['downloadURL']; - - if ($shareTmpl['previewSupported']) { - $shareTmpl['previewImage'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', - ['x' => 200, 'y' => 200, 'file' => $shareTmpl['directory_path'], 'token' => $shareTmpl['dirToken']]); - $ogPreview = $shareTmpl['previewImage']; - - // We just have direct previews for image files - if ($shareNode->getMimePart() === 'image') { - $shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $this->getToken()]); - - $ogPreview = $shareTmpl['previewURL']; - - //Whatapp is kind of picky about their size requirements - if ($this->request->isUserAgent(['/^WhatsApp/'])) { - $ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', [ - 'token' => $this->getToken(), - 'x' => 256, - 'y' => 256, - 'a' => true, - ]); - } - } - } else { - $shareTmpl['previewImage'] = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-fb.png')); - $ogPreview = $shareTmpl['previewImage']; - } - - // Load files we need - \OCP\Util::addScript('files', 'semaphore'); - \OCP\Util::addScript('files', 'file-upload'); - \OCP\Util::addStyle('files_sharing', 'publicView'); - \OCP\Util::addScript('files_sharing', 'public'); - \OCP\Util::addScript('files_sharing', 'templates'); - \OCP\Util::addScript('files', 'fileactions'); - \OCP\Util::addScript('files', 'fileactionsmenu'); - \OCP\Util::addScript('files', 'jquery.fileupload'); - \OCP\Util::addScript('files_sharing', 'files_drop'); - - if (isset($shareTmpl['folder'])) { - // JS required for folders - \OCP\Util::addStyle('files', 'merged'); - \OCP\Util::addScript('files', 'filesummary'); - \OCP\Util::addScript('files', 'templates'); - \OCP\Util::addScript('files', 'breadcrumb'); - \OCP\Util::addScript('files', 'fileinfomodel'); - \OCP\Util::addScript('files', 'newfilemenu'); - \OCP\Util::addScript('files', 'files'); - \OCP\Util::addScript('files', 'filemultiselectmenu'); - \OCP\Util::addScript('files', 'filelist'); - \OCP\Util::addScript('files', 'keyboardshortcuts'); - \OCP\Util::addScript('files', 'operationprogressbar'); - - // Load Viewer scripts - if (class_exists(LoadViewer::class)) { - $this->eventDispatcher->dispatchTyped(new LoadViewer()); - } - } - - // OpenGraph Support: http://ogp.me/ - \OCP\Util::addHeader('meta', ['property' => "og:title", 'content' => $shareTmpl['filename']]); - \OCP\Util::addHeader('meta', ['property' => "og:description", 'content' => $this->defaults->getName() . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')]); - \OCP\Util::addHeader('meta', ['property' => "og:site_name", 'content' => $this->defaults->getName()]); - \OCP\Util::addHeader('meta', ['property' => "og:url", 'content' => $shareTmpl['shareUrl']]); - \OCP\Util::addHeader('meta', ['property' => "og:type", 'content' => "object"]); - \OCP\Util::addHeader('meta', ['property' => "og:image", 'content' => $ogPreview]); - - $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); - - $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); - $csp->addAllowedFrameDomain('\'self\''); - - $response = new PublicTemplateResponse($this->appName, 'public', $shareTmpl); - $response->setHeaderTitle($shareTmpl['filename']); - if ($shareTmpl['shareOwner'] !== '') { - $response->setHeaderDetails($this->l10n->t('shared by %s', [$shareTmpl['shareOwner']])); - } - - $isNoneFileDropFolder = $shareIsFolder === false || $share->getPermissions() !== \OCP\Constants::PERMISSION_CREATE; - - if ($isNoneFileDropFolder && !$share->getHideDownload()) { - \OCP\Util::addScript('files_sharing', 'public_note'); - - $downloadWhite = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download-white', $shareTmpl['downloadURL'], 0); - $downloadAllWhite = new SimpleMenuAction('download', $this->l10n->t('Download all files'), 'icon-download-white', $shareTmpl['downloadURL'], 0); - $download = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $shareTmpl['downloadURL'], 10, $shareTmpl['fileSize']); - $downloadAll = new SimpleMenuAction('download', $this->l10n->t('Download all files'), 'icon-download', $shareTmpl['downloadURL'], 10, $shareTmpl['fileSize']); - $directLink = new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $shareTmpl['previewURL']); - // TRANSLATORS The placeholder refers to the software product name as in 'Add to your Nextcloud' - $externalShare = new ExternalShareMenuAction($this->l10n->t('Add to your %s', [$this->defaults->getProductName()]), 'icon-external', $shareTmpl['owner'], $shareTmpl['shareOwner'], $shareTmpl['filename']); - - $responseComposer = []; - - if ($shareIsFolder) { - $responseComposer[] = $downloadAllWhite; - $responseComposer[] = $downloadAll; - } else { - $responseComposer[] = $downloadWhite; - $responseComposer[] = $download; - } - $responseComposer[] = $directLink; - if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { - $responseComposer[] = $externalShare; - } - - $response->setHeaderActions($responseComposer); - } - - $response->setContentSecurityPolicy($csp); $this->emitAccessShareHook($share); $this->emitShareAccessEvent($share, self::SHARE_ACCESS); @@ -588,56 +338,38 @@ class ShareController extends AuthPublicShareController { } /** - * @PublicPage - * @NoCSRFRequired * @NoSameSiteCookieRequired * * @param string $token - * @param string $files + * @param string|null $files * @param string $path - * @param string $downloadStartSecret - * @return void|\OCP\AppFramework\Http\Response + * @return void|Response * @throws NotFoundException + * @deprecated 31.0.0 Users are encouraged to use the DAV endpoint */ - public function downloadShare($token, $files = null, $path = '', $downloadStartSecret = '') { + #[PublicPage] + #[NoCSRFRequired] + public function downloadShare($token, $files = null, $path = '') { \OC_User::setIncognitoMode(true); $share = $this->shareManager->getShareByToken($token); - if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) { - return new \OCP\AppFramework\Http\DataResponse('Share has no read permission'); + if (!($share->getPermissions() & Constants::PERMISSION_READ)) { + return new DataResponse('Share has no read permission'); } - $files_list = null; - if (!is_null($files)) { // download selected files - $files_list = json_decode($files); - // in case we get only a single file - if ($files_list === null) { - $files_list = [$files]; - } - // Just in case $files is a single int like '1234' - if (!is_array($files_list)) { - $files_list = [$files_list]; - } + $attributes = $share->getAttributes(); + if ($attributes?->getAttribute('permissions', 'download') === false) { + return new DataResponse('Share has no download permission'); } if (!$this->validateShare($share)) { throw new NotFoundException(); } - $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); - $originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath()); - - - // Single file share - if ($share->getNode() instanceof \OCP\Files\File) { - // Single file download - $this->singleFileDownloaded($share, $share->getNode()); - } - // Directory share - else { - /** @var \OCP\Files\Folder $node */ - $node = $share->getNode(); + $node = $share->getNode(); + if ($node instanceof Folder) { + // Directory share // Try to get the path if ($path !== '') { @@ -650,158 +382,22 @@ class ShareController extends AuthPublicShareController { } } - $originalSharePath = $userFolder->getRelativePath($node->getPath()); - - if ($node instanceof \OCP\Files\File) { - // Single file download - $this->singleFileDownloaded($share, $share->getNode()); - } else { - try { - if (!empty($files_list)) { - $this->fileListDownloaded($share, $files_list, $node); - } else { - // The folder is downloaded - $this->singleFileDownloaded($share, $share->getNode()); + if ($node instanceof Folder) { + if ($files === null || $files === '') { + if ($share->getHideDownload()) { + throw new NotFoundException('Downloading a folder'); } - } catch (NotFoundException $e) { - return new NotFoundResponse(); } } } - /* FIXME: We should do this all nicely in OCP */ - OC_Util::tearDownFS(); - OC_Util::setupFS($share->getShareOwner()); - - /** - * this sets a cookie to be able to recognize the start of the download - * the content must not be longer than 32 characters and must only contain - * alphanumeric characters - */ - if (!empty($downloadStartSecret) - && !isset($downloadStartSecret[32]) - && preg_match('!^[a-zA-Z0-9]+$!', $downloadStartSecret) === 1) { - - // FIXME: set on the response once we use an actual app framework response - setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/'); - } - $this->emitAccessShareHook($share); $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD); - $server_params = [ 'head' => $this->request->getMethod() === 'HEAD' ]; - - /** - * Http range requests support - */ - if (isset($_SERVER['HTTP_RANGE'])) { - $server_params['range'] = $this->request->getHeader('Range'); - } - - // download selected files - if (!is_null($files) && $files !== '') { - // FIXME: The exit is required here because otherwise the AppFramework is trying to add headers as well - // after dispatching the request which results in a "Cannot modify header information" notice. - OC_Files::get($originalSharePath, $files_list, $server_params); - exit(); - } else { - // FIXME: The exit is required here because otherwise the AppFramework is trying to add headers as well - // after dispatching the request which results in a "Cannot modify header information" notice. - OC_Files::get(dirname($originalSharePath), basename($originalSharePath), $server_params); - exit(); + $davUrl = '/public.php/dav/files/' . $token . '/?accept=zip'; + if ($files !== null) { + $davUrl .= '&files=' . $files; } - } - - /** - * create activity for every downloaded file - * - * @param Share\IShare $share - * @param array $files_list - * @param \OCP\Files\Folder $node - * @throws NotFoundException when trying to download a folder or multiple files of a "hide download" share - */ - protected function fileListDownloaded(Share\IShare $share, array $files_list, \OCP\Files\Folder $node) { - if ($share->getHideDownload() && count($files_list) > 1) { - throw new NotFoundException('Downloading more than 1 file'); - } - - foreach ($files_list as $file) { - $subNode = $node->get($file); - $this->singleFileDownloaded($share, $subNode); - } - } - - /** - * create activity if a single file was downloaded from a link share - * - * @param Share\IShare $share - * @throws NotFoundException when trying to download a folder of a "hide download" share - */ - protected function singleFileDownloaded(Share\IShare $share, \OCP\Files\Node $node) { - if ($share->getHideDownload() && $node instanceof Folder) { - throw new NotFoundException('Downloading a folder'); - } - - $fileId = $node->getId(); - - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); - $userNodeList = $userFolder->getById($fileId); - $userNode = $userNodeList[0]; - $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); - $userPath = $userFolder->getRelativePath($userNode->getPath()); - $ownerPath = $ownerFolder->getRelativePath($node->getPath()); - $remoteAddress = $this->request->getRemoteAddress(); - $dateTime = new \DateTime(); - $dateTime = $dateTime->format('Y-m-d H'); - $remoteAddressHash = md5($dateTime . '-' . $remoteAddress); - - $parameters = [$userPath]; - - if ($share->getShareType() === IShare::TYPE_EMAIL) { - if ($node instanceof \OCP\Files\File) { - $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED; - } else { - $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED; - } - $parameters[] = $share->getSharedWith(); - } else { - if ($node instanceof \OCP\Files\File) { - $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED; - $parameters[] = $remoteAddressHash; - } else { - $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED; - $parameters[] = $remoteAddressHash; - } - } - - $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath); - - if ($share->getShareOwner() !== $share->getSharedBy()) { - $parameters[0] = $ownerPath; - $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath); - } - } - - /** - * publish activity - * - * @param string $subject - * @param array $parameters - * @param string $affectedUser - * @param int $fileId - * @param string $filePath - */ - protected function publishActivity($subject, - array $parameters, - $affectedUser, - $fileId, - $filePath) { - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType('public_links') - ->setSubject($subject, $parameters) - ->setAffectedUser($affectedUser) - ->setObject('files', $fileId, $filePath); - $this->activityManager->publish($event); + return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl)); } } diff --git a/apps/files_sharing/lib/Controller/ShareInfoController.php b/apps/files_sharing/lib/Controller/ShareInfoController.php index 429eb91bc92..b7e79aec830 100644 --- a/apps/files_sharing/lib/Controller/ShareInfoController.php +++ b/apps/files_sharing/lib/Controller/ShareInfoController.php @@ -1,31 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Controller; use OCA\Files_External\NotFoundException; +use OCA\Files_Sharing\ResponseDefinitions; use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\JSONResponse; use OCP\Constants; use OCP\Files\File; @@ -35,11 +23,11 @@ use OCP\IRequest; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; +/** + * @psalm-import-type Files_SharingShareInfo from ResponseDefinitions + */ class ShareInfoController extends ApiController { - /** @var IManager */ - private $shareManager; - /** * ShareInfoController constructor. * @@ -47,25 +35,32 @@ class ShareInfoController extends ApiController { * @param IRequest $request * @param IManager $shareManager */ - public function __construct(string $appName, - IRequest $request, - IManager $shareManager) { + public function __construct( + string $appName, + IRequest $request, + private IManager $shareManager, + ) { parent::__construct($appName, $request); - - $this->shareManager = $shareManager; } /** - * @PublicPage - * @NoCSRFRequired - * @BruteForceProtection(action=shareinfo) + * Get the info about a share + * + * @param string $t Token of the share + * @param string|null $password Password of the share + * @param string|null $dir Subdirectory to get info about + * @param int $depth Maximum depth to get info about + * @return JSONResponse<Http::STATUS_OK, Files_SharingShareInfo, array{}>|JSONResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}> * - * @param string $t - * @param null $password - * @param null $dir - * @return JSONResponse + * 200: Share info returned + * 403: Getting share info is not allowed + * 404: Share not found */ - public function info($t, $password = null, $dir = null) { + #[PublicPage] + #[NoCSRFRequired] + #[BruteForceProtection(action: 'shareinfo')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function info(string $t, ?string $password = null, ?string $dir = null, int $depth = -1): JSONResponse { try { $share = $this->shareManager->getShareByToken($t); } catch (ShareNotFound $e) { @@ -96,34 +91,51 @@ class ShareInfoController extends ApiController { } } - return new JSONResponse($this->parseNode($node, $permissionMask)); + return new JSONResponse($this->parseNode($node, $permissionMask, $depth)); } - private function parseNode(Node $node, int $permissionMask) { + /** + * @return Files_SharingShareInfo + */ + private function parseNode(Node $node, int $permissionMask, int $depth): array { if ($node instanceof File) { return $this->parseFile($node, $permissionMask); } - return $this->parseFolder($node, $permissionMask); + /** @var Folder $node */ + return $this->parseFolder($node, $permissionMask, $depth); } - private function parseFile(File $file, int $permissionMask) { + /** + * @return Files_SharingShareInfo + */ + private function parseFile(File $file, int $permissionMask): array { return $this->format($file, $permissionMask); } - private function parseFolder(Folder $folder, int $permissionMask) { + /** + * @return Files_SharingShareInfo + */ + private function parseFolder(Folder $folder, int $permissionMask, int $depth): array { $data = $this->format($folder, $permissionMask); + if ($depth === 0) { + return $data; + } + $data['children'] = []; $nodes = $folder->getDirectoryListing(); foreach ($nodes as $node) { - $data['children'][] = $this->parseNode($node, $permissionMask); + $data['children'][] = $this->parseNode($node, $permissionMask, $depth <= -1 ? -1 : $depth - 1); } return $data; } - private function format(Node $node, int $permissionMask) { + /** + * @return Files_SharingShareInfo + */ + private function format(Node $node, int $permissionMask): array { $entry = []; $entry['id'] = $node->getId(); diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index 399cd4e00be..0c458ce9662 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -1,81 +1,51 @@ <?php declare(strict_types=1); - /** - * @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 Daniel Kesselberg <mail@danielkesselberg.de> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Maxence Lange <maxence@nextcloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 OCA\Files_Sharing\Controller; -use OCP\Constants; -use function array_slice; -use function array_values; use Generator; use OC\Collaboration\Collaborators\SearchResult; +use OC\Share\Share; +use OCA\Files_Sharing\ResponseDefinitions; +use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCSController; use OCP\Collaboration\Collaborators\ISearch; use OCP\Collaboration\Collaborators\ISearchResult; use OCP\Collaboration\Collaborators\SearchResultType; +use OCP\Constants; +use OCP\GlobalScale\IConfig as GlobalScaleIConfig; use OCP\IConfig; use OCP\IRequest; use OCP\IURLGenerator; -use OCP\Share\IShare; +use OCP\Server; use OCP\Share\IManager; +use OCP\Share\IShare; +use function array_slice; +use function array_values; use function usort; +/** + * @psalm-import-type Files_SharingShareesSearchResult from ResponseDefinitions + * @psalm-import-type Files_SharingShareesRecommendedResult from ResponseDefinitions + */ class ShareesAPIController extends OCSController { - /** @var string */ - protected $userId; - - /** @var IConfig */ - protected $config; - - /** @var IURLGenerator */ - protected $urlGenerator; - - /** @var IManager */ - protected $shareManager; - /** @var int */ protected $offset = 0; /** @var int */ protected $limit = 10; - /** @var array */ + /** @var Files_SharingShareesSearchResult */ protected $result = [ 'exact' => [ 'users' => [], @@ -85,7 +55,6 @@ class ShareesAPIController extends OCSController { 'emails' => [], 'circles' => [], 'rooms' => [], - 'deck' => [], ], 'users' => [], 'groups' => [], @@ -95,53 +64,39 @@ class ShareesAPIController extends OCSController { 'lookup' => [], 'circles' => [], 'rooms' => [], - 'deck' => [], 'lookupEnabled' => false, ]; protected $reachedEndFor = []; - /** @var ISearch */ - private $collaboratorSearch; - /** - * @param string $UserId - * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param IURLGenerator $urlGenerator - * @param IManager $shareManager - * @param ISearch $collaboratorSearch - */ public function __construct( - $UserId, string $appName, IRequest $request, - IConfig $config, - IURLGenerator $urlGenerator, - IManager $shareManager, - ISearch $collaboratorSearch + protected ?string $userId, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IManager $shareManager, + protected ISearch $collaboratorSearch, ) { parent::__construct($appName, $request); - $this->userId = $UserId; - $this->config = $config; - $this->urlGenerator = $urlGenerator; - $this->shareManager = $shareManager; - $this->collaboratorSearch = $collaboratorSearch; } /** - * @NoAdminRequired + * Search for sharees * - * @param string $search - * @param string $itemType - * @param int $page - * @param int $perPage - * @param int|int[] $shareType - * @param bool $lookup - * @return DataResponse - * @throws OCSBadRequestException + * @param string $search Text to search for + * @param string|null $itemType Limit to specific item types + * @param int $page Page offset for searching + * @param int $perPage Limit amount of search results per page + * @param int|list<int>|null $shareType Limit to specific share types + * @param bool $lookup If a global lookup should be performed too + * @return DataResponse<Http::STATUS_OK, Files_SharingShareesSearchResult, array{Link?: string}> + * @throws OCSBadRequestException Invalid search parameters + * + * 200: Sharees search result returned */ - public function search(string $search = '', string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse { + #[NoAdminRequired] + public function search(string $search = '', ?string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse { // only search for string larger than a given threshold $threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0); @@ -149,6 +104,10 @@ class ShareesAPIController extends OCSController { return new DataResponse($this->result); } + if ($this->shareManager->sharingDisabledForUser($this->userId)) { + return new DataResponse($this->result); + } + // never return more than the max. number of results configured in the config.php $maxResults = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT); if ($maxResults > 0) { @@ -188,8 +147,8 @@ class ShareesAPIController extends OCSController { $shareTypes[] = IShare::TYPE_ROOM; } - if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) { - $shareTypes[] = IShare::TYPE_DECK; + if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) { + $shareTypes[] = IShare::TYPE_SCIENCEMESH; } } else { if ($this->shareManager->allowGroupSharing()) { @@ -199,33 +158,29 @@ class ShareesAPIController extends OCSController { } // FIXME: DI - if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { + if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { $shareTypes[] = IShare::TYPE_CIRCLE; } - if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) { - $shareTypes[] = IShare::TYPE_DECK; + if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) { + $shareTypes[] = IShare::TYPE_SCIENCEMESH; } if ($shareType !== null && is_array($shareType)) { $shareTypes = array_intersect($shareTypes, $shareType); } elseif (is_numeric($shareType)) { - $shareTypes = array_intersect($shareTypes, [(int) $shareType]); + $shareTypes = array_intersect($shareTypes, [(int)$shareType]); } sort($shareTypes); $this->limit = $perPage; $this->offset = $perPage * ($page - 1); - // In global scale mode we always search the loogup server - if ($this->config->getSystemValueBool('gs.enabled', false)) { - $lookup = true; - $this->result['lookupEnabled'] = true; - } else { - $this->result['lookupEnabled'] = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'yes') === 'yes'; - } + // In global scale mode we always search the lookup server + $this->result['lookupEnabled'] = Server::get(GlobalScaleIConfig::class)->isGlobalScaleEnabled(); + // TODO: Reconsider using lookup server for non-global-scale federation - [$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $this->limit, $this->offset); + [$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $this->result['lookupEnabled'], $this->limit, $this->offset); // extra treatment for 'exact' subarray, with a single merge expected keys might be lost if (isset($result['exact'])) { @@ -235,12 +190,12 @@ class ShareesAPIController extends OCSController { $response = new DataResponse($this->result); if ($hasMoreResults) { - $response->addHeader('Link', $this->getPaginationLink($page, [ + $response->setHeaders(['Link' => $this->getPaginationLink($page, [ 'search' => $search, 'itemType' => $itemType, 'shareType' => $shareTypes, 'perPage' => $perPage, - ])); + ])]); } return $response; @@ -273,7 +228,7 @@ class ShareesAPIController extends OCSController { } private function sortShareesByFrequency(array $sharees): array { - usort($sharees, function (array $s1, array $s2) { + usort($sharees, function (array $s1, array $s2): int { return $s2['count'] - $s1['count']; }); return $sharees; @@ -293,7 +248,7 @@ class ShareesAPIController extends OCSController { $sharees = $this->getAllShareesByType($user, $shareType); $shareTypeResults = []; foreach ($sharees as [$sharee, $displayname]) { - if (!isset($this->searchResultTypeMap[$shareType])) { + if (!isset($this->searchResultTypeMap[$shareType]) || trim($sharee) === '') { continue; } @@ -332,20 +287,21 @@ class ShareesAPIController extends OCSController { } /** - * @NoAdminRequired + * Find recommended sharees * - * @param string $itemType - * @return DataResponse - * @throws OCSBadRequestException + * @param string $itemType Limit to specific item types + * @param int|list<int>|null $shareType Limit to specific share types + * @return DataResponse<Http::STATUS_OK, Files_SharingShareesRecommendedResult, array{}> + * + * 200: Recommended sharees returned */ - public function findRecommended(string $itemType = null, $shareType = null): DataResponse { + #[NoAdminRequired] + public function findRecommended(string $itemType, $shareType = null): DataResponse { $shareTypes = [ IShare::TYPE_USER, ]; - if ($itemType === null) { - throw new OCSBadRequestException('Missing itemType'); - } elseif ($itemType === 'file' || $itemType === 'folder') { + if ($itemType === 'file' || $itemType === 'folder') { if ($this->shareManager->allowGroupSharing()) { $shareTypes[] = IShare::TYPE_GROUP; } @@ -371,7 +327,7 @@ class ShareesAPIController extends OCSController { } // FIXME: DI - if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { + if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) { $shareTypes[] = IShare::TYPE_CIRCLE; } @@ -379,7 +335,7 @@ class ShareesAPIController extends OCSController { $shareTypes = array_intersect($shareTypes, $_GET['shareType']); sort($shareTypes); } elseif (is_numeric($shareType)) { - $shareTypes = array_intersect($shareTypes, [(int) $shareType]); + $shareTypes = array_intersect($shareTypes, [(int)$shareType]); sort($shareTypes); } @@ -397,7 +353,7 @@ class ShareesAPIController extends OCSController { protected function isRemoteSharingAllowed(string $itemType): bool { try { // FIXME: static foo makes unit testing unnecessarily difficult - $backend = \OC\Share\Share::getBackend($itemType); + $backend = Share::getBackend($itemType); return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE); } catch (\Exception $e) { return false; @@ -407,7 +363,7 @@ class ShareesAPIController extends OCSController { protected function isRemoteGroupSharingAllowed(string $itemType): bool { try { // FIXME: static foo makes unit testing unnecessarily difficult - $backend = \OC\Share\Share::getBackend($itemType); + $backend = Share::getBackend($itemType); return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE_GROUP); } catch (\Exception $e) { return false; diff --git a/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php new file mode 100644 index 00000000000..afba45cac4a --- /dev/null +++ b/apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php @@ -0,0 +1,261 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing; + +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\Files_Sharing\AppInfo\Application; +use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; +use OCA\Viewer\Event\LoadViewer; +use OCP\Accounts\IAccountManager; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\Template\ExternalShareMenuAction; +use OCP\AppFramework\Http\Template\LinkMenuAction; +use OCP\AppFramework\Http\Template\PublicTemplateResponse; +use OCP\AppFramework\Http\Template\SimpleMenuAction; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\Constants; +use OCP\Defaults; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\File; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IPreview; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Share\IPublicShareTemplateProvider; +use OCP\Share\IShare; +use OCP\Util; + +class DefaultPublicShareTemplateProvider implements IPublicShareTemplateProvider { + + public function __construct( + private IUserManager $userManager, + private IAccountManager $accountManager, + private IPreview $previewManager, + protected FederatedShareProvider $federatedShareProvider, + private IUrlGenerator $urlGenerator, + private IEventDispatcher $eventDispatcher, + private IL10N $l10n, + private Defaults $defaults, + private IConfig $config, + private IRequest $request, + private IInitialState $initialState, + private IAppConfig $appConfig, + ) { + } + + public function shouldRespond(IShare $share): bool { + return true; + } + + public function renderPage(IShare $share, string $token, string $path): TemplateResponse { + $shareNode = $share->getNode(); + $ownerName = ''; + $ownerId = ''; + + // Only make the share owner public if they allowed to show their name + $owner = $this->userManager->get($share->getShareOwner()); + if ($owner instanceof IUser) { + $ownerAccount = $this->accountManager->getAccount($owner); + + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); + if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { + $ownerId = $owner->getUID(); + $ownerName = $owner->getDisplayName(); + $this->initialState->provideInitialState('owner', $ownerId); + $this->initialState->provideInitialState('ownerDisplayName', $ownerName); + } + } + + $view = 'public-share'; + if ($shareNode instanceof File) { + $view = 'public-file-share'; + $this->initialState->provideInitialState('fileId', $shareNode->getId()); + } elseif (($share->getPermissions() & Constants::PERMISSION_CREATE) + && !($share->getPermissions() & Constants::PERMISSION_READ) + ) { + // share is a folder with create but no read permissions -> file drop only + $view = 'public-file-drop'; + // Only needed for file drops + $this->initialState->provideInitialState( + 'disclaimer', + $this->appConfig->getValueString('core', 'shareapi_public_link_disclaimertext'), + ); + // file drops do not request the root folder so we need to provide label and note if available + $this->initialState->provideInitialState('label', $share->getLabel()); + $this->initialState->provideInitialState('note', $share->getNote()); + } + // Set up initial state + $this->initialState->provideInitialState('isPublic', true); + $this->initialState->provideInitialState('sharingToken', $token); + $this->initialState->provideInitialState('sharePermissions', $share->getPermissions()); + $this->initialState->provideInitialState('filename', $shareNode->getName()); + $this->initialState->provideInitialState('view', $view); + + // Load scripts and styles for UI + Util::addInitScript('files', 'init'); + Util::addInitScript(Application::APP_ID, 'init'); + Util::addInitScript(Application::APP_ID, 'init-public'); + Util::addScript('files', 'main'); + Util::addScript(Application::APP_ID, 'public-nickname-handler'); + + // Add file-request script if needed + $attributes = $share->getAttributes(); + $isFileRequest = $attributes?->getAttribute('fileRequest', 'enabled') === true; + $this->initialState->provideInitialState('isFileRequest', $isFileRequest); + + // Load Viewer scripts + if (class_exists(LoadViewer::class)) { + $this->eventDispatcher->dispatchTyped(new LoadViewer()); + } + + // Allow external apps to register their scripts + $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($share)); + + $this->addMetaHeaders($share); + + // CSP to allow office + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFrameDomain('\'self\''); + + $response = new PublicTemplateResponse( + 'files', + 'index', + ); + $response->setContentSecurityPolicy($csp); + + // If the share has a label, use it as the title + if ($share->getLabel() !== '') { + $response->setHeaderTitle($share->getLabel()); + $response->setParams(['pageTitle' => $share->getLabel()]); + } else { + $response->setHeaderTitle($shareNode->getName()); + $response->setParams(['pageTitle' => $shareNode->getName()]); + } + + if ($ownerName !== '') { + $response->setHeaderDetails($this->l10n->t('shared by %s', [$ownerName])); + } + + // Create the header action menu + $headerActions = []; + if ($view !== 'public-file-drop' && !$share->getHideDownload()) { + // The download URL is used for the "download" header action as well as in some cases for the direct link + $downloadUrl = $this->urlGenerator->getAbsoluteURL('/public.php/dav/files/' . $token . '/?accept=zip'); + + // If not a file drop, then add the download header action + $headerActions[] = new SimpleMenuAction('download', $this->l10n->t('Download'), 'icon-download', $downloadUrl, 0, (string)$shareNode->getSize()); + + // If remote sharing is enabled also add the remote share action to the menu + if ($this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { + $headerActions[] = new ExternalShareMenuAction( + // TRANSLATORS The placeholder refers to the software product name as in 'Add to your Nextcloud' + $this->l10n->t('Add to your %s', [$this->defaults->getProductName()]), + 'icon-external', + $ownerId, + $ownerName, + $shareNode->getName(), + ); + } + } + + $shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); + // By default use the share link as the direct link + $directLink = $shareUrl; + // Add the direct link header actions + if ($shareNode->getMimePart() === 'image') { + // If this is a file and especially an image directly point to the image preview + $directLink = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + } elseif (($share->getPermissions() & Constants::PERMISSION_READ) && !$share->getHideDownload()) { + // Can read and no download restriction, so just download it + $directLink = $downloadUrl ?? $shareUrl; + } + $headerActions[] = new LinkMenuAction($this->l10n->t('Direct link'), 'icon-public', $directLink); + $response->setHeaderActions($headerActions); + + return $response; + } + + /** + * Add OpenGraph headers to response for preview + * @param IShare $share The share for which to add the headers + */ + protected function addMetaHeaders(IShare $share): void { + $shareNode = $share->getNode(); + $token = $share->getToken(); + $shareUrl = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]); + + // Handle preview generation for OpenGraph + $hasImagePreview = false; + if ($this->previewManager->isMimeSupported($shareNode->getMimetype())) { + // For images we can use direct links + if ($shareNode->getMimePart() === 'image') { + $hasImagePreview = true; + $ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]); + // Whatsapp is kind of picky about their size requirements + if ($this->request->isUserAgent(['/^WhatsApp/'])) { + $ogPreview = $this->urlGenerator->linkToRouteAbsolute('files_sharing.PublicPreview.getPreview', [ + 'token' => $token, + 'x' => 256, + 'y' => 256, + 'a' => true, + ]); + } + } else { + // For normal files use preview API + $ogPreview = $this->urlGenerator->linkToRouteAbsolute( + 'files_sharing.PublicPreview.getPreview', + [ + 'x' => 256, + 'y' => 256, + 'file' => $share->getTarget(), + 'token' => $token, + ], + ); + } + } else { + // No preview supported, so we just add the favicon + $ogPreview = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-fb.png')); + } + + $title = $shareNode->getName(); + $siteName = $this->defaults->getName(); + $description = $siteName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''); + + // OpenGraph Support: http://ogp.me/ + Util::addHeader('meta', ['property' => 'og:title', 'content' => $title]); + Util::addHeader('meta', ['property' => 'og:description', 'content' => $description]); + Util::addHeader('meta', ['property' => 'og:site_name', 'content' => $siteName]); + Util::addHeader('meta', ['property' => 'og:url', 'content' => $shareUrl]); + Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']); + Util::addHeader('meta', ['property' => 'og:image', 'content' => $ogPreview]); // recommended to always have the image + if ($shareNode->getMimePart() === 'image') { + Util::addHeader('meta', ['property' => 'og:image:type', 'content' => $shareNode->getMimeType()]); + } elseif ($shareNode->getMimePart() === 'audio') { + $audio = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadshare', ['token' => $token]); + Util::addHeader('meta', ['property' => 'og:audio', 'content' => $audio]); + Util::addHeader('meta', ['property' => 'og:audio:type', 'content' => $shareNode->getMimeType()]); + } elseif ($shareNode->getMimePart() === 'video') { + $video = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadshare', ['token' => $token]); + Util::addHeader('meta', ['property' => 'og:video', 'content' => $video]); + Util::addHeader('meta', ['property' => 'og:video:type', 'content' => $shareNode->getMimeType()]); + } + + + // Twitter Support: https://developer.x.com/en/docs/x-for-websites/cards/overview/markup + Util::addHeader('meta', ['property' => 'twitter:title', 'content' => $title]); + Util::addHeader('meta', ['property' => 'twitter:description', 'content' => $description]); + Util::addHeader('meta', ['property' => 'twitter:card', 'content' => $hasImagePreview ? 'summary_large_image' : 'summary']); + Util::addHeader('meta', ['property' => 'twitter:image', 'content' => $ogPreview]); + } +} diff --git a/apps/files_sharing/lib/DeleteOrphanedSharesJob.php b/apps/files_sharing/lib/DeleteOrphanedSharesJob.php index 6a641734680..63f057e3bf4 100644 --- a/apps/files_sharing/lib/DeleteOrphanedSharesJob.php +++ b/apps/files_sharing/lib/DeleteOrphanedSharesJob.php @@ -1,48 +1,45 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 2020-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing; -use OC\BackgroundJob\TimedJob; +use OCP\AppFramework\Db\TTransactional; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use PDO; +use Psr\Log\LoggerInterface; +use function array_map; /** * Delete all share entries that have no matching entries in the file cache table. */ class DeleteOrphanedSharesJob extends TimedJob { - /** - * Default interval in minutes - * - * @var int $defaultIntervalMin - **/ - protected $defaultIntervalMin = 15; + use TTransactional; + + private const CHUNK_SIZE = 1000; + + private const INTERVAL = 24 * 60 * 60; /** * sets the correct interval for this timed job */ - public function __construct() { - $this->interval = $this->defaultIntervalMin * 60; + public function __construct( + ITimeFactory $time, + private IDBConnection $db, + private LoggerInterface $logger, + ) { + parent::__construct($time); + + $this->setInterval(self::INTERVAL); // 1 day + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } /** @@ -51,15 +48,86 @@ class DeleteOrphanedSharesJob extends TimedJob { * @param array $argument unused argument */ public function run($argument) { - $connection = \OC::$server->getDatabaseConnection(); - $logger = \OC::$server->getLogger(); + if ($this->db->getShardDefinition('filecache')) { + $this->shardingCleanup(); + return; + } - $sql = - 'DELETE FROM `*PREFIX*share` ' . - 'WHERE `item_type` in (\'file\', \'folder\') ' . - 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`)'; + $qbSelect = $this->db->getQueryBuilder(); + $qbSelect->select('id') + ->from('share', 's') + ->leftJoin('s', 'filecache', 'fc', $qbSelect->expr()->eq('s.file_source', 'fc.fileid')) + ->where($qbSelect->expr()->isNull('fc.fileid')) + ->setMaxResults(self::CHUNK_SIZE); + $deleteQb = $this->db->getQueryBuilder(); + $deleteQb->delete('share') + ->where( + $deleteQb->expr()->in('id', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY) + ); + + /** + * Read a chunk of orphan rows and delete them. Continue as long as the + * chunk is filled and time before the next cron run does not run out. + * + * Note: With isolation level READ COMMITTED, the database will allow + * other transactions to delete rows between our SELECT and DELETE. In + * that (unlikely) case, our DELETE will have fewer affected rows than + * IDs passed for the WHERE IN. If this happens while processing a full + * chunk, the logic below will stop prematurely. + * Note: The queries below are optimized for low database locking. They + * could be combined into one single DELETE with join or sub query, but + * that has shown to (dead)lock often. + */ + $cutOff = $this->time->getTime() + self::INTERVAL; + do { + $deleted = $this->atomic(function () use ($qbSelect, $deleteQb) { + $result = $qbSelect->executeQuery(); + $ids = array_map('intval', $result->fetchAll(PDO::FETCH_COLUMN)); + $result->closeCursor(); + $deleteQb->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY); + $deleted = $deleteQb->executeStatement(); + $this->logger->debug('{deleted} orphaned share(s) deleted', [ + 'app' => 'DeleteOrphanedSharesJob', + 'deleted' => $deleted, + ]); + return $deleted; + }, $this->db); + } while ($deleted >= self::CHUNK_SIZE && $this->time->getTime() <= $cutOff); + } + + private function shardingCleanup(): void { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct('file_source') + ->from('share', 's'); + $sourceFiles = $qb->executeQuery()->fetchAll(PDO::FETCH_COLUMN); + + $deleteQb = $this->db->getQueryBuilder(); + $deleteQb->delete('share') + ->where( + $deleteQb->expr()->in('file_source', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY) + ); + + $chunks = array_chunk($sourceFiles, self::CHUNK_SIZE); + foreach ($chunks as $chunk) { + $deletedFiles = $this->findMissingSources($chunk); + $this->atomic(function () use ($deletedFiles, $deleteQb) { + $deleteQb->setParameter('ids', $deletedFiles, IQueryBuilder::PARAM_INT_ARRAY); + $deleted = $deleteQb->executeStatement(); + $this->logger->debug('{deleted} orphaned share(s) deleted', [ + 'app' => 'DeleteOrphanedSharesJob', + 'deleted' => $deleted, + ]); + return $deleted; + }, $this->db); + } + } - $deletedEntries = $connection->executeUpdate($sql); - $logger->debug("$deletedEntries orphaned share(s) deleted", ['app' => 'DeleteOrphanedSharesJob']); + private function findMissingSources(array $ids): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('fileid') + ->from('filecache') + ->where($qb->expr()->in('fileid', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $found = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + return array_diff($ids, $found); } } diff --git a/apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php b/apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php index 1a889cfebe7..709d7bacd4a 100644 --- a/apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php +++ b/apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Event; @@ -41,19 +23,14 @@ class BeforeTemplateRenderedEvent extends Event { */ public const SCOPE_PUBLIC_SHARE_AUTH = 'publicShareAuth'; - /** @var IShare */ - private $share; - /** @var string|null */ - private $scope; - /** * @since 20.0.0 */ - public function __construct(IShare $share, ?string $scope = null) { + public function __construct( + private IShare $share, + private ?string $scope = null, + ) { parent::__construct(); - - $this->share = $share; - $this->scope = $scope; } /** diff --git a/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php b/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php index 490ada1eef2..d0cb0a1949d 100644 --- a/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php +++ b/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Event; @@ -30,24 +13,13 @@ use OCP\EventDispatcher\Event; use OCP\Share\IShare; class ShareLinkAccessedEvent extends Event { - /** @var IShare */ - private $share; - - /** @var string */ - private $step; - - /** @var int */ - private $errorCode; - - /** @var string */ - private $errorMessage; - - public function __construct(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = '') { + public function __construct( + private IShare $share, + private string $step = '', + private int $errorCode = 200, + private string $errorMessage = '', + ) { parent::__construct(); - $this->share = $share; - $this->step = $step; - $this->errorCode = $errorCode; - $this->errorMessage = $errorMessage; } public function getShare(): IShare { diff --git a/apps/files_sharing/lib/Event/ShareMountedEvent.php b/apps/files_sharing/lib/Event/ShareMountedEvent.php index 15184827389..0f56873cb2c 100644 --- a/apps/files_sharing/lib/Event/ShareMountedEvent.php +++ b/apps/files_sharing/lib/Event/ShareMountedEvent.php @@ -2,23 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Event; @@ -28,15 +13,13 @@ use OCP\EventDispatcher\Event; use OCP\Files\Mount\IMountPoint; class ShareMountedEvent extends Event { - /** @var SharedMount */ - private $mount; - /** @var IMountPoint[] */ private $additionalMounts = []; - public function __construct(SharedMount $mount) { + public function __construct( + private SharedMount $mount, + ) { parent::__construct(); - $this->mount = $mount; } public function getMount(): SharedMount { diff --git a/apps/files_sharing/lib/Exceptions/BrokenPath.php b/apps/files_sharing/lib/Exceptions/BrokenPath.php index e9908ac293a..a68a8fc05d4 100644 --- a/apps/files_sharing/lib/Exceptions/BrokenPath.php +++ b/apps/files_sharing/lib/Exceptions/BrokenPath.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @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: 2020-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Exceptions; diff --git a/apps/files_sharing/lib/Exceptions/S2SException.php b/apps/files_sharing/lib/Exceptions/S2SException.php index c6becc057dd..10360820432 100644 --- a/apps/files_sharing/lib/Exceptions/S2SException.php +++ b/apps/files_sharing/lib/Exceptions/S2SException.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Exceptions; diff --git a/apps/files_sharing/lib/Exceptions/SharingRightsException.php b/apps/files_sharing/lib/Exceptions/SharingRightsException.php index f235e56cbe4..2ffe72c4e69 100644 --- a/apps/files_sharing/lib/Exceptions/SharingRightsException.php +++ b/apps/files_sharing/lib/Exceptions/SharingRightsException.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2019, Maxence Lange <maxence@artificial-owl.com> - * - * @author Maxence Lange <maxence@artificial-owl.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Exceptions; diff --git a/apps/files_sharing/lib/ExpireSharesJob.php b/apps/files_sharing/lib/ExpireSharesJob.php index dd0979e4b0b..b1c6c592e80 100644 --- a/apps/files_sharing/lib/ExpireSharesJob.php +++ b/apps/files_sharing/lib/ExpireSharesJob.php @@ -1,32 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; @@ -37,21 +20,16 @@ use OCP\Share\IShare; */ class ExpireSharesJob extends TimedJob { - /** @var IManager */ - private $shareManager; - - /** @var IDBConnection */ - private $db; - - public function __construct(ITimeFactory $time, IManager $shareManager, IDBConnection $db) { - $this->shareManager = $shareManager; - $this->db = $db; - + public function __construct( + ITimeFactory $time, + private IManager $shareManager, + private IDBConnection $db, + ) { parent::__construct($time); // Run once a day $this->setInterval(24 * 60 * 60); - $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } @@ -73,19 +51,13 @@ class ExpireSharesJob extends TimedJob { ->from('share') ->where( $qb->expr()->andX( - $qb->expr()->orX( - $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_LINK)), - $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_EMAIL)) - ), + $qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_LINK, IShare::TYPE_EMAIL], IQueryBuilder::PARAM_INT_ARRAY)), $qb->expr()->lte('expiration', $qb->expr()->literal($now)), - $qb->expr()->orX( - $qb->expr()->eq('item_type', $qb->expr()->literal('file')), - $qb->expr()->eq('item_type', $qb->expr()->literal('folder')) - ) + $qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)) ) ); - $shares = $qb->execute(); + $shares = $qb->executeQuery(); while ($share = $shares->fetch()) { if ((int)$share['share_type'] === IShare::TYPE_LINK) { $id = 'ocinternal'; diff --git a/apps/files_sharing/lib/External/Cache.php b/apps/files_sharing/lib/External/Cache.php index f8d9a2548a8..027f682d818 100644 --- a/apps/files_sharing/lib/External/Cache.php +++ b/apps/files_sharing/lib/External/Cache.php @@ -1,47 +1,30 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\External; use OCP\Federation\ICloudId; class Cache extends \OC\Files\Cache\Cache { - /** @var ICloudId */ - private $cloudId; private $remote; private $remoteUser; - private $storage; /** - * @param \OCA\Files_Sharing\External\Storage $storage + * @param Storage $storage * @param ICloudId $cloudId */ - public function __construct($storage, ICloudId $cloudId) { - $this->cloudId = $cloudId; - $this->storage = $storage; - [, $remote] = explode('://', $cloudId->getRemote(), 2); + public function __construct( + private $storage, + private ICloudId $cloudId, + ) { + [, $remote] = explode('://', $this->cloudId->getRemote(), 2); $this->remote = $remote; - $this->remoteUser = $cloudId->getUser(); - parent::__construct($storage); + $this->remoteUser = $this->cloudId->getUser(); + parent::__construct($this->storage); } public function get($file) { @@ -58,8 +41,8 @@ class Cache extends \OC\Files\Cache\Cache { return $result; } - public function getFolderContentsById($id) { - $results = parent::getFolderContentsById($id); + public function getFolderContentsById($fileId) { + $results = parent::getFolderContentsById($fileId); foreach ($results as &$file) { $file['displayname_owner'] = $this->cloudId->getDisplayId(); } diff --git a/apps/files_sharing/lib/External/Manager.php b/apps/files_sharing/lib/External/Manager.php index b9ed4acd57f..ff4781eba0f 100644 --- a/apps/files_sharing/lib/External/Manager.php +++ b/apps/files_sharing/lib/External/Manager.php @@ -1,35 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Hansson <daniel@techandme.se> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @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 OCA\Files_Sharing\External; @@ -38,11 +12,13 @@ use Doctrine\DBAL\Driver\Exception; use OC\Files\Filesystem; use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent; use OCA\Files_Sharing\Helper; +use OCA\Files_Sharing\ResponseDefinitions; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; use OCP\Files; +use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorageFactory; use OCP\Http\Client\IClientService; @@ -56,77 +32,32 @@ use OCP\Share; use OCP\Share\IShare; use Psr\Log\LoggerInterface; +/** + * @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions + */ class Manager { public const STORAGE = '\OCA\Files_Sharing\External\Storage'; /** @var string|null */ private $uid; - /** @var IDBConnection */ - private $connection; - - /** @var \OC\Files\Mount\Manager */ - private $mountManager; - - /** @var IStorageFactory */ - private $storageLoader; - - /** @var IClientService */ - private $clientService; - - /** @var IManager */ - private $notificationManager; - - /** @var IDiscoveryService */ - private $discoveryService; - - /** @var ICloudFederationProviderManager */ - private $cloudFederationProviderManager; - - /** @var ICloudFederationFactory */ - private $cloudFederationFactory; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IUserManager */ - private $userManager; - - /** @var IEventDispatcher */ - private $eventDispatcher; - - /** @var LoggerInterface */ - private $logger; - public function __construct( - IDBConnection $connection, - \OC\Files\Mount\Manager $mountManager, - IStorageFactory $storageLoader, - IClientService $clientService, - IManager $notificationManager, - IDiscoveryService $discoveryService, - ICloudFederationProviderManager $cloudFederationProviderManager, - ICloudFederationFactory $cloudFederationFactory, - IGroupManager $groupManager, - IUserManager $userManager, - IUserSession $userSession, - IEventDispatcher $eventDispatcher, - LoggerInterface $logger + private IDBConnection $connection, + private \OC\Files\Mount\Manager $mountManager, + private IStorageFactory $storageLoader, + private IClientService $clientService, + private IManager $notificationManager, + private IDiscoveryService $discoveryService, + private ICloudFederationProviderManager $cloudFederationProviderManager, + private ICloudFederationFactory $cloudFederationFactory, + private IGroupManager $groupManager, + private IUserManager $userManager, + IUserSession $userSession, + private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger, ) { $user = $userSession->getUser(); - $this->connection = $connection; - $this->mountManager = $mountManager; - $this->storageLoader = $storageLoader; - $this->clientService = $clientService; $this->uid = $user ? $user->getUID() : null; - $this->notificationManager = $notificationManager; - $this->discoveryService = $discoveryService; - $this->cloudFederationProviderManager = $cloudFederationProviderManager; - $this->cloudFederationFactory = $cloudFederationFactory; - $this->groupManager = $groupManager; - $this->userManager = $userManager; - $this->eventDispatcher = $eventDispatcher; - $this->logger = $logger; } /** @@ -146,7 +77,7 @@ class Manager { * @throws \Doctrine\DBAL\Exception */ public function addShare($remote, $token, $password, $name, $owner, $shareType, $accepted = false, $user = null, $remoteId = '', $parent = -1) { - $user = $user ? $user : $this->uid; + $user = $user ?? $this->uid; $accepted = $accepted ? IShare::STATUS_ACCEPTED : IShare::STATUS_PENDING; $name = Filesystem::normalizePath('/' . $name); @@ -195,7 +126,7 @@ class Manager { 'mountpoint' => $mountPoint, 'owner' => $owner ]; - return $this->mountShare($options); + return $this->mountShare($options, $user); } /** @@ -226,18 +157,29 @@ class Manager { $query->execute([$remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType]); } + private function fetchShare(int $id): array|false { + $getShare = $this->connection->prepare(' + SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`, `parent`, `share_type`, `password`, `mountpoint_hash` + FROM `*PREFIX*share_external` + WHERE `id` = ?'); + $result = $getShare->execute([$id]); + $share = $result->fetch(); + $result->closeCursor(); + return $share; + } + /** - * get share + * get share by token * - * @param int $id share id + * @param string $token * @return mixed share of false */ - private function fetchShare($id) { + private function fetchShareByToken($token) { $getShare = $this->connection->prepare(' SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`, `parent`, `share_type`, `password`, `mountpoint_hash` FROM `*PREFIX*share_external` - WHERE `id` = ?'); - $result = $getShare->execute([$id]); + WHERE `share_token` = ?'); + $result = $getShare->execute([$token]); $share = $result->fetch(); $result->closeCursor(); return $share; @@ -257,20 +199,54 @@ class Manager { return null; } - /** - * get share - * - * @param int $id share id - * @return mixed share of false - */ - public function getShare($id) { + public function getShare(int $id, ?string $user = null): array|false { + $user = $user ?? $this->uid; $share = $this->fetchShare($id); - $validShare = is_array($share) && isset($share['share_type']) && isset($share['user']); + if ($share === false) { + return false; + } // check if the user is allowed to access it - if ($validShare && (int)$share['share_type'] === IShare::TYPE_USER && $share['user'] === $this->uid) { + if ($this->canAccessShare($share, $user)) { return $share; - } elseif ($validShare && (int)$share['share_type'] === IShare::TYPE_GROUP) { + } + + return false; + } + + /** + * Get share by token + * + * @param string $token + * @return array|false + */ + public function getShareByToken(string $token): array|false { + $share = $this->fetchShareByToken($token); + + // We do not check if the user is allowed to access it here, + // as this is not used from a user context. + if ($share === false) { + return false; + } + + return $share; + } + + private function canAccessShare(array $share, string $user): bool { + $validShare = isset($share['share_type']) && isset($share['user']); + + if (!$validShare) { + return false; + } + + // If the share is a user share, check if the user is the recipient + if ((int)$share['share_type'] === IShare::TYPE_USER + && $share['user'] === $user) { + return true; + } + + // If the share is a group share, check if the user is in the group + if ((int)$share['share_type'] === IShare::TYPE_GROUP) { $parentId = (int)$share['parent']; if ($parentId !== -1) { // we just retrieved a sub-share, switch to the parent entry for verification @@ -278,9 +254,10 @@ class Manager { } else { $groupShare = $share; } - $user = $this->userManager->get($this->uid); + + $user = $this->userManager->get($user); if ($this->groupManager->get($groupShare['user'])->inGroup($user)) { - return $share; + return true; } } @@ -307,13 +284,22 @@ class Manager { * @param int $id * @return bool True if the share could be accepted, false otherwise */ - public function acceptShare($id) { - $share = $this->getShare($id); + public function acceptShare(int $id, ?string $user = null) { + // If we're auto-accepting a share, we need to know the user id + // as there is no session available while processing the share + // from the remote server request. + $user = $user ?? $this->uid; + if ($user === null) { + $this->logger->error('No user specified for accepting share'); + return false; + } + + $share = $this->getShare($id, $user); $result = false; if ($share) { - \OC_Util::setupFS($this->uid); - $shareFolder = Helper::getShareFolder(null, $this->uid); + \OC_Util::setupFS($user); + $shareFolder = Helper::getShareFolder(null, $user); $mountPoint = Files::buildNotExistingFileName($shareFolder, $share['name']); $mountPoint = Filesystem::normalizePath($mountPoint); $hash = md5($mountPoint); @@ -326,14 +312,14 @@ class Manager { `mountpoint` = ?, `mountpoint_hash` = ? WHERE `id` = ? AND `user` = ?'); - $userShareAccepted = $acceptShare->execute([1, $mountPoint, $hash, $id, $this->uid]); + $userShareAccepted = $acceptShare->execute([1, $mountPoint, $hash, $id, $user]); } else { $parentId = (int)$share['parent']; if ($parentId !== -1) { // this is the sub-share $subshare = $share; } else { - $subshare = $this->fetchUserShare($id, $this->uid); + $subshare = $this->fetchUserShare($id, $user); } if ($subshare !== null) { @@ -344,7 +330,7 @@ class Manager { `mountpoint` = ?, `mountpoint_hash` = ? WHERE `id` = ? AND `user` = ?'); - $acceptShare->execute([1, $mountPoint, $hash, $subshare['id'], $this->uid]); + $acceptShare->execute([1, $mountPoint, $hash, $subshare['id'], $user]); $result = true; } catch (Exception $e) { $this->logger->emergency('Could not update share', ['exception' => $e]); @@ -358,7 +344,7 @@ class Manager { $share['password'], $share['name'], $share['owner'], - $this->uid, + $user, $mountPoint, $hash, 1, $share['remote_id'], $id, @@ -370,17 +356,18 @@ class Manager { } } } + if ($userShareAccepted !== false) { $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept'); $event = new FederatedShareAddedEvent($share['remote']); $this->eventDispatcher->dispatchTyped($event); - $this->eventDispatcher->dispatchTyped(new Files\Events\InvalidateMountCacheEvent($this->userManager->get($this->uid))); + $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->userManager->get($user))); $result = true; } } // Make sure the user has no notification for something that does not exist anymore. - $this->processNotification($id); + $this->processNotification($id, $user); return $result; } @@ -391,17 +378,23 @@ class Manager { * @param int $id * @return bool True if the share could be declined, false otherwise */ - public function declineShare($id) { - $share = $this->getShare($id); + public function declineShare(int $id, ?string $user = null) { + $user = $user ?? $this->uid; + if ($user === null) { + $this->logger->error('No user specified for declining share'); + return false; + } + + $share = $this->getShare($id, $user); $result = false; if ($share && (int)$share['share_type'] === IShare::TYPE_USER) { $removeShare = $this->connection->prepare(' DELETE FROM `*PREFIX*share_external` WHERE `id` = ? AND `user` = ?'); - $removeShare->execute([$id, $this->uid]); + $removeShare->execute([$id, $user]); $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); - $this->processNotification($id); + $this->processNotification($id, $user); $result = true; } elseif ($share && (int)$share['share_type'] === IShare::TYPE_GROUP) { $parentId = (int)$share['parent']; @@ -409,7 +402,7 @@ class Manager { // this is the sub-share $subshare = $share; } else { - $subshare = $this->fetchUserShare($id, $this->uid); + $subshare = $this->fetchUserShare($id, $user); } if ($subshare !== null) { @@ -428,7 +421,7 @@ class Manager { $share['password'], $share['name'], $share['owner'], - $this->uid, + $user, $share['mountpoint'], $share['mountpoint_hash'], 0, @@ -441,20 +434,28 @@ class Manager { $result = false; } } - $this->processNotification($id); + $this->processNotification($id, $user); } return $result; } - /** - * @param int $remoteShare - */ - public function processNotification($remoteShare) { + public function processNotification(int $remoteShare, ?string $user = null): void { + $user = $user ?? $this->uid; + if ($user === null) { + $this->logger->error('No user specified for processing notification'); + return; + } + + $share = $this->fetchShare($remoteShare); + if ($share === false) { + return; + } + $filter = $this->notificationManager->createNotification(); $filter->setApp('files_sharing') - ->setUser($this->uid) - ->setObject('remote_share', (int) $remoteShare); + ->setUser($user) + ->setObject('remote_share', (string)$remoteShare); $this->notificationManager->markProcessed($filter); } @@ -475,7 +476,7 @@ class Manager { } $federationEndpoints = $this->discoveryService->discover($remote, 'FEDERATED_SHARING'); - $endpoint = isset($federationEndpoints['share']) ? $federationEndpoints['share'] : '/ocs/v2.php/cloud/shares'; + $endpoint = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares'; $url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=' . Share::RESPONSE_FORMAT; $fields = ['token' => $token]; @@ -553,9 +554,10 @@ class Manager { return rtrim(substr($path, strlen($prefix)), '/'); } - public function getMount($data) { + public function getMount($data, ?string $user = null) { + $user = $user ?? $this->uid; $data['manager'] = $this; - $mountPoint = '/' . $this->uid . '/files' . $data['mountpoint']; + $mountPoint = '/' . $user . '/files' . $data['mountpoint']; $data['mountpoint'] = $mountPoint; $data['certificateManager'] = \OC::$server->getCertificateManager(); return new Mount(self::STORAGE, $mountPoint, $data, $this, $this->storageLoader); @@ -565,8 +567,8 @@ class Manager { * @param array $data * @return Mount */ - protected function mountShare($data) { - $mount = $this->getMount($data); + protected function mountShare($data, ?string $user = null) { + $mount = $this->getMount($data, $user); $this->mountManager->addMount($mount); return $mount; } @@ -597,7 +599,7 @@ class Manager { '); $result = (bool)$query->execute([$target, $targetHash, $sourceHash, $this->uid]); - $this->eventDispatcher->dispatchTyped(new Files\Events\InvalidateMountCacheEvent($this->userManager->get($this->uid))); + $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->userManager->get($this->uid))); return $result; } @@ -609,6 +611,10 @@ class Manager { $this->logger->error('Mount point to remove share not found', ['mountPoint' => $mountPoint]); return false; } + if (!$mountPointObj instanceof Mount) { + $this->logger->error('Mount point to remove share is not an external share, share probably doesn\'t exist', ['mountPoint' => $mountPoint]); + return false; + } $id = $mountPointObj->getStorage()->getCache()->getId(''); $mountPoint = $this->stripPath($mountPoint); @@ -663,7 +669,7 @@ class Manager { $query->delete('federated_reshares') - ->where($query->expr()->in('share_id', $query->createFunction('(' . $select . ')'))); + ->where($query->expr()->in('share_id', $query->createFunction($select))); $query->execute(); $deleteReShares = $this->connection->getQueryBuilder(); @@ -732,12 +738,12 @@ class Manager { $qb = $this->connection->getQueryBuilder(); // delete group share entry and matching sub-entries $qb->delete('share_external') - ->where( - $qb->expr()->orX( - $qb->expr()->eq('id', $qb->createParameter('share_id')), - $qb->expr()->eq('parent', $qb->createParameter('share_parent_id')) - ) - ); + ->where( + $qb->expr()->orX( + $qb->expr()->eq('id', $qb->createParameter('share_id')), + $qb->expr()->eq('parent', $qb->createParameter('share_parent_id')) + ) + ); foreach ($shares as $share) { $qb->setParameter('share_id', $share['id']); @@ -755,7 +761,7 @@ class Manager { /** * return a list of shares which are not yet accepted by the user * - * @return array list of open server-to-server shares + * @return list<Files_SharingRemoteShare> list of open server-to-server shares */ public function getOpenShares() { return $this->getShares(false); @@ -764,7 +770,7 @@ class Manager { /** * return a list of shares which are accepted by the user * - * @return array list of accepted server-to-server shares + * @return list<Files_SharingRemoteShare> list of accepted server-to-server shares */ public function getAcceptedShares() { return $this->getShares(true); @@ -776,9 +782,11 @@ class Manager { * @param bool|null $accepted True for accepted only, * false for not accepted, * null for all shares of the user - * @return array list of open server-to-server shares + * @return list<Files_SharingRemoteShare> list of open server-to-server shares */ private function getShares($accepted) { + // Not allowing providing a user here, + // as we only want to retrieve shares for the current user. $user = $this->userManager->get($this->uid); $groups = $this->groupManager->getUserGroups($user); $userGroups = []; diff --git a/apps/files_sharing/lib/External/Mount.php b/apps/files_sharing/lib/External/Mount.php index 2047dede39b..f50c379f85f 100644 --- a/apps/files_sharing/lib/External/Mount.php +++ b/apps/files_sharing/lib/External/Mount.php @@ -1,49 +1,34 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Robin Appelman <robin@icewind.nl> - * @author szaimen <szaimen@e.mail.de> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\External; use OC\Files\Mount\MountPoint; use OC\Files\Mount\MoveableMount; +use OC\Files\Storage\Storage; +use OCA\Files_Sharing\ISharedMountPoint; -class Mount extends MountPoint implements MoveableMount { - - /** - * @var \OCA\Files_Sharing\External\Manager - */ - protected $manager; +class Mount extends MountPoint implements MoveableMount, ISharedMountPoint { /** - * @param string|\OC\Files\Storage\Storage $storage + * @param string|Storage $storage * @param string $mountpoint * @param array $options * @param \OCA\Files_Sharing\External\Manager $manager * @param \OC\Files\Storage\StorageFactory $loader */ - public function __construct($storage, $mountpoint, $options, $manager, $loader = null) { + public function __construct( + $storage, + $mountpoint, + $options, + protected $manager, + $loader = null, + ) { parent::__construct($storage, $mountpoint, $options, $loader, null, null, MountProvider::class); - $this->manager = $manager; } /** @@ -61,11 +46,8 @@ class Mount extends MountPoint implements MoveableMount { /** * Remove the mount points - * - * @return mixed - * @return bool */ - public function removeMount() { + public function removeMount(): bool { return $this->manager->removeShare($this->mountPoint); } diff --git a/apps/files_sharing/lib/External/MountProvider.php b/apps/files_sharing/lib/External/MountProvider.php index 5b315e81f69..a5781d5d35a 100644 --- a/apps/files_sharing/lib/External/MountProvider.php +++ b/apps/files_sharing/lib/External/MountProvider.php @@ -1,63 +1,40 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @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 OCA\Files_Sharing\External; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Federation\ICloudIdManager; use OCP\Files\Config\IMountProvider; use OCP\Files\Storage\IStorageFactory; +use OCP\Http\Client\IClientService; use OCP\IDBConnection; -use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IUser; +use OCP\Server; class MountProvider implements IMountProvider { public const STORAGE = '\OCA\Files_Sharing\External\Storage'; /** - * @var \OCP\IDBConnection - */ - private $connection; - - /** * @var callable */ private $managerProvider; /** - * @var ICloudIdManager - */ - private $cloudIdManager; - - /** - * @param \OCP\IDBConnection $connection + * @param IDBConnection $connection * @param callable $managerProvider due to setup order we need a callable that return the manager instead of the manager itself * @param ICloudIdManager $cloudIdManager */ - public function __construct(IDBConnection $connection, callable $managerProvider, ICloudIdManager $cloudIdManager) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + callable $managerProvider, + private ICloudIdManager $cloudIdManager, + ) { $this->managerProvider = $managerProvider; - $this->cloudIdManager = $cloudIdManager; } public function getMount(IUser $user, $data, IStorageFactory $storageFactory) { @@ -68,7 +45,7 @@ class MountProvider implements IMountProvider { $data['mountpoint'] = $mountPoint; $data['cloudId'] = $this->cloudIdManager->getCloudId($data['owner'], $data['remote']); $data['certificateManager'] = \OC::$server->getCertificateManager(); - $data['HttpClientService'] = \OC::$server->getHTTPClientService(); + $data['HttpClientService'] = Server::get(IClientService::class); return new Mount(self::STORAGE, $mountPoint, $data, $manager, $storageFactory); } diff --git a/apps/files_sharing/lib/External/Scanner.php b/apps/files_sharing/lib/External/Scanner.php index 009e206b959..0d57248595b 100644 --- a/apps/files_sharing/lib/External/Scanner.php +++ b/apps/files_sharing/lib/External/Scanner.php @@ -1,39 +1,27 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <robin@icewind.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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\External; +use OC\Files\Cache\CacheEntry; use OC\ForbiddenException; use OCP\Files\NotFoundException; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; class Scanner extends \OC\Files\Cache\Scanner { - /** @var \OCA\Files_Sharing\External\Storage */ + /** @var Storage */ protected $storage; + public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) { + // Disable locking for federated shares + parent::scan($path, $recursive, $reuse, false); + } + /** * Scan a single file and store it in the cache. * If an exception happened while accessing the external storage, @@ -43,13 +31,14 @@ class Scanner extends \OC\Files\Cache\Scanner { * @param string $file file to scan * @param int $reuseExisting * @param int $parentId - * @param array | null $cacheData existing data in the cache for the file to be scanned + * @param CacheEntry|array|null|false $cacheData existing data in the cache for the file to be scanned * @param bool $lock set to false to disable getting an additional read lock during scanning - * @return array | null an array of metadata of the scanned file + * @param array|null $data the metadata for the file, as returned by the storage + * @return array|null an array of metadata of the scanned file */ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { try { - return parent::scanFile($file, $reuseExisting); + return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data); } catch (ForbiddenException $e) { $this->storage->checkStorageAvailability(); } catch (NotFoundException $e) { diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php index 296e7ddf85b..a9781b91a6c 100644 --- a/apps/files_sharing/lib/External/Storage.php +++ b/apps/files_sharing/lib/External/Storage.php @@ -1,33 +1,10 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @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 OCA\Files_Sharing\External; @@ -36,71 +13,92 @@ use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use OC\Files\Storage\DAV; use OC\ForbiddenException; -use OCA\Files_Sharing\ISharedStorage; +use OC\Share\Share; use OCA\Files_Sharing\External\Manager as ExternalShareManager; +use OCA\Files_Sharing\ISharedStorage; use OCP\AppFramework\Http; use OCP\Constants; use OCP\Federation\ICloudId; +use OCP\Files\Cache\ICache; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IWatcher; use OCP\Files\NotFoundException; use OCP\Files\Storage\IDisableEncryptionStorage; use OCP\Files\Storage\IReliableEtagStorage; +use OCP\Files\Storage\IStorage; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; -use OCP\Http\Client\LocalServerException; use OCP\Http\Client\IClientService; +use OCP\Http\Client\LocalServerException; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\OCM\Exceptions\OCMArgumentException; +use OCP\OCM\Exceptions\OCMProviderException; +use OCP\OCM\IOCMDiscoveryService; +use OCP\Server; +use OCP\Util; +use Psr\Log\LoggerInterface; class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, IReliableEtagStorage { - /** @var ICloudId */ - private $cloudId; - /** @var string */ - private $mountPoint; - /** @var string */ - private $token; - /** @var \OCP\ICacheFactory */ - private $memcacheFactory; - /** @var \OCP\Http\Client\IClientService */ - private $httpClient; - /** @var bool */ - private $updateChecked = false; - - /** @var ExternalShareManager */ - private $manager; + private ICloudId $cloudId; + private string $mountPoint; + private string $token; + private ICacheFactory $memcacheFactory; + private IClientService $httpClient; + private bool $updateChecked = false; + private ExternalShareManager $manager; + private IConfig $config; /** * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options */ public function __construct($options) { - $this->memcacheFactory = \OC::$server->getMemCacheFactory(); + $this->memcacheFactory = Server::get(ICacheFactory::class); $this->httpClient = $options['HttpClientService']; - $this->manager = $options['manager']; $this->cloudId = $options['cloudId']; - $discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class); + $this->logger = Server::get(LoggerInterface::class); + $discoveryService = Server::get(IOCMDiscoveryService::class); + $this->config = Server::get(IConfig::class); - [$protocol, $remote] = explode('://', $this->cloudId->getRemote()); - if (strpos($remote, '/')) { - [$host, $root] = explode('/', $remote, 2); - } else { - $host = $remote; - $root = ''; + // use default path to webdav if not found on discovery + try { + $ocmProvider = $discoveryService->discover($this->cloudId->getRemote()); + $webDavEndpoint = $ocmProvider->extractProtocolEntry('file', 'webdav'); + $remote = $ocmProvider->getEndPoint(); + } catch (OCMProviderException|OCMArgumentException $e) { + $this->logger->notice('exception while retrieving webdav endpoint', ['exception' => $e]); + $webDavEndpoint = '/public.php/webdav'; + $remote = $this->cloudId->getRemote(); + } + + $host = parse_url($remote, PHP_URL_HOST); + $port = parse_url($remote, PHP_URL_PORT); + $host .= ($port === null) ? '' : ':' . $port; // we add port if available + + // in case remote NC is on a sub folder and using deprecated ocm provider + $tmpPath = rtrim(parse_url($this->cloudId->getRemote(), PHP_URL_PATH) ?? '', '/'); + if (!str_starts_with($webDavEndpoint, $tmpPath)) { + $webDavEndpoint = $tmpPath . $webDavEndpoint; } - $secure = $protocol === 'https'; - $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING'); - $webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav'; - $root = rtrim($root, '/') . $webDavEndpoint; + $this->mountPoint = $options['mountpoint']; $this->token = $options['token']; - parent::__construct([ - 'secure' => $secure, - 'host' => $host, - 'root' => $root, - 'user' => $options['token'], - 'password' => (string)$options['password'] - ]); + parent::__construct( + [ + 'secure' => ((parse_url($remote, PHP_URL_SCHEME) ?? 'https') === 'https'), + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), + 'host' => $host, + 'root' => $webDavEndpoint, + 'user' => $options['token'], + 'authType' => \Sabre\DAV\Client::AUTH_BASIC, + 'password' => (string)$options['password'] + ] + ); } - public function getWatcher($path = '', $storage = null) { + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { if (!$storage) { $storage = $this; } @@ -131,46 +129,29 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, return $this->password; } - /** - * Get id of the mount point. - * @return string - */ - public function getId() { + public function getId(): string { return 'shared::' . md5($this->token . '@' . $this->getRemote()); } - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if (is_null($this->cache)) { $this->cache = new Cache($this, $this->cloudId); } return $this->cache; } - /** - * @param string $path - * @param \OC\Files\Storage\Storage $storage - * @return \OCA\Files_Sharing\External\Scanner - */ - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } if (!isset($this->scanner)) { $this->scanner = new Scanner($storage); } + /** @var Scanner */ return $this->scanner; } - /** - * Check if a file or folder has been updated since $time - * - * @param string $path - * @param int $time - * @throws \OCP\Files\StorageNotAvailableException - * @throws \OCP\Files\StorageInvalidException - * @return bool - */ - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { // since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage // because of that we only do one check for the entire storage per request if ($this->updateChecked) { @@ -190,7 +171,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, } } - public function test() { + public function test(): bool { try { return parent::test(); } catch (StorageInvalidException $e) { @@ -208,13 +189,13 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, * Check whether this storage is permanently or temporarily * unavailable * - * @throws \OCP\Files\StorageNotAvailableException - * @throws \OCP\Files\StorageInvalidException + * @throws StorageNotAvailableException + * @throws StorageInvalidException */ public function checkStorageAvailability() { // see if we can find out why the share is unavailable try { - $this->getShareInfo(); + $this->getShareInfo(0); } catch (NotFoundException $e) { // a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote if ($this->testRemote()) { @@ -223,24 +204,24 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, // we remove the invalid storage $this->manager->removeShare($this->mountPoint); $this->manager->getMountManager()->removeMount($this->mountPoint); - throw new StorageInvalidException("Remote share not found", 0, $e); + throw new StorageInvalidException('Remote share not found', 0, $e); } else { // Nextcloud instance is gone, likely to be a temporary server configuration error - throw new StorageNotAvailableException("No nextcloud instance found at remote", 0, $e); + throw new StorageNotAvailableException('No nextcloud instance found at remote', 0, $e); } } catch (ForbiddenException $e) { // auth error, remove share for now (provide a dialog in the future) $this->manager->removeShare($this->mountPoint); $this->manager->getMountManager()->removeMount($this->mountPoint); - throw new StorageInvalidException("Auth error when getting remote share"); + throw new StorageInvalidException('Auth error when getting remote share'); } catch (\GuzzleHttp\Exception\ConnectException $e) { - throw new StorageNotAvailableException("Failed to connect to remote instance", 0, $e); + throw new StorageNotAvailableException('Failed to connect to remote instance', 0, $e); } catch (\GuzzleHttp\Exception\RequestException $e) { - throw new StorageNotAvailableException("Error while sending request to remote instance", 0, $e); + throw new StorageNotAvailableException('Error while sending request to remote instance', 0, $e); } } - public function file_exists($path) { + public function file_exists(string $path): bool { if ($path === '') { return true; } else { @@ -255,9 +236,9 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, */ protected function testRemote(): bool { try { - return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php') - || $this->testRemoteUrl($this->getRemote() . '/ocs-provider/') - || $this->testRemoteUrl($this->getRemote() . '/status.php'); + return $this->testRemoteUrl($this->getRemote() . '/ocm-provider/index.php') + || $this->testRemoteUrl($this->getRemote() . '/ocm-provider/') + || $this->testRemoteUrl($this->getRemote() . '/status.php'); } catch (\Exception $e) { return false; } @@ -265,24 +246,19 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, private function testRemoteUrl(string $url): bool { $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url'); - if ($cache->hasKey($url)) { - return (bool)$cache->get($url); + $cached = $cache->get($url); + if ($cached !== null) { + return (bool)$cached; } $client = $this->httpClient->newClient(); try { - $result = $client->get($url, [ - 'timeout' => 10, - 'connect_timeout' => 10, - ])->getBody(); + $result = $client->get($url, $this->getDefaultRequestOptions())->getBody(); $data = json_decode($result); $returnValue = (is_object($data) && !empty($data->version)); - } catch (ConnectException $e) { - $returnValue = false; - } catch (ClientException $e) { - $returnValue = false; - } catch (RequestException $e) { + } catch (ConnectException|ClientException|RequestException $e) { $returnValue = false; + $this->logger->warning('Failed to test remote URL', ['exception' => $e]); } $cache->set($url, $returnValue, 60 * 60 * 24); @@ -308,7 +284,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, * @throws NotFoundException * @throws \Exception */ - public function getShareInfo() { + public function getShareInfo(int $depth = -1) { $remote = $this->getRemote(); $token = $this->getToken(); $password = $this->getPassword(); @@ -328,14 +304,13 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token; // TODO: DI - $client = \OC::$server->getHTTPClientService()->newClient(); + $client = Server::get(IClientService::class)->newClient(); try { - $response = $client->post($url, [ - 'body' => ['password' => $password], - 'timeout' => 10, - 'connect_timeout' => 10, - ]); + $response = $client->post($url, array_merge($this->getDefaultRequestOptions(), [ + 'body' => ['password' => $password, 'depth' => $depth], + ])); } catch (\GuzzleHttp\Exception\RequestException $e) { + $this->logger->warning('Failed to fetch share info', ['exception' => $e]); if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) { throw new ForbiddenException(); } @@ -351,27 +326,34 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, return json_decode($response->getBody(), true); } - public function getOwner($path) { + public function getOwner(string $path): string|false { return $this->cloudId->getDisplayId(); } - public function isSharable($path) { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { return false; } - return ($this->getPermissions($path) & Constants::PERMISSION_SHARE); + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); } - public function getPermissions($path) { + public function getPermissions(string $path): int { $response = $this->propfind($path); + if ($response === false) { + return 0; + } + + $ocsPermissions = $response['{http://open-collaboration-services.org/ns}share-permissions'] ?? null; + $ocmPermissions = $response['{http://open-cloud-mesh.org/ns}share-permissions'] ?? null; + $ocPermissions = $response['{http://owncloud.org/ns}permissions'] ?? null; // old federated sharing permissions - if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) { - $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions']; - } elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) { + if ($ocsPermissions !== null) { + $permissions = (int)$ocsPermissions; + } elseif ($ocmPermissions !== null) { // permissions provided by the OCM API - $permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path); - } elseif (isset($response['{http://owncloud.org/ns}permissions'])) { - return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); + $permissions = $this->ocmPermissions2ncPermissions($ocmPermissions, $path); + } elseif ($ocPermissions !== null) { + return $this->parsePermissions($ocPermissions); } else { // use default permission if remote server doesn't provide the share permissions $permissions = $this->getDefaultPermissions($path); @@ -380,7 +362,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, return $permissions; } - public function needsPartFile() { + public function needsPartFile(): bool { return false; } @@ -430,7 +412,18 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage, return $permissions; } - public function free_space($path) { - return parent::free_space(""); + public function free_space(string $path): int|float|false { + return parent::free_space(''); + } + + private function getDefaultRequestOptions(): array { + $options = [ + 'timeout' => 10, + 'connect_timeout' => 10, + ]; + if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates')) { + $options['verify'] = false; + } + return $options; } } diff --git a/apps/files_sharing/lib/External/Watcher.php b/apps/files_sharing/lib/External/Watcher.php index 6afd1c6d538..f3616feabba 100644 --- a/apps/files_sharing/lib/External/Watcher.php +++ b/apps/files_sharing/lib/External/Watcher.php @@ -1,23 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\External; diff --git a/apps/files_sharing/lib/Helper.php b/apps/files_sharing/lib/Helper.php index 931301a04c4..92e874b73db 100644 --- a/apps/files_sharing/lib/Helper.php +++ b/apps/files_sharing/lib/Helper.php @@ -1,60 +1,42 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.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 OCA\Files_Sharing; use OC\Files\Filesystem; use OC\Files\View; use OCA\Files_Sharing\AppInfo\Application; +use OCP\IConfig; +use OCP\Server; +use OCP\Util; class Helper { public static function registerHooks() { - \OCP\Util::connectHook('OC_Filesystem', 'post_rename', '\OCA\Files_Sharing\Updater', 'renameHook'); - \OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren'); + Util::connectHook('OC_Filesystem', 'post_rename', '\OCA\Files_Sharing\Updater', 'renameHook'); + Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren'); - \OCP\Util::connectHook('OC_User', 'post_deleteUser', '\OCA\Files_Sharing\Hooks', 'deleteUser'); + Util::connectHook('OC_User', 'post_deleteUser', '\OCA\Files_Sharing\Hooks', 'deleteUser'); } /** * check if file name already exists and generate unique target * * @param string $path - * @param array $excludeList * @param View $view * @return string $path */ - public static function generateUniqueTarget($path, $excludeList, $view) { + public static function generateUniqueTarget($path, $view) { $pathinfo = pathinfo($path); - $ext = isset($pathinfo['extension']) ? '.'.$pathinfo['extension'] : ''; + $ext = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : ''; $name = $pathinfo['filename']; $dir = $pathinfo['dirname']; $i = 2; - while ($view->file_exists($path) || in_array($path, $excludeList)) { - $path = Filesystem::normalizePath($dir . '/' . $name . ' ('.$i.')' . $ext); + while ($view->file_exists($path)) { + $path = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext); $i++; } @@ -64,16 +46,16 @@ class Helper { /** * get default share folder * - * @param \OC\Files\View|null $view + * @param View|null $view * @param string|null $userId * @return string */ - public static function getShareFolder(View $view = null, string $userId = null): string { + public static function getShareFolder(?View $view = null, ?string $userId = null): string { if ($view === null) { $view = Filesystem::getView(); } - $config = \OC::$server->getConfig(); + $config = Server::get(IConfig::class); $systemDefault = $config->getSystemValue('share_folder', '/'); $allowCustomShareFolder = $config->getSystemValueBool('sharing.allow_custom_share_folder', true); @@ -107,6 +89,6 @@ class Helper { * @param string $shareFolder */ public static function setShareFolder($shareFolder) { - \OC::$server->getConfig()->setSystemValue('share_folder', $shareFolder); + Server::get(IConfig::class)->setSystemValue('share_folder', $shareFolder); } } diff --git a/apps/files_sharing/lib/Hooks.php b/apps/files_sharing/lib/Hooks.php index 1c93d913eaf..e90b9f5c23d 100644 --- a/apps/files_sharing/lib/Hooks.php +++ b/apps/files_sharing/lib/Hooks.php @@ -1,46 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 OCA\Files_Sharing; use OC\Files\Filesystem; +use OC\Files\View; +use OCP\Server; class Hooks { public static function deleteUser($params) { - $manager = \OC::$server->get(External\Manager::class); + $manager = Server::get(External\Manager::class); $manager->removeUserShares($params['uid']); } public static function unshareChildren($params) { $path = Filesystem::getView()->getAbsolutePath($params['path']); - $view = new \OC\Files\View('/'); + $view = new View('/'); // find share mount points within $path and unmount them - $mountManager = \OC\Files\Filesystem::getMountManager(); + $mountManager = Filesystem::getMountManager(); $mountedShares = $mountManager->findIn($path); foreach ($mountedShares as $mount) { if ($mount->getStorage()->instanceOfStorage(ISharedStorage::class)) { diff --git a/apps/files_sharing/lib/ISharedMountPoint.php b/apps/files_sharing/lib/ISharedMountPoint.php new file mode 100644 index 00000000000..bfce830035d --- /dev/null +++ b/apps/files_sharing/lib/ISharedMountPoint.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing; + +interface ISharedMountPoint { + +} diff --git a/apps/files_sharing/lib/ISharedStorage.php b/apps/files_sharing/lib/ISharedStorage.php index c54b90f3f05..9bd3e4c9476 100644 --- a/apps/files_sharing/lib/ISharedStorage.php +++ b/apps/files_sharing/lib/ISharedStorage.php @@ -1,28 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * - * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing; use OCP\Files\Storage\IStorage; +/** + * @deprecated 30.0.0 use `\OCP\Files\Storage\ISharedStorage` instead + */ interface ISharedStorage extends IStorage { } diff --git a/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php b/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php new file mode 100644 index 00000000000..717edd4869e --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\ViewOnly; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\Files\IRootFolder; +use OCP\IUserSession; + +/** + * @template-implements IEventListener<BeforeDirectFileDownloadEvent|Event> + */ +class BeforeDirectFileDownloadListener implements IEventListener { + + public function __construct( + private IUserSession $userSession, + private IRootFolder $rootFolder, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeDirectFileDownloadEvent)) { + return; + } + + $pathsToCheck = [$event->getPath()]; + // Check only for user/group shares. Don't restrict e.g. share links + $user = $this->userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $this->rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setSuccessful(false); + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + } + } + } +} diff --git a/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php b/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php new file mode 100644 index 00000000000..d19bc8dfae9 --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\Activity\Providers\Downloads; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\Events\Node\BeforeNodeReadEvent; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IRequest; +use OCP\ISession; +use OCP\Share\IShare; + +/** + * @template-implements IEventListener<BeforeNodeReadEvent|BeforeZipCreatedEvent|Event> + */ +class BeforeNodeReadListener implements IEventListener { + private ICache $cache; + + public function __construct( + private ISession $session, + private IRootFolder $rootFolder, + private \OCP\Activity\IManager $activityManager, + private IRequest $request, + ICacheFactory $cacheFactory, + ) { + $this->cache = $cacheFactory->createDistributed('files_sharing_activity_events'); + } + + public function handle(Event $event): void { + if ($event instanceof BeforeZipCreatedEvent) { + $this->handleBeforeZipCreatedEvent($event); + } elseif ($event instanceof BeforeNodeReadEvent) { + $this->handleBeforeNodeReadEvent($event); + } + } + + public function handleBeforeZipCreatedEvent(BeforeZipCreatedEvent $event): void { + $files = $event->getFiles(); + if (count($files) !== 0) { + /* No need to do anything, activity will be triggered for each file in the zip by the BeforeNodeReadEvent */ + return; + } + + $node = $event->getFolder(); + if (!($node instanceof Folder)) { + return; + } + + try { + $storage = $node->getStorage(); + } catch (NotFoundException) { + return; + } + + if (!$storage->instanceOfStorage(ISharedStorage::class)) { + return; + } + + /** @var ISharedStorage $storage */ + $share = $storage->getShare(); + + if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) { + return; + } + + /* Cache that that folder download activity was published */ + $this->cache->set($this->request->getId(), $node->getPath(), 3600); + + $this->singleFileDownloaded($share, $node); + } + + public function handleBeforeNodeReadEvent(BeforeNodeReadEvent $event): void { + $node = $event->getNode(); + if (!($node instanceof File)) { + return; + } + + try { + $storage = $node->getStorage(); + } catch (NotFoundException) { + return; + } + + if (!$storage->instanceOfStorage(ISharedStorage::class)) { + return; + } + + /** @var ISharedStorage $storage */ + $share = $storage->getShare(); + + if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) { + return; + } + + $path = $this->cache->get($this->request->getId()); + if (is_string($path) && str_starts_with($node->getPath(), $path)) { + /* An activity was published for a containing folder already */ + return; + } + + /* Avoid publishing several activities for one video playing */ + $cacheKey = $node->getId() . $node->getPath() . $this->session->getId(); + if (($this->request->getHeader('range') !== '') && ($this->cache->get($cacheKey) === 'true')) { + /* This is a range request and an activity for the same file was published in the same session */ + return; + } + $this->cache->set($cacheKey, 'true', 3600); + + $this->singleFileDownloaded($share, $node); + } + + /** + * create activity if a single file or folder was downloaded from a link share + */ + protected function singleFileDownloaded(IShare $share, File|Folder $node): void { + $fileId = $node->getId(); + + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); + $userNode = $userFolder->getFirstNodeById($fileId); + $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); + $userPath = $userFolder->getRelativePath($userNode?->getPath() ?? '') ?? ''; + $ownerPath = $ownerFolder->getRelativePath($node->getPath()) ?? ''; + + $parameters = [$userPath]; + + if ($share->getShareType() === IShare::TYPE_EMAIL) { + if ($node instanceof File) { + $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED; + } else { + $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED; + } + $parameters[] = $share->getSharedWith(); + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + if ($node instanceof File) { + $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED; + } else { + $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED; + } + $remoteAddress = $this->request->getRemoteAddress(); + $dateTime = new \DateTime(); + $dateTime = $dateTime->format('Y-m-d H'); + $remoteAddressHash = md5($dateTime . '-' . $remoteAddress); + $parameters[] = $remoteAddressHash; + } else { + return; + } + + $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath); + + if ($share->getShareOwner() !== $share->getSharedBy()) { + $parameters[0] = $ownerPath; + $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath); + } + } + + /** + * publish activity + */ + protected function publishActivity( + string $subject, + array $parameters, + string $affectedUser, + int $fileId, + string $filePath, + ): void { + $event = $this->activityManager->generateEvent(); + $event->setApp('files_sharing') + ->setType('public_links') + ->setSubject($subject, $parameters) + ->setAffectedUser($affectedUser) + ->setObject('files', $fileId, $filePath); + $this->activityManager->publish($event); + } +} diff --git a/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php b/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php new file mode 100644 index 00000000000..1fc62bfe0fa --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\ViewOnly; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\IRootFolder; +use OCP\IUserSession; + +/** + * @template-implements IEventListener<BeforeZipCreatedEvent|Event> + */ +class BeforeZipCreatedListener implements IEventListener { + + public function __construct( + private IUserSession $userSession, + private IRootFolder $rootFolder, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeZipCreatedEvent)) { + return; + } + + $dir = $event->getDirectory(); + $files = $event->getFiles(); + + $pathsToCheck = []; + foreach ($files as $file) { + $pathsToCheck[] = $dir . '/' . $file; + } + + // Check only for user/group shares. Don't restrict e.g. share links + $user = $this->userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $this->rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + $event->setSuccessful(false); + } else { + $event->setSuccessful(true); + } + } else { + $event->setSuccessful(true); + } + } +} diff --git a/apps/files_sharing/lib/Listener/LegacyBeforeTemplateRenderedListener.php b/apps/files_sharing/lib/Listener/LegacyBeforeTemplateRenderedListener.php deleted file mode 100644 index e7e81c3a17a..00000000000 --- a/apps/files_sharing/lib/Listener/LegacyBeforeTemplateRenderedListener.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\Files_Sharing\Listener; - -use OC\EventDispatcher\SymfonyAdapter; -use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; -use Symfony\Component\EventDispatcher\GenericEvent; - -class LegacyBeforeTemplateRenderedListener implements IEventListener { - - /** @var SymfonyAdapter */ - private $dispatcher; - - public function __construct(SymfonyAdapter $dispatcher) { - $this->dispatcher = $dispatcher; - } - - public function handle(Event $event): void { - if (!($event instanceof BeforeTemplateRenderedEvent)) { - return; - } - - $eventName = 'OCA\Files_Sharing::loadAdditionalScripts'; - - if ($event->getScope() !== null) { - $eventName .= '::' . $event->getScope(); - } - - $legacyEvent = new GenericEvent(null, ['share' => $event->getShare()]); - $this->dispatcher->dispatch($eventName, $legacyEvent); - } -} diff --git a/apps/files_sharing/lib/Listener/LoadAdditionalListener.php b/apps/files_sharing/lib/Listener/LoadAdditionalListener.php index 8c11fec3999..b089c8309b7 100644 --- a/apps/files_sharing/lib/Listener/LoadAdditionalListener.php +++ b/apps/files_sharing/lib/Listener/LoadAdditionalListener.php @@ -3,44 +3,33 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Listener; -use OCA\Files_Sharing\AppInfo\Application; use OCA\Files\Event\LoadAdditionalScriptsEvent; +use OCA\Files_Sharing\AppInfo\Application; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\Server; +use OCP\Share\IManager; use OCP\Util; +/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */ class LoadAdditionalListener implements IEventListener { public function handle(Event $event): void { if (!($event instanceof LoadAdditionalScriptsEvent)) { return; } - // After files for the files list shared content - Util::addScript(Application::APP_ID, 'files_sharing', 'files'); // After files for the breadcrumb share indicator Util::addScript(Application::APP_ID, 'additionalScripts', 'files'); Util::addStyle(Application::APP_ID, 'icons'); + + $shareManager = Server::get(IManager::class); + if ($shareManager->shareApiEnabled() && class_exists('\OCA\Files\App')) { + Util::addInitScript(Application::APP_ID, 'init'); + } } } diff --git a/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php new file mode 100644 index 00000000000..6da2476194b --- /dev/null +++ b/apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\AppInfo\Application; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Share\IManager; +use OCP\Util; + +/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */ +class LoadPublicFileRequestAuthListener implements IEventListener { + public function __construct( + private IManager $shareManager, + private IInitialState $initialState, + ) { + } + + public function handle(Event $event): void { + if (!$event instanceof BeforeTemplateRenderedEvent) { + return; + } + + // Make sure we are on a public page rendering + if ($event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_PUBLIC) { + return; + } + + $token = $event->getResponse()->getParams()['sharingToken'] ?? null; + if ($token === null || $token === '') { + return; + } + + // Check if the share is a file request + $isFileRequest = false; + try { + $share = $this->shareManager->getShareByToken($token); + $attributes = $share->getAttributes(); + if ($attributes === null) { + return; + } + + $isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true; + } catch (\Exception $e) { + // Ignore, this is not a file request or the share does not exist + } + + Util::addScript(Application::APP_ID, 'public-nickname-handler'); + + // Add file-request script if needed + $this->initialState->provideInitialState('isFileRequest', $isFileRequest); + } +} diff --git a/apps/files_sharing/lib/Listener/LoadSidebarListener.php b/apps/files_sharing/lib/Listener/LoadSidebarListener.php index cb5ebf8a792..17fee71978f 100644 --- a/apps/files_sharing/lib/Listener/LoadSidebarListener.php +++ b/apps/files_sharing/lib/Listener/LoadSidebarListener.php @@ -3,40 +3,48 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\Files_Sharing\Listener; -use OCA\Files_Sharing\AppInfo\Application; use OCA\Files\Event\LoadSidebar; +use OCA\Files_Sharing\AppInfo\Application; +use OCA\Files_Sharing\Config\ConfigLexicon; +use OCP\AppFramework\Services\IInitialState; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\GlobalScale\IConfig; +use OCP\IAppConfig; +use OCP\Server; +use OCP\Share\IManager; use OCP\Util; +/** + * @template-implements IEventListener<LoadSidebar> + */ class LoadSidebarListener implements IEventListener { + + public function __construct( + private IInitialState $initialState, + private IManager $shareManager, + ) { + } + public function handle(Event $event): void { if (!($event instanceof LoadSidebar)) { return; } - Util::addScript(Application::APP_ID, 'files_sharing_tab', 'files'); + + $appConfig = Server::get(IAppConfig::class); + $gsConfig = Server::get(IConfig::class); + $showFederatedToTrustedAsInternal = $gsConfig->isGlobalScaleEnabled() || $appConfig->getValueBool('files_sharing', ConfigLexicon::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL); + $showFederatedAsInternal = ($gsConfig->isGlobalScaleEnabled() && $gsConfig->onlyInternalFederation()) + || $appConfig->getValueBool('files_sharing', ConfigLexicon::SHOW_FEDERATED_AS_INTERNAL); + + $this->initialState->provideInitialState('showFederatedSharesAsInternal', $showFederatedAsInternal); + $this->initialState->provideInitialState('showFederatedSharesToTrustedServersAsInternal', $showFederatedToTrustedAsInternal); } } diff --git a/apps/files_sharing/lib/Listener/ShareInteractionListener.php b/apps/files_sharing/lib/Listener/ShareInteractionListener.php index 65ad555f5bd..7b11a472492 100644 --- a/apps/files_sharing/lib/Listener/ShareInteractionListener.php +++ b/apps/files_sharing/lib/Listener/ShareInteractionListener.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Listener; @@ -30,12 +12,13 @@ use OCP\Contacts\Events\ContactInteractedWithEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventListener; -use OCP\ILogger; use OCP\IUserManager; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; use function in_array; +/** @template-implements IEventListener<ShareCreatedEvent> */ class ShareInteractionListener implements IEventListener { private const SUPPORTED_SHARE_TYPES = [ IShare::TYPE_USER, @@ -43,21 +26,11 @@ class ShareInteractionListener implements IEventListener { IShare::TYPE_REMOTE, ]; - /** @var IEventDispatcher */ - private $dispatcher; - - /** @var IUserManager */ - private $userManager; - - /** @var ILogger */ - private $logger; - - public function __construct(IEventDispatcher $dispatcher, - IUserManager $userManager, - ILogger $logger) { - $this->dispatcher = $dispatcher; - $this->userManager = $userManager; - $this->logger = $logger; + public function __construct( + private IEventDispatcher $dispatcher, + private IUserManager $userManager, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { diff --git a/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php index 1f3898b3dcb..281c96ca5e7 100644 --- a/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php +++ b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Listener; @@ -33,17 +16,13 @@ use OCP\IConfig; use OCP\Share\IManager; use OCP\Share\IShare; +/** @template-implements IEventListener<UserAddedEvent> */ class UserAddedToGroupListener implements IEventListener { - /** @var IManager */ - private $shareManager; - - /** @var IConfig */ - private $config; - - public function __construct(IManager $shareManager, IConfig $config) { - $this->shareManager = $shareManager; - $this->config = $config; + public function __construct( + private IManager $shareManager, + private IConfig $config, + ) { } public function handle(Event $event): void { diff --git a/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php b/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php index 160a806f8ac..0ac447436bd 100644 --- a/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php +++ b/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Listener; @@ -36,19 +17,14 @@ use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\IManager; use OCP\Share\IShare; +/** @template-implements IEventListener<ShareCreatedEvent> */ class UserShareAcceptanceListener implements IEventListener { - /** @var IConfig */ - private $config; - /** @var IManager */ - private $shareManager; - /** @var IGroupManager */ - private $groupManager; - - public function __construct(IConfig $config, IManager $shareManager, IGroupManager $groupManager) { - $this->config = $config; - $this->shareManager = $shareManager; - $this->groupManager = $groupManager; + public function __construct( + private IConfig $config, + private IManager $shareManager, + private IGroupManager $groupManager, + ) { } public function handle(Event $event): void { diff --git a/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php b/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php index 1c961ffed34..6671a78efff 100644 --- a/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php +++ b/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Middleware; @@ -33,15 +15,10 @@ use OCP\IL10N; use OCP\Share\IManager; class OCSShareAPIMiddleware extends Middleware { - /** @var IManager */ - private $shareManager; - /** @var IL10N */ - private $l; - - public function __construct(IManager $shareManager, - IL10N $l) { - $this->shareManager = $shareManager; - $this->l = $l; + public function __construct( + private IManager $shareManager, + private IL10N $l, + ) { } /** diff --git a/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php b/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php index adc6116af45..e96940979bf 100644 --- a/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php +++ b/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Middleware; @@ -33,11 +16,9 @@ use OCP\AppFramework\Middleware; use OCP\Share\IManager; class ShareInfoMiddleware extends Middleware { - /** @var IManager */ - private $shareManager; - - public function __construct(IManager $shareManager) { - $this->shareManager = $shareManager; + public function __construct( + private IManager $shareManager, + ) { } /** diff --git a/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php b/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php index b9bab169d1d..8ea2eb59d73 100644 --- a/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php +++ b/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php @@ -1,31 +1,10 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Middleware; @@ -50,32 +29,14 @@ use OCP\Share\IManager; */ class SharingCheckMiddleware extends Middleware { - /** @var string */ - protected $appName; - /** @var IConfig */ - protected $config; - /** @var IAppManager */ - protected $appManager; - /** @var IControllerMethodReflector */ - protected $reflector; - /** @var IManager */ - protected $shareManager; - /** @var IRequest */ - protected $request; - - public function __construct(string $appName, - IConfig $config, - IAppManager $appManager, - IControllerMethodReflector $reflector, - IManager $shareManager, - IRequest $request - ) { - $this->appName = $appName; - $this->config = $config; - $this->appManager = $appManager; - $this->reflector = $reflector; - $this->shareManager = $shareManager; - $this->request = $request; + public function __construct( + protected string $appName, + protected IConfig $config, + protected IAppManager $appManager, + protected IControllerMethodReflector $reflector, + protected IManager $shareManager, + protected IRequest $request, + ) { } /** @@ -91,8 +52,8 @@ class SharingCheckMiddleware extends Middleware { throw new NotFoundException('Sharing is disabled.'); } - if ($controller instanceof ExternalSharesController && - !$this->externalSharesChecks()) { + if ($controller instanceof ExternalSharesController + && !$this->externalSharesChecks()) { throw new S2SException('Federated sharing not allowed'); } } @@ -123,13 +84,13 @@ class SharingCheckMiddleware extends Middleware { * @return bool */ private function externalSharesChecks(): bool { - if (!$this->reflector->hasAnnotation('NoIncomingFederatedSharingRequired') && - $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') !== 'yes') { + if (!$this->reflector->hasAnnotation('NoIncomingFederatedSharingRequired') + && $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') !== 'yes') { return false; } - if (!$this->reflector->hasAnnotation('NoOutgoingFederatedSharingRequired') && - $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') !== 'yes') { + if (!$this->reflector->hasAnnotation('NoOutgoingFederatedSharingRequired') + && $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') !== 'yes') { return false; } diff --git a/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php index d1ba645f93d..3718306e380 100644 --- a/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php +++ b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; @@ -36,16 +19,10 @@ use OCP\Share\IShare; */ class OwncloudGuestShareType implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** @var IConfig */ - private $config; - - - public function __construct(IDBConnection $connection, IConfig $config) { - $this->connection = $connection; - $this->config = $config; + public function __construct( + private IDBConnection $connection, + private IConfig $config, + ) { } /** @@ -68,14 +45,14 @@ class OwncloudGuestShareType implements IRepairStep { $query = $this->connection->getQueryBuilder(); $query->update('share') - ->set('share_type', $query->createNamedParameter(IShare::TYPE_GUEST)) + ->set('share_type', $query->createNamedParameter(IShare::TYPE_GUEST)) ->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_EMAIL))); $query->execute(); } protected function shouldRun() { $appVersion = $this->config->getAppValue('files_sharing', 'installed_version', '0.0.0'); - return $appVersion === '0.10.0' || - $this->config->getAppValue('core', 'vendor', '') === 'owncloud'; + return $appVersion === '0.10.0' + || $this->config->getAppValue('core', 'vendor', '') === 'owncloud'; } } diff --git a/apps/files_sharing/lib/Migration/SetAcceptedStatus.php b/apps/files_sharing/lib/Migration/SetAcceptedStatus.php index a27f12e87e1..4da6aad4b33 100644 --- a/apps/files_sharing/lib/Migration/SetAcceptedStatus.php +++ b/apps/files_sharing/lib/Migration/SetAcceptedStatus.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; @@ -34,16 +17,10 @@ use OCP\Share\IShare; class SetAcceptedStatus implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** @var IConfig */ - private $config; - - - public function __construct(IDBConnection $connection, IConfig $config) { - $this->connection = $connection; - $this->config = $config; + public function __construct( + private IDBConnection $connection, + private IConfig $config, + ) { } /** @@ -69,7 +46,7 @@ class SetAcceptedStatus implements IRepairStep { ->update('share') ->set('accepted', $query->createNamedParameter(IShare::STATUS_ACCEPTED)) ->where($query->expr()->in('share_type', $query->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_USERGROUP], IQueryBuilder::PARAM_INT_ARRAY))); - $query->execute(); + $query->executeStatement(); } protected function shouldRun() { diff --git a/apps/files_sharing/lib/Migration/SetPasswordColumn.php b/apps/files_sharing/lib/Migration/SetPasswordColumn.php index b4de4f574dd..f60af2817d4 100644 --- a/apps/files_sharing/lib/Migration/SetPasswordColumn.php +++ b/apps/files_sharing/lib/Migration/SetPasswordColumn.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; @@ -35,16 +19,10 @@ use OCP\Share\IShare; */ class SetPasswordColumn implements IRepairStep { - /** @var IDBConnection */ - private $connection; - - /** @var IConfig */ - private $config; - - - public function __construct(IDBConnection $connection, IConfig $config) { - $this->connection = $connection; - $this->config = $config; + public function __construct( + private IDBConnection $connection, + private IConfig $config, + ) { } /** diff --git a/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php index 50d0cd9e066..c9fe840d422 100644 --- a/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php +++ b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php @@ -3,45 +3,24 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; use Closure; use Doctrine\DBAL\Types\Type; -use OCP\DB\Types; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\IDBConnection; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; class Version11300Date20201120141438 extends SimpleMigrationStep { - /** @var IDBConnection */ - private $connection; - - public function __construct(IDBConnection $connection) { - $this->connection = $connection; + public function __construct( + private IDBConnection $connection, + ) { } public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { diff --git a/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php b/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php index ff7722eaa58..9bd07a19802 100644 --- a/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php +++ b/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php @@ -3,32 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Vincent Petry <vincent@nextcloud.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; use Closure; -use OCP\DB\Types; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; diff --git a/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php b/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php index 443f255d031..e82fb4a72d5 100644 --- a/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php +++ b/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021 Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; diff --git a/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php b/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php index d5f938dde6d..75da1de1d83 100644 --- a/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php +++ b/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php @@ -3,31 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Vincent Petry <vincent@nextloud.com> - * - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; use Closure; -use OCP\DB\Types; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; diff --git a/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php b/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php index 05176ebdae9..03985bd50c7 100644 --- a/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php +++ b/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Côme Chilliet <come.chilliet@nextcloud.com> - * - * @author Côme Chilliet <come.chilliet@nextcloud.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Migration; diff --git a/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php b/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php new file mode 100644 index 00000000000..71b2c1817e6 --- /dev/null +++ b/apps/files_sharing/lib/Migration/Version31000Date20240821142813.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\Attributes\AddColumn; +use OCP\Migration\Attributes\ColumnType; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +#[AddColumn(table: 'share', name: 'reminder_sent', type: ColumnType::BOOLEAN)] +class Version31000Date20240821142813 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + $schema = $schemaClosure(); + $table = $schema->getTable('share'); + if ($table->hasColumn('reminder_sent')) { + return null; + } + + $table->addColumn('reminder_sent', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + return $schema; + } + +} diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index bfb40387622..b7b0582493e 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -1,124 +1,88 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Maxence Lange <maxence@nextcloud.com> - * @author Morris Jobke <hey@morrisjobke.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 OCA\Files_Sharing; -use OC\Cache\CappedMemoryCache; use OC\Files\View; use OCA\Files_Sharing\Event\ShareMountedEvent; +use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Config\IMountProvider; +use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; use OCP\Files\Storage\IStorageFactory; use OCP\ICacheFactory; use OCP\IConfig; -use OCP\ILogger; use OCP\IUser; use OCP\Share\IManager; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; class MountProvider implements IMountProvider { /** - * @var \OCP\IConfig - */ - protected $config; - - /** - * @var IManager - */ - protected $shareManager; - - /** - * @var ILogger - */ - protected $logger; - - /** @var IEventDispatcher */ - protected $eventDispatcher; - - /** @var ICacheFactory */ - protected $cacheFactory; - - /** - * @param \OCP\IConfig $config + * @param IConfig $config * @param IManager $shareManager - * @param ILogger $logger + * @param LoggerInterface $logger */ public function __construct( - IConfig $config, - IManager $shareManager, - ILogger $logger, - IEventDispatcher $eventDispatcher, - ICacheFactory $cacheFactory + protected IConfig $config, + protected IManager $shareManager, + protected LoggerInterface $logger, + protected IEventDispatcher $eventDispatcher, + protected ICacheFactory $cacheFactory, + protected IMountManager $mountManager, ) { - $this->config = $config; - $this->shareManager = $shareManager; - $this->logger = $logger; - $this->eventDispatcher = $eventDispatcher; - $this->cacheFactory = $cacheFactory; } - /** * Get all mountpoints applicable for the user and check for shares where we need to update the etags * - * @param \OCP\IUser $user - * @param \OCP\Files\Storage\IStorageFactory $loader - * @return \OCP\Files\Mount\IMountPoint[] + * @param IUser $user + * @param IStorageFactory $loader + * @return IMountPoint[] */ public function getMountsForUser(IUser $user, IStorageFactory $loader) { - $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1)); - + $shares = array_merge( + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1), + ); // filter out excluded shares and group shares that includes self - $shares = array_filter($shares, function (\OCP\Share\IShare $share) use ($user) { - return $share->getPermissions() > 0 && $share->getShareOwner() !== $user->getUID(); + $shares = array_filter($shares, function (IShare $share) use ($user) { + return $share->getPermissions() > 0 && $share->getShareOwner() !== $user->getUID() && $share->getSharedBy() !== $user->getUID(); }); $superShares = $this->buildSuperShares($shares, $user); + $otherMounts = $this->mountManager->getAll(); $mounts = []; $view = new View('/' . $user->getUID() . '/files'); $ownerViews = []; $sharingDisabledForUser = $this->shareManager->sharingDisabledForUser($user->getUID()); + /** @var CappedMemoryCache<bool> $folderExistCache */ $foldersExistCache = new CappedMemoryCache(); + + $validShareCache = $this->cacheFactory->createLocal('share-valid-mountpoint-max'); + $maxValidatedShare = $validShareCache->get($user->getUID()) ?? 0; + $newMaxValidatedShare = $maxValidatedShare; + foreach ($superShares as $share) { try { - /** @var \OCP\Share\IShare $parentShare */ + /** @var IShare $parentShare */ $parentShare = $share[0]; - if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED && - ($parentShare->getShareType() === IShare::TYPE_GROUP || - $parentShare->getShareType() === IShare::TYPE_USERGROUP || - $parentShare->getShareType() === IShare::TYPE_USER)) { + if ($parentShare->getStatus() !== IShare::STATUS_ACCEPTED + && ($parentShare->getShareType() === IShare::TYPE_GROUP + || $parentShare->getShareType() === IShare::TYPE_USERGROUP + || $parentShare->getShareType() === IShare::TYPE_USER)) { continue; } @@ -126,9 +90,10 @@ class MountProvider implements IMountProvider { if (!isset($ownerViews[$owner])) { $ownerViews[$owner] = new View('/' . $parentShare->getShareOwner() . '/files'); } + $shareId = (int)$parentShare->getId(); $mount = new SharedMount( '\OCA\Files_Sharing\SharedStorage', - $mounts, + array_merge($mounts, $otherMounts), [ 'user' => $user->getUID(), // parent share @@ -143,9 +108,11 @@ class MountProvider implements IMountProvider { $foldersExistCache, $this->eventDispatcher, $user, - $this->cacheFactory->createLocal('share-valid-mountpoint') + ($shareId <= $maxValidatedShare), ); + $newMaxValidatedShare = max($shareId, $newMaxValidatedShare); + $event = new ShareMountedEvent($mount); $this->eventDispatcher->dispatchTyped($event); @@ -154,11 +121,18 @@ class MountProvider implements IMountProvider { $mounts[$additionalMount->getMountPoint()] = $additionalMount; } } catch (\Exception $e) { - $this->logger->logException($e); - $this->logger->error('Error while trying to create shared mount'); + $this->logger->error( + 'Error while trying to create shared mount', + [ + 'app' => 'files_sharing', + 'exception' => $e, + ], + ); } } + $validShareCache->set($user->getUID(), $newMaxValidatedShare, 24 * 60 * 60); + // array_filter removes the null values from the array return array_values(array_filter($mounts)); } @@ -166,9 +140,9 @@ class MountProvider implements IMountProvider { /** * Groups shares by path (nodeId) and target path * - * @param \OCP\Share\IShare[] $shares - * @return \OCP\Share\IShare[][] array of grouped shares, each element in the - * array is a group which itself is an array of shares + * @param IShare[] $shares + * @return IShare[][] array of grouped shares, each element in the + * array is a group which itself is an array of shares */ private function groupShares(array $shares) { $tmp = []; @@ -203,16 +177,16 @@ class MountProvider implements IMountProvider { * grouped shares. The most permissive permissions are used based on the permissions * of all shares within the group. * - * @param \OCP\Share\IShare[] $allShares - * @param \OCP\IUser $user user + * @param IShare[] $allShares + * @param IUser $user user * @return array Tuple of [superShare, groupedShares] */ - private function buildSuperShares(array $allShares, \OCP\IUser $user) { + private function buildSuperShares(array $allShares, IUser $user) { $result = []; $groupedShares = $this->groupShares($allShares); - /** @var \OCP\Share\IShare[] $shares */ + /** @var IShare[] $shares */ foreach ($groupedShares as $shares) { if (count($shares) === 0) { continue; @@ -227,15 +201,42 @@ class MountProvider implements IMountProvider { ->setShareType($shares[0]->getShareType()) ->setTarget($shares[0]->getTarget()); + // Gather notes from all the shares. + // Since these are readly available here, storing them + // enables the DAV FilesPlugin to avoid executing many + // DB queries to retrieve the same information. + $allNotes = implode("\n", array_map(function ($sh) { + return $sh->getNote(); + }, $shares)); + $superShare->setNote($allNotes); + // use most permissive permissions - $permissions = 0; + // this covers the case where there are multiple shares for the same + // file e.g. from different groups and different permissions + $superPermissions = 0; + $superAttributes = $this->shareManager->newShare()->newAttributes(); $status = IShare::STATUS_PENDING; foreach ($shares as $share) { - $permissions |= $share->getPermissions(); + $superPermissions |= $share->getPermissions(); $status = max($status, $share->getStatus()); + // update permissions + $superPermissions |= $share->getPermissions(); + + // update share permission attributes + $attributes = $share->getAttributes(); + if ($attributes !== null) { + foreach ($attributes->toArray() as $attribute) { + if ($superAttributes->getAttribute($attribute['scope'], $attribute['key']) === true) { + // if super share attribute is already enabled, it is most permissive + continue; + } + // update supershare attributes with subshare attribute + $superAttributes->setAttribute($attribute['scope'], $attribute['key'], $attribute['value']); + } + } + // adjust target, for database consistency if needed if ($share->getTarget() !== $superShare->getTarget()) { - // adjust target, for database consistency $share->setTarget($superShare->getTarget()); try { $this->shareManager->moveShare($share, $user->getUID()); @@ -260,8 +261,9 @@ class MountProvider implements IMountProvider { } } - $superShare->setPermissions($permissions) - ->setStatus($status); + $superShare->setPermissions($superPermissions); + $superShare->setStatus($status); + $superShare->setAttributes($superAttributes); $result[] = [$superShare, $shares]; } diff --git a/apps/files_sharing/lib/Notification/Listener.php b/apps/files_sharing/lib/Notification/Listener.php index db7939767d6..1cf0f845e7a 100644 --- a/apps/files_sharing/lib/Notification/Listener.php +++ b/apps/files_sharing/lib/Notification/Listener.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Notification; @@ -31,35 +13,22 @@ use OCP\IGroupManager; use OCP\IUser; use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; +use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use Symfony\Component\EventDispatcher\GenericEvent; class Listener { - /** @var INotificationManager */ - protected $notificationManager; - /** @var IShareManager */ - protected $shareManager; - /** @var IGroupManager */ - protected $groupManager; - public function __construct( - INotificationManager $notificationManager, - IShareManager $shareManager, - IGroupManager $groupManager + protected INotificationManager $notificationManager, + protected IShareManager $shareManager, + protected IGroupManager $groupManager, ) { - $this->notificationManager = $notificationManager; - $this->shareManager = $shareManager; - $this->groupManager = $groupManager; } - /** - * @param GenericEvent $event - */ - public function shareNotification(GenericEvent $event): void { - /** @var IShare $share */ - $share = $event->getSubject(); + public function shareNotification(ShareCreatedEvent $event): void { + $share = $event->getShare(); $notification = $this->instantiateNotification($share); if ($share->getShareType() === IShare::TYPE_USER) { @@ -71,8 +40,8 @@ class Listener { $group = $this->groupManager->get($share->getSharedWith()); foreach ($group->getUsers() as $user) { - if ($user->getUID() === $share->getShareOwner() || - $user->getUID() === $share->getSharedBy()) { + if ($user->getUID() === $share->getShareOwner() + || $user->getUID() === $share->getSharedBy()) { continue; } @@ -103,8 +72,8 @@ class Listener { continue; } - if ($user->getUID() === $share->getShareOwner() || - $user->getUID() === $share->getSharedBy()) { + if ($user->getUID() === $share->getShareOwner() + || $user->getUID() === $share->getSharedBy()) { continue; } diff --git a/apps/files_sharing/lib/Notification/Notifier.php b/apps/files_sharing/lib/Notification/Notifier.php index d35e22c935d..e4434ef0b37 100644 --- a/apps/files_sharing/lib/Notification/Notifier.php +++ b/apps/files_sharing/lib/Notification/Notifier.php @@ -3,32 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * @copyright Copyright (c) 2019, Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Notification; use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; use OCP\IGroupManager; use OCP\IL10N; use OCP\IURLGenerator; @@ -38,6 +19,7 @@ use OCP\L10N\IFactory; use OCP\Notification\AlreadyProcessedException; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager; use OCP\Share\IShare; @@ -46,31 +28,14 @@ class Notifier implements INotifier { public const INCOMING_USER_SHARE = 'incoming_user_share'; public const INCOMING_GROUP_SHARE = 'incoming_group_share'; - /** @var IFactory */ - protected $l10nFactory; - /** @var IManager */ - private $shareManager; - /** @var IRootFolder */ - private $rootFolder; - /** @var IGroupManager */ - protected $groupManager; - /** @var IUserManager */ - protected $userManager; - /** @var IURLGenerator */ - protected $url; - - public function __construct(IFactory $l10nFactory, - IManager $shareManager, - IRootFolder $rootFolder, - IGroupManager $groupManager, - IUserManager $userManager, - IURLGenerator $url) { - $this->l10nFactory = $l10nFactory; - $this->shareManager = $shareManager; - $this->rootFolder = $rootFolder; - $this->groupManager = $groupManager; - $this->userManager = $userManager; - $this->url = $url; + public function __construct( + protected IFactory $l10nFactory, + private IManager $shareManager, + private IRootFolder $rootFolder, + protected IGroupManager $groupManager, + protected IUserManager $userManager, + protected IURLGenerator $url, + ) { } /** @@ -97,15 +62,15 @@ class Notifier implements INotifier { * @param INotification $notification * @param string $languageCode The code of the language that should be used to prepare the notification * @return INotification - * @throws \InvalidArgumentException When the notification was not prepared by a notifier + * @throws UnknownNotificationException When the notification was not prepared by a notifier * @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted * @since 9.0.0 */ public function prepare(INotification $notification, string $languageCode): INotification { - if ($notification->getApp() !== 'files_sharing' || - ($notification->getSubject() !== 'expiresTomorrow' && - $notification->getObjectType() !== 'share')) { - throw new \InvalidArgumentException('Unhandled app or subject'); + if ($notification->getApp() !== 'files_sharing' + || ($notification->getSubject() !== 'expiresTomorrow' + && $notification->getObjectType() !== 'share')) { + throw new UnknownNotificationException('Unhandled app or subject'); } $l = $this->l10nFactory->get('files_sharing', $languageCode); @@ -117,6 +82,13 @@ class Notifier implements INotifier { throw new AlreadyProcessedException(); } + try { + $share->getNode(); + } catch (NotFoundException $e) { + // Node is already deleted, so discard the notification + throw new AlreadyProcessedException(); + } + if ($notification->getSubject() === 'expiresTomorrow') { $notification = $this->parseShareExpiration($share, $notification, $l); } else { @@ -132,15 +104,14 @@ class Notifier implements INotifier { $notification ->setParsedSubject($l->t('Share will expire tomorrow')) - ->setParsedMessage($l->t('One or more of your shares will expire tomorrow')) ->setRichMessage( $l->t('Your share of {node} will expire tomorrow'), [ 'node' => [ 'type' => 'file', - 'id' => $node->getId(), + 'id' => (string)$node->getId(), 'name' => $node->getName(), - 'path' => $path, + 'path' => (string)$path, ], ] ); @@ -157,6 +128,8 @@ class Notifier implements INotifier { if ($share->getStatus() !== IShare::STATUS_PENDING) { throw new AlreadyProcessedException(); } + } else { + throw new UnknownNotificationException('Invalid share type'); } switch ($notification->getSubject()) { @@ -227,17 +200,10 @@ class Notifier implements INotifier { break; default: - throw new \InvalidArgumentException('Invalid subject'); - } - - $placeholders = $replacements = []; - foreach ($subjectParameters as $placeholder => $parameter) { - $placeholders[] = '{' . $placeholder . '}'; - $replacements[] = $parameter['name']; + throw new UnknownNotificationException('Invalid subject'); } - $notification->setParsedSubject(str_replace($placeholders, $replacements, $subject)) - ->setRichSubject($subject, $subjectParameters) + $notification->setRichSubject($subject, $subjectParameters) ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); $acceptAction = $notification->createAction(); @@ -247,7 +213,7 @@ class Notifier implements INotifier { $notification->addParsedAction($acceptAction); $rejectAction = $notification->createAction(); - $rejectAction->setParsedLabel($l->t('Reject')) + $rejectAction->setParsedLabel($l->t('Decline')) ->setLink($this->url->linkToOCSRouteAbsolute('files_sharing.ShareAPI.deleteShare', ['id' => $share->getId()]), 'DELETE') ->setPrimary(false); $notification->addParsedAction($rejectAction); diff --git a/apps/files_sharing/lib/OrphanHelper.php b/apps/files_sharing/lib/OrphanHelper.php new file mode 100644 index 00000000000..6e070f1446b --- /dev/null +++ b/apps/files_sharing/lib/OrphanHelper.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing; + +use OC\User\NoUserException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\IRootFolder; +use OCP\IDBConnection; + +class OrphanHelper { + public function __construct( + private IDBConnection $connection, + private IRootFolder $rootFolder, + private IUserMountCache $userMountCache, + ) { + } + + public function isShareValid(string $owner, int $fileId): bool { + try { + $userFolder = $this->rootFolder->getUserFolder($owner); + } catch (NoUserException $e) { + return false; + } + $node = $userFolder->getFirstNodeById($fileId); + return $node !== null; + } + + /** + * @param int[] $ids + * @return void + */ + public function deleteShares(array $ids): void { + $query = $this->connection->getQueryBuilder(); + $query->delete('share') + ->where($query->expr()->in('id', $query->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $query->executeStatement(); + } + + public function fileExists(int $fileId): bool { + $query = $this->connection->getQueryBuilder(); + $query->select('fileid') + ->from('filecache') + ->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + return $query->executeQuery()->fetchOne() !== false; + } + + /** + * @return \Traversable<int, array{id: int, owner: string, fileid: int, target: string}> + */ + public function getAllShares() { + $query = $this->connection->getQueryBuilder(); + $query->select('id', 'file_source', 'uid_owner', 'file_target') + ->from('share') + ->where($query->expr()->in('item_type', $query->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY))); + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + yield [ + 'id' => (int)$row['id'], + 'owner' => (string)$row['uid_owner'], + 'fileid' => (int)$row['file_source'], + 'target' => (string)$row['file_target'], + ]; + } + } + + public function findOwner(int $fileId): ?string { + $mounts = $this->userMountCache->getMountsForFileId($fileId); + if (!$mounts) { + return null; + } + foreach ($mounts as $mount) { + $userHomeMountPoint = '/' . $mount->getUser()->getUID() . '/'; + if ($mount->getMountPoint() === $userHomeMountPoint) { + return $mount->getUser()->getUID(); + } + } + return null; + } + + public function updateShareOwner(int $shareId, string $owner): void { + $query = $this->connection->getQueryBuilder(); + $query->update('share') + ->set('uid_owner', $query->createNamedParameter($owner)) + ->where($query->expr()->eq('id', $query->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } +} diff --git a/apps/files_sharing/lib/ResponseDefinitions.php b/apps/files_sharing/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..71a2b25a70c --- /dev/null +++ b/apps/files_sharing/lib/ResponseDefinitions.php @@ -0,0 +1,237 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing; + +/** + * @psalm-type Files_SharingShare = array{ + * attributes: ?string, + * can_delete: bool, + * can_edit: bool, + * displayname_file_owner: string, + * displayname_owner: string, + * expiration: ?string, + * file_parent: int, + * file_source: int, + * file_target: string, + * has_preview: bool, + * hide_download: 0|1, + * is_trusted_server?: bool, + * is-mount-root: bool, + * id: string, + * item_mtime: int, + * item_permissions?: int, + * item_size: float|int, + * item_source: int, + * item_type: 'file'|'folder', + * label: string, + * mail_send: 0|1, + * mimetype: string, + * mount-type: string, + * note: string, + * parent: null, + * password?: null|string, + * password_expiration_time?: ?string, + * path: ?string, + * permissions: int, + * send_password_by_talk?: bool, + * share_type: int, + * share_with?: null|string, + * share_with_avatar?: string, + * share_with_displayname?: string, + * share_with_displayname_unique?: ?string, + * share_with_link?: string, + * status?: array{clearAt: int|null, icon: ?string, message: ?string, status: string}, + * stime: int, + * storage: int, + * storage_id: string, + * token: ?string, + * uid_file_owner: string, + * uid_owner: string, + * url?: string, + * } + * + * @psalm-type Files_SharingDeletedShare = array{ + * id: string, + * share_type: int, + * uid_owner: string, + * displayname_owner: string, + * permissions: int, + * stime: int, + * uid_file_owner: string, + * displayname_file_owner: string, + * path: string, + * item_type: string, + * mimetype: string, + * storage: int, + * item_source: int, + * file_source: int, + * file_parent: int, + * file_target: int, + * expiration: string|null, + * share_with: string|null, + * share_with_displayname: string|null, + * share_with_link: string|null, + * } + * + * @psalm-type Files_SharingRemoteShare = array{ + * accepted: bool, + * file_id: int|null, + * id: int, + * mimetype: string|null, + * mountpoint: string, + * mtime: int|null, + * name: string, + * owner: string, + * parent: int|null, + * permissions: int|null, + * remote: string, + * remote_id: string, + * share_token: string, + * share_type: int, + * type: string|null, + * user: string, + * } + * + * @psalm-type Files_SharingSharee = array{ + * label: string, + * } + * + * @psalm-type Files_SharingShareeValue = array{ + * shareType: int, + * shareWith: string, + * } + * + * @psalm-type Files_SharingShareeGroup = Files_SharingSharee&array{ + * value: Files_SharingShareeValue, + * } + * + * @psalm-type Files_SharingShareeRoom = Files_SharingSharee&array{ + * value: Files_SharingShareeValue, + * } + * + * @psalm-type Files_SharingShareeUser = Files_SharingSharee&array{ + * subline: string, + * icon: string, + * shareWithDisplayNameUnique: string, + * status: array{ + * status: string, + * message: string, + * icon: string, + * clearAt: int|null, + * }, + * value: Files_SharingShareeValue, + * } + * + * @psalm-type Files_SharingShareeRemoteGroup = Files_SharingSharee&array{ + * guid: string, + * name: string, + * value: Files_SharingShareeValue&array{ + * server: string, + * } + * } + * + * @psalm-type Files_SharingLookup = array{ + * value: string, + * verified: int, + * } + * + * @psalm-type Files_SharingShareeLookup = Files_SharingSharee&array{ + * extra: array{ + * federationId: string, + * name: Files_SharingLookup|null, + * email: Files_SharingLookup|null, + * address: Files_SharingLookup|null, + * website: Files_SharingLookup|null, + * twitter: Files_SharingLookup|null, + * phone: Files_SharingLookup|null, + * twitter_signature: Files_SharingLookup|null, + * website_signature: Files_SharingLookup|null, + * userid: Files_SharingLookup|null, + * }, + * value: Files_SharingShareeValue&array{ + * globalScale: bool, + * } + * } + * + * @psalm-type Files_SharingShareeEmail = Files_SharingSharee&array{ + * uuid: string, + * name: string, + * type: string, + * shareWithDisplayNameUnique: string, + * value: Files_SharingShareeValue, + * } + * + * @psalm-type Files_SharingShareeRemote = Files_SharingSharee&array{ + * uuid: string, + * name: string, + * type: string, + * value: Files_SharingShareeValue&array{ + * server: string, + * } + * } + * + * @psalm-type Files_SharingShareeCircle = Files_SharingSharee&array{ + * shareWithDescription: string, + * value: Files_SharingShareeValue&array{ + * circle: string, + * } + * } + * + * @psalm-type Files_SharingShareesSearchResult = array{ + * exact: array{ + * circles: list<Files_SharingShareeCircle>, + * emails: list<Files_SharingShareeEmail>, + * groups: list<Files_SharingShareeGroup>, + * remote_groups: list<Files_SharingShareeRemoteGroup>, + * remotes: list<Files_SharingShareeRemote>, + * rooms: list<Files_SharingShareeRoom>, + * users: list<Files_SharingShareeUser>, + * }, + * circles: list<Files_SharingShareeCircle>, + * emails: list<Files_SharingShareeEmail>, + * groups: list<Files_SharingShareeGroup>, + * lookup: list<Files_SharingShareeLookup>, + * remote_groups: list<Files_SharingShareeRemoteGroup>, + * remotes: list<Files_SharingShareeRemote>, + * rooms: list<Files_SharingShareeRoom>, + * users: list<Files_SharingShareeUser>, + * lookupEnabled: bool, + * } + * + * @psalm-type Files_SharingShareesRecommendedResult = array{ + * exact: array{ + * emails: list<Files_SharingShareeEmail>, + * groups: list<Files_SharingShareeGroup>, + * remote_groups: list<Files_SharingShareeRemoteGroup>, + * remotes: list<Files_SharingShareeRemote>, + * users: list<Files_SharingShareeUser>, + * }, + * emails: list<Files_SharingShareeEmail>, + * groups: list<Files_SharingShareeGroup>, + * remote_groups: list<Files_SharingShareeRemoteGroup>, + * remotes: list<Files_SharingShareeRemote>, + * users: list<Files_SharingShareeUser>, + * } + * + * @psalm-type Files_SharingShareInfo = array{ + * id: int, + * parentId: int, + * mtime: int, + * name: string, + * permissions: int, + * mimetype: string, + * size: int|float, + * type: string, + * etag: string, + * children?: list<array<string, mixed>>, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/files_sharing/lib/Scanner.php b/apps/files_sharing/lib/Scanner.php index baab7a862bd..28972c1b462 100644 --- a/apps/files_sharing/lib/Scanner.php +++ b/apps/files_sharing/lib/Scanner.php @@ -1,38 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @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 OCA\Files_Sharing; -use OC\Files\ObjectStore\NoopScanner; +use OC\Files\ObjectStore\ObjectStoreScanner; +use OC\Files\Storage\Storage; /** * Scanner for SharedStorage */ class Scanner extends \OC\Files\Cache\Scanner { /** - * @var \OCA\Files_Sharing\SharedStorage $storage + * @var SharedStorage $storage */ protected $storage; @@ -61,7 +45,7 @@ class Scanner extends \OC\Files\Cache\Scanner { return $this->sourceScanner; } if ($this->storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { - /** @var \OC\Files\Storage\Storage $storage */ + /** @var Storage $storage */ [$storage] = $this->storage->resolvePath(''); $this->sourceScanner = $storage->getScanner(); return $this->sourceScanner; @@ -72,8 +56,9 @@ class Scanner extends \OC\Files\Cache\Scanner { public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { $sourceScanner = $this->getSourceScanner(); - if ($sourceScanner instanceof NoopScanner) { - return []; + if ($sourceScanner instanceof ObjectStoreScanner) { + // ObjectStoreScanner doesn't scan + return null; } else { return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock); } diff --git a/apps/files_sharing/lib/Settings/Personal.php b/apps/files_sharing/lib/Settings/Personal.php index d3886321f97..171131b1819 100644 --- a/apps/files_sharing/lib/Settings/Personal.php +++ b/apps/files_sharing/lib/Settings/Personal.php @@ -3,31 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Hinrich Mahler <nextcloud@mahlerhome.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Settings; use OCA\Files_Sharing\AppInfo\Application; +use OCA\Files_Sharing\Helper; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\IConfig; @@ -35,31 +17,27 @@ use OCP\Settings\ISettings; class Personal implements ISettings { - /** @var IConfig */ - private $config; - /** @var IInitialState */ - private $initialState; - /** @var string */ - private $userId; - - public function __construct(IConfig $config, IInitialState $initialState, string $userId) { - $this->config = $config; - $this->initialState = $initialState; - $this->userId = $userId; + public function __construct( + private IConfig $config, + private IInitialState $initialState, + private string $userId, + ) { } public function getForm(): TemplateResponse { $defaultAcceptSystemConfig = $this->config->getSystemValueBool('sharing.enable_share_accept', false) ? 'no' : 'yes'; - $shareFolderSystemConfig = $this->config->getSystemValue('share_folder', '/'); + $defaultShareFolder = $this->config->getSystemValue('share_folder', '/'); + $userShareFolder = Helper::getShareFolder(userId: $this->userId); $acceptDefault = $this->config->getUserValue($this->userId, Application::APP_ID, 'default_accept', $defaultAcceptSystemConfig) === 'yes'; $enforceAccept = $this->config->getSystemValueBool('sharing.force_share_accept', false); $allowCustomDirectory = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true); - $shareFolderDefault = $this->config->getUserValue($this->userId, Application::APP_ID, 'share_folder', $shareFolderSystemConfig); + $this->initialState->provideInitialState('accept_default', $acceptDefault); $this->initialState->provideInitialState('enforce_accept', $enforceAccept); $this->initialState->provideInitialState('allow_custom_share_folder', $allowCustomDirectory); - $this->initialState->provideInitialState('share_folder', $shareFolderDefault); - $this->initialState->provideInitialState('default_share_folder', $shareFolderSystemConfig); + $this->initialState->provideInitialState('default_share_folder', $defaultShareFolder); + $this->initialState->provideInitialState('share_folder', $userShareFolder); + return new TemplateResponse('files_sharing', 'Settings/personal'); } diff --git a/apps/files_sharing/lib/ShareBackend/File.php b/apps/files_sharing/lib/ShareBackend/File.php index c84cbd66c67..2aa52ef1b7f 100644 --- a/apps/files_sharing/lib/ShareBackend/File.php +++ b/apps/files_sharing/lib/ShareBackend/File.php @@ -1,42 +1,24 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\ShareBackend; +use OC\Files\Filesystem; +use OC\Files\View; use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\Files_Sharing\Helper; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\Server; use OCP\Share\IShare; +use OCP\Share_Backend_File_Dependent; +use Psr\Log\LoggerInterface; -class File implements \OCP\Share_Backend_File_Dependent { +class File implements Share_Backend_File_Dependent { public const FORMAT_SHARED_STORAGE = 0; public const FORMAT_GET_FOLDER_CONTENTS = 1; public const FORMAT_FILE_APP_ROOT = 2; @@ -47,26 +29,25 @@ class File implements \OCP\Share_Backend_File_Dependent { private $path; - /** @var FederatedShareProvider */ - private $federatedShareProvider; - - public function __construct(FederatedShareProvider $federatedShareProvider = null) { + public function __construct( + private ?FederatedShareProvider $federatedShareProvider = null, + ) { if ($federatedShareProvider) { $this->federatedShareProvider = $federatedShareProvider; } else { - $this->federatedShareProvider = \OC::$server->query(FederatedShareProvider::class); + $this->federatedShareProvider = Server::get(FederatedShareProvider::class); } } public function isValidSource($itemSource, $uidOwner) { try { - $path = \OC\Files\Filesystem::getPath($itemSource); + $path = Filesystem::getPath($itemSource); // FIXME: attributes should not be set here, // keeping this pattern for now to avoid unexpected // regressions - $this->path = \OC\Files\Filesystem::normalizePath(basename($path)); + $this->path = Filesystem::normalizePath(basename($path)); return true; - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { return false; } } @@ -78,9 +59,9 @@ class File implements \OCP\Share_Backend_File_Dependent { return $path; } else { try { - $path = \OC\Files\Filesystem::getPath($itemSource); + $path = Filesystem::getPath($itemSource); return $path; - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { return false; } } @@ -91,20 +72,14 @@ class File implements \OCP\Share_Backend_File_Dependent { * * @param string $itemSource * @param string $shareWith - * @param array $exclude (optional) * @return string */ - public function generateTarget($itemSource, $shareWith, $exclude = null) { - $shareFolder = \OCA\Files_Sharing\Helper::getShareFolder(); - $target = \OC\Files\Filesystem::normalizePath($shareFolder . '/' . basename($itemSource)); + public function generateTarget($itemSource, $shareWith) { + $shareFolder = Helper::getShareFolder(); + $target = Filesystem::normalizePath($shareFolder . '/' . basename($itemSource)); - // for group shares we return the target right away - if ($shareWith === false) { - return $target; - } - - \OC\Files\Filesystem::initMountPoints($shareWith); - $view = new \OC\Files\View('/' . $shareWith . '/files'); + Filesystem::initMountPoints($shareWith); + $view = new View('/' . $shareWith . '/files'); if (!$view->is_dir($shareFolder)) { $dir = ''; @@ -117,9 +92,7 @@ class File implements \OCP\Share_Backend_File_Dependent { } } - $excludeList = is_array($exclude) ? $exclude : []; - - return \OCA\Files_Sharing\Helper::generateUniqueTarget($target, $excludeList, $view); + return Helper::generateUniqueTarget($target, $view); } public function formatItems($items, $format, $parameters = null) { @@ -150,7 +123,7 @@ class File implements \OCP\Share_Backend_File_Dependent { $file['uid_owner'] = $item['uid_owner']; $file['displayname_owner'] = $item['displayname_owner']; - $storage = \OC\Files\Filesystem::getStorage('/'); + $storage = Filesystem::getStorage('/'); $cache = $storage->getCache(); $file['size'] = $item['size']; $files[] = $file; @@ -211,13 +184,13 @@ class File implements \OCP\Share_Backend_File_Dependent { if (isset($source['parent'])) { $parent = $source['parent']; while (isset($parent)) { - $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $qb = Server::get(IDBConnection::class)->getQueryBuilder(); $qb->select('parent', 'uid_owner') ->from('share') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($parent)) ); - $result = $qb->execute(); + $result = $qb->executeQuery(); $item = $result->fetch(); $result->closeCursor(); if (isset($item['parent'])) { @@ -233,7 +206,7 @@ class File implements \OCP\Share_Backend_File_Dependent { if (isset($fileOwner)) { $source['fileOwner'] = $fileOwner; } else { - \OC::$server->getLogger()->error('No owner found for reshare', ['app' => 'files_sharing']); + Server::get(LoggerInterface::class)->error('No owner found for reshare', ['app' => 'files_sharing']); } return $source; diff --git a/apps/files_sharing/lib/ShareBackend/Folder.php b/apps/files_sharing/lib/ShareBackend/Folder.php index 51d0c709d98..df5529c3c4a 100644 --- a/apps/files_sharing/lib/ShareBackend/Folder.php +++ b/apps/files_sharing/lib/ShareBackend/Folder.php @@ -1,98 +1,22 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\ShareBackend; -class Folder extends File implements \OCP\Share_Backend_Collection { - - /** - * get shared parents - * - * @param int $itemSource item source ID - * @param string $shareWith with whom should the item be shared - * @param string $owner owner of the item - * @return array with shares - */ - public function getParents($itemSource, $shareWith = null, $owner = null) { - $result = []; - $parent = $this->getParentId($itemSource); - - $userManager = \OC::$server->getUserManager(); - - while ($parent) { - $shares = \OCP\Share::getItemSharedWithUser('folder', $parent, $shareWith, $owner); - if ($shares) { - foreach ($shares as $share) { - $name = basename($share['path']); - $share['collection']['path'] = $name; - $share['collection']['item_type'] = 'folder'; - $share['file_path'] = $name; - - $ownerUser = $userManager->get($share['uid_owner']); - $displayNameOwner = $ownerUser === null ? $share['uid_owner'] : $ownerUser->getDisplayName(); - $shareWithUser = $userManager->get($share['share_with']); - $displayNameShareWith = $shareWithUser === null ? $share['share_with'] : $shareWithUser->getDisplayName(); - $share['displayname_owner'] = $displayNameOwner ? $displayNameOwner : $share['uid_owner']; - $share['share_with_displayname'] = $displayNameShareWith ? $displayNameShareWith : $share['uid_owner']; - - $result[] = $share; - } - } - $parent = $this->getParentId($parent); - } - - return $result; - } - - /** - * get file cache ID of parent - * - * @param int $child file cache ID of child - * @return mixed parent ID or null - */ - private function getParentId($child) { - $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - $qb->select('parent') - ->from('filecache') - ->where( - $qb->expr()->eq('fileid', $qb->createNamedParameter($child)) - ); - $result = $qb->execute(); - $row = $result->fetch(); - $result->closeCursor(); - return $row ? $row['parent'] : null; - } +use OCP\IDBConnection; +use OCP\Server; +use OCP\Share_Backend_Collection; +class Folder extends File implements Share_Backend_Collection { public function getChildren($itemSource) { $children = []; $parents = [$itemSource]; - $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $qb = Server::get(IDBConnection::class)->getQueryBuilder(); $qb->select('id') ->from('mimetypes') ->where( @@ -103,12 +27,12 @@ class Folder extends File implements \OCP\Share_Backend_Collection { $result->closeCursor(); if ($row = $result->fetchRow()) { - $mimetype = (int) $row['id']; + $mimetype = (int)$row['id']; } else { $mimetype = -1; } while (!empty($parents)) { - $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $qb = Server::get(IDBConnection::class)->getQueryBuilder(); $parents = array_map(function ($parent) use ($qb) { return $qb->createNamedParameter($parent); @@ -126,7 +50,7 @@ class Folder extends File implements \OCP\Share_Backend_Collection { while ($file = $result->fetch()) { $children[] = ['source' => $file['fileid'], 'file_path' => $file['name']]; // If a child folder is found look inside it - if ((int) $file['mimetype'] === $mimetype) { + if ((int)$file['mimetype'] === $mimetype) { $parents[] = $file['fileid']; } } diff --git a/apps/files_sharing/lib/SharedMount.php b/apps/files_sharing/lib/SharedMount.php index 398da5eaf23..692a6c8979b 100644 --- a/apps/files_sharing/lib/SharedMount.php +++ b/apps/files_sharing/lib/SharedMount.php @@ -1,114 +1,82 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Frédéric Fortier <frederic.fortier@oronospolytechnique.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.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 OCA\Files_Sharing; -use OC\Cache\CappedMemoryCache; use OC\Files\Filesystem; use OC\Files\Mount\MountPoint; use OC\Files\Mount\MoveableMount; use OC\Files\View; +use OCA\Files_Sharing\Exceptions\BrokenPath; +use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Events\InvalidateMountCacheEvent; use OCP\Files\Storage\IStorageFactory; -use OCP\ICache; +use OCP\IDBConnection; use OCP\IUser; +use OCP\Server; use OCP\Share\Events\VerifyMountPointEvent; +use OCP\Share\IShare; +use Psr\Log\LoggerInterface; /** * Shared mount points can be moved by the user */ -class SharedMount extends MountPoint implements MoveableMount { +class SharedMount extends MountPoint implements MoveableMount, ISharedMountPoint { /** - * @var \OCA\Files_Sharing\SharedStorage $storage + * @var SharedStorage $storage */ protected $storage = null; - /** - * @var \OC\Files\View - */ - private $recipientView; - - private IUser $user; - - /** @var \OCP\Share\IShare */ + /** @var IShare */ private $superShare; - /** @var \OCP\Share\IShare[] */ + /** @var IShare[] */ private $groupedShares; - private IEventDispatcher $eventDispatcher; - - private ICache $cache; - public function __construct( $storage, array $mountpoints, $arguments, IStorageFactory $loader, - View $recipientView, + private View $recipientView, CappedMemoryCache $folderExistCache, - IEventDispatcher $eventDispatcher, - IUser $user, - ICache $cache + private IEventDispatcher $eventDispatcher, + private IUser $user, + bool $alreadyVerified, ) { - $this->user = $user; - $this->recipientView = $recipientView; - $this->eventDispatcher = $eventDispatcher; - $this->cache = $cache; - $this->superShare = $arguments['superShare']; $this->groupedShares = $arguments['groupedShares']; - $newMountPoint = $this->verifyMountPoint($this->superShare, $mountpoints, $folderExistCache); - $absMountPoint = '/' . $user->getUID() . '/files' . $newMountPoint; + $absMountPoint = '/' . $user->getUID() . '/files/' . trim($this->superShare->getTarget(), '/') . '/'; + + // after the mountpoint is verified for the first time, only new mountpoints (e.g. groupfolders can overwrite the target) + if (!$alreadyVerified || isset($mountpoints[$absMountPoint])) { + $newMountPoint = $this->verifyMountPoint($this->superShare, $mountpoints, $folderExistCache); + $absMountPoint = '/' . $user->getUID() . '/files/' . trim($newMountPoint, '/') . '/'; + } + parent::__construct($storage, $absMountPoint, $arguments, $loader, null, null, MountProvider::class); } /** * check if the parent folder exists otherwise move the mount point up * - * @param \OCP\Share\IShare $share + * @param IShare $share * @param SharedMount[] $mountpoints + * @param CappedMemoryCache<bool> $folderExistCache * @return string */ private function verifyMountPoint( - \OCP\Share\IShare $share, + IShare $share, array $mountpoints, - CappedMemoryCache $folderExistCache + CappedMemoryCache $folderExistCache, ) { - $cacheKey = $this->user->getUID() . '/' . $share->getTarget(); - $cached = $this->cache->get($cacheKey); - if ($cached !== null) { - return $cached; - } - $mountPoint = basename($share->getTarget()); $parent = dirname($share->getTarget()); @@ -116,8 +84,9 @@ class SharedMount extends MountPoint implements MoveableMount { $this->eventDispatcher->dispatchTyped($event); $parent = $event->getParent(); - if ($folderExistCache->hasKey($parent)) { - $parentExists = $folderExistCache->get($parent); + $cached = $folderExistCache->get($parent); + if ($cached) { + $parentExists = $cached; } else { $parentExists = $this->recipientView->is_dir($parent); $folderExistCache->set($parent, $parentExists); @@ -127,7 +96,7 @@ class SharedMount extends MountPoint implements MoveableMount { } $newMountPoint = $this->generateUniqueTarget( - \OC\Files\Filesystem::normalizePath($parent . '/' . $mountPoint), + Filesystem::normalizePath($parent . '/' . $mountPoint), $this->recipientView, $mountpoints ); @@ -136,8 +105,6 @@ class SharedMount extends MountPoint implements MoveableMount { $this->updateFileTarget($newMountPoint, $share); } - $this->cache->set($cacheKey, $newMountPoint, 60 * 60); - return $newMountPoint; } @@ -145,7 +112,7 @@ class SharedMount extends MountPoint implements MoveableMount { * update fileTarget in the database if the mount point changed * * @param string $newPath - * @param \OCP\Share\IShare $share + * @param IShare $share * @return bool */ private function updateFileTarget($newPath, &$share) { @@ -153,7 +120,7 @@ class SharedMount extends MountPoint implements MoveableMount { foreach ($this->groupedShares as $tmpShare) { $tmpShare->setTarget($newPath); - \OC::$server->getShareManager()->moveShare($tmpShare, $this->user->getUID()); + Server::get(\OCP\Share\IManager::class)->moveShare($tmpShare, $this->user->getUID()); } $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->user)); @@ -188,7 +155,7 @@ class SharedMount extends MountPoint implements MoveableMount { * * @param string $path the absolute path * @return string e.g. turns '/admin/files/test.txt' into '/test.txt' - * @throws \OCA\Files_Sharing\Exceptions\BrokenPath + * @throws BrokenPath */ protected function stripUserFilesPath($path) { $trimmed = ltrim($path, '/'); @@ -196,8 +163,8 @@ class SharedMount extends MountPoint implements MoveableMount { // it is not a file relative to data/user/files if (count($split) < 3 || $split[1] !== 'files') { - \OC::$server->getLogger()->error('Can not strip userid and "files/" from path: ' . $path, ['app' => 'files_sharing']); - throw new \OCA\Files_Sharing\Exceptions\BrokenPath('Path does not start with /user/files', 10); + Server::get(LoggerInterface::class)->error('Can not strip userid and "files/" from path: ' . $path, ['app' => 'files_sharing']); + throw new BrokenPath('Path does not start with /user/files', 10); } // skip 'user' and 'files' @@ -224,7 +191,13 @@ class SharedMount extends MountPoint implements MoveableMount { $this->setMountPoint($target); $this->storage->setMountPoint($relTargetPath); } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_sharing', 'message' => 'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"']); + Server::get(LoggerInterface::class)->error( + 'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"', + [ + 'app' => 'files_sharing', + 'exception' => $e, + ] + ); } return $result; @@ -236,8 +209,8 @@ class SharedMount extends MountPoint implements MoveableMount { * @return bool */ public function removeMount() { - $mountManager = \OC\Files\Filesystem::getMountManager(); - /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $mountManager = Filesystem::getMountManager(); + /** @var SharedStorage $storage */ $storage = $this->getStorage(); $result = $storage->unshareStorage(); $mountManager->removeMount($this->mountPoint); @@ -246,13 +219,20 @@ class SharedMount extends MountPoint implements MoveableMount { } /** - * @return \OCP\Share\IShare + * @return IShare */ public function getShare() { return $this->superShare; } /** + * @return IShare[] + */ + public function getGroupedShares(): array { + return $this->groupedShares; + } + + /** * Get the file id of the root of the storage * * @return int @@ -268,13 +248,13 @@ class SharedMount extends MountPoint implements MoveableMount { if (!is_null($this->getShare()->getNodeCacheEntry())) { return $this->getShare()->getNodeCacheEntry()->getStorageId(); } else { - $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $builder = Server::get(IDBConnection::class)->getQueryBuilder(); $query = $builder->select('storage') ->from('filecache') ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($this->getStorageRootId()))); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(); $result->closeCursor(); if ($row) { diff --git a/apps/files_sharing/lib/SharedStorage.php b/apps/files_sharing/lib/SharedStorage.php index 60a292c498d..e310c5f3138 100644 --- a/apps/files_sharing/lib/SharedStorage.php +++ b/apps/files_sharing/lib/SharedStorage.php @@ -1,74 +1,59 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author J0WI <J0WI@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author scambra <sergio@entrecables.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @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 OCA\Files_Sharing; +use OC\Files\Cache\CacheDependencies; +use OC\Files\Cache\CacheEntry; use OC\Files\Cache\FailedCache; use OC\Files\Cache\NullWatcher; -use OC\Files\Cache\Watcher; use OC\Files\ObjectStore\HomeObjectStoreStorage; use OC\Files\Storage\Common; -use OC\Files\Storage\Home; -use OC\User\DisplayNameCache; -use OCP\Files\Folder; -use OCP\Files\IHomeStorage; -use OCP\Files\Node; use OC\Files\Storage\FailedStorage; +use OC\Files\Storage\Home; +use OC\Files\Storage\Storage; +use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\PermissionsMask; +use OC\Files\Storage\Wrapper\Wrapper; +use OC\Files\View; +use OC\Share\Share; use OC\User\NoUserException; -use OCA\Files_External\Config\ExternalMountPoint; +use OCA\Files_Sharing\ISharedStorage as LegacyISharedStorage; use OCP\Constants; +use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Cache\IScanner; +use OCP\Files\Cache\IWatcher; +use OCP\Files\Folder; +use OCP\Files\IHomeStorage; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\Storage\IDisableEncryptionStorage; +use OCP\Files\Storage\ILockingStorage; +use OCP\Files\Storage\ISharedStorage; use OCP\Files\Storage\IStorage; -use OCP\IUserManager; use OCP\Lock\ILockingProvider; +use OCP\Server; use OCP\Share\IShare; +use OCP\Util; +use Psr\Log\LoggerInterface; /** * Convert target path to source path and pass the function call to the correct storage provider */ -class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedStorage, IDisableEncryptionStorage { - - /** @var \OCP\Share\IShare */ +class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage { + /** @var IShare */ private $superShare; - /** @var \OCP\Share\IShare[] */ + /** @var IShare[] */ private $groupedShares; /** - * @var \OC\Files\View + * @var View */ private $ownerView; @@ -82,15 +67,12 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto /** @var string */ private $user; - /** - * @var \OCP\ILogger - */ - private $logger; + private LoggerInterface $logger; - /** @var IStorage */ + /** @var IStorage */ private $nonMaskedStorage; - private $options; + private array $mountOptions = []; /** @var boolean */ private $sharingDisabledForUser; @@ -100,16 +82,24 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto private string $sourcePath = ''; - public function __construct($arguments) { - $this->ownerView = $arguments['ownerView']; - $this->logger = \OC::$server->getLogger(); + private static int $initDepth = 0; + + /** + * @psalm-suppress NonInvariantDocblockPropertyType + * @var ?Storage $storage + */ + protected $storage; + + public function __construct(array $parameters) { + $this->ownerView = $parameters['ownerView']; + $this->logger = Server::get(LoggerInterface::class); - $this->superShare = $arguments['superShare']; - $this->groupedShares = $arguments['groupedShares']; + $this->superShare = $parameters['superShare']; + $this->groupedShares = $parameters['groupedShares']; - $this->user = $arguments['user']; - if (isset($arguments['sharingDisabledForUser'])) { - $this->sharingDisabledForUser = $arguments['sharingDisabledForUser']; + $this->user = $parameters['user']; + if (isset($parameters['sharingDisabledForUser'])) { + $this->sharingDisabledForUser = $parameters['sharingDisabledForUser']; } else { $this->sharingDisabledForUser = false; } @@ -135,27 +125,60 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->sourceRootInfo; } + /** + * @psalm-assert Storage $this->storage + */ private function init() { if ($this->initialized) { + if (!$this->storage) { + // marked as initialized but no storage set + // this is probably because some code path has caused recursion during the share setup + // we setup a "failed storage" so `getWrapperStorage` doesn't return null. + // If the share setup completes after this the "failed storage" will be overwritten by the correct one + $this->logger->warning('Possible share setup recursion detected'); + $this->storage = new FailedStorage(['exception' => new \Exception('Possible share setup recursion detected')]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } return; } + $this->initialized = true; + self::$initDepth++; + try { + if (self::$initDepth > 10) { + throw new \Exception('Maximum share depth reached'); + } + /** @var IRootFolder $rootFolder */ - $rootFolder = \OC::$server->get(IRootFolder::class); + $rootFolder = Server::get(IRootFolder::class); $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner()); $sourceId = $this->superShare->getNodeId(); $ownerNodes = $this->ownerUserFolder->getById($sourceId); - /** @var Node|false $ownerNode */ - $ownerNode = current($ownerNodes); - if (!$ownerNode) { + + if (count($ownerNodes) === 0) { $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]); $this->cache = new FailedCache(); $this->rootPath = ''; } else { - $this->nonMaskedStorage = $ownerNode->getStorage(); - $this->sourcePath = $ownerNode->getPath(); - $this->rootPath = $ownerNode->getInternalPath(); + foreach ($ownerNodes as $ownerNode) { + $nonMaskedStorage = $ownerNode->getStorage(); + + // check if potential source node would lead to a recursive share setup + if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) { + continue; + } + $this->nonMaskedStorage = $nonMaskedStorage; + $this->sourcePath = $ownerNode->getPath(); + $this->rootPath = $ownerNode->getInternalPath(); + $this->cache = null; + break; + } + if (!$this->nonMaskedStorage) { + // all potential source nodes would have been recursive + throw new \Exception('recursive share detected'); + } $this->storage = new PermissionsMask([ 'storage' => $this->nonMaskedStorage, 'mask' => $this->superShare->getPermissions(), @@ -175,18 +198,16 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto $this->storage = new FailedStorage(['exception' => $e]); $this->cache = new FailedCache(); $this->rootPath = ''; - $this->logger->logException($e); + $this->logger->error($e->getMessage(), ['exception' => $e]); } if (!$this->nonMaskedStorage) { $this->nonMaskedStorage = $this->storage; } + self::$initDepth--; } - /** - * @inheritdoc - */ - public function instanceOfStorage($class): bool { + public function instanceOfStorage(string $class): bool { if ($class === '\OC\Files\Storage\Common' || $class == Common::class) { return true; } @@ -214,44 +235,33 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; } - /** - * get id of the mount point - * - * @return string - */ public function getId(): string { return 'shared::' . $this->getMountPoint(); } - /** - * Get the permissions granted for a shared file - * - * @param string $target Shared target file path - * @return int CRUDS permissions granted - */ - public function getPermissions($target = ''): int { + public function getPermissions(string $path = ''): int { if (!$this->isValid()) { return 0; } - $permissions = parent::getPermissions($target) & $this->superShare->getPermissions(); + $permissions = parent::getPermissions($path) & $this->superShare->getPermissions(); // part files and the mount point always have delete permissions - if ($target === '' || pathinfo($target, PATHINFO_EXTENSION) === 'part') { - $permissions |= \OCP\Constants::PERMISSION_DELETE; + if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') { + $permissions |= Constants::PERMISSION_DELETE; } if ($this->sharingDisabledForUser) { - $permissions &= ~\OCP\Constants::PERMISSION_SHARE; + $permissions &= ~Constants::PERMISSION_SHARE; } return $permissions; } - public function isCreatable($path): bool { - return (bool)($this->getPermissions($path) & \OCP\Constants::PERMISSION_CREATE); + public function isCreatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); } - public function isReadable($path): bool { + public function isReadable(string $path): bool { if (!$this->isValid()) { return false; } @@ -264,22 +274,22 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $storage->isReadable($internalPath); } - public function isUpdatable($path): bool { - return (bool)($this->getPermissions($path) & \OCP\Constants::PERMISSION_UPDATE); + public function isUpdatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); } - public function isDeletable($path): bool { - return (bool)($this->getPermissions($path) & \OCP\Constants::PERMISSION_DELETE); + public function isDeletable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); } - public function isSharable($path): bool { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { return false; } - return (bool)($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE); + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $source = $this->getUnjailedPath($path); switch ($mode) { case 'r+': @@ -327,22 +337,15 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto 'source' => $source, 'mode' => $mode, ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode); } - /** - * see https://www.php.net/manual/en/function.rename.php - * - * @param string $path1 - * @param string $path2 - * @return bool - */ - public function rename($path1, $path2): bool { + public function rename(string $source, string $target): bool { $this->init(); - $isPartFile = pathinfo($path1, PATHINFO_EXTENSION) === 'part'; - $targetExists = $this->file_exists($path2); - $sameFolder = dirname($path1) === dirname($path2); + $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part'; + $targetExists = $this->file_exists($target); + $sameFolder = dirname($source) === dirname($target); if ($targetExists || ($sameFolder && !$isPartFile)) { if (!$this->isUpdatable('')) { @@ -354,7 +357,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto } } - return $this->nonMaskedStorage->rename($this->getUnjailedPath($path1), $this->getUnjailedPath($path2)); + return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); } /** @@ -366,10 +369,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->superShare->getTarget(); } - /** - * @param string $path - */ - public function setMountPoint($path): void { + public function setMountPoint(string $path): void { $this->superShare->setTarget($path); foreach ($this->groupedShares as $share) { @@ -386,9 +386,6 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->superShare->getShareOwner(); } - /** - * @return \OCP\Share\IShare - */ public function getShare(): IShare { return $this->superShare; } @@ -402,7 +399,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->superShare->getNodeType(); } - public function getCache($path = '', $storage = null) { + public function getCache(string $path = '', ?IStorage $storage = null): ICache { if ($this->cache) { return $this->cache; } @@ -414,41 +411,50 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return new FailedCache(); } - $this->cache = new \OCA\Files_Sharing\Cache( + $this->cache = new Cache( $storage, $sourceRoot, - \OC::$server->get(DisplayNameCache::class) + Server::get(CacheDependencies::class), + $this->getShare() ); return $this->cache; } - public function getScanner($path = '', $storage = null) { + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { if (!$storage) { $storage = $this; } - return new \OCA\Files_Sharing\Scanner($storage); + return new Scanner($storage); } - public function getOwner($path): string { + public function getOwner(string $path): string|false { return $this->superShare->getShareOwner(); } - public function getWatcher($path = '', $storage = null): Watcher { - $mountManager = \OC::$server->getMountManager(); + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { + if ($this->watcher) { + return $this->watcher; + } - // Get node informations + // Get node information $node = $this->getShare()->getNodeCacheEntry(); - if ($node) { - $mount = $mountManager->findByNumericId($node->getStorageId()); - // If the share is originating from an external storage - if (count($mount) > 0 && $mount[0] instanceof ExternalMountPoint) { - // Propagate original storage scan - return parent::getWatcher($path, $storage); + if ($node instanceof CacheEntry) { + $storageId = $node->getData()['storage_string_id'] ?? null; + // for shares from the home storage we can rely on the home storage to keep itself up to date + // for other storages we need use the proper watcher + if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) { + $cache = $this->getCache(); + $this->watcher = parent::getWatcher($path, $storage); + if ($cache instanceof Cache) { + $this->watcher->onUpdate($cache->markRootChanged(...)); + } + return $this->watcher; } } // cache updating is handled by the share source - return new NullWatcher(); + $this->watcher = new NullWatcher(); + return $this->watcher; } /** @@ -458,19 +464,13 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto */ public function unshareStorage(): bool { foreach ($this->groupedShares as $share) { - \OC::$server->getShareManager()->deleteFromSelf($share, $this->user); + Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user); } return true; } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - * @throws \OCP\Lock\LockedException - */ - public function acquireLock($path, $type, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->acquireLock($targetInternalPath, $type, $provider); // lock the parent folders of the owner when locking the share as recipient @@ -480,13 +480,8 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function releaseLock($path, $type, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->releaseLock($targetInternalPath, $type, $provider); // unlock the parent folders of the owner when unlocking the share as recipient @@ -496,21 +491,13 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto } } - /** - * @param string $path - * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE - * @param \OCP\Lock\ILockingProvider $provider - */ - public function changeLock($path, $type, ILockingProvider $provider) { - /** @var \OCP\Files\Storage $targetStorage */ + public function changeLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ [$targetStorage, $targetInternalPath] = $this->resolvePath($path); $targetStorage->changeLock($targetInternalPath, $type, $provider); } - /** - * @return array [ available, last_checked ] - */ - public function getAvailability() { + public function getAvailability(): array { // shares do not participate in availability logic return [ 'available' => true, @@ -518,10 +505,7 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto ]; } - /** - * @param bool $available - */ - public function setAvailability($available) { + public function setAvailability(bool $isAvailable): void { // shares do not participate in availability logic } @@ -530,35 +514,51 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto return $this->nonMaskedStorage; } - public function getWrapperStorage() { + public function getWrapperStorage(): Storage { $this->init(); + + /** + * @psalm-suppress DocblockTypeContradiction + */ + if (!$this->storage) { + $message = 'no storage set after init for share ' . $this->getShareId(); + $this->logger->error($message); + $this->storage = new FailedStorage(['exception' => new \Exception($message)]); + } + return $this->storage; } - public function file_get_contents($path) { + public function file_get_contents(string $path): string|false { $info = [ 'target' => $this->getMountPoint() . '/' . $path, 'source' => $this->getUnjailedPath($path), ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); return parent::file_get_contents($path); } - public function file_put_contents($path, $data) { + public function file_put_contents(string $path, mixed $data): int|float|false { $info = [ 'target' => $this->getMountPoint() . '/' . $path, 'source' => $this->getUnjailedPath($path), ]; - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); + Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); return parent::file_put_contents($path, $data); } - public function setMountOptions(array $options) { + public function setMountOptions(array $options): void { + /* Note: This value is never read */ $this->mountOptions = $options; } - public function getUnjailedPath($path) { + public function getUnjailedPath(string $path): string { $this->init(); return parent::getUnjailedPath($path); } + + public function getDirectDownload(string $path): array|false { + // disable direct download for shares + return []; + } } diff --git a/apps/files_sharing/lib/SharesReminderJob.php b/apps/files_sharing/lib/SharesReminderJob.php new file mode 100644 index 00000000000..854ad196d6b --- /dev/null +++ b/apps/files_sharing/lib/SharesReminderJob.php @@ -0,0 +1,307 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\Constants; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Defaults; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use OCP\Util; +use Psr\Log\LoggerInterface; + +/** + * Send a reminder via email to the sharee(s) if the folder is still empty a predefined time before the expiration date + */ +class SharesReminderJob extends TimedJob { + private const SECONDS_BEFORE_REMINDER = 24 * 60 * 60; + private const CHUNK_SIZE = 1000; + private int $folderMimeTypeId; + + public function __construct( + ITimeFactory $time, + private readonly IDBConnection $db, + private readonly IManager $shareManager, + private readonly IUserManager $userManager, + private readonly LoggerInterface $logger, + private readonly IURLGenerator $urlGenerator, + private readonly IFactory $l10nFactory, + private readonly IMailer $mailer, + private readonly Defaults $defaults, + IMimeTypeLoader $mimeTypeLoader, + ) { + parent::__construct($time); + $this->setInterval(60 * 60); + $this->folderMimeTypeId = $mimeTypeLoader->getId(ICacheEntry::DIRECTORY_MIMETYPE); + } + + + /** + * Makes the background job do its work + * + * @param array $argument unused argument + * @throws Exception if a database error occurs + */ + public function run(mixed $argument): void { + foreach ($this->getShares() as $share) { + $reminderInfo = $this->prepareReminder($share); + if ($reminderInfo !== null) { + $this->sendReminder($reminderInfo); + } + } + } + + /** + * Finds all shares of empty folders, for which the user has write permissions. + * The returned shares are of type user or email only, have expiration dates within the specified time frame + * and have not yet received a reminder. + * + * @return array<IShare>|\Iterator + * @throws Exception if a database error occurs + */ + private function getShares(): array|\Iterator { + if ($this->db->getShardDefinition('filecache')) { + $sharesResult = $this->getSharesDataSharded(); + } else { + $sharesResult = $this->getSharesData(); + } + foreach ($sharesResult as $share) { + if ($share['share_type'] === IShare::TYPE_EMAIL) { + $id = "ocMailShare:$share[id]"; + } else { + $id = "ocinternal:$share[id]"; + } + + try { + yield $this->shareManager->getShareById($id); + } catch (ShareNotFound) { + $this->logger->error("Share with ID $id not found."); + } + } + } + + /** + * @return list<array{id: int, share_type: int}> + */ + private function getSharesData(): array { + $minDate = new \DateTime(); + $maxDate = new \DateTime(); + $maxDate->setTimestamp($maxDate->getTimestamp() + self::SECONDS_BEFORE_REMINDER); + + $qb = $this->db->getQueryBuilder(); + $qb->select('s.id', 's.share_type') + ->from('share', 's') + ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('f.parent', 's.file_source')) + ->where( + $qb->expr()->andX( + $qb->expr()->orX( + $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_USER)), + $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_EMAIL)) + ), + $qb->expr()->eq('s.item_type', $qb->expr()->literal('folder')), + $qb->expr()->gte('s.expiration', $qb->createNamedParameter($minDate, IQueryBuilder::PARAM_DATE)), + $qb->expr()->lte('s.expiration', $qb->createNamedParameter($maxDate, IQueryBuilder::PARAM_DATE)), + $qb->expr()->eq('s.reminder_sent', $qb->createNamedParameter( + false, IQueryBuilder::PARAM_BOOL + )), + $qb->expr()->eq( + $qb->expr()->bitwiseAnd('s.permissions', Constants::PERMISSION_CREATE), + $qb->createNamedParameter(Constants::PERMISSION_CREATE, IQueryBuilder::PARAM_INT) + ), + $qb->expr()->isNull('f.fileid') + ) + ) + ->setMaxResults(SharesReminderJob::CHUNK_SIZE); + + $shares = $qb->executeQuery()->fetchAll(); + return array_values(array_map(fn ($share): array => [ + 'id' => (int)$share['id'], + 'share_type' => (int)$share['share_type'], + ], $shares)); + } + + /** + * Sharding compatible version of getSharesData + * + * @return list<array{id: int, share_type: int, file_source: int}> + */ + private function getSharesDataSharded(): array|\Iterator { + $minDate = new \DateTime(); + $maxDate = new \DateTime(); + $maxDate->setTimestamp($maxDate->getTimestamp() + self::SECONDS_BEFORE_REMINDER); + + $qb = $this->db->getQueryBuilder(); + $qb->select('s.id', 's.share_type', 's.file_source') + ->from('share', 's') + ->where( + $qb->expr()->andX( + $qb->expr()->orX( + $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_USER)), + $qb->expr()->eq('s.share_type', $qb->expr()->literal(IShare::TYPE_EMAIL)) + ), + $qb->expr()->eq('s.item_type', $qb->expr()->literal('folder')), + $qb->expr()->gte('s.expiration', $qb->createNamedParameter($minDate, IQueryBuilder::PARAM_DATE)), + $qb->expr()->lte('s.expiration', $qb->createNamedParameter($maxDate, IQueryBuilder::PARAM_DATE)), + $qb->expr()->eq('s.reminder_sent', $qb->createNamedParameter( + false, IQueryBuilder::PARAM_BOOL + )), + $qb->expr()->eq( + $qb->expr()->bitwiseAnd('s.permissions', Constants::PERMISSION_CREATE), + $qb->createNamedParameter(Constants::PERMISSION_CREATE, IQueryBuilder::PARAM_INT) + ), + ) + ); + + $shares = $qb->executeQuery()->fetchAll(); + $shares = array_values(array_map(fn ($share): array => [ + 'id' => (int)$share['id'], + 'share_type' => (int)$share['share_type'], + 'file_source' => (int)$share['file_source'], + ], $shares)); + return $this->filterSharesWithEmptyFolders($shares, self::CHUNK_SIZE); + } + + /** + * Check which of the supplied file ids is an empty folder until there are `$maxResults` folders + * @param list<array{id: int, share_type: int, file_source: int}> $shares + * @return list<array{id: int, share_type: int, file_source: int}> + */ + private function filterSharesWithEmptyFolders(array $shares, int $maxResults): array { + $query = $this->db->getQueryBuilder(); + $query->select('fileid') + ->from('filecache') + ->where($query->expr()->eq('size', $query->createNamedParameter(0), IQueryBuilder::PARAM_INT_ARRAY)) + ->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($this->folderMimeTypeId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->in('fileid', $query->createParameter('fileids'))); + $chunks = array_chunk($shares, SharesReminderJob::CHUNK_SIZE); + $results = []; + foreach ($chunks as $chunk) { + $chunkFileIds = array_map(fn ($share): int => $share['file_source'], $chunk); + $chunkByFileId = array_combine($chunkFileIds, $chunk); + $query->setParameter('fileids', $chunkFileIds, IQueryBuilder::PARAM_INT_ARRAY); + $chunkResults = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + foreach ($chunkResults as $folderId) { + $results[] = $chunkByFileId[$folderId]; + } + if (count($results) >= $maxResults) { + break; + } + } + return $results; + } + + /** + * Retrieves and returns all the necessary data before sending a reminder. + * It also updates the reminder sent flag for the affected shares (to avoid multiple reminders). + * + * @param IShare $share Share that was obtained with {@link getShares} + * @return array|null Info needed to send a reminder + */ + private function prepareReminder(IShare $share): ?array { + $sharedWith = $share->getSharedWith(); + $reminderInfo = []; + if ($share->getShareType() == IShare::TYPE_USER) { + $user = $this->userManager->get($sharedWith); + if ($user === null) { + return null; + } + $reminderInfo['email'] = $user->getEMailAddress(); + $reminderInfo['userLang'] = $this->l10nFactory->getUserLanguage($user); + $reminderInfo['folderLink'] = $this->urlGenerator->linkToRouteAbsolute('files.view.index', [ + 'dir' => $share->getTarget() + ]); + } else { + $reminderInfo['email'] = $sharedWith; + $reminderInfo['folderLink'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', [ + 'token' => $share->getToken() + ]); + } + if (empty($reminderInfo['email'])) { + return null; + } + + try { + $reminderInfo['folderName'] = $share->getNode()->getName(); + } catch (NotFoundException) { + $id = $share->getFullId(); + $this->logger->error("File by share ID $id not found."); + } + $share->setReminderSent(true); + $this->shareManager->updateShare($share); + return $reminderInfo; + } + + /** + * This method accepts data obtained by {@link prepareReminder} and sends reminder email. + * + * @param array $reminderInfo + * @return void + */ + private function sendReminder(array $reminderInfo): void { + $instanceName = $this->defaults->getName(); + $from = [Util::getDefaultEmailAddress($instanceName) => $instanceName]; + $l = $this->l10nFactory->get('files_sharing', $reminderInfo['userLang'] ?? null); + $emailTemplate = $this->generateEMailTemplate($l, [ + 'link' => $reminderInfo['folderLink'], 'name' => $reminderInfo['folderName'] + ]); + + $message = $this->mailer->createMessage(); + $message->setFrom($from); + $message->setTo([$reminderInfo['email']]); + $message->useTemplate($emailTemplate); + $errorText = "Sending email with share reminder to $reminderInfo[email] failed."; + try { + $failedRecipients = $this->mailer->send($message); + if (count($failedRecipients) > 0) { + $this->logger->error($errorText); + } + } catch (\Exception) { + $this->logger->error($errorText); + } + } + + /** + * Returns the reminder email template + * + * @param IL10N $l + * @param array $folder Folder the user should be reminded of + * @return IEMailTemplate + */ + private function generateEMailTemplate(IL10N $l, array $folder): IEMailTemplate { + $emailTemplate = $this->mailer->createEMailTemplate('files_sharing.SharesReminder', [ + 'folder' => $folder, + ]); + $emailTemplate->addHeader(); + $emailTemplate->setSubject( + $l->t('Remember to upload the files to %s', [$folder['name']]) + ); + $emailTemplate->addBodyText($l->t( + 'We would like to kindly remind you that you have not yet uploaded any files to the shared folder.' + )); + $emailTemplate->addBodyButton( + $l->t('Open "%s"', [$folder['name']]), + $folder['link'] + ); + $emailTemplate->addFooter(); + return $emailTemplate; + } +} diff --git a/apps/files_sharing/lib/Updater.php b/apps/files_sharing/lib/Updater.php index ad194dde016..24e82330d43 100644 --- a/apps/files_sharing/lib/Updater.php +++ b/apps/files_sharing/lib/Updater.php @@ -1,33 +1,19 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing; +use OC\Files\Cache\FileAccess; +use OC\Files\Filesystem; use OC\Files\Mount\MountPoint; use OCP\Constants; +use OCP\Files\Folder; +use OCP\Files\Mount\IMountManager; +use OCP\Server; use OCP\Share\IShare; class Updater { @@ -37,7 +23,7 @@ class Updater { */ public static function renameHook($params) { self::renameChildren($params['oldpath'], $params['newpath']); - self::moveShareToShare($params['newpath']); + self::moveShareInOrOutOfShare($params['newpath']); } /** @@ -50,21 +36,53 @@ class Updater { * * @param string $path */ - private static function moveShareToShare($path) { + private static function moveShareInOrOutOfShare($path): void { $userFolder = \OC::$server->getUserFolder(); // If the user folder can't be constructed (e.g. link share) just return. if ($userFolder === null) { return; } + $user = $userFolder->getOwner(); + if (!$user) { + throw new \Exception('user folder has no owner'); + } $src = $userFolder->get($path); - $shareManager = \OC::$server->getShareManager(); + $shareManager = Server::get(\OCP\Share\IManager::class); + + // We intentionally include invalid shares, as they have been automatically invalidated due to the node no longer + // being accessible for the user. Only in this case where we adjust the share after it was moved we want to ignore + // this to be able to still adjust it. + + // FIXME: should CIRCLES be included here ?? + $shares = $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER, $src, false, -1, onlyValid: false); + $shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP, $src, false, -1, onlyValid: false)); + $shares = array_merge($shares, $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM, $src, false, -1, onlyValid: false)); - $shares = $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_USER, $src, false, -1); - $shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_GROUP, $src, false, -1)); - $shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), IShare::TYPE_ROOM, $src, false, -1)); + if ($src instanceof Folder) { + $cacheAccess = Server::get(FileAccess::class); + + $sourceStorageId = $src->getStorage()->getCache()->getNumericStorageId(); + $sourceInternalPath = $src->getInternalPath(); + $subShares = array_merge( + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_USER, onlyValid: false), + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_GROUP, onlyValid: false), + $shareManager->getSharesBy($user->getUID(), IShare::TYPE_ROOM, onlyValid: false), + ); + $shareSourceIds = array_map(fn (IShare $share) => $share->getNodeId(), $subShares); + $shareSources = $cacheAccess->getByFileIdsInStorage($shareSourceIds, $sourceStorageId); + foreach ($subShares as $subShare) { + $shareCacheEntry = $shareSources[$subShare->getNodeId()] ?? null; + if ( + $shareCacheEntry + && str_starts_with($shareCacheEntry->getPath(), $sourceInternalPath . '/') + ) { + $shares[] = $subShare; + } + } + } // If the path we move is not a share we don't care if (empty($shares)) { @@ -72,24 +90,34 @@ class Updater { } // Check if the destination is inside a share - $mountManager = \OC::$server->getMountManager(); + $mountManager = Server::get(IMountManager::class); $dstMount = $mountManager->find($src->getPath()); - if (!($dstMount instanceof \OCA\Files_Sharing\SharedMount)) { - return; - } - - $newOwner = $dstMount->getShare()->getShareOwner(); //Ownership is moved over foreach ($shares as $share) { - /** @var IShare $share */ - if (!($dstMount->getShare()->getPermissions() & Constants::PERMISSION_SHARE)) { - $shareManager->deleteShare($share); + if ( + $share->getShareType() !== IShare::TYPE_USER + && $share->getShareType() !== IShare::TYPE_GROUP + && $share->getShareType() !== IShare::TYPE_ROOM + ) { continue; } + + if ($dstMount instanceof SharedMount) { + if (!($dstMount->getShare()->getPermissions() & Constants::PERMISSION_SHARE)) { + $shareManager->deleteShare($share); + continue; + } + $newOwner = $dstMount->getShare()->getShareOwner(); + $newPermissions = $share->getPermissions() & $dstMount->getShare()->getPermissions(); + } else { + $newOwner = $userFolder->getOwner()->getUID(); + $newPermissions = $share->getPermissions(); + } + $share->setShareOwner($newOwner); - $share->setPermissions($share->getPermissions() & $dstMount->getShare()->getPermissions()); - $shareManager->updateShare($share); + $share->setPermissions($newPermissions); + $shareManager->updateShare($share, onlyValid: false); } } @@ -100,10 +128,10 @@ class Updater { * @param string $newPath new path relative to data/user/files */ private static function renameChildren($oldPath, $newPath) { - $absNewPath = \OC\Files\Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $newPath); - $absOldPath = \OC\Files\Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $oldPath); + $absNewPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $newPath); + $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $oldPath); - $mountManager = \OC\Files\Filesystem::getMountManager(); + $mountManager = Filesystem::getMountManager(); $mountedShares = $mountManager->findIn('/' . \OC_User::getUser() . '/files/' . $oldPath); foreach ($mountedShares as $mount) { /** @var MountPoint $mount */ diff --git a/apps/files_sharing/lib/ViewOnly.php b/apps/files_sharing/lib/ViewOnly.php new file mode 100644 index 00000000000..e075677248a --- /dev/null +++ b/apps/files_sharing/lib/ViewOnly.php @@ -0,0 +1,99 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2019 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\Files_Sharing; + +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; + +/** + * Handles restricting for download of files + */ +class ViewOnly { + + public function __construct( + private Folder $userFolder, + ) { + } + + /** + * @param string[] $pathsToCheck + * @return bool + */ + public function check(array $pathsToCheck): bool { + // If any of elements cannot be downloaded, prevent whole download + foreach ($pathsToCheck as $file) { + try { + $info = $this->userFolder->get($file); + if ($info instanceof File) { + // access to filecache is expensive in the loop + if (!$this->checkFileInfo($info)) { + return false; + } + } elseif ($info instanceof Folder) { + // get directory content is rather cheap query + if (!$this->dirRecursiveCheck($info)) { + return false; + } + } + } catch (NotFoundException $e) { + continue; + } + } + return true; + } + + /** + * @param Folder $dirInfo + * @return bool + * @throws NotFoundException + */ + private function dirRecursiveCheck(Folder $dirInfo): bool { + if (!$this->checkFileInfo($dirInfo)) { + return false; + } + // If any of elements cannot be downloaded, prevent whole download + $files = $dirInfo->getDirectoryListing(); + foreach ($files as $file) { + if ($file instanceof File) { + if (!$this->checkFileInfo($file)) { + return false; + } + } elseif ($file instanceof Folder) { + return $this->dirRecursiveCheck($file); + } + } + + return true; + } + + /** + * @param Node $fileInfo + * @return bool + * @throws NotFoundException + */ + private function checkFileInfo(Node $fileInfo): bool { + // Restrict view-only to nodes which are shared + $storage = $fileInfo->getStorage(); + if (!$storage->instanceOfStorage(SharedStorage::class)) { + return true; + } + + // Extract extra permissions + /** @var SharedStorage $storage */ + $share = $storage->getShare(); + + // Check whether download-permission was denied (granted if not set) + $attributes = $share->getAttributes(); + $canDownload = $attributes?->getAttribute('permissions', 'download'); + + return $canDownload !== false; + } +} |