aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/lib')
-rw-r--r--apps/files_sharing/lib/Activity/Filter.php49
-rw-r--r--apps/files_sharing/lib/Activity/Providers/Base.php169
-rw-r--r--apps/files_sharing/lib/Activity/Providers/Downloads.php74
-rw-r--r--apps/files_sharing/lib/Activity/Providers/Groups.php118
-rw-r--r--apps/files_sharing/lib/Activity/Providers/PublicLinks.php62
-rw-r--r--apps/files_sharing/lib/Activity/Providers/RemoteShares.php88
-rw-r--r--apps/files_sharing/lib/Activity/Providers/Users.php87
-rw-r--r--apps/files_sharing/lib/Activity/Settings/PublicLinks.php45
-rw-r--r--apps/files_sharing/lib/Activity/Settings/PublicLinksUpload.php67
-rw-r--r--apps/files_sharing/lib/Activity/Settings/RemoteShare.php46
-rw-r--r--apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php30
-rw-r--r--apps/files_sharing/lib/Activity/Settings/Shared.php46
-rw-r--r--apps/files_sharing/lib/AppInfo/Application.php277
-rw-r--r--apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php50
-rw-r--r--apps/files_sharing/lib/Cache.php147
-rw-r--r--apps/files_sharing/lib/Capabilities.php171
-rw-r--r--apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php59
-rw-r--r--apps/files_sharing/lib/Command/CleanupRemoteStorages.php74
-rw-r--r--apps/files_sharing/lib/Command/DeleteOrphanShares.php79
-rw-r--r--apps/files_sharing/lib/Command/ExiprationNotification.php72
-rw-r--r--apps/files_sharing/lib/Command/FixShareOwners.php65
-rw-r--r--apps/files_sharing/lib/Command/ListShares.php161
-rw-r--r--apps/files_sharing/lib/Config/ConfigLexicon.php41
-rw-r--r--apps/files_sharing/lib/Controller/AcceptController.php61
-rw-r--r--apps/files_sharing/lib/Controller/DeletedShareAPIController.php240
-rw-r--r--apps/files_sharing/lib/Controller/ExternalSharesController.php116
-rw-r--r--apps/files_sharing/lib/Controller/PublicPreviewController.php164
-rw-r--r--apps/files_sharing/lib/Controller/RemoteController.php127
-rw-r--r--apps/files_sharing/lib/Controller/SettingsController.php45
-rw-r--r--apps/files_sharing/lib/Controller/ShareAPIController.php2269
-rw-r--r--apps/files_sharing/lib/Controller/ShareController.php739
-rw-r--r--apps/files_sharing/lib/Controller/ShareInfoController.php135
-rw-r--r--apps/files_sharing/lib/Controller/ShareesAPIController.php361
-rw-r--r--apps/files_sharing/lib/DefaultPublicShareTemplateProvider.php261
-rw-r--r--apps/files_sharing/lib/DeleteOrphanedSharesJob.php143
-rw-r--r--apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php49
-rw-r--r--apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php40
-rw-r--r--apps/files_sharing/lib/Event/ShareMountedEvent.php39
-rw-r--r--apps/files_sharing/lib/Exceptions/BrokenPath.php26
-rw-r--r--apps/files_sharing/lib/Exceptions/S2SException.php24
-rw-r--r--apps/files_sharing/lib/Exceptions/SharingRightsException.php19
-rw-r--r--apps/files_sharing/lib/ExpireSharesJob.php78
-rw-r--r--apps/files_sharing/lib/External/Cache.php47
-rw-r--r--apps/files_sharing/lib/External/Manager.php766
-rw-r--r--apps/files_sharing/lib/External/Mount.php54
-rw-r--r--apps/files_sharing/lib/External/MountProvider.php68
-rw-r--r--apps/files_sharing/lib/External/Scanner.php99
-rw-r--r--apps/files_sharing/lib/External/Storage.php360
-rw-r--r--apps/files_sharing/lib/External/Watcher.php23
-rw-r--r--apps/files_sharing/lib/Helper.php247
-rw-r--r--apps/files_sharing/lib/Hooks.php47
-rw-r--r--apps/files_sharing/lib/ISharedMountPoint.php13
-rw-r--r--apps/files_sharing/lib/ISharedStorage.php30
-rw-r--r--apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php48
-rw-r--r--apps/files_sharing/lib/Listener/BeforeNodeReadListener.php189
-rw-r--r--apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php59
-rw-r--r--apps/files_sharing/lib/Listener/LoadAdditionalListener.php35
-rw-r--r--apps/files_sharing/lib/Listener/LoadPublicFileRequestAuthListener.php61
-rw-r--r--apps/files_sharing/lib/Listener/LoadSidebarListener.php50
-rw-r--r--apps/files_sharing/lib/Listener/ShareInteractionListener.php71
-rw-r--r--apps/files_sharing/lib/Listener/UserAddedToGroupListener.php61
-rw-r--r--apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php60
-rw-r--r--apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php37
-rw-r--r--apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php32
-rw-r--r--apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php137
-rw-r--r--apps/files_sharing/lib/Migration/OwncloudGuestShareType.php48
-rw-r--r--apps/files_sharing/lib/Migration/SetAcceptedStatus.php56
-rw-r--r--apps/files_sharing/lib/Migration/SetPasswordColumn.php45
-rw-r--r--apps/files_sharing/lib/Migration/Version11300Date20201120141438.php124
-rw-r--r--apps/files_sharing/lib/Migration/Version21000Date20201223143245.php51
-rw-r--r--apps/files_sharing/lib/Migration/Version22000Date20210216084241.php37
-rw-r--r--apps/files_sharing/lib/Migration/Version24000Date20220208195521.php34
-rw-r--r--apps/files_sharing/lib/Migration/Version24000Date20220404142216.php39
-rw-r--r--apps/files_sharing/lib/Migration/Version31000Date20240821142813.php43
-rw-r--r--apps/files_sharing/lib/MountProvider.php223
-rw-r--r--apps/files_sharing/lib/Notification/Listener.php102
-rw-r--r--apps/files_sharing/lib/Notification/Notifier.php223
-rw-r--r--apps/files_sharing/lib/OrphanHelper.php94
-rw-r--r--apps/files_sharing/lib/ResponseDefinitions.php237
-rw-r--r--apps/files_sharing/lib/Scanner.php45
-rw-r--r--apps/files_sharing/lib/Settings/Personal.php51
-rw-r--r--apps/files_sharing/lib/ShareBackend/File.php154
-rw-r--r--apps/files_sharing/lib/ShareBackend/Folder.php129
-rw-r--r--apps/files_sharing/lib/SharedMount.php174
-rw-r--r--apps/files_sharing/lib/SharedStorage.php499
-rw-r--r--apps/files_sharing/lib/SharesReminderJob.php307
-rw-r--r--apps/files_sharing/lib/Updater.php128
-rw-r--r--apps/files_sharing/lib/ViewOnly.php99
88 files changed, 8586 insertions, 3963 deletions
diff --git a/apps/files_sharing/lib/Activity/Filter.php b/apps/files_sharing/lib/Activity/Filter.php
index ab1ad0c8145..4f3c4a7c914 100644
--- a/apps/files_sharing/lib/Activity/Filter.php
+++ b/apps/files_sharing/lib/Activity/Filter.php
@@ -1,46 +1,23 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity;
-
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class Filter implements IFilter {
- const TYPE_REMOTE_SHARE = 'remote_share';
- const TYPE_SHARED = 'shared';
-
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
+ public const TYPE_REMOTE_SHARE = 'remote_share';
+ public const TYPE_SHARED = 'shared';
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -83,7 +60,8 @@ class Filter implements IFilter {
public function filterTypes(array $types) {
return array_intersect([
self::TYPE_SHARED,
- self::TYPE_REMOTE_SHARE
+ self::TYPE_REMOTE_SHARE,
+ 'file_downloaded',
], $types);
}
@@ -92,6 +70,9 @@ class Filter implements IFilter {
* @since 11.0.0
*/
public function allowedApps() {
- return ['files_sharing'];
+ return [
+ 'files_sharing',
+ 'files_downloadactivity',
+ ];
}
}
diff --git a/apps/files_sharing/lib/Activity/Providers/Base.php b/apps/files_sharing/lib/Activity/Providers/Base.php
index d9d041f21f8..7428af382fc 100644
--- a/apps/files_sharing/lib/Activity/Providers/Base.php
+++ b/apps/files_sharing/lib/Activity/Providers/Base.php
@@ -1,68 +1,39 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 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;
use OCP\Activity\IProvider;
+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 array */
protected $displayNames = [];
- /**
- * @param IFactory $languageFactory
- * @param IURLGenerator $url
- * @param IManager $activityManager
- * @param IUserManager $userManager
- */
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager) {
- $this->languageFactory = $languageFactory;
- $this->url = $url;
- $this->activityManager = $activityManager;
- $this->userManager = $userManager;
+ public function __construct(
+ protected IFactory $languageFactory,
+ protected IURLGenerator $url,
+ protected IManager $activityManager,
+ protected IUserManager $userManager,
+ protected ICloudIdManager $cloudIdManager,
+ protected IContactsManager $contactsManager,
+ protected IEventMerger $eventMerger,
+ ) {
}
/**
@@ -70,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);
@@ -88,7 +59,7 @@ abstract class Base implements IProvider {
}
}
- return $this->parseLongVersion($event);
+ return $this->parseLongVersion($event, $previousEvent);
}
/**
@@ -101,31 +72,18 @@ abstract class Base implements IProvider {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- abstract protected function parseLongVersion(IEvent $event);
+ 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);
}
/**
@@ -134,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);
- } else if ($event !== null) {
- // Legacy from before ownCloud 8.2
+ $id = (string)key($parameter);
+ } elseif ($event !== null) {
$path = $parameter;
- $id = $event->getObjectId();
+ $id = (string)$event->getObjectId();
} else {
throw new \InvalidArgumentException('Could not generate file parameter');
}
@@ -157,30 +114,72 @@ 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) {
- if (!isset($this->displayNames[$uid])) {
- $this->displayNames[$uid] = $this->getDisplayName($uid);
+ protected function getUser(string $uid, string $overwriteDisplayName = '') {
+ // First try local user
+ $displayName = $this->userManager->getDisplayName($uid);
+ if ($displayName !== null) {
+ return [
+ 'type' => 'user',
+ 'id' => $uid,
+ 'name' => $displayName,
+ ];
+ }
+
+ // Then a contact from the addressbook
+ if ($this->cloudIdManager->isValidCloudId($uid)) {
+ $cloudId = $this->cloudIdManager->resolveCloudId($uid);
+ return [
+ 'type' => 'user',
+ 'id' => $cloudId->getUser(),
+ 'name' => (($overwriteDisplayName !== '') ? $overwriteDisplayName : $this->getDisplayNameFromAddressBook($cloudId->getDisplayId())),
+ 'server' => $cloudId->getRemote(),
+ ];
}
+ // Fallback to empty dummy data
return [
'type' => 'user',
'id' => $uid,
- 'name' => $this->displayNames[$uid],
+ 'name' => (($overwriteDisplayName !== '') ? $overwriteDisplayName : $uid),
];
}
- /**
- * @param string $uid
- * @return string
- */
- protected function getDisplayName($uid) {
- $user = $this->userManager->get($uid);
- if ($user instanceof IUser) {
- return $user->getDisplayName();
- } else {
- return $uid;
+ protected function getDisplayNameFromAddressBook(string $search): string {
+ if (isset($this->displayNames[$search])) {
+ return $this->displayNames[$search];
}
+
+ $addressBookContacts = $this->contactsManager->search($search, ['CLOUD'], [
+ 'limit' => 1,
+ 'enumeration' => false,
+ 'fullmatch' => false,
+ 'strict_search' => true,
+ ]);
+ foreach ($addressBookContacts as $contact) {
+ if (isset($contact['isLocalSystemBook'])) {
+ continue;
+ }
+
+ if (isset($contact['CLOUD'])) {
+ $cloudIds = $contact['CLOUD'];
+ if (is_string($cloudIds)) {
+ $cloudIds = [$cloudIds];
+ }
+
+ $lowerSearch = strtolower($search);
+ foreach ($cloudIds as $cloudId) {
+ if (strtolower($cloudId) === $lowerSearch) {
+ $this->displayNames[$search] = $contact['FN'] . " ($cloudId)";
+ return $this->displayNames[$search];
+ }
+ }
+ }
+ }
+
+ return $search;
}
}
diff --git a/apps/files_sharing/lib/Activity/Providers/Downloads.php b/apps/files_sharing/lib/Activity/Providers/Downloads.php
index 53c60356dd6..bddf2d30f73 100644
--- a/apps/files_sharing/lib/Activity/Providers/Downloads.php
+++ b/apps/files_sharing/lib/Activity/Providers/Downloads.php
@@ -1,38 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Providers;
use OCP\Activity\IEvent;
class Downloads extends Base {
+ public const SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED = 'public_shared_file_downloaded';
+ public const SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED = 'public_shared_folder_downloaded';
-
- const SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED = 'public_shared_file_downloaded';
- const SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED = 'public_shared_folder_downloaded';
-
- const SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED = 'file_shared_with_email_downloaded';
- const SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED = 'folder_shared_with_email_downloaded';
+ public const SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED = 'file_shared_with_email_downloaded';
+ public const SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED = 'folder_shared_with_email_downloaded';
/**
* @param IEvent $event
@@ -43,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');
- } else if ($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();
@@ -65,19 +46,28 @@ class Downloads extends Base {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event) {
+ 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) {
- $subject = $this->l->t('{file} downloaded via public link');
- } else if ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED ||
- $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_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);
+ } else {
+ $subject = $this->l->t('{file} downloaded via public link');
+ $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) {
$subject = $this->l->t('{email} downloaded {file}');
+ $this->setSubjects($event, $subject, $parsedParameters);
} else {
throw new \InvalidArgumentException();
}
@@ -87,7 +77,6 @@ class Downloads extends Base {
} else {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/download.svg')));
}
- $this->setSubjects($event, $subject, $parsedParameters);
return $event;
}
@@ -104,6 +93,17 @@ class Downloads extends Base {
switch ($subject) {
case self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED:
case self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED:
+ if (isset($parameters[1])) {
+ return [
+ 'file' => $this->getFile($parameters[0], $event),
+ 'remote-address-hash' => [
+ 'type' => 'highlight',
+ 'id' => $parameters[1],
+ 'name' => $parameters[1],
+ 'link' => '',
+ ],
+ ];
+ }
return [
'file' => $this->getFile($parameters[0], $event),
];
diff --git a/apps/files_sharing/lib/Activity/Providers/Groups.php b/apps/files_sharing/lib/Activity/Providers/Groups.php
index 53262e19311..d0086c05ced 100644
--- a/apps/files_sharing/lib/Activity/Providers/Groups.php
+++ b/apps/files_sharing/lib/Activity/Providers/Groups.php
@@ -1,36 +1,46 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Providers;
use OCP\Activity\IEvent;
+use OCP\Activity\IEventMerger;
+use OCP\Activity\IManager;
+use OCP\Contacts\IManager as IContactsManager;
+use OCP\Federation\ICloudIdManager;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use OCP\L10N\IFactory;
class Groups extends Base {
+ public const SUBJECT_SHARED_GROUP_SELF = 'shared_group_self';
+ public const SUBJECT_RESHARED_GROUP_BY = 'reshared_group_by';
+
+ public const SUBJECT_UNSHARED_GROUP_SELF = 'unshared_group_self';
+ public const SUBJECT_UNSHARED_GROUP_BY = 'unshared_group_by';
- const SUBJECT_SHARED_GROUP_SELF = 'shared_group_self';
- const SUBJECT_RESHARED_GROUP_BY = 'reshared_group_by';
- const SUBJECT_UNSHARED_GROUP_SELF = 'unshared_group_self';
- const SUBJECT_UNSHARED_GROUP_BY = 'unshared_group_by';
+ public const SUBJECT_EXPIRED_GROUP = 'expired_group';
+
+ /** @var string[] */
+ protected $groupDisplayNames = [];
+
+ 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);
+ }
/**
* @param IEvent $event
@@ -43,12 +53,14 @@ class Groups extends Base {
if ($event->getSubject() === self::SUBJECT_SHARED_GROUP_SELF) {
$subject = $this->l->t('Shared with group {group}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) {
$subject = $this->l->t('Removed share for group {group}');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) {
$subject = $this->l->t('{actor} shared with group {group}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_BY) {
$subject = $this->l->t('{actor} removed share for group {group}');
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED_GROUP) {
+ $subject = $this->l->t('Share for group {group} expired');
} else {
throw new \InvalidArgumentException();
}
@@ -65,21 +77,24 @@ class Groups extends Base {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) {
$parsedParameters = $this->getParsedParameters($event);
if ($event->getSubject() === self::SUBJECT_SHARED_GROUP_SELF) {
$subject = $this->l->t('You shared {file} with group {group}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) {
$subject = $this->l->t('You removed group {group} from {file}');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) {
$subject = $this->l->t('{actor} shared {file} with group {group}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_BY) {
$subject = $this->l->t('{actor} removed group {group} from {file}');
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED_GROUP) {
+ $subject = $this->l->t('Share for file {file} with group {group} expired');
} else {
throw new \InvalidArgumentException();
}
@@ -103,24 +118,45 @@ class Groups extends Base {
case self::SUBJECT_UNSHARED_GROUP_BY:
return [
'file' => $this->getFile($parameters[0], $event),
- 'group' => [
- 'type' => 'group',
- 'id' => $parameters[2],
- 'name' => $parameters[2],
- ],
+ 'group' => $this->generateGroupParameter($parameters[2]),
'actor' => $this->getUser($parameters[1]),
];
case self::SUBJECT_SHARED_GROUP_SELF:
case self::SUBJECT_UNSHARED_GROUP_SELF:
+ case self::SUBJECT_EXPIRED_GROUP:
return [
'file' => $this->getFile($parameters[0], $event),
- 'group' => [
- 'type' => 'group',
- 'id' => $parameters[1],
- 'name' => $parameters[1],
- ],
+ 'group' => $this->generateGroupParameter($parameters[1]),
];
}
return [];
}
+
+ /**
+ * @param string $gid
+ * @return array
+ */
+ protected function generateGroupParameter($gid) {
+ if (!isset($this->groupDisplayNames[$gid])) {
+ $this->groupDisplayNames[$gid] = $this->getGroupDisplayName($gid);
+ }
+
+ return [
+ 'type' => 'user-group',
+ 'id' => $gid,
+ 'name' => $this->groupDisplayNames[$gid],
+ ];
+ }
+
+ /**
+ * @param string $gid
+ * @return string
+ */
+ protected function getGroupDisplayName($gid) {
+ $group = $this->groupManager->get($gid);
+ if ($group instanceof IGroup) {
+ return $group->getDisplayName();
+ }
+ return $gid;
+ }
}
diff --git a/apps/files_sharing/lib/Activity/Providers/PublicLinks.php b/apps/files_sharing/lib/Activity/Providers/PublicLinks.php
index 5dab4179149..15ffaf2cdb0 100644
--- a/apps/files_sharing/lib/Activity/Providers/PublicLinks.php
+++ b/apps/files_sharing/lib/Activity/Providers/PublicLinks.php
@@ -1,38 +1,20 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Providers;
use OCP\Activity\IEvent;
class PublicLinks extends Base {
-
- const SUBJECT_SHARED_LINK_SELF = 'shared_link_self';
- const SUBJECT_RESHARED_LINK_BY = 'reshared_link_by';
- const SUBJECT_UNSHARED_LINK_SELF = 'unshared_link_self';
- const SUBJECT_UNSHARED_LINK_BY = 'unshared_link_by';
- const SUBJECT_LINK_EXPIRED = 'link_expired';
- const SUBJECT_LINK_BY_EXPIRED = 'link_by_expired';
+ public const SUBJECT_SHARED_LINK_SELF = 'shared_link_self';
+ public const SUBJECT_RESHARED_LINK_BY = 'reshared_link_by';
+ public const SUBJECT_UNSHARED_LINK_SELF = 'unshared_link_self';
+ public const SUBJECT_UNSHARED_LINK_BY = 'unshared_link_by';
+ public const SUBJECT_LINK_EXPIRED = 'link_expired';
+ public const SUBJECT_LINK_BY_EXPIRED = 'link_by_expired';
/**
* @param IEvent $event
@@ -45,17 +27,16 @@ class PublicLinks extends Base {
if ($event->getSubject() === self::SUBJECT_SHARED_LINK_SELF) {
$subject = $this->l->t('Shared as public link');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) {
$subject = $this->l->t('Removed public link');
- } else if ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) {
+ } elseif ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) {
$subject = $this->l->t('Public link expired');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) {
$subject = $this->l->t('{actor} shared as public link');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) {
$subject = $this->l->t('{actor} removed public link');
- } else if ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) {
+ } elseif ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) {
$subject = $this->l->t('Public link of {actor} expired');
-
} else {
throw new \InvalidArgumentException();
}
@@ -72,26 +53,26 @@ class PublicLinks extends Base {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) {
$parsedParameters = $this->getParsedParameters($event);
if ($event->getSubject() === self::SUBJECT_SHARED_LINK_SELF) {
$subject = $this->l->t('You shared {file} as public link');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) {
$subject = $this->l->t('You removed public link for {file}');
- } else if ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) {
+ } elseif ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) {
$subject = $this->l->t('Public link expired for {file}');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) {
$subject = $this->l->t('{actor} shared {file} as public link');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) {
$subject = $this->l->t('{actor} removed public link for {file}');
- } else if ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) {
+ } elseif ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) {
$subject = $this->l->t('Public link of {actor} for {file} expired');
-
} else {
throw new \InvalidArgumentException();
}
@@ -127,5 +108,4 @@ class PublicLinks extends Base {
}
return [];
}
-
}
diff --git a/apps/files_sharing/lib/Activity/Providers/RemoteShares.php b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php
index 4a5c6d7c8e6..750d0747b62 100644
--- a/apps/files_sharing/lib/Activity/Providers/RemoteShares.php
+++ b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php
@@ -1,60 +1,34 @@
<?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\Providers;
use OCP\Activity\IEvent;
+use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
+use OCP\Contacts\IManager as IContactsManager;
use OCP\Federation\ICloudIdManager;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
class RemoteShares extends Base {
+ public const SUBJECT_REMOTE_SHARE_ACCEPTED = 'remote_share_accepted';
+ public const SUBJECT_REMOTE_SHARE_DECLINED = 'remote_share_declined';
+ public const SUBJECT_REMOTE_SHARE_RECEIVED = 'remote_share_received';
+ public const SUBJECT_REMOTE_SHARE_UNSHARED = 'remote_share_unshared';
- protected $cloudIdManager;
-
- const SUBJECT_REMOTE_SHARE_ACCEPTED = 'remote_share_accepted';
- const SUBJECT_REMOTE_SHARE_DECLINED = 'remote_share_declined';
- const SUBJECT_REMOTE_SHARE_RECEIVED = 'remote_share_received';
- const SUBJECT_REMOTE_SHARE_UNSHARED = 'remote_share_unshared';
-
- /**
- * @param IFactory $languageFactory
- * @param IURLGenerator $url
- * @param IManager $activityManager
- * @param IUserManager $userManager
- * @param ICloudIdManager $cloudIdManager
- */
public function __construct(IFactory $languageFactory,
- IURLGenerator $url,
- IManager $activityManager,
- IUserManager $userManager,
- ICloudIdManager $cloudIdManager
- ) {
- parent::__construct($languageFactory, $url, $activityManager, $userManager);
- $this->cloudIdManager = $cloudIdManager;
+ IURLGenerator $url,
+ IManager $activityManager,
+ IUserManager $userManager,
+ ICloudIdManager $cloudIdManager,
+ IContactsManager $contactsManager,
+ IEventMerger $eventMerger) {
+ parent::__construct($languageFactory, $url, $activityManager, $userManager, $cloudIdManager, $contactsManager, $eventMerger);
}
/**
@@ -68,7 +42,7 @@ class RemoteShares extends Base {
if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_ACCEPTED) {
$subject = $this->l->t('{user} accepted the remote share');
- } else if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) {
+ } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) {
$subject = $this->l->t('{user} declined the remote share');
} else {
throw new \InvalidArgumentException();
@@ -86,20 +60,21 @@ class RemoteShares extends Base {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) {
$parsedParameters = $this->getParsedParameters($event);
if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_RECEIVED) {
$subject = $this->l->t('You received a new remote share {file} from {user}');
- } else if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_ACCEPTED) {
+ } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_ACCEPTED) {
$subject = $this->l->t('{user} accepted the remote share of {file}');
- } else if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) {
+ } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) {
$subject = $this->l->t('{user} declined the remote share of {file}');
- } else if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_UNSHARED) {
+ } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_UNSHARED) {
$subject = $this->l->t('{user} unshared {file} from you');
} else {
throw new \InvalidArgumentException();
@@ -122,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->getFederatedUser($parameters[0]),
+ 'user' => $this->getUser($parameters[0], $displayName)
];
case self::SUBJECT_REMOTE_SHARE_ACCEPTED:
case self::SUBJECT_REMOTE_SHARE_DECLINED:
@@ -138,23 +114,9 @@ class RemoteShares extends Base {
}
return [
'file' => $this->getFile($fileParameter),
- 'user' => $this->getFederatedUser($parameters[0]),
+ 'user' => $this->getUser($parameters[0]),
];
}
throw new \InvalidArgumentException();
}
-
- /**
- * @param string $cloudId
- * @return array
- */
- protected function getFederatedUser($cloudId) {
- $remoteUser = $this->cloudIdManager->resolveCloudId($cloudId);
- return [
- 'type' => 'user',
- 'id' => $remoteUser->getUser(),
- 'name' => $cloudId,// Todo display name from contacts
- 'server' => $remoteUser->getRemote(),
- ];
- }
}
diff --git a/apps/files_sharing/lib/Activity/Providers/Users.php b/apps/files_sharing/lib/Activity/Providers/Users.php
index b5322db4270..5c833ffae93 100644
--- a/apps/files_sharing/lib/Activity/Providers/Users.php
+++ b/apps/files_sharing/lib/Activity/Providers/Users.php
@@ -1,40 +1,26 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Providers;
use OCP\Activity\IEvent;
class Users extends Base {
+ public const SUBJECT_SHARED_USER_SELF = 'shared_user_self';
+ public const SUBJECT_RESHARED_USER_BY = 'reshared_user_by';
+ public const SUBJECT_UNSHARED_USER_SELF = 'unshared_user_self';
+ public const SUBJECT_UNSHARED_USER_BY = 'unshared_user_by';
+ public const SUBJECT_SHARED_WITH_BY = 'shared_with_by';
+ public const SUBJECT_UNSHARED_BY = 'unshared_by';
+ public const SUBJECT_SELF_UNSHARED = 'self_unshared';
+ public const SUBJECT_SELF_UNSHARED_BY = 'self_unshared_by';
- const SUBJECT_SHARED_USER_SELF = 'shared_user_self';
- const SUBJECT_RESHARED_USER_BY = 'reshared_user_by';
- const SUBJECT_UNSHARED_USER_SELF = 'unshared_user_self';
- const SUBJECT_UNSHARED_USER_BY = 'unshared_user_by';
-
- const SUBJECT_SHARED_WITH_BY = 'shared_with_by';
- const SUBJECT_UNSHARED_BY = 'unshared_by';
+ public const SUBJECT_EXPIRED_USER = 'expired_user';
+ public const SUBJECT_EXPIRED = 'expired';
/**
* @param IEvent $event
@@ -47,17 +33,24 @@ class Users extends Base {
if ($event->getSubject() === self::SUBJECT_SHARED_USER_SELF) {
$subject = $this->l->t('Shared with {user}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) {
$subject = $this->l->t('Removed share for {user}');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_USER_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_SELF_UNSHARED) {
+ $subject = $this->l->t('You removed yourself');
+ } elseif ($event->getSubject() === self::SUBJECT_SELF_UNSHARED_BY) {
+ $subject = $this->l->t('{actor} removed themselves');
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_USER_BY) {
$subject = $this->l->t('{actor} shared with {user}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) {
$subject = $this->l->t('{actor} removed share for {user}');
- } else if ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) {
$subject = $this->l->t('Shared by {actor}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_BY) {
$subject = $this->l->t('{actor} removed share');
-
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED_USER) {
+ $subject = $this->l->t('Share for {user} expired');
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED) {
+ $subject = $this->l->t('Share expired');
} else {
throw new \InvalidArgumentException();
}
@@ -74,26 +67,34 @@ class Users extends Base {
/**
* @param IEvent $event
+ * @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
- public function parseLongVersion(IEvent $event) {
+ public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) {
$parsedParameters = $this->getParsedParameters($event);
if ($event->getSubject() === self::SUBJECT_SHARED_USER_SELF) {
$subject = $this->l->t('You shared {file} with {user}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) {
$subject = $this->l->t('You removed {user} from {file}');
- } else if ($event->getSubject() === self::SUBJECT_RESHARED_USER_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_SELF_UNSHARED) {
+ $subject = $this->l->t('You removed yourself from {file}');
+ } elseif ($event->getSubject() === self::SUBJECT_SELF_UNSHARED_BY) {
+ $subject = $this->l->t('{actor} removed themselves from {file}');
+ } elseif ($event->getSubject() === self::SUBJECT_RESHARED_USER_BY) {
$subject = $this->l->t('{actor} shared {file} with {user}');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) {
$subject = $this->l->t('{actor} removed {user} from {file}');
- } else if ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) {
+ } elseif ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) {
$subject = $this->l->t('{actor} shared {file} with you');
- } else if ($event->getSubject() === self::SUBJECT_UNSHARED_BY) {
- $subject = $this->l->t('{actor} removed you from {file}');
-
+ } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_BY) {
+ $subject = $this->l->t('{actor} removed you from the share named {file}');
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED_USER) {
+ $subject = $this->l->t('Share for file {file} with {user} expired');
+ } elseif ($event->getSubject() === self::SUBJECT_EXPIRED) {
+ $subject = $this->l->t('Share for file {file} expired');
} else {
throw new \InvalidArgumentException();
}
@@ -115,12 +116,16 @@ class Users extends Base {
switch ($subject) {
case self::SUBJECT_SHARED_USER_SELF:
case self::SUBJECT_UNSHARED_USER_SELF:
+ case self::SUBJECT_EXPIRED_USER:
+ case self::SUBJECT_EXPIRED:
return [
'file' => $this->getFile($parameters[0], $event),
'user' => $this->getUser($parameters[1]),
];
case self::SUBJECT_SHARED_WITH_BY:
case self::SUBJECT_UNSHARED_BY:
+ case self::SUBJECT_SELF_UNSHARED:
+ case self::SUBJECT_SELF_UNSHARED_BY:
return [
'file' => $this->getFile($parameters[0], $event),
'actor' => $this->getUser($parameters[1]),
diff --git a/apps/files_sharing/lib/Activity/Settings/PublicLinks.php b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php
index 081081b9adf..0d3d00d2a7b 100644
--- a/apps/files_sharing/lib/Activity/Settings/PublicLinks.php
+++ b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php
@@ -1,44 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Settings;
-
-use OCP\Activity\ISetting;
-use OCP\IL10N;
-
-class PublicLinks implements ISetting {
-
- /** @var IL10N */
- protected $l;
-
- /**
- * @param IL10N $l
- */
- public function __construct(IL10N $l) {
- $this->l = $l;
- }
-
+class PublicLinks extends ShareActivitySettings {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
@@ -57,8 +25,8 @@ class PublicLinks implements ISetting {
/**
* @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() {
@@ -97,4 +65,3 @@ class PublicLinks implements ISetting {
return false;
}
}
-
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 a208ead7ba5..c04364bef20 100644
--- a/apps/files_sharing/lib/Activity/Settings/RemoteShare.php
+++ b/apps/files_sharing/lib/Activity/Settings/RemoteShare.php
@@ -1,45 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Settings;
-
-use OCP\Activity\ISetting;
-use OCP\IL10N;
-
-class RemoteShare implements ISetting {
-
- /** @var IL10N */
- protected $l;
-
- /**
- * @param IL10N $l
- */
- public function __construct(IL10N $l) {
- $this->l = $l;
- }
-
+class RemoteShare extends ShareActivitySettings {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
@@ -58,8 +25,8 @@ class RemoteShare implements ISetting {
/**
* @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() {
@@ -98,4 +65,3 @@ class RemoteShare implements ISetting {
return false;
}
}
-
diff --git a/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php b/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php
new file mode 100644
index 00000000000..4d8d8278433
--- /dev/null
+++ b/apps/files_sharing/lib/Activity/Settings/ShareActivitySettings.php
@@ -0,0 +1,30 @@
+<?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\Activity\Settings;
+
+use OCP\Activity\ActivitySettings;
+use OCP\IL10N;
+
+abstract class ShareActivitySettings extends ActivitySettings {
+ /**
+ * @param IL10N $l
+ */
+ public function __construct(
+ protected IL10N $l,
+ ) {
+ }
+
+ public function getGroupIdentifier() {
+ return 'sharing';
+ }
+
+ public function getGroupName() {
+ 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 b07872ded99..3717512eebd 100644
--- a/apps/files_sharing/lib/Activity/Settings/Shared.php
+++ b/apps/files_sharing/lib/Activity/Settings/Shared.php
@@ -1,45 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OCA\Files_Sharing\Activity\Settings;
-
-use OCP\Activity\ISetting;
-use OCP\IL10N;
-
-class Shared implements ISetting {
-
- /** @var IL10N */
- protected $l;
-
- /**
- * @param IL10N $l
- */
- public function __construct(IL10N $l) {
- $this->l = $l;
- }
-
+class Shared extends ShareActivitySettings {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
@@ -58,8 +25,8 @@ class Shared implements ISetting {
/**
* @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() {
@@ -98,4 +65,3 @@ class Shared implements ISetting {
return false;
}
}
-
diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php
index fe2669640ca..8ddb3afaf33 100644
--- a/apps/files_sharing/lib/AppInfo/Application.php
+++ b/apps/files_sharing/lib/AppInfo/Application.php
@@ -1,176 +1,157 @@
<?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 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>
- *
- * @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\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\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;
+use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
use OCA\Files_Sharing\Middleware\ShareInfoMiddleware;
+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_Sharing\ShareBackend\File;
+use OCA\Files_Sharing\ShareBackend\Folder;
use OCP\AppFramework\App;
-use OC\AppFramework\Utility\SimpleContainer;
-use OCA\Files_Sharing\Controller\ExternalSharesController;
-use OCA\Files_Sharing\Controller\ShareController;
-use OCA\Files_Sharing\Middleware\SharingCheckMiddleware;
-use OCP\Defaults;
+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\IContainer;
-use OCP\IServerContainer;
-
-class Application extends App {
- public function __construct(array $urlParams = array()) {
- parent::__construct('files_sharing', $urlParams);
+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\Share\Events\ShareCreatedEvent;
+use OCP\User\Events\UserChangedEvent;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\Util;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\EventDispatcher\GenericEvent as OldGenericEvent;
- $container = $this->getContainer();
- /** @var IServerContainer $server */
- $server = $container->getServer();
+class Application extends App implements IBootstrap {
+ public const APP_ID = 'files_sharing';
- /**
- * Controllers
- */
- $container->registerService('ShareController', function (SimpleContainer $c) use ($server) {
- $federatedSharingApp = new \OCA\FederatedFileSharing\AppInfo\Application();
- return new ShareController(
- $c->query('AppName'),
- $c->query('Request'),
- $server->getConfig(),
- $server->getURLGenerator(),
- $server->getUserManager(),
- $server->getLogger(),
- $server->getActivityManager(),
- $server->getShareManager(),
- $server->getSession(),
- $server->getPreviewManager(),
- $server->getRootFolder(),
- $federatedSharingApp->getFederatedShareProvider(),
- $server->getEventDispatcher(),
- $server->getL10N($c->query('AppName')),
- $server->query(Defaults::class)
- );
- });
- $container->registerService('ExternalSharesController', function (SimpleContainer $c) {
- return new ExternalSharesController(
- $c->query('AppName'),
- $c->query('Request'),
- $c->query('ExternalManager'),
- $c->query('HttpClientService')
- );
- });
+ public function __construct(array $urlParams = []) {
+ parent::__construct(self::APP_ID, $urlParams);
+ }
- /**
- * Core class wrappers
- */
- $container->registerService('HttpClientService', function (SimpleContainer $c) use ($server) {
- return $server->getHTTPClientService();
- });
- $container->registerService(ICloudIdManager::class, function (SimpleContainer $c) use ($server) {
- return $server->getCloudIdManager();
- });
- $container->registerService('ExternalManager', function (SimpleContainer $c) use ($server) {
- $user = $server->getUserSession()->getUser();
- $uid = $user ? $user->getUID() : null;
- return new \OCA\Files_Sharing\External\Manager(
- $server->getDatabaseConnection(),
- \OC\Files\Filesystem::getMountManager(),
- \OC\Files\Filesystem::getLoader(),
- $server->getHTTPClientService(),
- $server->getNotificationManager(),
- $server->query(\OCP\OCS\IDiscoveryService::class),
- $uid
+ public function register(IRegistrationContext $context): void {
+ $context->registerService(ExternalMountProvider::class, function (ContainerInterface $c) {
+ return new ExternalMountProvider(
+ $c->get(IDBConnection::class),
+ function () use ($c) {
+ return $c->get(Manager::class);
+ },
+ $c->get(ICloudIdManager::class)
);
});
- $container->registerAlias('OCA\Files_Sharing\External\Manager', 'ExternalManager');
/**
* Middleware
*/
- $container->registerService('SharingCheckMiddleware', function (SimpleContainer $c) use ($server) {
- return new SharingCheckMiddleware(
- $c->query('AppName'),
- $server->getConfig(),
- $server->getAppManager(),
- $c['ControllerMethodReflector'],
- $server->getShareManager(),
- $server->getRequest()
- );
- });
+ $context->registerMiddleWare(SharingCheckMiddleware::class);
+ $context->registerMiddleWare(OCSShareAPIMiddleware::class);
+ $context->registerMiddleWare(ShareInfoMiddleware::class);
- $container->registerService('OCSShareAPIMiddleware', function (SimpleContainer $c) use ($server) {
- return new OCSShareAPIMiddleware(
- $server->getShareManager(),
- $server->getL10N($c->query('AppName'))
- );
- });
+ $context->registerCapability(Capabilities::class);
- $container->registerService(ShareInfoMiddleware::class, function () use ($server) {
- return new ShareInfoMiddleware(
- $server->getShareManager()
- );
- });
+ $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);
- // Execute middlewares
- $container->registerMiddleWare('SharingCheckMiddleware');
- $container->registerMiddleWare('OCSShareAPIMiddleware');
- $container->registerMiddleWare(ShareInfoMiddleware::class);
-
- $container->registerService('MountProvider', function (IContainer $c) {
- /** @var \OCP\IServerContainer $server */
- $server = $c->query('ServerContainer');
- return new MountProvider(
- $server->getConfig(),
- $server->getShareManager(),
- $server->getLogger()
- );
- });
+ // 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);
- $container->registerService('ExternalMountProvider', function (IContainer $c) {
- /** @var \OCP\IServerContainer $server */
- $server = $c->query('ServerContainer');
- return new \OCA\Files_Sharing\External\MountProvider(
- $server->getDatabaseConnection(),
- function() use ($c) {
- return $c->query('ExternalManager');
- },
- $server->getCloudIdManager()
- );
- });
+ // Publish activity for public download
+ $context->registerEventListener(BeforeNodeReadEvent::class, BeforeNodeReadListener::class);
+ $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeNodeReadListener::class);
- /*
- * Register capabilities
- */
- $container->registerCapability('OCA\Files_Sharing\Capabilities');
+ // 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 registerMountProviders() {
- /** @var \OCP\IServerContainer $server */
- $server = $this->getContainer()->query('ServerContainer');
- $mountProviderCollection = $server->getMountProviderCollection();
- $mountProviderCollection->registerProvider($this->getContainer()->query('MountProvider'));
- $mountProviderCollection->registerProvider($this->getContainer()->query('ExternalMountProvider'));
+ public function boot(IBootContext $context): void {
+ $context->injectFn([$this, 'registerMountProviders']);
+ $context->injectFn([$this, 'registerEventsScripts']);
+
+ Helper::registerHooks();
+
+ Share::registerBackend('file', File::class);
+ Share::registerBackend('folder', Folder::class, 'file');
+ }
+
+
+ public function registerMountProviders(IMountProviderCollection $mountProviderCollection, MountProvider $mountProvider, ExternalMountProvider $externalMountProvider): void {
+ $mountProviderCollection->registerProvider($mountProvider);
+ $mountProviderCollection->registerProvider($externalMountProvider);
+ }
+
+ 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');
+ });
+
+ // notifications api to accept incoming user shares
+ $dispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event): void {
+ /** @var Listener $listener */
+ $listener = $this->getContainer()->query(Listener::class);
+ $listener->shareNotification($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);
+ });
}
}
diff --git a/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php b/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php
new file mode 100644
index 00000000000..ca4c82c03d7
--- /dev/null
+++ b/apps/files_sharing/lib/BackgroundJob/FederatedSharesDiscoverJob.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Sharing\BackgroundJob;
+
+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 {
+
+ 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) {
+ $qb = $this->connection->getQueryBuilder();
+
+ $qb->selectDistinct('remote')
+ ->from('share_external');
+
+ $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 352001ecbd4..f9042fc0765 100644
--- a/apps/files_sharing/lib/Cache.php
+++ b/apps/files_sharing/lib/Cache.php
@@ -1,37 +1,26 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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\Share\IShare;
/**
* Metadata cache for shared files
@@ -39,55 +28,52 @@ use OCP\Files\Cache\ICacheEntry;
* don't use this class directly if you need to get metadata, use \OC\Files\Filesystem::getFileInfo instead
*/
class Cache extends CacheJail {
- /**
- * @var \OCA\Files_Sharing\SharedStorage
- */
- private $storage;
-
- /**
- * @var ICacheEntry
- */
- private $sourceRootInfo;
-
- private $rootUnchanged = true;
-
- private $ownerDisplayName;
-
+ private bool $rootUnchanged = true;
+ private ?string $ownerDisplayName = null;
private $numericId;
+ private DisplayNameCache $displayNameCache;
/**
- * @param \OCA\Files_Sharing\SharedStorage $storage
- * @param ICacheEntry $sourceRootInfo
+ * @param SharedStorage $storage
*/
- public function __construct($storage, ICacheEntry $sourceRootInfo) {
- $this->storage = $storage;
- $this->sourceRootInfo = $sourceRootInfo;
- $this->numericId = $sourceRootInfo->getStorageId();
+ 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,
- null
+ '',
+ $dependencies,
);
}
protected function getRoot() {
- if (is_null($this->root)) {
+ if ($this->root === '') {
$absoluteRoot = $this->sourceRootInfo->getPath();
// 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;
}
- public function getCache() {
+ public function getGetUnjailedRoot(): string {
+ return $this->sourceRootInfo->getPath();
+ }
+
+ public function getCache(): ICache {
if (is_null($this->cache)) {
$sourceStorage = $this->storage->getSourceStorage();
if ($sourceStorage) {
@@ -104,7 +90,7 @@ class Cache extends CacheJail {
if (isset($this->numericId)) {
return $this->numericId;
} else {
- return false;
+ return -1;
}
}
@@ -130,25 +116,35 @@ 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);
}
protected function formatCacheEntry($entry, $path = null) {
if (is_null($path)) {
- $path = isset($entry['path']) ? $entry['path'] : '';
+ $path = $entry['path'] ?? '';
$entry['path'] = $this->getJailedPath($path);
} else {
$entry['path'] = $path;
}
- $sharePermissions = $this->storage->getPermissions($path);
- if (isset($entry['permissions'])) {
- $entry['permissions'] &= $sharePermissions;
- } else {
- $entry['permissions'] = $sharePermissions;
+
+ try {
+ if (isset($entry['permissions'])) {
+ $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;
@@ -158,7 +154,8 @@ class Cache extends CacheJail {
private function getOwnerDisplayName() {
if (!$this->ownerDisplayName) {
- $this->ownerDisplayName = \OC_User::getDisplayName($this->storage->getOwner(''));
+ $uid = $this->share->getShareOwner();
+ $this->ownerDisplayName = $this->displayNameCache->getDisplayName($uid) ?? $uid;
}
return $this->ownerDisplayName;
}
@@ -169,4 +166,34 @@ class Cache extends CacheJail {
public function clear() {
// Not a valid action for Shared Cache
}
+
+ public function getQueryFilterForStorage(): ISearchOperator {
+ $storageFilter = \OC\Files\Cache\Cache::getQueryFilterForStorage();
+
+ // Do the normal jail behavior for non files
+ if ($this->storage->getItemType() !== 'file') {
+ return $this->addJailFilterQuery($storageFilter);
+ }
+
+ // for single file shares we don't need to do the LIKE
+ return new SearchBinaryOperator(
+ ISearchBinaryOperator::OPERATOR_AND,
+ [
+ $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 af41add250c..06aa1271c8f 100644
--- a/apps/files_sharing/lib/Capabilities.php
+++ b/apps/files_sharing/lib/Capabilities.php
@@ -1,29 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <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-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\IConfig;
+use OCP\Constants;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\Share\IManager;
/**
* Class Capabilities
@@ -31,23 +21,83 @@ use \OCP\IConfig;
* @package OCA\Files_Sharing
*/
class Capabilities implements ICapability {
-
- /** @var IConfig */
- private $config;
-
- public function __construct(IConfig $config) {
- $this->config = $config;
+ 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 = [];
- if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') {
+ if (!$this->shareManager->shareApiEnabled()) {
$res['api_enabled'] = false;
$res['public'] = ['enabled' => false];
$res['user'] = ['send_mail' => false];
@@ -56,21 +106,43 @@ class Capabilities implements ICapability {
$res['api_enabled'] = true;
$public = [];
- $public['enabled'] = $this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes';
+ $public['enabled'] = $this->shareManager->shareApiAllowLinks();
if ($public['enabled']) {
$public['password'] = [];
- $public['password']['enforced'] = ($this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes');
+ $public['password']['enforced'] = $this->shareManager->shareApiLinkEnforcePassword();
+
+ if ($public['password']['enforced']) {
+ $public['password']['askForOptionalPassword'] = false;
+ } else {
+ $public['password']['askForOptionalPassword'] = $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_LINK_PASSWORD_DEFAULT);
+ }
$public['expire_date'] = [];
- $public['expire_date']['enabled'] = $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes';
+ $public['multiple_links'] = true;
+ $public['expire_date']['enabled'] = $this->shareManager->shareApiLinkDefaultExpireDate();
if ($public['expire_date']['enabled']) {
- $public['expire_date']['days'] = $this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
- $public['expire_date']['enforced'] = $this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes';
+ $public['expire_date']['days'] = $this->shareManager->shareApiLinkDefaultExpireDays();
+ $public['expire_date']['enforced'] = $this->shareManager->shareApiLinkDefaultExpireDateEnforced();
+ }
+
+ $public['expire_date_internal'] = [];
+ $public['expire_date_internal']['enabled'] = $this->shareManager->shareApiInternalDefaultExpireDate();
+ if ($public['expire_date_internal']['enabled']) {
+ $public['expire_date_internal']['days'] = $this->shareManager->shareApiInternalDefaultExpireDays();
+ $public['expire_date_internal']['enforced'] = $this->shareManager->shareApiInternalDefaultExpireDateEnforced();
+ }
+
+ $public['expire_date_remote'] = [];
+ $public['expire_date_remote']['enabled'] = $this->shareManager->shareApiRemoteDefaultExpireDate();
+ if ($public['expire_date_remote']['enabled']) {
+ $public['expire_date_remote']['days'] = $this->shareManager->shareApiRemoteDefaultExpireDays();
+ $public['expire_date_remote']['enforced'] = $this->shareManager->shareApiRemoteDefaultExpireDateEnforced();
}
$public['send_mail'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no') === 'yes';
- $public['upload'] = $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
+ $public['upload'] = $this->shareManager->shareApiLinkAllowPublicUpload();
$public['upload_files_drop'] = $public['upload'];
+ $public['custom_tokens'] = $this->shareManager->allowCustomTokens();
}
$res['public'] = $public;
@@ -81,18 +153,37 @@ class Capabilities implements ICapability {
// deprecated in favour of 'group', but we need to keep it for now
// in order to stay compatible with older clients
- $res['group_sharing'] = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
+ $res['group_sharing'] = $this->shareManager->allowGroupSharing();
$res['group'] = [];
- $res['group']['enabled'] = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
+ $res['group']['enabled'] = $this->shareManager->allowGroupSharing();
$res['group']['expire_date']['enabled'] = true;
+ $res['default_permissions'] = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
}
//Federated sharing
- $res['federation'] = [
- 'outgoing' => $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes',
- 'incoming' => $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'yes',
- 'expire_date' => ['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'] = [
+ 'query_lookup_default' => $this->config->getSystemValueBool('gs.enabled', false),
+ 'always_show_unique' => $this->config->getAppValue('files_sharing', 'always_show_unique', 'yes') === 'yes',
];
return [
diff --git a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
index db213398118..803dfd6325f 100644
--- a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
+++ b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php
@@ -1,31 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.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\Collaboration;
-
use OCP\Collaboration\AutoComplete\ISorter;
-use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IUserSession;
@@ -33,55 +14,49 @@ 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';
}
public function sort(array &$sortArray, array $context) {
// let's be tolerant. Comments uses "files" by default, other usages are often singular
- if($context['itemType'] !== 'files' && $context['itemType'] !== 'file') {
+ if ($context['itemType'] !== 'files' && $context['itemType'] !== 'file') {
return;
}
$user = $this->userSession->getUser();
- if($user === null) {
+ if ($user === null) {
return;
}
$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])) {
+ if (!isset($al[$type]) || !is_array($al[$type])) {
continue;
}
// at least on PHP 5.6 usort turned out to be not stable. So we add
// the current index to the value and compare it on a draw
$i = 0;
- $workArray = array_map(function($element) use (&$i) {
+ $workArray = array_map(function ($element) use (&$i) {
return [$i++, $element];
}, $byType);
usort($workArray, function ($a, $b) use ($al, $type) {
$result = $this->compare($a[1], $b[1], $al[$type]);
- if($result === 0) {
+ if ($result === 0) {
$result = $a[0] - $b[0];
}
return $result;
diff --git a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php
index f269b86ea9f..809481e5c0f 100644
--- a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php
+++ b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php
@@ -1,30 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- *
- * @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;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Federation\ICloudIdManager;
use OCP\IDBConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -37,13 +21,10 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class CleanupRemoteStorages extends Command {
- /**
- * @var IDBConnection
- */
- protected $connection;
-
- public function __construct(IDBConnection $connection) {
- $this->connection = $connection;
+ public function __construct(
+ protected IDBConnection $connection,
+ private ICloudIdManager $cloudIdManager,
+ ) {
parent::__construct();
}
@@ -59,8 +40,7 @@ class CleanupRemoteStorages extends Command {
);
}
- public function execute(InputInterface $input, OutputInterface $output) {
-
+ public function execute(InputInterface $input, OutputInterface $output): int {
$remoteStorages = $this->getRemoteStorages();
$output->writeln(count($remoteStorages) . ' remote storage(s) need(s) to be checked');
@@ -94,19 +74,21 @@ class CleanupRemoteStorages extends Command {
}
}
}
+ return 0;
}
public function countFiles($numericId, OutputInterface $output) {
$queryBuilder = $this->connection->getQueryBuilder();
- $queryBuilder->select($queryBuilder->createFunction('count(fileid)'))
+ $queryBuilder->select($queryBuilder->func()->count('fileid'))
->from('filecache')
->where($queryBuilder->expr()->eq(
'storage',
$queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR)
);
- $result = $queryBuilder->execute();
- $count = $result->fetchColumn();
+ $result = $queryBuilder->executeQuery();
+ $count = $result->fetchOne();
+ $result->closeCursor();
$output->writeln("$count files can be deleted for storage $numericId");
}
@@ -119,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);
}
@@ -133,12 +115,11 @@ 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");
}
public function getRemoteStorages() {
-
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->select(['id', 'numeric_id'])
->from('storages')
@@ -153,30 +134,35 @@ 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;
}
public function getRemoteShareIds() {
-
$queryBuilder = $this->connection->getQueryBuilder();
- $queryBuilder->select(['id', 'share_token', 'remote'])
+ $queryBuilder->select(['id', 'share_token', 'owner', 'remote'])
->from('share_external');
- $query = $queryBuilder->execute();
+ $result = $queryBuilder->executeQuery();
$remoteShareIds = [];
- while ($row = $query->fetch()) {
- $remoteShareIds[$row['id']] = 'shared::' . md5($row['share_token'] . '@' . $row['remote']);
+ 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
new file mode 100644
index 00000000000..b7ea5c5f14e
--- /dev/null
+++ b/apps/files_sharing/lib/Command/ExiprationNotification.php
@@ -0,0 +1,72 @@
+<?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\Command;
+
+use OCA\Files_Sharing\OrphanHelper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IDBConnection;
+use OCP\Notification\IManager as NotificationManager;
+use OCP\Share\IManager as ShareManager;
+use OCP\Share\IShare;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ExiprationNotification extends Command {
+ public function __construct(
+ private ITimeFactory $time,
+ private NotificationManager $notificationManager,
+ private IDBConnection $connection,
+ private ShareManager $shareManager,
+ private OrphanHelper $orphanHelper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('sharing:expiration-notification')
+ ->setDescription('Notify share initiators when a share will expire the next day.');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ //Current time
+ $minTime = $this->time->getDateTime();
+ $minTime->add(new \DateInterval('P1D'));
+ $minTime->setTime(0, 0, 0);
+
+ $maxTime = clone $minTime;
+ $maxTime->setTime(23, 59, 59);
+
+ $shares = $this->shareManager->getAllShares();
+
+ $now = $this->time->getDateTime();
+
+ /** @var IShare $share */
+ foreach ($shares as $share) {
+ if ($share->getExpirationDate() === null
+ || $share->getExpirationDate()->getTimestamp() < $minTime->getTimestamp()
+ || $share->getExpirationDate()->getTimestamp() > $maxTime->getTimestamp()
+ || !$this->orphanHelper->isShareValid($share->getSharedBy(), $share->getNodeId())) {
+ continue;
+ }
+
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp('files_sharing')
+ ->setDateTime($now)
+ ->setObject('share', $share->getFullId())
+ ->setSubject('expiresTomorrow');
+
+ // Only send to initiator for now
+ $notification->setUser($share->getSharedBy());
+ $this->notificationManager->notify($notification);
+ }
+ return 0;
+ }
+}
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
new file mode 100644
index 00000000000..721ddec7d2b
--- /dev/null
+++ b/apps/files_sharing/lib/Controller/AcceptController.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\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;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager as ShareManager;
+
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
+class AcceptController extends Controller {
+
+ public function __construct(
+ IRequest $request,
+ private ShareManager $shareManager,
+ private IUserSession $userSession,
+ private IURLGenerator $urlGenerator,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function accept(string $shareId): Response {
+ try {
+ $share = $this->shareManager->getShareById($shareId);
+ } catch (ShareNotFound $e) {
+ return new NotFoundResponse();
+ }
+
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return new NotFoundResponse();
+ }
+
+ try {
+ $share = $this->shareManager->acceptShare($share, $user->getUID());
+ } catch (\Exception $e) {
+ // Just ignore
+ }
+
+ $url = $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $share->getNode()->getId()]);
+
+ return new RedirectResponse($url);
+ }
+}
diff --git a/apps/files_sharing/lib/Controller/DeletedShareAPIController.php b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php
new file mode 100644
index 00000000000..2fa4d7c668f
--- /dev/null
+++ b/apps/files_sharing/lib/Controller/DeletedShareAPIController.php
@@ -0,0 +1,240 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\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 {
+
+ 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);
+ }
+
+ /**
+ * @suppress PhanUndeclaredClassMethod
+ *
+ * @return Files_SharingDeletedShare
+ */
+ private function formatShare(IShare $share): array {
+ $result = [
+ 'id' => $share->getFullId(),
+ 'share_type' => $share->getShareType(),
+ 'uid_owner' => $share->getSharedBy(),
+ 'displayname_owner' => $this->userManager->get($share->getSharedBy())->getDisplayName(),
+ 'permissions' => 0,
+ 'stime' => $share->getShareTime()->getTimestamp(),
+ 'parent' => null,
+ 'expiration' => null,
+ 'token' => null,
+ 'uid_file_owner' => $share->getShareOwner(),
+ 'displayname_file_owner' => $this->userManager->get($share->getShareOwner())->getDisplayName(),
+ 'path' => $share->getTarget(),
+ ];
+ $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
+ $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();
+ }
+ }
+
+ $result['path'] = $userFolder->getRelativePath($node->getPath());
+ if ($node instanceof Folder) {
+ $result['item_type'] = 'folder';
+ } else {
+ $result['item_type'] = 'file';
+ }
+ $result['mimetype'] = $node->getMimetype();
+ $result['storage_id'] = $node->getStorage()->getId();
+ $result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
+ $result['item_source'] = $node->getId();
+ $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) {
+ $result['expiration'] = $expiration->format('Y-m-d 00:00:00');
+ }
+
+ if ($share->getShareType() === IShare::TYPE_GROUP) {
+ $group = $this->groupManager->get($share->getSharedWith());
+ $result['share_with'] = $share->getSharedWith();
+ $result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
+ } 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) {
+ }
+ } 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) {
+ }
+ } 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;
+ }
+
+ /**
+ * 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, $teamShares, $roomShares, $deckShares, $sciencemeshShares);
+
+ $shares = array_values(array_map(function (IShare $share) {
+ return $this->formatShare($share);
+ }, $shares));
+
+ return new DataResponse($shares);
+ }
+
+ /**
+ * 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);
+ } catch (ShareNotFound $e) {
+ throw new OCSNotFoundException('Share not found');
+ }
+
+ if ($share->getPermissions() !== 0) {
+ throw new OCSNotFoundException('No deleted share found');
+ }
+
+ try {
+ $this->shareManager->restoreShare($share, $this->userId);
+ } catch (GenericShareException $e) {
+ throw new OCSException('Something went wrong');
+ }
+
+ return new DataResponse([]);
+ }
+
+ /**
+ * Returns the helper of DeletedShareAPIController for room shares.
+ *
+ * If the Talk application is not enabled or the helper is not available
+ * a QueryException is thrown instead.
+ *
+ * @return \OCA\Talk\Share\Helper\DeletedShareAPIController
+ * @throws QueryException
+ */
+ private function getRoomShareHelper() {
+ if (!$this->appManager->isEnabledForUser('spreed')) {
+ throw new QueryException();
+ }
+
+ return Server::get('\OCA\Talk\Share\Helper\DeletedShareAPIController');
+ }
+
+ /**
+ * 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 ShareAPIHelper
+ * @throws QueryException
+ */
+ private function getDeckShareHelper() {
+ if (!$this->appManager->isEnabledForUser('deck')) {
+ throw new QueryException();
+ }
+
+ 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 fe4c09dd195..fa828a9d2c2 100644
--- a/apps/files_sharing/lib/Controller/ExternalSharesController.php
+++ b/apps/files_sharing/lib/Controller/ExternalSharesController.php
@@ -1,36 +1,16 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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>
- *
- * @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\IRequest;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
-use OCP\Http\Client\IClientService;
-use OCP\AppFramework\Http\DataResponse;
+use OCP\IRequest;
/**
* Class ExternalSharesController
@@ -38,113 +18,45 @@ use OCP\AppFramework\Http\DataResponse;
* @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 (
- $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 0870995fc7b..d917f6e0ebb 100644
--- a/apps/files_sharing/lib/Controller/PublicPreviewController.php
+++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php
@@ -1,83 +1,100 @@
<?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 OCP\AppFramework\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;
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 {
-class PublicPreviewController extends Controller {
+ /** @var IShare */
+ private $share;
- /** @var ShareManager */
- private $shareManager;
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private ShareManager $shareManager,
+ ISession $session,
+ private IPreview $previewManager,
+ private IMimeIconProvider $mimeIconProvider,
+ ) {
+ parent::__construct($appName, $request, $session);
+ }
- /** @var IPreview */
- private $previewManager;
+ protected function getPasswordHash(): ?string {
+ return $this->share->getPassword();
+ }
- public function __construct($appName,
- IRequest $request,
- ShareManager $shareManger,
- IPreview $previewManager) {
- parent::__construct($appName, $request);
+ public function isValidToken(): bool {
+ try {
+ $this->share = $this->shareManager->getShareByToken($this->getToken());
+ return true;
+ } catch (ShareNotFound $e) {
+ return false;
+ }
+ }
- $this->shareManager = $shareManger;
- $this->previewManager = $previewManager;
+ protected function isPasswordProtected(): bool {
+ return $this->share->getPassword() !== null;
}
+
/**
- * @PublicPage
- * @NoCSRFRequired
+ * Get a preview for a shared file
+ *
+ * @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{}>
*
- * @param string $file
- * @param int $x
- * @param int $y
- * @param string $t
- * @param bool $a
- * @return DataResponse|FileDisplayResponse
+ * 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(
- $file = '',
- $x = 32,
- $y = 32,
- $t = '',
- $a = false
+ string $token,
+ string $file = '',
+ int $x = 32,
+ int $y = 32,
+ $a = false,
+ bool $mimeFallback = false,
) {
+ $cacheForSeconds = 60 * 60 * 24; // 1 day
- if ($t === '' || $x === 0 || $y === 0) {
+ if ($token === '' || $x === 0 || $y === 0) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try {
- $share = $this->shareManager->getShareByToken($t);
+ $share = $this->shareManager->getShareByToken($token);
} catch (ShareNotFound $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
@@ -86,6 +103,21 @@ class PublicPreviewController extends Controller {
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) {
@@ -95,8 +127,16 @@ class PublicPreviewController extends Controller {
}
$f = $this->previewManager->getPreview($file, $x, $y, !$a);
- return new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
+ $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
+ $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);
@@ -104,14 +144,22 @@ class PublicPreviewController extends Controller {
}
/**
- * @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);
@@ -134,6 +182,10 @@ class PublicPreviewController extends Controller {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
+ if (!$share->canSeeContent()) {
+ return new DataResponse([], Http::STATUS_FORBIDDEN);
+ }
+
try {
$node = $share->getNode();
if ($node instanceof Folder) {
@@ -142,7 +194,9 @@ class PublicPreviewController extends Controller {
}
$f = $this->previewManager->getPreview($node, -1, -1, false);
- return new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
+ $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
+ $response->cacheFor(3600 * 24);
+ return $response;
} catch (NotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (\InvalidArgumentException $e) {
diff --git a/apps/files_sharing/lib/Controller/RemoteController.php b/apps/files_sharing/lib/Controller/RemoteController.php
index d6206391180..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 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,9 +98,13 @@ 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) {
+ return $share;
+ }
+
$share['mimetype'] = $info->getMimetype();
$share['mtime'] = $info->getMTime();
$share['permissions'] = $info->getPermissions();
@@ -127,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);
@@ -161,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
new file mode 100644
index 00000000000..67d9193be78
--- /dev/null
+++ b/apps/files_sharing/lib/Controller/SettingsController.php
@@ -0,0 +1,45 @@
+<?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\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 {
+
+ public function __construct(
+ IRequest $request,
+ private IConfig $config,
+ private string $userId,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ #[NoAdminRequired]
+ public function setDefaultAccept(bool $accept): JSONResponse {
+ $this->config->setUserValue($this->userId, Application::APP_ID, 'default_accept', $accept ? 'yes' : 'no');
+ return new JSONResponse();
+ }
+
+ #[NoAdminRequired]
+ public function setUserShareFolder(string $shareFolder): JSONResponse {
+ $this->config->setUserValue($this->userId, Application::APP_ID, 'share_folder', $shareFolder);
+ return new JSONResponse();
+ }
+
+ #[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 10876e16568..095a8a75963 100644
--- a/apps/files_sharing/lib/Controller/ShareAPIController.php
+++ b/apps/files_sharing/lib/Controller/ShareAPIController.php
@@ -1,229 +1,385 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@nextcloud.com>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <pvince81@owncloud.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 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_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;
use OCP\AppFramework\OCS\OCSForbiddenException;
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\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\IUserManager;
+use OCP\IPreview;
use OCP\IRequest;
+use OCP\ITagManager;
use OCP\IURLGenerator;
-use OCP\Files\IRootFolder;
+use OCP\IUserManager;
+use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
-use OCP\Share\IManager;
-use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Mail\IMailer;
+use OCP\Server;
use OCP\Share\Exceptions\GenericShareException;
-use OCP\Lock\ILockingProvider;
+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 IRequest */
- protected $request;
- /** @var IRootFolder */
- private $rootFolder;
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var string */
- private $currentUser;
- /** @var IL10N */
- private $l;
- /** @var \OCP\Files\Node */
- private $lockedNode;
+ 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
*/
public function __construct(
- $appName,
+ string $appName,
IRequest $request,
- IManager $shareManager,
- IGroupManager $groupManager,
- IUserManager $userManager,
- IRootFolder $rootFolder,
- IURLGenerator $urlGenerator,
- $userId,
- IL10N $l10n
+ 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;
}
/**
* 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(\OCP\Share\IShare $share, Node $recipientNode = null) {
+ 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(),
'uid_owner' => $share->getSharedBy(),
'displayname_owner' => $sharedBy !== null ? $sharedBy->getDisplayName() : $share->getSharedBy(),
+ // recipient permissions
'permissions' => $share->getPermissions(),
+ // current user permissions on this share
+ 'can_edit' => $this->canEditShare($share),
+ 'can_delete' => $this->canDeleteShare($share),
'stime' => $share->getShareTime()->getTimestamp(),
'parent' => null,
'expiration' => null,
'token' => null,
'uid_file_owner' => $share->getShareOwner(),
+ 'note' => $share->getNote(),
+ 'label' => $share->getLabel(),
'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) {
+ 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';
}
+
+ // 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();
$result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
$result['item_source'] = $node->getId();
$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');
}
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER) {
+ if ($share->getShareType() === IShare::TYPE_USER) {
$sharedWith = $this->userManager->get($share->getSharedWith());
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith();
- } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) {
+ $result['share_with_displayname_unique'] = $sharedWith !== null ? (
+ !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID()
+ ) : $share->getSharedWith();
+
+ $userStatuses = $this->userStatusManager->getUserStatuses([$share->getSharedWith()]);
+ $userStatus = array_shift($userStatuses);
+ if ($userStatus) {
+ $result['status'] = [
+ 'status' => $userStatus->getStatus(),
+ 'message' => $userStatus->getMessage(),
+ 'icon' => $userStatus->getIcon(),
+ 'clearAt' => $userStatus->getClearAt()
+ ? (int)$userStatus->getClearAt()->format('U')
+ : null,
+ ];
+ }
+ } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
$group = $this->groupManager->get($share->getSharedWith());
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
- } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) {
+ } elseif ($share->getShareType() === IShare::TYPE_LINK) {
+ // "share_with" and "share_with_displayname" for passwords of link
+ // shares was deprecated in Nextcloud 15, use "password" instead.
$result['share_with'] = $share->getPassword();
- $result['share_with_displayname'] = $share->getPassword();
+ $result['share_with_displayname'] = '(' . $this->l->t('Shared link') . ')';
+
+ $result['password'] = $share->getPassword();
+
+ $result['send_password_by_talk'] = $share->getSendPasswordByTalk();
$result['token'] = $share->getToken();
$result['url'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $share->getToken()]);
-
- } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) {
+ } 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();
- } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) {
+ } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
$result['share_with'] = $share->getSharedWith();
$result['password'] = $share->getPassword();
+ $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();
- } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_CIRCLE) {
- $result['share_with_displayname'] = $share->getSharedWith();
- $result['share_with'] = explode(' ', $share->getSharedWith(), 2)[0];
+ } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
+ // getSharedWith() returns either "name (type, owner)" or
+ // "name (type, owner) [id]", depending on the Teams app version.
+ $hasCircleId = (substr($share->getSharedWith(), -1) === ']');
+
+ $result['share_with_displayname'] = $share->getSharedWithDisplayName();
+ if (empty($result['share_with_displayname'])) {
+ $displayNameLength = ($hasCircleId ? strrpos($share->getSharedWith(), ' ') : strlen($share->getSharedWith()));
+ $result['share_with_displayname'] = substr($share->getSharedWith(), 0, $displayNameLength);
+ }
+
+ $result['share_with_avatar'] = $share->getSharedWithAvatar();
+
+ $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
+ $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
+ if ($shareWithLength === false) {
+ $result['share_with'] = substr($share->getSharedWith(), $shareWithStart);
+ } else {
+ $result['share_with'] = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
+ }
+ } elseif ($share->getShareType() === IShare::TYPE_ROOM) {
+ $result['share_with'] = $share->getSharedWith();
+ $result['share_with_displayname'] = '';
+
+ try {
+ /** @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 {
+ /** @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) {
+ }
}
$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
* @return string
*/
- private function getDisplayNameFromAddressBook($query, $property) {
- // FIXME: If we inject the contacts manager it gets initialized bofore any address books are registered
- $result = \OC::$server->getContactsManager()->search($query, [$property]);
+ private function getDisplayNameFromAddressBook(string $query, string $property): string {
+ // FIXME: If we inject the contacts manager it gets initialized before any address books are registered
+ 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) {
+ foreach ($r[$property] as $value) {
+ if ($value === $query && $r['FN']) {
return $r['FN'];
}
}
@@ -232,65 +388,178 @@ 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($id) {
+ #[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'));
}
- if ($this->canAccessShare($share)) {
- try {
+ try {
+ if ($this->canAccessShare($share)) {
$share = $this->formatShare($share);
- return new DataResponse([$share]);
- } catch (NotFoundException $e) {
- //Fall trough
+
+ if ($include_tags) {
+ $share = $this->populateTags([$share]);
+ } else {
+ $share = [$share];
+ }
+
+ return new DataResponse($share);
}
+ } catch (NotFoundException $e) {
+ // 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
*/
- public function deleteShare($id) {
+ #[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 {
$this->lock($share->getNode());
} catch (LockedException $e) {
- throw new OCSNotFoundException($this->l->t('could not delete share'));
+ throw new OCSNotFoundException($this->l->t('Could not delete share'));
}
if (!$this->canAccessShare($share)) {
- throw new OCSNotFoundException($this->l->t('Could not delete share'));
+ throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
}
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP &&
- $share->getShareOwner() !== $this->currentUser &&
- $share->getSharedBy() !== $this->currentUser) {
- $this->shareManager->deleteFromSelf($share, $this->currentUser);
+ // if it's a group share or a room share
+ // we don't delete the share, but only the
+ // mount point. Allowing it to be restored
+ // from the deleted shares
+ if ($this->canDeleteShareFromSelf($share)) {
+ $this->shareManager->deleteFromSelf($share, $this->userId);
} else {
+ if (!$this->canDeleteShare($share)) {
+ throw new OCSForbiddenException($this->l->t('Could not delete share'));
+ }
+
$this->shareManager->deleteShare($share);
}
@@ -298,48 +567,75 @@ 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 $expireDate
+ * @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 OCSNotFoundException
- * @throws OCSForbiddenException
- * @throws OCSBadRequestException
+ * @return DataResponse<Http::STATUS_OK, Files_SharingShare, array{}>
+ * @throws OCSBadRequestException Unknown share type
* @throws OCSException
- *
+ * @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(
- $path = null,
- $permissions = \OCP\Constants::PERMISSION_ALL,
- $shareType = -1,
- $shareWith = null,
- $publicUpload = 'false',
- $password = '',
- $expireDate = ''
- ) {
+ ?string $path = null,
+ ?int $permissions = null,
+ int $shareType = -1,
+ ?string $shareWith = null,
+ ?string $publicUpload = null,
+ string $password = '',
+ ?string $sendPasswordByTalk = null,
+ ?string $expireDate = null,
+ string $note = '',
+ string $label = '',
+ ?string $attributes = null,
+ ?string $sendMail = null,
+ ): DataResponse {
+ assert($this->userId !== null);
+
$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 {
- $path = $userFolder->get($path);
+ /** @var \OC\Files\Node\Node $node */
+ $node = $userFolder->get($path);
} catch (NotFoundException $e) {
- throw new OCSNotFoundException($this->l->t('Wrong path, file/folder doesn\'t exist'));
+ throw new OCSNotFoundException($this->l->t('Wrong path, file/folder does not exist'));
+ }
+
+ // a user can have access to a file through different paths, with differing permissions
+ // combine all permissions to determine if the user can share this file
+ $nodes = $userFolder->getById($node->getId());
+ foreach ($nodes as $nodeById) {
+ /** @var FileInfo $fileInfo */
+ $fileInfo = $node->getFileInfo();
+ $fileInfo['permissions'] |= $nodeById->getPermissions();
}
- $share->setNode($path);
+ $share->setNode($node);
try {
$this->lock($share->getNode());
@@ -347,36 +643,80 @@ class ShareAPIController extends OCSController {
throw new OCSNotFoundException($this->l->t('Could not create share'));
}
- if ($permissions < 0 || $permissions > \OCP\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 |= \OCP\Constants::PERMISSION_READ;
-
- if ($path instanceof \OCP\Files\File) {
- // Single file shares should never have delete or create permissions
- $permissions &= ~\OCP\Constants::PERMISSION_DELETE;
- $permissions &= ~\OCP\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);
}
- /*
+ /**
* Hack for https://github.com/owncloud/core/issues/22587
* We check the permissions via webdav. But the permissions of the mount point
* do not equal the share permissions. Here we fix that for federated mounts.
*/
- if ($path->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
- $permissions &= ~($permissions & ~$path->getPermissions());
+ if ($node->getStorage()->instanceOfStorage(Storage::class)) {
+ $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);
+ }
}
- if ($shareType === \OCP\Share::SHARE_TYPE_USER) {
+ $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);
- } else if ($shareType === \OCP\Share::SHARE_TYPE_GROUP) {
+ } elseif ($shareType === IShare::TYPE_GROUP) {
if (!$this->shareManager->allowGroupSharing()) {
throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator'));
}
@@ -387,102 +727,122 @@ class ShareAPIController extends OCSController {
}
$share->setSharedWith($shareWith);
$share->setPermissions($permissions);
- } else if ($shareType === \OCP\Share::SHARE_TYPE_LINK) {
- //Can we even share links?
+ } elseif ($shareType === IShare::TYPE_LINK
+ || $shareType === IShare::TYPE_EMAIL) {
+
+ // Can we even share links?
if (!$this->shareManager->shareApiAllowLinks()) {
throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
}
- /*
- * For now we only allow 1 link share.
- * Return the existing link share if this is a duplicate
- */
- $existingShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $path, false, 1, 0);
- if (!empty($existingShares)) {
- return new DataResponse($this->formatShare($existingShares[0]));
- }
-
- 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 ($path instanceof \OCP\Files\File) {
- throw new OCSNotFoundException($this->l->t('Public upload is only possible for publicly shared folders'));
- }
-
- $share->setPermissions(
- \OCP\Constants::PERMISSION_READ |
- \OCP\Constants::PERMISSION_CREATE |
- \OCP\Constants::PERMISSION_UPDATE |
- \OCP\Constants::PERMISSION_DELETE
- );
- } else {
- $share->setPermissions(\OCP\Constants::PERMISSION_READ);
- }
+ $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
+ $share->setPermissions($permissions);
// Set password
if ($password !== '') {
$share->setPassword($password);
}
- //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'));
+ // 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 ($label !== '') {
+ if (strlen($label) > 255) {
+ throw new OCSBadRequestException('Maximum label length is 255');
}
+ $share->setLabel($label);
}
- } else if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) {
+ if ($sendPasswordByTalk === 'true') {
+ if (!$this->appManager->isEnabledForUser('spreed')) {
+ throw new OCSForbiddenException($this->l->t('Sharing %s sending the password by Nextcloud Talk failed because Nextcloud Talk is not enabled', [$node->getPath()]));
+ }
+
+ $share->setSendPasswordByTalk(true);
+ }
+ } elseif ($shareType === IShare::TYPE_REMOTE) {
if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
- throw new OCSForbiddenException($this->l->t('Sharing %s failed because the back end does not allow shares from type %s', [$path->getPath(), $shareType]));
+ 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 account ID'));
}
$share->setSharedWith($shareWith);
$share->setPermissions($permissions);
- } else if ($shareType === \OCP\Share::SHARE_TYPE_EMAIL) {
- if ($share->getNodeType() === 'file') {
- $share->setPermissions(\OCP\Constants::PERMISSION_READ);
- } else {
- $share->setPermissions(
- \OCP\Constants::PERMISSION_READ |
- \OCP\Constants::PERMISSION_CREATE |
- \OCP\Constants::PERMISSION_UPDATE |
- \OCP\Constants::PERMISSION_DELETE);
+ $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]));
}
+
+ if ($shareWith === null) {
+ throw new OCSNotFoundException($this->l->t('Please specify a valid federated group ID'));
+ }
+
$share->setSharedWith($shareWith);
- } else if ($shareType === \OCP\Share::SHARE_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'));
+ $share->setPermissions($permissions);
+ } elseif ($shareType === IShare::TYPE_CIRCLE) {
+ 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 (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 (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);
+ }
try {
$share = $this->shareManager->createShare($share);
- } catch (GenericShareException $e) {
+ } catch (HintException $e) {
$code = $e->getCode() === 0 ? 403 : $e->getCode();
throw new OCSException($e->getHint(), $code);
+ } catch (GenericShareException|\InvalidArgumentException $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ throw new OCSForbiddenException($e->getMessage(), $e);
} catch (\Exception $e) {
- throw new OCSForbiddenException($e->getMessage());
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ throw new OCSForbiddenException('Failed to create share.', $e);
}
$output = $this->formatShare($share);
@@ -491,24 +851,27 @@ class ShareAPIController extends OCSController {
}
/**
- * @param \OCP\Files\File|\OCP\Files\Folder $node
+ * @param null|Node $node
* @param boolean $includeTags
- * @return DataResponse
+ *
+ * @return list<Files_SharingShare>
*/
- private function getSharedWithMe($node = null, $includeTags) {
-
- $userShares = $this->shareManager->getSharedWith($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $node, -1, 0);
- $groupShares = $this->shareManager->getSharedWith($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $node, -1, 0);
- $circleShares = $this->shareManager->getSharedWith($this->currentUser, \OCP\Share::SHARE_TYPE_CIRCLE, $node, -1, 0);
-
- $shares = array_merge($userShares, $groupShares, $circleShares);
-
- $shares = array_filter($shares, function (IShare $share) {
- return $share->getShareOwner() !== $this->currentUser;
+ private function getSharedWithMe($node, bool $includeTags): array {
+ $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, $sciencemeshShares);
+
+ $filteredShares = array_filter($shares, function (IShare $share) {
+ return $share->getShareOwner() !== $this->userId;
});
$formatted = [];
- foreach ($shares as $share) {
+ foreach ($filteredShares as $share) {
if ($this->canAccessShare($share)) {
try {
$formatted[] = $this->formatShare($share);
@@ -519,339 +882,838 @@ class ShareAPIController extends OCSController {
}
if ($includeTags) {
- $formatted = Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager());
+ $formatted = $this->populateTags($formatted);
}
- return new DataResponse($formatted);
+ return $formatted;
}
/**
- * @param \OCP\Files\Folder $folder
- * @return DataResponse
+ * @param Node $folder
+ *
+ * @return list<Files_SharingShare>
* @throws OCSBadRequestException
+ * @throws NotFoundException
*/
- private function getSharesInDir($folder) {
- if (!($folder instanceof \OCP\Files\Folder)) {
+ private function getSharesInDir(Node $folder): array {
+ if (!($folder instanceof Folder)) {
throw new OCSBadRequestException($this->l->t('Not a directory'));
}
$nodes = $folder->getDirectoryListing();
- /** @var \OCP\Share\IShare[] $shares */
- $shares = [];
- foreach ($nodes as $node) {
- $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $node, false, -1, 0));
- $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $node, false, -1, 0));
- $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $node, false, -1, 0));
- if($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) {
- $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $node, false, -1, 0));
- }
- if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
- $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $node, false, -1, 0));
- }
- }
- $formatted = [];
+ /** @var IShare[] $shares */
+ $shares = array_reduce($nodes, function ($carry, $node) {
+ $carry = array_merge($carry, $this->getAllShares($node, true));
+ return $carry;
+ }, []);
+
+ // filter out duplicate shares
+ $known = [];
+
+ $formatted = $miniFormatted = [];
+ $resharingRight = false;
+ $known = [];
foreach ($shares as $share) {
+ if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
+ continue;
+ }
+
try {
- $formatted[] = $this->formatShare($share);
- } catch (NotFoundException $e) {
+ $format = $this->formatShare($share);
+
+ $known[] = $share->getId();
+ $formatted[] = $format;
+ if ($share->getSharedBy() === $this->userId) {
+ $miniFormatted[] = $format;
+ }
+ if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
+ $resharingRight = true;
+ }
+ } catch (\Exception $e) {
//Ignore this share
}
}
- return new DataResponse($formatted);
+ if (!$resharingRight) {
+ $formatted = $miniFormatted;
+ }
+
+ return $formatted;
}
/**
- * The getShares function.
- *
- * @NoAdminRequired
+ * Get shares of the current user
*
- * @param string $shared_with_me
- * @param string $reshares
- * @param string $subfiles
- * @param string $path
+ * @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
*
- * - 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=..)
+ * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
+ * @throws OCSNotFoundException The folder was not found or is inaccessible
*
- * @return DataResponse
- * @throws OCSNotFoundException
+ * 200: Shares returned
*/
+ #[NoAdminRequired]
public function getShares(
- $shared_with_me = 'false',
- $reshares = 'false',
- $subfiles = 'false',
- $path = null,
- $include_tags = 'false'
- ) {
-
- if ($path !== null) {
- $userFolder = $this->rootFolder->getUserFolder($this->currentUser);
+ string $shared_with_me = 'false',
+ string $reshares = 'false',
+ string $subfiles = 'false',
+ string $path = '',
+ string $include_tags = 'false',
+ ): DataResponse {
+ $node = null;
+ if ($path !== '') {
+ $userFolder = $this->rootFolder->getUserFolder($this->userId);
try {
- $path = $userFolder->get($path);
- $this->lock($path);
- } catch (\OCP\Files\NotFoundException $e) {
- throw new OCSNotFoundException($this->l->t('Wrong path, file/folder doesn\'t exist'));
+ $node = $userFolder->get($path);
+ $this->lock($node);
+ } 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'));
+ throw new OCSNotFoundException($this->l->t('Could not lock node'));
}
}
- if ($shared_with_me === 'true') {
- $result = $this->getSharedWithMe($path, $include_tags);
- return $result;
+ $shares = $this->getFormattedShares(
+ $this->userId,
+ $node,
+ ($shared_with_me === 'true'),
+ ($reshares === 'true'),
+ ($subfiles === 'true'),
+ ($include_tags === 'true')
+ );
+
+ 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 ($subfiles === 'true') {
- $result = $this->getSharesInDir($path);
- return $result;
+ 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;
}
- if ($reshares === 'true') {
- $reshares = true;
- } else {
- $reshares = false;
+ 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;
+ }
- // Get all shares
- $userShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $path, $reshares, -1, 0);
- $groupShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $path, $reshares, -1, 0);
- $linkShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $path, $reshares, -1, 0);
- if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) {
- $mailShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $path, $reshares, -1, 0);
- } else {
- $mailShares = [];
+ /**
+ * 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'));
}
- if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_CIRCLE)) {
- $circleShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_CIRCLE, $path, $reshares, -1, 0);
- } else {
- $circleShares = [];
+
+ // 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'));
}
- $shares = array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares);
+ // 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'));
+ }
- if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
- $federatedShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $path, $reshares, -1, 0);
- $shares = array_merge($shares, $federatedShares);
+ // 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'));
}
+ }
- $formatted = [];
+ /**
+ * @param string $viewer
+ * @param Node $node
+ * @param bool $sharedWithMe
+ * @param bool $reShares
+ * @param bool $subFiles
+ * @param bool $includeTags
+ *
+ * @return list<Files_SharingShare>
+ * @throws NotFoundException
+ * @throws OCSBadRequestException
+ */
+ private function getFormattedShares(
+ string $viewer,
+ $node = null,
+ bool $sharedWithMe = false,
+ bool $reShares = false,
+ bool $subFiles = false,
+ bool $includeTags = false,
+ ): array {
+ if ($sharedWithMe) {
+ return $this->getSharedWithMe($node, $includeTags);
+ }
+
+ if ($subFiles) {
+ return $this->getSharesInDir($node);
+ }
+
+ $shares = $this->getSharesFromNode($viewer, $node, $reShares);
+
+ $known = $formatted = $miniFormatted = [];
+ $resharingRight = false;
foreach ($shares as $share) {
try {
- $formatted[] = $this->formatShare($share, $path);
+ $share->getNode();
} catch (NotFoundException $e) {
- //Ignore share
+ /*
+ * Ignore shares where we can't get the node
+ * For example deleted shares
+ */
+ continue;
+ }
+
+ if (in_array($share->getId(), $known)
+ || ($share->getSharedWith() === $this->userId && $share->getShareType() === IShare::TYPE_USER)) {
+ continue;
+ }
+
+ $known[] = $share->getId();
+ try {
+ /** @var IShare $share */
+ $format = $this->formatShare($share, $node);
+ $formatted[] = $format;
+
+ // 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->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->userId, $share, $node)) {
+ $resharingRight = true;
+ }
+ } catch (InvalidPathException|NotFoundException $e) {
}
}
- if ($include_tags) {
- $formatted = Helper::populateTags($formatted, 'file_source', \OC::$server->getTagManager());
+ if (!$resharingRight) {
+ $formatted = $miniFormatted;
}
- return new DataResponse($formatted);
+ // fix eventual missing display name from federated shares
+ $formatted = $this->fixMissingDisplayName($formatted);
+
+ if ($includeTags) {
+ $formatted = $this->populateTags($formatted);
+ }
+
+ return $formatted;
}
+
/**
- * @NoAdminRequired
+ * Get all shares relative to a file, including parent folders shares rights
*
- * @param int $id
- * @param int $permissions
- * @param string $password
- * @param string $publicUpload
- * @param string $expireDate
- * @return DataResponse
- * @throws OCSNotFoundException
- * @throws OCSBadRequestException
- * @throws OCSForbiddenException
+ * @param string $path Path all shares will be relative to
+ *
+ * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}>
+ * @throws InvalidPathException
+ * @throws NotFoundException
+ * @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->userId);
+ try {
+ $node = $userFolder->get($path);
+ $this->lock($node);
+ } 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($this->l->t('no sharing rights on this item'));
+ }
+
+ // The current top parent we have access to
+ $parent = $node;
+
+ // initiate real owner.
+ $owner = $node->getOwner()
+ ->getUID();
+ if (!$this->userManager->userExists($owner)) {
+ return new DataResponse([]);
+ }
+
+ // get node based on the owner, fix owner in case of external storage
+ $userFolder = $this->rootFolder->getUserFolder($owner);
+ if ($node->getId() !== $userFolder->getId() && !$userFolder->isSubNode($node)) {
+ $owner = $node->getOwner()
+ ->getUID();
+ $userFolder = $this->rootFolder->getUserFolder($owner);
+ $node = $userFolder->getFirstNodeById($node->getId());
+ }
+ $basePath = $userFolder->getPath();
+
+ // generate node list for each parent folders
+ /** @var Node[] $nodes */
+ $nodes = [];
+ while (true) {
+ $node = $node->getParent();
+ if ($node->getPath() === $basePath) {
+ break;
+ }
+ $nodes[] = $node;
+ }
+
+ // The user that is requesting this list
+ $currentUserFolder = $this->rootFolder->getUserFolder($this->userId);
+
+ // for each nodes, retrieve shares.
+ $shares = [];
+
+ foreach ($nodes as $node) {
+ $getShares = $this->getFormattedShares($owner, $node, false, true);
+
+ $currentUserNode = $currentUserFolder->getFirstNodeById($node->getId());
+ if ($currentUserNode) {
+ $parent = $currentUserNode;
+ }
+
+ $subPath = $currentUserFolder->getRelativePath($parent->getPath());
+ foreach ($getShares as &$share) {
+ $share['via_fileid'] = $parent->getId();
+ $share['via_path'] = $subPath;
+ }
+ $this->mergeFormattedShares($shares, $getShares);
+ }
+
+ return new DataResponse(array_values($shares));
+ }
+
+ /**
+ * Check whether a set of permissions contains the permissions to check.
+ */
+ private function hasPermission(int $permissionsSet, int $permissionsToCheck): bool {
+ return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
+ }
+
+ /**
+ * Update a share
+ *
+ * @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(
- $id,
- $permissions = null,
- $password = null,
- $publicUpload = null,
- $expireDate = null
- ) {
+ 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,
+ ?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 ($permissions === null && $password === null && $publicUpload === null && $expireDate === null) {
- throw new OCSBadRequestException($this->l->t('Wrong or no update parameter given'));
+ if (!$this->canEditShare($share)) {
+ throw new OCSForbiddenException($this->l->t('You are not allowed to edit incoming shares'));
}
- /*
- * expirationdate, password and publicUpload only make sense for link shares
- */
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) {
-
- $newPermissions = null;
- if ($publicUpload === 'true') {
- $newPermissions = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
- } else if ($publicUpload === 'false') {
- $newPermissions = \OCP\Constants::PERMISSION_READ;
- }
+ if (
+ $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'));
+ }
- if ($permissions !== null) {
- $newPermissions = (int)$permissions;
- $newPermissions = $newPermissions & ~\OCP\Constants::PERMISSION_SHARE;
- }
-
- if ($newPermissions !== null &&
- !in_array($newPermissions, [
- \OCP\Constants::PERMISSION_READ,
- \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE, // legacy
- \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE, // correct
- \OCP\Constants::PERMISSION_CREATE, // hidden file list
- \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE, // allow to edit single files
- ])
- ) {
- throw new OCSBadRequestException($this->l->t('Can\'t change permissions for public share links'));
- }
-
- if (
- // legacy
- $newPermissions === (\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE) ||
- // correct
- $newPermissions === (\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE)
- ) {
- if (!$this->shareManager->shareApiLinkAllowPublicUpload()) {
- throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
- }
+ if ($note !== null) {
+ $share->setNote($note);
+ }
- if (!($share->getNode() instanceof \OCP\Files\Folder)) {
- throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
- }
+ if ($attributes !== null) {
+ $share = $this->setShareAttributes($share, $attributes);
+ }
- // normalize to correct public upload permissions
- $newPermissions = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE;
- }
+ // Handle mail send
+ if ($sendMail === 'true' || $sendMail === 'false') {
+ $share->setMailSend($sendMail === 'true');
+ }
- if ($newPermissions !== null) {
- $share->setPermissions($newPermissions);
- $permissions = $newPermissions;
+ /**
+ * expiration date, password and publicUpload only make sense for link shares
+ */
+ if ($share->getShareType() === IShare::TYPE_LINK
+ || $share->getShareType() === IShare::TYPE_EMAIL) {
+
+ // Update hide download state
+ if ($hideDownload === 'true') {
+ $share->setHideDownload(true);
+ } elseif ($hideDownload === 'false') {
+ $share->setHideDownload(false);
}
- if ($expireDate === '') {
- $share->setExpirationDate(null);
- } else if ($expireDate !== null) {
- try {
- $expireDate = $this->parseDate($expireDate);
- } catch (\Exception $e) {
- throw new OCSBadRequestException($e->getMessage());
- }
- $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 === '') {
$share->setPassword(null);
- } else if ($password !== null) {
+ } elseif ($password !== null) {
$share->setPassword($password);
}
- } else {
- if ($permissions !== null) {
- $permissions = (int)$permissions;
- $share->setPermissions($permissions);
+ if ($label !== null) {
+ if (strlen($label) > 255) {
+ throw new OCSBadRequestException('Maximum label length is 255');
+ }
+ $share->setLabel($label);
}
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) {
- if ($password === '') {
- $share->setPassword(null);
- } else if ($password !== null) {
- $share->setPassword($password);
+ if ($sendPasswordByTalk === 'true') {
+ if (!$this->appManager->isEnabledForUser('spreed')) {
+ throw new OCSForbiddenException($this->l->t('"Sending the password by Nextcloud Talk" for sharing a file or folder failed because Nextcloud Talk is not enabled.'));
}
+
+ $share->setSendPasswordByTalk(true);
+ } elseif ($sendPasswordByTalk !== null) {
+ $share->setSendPasswordByTalk(false);
}
- if ($expireDate === '') {
- $share->setExpirationDate(null);
- } else if ($expireDate !== null) {
- try {
- $expireDate = $this->parseDate($expireDate);
- } catch (\Exception $e) {
- throw new OCSBadRequestException($e->getMessage());
+ 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->setExpirationDate($expireDate);
+ $share->setToken($token);
}
+ }
+ // NOT A LINK SHARE
+ else {
+ if ($permissions !== null) {
+ $share->setPermissions($permissions);
+ }
+ }
+
+ 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);
+ }
}
- if ($permissions !== null && $share->getShareOwner() !== $this->currentUser) {
- /* Check if this is an incomming share */
- $incomingShares = $this->shareManager->getSharedWith($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $share->getNode(), -1, 0);
- $incomingShares = array_merge($incomingShares, $this->shareManager->getSharedWith($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $share->getNode(), -1, 0));
+ try {
+ $this->checkInheritedAttributes($share);
+ $share = $this->shareManager->updateShare($share);
+ } catch (HintException $e) {
+ $code = $e->getCode() === 0 ? 403 : $e->getCode();
+ throw new OCSException($e->getHint(), (int)$code);
+ } catch (\Exception $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;
+ }
+
+ /**
+ * 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 = [];
+
+ $shareTypes = [
+ IShare::TYPE_USER,
+ IShare::TYPE_GROUP
+ ];
- /** @var \OCP\Share\IShare[] $incomingShares */
- if (!empty($incomingShares)) {
- $maxPermissions = 0;
- foreach ($incomingShares as $incomingShare) {
- $maxPermissions |= $incomingShare->getPermissions();
+ foreach ($shareTypes as $shareType) {
+ $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) {
+ $pendingShares[] = $share;
}
+ }
+ }
- if ($share->getPermissions() & ~$maxPermissions) {
- throw new OCSNotFoundException($this->l->t('Cannot increase permissions'));
+ $result = array_values(array_filter(array_map(function (IShare $share) {
+ $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
+ $node = $userFolder->getFirstNodeById($share->getNodeId());
+ if (!$node) {
+ // fallback to guessing the path
+ $node = $userFolder->get($share->getTarget());
+ if ($node === null || $share->getTarget() === '') {
+ return null;
}
}
+
+ try {
+ $formattedShare = $this->formatShare($share, $node);
+ $formattedShare['path'] = '/' . $share->getNode()->getName();
+ $formattedShare['permissions'] = 0;
+ return $formattedShare;
+ } catch (NotFoundException $e) {
+ return null;
+ }
+ }, $pendingShares), function ($entry) {
+ return $entry !== null;
+ }));
+
+ return new DataResponse($result);
+ }
+
+ /**
+ * Accept a share
+ *
+ * @param string $id ID of the share
+ * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
+ * @throws OCSNotFoundException Share not found
+ * @throws OCSException
+ * @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 does not exist'));
}
+ if (!$this->canAccessShare($share)) {
+ throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist'));
+ }
try {
- $share = $this->shareManager->updateShare($share);
+ $this->shareManager->acceptShare($share, $this->userId);
+ } catch (HintException $e) {
+ $code = $e->getCode() === 0 ? 403 : $e->getCode();
+ throw new OCSException($e->getHint(), (int)$code);
} catch (\Exception $e) {
- throw new OCSBadRequestException($e->getMessage());
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ throw new OCSBadRequestException('Failed to accept share.', $e);
}
- return new DataResponse($this->formatShare($share));
+ return new DataResponse();
}
/**
- * @param \OCP\Share\IShare $share
- * @return bool
+ * Does the user have read permission on the share
+ *
+ * @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, $checkGroups = true) {
+ 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 (or a group you are a member of)
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER &&
- $share->getSharedWith() === $this->currentUser
- ) {
+ // If the share is shared with you, you can access it!
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedWith() === $this->userId) {
return true;
}
- if ($checkGroups && $share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP) {
+ // Have reshare rights on the shared file/folder ?
+ // Does the currentUser have access to the shared file?
+ $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;
}
}
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_CIRCLE) {
+ if ($share->getShareType() === IShare::TYPE_CIRCLE) {
// TODO: have a sanity check like above?
return true;
}
+ if ($share->getShareType() === IShare::TYPE_ROOM) {
+ try {
+ 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->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;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Does the user have edit permission on the share
+ *
+ * @param IShare $share the share to check
+ * @return boolean
+ */
+ 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;
+ }
+
+ // The owner of the file and the creator of the share
+ // can always edit the share
+ 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!
+
+ return false;
+ }
+
+ /**
+ * Does the user have delete permission on the share
+ *
+ * @param IShare $share the share to check
+ * @return boolean
+ */
+ 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;
+ }
+
+ // if the user is the recipient, i can unshare
+ // the share with self
+ 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->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;
+ }
+
+ /**
+ * Does the user have delete permission on the share
+ * This differs from the canDeleteShare function as it only
+ * remove the share for the current user. It does NOT
+ * completely delete the share but only the mount point.
+ * It can then be restored from the deleted shares section.
+ *
+ * @param IShare $share the share to check
+ * @return boolean
+ *
+ * @suppress PhanUndeclaredClassMethod
+ */
+ 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->userId
+ || $share->getSharedBy() === $this->userId
+ ) {
+ // Delete the whole share, not just for self
+ return false;
+ }
+
+ // 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->userId);
+ if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
+ return true;
+ }
+ }
+
+ if ($share->getShareType() === IShare::TYPE_ROOM) {
+ try {
+ 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->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;
+ }
+ }
+
return false;
}
@@ -865,19 +1727,15 @@ class ShareAPIController extends OCSController {
* @throws \Exception
* @return \DateTime
*/
- private function parseDate($expireDate) {
+ 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');
- }
-
- if ($date === false) {
- 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;
}
@@ -886,15 +1744,15 @@ 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($id) {
+ private function getShareById(string $id): IShare {
$share = null;
// First check if it is an internal share.
try {
- $share = $this->shareManager->getShareById('ocinternal:' . $id);
+ $share = $this->shareManager->getShareById('ocinternal:' . $id, $this->userId);
return $share;
} catch (ShareNotFound $e) {
// Do nothing, just try the other share type
@@ -902,8 +1760,33 @@ class ShareAPIController extends OCSController {
try {
- if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_CIRCLE)) {
- $share = $this->shareManager->getShareById('ocCircleShare:' . $id);
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_CIRCLE)) {
+ $share = $this->shareManager->getShareById('ocCircleShare:' . $id, $this->userId);
+ return $share;
+ }
+ } catch (ShareNotFound $e) {
+ // Do nothing, just try the other share type
+ }
+
+ try {
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
+ $share = $this->shareManager->getShareById('ocMailShare:' . $id, $this->userId);
+ return $share;
+ }
+ } catch (ShareNotFound $e) {
+ // Do nothing, just try the other share type
+ }
+
+ try {
+ $share = $this->shareManager->getShareById('ocRoomShare:' . $id, $this->userId);
+ return $share;
+ } catch (ShareNotFound $e) {
+ // Do nothing, just try the other share type
+ }
+
+ try {
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) {
+ $share = $this->shareManager->getShareById('deck:' . $id, $this->userId);
return $share;
}
} catch (ShareNotFound $e) {
@@ -911,8 +1794,8 @@ class ShareAPIController extends OCSController {
}
try {
- if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) {
- $share = $this->shareManager->getShareById('ocMailShare:' . $id);
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) {
+ $share = $this->shareManager->getShareById('sciencemesh:' . $id, $this->userId);
return $share;
}
} catch (ShareNotFound $e) {
@@ -922,7 +1805,7 @@ class ShareAPIController extends OCSController {
if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) {
throw new ShareNotFound();
}
- $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id);
+ $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id, $this->userId);
return $share;
}
@@ -930,19 +1813,483 @@ 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;
}
/**
* Cleanup the remaining locks
+ * @throws LockedException
*/
public function cleanup() {
if ($this->lockedNode !== null) {
$this->lockedNode->unlock(ILockingProvider::LOCK_SHARED);
}
}
+
+ /**
+ * Returns the helper of ShareAPIController for room shares.
+ *
+ * If the Talk application is not enabled or the helper is not available
+ * a ContainerExceptionInterface is thrown instead.
+ *
+ * @return \OCA\Talk\Share\Helper\ShareAPIController
+ * @throws ContainerExceptionInterface
+ */
+ private function getRoomShareHelper() {
+ if (!$this->appManager->isEnabledForUser('spreed')) {
+ throw new QueryException();
+ }
+
+ return $this->serverContainer->get('\OCA\Talk\Share\Helper\ShareAPIController');
+ }
+
+ /**
+ * Returns the helper of ShareAPIHelper for deck shares.
+ *
+ * If the Deck application is not enabled or the helper is not available
+ * a ContainerExceptionInterface is thrown instead.
+ *
+ * @return ShareAPIHelper
+ * @throws ContainerExceptionInterface
+ */
+ private function getDeckShareHelper() {
+ if (!$this->appManager->isEnabledForUser('deck')) {
+ throw new QueryException();
+ }
+
+ return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper');
+ }
+
+ /**
+ * 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
+ *
+ * @return IShare[]
+ */
+ private function getSharesFromNode(string $viewer, $node, bool $reShares): array {
+ $providers = [
+ IShare::TYPE_USER,
+ IShare::TYPE_GROUP,
+ IShare::TYPE_LINK,
+ IShare::TYPE_EMAIL,
+ IShare::TYPE_CIRCLE,
+ IShare::TYPE_ROOM,
+ IShare::TYPE_DECK,
+ IShare::TYPE_SCIENCEMESH
+ ];
+
+ // Should we assume that the (currentUser) viewer is the owner of the node !?
+ $shares = [];
+ foreach ($providers as $provider) {
+ if (!$this->shareManager->shareProviderExists($provider)) {
+ continue;
+ }
+
+ $providerShares
+ = $this->shareManager->getSharesBy($viewer, $provider, $node, $reShares, -1, 0);
+ $shares = array_merge($shares, $providerShares);
+ }
+
+ if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
+ $federatedShares = $this->shareManager->getSharesBy(
+ $this->userId, IShare::TYPE_REMOTE, $node, $reShares, -1, 0
+ );
+ $shares = array_merge($shares, $federatedShares);
+ }
+
+ if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
+ $federatedShares = $this->shareManager->getSharesBy(
+ $this->userId, IShare::TYPE_REMOTE_GROUP, $node, $reShares, -1, 0
+ );
+ $shares = array_merge($shares, $federatedShares);
+ }
+
+ return $shares;
+ }
+
+
+ /**
+ * @param Node $node
+ *
+ * @throws SharingRightsException
+ */
+ private function confirmSharingRights(Node $node): void {
+ if (!$this->hasResharingRights($this->userId, $node)) {
+ throw new SharingRightsException($this->l->t('No sharing rights on this item'));
+ }
+ }
+
+
+ /**
+ * @param string $viewer
+ * @param Node $node
+ *
+ * @return bool
+ */
+ private function hasResharingRights($viewer, $node): bool {
+ if ($viewer === $node->getOwner()->getUID()) {
+ return true;
+ }
+
+ foreach ([$node, $node->getParent()] as $node) {
+ $shares = $this->getSharesFromNode($viewer, $node, true);
+ foreach ($shares as $share) {
+ try {
+ if ($this->shareProviderResharingRights($viewer, $share, $node)) {
+ return true;
+ }
+ } catch (InvalidPathException|NotFoundException $e) {
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Returns if we can find resharing rights in an IShare object for a specific user.
+ *
+ * @suppress PhanUndeclaredClassMethod
+ *
+ * @param string $userId
+ * @param IShare $share
+ * @param Node $node
+ *
+ * @return bool
+ * @throws NotFoundException
+ * @throws InvalidPathException
+ */
+ private function shareProviderResharingRights(string $userId, IShare $share, $node): bool {
+ if ($share->getShareOwner() === $userId) {
+ return true;
+ }
+
+ // we check that current user have parent resharing rights on the current file
+ if ($node !== null && ($node->getPermissions() & Constants::PERMISSION_SHARE) !== 0) {
+ return true;
+ }
+
+ if ((Constants::PERMISSION_SHARE & $share->getPermissions()) === 0) {
+ return false;
+ }
+
+ if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() === $userId) {
+ return true;
+ }
+
+ if ($share->getShareType() === IShare::TYPE_GROUP && $this->groupManager->isInGroup($userId, $share->getSharedWith())) {
+ return true;
+ }
+
+ if ($share->getShareType() === IShare::TYPE_CIRCLE && Server::get(IAppManager::class)->isEnabledForUser('circles')
+ && class_exists('\OCA\Circles\Api\v1\Circles')) {
+ $hasCircleId = (str_ends_with($share->getSharedWith(), ']'));
+ $shareWithStart = ($hasCircleId ? strrpos($share->getSharedWith(), '[') + 1 : 0);
+ $shareWithLength = ($hasCircleId ? -1 : strpos($share->getSharedWith(), ' '));
+ if ($shareWithLength === false) {
+ $sharedWith = substr($share->getSharedWith(), $shareWithStart);
+ } else {
+ $sharedWith = substr($share->getSharedWith(), $shareWithStart, $shareWithLength);
+ }
+ try {
+ $member = Circles::getMember($sharedWith, $userId, 1);
+ if ($member->getLevel() >= 4) {
+ return true;
+ }
+ return false;
+ } catch (ContainerExceptionInterface $e) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get all the shares for the current user
+ *
+ * @param Node|null $path
+ * @param boolean $reshares
+ * @return IShare[]
+ */
+ private function getAllShares(?Node $path = null, bool $reshares = false) {
+ // Get all shares
+ $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->userId, IShare::TYPE_EMAIL, $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->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0);
+
+ // DECK SHARES
+ $deckShares = $this->shareManager->getSharesBy($this->userId, 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->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0);
+ } else {
+ $federatedShares = [];
+ }
+ if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
+ $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, $sciencemeshShares, $federatedShares, $federatedGroupShares);
+ }
+
+
+ /**
+ * merging already formatted shares.
+ * We'll make an associative array to easily detect duplicate Ids.
+ * Keys _needs_ to be removed after all shares are retrieved and merged.
+ *
+ * @param array $shares
+ * @param array $newShares
+ */
+ private function mergeFormattedShares(array &$shares, array $newShares) {
+ foreach ($newShares as $newShare) {
+ if (!array_key_exists($newShare['id'], $shares)) {
+ $shares[$newShare['id']] = $newShare;
+ }
+ }
+ }
+
+ /**
+ * @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 c51bc1a75dd..5a776379fce 100644
--- a/apps/files_sharing/lib/Controller/ShareController.php
+++ b/apps/files_sharing/lib/Controller/ShareController.php
@@ -1,239 +1,228 @@
<?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 Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Maxence Lange <maxence@pontapreta.net>
- * @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 <pvince81@owncloud.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\Files\Node\Folder;
-use OC_Files;
-use OC_Util;
+use OC\Security\CSP\ContentSecurityPolicy;
+use OCA\DAV\Connector\Sabre\PublicAuth;
use OCA\FederatedFileSharing\FederatedShareProvider;
+use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
+use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
+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\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\Template;
-use OCP\Share;
-use OCP\AppFramework\Controller;
+use OCP\IPreview;
use OCP\IRequest;
-use OCP\AppFramework\Http\TemplateResponse;
-use OCP\AppFramework\Http\RedirectResponse;
-use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\ISession;
use OCP\IURLGenerator;
-use OCP\IConfig;
-use OCP\ILogger;
use OCP\IUserManager;
-use OCP\ISession;
-use OCP\IPreview;
-use OCA\Files_Sharing\Activity\Providers\Downloads;
-use \OCP\Files\NotFoundException;
-use OCP\Files\IRootFolder;
+use OCP\Security\Events\GenerateSecurePasswordEvent;
+use OCP\Security\ISecureRandom;
+use OCP\Security\PasswordContext;
+use OCP\Share;
use OCP\Share\Exceptions\ShareNotFound;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use OCP\Share\IManager as ShareManager;
+use OCP\Share\IPublicShareTemplateFactory;
+use OCP\Share\IShare;
/**
- * Class ShareController
- *
* @package OCA\Files_Sharing\Controllers
*/
-class ShareController extends Controller {
-
- /** @var IConfig */
- protected $config;
- /** @var IURLGenerator */
- protected $urlGenerator;
- /** @var IUserManager */
- protected $userManager;
- /** @var ILogger */
- protected $logger;
- /** @var \OCP\Activity\IManager */
- protected $activityManager;
- /** @var \OCP\Share\IManager */
- protected $shareManager;
- /** @var ISession */
- protected $session;
- /** @var IPreview */
- protected $previewManager;
- /** @var IRootFolder */
- protected $rootFolder;
- /** @var FederatedShareProvider */
- protected $federatedShareProvider;
- /** @var EventDispatcherInterface */
- protected $eventDispatcher;
- /** @var IL10N */
- protected $l10n;
- /** @var Defaults */
- protected $defaults;
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
+class ShareController extends AuthPublicShareController {
+ 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);
+ }
/**
- * @param string $appName
- * @param IRequest $request
- * @param IConfig $config
- * @param IURLGenerator $urlGenerator
- * @param IUserManager $userManager
- * @param ILogger $logger
- * @param \OCP\Activity\IManager $activityManager
- * @param \OCP\Share\IManager $shareManager
- * @param ISession $session
- * @param IPreview $previewManager
- * @param IRootFolder $rootFolder
- * @param FederatedShareProvider $federatedShareProvider
- * @param EventDispatcherInterface $eventDispatcher
- * @param IL10N $l10n
- * @param Defaults $defaults
+ * Show the authentication page
+ * The form has to submit to the authenticate method route
*/
- public function __construct($appName,
- IRequest $request,
- IConfig $config,
- IURLGenerator $urlGenerator,
- IUserManager $userManager,
- ILogger $logger,
- \OCP\Activity\IManager $activityManager,
- \OCP\Share\IManager $shareManager,
- ISession $session,
- IPreview $previewManager,
- IRootFolder $rootFolder,
- FederatedShareProvider $federatedShareProvider,
- EventDispatcherInterface $eventDispatcher,
- IL10N $l10n,
- Defaults $defaults) {
- parent::__construct($appName, $request);
-
- $this->config = $config;
- $this->urlGenerator = $urlGenerator;
- $this->userManager = $userManager;
- $this->logger = $logger;
- $this->activityManager = $activityManager;
- $this->shareManager = $shareManager;
- $this->session = $session;
- $this->previewManager = $previewManager;
- $this->rootFolder = $rootFolder;
- $this->federatedShareProvider = $federatedShareProvider;
- $this->eventDispatcher = $eventDispatcher;
- $this->l10n = $l10n;
- $this->defaults = $defaults;
+ #[PublicPage]
+ #[NoCSRFRequired]
+ public function showAuthenticate(): TemplateResponse {
+ $templateParameters = ['share' => $this->share];
+
+ $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
+
+ $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
+ if ($this->share->getSendPasswordByTalk()) {
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedConnectDomain('*');
+ $csp->addAllowedMediaDomain('blob:');
+ $response->setContentSecurityPolicy($csp);
+ }
+
+ return $response;
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
- * @param string $token
- * @return TemplateResponse|RedirectResponse
+ * The template to show when authentication failed
*/
- public function showAuthenticate($token) {
- $share = $this->shareManager->getShareByToken($token);
+ protected function showAuthFailed(): TemplateResponse {
+ $templateParameters = ['share' => $this->share, 'wrongpw' => true];
+
+ $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
- if($this->linkShareAuth($share)) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', array('token' => $token)));
+ $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
+ if ($this->share->getSendPasswordByTalk()) {
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedConnectDomain('*');
+ $csp->addAllowedMediaDomain('blob:');
+ $response->setContentSecurityPolicy($csp);
}
- return new TemplateResponse($this->appName, 'authenticate', array(), 'guest');
+ return $response;
}
/**
- * @PublicPage
- * @UseSession
- * @BruteForceProtection(action=publicLinkAuth)
- *
- * Authenticates against password-protected shares
- * @param string $token
- * @param string $password
- * @return RedirectResponse|TemplateResponse|NotFoundResponse
+ * The template to show after user identification
*/
- public function authenticate($token, $password = '') {
+ protected function showIdentificationResult(bool $success = false): TemplateResponse {
+ $templateParameters = ['share' => $this->share, 'identityOk' => $success];
- // Check whether share exists
- try {
- $share = $this->shareManager->getShareByToken($token);
- } catch (ShareNotFound $e) {
- return new NotFoundResponse();
- }
+ $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
- $authenticate = $this->linkShareAuth($share, $password);
-
- if($authenticate === true) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', array('token' => $token)));
+ $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
+ if ($this->share->getSendPasswordByTalk()) {
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedConnectDomain('*');
+ $csp->addAllowedMediaDomain('blob:');
+ $response->setContentSecurityPolicy($csp);
}
- $response = new TemplateResponse($this->appName, 'authenticate', array('wrongpw' => true), 'guest');
- $response->throttle();
return $response;
}
/**
- * Authenticate a link item with the given password.
- * Or use the session if no password is provided.
- *
- * This is a modified version of Helper::authenticate
- * TODO: Try to merge back eventually with Helper::authenticate
+ * Validate the identity token of a public share
*
- * @param \OCP\Share\IShare $share
- * @param string|null $password
+ * @param ?string $identityToken
* @return bool
*/
- private function linkShareAuth(\OCP\Share\IShare $share, $password = null) {
- if ($password !== null) {
- if ($this->shareManager->checkPassword($share, $password)) {
- $this->session->set('public_link_authenticated', (string)$share->getId());
- } else {
- $this->emitAccessShareHook($share, 403, 'Wrong password');
- return false;
- }
- } else {
- // not authenticated ?
- if ( ! $this->session->exists('public_link_authenticated')
- || $this->session->get('public_link_authenticated') !== (string)$share->getId()) {
- return false;
- }
+ protected function validateIdentity(?string $identityToken = null): bool {
+ if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
+ return false;
}
+
+ if ($identityToken === null || $this->share->getSharedWith() === null) {
+ return false;
+ }
+
+ return $identityToken === $this->share->getSharedWith();
+ }
+
+ /**
+ * Generates a password for the share, respecting any password policy defined
+ */
+ protected function generatePassword(): void {
+ $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
+ $this->eventDispatcher->dispatchTyped($event);
+ $password = $event->getPassword() ?? $this->secureRandom->generate(20);
+
+ $this->share->setPassword($password);
+ $this->shareManager->updateShare($this->share);
+ }
+
+ protected function verifyPassword(string $password): bool {
+ return $this->shareManager->checkPassword($this->share, $password);
+ }
+
+ protected function getPasswordHash(): ?string {
+ return $this->share->getPassword();
+ }
+
+ public function isValidToken(): bool {
+ try {
+ $this->share = $this->shareManager->getShareByToken($this->getToken());
+ } catch (ShareNotFound $e) {
+ return false;
+ }
+
return true;
}
+ protected function isPasswordProtected(): bool {
+ return $this->share->getPassword() !== null;
+ }
+
+ 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(PublicAuth::DAV_AUTHENTICATED, $this->share->getId());
+ }
+
+ protected function authFailed() {
+ $this->emitAccessShareHook($this->share, 403, 'Wrong password');
+ $this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
+ }
+
/**
* 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 \OC\HintException
+ *
+ * @throws HintException
* @throws \OC\ServerNotAvailableException
+ *
+ * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
*/
- protected function emitAccessShareHook($share, $errorCode = 200, $errorMessage = '') {
+ protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
$itemType = $itemSource = $uidOwner = '';
$token = $share;
$exception = null;
- if($share instanceof \OCP\Share\IShare) {
+ if ($share instanceof IShare) {
try {
$token = $share->getToken();
$uidOwner = $share->getSharedBy();
@@ -244,250 +233,143 @@ class ShareController extends Controller {
$exception = $e;
}
}
- \OC_Hook::emit('OCP\Share', 'share_link_access', [
+
+ \OC_Hook::emit(Share::class, 'share_link_access', [
'itemType' => $itemType,
'itemSource' => $itemSource,
'uidOwner' => $uidOwner,
'token' => $token,
'errorCode' => $errorCode,
- 'errorMessage' => $errorMessage,
+ 'errorMessage' => $errorMessage
]);
- if(!is_null($exception)) {
+
+ if (!is_null($exception)) {
throw $exception;
}
}
/**
+ * 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) {
+ return;
+ }
+ $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
+ }
+
+ /**
* Validate the permissions of the share
*
* @param Share\IShare $share
* @return bool
*/
- private function validateShare(\OCP\Share\IShare $share) {
+ 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;
+ }
+
+ // If the initiator of the share is disabled no access is granted
+ $initiator = $this->userManager->get($share->getSharedBy());
+ if ($initiator === null || !$initiator->isEnabled()) {
+ return false;
+ }
+
return $share->getNode()->isReadable() && $share->getNode()->isShareable();
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
- * @param string $token
* @param string $path
- * @return TemplateResponse|RedirectResponse|NotFoundResponse
+ * @return TemplateResponse
* @throws NotFoundException
* @throws \Exception
*/
- public function showShare($token, $path = '') {
+ #[PublicPage]
+ #[NoCSRFRequired]
+ public function showShare($path = ''): TemplateResponse {
\OC_User::setIncognitoMode(true);
// Check whether share exists
try {
- $share = $this->shareManager->getShareByToken($token);
+ $share = $this->shareManager->getShareByToken($this->getToken());
} catch (ShareNotFound $e) {
- $this->emitAccessShareHook($token, 404, 'Share not found');
- return new NotFoundResponse();
+ // The share does not exists, we do not emit an ShareLinkAccessedEvent
+ $this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
+ throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}
- // Share is password protected - check whether the user is permitted to access the share
- if ($share->getPassword() !== null && !$this->linkShareAuth($share)) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.authenticate',
- array('token' => $token)));
+ if (!$this->validateShare($share)) {
+ throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
}
- if (!$this->validateShare($share)) {
- throw new NotFoundException();
+ $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 ($share->getNode() instanceof \OCP\Files\File && $path !== '') {
+ if ($shareNode instanceof File && $path !== '') {
$this->emitAccessShareHook($share, 404, 'Share not found');
- throw new NotFoundException();
+ $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
+ 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');
+ $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
throw $e;
}
- $shareTmpl = [];
- $shareTmpl['displayName'] = $this->userManager->get($share->getShareOwner())->getDisplayName();
- $shareTmpl['owner'] = $share->getShareOwner();
- $shareTmpl['filename'] = $share->getNode()->getName();
- $shareTmpl['directory_path'] = $share->getTarget();
- $shareTmpl['mimetype'] = $share->getNode()->getMimetype();
- $shareTmpl['previewSupported'] = $this->previewManager->isMimeSupported($share->getNode()->getMimetype());
- $shareTmpl['dirToken'] = $token;
- $shareTmpl['sharingToken'] = $token;
- $shareTmpl['server2serversharing'] = $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
- $shareTmpl['protected'] = $share->getPassword() !== null ? 'true' : 'false';
- $shareTmpl['dir'] = '';
- $shareTmpl['nonHumanFileSize'] = $share->getNode()->getSize();
- $shareTmpl['fileSize'] = \OCP\Util::humanFileSize($share->getNode()->getSize());
-
- // Show file list
- $hideFileList = false;
- if ($share->getNode() instanceof \OCP\Files\Folder) {
- /** @var \OCP\Files\Folder $rootFolder */
- $rootFolder = $share->getNode();
-
- try {
- $folderNode = $rootFolder->get($path);
- } catch (\OCP\Files\NotFoundException $e) {
- $this->emitAccessShareHook($share, 404, 'Share not found');
- throw new NotFoundException();
- }
-
- $shareTmpl['dir'] = $rootFolder->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 ? false : true;
- $maxUploadFilesize = $freeSpace;
-
- $folder = new Template('files', 'list', '');
- $folder->assign('dir', $rootFolder->getRelativePath($folderNode->getPath()));
- $folder->assign('dirToken', $token);
- $folder->assign('permissions', \OCP\Constants::PERMISSION_READ);
- $folder->assign('isPublic', true);
- $folder->assign('hideFileList', $hideFileList);
- $folder->assign('publicUploadEnabled', 'no');
- $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();
- }
-
- $shareTmpl['hideFileList'] = $hideFileList;
- $shareTmpl['shareOwner'] = $this->userManager->get($share->getShareOwner())->getDisplayName();
- $shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', ['token' => $token]);
- $shareTmpl['shareUrl'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $token]);
- $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'];
- $ogPreview = '';
- if ($shareTmpl['previewSupported']) {
- $shareTmpl['previewImage'] = $this->urlGenerator->linkToRouteAbsolute( 'files_sharing.PublicPreview.getPreview',
- ['x' => 200, 'y' => 200, 'file' => $shareTmpl['directory_path'], 't' => $shareTmpl['dirToken']]);
- $ogPreview = $shareTmpl['previewImage'];
-
- // We just have direct previews for image files
- if ($share->getNode()->getMimePart() === 'image') {
- $shareTmpl['previewURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.publicpreview.directLink', ['token' => $token]);
- $ogPreview = $shareTmpl['previewURL'];
- }
- } else {
- $shareTmpl['previewImage'] = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-fb.png'));
- $ogPreview = $shareTmpl['previewImage'];
- }
-
- // Load files we need
- \OCP\Util::addScript('files', 'file-upload');
- \OCP\Util::addStyle('files_sharing', 'publicView');
- \OCP\Util::addScript('files_sharing', 'public');
- \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', 'breadcrumb');
- \OCP\Util::addScript('files', 'fileinfomodel');
- \OCP\Util::addScript('files', 'newfilemenu');
- \OCP\Util::addScript('files', 'files');
- \OCP\Util::addScript('files', 'filelist');
- \OCP\Util::addScript('files', 'keyboardshortcuts');
- }
-
- // 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->dispatch('OCA\Files_Sharing::loadAdditionalScripts');
-
- $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy();
- $csp->addAllowedFrameDomain('\'self\'');
- $response = new TemplateResponse($this->appName, 'public', $shareTmpl, 'base');
- $response->setContentSecurityPolicy($csp);
$this->emitAccessShareHook($share);
+ $this->emitShareAccessEvent($share, self::SHARE_ACCESS);
return $response;
}
/**
- * @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 is read-only');
- }
-
- // Share is password protected - check whether the user is permitted to access the share
- if ($share->getPassword() !== null && !$this->linkShareAuth($share)) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.authenticate',
- ['token' => $token]));
+ 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');
}
- $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
- $originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath());
-
if (!$this->validateShare($share)) {
throw new NotFoundException();
}
- // 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 !== '') {
@@ -495,144 +377,27 @@ class ShareController extends Controller {
$node = $node->get($path);
} catch (NotFoundException $e) {
$this->emitAccessShareHook($share, 404, 'Share not found');
+ $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
return new NotFoundResponse();
}
}
- $originalSharePath = $userFolder->getRelativePath($node->getPath());
-
- if ($node instanceof \OCP\Files\File) {
- // Single file download
- $this->singleFileDownloaded($share, $share->getNode());
- } else 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');
+ }
+ }
}
}
- /* 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 = array( '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();
- }
- }
-
- /**
- * create activity for every downloaded file
- *
- * @param Share\IShare $share
- * @param array $files_list
- * @param \OCP\Files\Folder $node
- */
- protected function fileListDownloaded(Share\IShare $share, array $files_list, \OCP\Files\Folder $node) {
- 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
- */
- protected function singleFileDownloaded(Share\IShare $share, \OCP\Files\Node $node) {
-
- $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());
-
- $parameters = [$userPath];
-
- if ($share->getShareType() === \OCP\Share::SHARE_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;
- } else {
- $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
- }
- }
-
- $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);
-
- if ($share->getShareOwner() !== $share->getSharedBy()) {
- $parameters[0] = $ownerPath;
- $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
+ $davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
+ if ($files !== null) {
+ $davUrl .= '&files=' . $files;
}
+ return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
}
-
- /**
- * 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);
- }
-
-
}
diff --git a/apps/files_sharing/lib/Controller/ShareInfoController.php b/apps/files_sharing/lib/Controller/ShareInfoController.php
index 28bfcd12c24..b7e79aec830 100644
--- a/apps/files_sharing/lib/Controller/ShareInfoController.php
+++ b/apps/files_sharing/lib/Controller/ShareInfoController.php
@@ -1,46 +1,33 @@
<?php
+
/**
- *
- *
- * @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;
use OCP\Files\Folder;
use OCP\Files\Node;
-use OCP\ILogger;
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.
*
@@ -48,82 +35,107 @@ class ShareInfoController extends ApiController {
* @param IRequest $request
* @param IManager $shareManager
*/
- public function __construct($appName,
- IRequest $request,
- IManager $shareManager) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IManager $shareManager,
+ ) {
parent::__construct($appName, $request);
-
- $this->shareManager = $shareManager;
}
/**
- * @PublicPage
- * @NoCSRFRequired
+ * 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
- * @throws ShareNotFound
+ * 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) {
- return new JSONResponse([], Http::STATUS_NOT_FOUND);
+ $response = new JSONResponse([], Http::STATUS_NOT_FOUND);
+ $response->throttle(['token' => $t]);
+ return $response;
}
if ($share->getPassword() && !$this->shareManager->checkPassword($share, $password)) {
- return new JSONResponse([], Http::STATUS_FORBIDDEN);
+ $response = new JSONResponse([], Http::STATUS_FORBIDDEN);
+ $response->throttle(['token' => $t]);
+ return $response;
}
if (!($share->getPermissions() & Constants::PERMISSION_READ)) {
- return new JSONResponse([], Http::STATUS_FORBIDDEN);
- }
-
- $isWritable = $share->getPermissions() & (\OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_CREATE);
- if (!$isWritable) {
- $this->addROWrapper();
+ $response = new JSONResponse([], Http::STATUS_FORBIDDEN);
+ $response->throttle(['token' => $t]);
+ return $response;
}
+ $permissionMask = $share->getPermissions();
$node = $share->getNode();
if ($dir !== null && $node instanceof Folder) {
try {
$node = $node->get($dir);
} catch (NotFoundException $e) {
-
}
}
- return new JSONResponse($this->parseNode($node));
+ return new JSONResponse($this->parseNode($node, $permissionMask, $depth));
}
- private function parseNode(Node $node) {
+ /**
+ * @return Files_SharingShareInfo
+ */
+ private function parseNode(Node $node, int $permissionMask, int $depth): array {
if ($node instanceof File) {
- return $this->parseFile($node);
+ return $this->parseFile($node, $permissionMask);
}
- return $this->parseFolder($node);
+ /** @var Folder $node */
+ return $this->parseFolder($node, $permissionMask, $depth);
}
- private function parseFile(File $file) {
- return $this->format($file);
+ /**
+ * @return Files_SharingShareInfo
+ */
+ private function parseFile(File $file, int $permissionMask): array {
+ return $this->format($file, $permissionMask);
}
- private function parseFolder(Folder $folder) {
- $data = $this->format($folder);
+ /**
+ * @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);
+ $data['children'][] = $this->parseNode($node, $permissionMask, $depth <= -1 ? -1 : $depth - 1);
}
return $data;
}
- private function format(Node $node) {
+ /**
+ * @return Files_SharingShareInfo
+ */
+ private function format(Node $node, int $permissionMask): array {
$entry = [];
$entry['id'] = $node->getId();
@@ -131,7 +143,7 @@ class ShareInfoController extends ApiController {
$entry['mtime'] = $node->getMTime();
$entry['name'] = $node->getName();
- $entry['permissions'] = $node->getPermissions();
+ $entry['permissions'] = $node->getPermissions() & $permissionMask;
$entry['mimetype'] = $node->getMimetype();
$entry['size'] = $node->getSize();
$entry['type'] = $node->getType();
@@ -139,13 +151,4 @@ class ShareInfoController extends ApiController {
return $entry;
}
-
- protected function addROWrapper() {
- // FIXME: should not add storage wrappers outside of preSetup, need to find a better way
- $previousLog = \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper(false);
- \OC\Files\Filesystem::addStorageWrapper('readonly', function ($mountPoint, $storage) {
- return new \OC\Files\Storage\Wrapper\PermissionsMask(array('storage' => $storage, 'mask' => \OCP\Constants::PERMISSION_READ + \OCP\Constants::PERMISSION_SHARE));
- });
- \OC\Files\Filesystem::logWarningWhenAddingStorageWrapper($previousLog);
- }
}
diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php
index 575bf01fdb0..0c458ce9662 100644
--- a/apps/files_sharing/lib/Controller/ShareesAPIController.php
+++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php
@@ -1,58 +1,43 @@
<?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 Joas Schilling <coding@schilljs.com>
- * @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 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\IRequest;
+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;
+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 IConfig */
- protected $config;
-
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var IManager */
- protected $shareManager;
-
- /** @var bool */
- protected $shareWithGroupOnly = false;
-
- /** @var bool */
- protected $shareeEnumeration = true;
/** @var int */
protected $offset = 0;
@@ -60,73 +45,71 @@ class ShareesAPIController extends OCSController {
/** @var int */
protected $limit = 10;
- /** @var array */
+ /** @var Files_SharingShareesSearchResult */
protected $result = [
'exact' => [
'users' => [],
'groups' => [],
'remotes' => [],
+ 'remote_groups' => [],
'emails' => [],
'circles' => [],
+ 'rooms' => [],
],
'users' => [],
'groups' => [],
'remotes' => [],
+ 'remote_groups' => [],
'emails' => [],
'lookup' => [],
'circles' => [],
+ 'rooms' => [],
+ 'lookupEnabled' => false,
];
protected $reachedEndFor = [];
- /** @var ISearch */
- private $collaboratorSearch;
- /**
- * @param string $appName
- * @param IRequest $request
- * @param IConfig $config
- * @param IURLGenerator $urlGenerator
- * @param IManager $shareManager
- * @param ISearch $collaboratorSearch
- */
public function __construct(
- $appName,
+ 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->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($search = '', $itemType = null, $page = 1, $perPage = 200, $shareType = null, $lookup = true) {
+ #[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 = (int)$this->config->getSystemValue('sharing.minSearchStringLength', 0);
+ $threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0);
if (strlen($search) < $threshold) {
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 = (int)$this->config->getSystemValue('sharing.maxAutocompleteResults', 0);
+ $maxResults = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
if ($maxResults > 0) {
$perPage = min($perPage, $maxResults);
}
@@ -138,78 +121,250 @@ class ShareesAPIController extends OCSController {
}
$shareTypes = [
- Share::SHARE_TYPE_USER,
+ IShare::TYPE_USER,
];
if ($itemType === null) {
throw new OCSBadRequestException('Missing itemType');
} elseif ($itemType === 'file' || $itemType === 'folder') {
if ($this->shareManager->allowGroupSharing()) {
- $shareTypes[] = Share::SHARE_TYPE_GROUP;
+ $shareTypes[] = IShare::TYPE_GROUP;
}
if ($this->isRemoteSharingAllowed($itemType)) {
- $shareTypes[] = Share::SHARE_TYPE_REMOTE;
+ $shareTypes[] = IShare::TYPE_REMOTE;
+ }
+
+ if ($this->isRemoteGroupSharingAllowed($itemType)) {
+ $shareTypes[] = IShare::TYPE_REMOTE_GROUP;
+ }
+
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
+ $shareTypes[] = IShare::TYPE_EMAIL;
}
- if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_EMAIL)) {
- $shareTypes[] = Share::SHARE_TYPE_EMAIL;
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
+ $shareTypes[] = IShare::TYPE_ROOM;
+ }
+
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) {
+ $shareTypes[] = IShare::TYPE_SCIENCEMESH;
}
} else {
- $shareTypes[] = Share::SHARE_TYPE_GROUP;
- $shareTypes[] = Share::SHARE_TYPE_EMAIL;
+ if ($this->shareManager->allowGroupSharing()) {
+ $shareTypes[] = IShare::TYPE_GROUP;
+ }
+ $shareTypes[] = IShare::TYPE_EMAIL;
}
// FIXME: DI
- if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
- $shareTypes[] = Share::SHARE_TYPE_CIRCLE;
+ if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
+ $shareTypes[] = IShare::TYPE_CIRCLE;
}
- if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
- $shareTypes = array_intersect($shareTypes, $_GET['shareType']);
- sort($shareTypes);
- } else if (is_numeric($shareType)) {
- $shareTypes = array_intersect($shareTypes, [(int) $shareType]);
- sort($shareTypes);
+ 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]);
}
+ sort($shareTypes);
- $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
- $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
- $this->limit = (int) $perPage;
+ $this->limit = $perPage;
$this->offset = $perPage * ($page - 1);
- list($result, $hasMoreResults) = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $this->limit, $this->offset);
+ // 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, $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'])) {
+ if (isset($result['exact'])) {
$result['exact'] = array_merge($this->result['exact'], $result['exact']);
}
$this->result = array_merge($this->result, $result);
$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;
}
/**
+ * @param string $user
+ * @param int $shareType
+ *
+ * @return Generator<array<string>>
+ */
+ private function getAllShareesByType(string $user, int $shareType): Generator {
+ $offset = 0;
+ $pageSize = 50;
+
+ while (count($page = $this->shareManager->getSharesBy(
+ $user,
+ $shareType,
+ null,
+ false,
+ $pageSize,
+ $offset
+ ))) {
+ foreach ($page as $share) {
+ yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
+ }
+
+ $offset += $pageSize;
+ }
+ }
+
+ private function sortShareesByFrequency(array $sharees): array {
+ usort($sharees, function (array $s1, array $s2): int {
+ return $s2['count'] - $s1['count'];
+ });
+ return $sharees;
+ }
+
+ private $searchResultTypeMap = [
+ IShare::TYPE_USER => 'users',
+ IShare::TYPE_GROUP => 'groups',
+ IShare::TYPE_REMOTE => 'remotes',
+ IShare::TYPE_REMOTE_GROUP => 'remote_groups',
+ IShare::TYPE_EMAIL => 'emails',
+ ];
+
+ private function getAllSharees(string $user, array $shareTypes): ISearchResult {
+ $result = [];
+ foreach ($shareTypes as $shareType) {
+ $sharees = $this->getAllShareesByType($user, $shareType);
+ $shareTypeResults = [];
+ foreach ($sharees as [$sharee, $displayname]) {
+ if (!isset($this->searchResultTypeMap[$shareType]) || trim($sharee) === '') {
+ continue;
+ }
+
+ if (!isset($shareTypeResults[$sharee])) {
+ $shareTypeResults[$sharee] = [
+ 'count' => 1,
+ 'label' => $displayname,
+ 'value' => [
+ 'shareType' => $shareType,
+ 'shareWith' => $sharee,
+ ],
+ ];
+ } else {
+ $shareTypeResults[$sharee]['count']++;
+ }
+ }
+ $result = array_merge($result, array_values($shareTypeResults));
+ }
+
+ $top5 = array_slice(
+ $this->sortShareesByFrequency($result),
+ 0,
+ 5
+ );
+
+ $searchResult = new SearchResult();
+ foreach ($this->searchResultTypeMap as $int => $str) {
+ $searchResult->addResultSet(new SearchResultType($str), [], []);
+ foreach ($top5 as $x) {
+ if ($x['value']['shareType'] === $int) {
+ $searchResult->addResultSet(new SearchResultType($str), [], [$x]);
+ }
+ }
+ }
+ return $searchResult;
+ }
+
+ /**
+ * Find recommended sharees
+ *
+ * @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
+ */
+ #[NoAdminRequired]
+ public function findRecommended(string $itemType, $shareType = null): DataResponse {
+ $shareTypes = [
+ IShare::TYPE_USER,
+ ];
+
+ if ($itemType === 'file' || $itemType === 'folder') {
+ if ($this->shareManager->allowGroupSharing()) {
+ $shareTypes[] = IShare::TYPE_GROUP;
+ }
+
+ if ($this->isRemoteSharingAllowed($itemType)) {
+ $shareTypes[] = IShare::TYPE_REMOTE;
+ }
+
+ if ($this->isRemoteGroupSharingAllowed($itemType)) {
+ $shareTypes[] = IShare::TYPE_REMOTE_GROUP;
+ }
+
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
+ $shareTypes[] = IShare::TYPE_EMAIL;
+ }
+
+ if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
+ $shareTypes[] = IShare::TYPE_ROOM;
+ }
+ } else {
+ $shareTypes[] = IShare::TYPE_GROUP;
+ $shareTypes[] = IShare::TYPE_EMAIL;
+ }
+
+ // FIXME: DI
+ if (Server::get(IAppManager::class)->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
+ $shareTypes[] = IShare::TYPE_CIRCLE;
+ }
+
+ if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
+ $shareTypes = array_intersect($shareTypes, $_GET['shareType']);
+ sort($shareTypes);
+ } elseif (is_numeric($shareType)) {
+ $shareTypes = array_intersect($shareTypes, [(int)$shareType]);
+ sort($shareTypes);
+ }
+
+ return new DataResponse(
+ $this->getAllSharees($this->userId, $shareTypes)->asArray()
+ );
+ }
+
+ /**
* Method to get out the static call for better testing
*
* @param string $itemType
* @return bool
*/
- protected function isRemoteSharingAllowed($itemType) {
+ protected function isRemoteSharingAllowed(string $itemType): bool {
try {
// FIXME: static foo makes unit testing unnecessarily difficult
- $backend = \OC\Share\Share::getBackend($itemType);
- return $backend->isShareTypeAllowed(Share::SHARE_TYPE_REMOTE);
+ $backend = Share::getBackend($itemType);
+ return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE);
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ protected function isRemoteGroupSharingAllowed(string $itemType): bool {
+ try {
+ // FIXME: static foo makes unit testing unnecessarily difficult
+ $backend = Share::getBackend($itemType);
+ return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE_GROUP);
} catch (\Exception $e) {
return false;
}
@@ -223,22 +378,20 @@ class ShareesAPIController extends OCSController {
* @param array $params Parameters for the URL
* @return string
*/
- protected function getPaginationLink($page, array $params) {
+ protected function getPaginationLink(int $page, array $params): string {
if ($this->isV2()) {
$url = $this->urlGenerator->getAbsoluteURL('/ocs/v2.php/apps/files_sharing/api/v1/sharees') . '?';
} else {
$url = $this->urlGenerator->getAbsoluteURL('/ocs/v1.php/apps/files_sharing/api/v1/sharees') . '?';
}
$params['page'] = $page + 1;
- $link = '<' . $url . http_build_query($params) . '>; rel="next"';
-
- return $link;
+ return '<' . $url . http_build_query($params) . '>; rel="next"';
}
/**
* @return bool
*/
- protected function isV2() {
+ protected function isV2(): bool {
return $this->request->getScriptName() === '/ocs/v2.php';
}
}
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 56cbbe10336..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 Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Vincent Petry <pvince81@owncloud.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,16 +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;
+ }
+
+ $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);
- $sql =
- 'DELETE FROM `*PREFIX*share` ' .
- 'WHERE `item_type` in (\'file\', \'folder\') ' .
- 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`)';
+ $deleteQb = $this->db->getQueryBuilder();
+ $deleteQb->delete('share')
+ ->where(
+ $deleteQb->expr()->in('file_source', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY)
+ );
- $deletedEntries = $connection->executeUpdate($sql);
- $logger->debug("$deletedEntries orphaned share(s) deleted", ['app' => 'DeleteOrphanedSharesJob']);
+ $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);
+ }
}
+ 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
new file mode 100644
index 00000000000..709d7bacd4a
--- /dev/null
+++ b/apps/files_sharing/lib/Event/BeforeTemplateRenderedEvent.php
@@ -0,0 +1,49 @@
+<?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\Event;
+
+use OCP\EventDispatcher\Event;
+use OCP\Share\IShare;
+
+/**
+ * Emitted before the rendering step of the public share page happens. The event
+ * holds a flag that specifies if it is the authentication page of a public share.
+ *
+ * @since 20.0.0
+ */
+class BeforeTemplateRenderedEvent extends Event {
+ /**
+ * @since 20.0.0
+ */
+ public const SCOPE_PUBLIC_SHARE_AUTH = 'publicShareAuth';
+
+ /**
+ * @since 20.0.0
+ */
+ public function __construct(
+ private IShare $share,
+ private ?string $scope = null,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @since 20.0.0
+ */
+ public function getShare(): IShare {
+ return $this->share;
+ }
+
+ /**
+ * @since 20.0.0
+ */
+ public function getScope(): ?string {
+ return $this->scope;
+ }
+}
diff --git a/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php b/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php
new file mode 100644
index 00000000000..d0cb0a1949d
--- /dev/null
+++ b/apps/files_sharing/lib/Event/ShareLinkAccessedEvent.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Sharing\Event;
+
+use OCP\EventDispatcher\Event;
+use OCP\Share\IShare;
+
+class ShareLinkAccessedEvent extends Event {
+ public function __construct(
+ private IShare $share,
+ private string $step = '',
+ private int $errorCode = 200,
+ private string $errorMessage = '',
+ ) {
+ parent::__construct();
+ }
+
+ public function getShare(): IShare {
+ return $this->share;
+ }
+
+ public function getStep(): string {
+ return $this->step;
+ }
+
+ public function getErrorCode(): int {
+ return $this->errorCode;
+ }
+
+ public function getErrorMessage(): string {
+ return $this->errorMessage;
+ }
+}
diff --git a/apps/files_sharing/lib/Event/ShareMountedEvent.php b/apps/files_sharing/lib/Event/ShareMountedEvent.php
new file mode 100644
index 00000000000..0f56873cb2c
--- /dev/null
+++ b/apps/files_sharing/lib/Event/ShareMountedEvent.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Sharing\Event;
+
+use OCA\Files_Sharing\SharedMount;
+use OCP\EventDispatcher\Event;
+use OCP\Files\Mount\IMountPoint;
+
+class ShareMountedEvent extends Event {
+ /** @var IMountPoint[] */
+ private $additionalMounts = [];
+
+ public function __construct(
+ private SharedMount $mount,
+ ) {
+ parent::__construct();
+ }
+
+ public function getMount(): SharedMount {
+ return $this->mount;
+ }
+
+ public function addAdditionalMount(IMountPoint $mountPoint): void {
+ $this->additionalMounts[] = $mountPoint;
+ }
+
+ /**
+ * @return IMountPoint[]
+ */
+ public function getAdditionalMounts(): array {
+ return $this->additionalMounts;
+ }
+}
diff --git a/apps/files_sharing/lib/Exceptions/BrokenPath.php b/apps/files_sharing/lib/Exceptions/BrokenPath.php
index c6676f3cd0c..a68a8fc05d4 100644
--- a/apps/files_sharing/lib/Exceptions/BrokenPath.php
+++ b/apps/files_sharing/lib/Exceptions/BrokenPath.php
@@ -1,33 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Morris Jobke <hey@morrisjobke.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: 2020-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_Sharing\Exceptions;
/**
* Expected path with a different root
* Possible Error Codes:
* 10 - Path not relative to data/ and point to the users file directory
-
+ *
*/
class BrokenPath extends \Exception {
}
diff --git a/apps/files_sharing/lib/Exceptions/S2SException.php b/apps/files_sharing/lib/Exceptions/S2SException.php
index 7a888020f84..10360820432 100644
--- a/apps/files_sharing/lib/Exceptions/S2SException.php
+++ b/apps/files_sharing/lib/Exceptions/S2SException.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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: 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
new file mode 100644
index 00000000000..2ffe72c4e69
--- /dev/null
+++ b/apps/files_sharing/lib/Exceptions/SharingRightsException.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Sharing\Exceptions;
+
+use Exception;
+
+/**
+ * Sharing and Resharing rights.
+ *
+ * Class SharingRightsException
+ *
+ * @package OCA\Files_Sharing\Exceptions
+ */
+class SharingRightsException extends Exception {
+}
diff --git a/apps/files_sharing/lib/ExpireSharesJob.php b/apps/files_sharing/lib/ExpireSharesJob.php
index 39965336bff..b1c6c592e80 100644
--- a/apps/files_sharing/lib/ExpireSharesJob.php
+++ b/apps/files_sharing/lib/ExpireSharesJob.php
@@ -1,51 +1,44 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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: 2017-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\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
/**
* Delete all shares that are expired
*/
class ExpireSharesJob extends TimedJob {
- /**
- * sets the correct interval for this timed job
- */
- public function __construct() {
+ 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(self::TIME_INSENSITIVE);
}
+
/**
* Makes the background job do its work
*
* @param array $argument unused argument
*/
public function run($argument) {
- $connection = \OC::$server->getDatabaseConnection();
-
//Current time
$now = new \DateTime();
$now = $now->format('Y-m-d H:i:s');
@@ -53,25 +46,34 @@ class ExpireSharesJob extends TimedJob {
/*
* Expire file link shares only (for now)
*/
- $qb = $connection->getQueryBuilder();
- $qb->select('id', 'file_source', 'uid_owner', 'item_type')
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id', 'share_type')
->from('share')
->where(
$qb->expr()->andX(
- $qb->expr()->eq('share_type', $qb->expr()->literal(\OCP\Share::SHARE_TYPE_LINK)),
+ $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();
- while($share = $shares->fetch()) {
- \OC\Share\Share::unshare($share['item_type'], $share['file_source'], \OCP\Share::SHARE_TYPE_LINK, null, $share['uid_owner']);
+ $shares = $qb->executeQuery();
+ while ($share = $shares->fetch()) {
+ if ((int)$share['share_type'] === IShare::TYPE_LINK) {
+ $id = 'ocinternal';
+ } elseif ((int)$share['share_type'] === IShare::TYPE_EMAIL) {
+ $id = 'ocMailShare';
+ }
+
+ $id .= ':' . $share['id'];
+
+ try {
+ $share = $this->shareManager->getShareById($id);
+ $this->shareManager->deleteShare($share);
+ } catch (ShareNotFound $e) {
+ // Normally the share gets automatically expired on fetching it
+ }
}
$shares->closeCursor();
}
-
}
diff --git a/apps/files_sharing/lib/External/Cache.php b/apps/files_sharing/lib/External/Cache.php
index c7793cf0595..027f682d818 100644
--- a/apps/files_sharing/lib/External/Cache.php
+++ b/apps/files_sharing/lib/External/Cache.php
@@ -1,49 +1,30 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Vincent Petry <pvince81@owncloud.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;
- list(, $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) {
@@ -60,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 6935685685c..ff4781eba0f 100644
--- a/apps/files_sharing/lib/External/Manager.php
+++ b/apps/files_sharing/lib/External/Manager.php
@@ -1,106 +1,63 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Daniel Hansson <daniel@techandme.se>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.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 Stefan Weil <sw@weilnetz.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: 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 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;
use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use OCP\IUserSession;
use OCP\Notification\IManager;
use OCP\OCS\IDiscoveryService;
+use OCP\Share;
+use OCP\Share\IShare;
+use Psr\Log\LoggerInterface;
+/**
+ * @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions
+ */
class Manager {
- const STORAGE = '\OCA\Files_Sharing\External\Storage';
+ public const STORAGE = '\OCA\Files_Sharing\External\Storage';
- /**
- * @var string
- */
+ /** @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;
-
- /**
- * @param IDBConnection $connection
- * @param \OC\Files\Mount\Manager $mountManager
- * @param IStorageFactory $storageLoader
- * @param IClientService $clientService
- * @param IManager $notificationManager
- * @param IDiscoveryService $discoveryService
- * @param string $uid
- */
- public function __construct(IDBConnection $connection,
- \OC\Files\Mount\Manager $mountManager,
- IStorageFactory $storageLoader,
- IClientService $clientService,
- IManager $notificationManager,
- IDiscoveryService $discoveryService,
- $uid) {
- $this->connection = $connection;
- $this->mountManager = $mountManager;
- $this->storageLoader = $storageLoader;
- $this->clientService = $clientService;
- $this->uid = $uid;
- $this->notificationManager = $notificationManager;
- $this->discoveryService = $discoveryService;
+ public function __construct(
+ 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->uid = $user ? $user->getUID() : null;
}
/**
@@ -111,18 +68,20 @@ class Manager {
* @param string $password
* @param string $name
* @param string $owner
+ * @param int $shareType
* @param boolean $accepted
* @param string $user
- * @param int $remoteId
+ * @param string $remoteId
+ * @param int $parent
* @return Mount|null
+ * @throws \Doctrine\DBAL\Exception
*/
- public function addShare($remote, $token, $password, $name, $owner, $accepted=false, $user = null, $remoteId = -1) {
-
- $user = $user ? $user : $this->uid;
- $accepted = $accepted ? 1 : 0;
+ public function addShare($remote, $token, $password, $name, $owner, $shareType, $accepted = false, $user = null, $remoteId = '', $parent = -1) {
+ $user = $user ?? $this->uid;
+ $accepted = $accepted ? IShare::STATUS_ACCEPTED : IShare::STATUS_PENDING;
$name = Filesystem::normalizePath('/' . $name);
- if (!$accepted) {
+ if ($accepted !== IShare::STATUS_ACCEPTED) {
// To avoid conflicts with the mount point generation later,
// we only use a temporary mount point name here. The real
// mount point name will be generated when accepting the share,
@@ -131,16 +90,17 @@ class Manager {
$mountPoint = $tmpMountPointName;
$hash = md5($tmpMountPointName);
$data = [
- 'remote' => $remote,
- 'share_token' => $token,
- 'password' => $password,
- 'name' => $name,
- 'owner' => $owner,
- 'user' => $user,
- 'mountpoint' => $mountPoint,
- 'mountpoint_hash' => $hash,
- 'accepted' => $accepted,
- 'remote_id' => $remoteId,
+ 'remote' => $remote,
+ 'share_token' => $token,
+ 'password' => $password,
+ 'name' => $name,
+ 'owner' => $owner,
+ 'user' => $user,
+ 'mountpoint' => $mountPoint,
+ 'mountpoint_hash' => $hash,
+ 'accepted' => $accepted,
+ 'remote_id' => $remoteId,
+ 'share_type' => $shareType,
];
$i = 1;
@@ -157,37 +117,165 @@ class Manager {
$mountPoint = Filesystem::normalizePath('/' . $mountPoint);
$hash = md5($mountPoint);
+ $this->writeShareToDb($remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType);
+
+ $options = [
+ 'remote' => $remote,
+ 'token' => $token,
+ 'password' => $password,
+ 'mountpoint' => $mountPoint,
+ 'owner' => $owner
+ ];
+ return $this->mountShare($options, $user);
+ }
+
+ /**
+ * write remote share to the database
+ *
+ * @param $remote
+ * @param $token
+ * @param $password
+ * @param $name
+ * @param $owner
+ * @param $user
+ * @param $mountPoint
+ * @param $hash
+ * @param $accepted
+ * @param $remoteId
+ * @param $parent
+ * @param $shareType
+ *
+ * @return void
+ * @throws \Doctrine\DBAL\Driver\Exception
+ */
+ private function writeShareToDb($remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType): void {
$query = $this->connection->prepare('
INSERT INTO `*PREFIX*share_external`
- (`remote`, `share_token`, `password`, `name`, `owner`, `user`, `mountpoint`, `mountpoint_hash`, `accepted`, `remote_id`)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ (`remote`, `share_token`, `password`, `name`, `owner`, `user`, `mountpoint`, `mountpoint_hash`, `accepted`, `remote_id`, `parent`, `share_type`)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
');
- $query->execute(array($remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId));
-
- $options = array(
- 'remote' => $remote,
- 'token' => $token,
- 'password' => $password,
- 'mountpoint' => $mountPoint,
- 'owner' => $owner
- );
- return $this->mountShare($options);
+ $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
*/
- public function getShare($id) {
+ private function fetchShareByToken($token) {
$getShare = $this->connection->prepare('
- SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`
+ SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`, `parent`, `share_type`, `password`, `mountpoint_hash`
FROM `*PREFIX*share_external`
- WHERE `id` = ? AND `user` = ?');
- $result = $getShare->execute(array($id, $this->uid));
+ WHERE `share_token` = ?');
+ $result = $getShare->execute([$token]);
+ $share = $result->fetch();
+ $result->closeCursor();
+ return $share;
+ }
- return $result ? $getShare->fetch() : false;
+ private function fetchUserShare($parentId, $uid) {
+ $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 `parent` = ? AND `user` = ?');
+ $result = $getShare->execute([$parentId, $uid]);
+ $share = $result->fetch();
+ $result->closeCursor();
+ if ($share !== false) {
+ return $share;
+ }
+ return null;
+ }
+
+ public function getShare(int $id, ?string $user = null): array|false {
+ $user = $user ?? $this->uid;
+ $share = $this->fetchShare($id);
+ if ($share === false) {
+ return false;
+ }
+
+ // check if the user is allowed to access it
+ if ($this->canAccessShare($share, $user)) {
+ return $share;
+ }
+
+ 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
+ $groupShare = $this->fetchShare($parentId);
+ } else {
+ $groupShare = $share;
+ }
+
+ $user = $this->userManager->get($user);
+ if ($this->groupManager->get($groupShare['user'])->inGroup($user)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Updates accepted flag in the database
+ *
+ * @param int $id
+ */
+ private function updateAccepted(int $shareId, bool $accepted) : void {
+ $query = $this->connection->prepare('
+ UPDATE `*PREFIX*share_external`
+ SET `accepted` = ?
+ WHERE `id` = ?');
+ $updateResult = $query->execute([$accepted ? 1 : 0, $shareId]);
+ $updateResult->closeCursor();
}
/**
@@ -196,34 +284,90 @@ class Manager {
* @param int $id
* @return bool True if the share could be accepted, false otherwise
*/
- public function acceptShare($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);
+ $share = $this->getShare($id, $user);
$result = false;
if ($share) {
- \OC_Util::setupFS($this->uid);
- $shareFolder = Helper::getShareFolder();
+ \OC_Util::setupFS($user);
+ $shareFolder = Helper::getShareFolder(null, $user);
$mountPoint = Files::buildNotExistingFileName($shareFolder, $share['name']);
$mountPoint = Filesystem::normalizePath($mountPoint);
$hash = md5($mountPoint);
+ $userShareAccepted = false;
- $acceptShare = $this->connection->prepare('
+ if ((int)$share['share_type'] === IShare::TYPE_USER) {
+ $acceptShare = $this->connection->prepare('
UPDATE `*PREFIX*share_external`
SET `accepted` = ?,
`mountpoint` = ?,
`mountpoint_hash` = ?
WHERE `id` = ? AND `user` = ?');
- $updated = $acceptShare->execute(array(1, $mountPoint, $hash, $id, $this->uid));
- if ($updated === true) {
+ $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, $user);
+ }
+
+ if ($subshare !== null) {
+ try {
+ $acceptShare = $this->connection->prepare('
+ UPDATE `*PREFIX*share_external`
+ SET `accepted` = ?,
+ `mountpoint` = ?,
+ `mountpoint_hash` = ?
+ WHERE `id` = ? AND `user` = ?');
+ $acceptShare->execute([1, $mountPoint, $hash, $subshare['id'], $user]);
+ $result = true;
+ } catch (Exception $e) {
+ $this->logger->emergency('Could not update share', ['exception' => $e]);
+ $result = false;
+ }
+ } else {
+ try {
+ $this->writeShareToDb(
+ $share['remote'],
+ $share['share_token'],
+ $share['password'],
+ $share['name'],
+ $share['owner'],
+ $user,
+ $mountPoint, $hash, 1,
+ $share['remote_id'],
+ $id,
+ $share['share_type']);
+ $result = true;
+ } catch (Exception $e) {
+ $this->logger->emergency('Could not create share', ['exception' => $e]);
+ $result = false;
+ }
+ }
+ }
+
+ if ($userShareAccepted !== false) {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept');
- \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $share['remote']]);
+ $event = new FederatedShareAddedEvent($share['remote']);
+ $this->eventDispatcher->dispatchTyped($event);
+ $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;
}
@@ -234,31 +378,84 @@ class Manager {
* @param int $id
* @return bool True if the share could be declined, false otherwise
*/
- public function declineShare($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);
+ $share = $this->getShare($id, $user);
+ $result = false;
- if ($share) {
+ if ($share && (int)$share['share_type'] === IShare::TYPE_USER) {
$removeShare = $this->connection->prepare('
DELETE FROM `*PREFIX*share_external` WHERE `id` = ? AND `user` = ?');
- $removeShare->execute(array($id, $this->uid));
+ $removeShare->execute([$id, $user]);
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
- $this->processNotification($id);
- return true;
+ $this->processNotification($id, $user);
+ $result = true;
+ } elseif ($share && (int)$share['share_type'] === IShare::TYPE_GROUP) {
+ $parentId = (int)$share['parent'];
+ if ($parentId !== -1) {
+ // this is the sub-share
+ $subshare = $share;
+ } else {
+ $subshare = $this->fetchUserShare($id, $user);
+ }
+
+ if ($subshare !== null) {
+ try {
+ $this->updateAccepted((int)$subshare['id'], false);
+ $result = true;
+ } catch (Exception $e) {
+ $this->logger->emergency('Could not update share', ['exception' => $e]);
+ $result = false;
+ }
+ } else {
+ try {
+ $this->writeShareToDb(
+ $share['remote'],
+ $share['share_token'],
+ $share['password'],
+ $share['name'],
+ $share['owner'],
+ $user,
+ $share['mountpoint'],
+ $share['mountpoint_hash'],
+ 0,
+ $share['remote_id'],
+ $id,
+ $share['share_type']);
+ $result = true;
+ } catch (Exception $e) {
+ $this->logger->emergency('Could not create share', ['exception' => $e]);
+ $result = false;
+ }
+ }
+ $this->processNotification($id, $user);
}
- return false;
+ 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);
}
@@ -267,17 +464,22 @@ class Manager {
*
* @param string $remote
* @param string $token
- * @param int $remoteId Share id on the remote host
+ * @param string $remoteId Share id on the remote host
* @param string $feedback
* @return boolean
*/
private function sendFeedbackToRemote($remote, $token, $remoteId, $feedback) {
+ $result = $this->tryOCMEndPoint($remote, $token, $remoteId, $feedback);
+
+ if (is_array($result)) {
+ return true;
+ }
$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=' . \OCP\Share::RESPONSE_FORMAT;
- $fields = array('token' => $token);
+ $url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=' . Share::RESPONSE_FORMAT;
+ $fields = ['token' => $token];
$client = $this->clientService->newClient();
@@ -299,6 +501,49 @@ class Manager {
}
/**
+ * try send accept message to ocm end-point
+ *
+ * @param string $remoteDomain
+ * @param string $token
+ * @param string $remoteId id of the share
+ * @param string $feedback
+ * @return array|false
+ */
+ protected function tryOCMEndPoint($remoteDomain, $token, $remoteId, $feedback) {
+ switch ($feedback) {
+ case 'accept':
+ $notification = $this->cloudFederationFactory->getCloudFederationNotification();
+ $notification->setMessage(
+ 'SHARE_ACCEPTED',
+ 'file',
+ $remoteId,
+ [
+ 'sharedSecret' => $token,
+ 'message' => 'Recipient accept the share'
+ ]
+
+ );
+ return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
+ case 'decline':
+ $notification = $this->cloudFederationFactory->getCloudFederationNotification();
+ $notification->setMessage(
+ 'SHARE_DECLINED',
+ 'file',
+ $remoteId,
+ [
+ 'sharedSecret' => $token,
+ 'message' => 'Recipient declined the share'
+ ]
+
+ );
+ return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
+ }
+
+ return false;
+ }
+
+
+ /**
* remove '/user/files' from the path and trailing slashes
*
* @param string $path
@@ -309,11 +554,12 @@ 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($this->uid);
+ $data['certificateManager'] = \OC::$server->getCertificateManager();
return new Mount(self::STORAGE, $mountPoint, $data, $this, $this->storageLoader);
}
@@ -321,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;
}
@@ -351,48 +597,62 @@ class Manager {
WHERE `mountpoint_hash` = ?
AND `user` = ?
');
- $result = (bool)$query->execute(array($target, $targetHash, $sourceHash, $this->uid));
+ $result = (bool)$query->execute([$target, $targetHash, $sourceHash, $this->uid]);
+
+ $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->userManager->get($this->uid)));
return $result;
}
- public function removeShare($mountPoint) {
-
- $mountPointObj = $this->mountManager->find($mountPoint);
+ public function removeShare($mountPoint): bool {
+ try {
+ $mountPointObj = $this->mountManager->find($mountPoint);
+ } catch (NotFoundException $e) {
+ $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);
$hash = md5($mountPoint);
- $getShare = $this->connection->prepare('
- SELECT `remote`, `share_token`, `remote_id`
- FROM `*PREFIX*share_external`
- WHERE `mountpoint_hash` = ? AND `user` = ?');
- $result = $getShare->execute(array($hash, $this->uid));
-
- if ($result) {
- try {
- $share = $getShare->fetch();
- $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
- } catch (\Exception $e) {
- // if we fail to notify the remote (probably cause the remote is down)
- // we still want the share to be gone to prevent undeletable remotes
+ try {
+ $getShare = $this->connection->prepare('
+ SELECT `remote`, `share_token`, `remote_id`, `share_type`, `id`
+ FROM `*PREFIX*share_external`
+ WHERE `mountpoint_hash` = ? AND `user` = ?');
+ $result = $getShare->execute([$hash, $this->uid]);
+ $share = $result->fetch();
+ $result->closeCursor();
+ if ($share !== false && (int)$share['share_type'] === IShare::TYPE_USER) {
+ try {
+ $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
+ } catch (\Throwable $e) {
+ // if we fail to notify the remote (probably cause the remote is down)
+ // we still want the share to be gone to prevent undeletable remotes
+ }
+
+ $query = $this->connection->prepare('
+ DELETE FROM `*PREFIX*share_external`
+ WHERE `id` = ?
+ ');
+ $deleteResult = $query->execute([(int)$share['id']]);
+ $deleteResult->closeCursor();
+ } elseif ($share !== false && (int)$share['share_type'] === IShare::TYPE_GROUP) {
+ $this->updateAccepted((int)$share['id'], false);
}
- }
- $getShare->closeCursor();
-
- $query = $this->connection->prepare('
- DELETE FROM `*PREFIX*share_external`
- WHERE `mountpoint_hash` = ?
- AND `user` = ?
- ');
- $result = (bool)$query->execute(array($hash, $this->uid));
- if($result) {
$this->removeReShares($id);
+ } catch (\Doctrine\DBAL\Exception $ex) {
+ $this->logger->emergency('Could not update share', ['exception' => $ex]);
+ return false;
}
- return $result;
+ return true;
}
/**
@@ -409,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();
@@ -422,33 +682,86 @@ class Manager {
* remove all shares for user $uid if the user was deleted
*
* @param string $uid
- * @return bool
*/
- public function removeUserShares($uid) {
- $getShare = $this->connection->prepare('
- SELECT `remote`, `share_token`, `remote_id`
- FROM `*PREFIX*share_external`
- WHERE `user` = ?');
- $result = $getShare->execute(array($uid));
-
- if ($result) {
- $shares = $getShare->fetchAll();
- foreach($shares as $share) {
+ public function removeUserShares($uid): bool {
+ try {
+ // TODO: use query builder
+ $getShare = $this->connection->prepare('
+ SELECT `id`, `remote`, `share_type`, `share_token`, `remote_id`
+ FROM `*PREFIX*share_external`
+ WHERE `user` = ?
+ AND `share_type` = ?');
+ $result = $getShare->execute([$uid, IShare::TYPE_USER]);
+ $shares = $result->fetchAll();
+ $result->closeCursor();
+
+ foreach ($shares as $share) {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
}
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->delete('share_external')
+ // user field can specify a user or a group
+ ->where($qb->expr()->eq('user', $qb->createNamedParameter($uid)))
+ ->andWhere(
+ $qb->expr()->orX(
+ // delete direct shares
+ $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_USER)),
+ // delete sub-shares of group shares for that user
+ $qb->expr()->andX(
+ $qb->expr()->eq('share_type', $qb->expr()->literal(IShare::TYPE_GROUP)),
+ $qb->expr()->neq('parent', $qb->expr()->literal(-1)),
+ )
+ )
+ );
+ $qb->execute();
+ } catch (\Doctrine\DBAL\Exception $ex) {
+ $this->logger->emergency('Could not delete user shares', ['exception' => $ex]);
+ return false;
}
- $query = $this->connection->prepare('
- DELETE FROM `*PREFIX*share_external`
- WHERE `user` = ?
- ');
- return (bool)$query->execute(array($uid));
+ return true;
+ }
+
+ public function removeGroupShares($gid): bool {
+ try {
+ $getShare = $this->connection->prepare('
+ SELECT `id`, `remote`, `share_type`, `share_token`, `remote_id`
+ FROM `*PREFIX*share_external`
+ WHERE `user` = ?
+ AND `share_type` = ?');
+ $result = $getShare->execute([$gid, IShare::TYPE_GROUP]);
+ $shares = $result->fetchAll();
+ $result->closeCursor();
+
+ $deletedGroupShares = [];
+ $qb = $this->connection->getQueryBuilder();
+ // delete group share entry and matching sub-entries
+ $qb->delete('share_external')
+ ->where(
+ $qb->expr()->orX(
+ $qb->expr()->eq('id', $qb->createParameter('share_id')),
+ $qb->expr()->eq('parent', $qb->createParameter('share_parent_id'))
+ )
+ );
+
+ foreach ($shares as $share) {
+ $qb->setParameter('share_id', $share['id']);
+ $qb->setParameter('share_parent_id', $share['id']);
+ $qb->execute();
+ }
+ } catch (\Doctrine\DBAL\Exception $ex) {
+ $this->logger->emergency('Could not delete user shares', ['exception' => $ex]);
+ return false;
+ }
+
+ return true;
}
/**
* 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);
@@ -457,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);
@@ -469,22 +782,57 @@ 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) {
- $query = 'SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`
- FROM `*PREFIX*share_external`
- WHERE `user` = ?';
- $parameters = [$this->uid];
- if (!is_null($accepted)) {
- $query .= ' AND `accepted` = ?';
- $parameters[] = (int) $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 = [];
+ foreach ($groups as $group) {
+ $userGroups[] = $group->getGID();
}
- $query .= ' ORDER BY `id` ASC';
- $shares = $this->connection->prepare($query);
- $result = $shares->execute($parameters);
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('id', 'share_type', 'parent', 'remote', 'remote_id', 'share_token', 'name', 'owner', 'user', 'mountpoint', 'accepted')
+ ->from('share_external')
+ ->where(
+ $qb->expr()->orX(
+ $qb->expr()->eq('user', $qb->createNamedParameter($this->uid)),
+ $qb->expr()->in(
+ 'user',
+ $qb->createNamedParameter($userGroups, IQueryBuilder::PARAM_STR_ARRAY)
+ )
+ )
+ )
+ ->orderBy('id', 'ASC');
- return $result ? $shares->fetchAll() : [];
+ try {
+ $result = $qb->execute();
+ $shares = $result->fetchAll();
+ $result->closeCursor();
+
+ // remove parent group share entry if we have a specific user share entry for the user
+ $toRemove = [];
+ foreach ($shares as $share) {
+ if ((int)$share['share_type'] === IShare::TYPE_GROUP && (int)$share['parent'] > 0) {
+ $toRemove[] = $share['parent'];
+ }
+ }
+ $shares = array_filter($shares, function ($share) use ($toRemove) {
+ return !in_array($share['id'], $toRemove, true);
+ });
+
+ if (!is_null($accepted)) {
+ $shares = array_filter($shares, function ($share) use ($accepted) {
+ return (bool)$share['accepted'] === $accepted;
+ });
+ }
+ return array_values($shares);
+ } catch (\Doctrine\DBAL\Exception $e) {
+ $this->logger->emergency('Error when retrieving shares', ['exception' => $e]);
+ return [];
+ }
}
}
diff --git a/apps/files_sharing/lib/External/Mount.php b/apps/files_sharing/lib/External/Mount.php
index e12f8823cd8..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 Björn Schießle <bjoern@schiessle.org>
- * @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: 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 {
+class Mount extends MountPoint implements MoveableMount, ISharedMountPoint {
/**
- * @var \OCA\Files_Sharing\External\Manager
- */
- protected $manager;
-
- /**
- * @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) {
- parent::__construct($storage, $mountpoint, $options, $loader);
- $this->manager = $manager;
+ public function __construct(
+ $storage,
+ $mountpoint,
+ $options,
+ protected $manager,
+ $loader = null,
+ ) {
+ parent::__construct($storage, $mountpoint, $options, $loader, null, null, MountProvider::class);
}
/**
@@ -61,16 +46,13 @@ 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);
}
/**
- * Get the type of mount point, used to distinguish things like shares and external storages
+ * Get the type of mount point, used to distinguish things like shares and external storage
* in the web interface
*
* @return string
diff --git a/apps/files_sharing/lib/External/MountProvider.php b/apps/files_sharing/lib/External/MountProvider.php
index 0b07569b307..a5781d5d35a 100644
--- a/apps/files_sharing/lib/External/MountProvider.php
+++ b/apps/files_sharing/lib/External/MountProvider.php
@@ -1,41 +1,23 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @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\IUser;
+use OCP\Server;
class MountProvider implements IMountProvider {
- const STORAGE = '\OCA\Files_Sharing\External\Storage';
-
- /**
- * @var \OCP\IDBConnection
- */
- private $connection;
+ public const STORAGE = '\OCA\Files_Sharing\External\Storage';
/**
* @var callable
@@ -43,19 +25,16 @@ class MountProvider implements IMountProvider {
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) {
@@ -65,24 +44,25 @@ class MountProvider implements IMountProvider {
$mountPoint = '/' . $user->getUID() . '/files/' . ltrim($data['mountpoint'], '/');
$data['mountpoint'] = $mountPoint;
$data['cloudId'] = $this->cloudIdManager->getCloudId($data['owner'], $data['remote']);
- $data['certificateManager'] = \OC::$server->getCertificateManager($user->getUID());
- $data['HttpClientService'] = \OC::$server->getHTTPClientService();
+ $data['certificateManager'] = \OC::$server->getCertificateManager();
+ $data['HttpClientService'] = Server::get(IClientService::class);
return new Mount(self::STORAGE, $mountPoint, $data, $manager, $storageFactory);
}
public function getMountsForUser(IUser $user, IStorageFactory $loader) {
- $query = $this->connection->prepare('
- SELECT `remote`, `share_token`, `password`, `mountpoint`, `owner`
- FROM `*PREFIX*share_external`
- WHERE `user` = ? AND `accepted` = ?
- ');
- $query->execute([$user->getUID(), 1]);
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('remote', 'share_token', 'password', 'mountpoint', 'owner')
+ ->from('share_external')
+ ->where($qb->expr()->eq('user', $qb->createNamedParameter($user->getUID())))
+ ->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT)));
+ $result = $qb->executeQuery();
$mounts = [];
- while ($row = $query->fetch()) {
+ while ($row = $result->fetch()) {
$row['manager'] = $this;
$row['token'] = $row['share_token'];
$mounts[] = $this->getMount($user, $row, $loader);
}
+ $result->closeCursor();
return $mounts;
}
}
diff --git a/apps/files_sharing/lib/External/Scanner.php b/apps/files_sharing/lib/External/Scanner.php
index a0443338114..0d57248595b 100644
--- a/apps/files_sharing/lib/External/Scanner.php
+++ b/apps/files_sharing/lib/External/Scanner.php
@@ -1,47 +1,25 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Olivier Paroz <github@oparoz.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Vincent Petry <pvince81@owncloud.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;
- /** {@inheritDoc} */
public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
- if(!$this->storage->remoteIsOwnCloud()) {
- return parent::scan($path, $recursive, $recursive, $lock);
- }
-
- $this->scanAll();
+ // Disable locking for federated shares
+ parent::scan($path, $recursive, $reuse, false);
}
/**
@@ -53,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 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) {
+ 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) {
@@ -73,56 +52,4 @@ class Scanner extends \OC\Files\Cache\Scanner {
$this->storage->checkStorageAvailability();
}
}
-
- /**
- * Checks the remote share for changes.
- * If changes are available, scan them and update
- * the cache.
- * @throws NotFoundException
- * @throws StorageInvalidException
- * @throws \Exception
- */
- public function scanAll() {
- try {
- $data = $this->storage->getShareInfo();
- } catch (\Exception $e) {
- $this->storage->checkStorageAvailability();
- throw new \Exception(
- 'Error while scanning remote share: "' .
- $this->storage->getRemote() . '" ' .
- $e->getMessage()
- );
- }
- if ($data['status'] === 'success') {
- $this->addResult($data['data'], '');
- } else {
- throw new \Exception(
- 'Error while scanning remote share: "' .
- $this->storage->getRemote() . '"'
- );
- }
- }
-
- /**
- * @param array $data
- * @param string $path
- */
- private function addResult($data, $path) {
- $id = $this->cache->put($path, $data);
- if (isset($data['children'])) {
- $children = [];
- foreach ($data['children'] as $child) {
- $children[$child['name']] = true;
- $this->addResult($child, ltrim($path . '/' . $child['name'], '/'));
- }
-
- $existingCache = $this->cache->getFolderContentsById($id);
- foreach ($existingCache as $existingChild) {
- // if an existing child is not in the new data, remove it
- if (!isset($children[$existingChild['name']])) {
- $this->cache->remove(ltrim($path . '/' . $existingChild['name'], '/'));
- }
- }
- }
- }
}
diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php
index 638f82f7027..a9781b91a6c 100644
--- a/apps/files_sharing/lib/External/Storage.php
+++ b/apps/files_sharing/lib/External/Storage.php
@@ -1,97 +1,104 @@
<?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 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 <pvince81@owncloud.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;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
+use GuzzleHttp\Exception\RequestException;
use OC\Files\Storage\DAV;
use OC\ForbiddenException;
+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;
-
-class Storage extends DAV implements ISharedStorage {
- /** @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;
+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 {
+ 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;
/**
- * @var \OCA\Files_Sharing\External\Manager
+ * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options
*/
- private $manager;
-
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);
- list($protocol, $remote) = explode('://', $this->cloudId->getRemote());
- if (strpos($remote, '/')) {
- list($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();
}
- $secure = $protocol === 'https';
- $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
- $webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
- $root = rtrim($root, '/') . $webDavEndpoint;
+
+ $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;
+ }
+
$this->mountPoint = $options['mountpoint'];
$this->token = $options['token'];
- parent::__construct(array(
- '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;
}
@@ -102,66 +109,49 @@ class Storage extends DAV implements ISharedStorage {
return $this->watcher;
}
- public function getRemoteUser() {
+ public function getRemoteUser(): string {
return $this->cloudId->getUser();
}
- public function getRemote() {
+ public function getRemote(): string {
return $this->cloudId->getRemote();
}
- public function getMountPoint() {
+ public function getMountPoint(): string {
return $this->mountPoint;
}
- public function getToken() {
+ public function getToken(): string {
return $this->token;
}
- public function getPassword() {
+ public function getPassword(): ?string {
return $this->password;
}
- /**
- * @brief 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) {
@@ -181,7 +171,7 @@ class Storage extends DAV implements ISharedStorage {
}
}
- public function test() {
+ public function test(): bool {
try {
return parent::test();
} catch (StorageInvalidException $e) {
@@ -199,13 +189,13 @@ class Storage extends DAV implements ISharedStorage {
* 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()) {
@@ -214,26 +204,24 @@ class Storage extends DAV implements ISharedStorage {
// we remove the invalid storage
$this->manager->removeShare($this->mountPoint);
$this->manager->getMountManager()->removeMount($this->mountPoint);
- throw new StorageInvalidException();
+ 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();
+ 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();
+ throw new StorageInvalidException('Auth error when getting remote share');
} catch (\GuzzleHttp\Exception\ConnectException $e) {
- throw new StorageNotAvailableException();
+ throw new StorageNotAvailableException('Failed to connect to remote instance', 0, $e);
} catch (\GuzzleHttp\Exception\RequestException $e) {
- throw new StorageNotAvailableException();
- } catch (\Exception $e) {
- throw $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 {
@@ -242,56 +230,49 @@ class Storage extends DAV implements ISharedStorage {
}
/**
- * check if the configured remote is a valid federated share provider
+ * Check if the configured remote is a valid federated share provider
*
* @return bool
*/
- protected function testRemote() {
+ 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;
}
}
- /**
- * @param string $url
- * @return bool
- */
- private function testRemoteUrl($url) {
+ 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) {
+ } catch (ConnectException|ClientException|RequestException $e) {
$returnValue = false;
+ $this->logger->warning('Failed to test remote URL', ['exception' => $e]);
}
- $cache->set($url, $returnValue, 60*60*24);
+ $cache->set($url, $returnValue, 60 * 60 * 24);
return $returnValue;
}
/**
- * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
- * standardized. Let's use this to detect whether to use it.
+ * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing
+ * features are not standardized.
*
- * @return bool
+ * @throws LocalServerException
*/
- public function remoteIsOwnCloud() {
- if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
+ public function remoteIsOwnCloud(): bool {
+ if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
return false;
}
return true;
@@ -303,27 +284,33 @@ class Storage extends DAV implements ISharedStorage {
* @throws NotFoundException
* @throws \Exception
*/
- public function getShareInfo() {
+ public function getShareInfo(int $depth = -1) {
$remote = $this->getRemote();
$token = $this->getToken();
$password = $this->getPassword();
- // If remote is not an ownCloud do not try to get any share info
- if(!$this->remoteIsOwnCloud()) {
- return ['status' => 'unsupported'];
+ try {
+ // If remote is not an ownCloud do not try to get any share info
+ if (!$this->remoteIsOwnCloud()) {
+ return ['status' => 'unsupported'];
+ }
+ } catch (LocalServerException $e) {
+ // throw this to be on the safe side: the share will still be visible
+ // in the UI in case the failure is intermittent, and the user will
+ // be able to decide whether to remove it if it's really gone
+ throw new StorageNotAvailableException();
}
$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();
}
@@ -339,31 +326,104 @@ class Storage extends DAV implements ISharedStorage {
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) & \OCP\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 (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
- $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
+ 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 ($ocsPermissions !== null) {
+ $permissions = (int)$ocsPermissions;
+ } elseif ($ocmPermissions !== null) {
+ // permissions provided by the OCM API
+ $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
- if ($this->is_dir($path)) {
- $permissions = \OCP\Constants::PERMISSION_ALL;
- } else {
- $permissions = \OCP\Constants::PERMISSION_ALL & ~\OCP\Constants::PERMISSION_CREATE;
+ $permissions = $this->getDefaultPermissions($path);
+ }
+
+ return $permissions;
+ }
+
+ public function needsPartFile(): bool {
+ return false;
+ }
+
+ /**
+ * Translate OCM Permissions to Nextcloud permissions
+ *
+ * @param string $ocmPermissions json encoded OCM permissions
+ * @param string $path path to file
+ * @return int
+ */
+ protected function ocmPermissions2ncPermissions(string $ocmPermissions, string $path): int {
+ try {
+ $ocmPermissions = json_decode($ocmPermissions);
+ $ncPermissions = 0;
+ foreach ($ocmPermissions as $permission) {
+ switch (strtolower($permission)) {
+ case 'read':
+ $ncPermissions += Constants::PERMISSION_READ;
+ break;
+ case 'write':
+ $ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
+ break;
+ case 'share':
+ $ncPermissions += Constants::PERMISSION_SHARE;
+ break;
+ default:
+ throw new \Exception();
+ }
}
+ } catch (\Exception $e) {
+ $ncPermissions = $this->getDefaultPermissions($path);
+ }
+
+ return $ncPermissions;
+ }
+
+ /**
+ * Calculate the default permissions in case no permissions are provided
+ */
+ protected function getDefaultPermissions(string $path): int {
+ if ($this->is_dir($path)) {
+ $permissions = Constants::PERMISSION_ALL;
+ } else {
+ $permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
}
return $permissions;
}
+ 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 e733e166798..f3616feabba 100644
--- a/apps/files_sharing/lib/External/Watcher.php
+++ b/apps/files_sharing/lib/External/Watcher.php
@@ -1,25 +1,10 @@
<?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;
class Watcher extends \OC\Files\Cache\Watcher {
diff --git a/apps/files_sharing/lib/Helper.php b/apps/files_sharing/lib/Helper.php
index c8f46fa8132..92e874b73db 100644
--- a/apps/files_sharing/lib/Helper.php
+++ b/apps/files_sharing/lib/Helper.php
@@ -1,236 +1,42 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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 Vincent Petry <pvince81@owncloud.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 OCP\Files\NotFoundException;
-use OCP\Share\Exceptions\ShareNotFound;
-use OCP\User;
+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');
- }
-
- /**
- * Sets up the filesystem and user for public sharing
- * @param string $token string share token
- * @param string $relativePath optional path relative to the share
- * @param string $password optional password
- * @return array
- */
- public static function setupFromToken($token, $relativePath = null, $password = null) {
- \OC_User::setIncognitoMode(true);
-
- $shareManager = \OC::$server->getShareManager();
-
- try {
- $share = $shareManager->getShareByToken($token);
- } catch (ShareNotFound $e) {
- \OC_Response::setStatus(404);
- \OCP\Util::writeLog('core-preview', 'Passed token parameter is not valid', \OCP\Util::DEBUG);
- exit;
- }
-
- \OCP\JSON::checkUserExists($share->getShareOwner());
- \OC_Util::tearDownFS();
- \OC_Util::setupFS($share->getShareOwner());
-
-
- try {
- $path = Filesystem::getPath($share->getNodeId());
- } catch (NotFoundException $e) {
- \OCP\Util::writeLog('share', 'could not resolve linkItem', \OCP\Util::DEBUG);
- \OC_Response::setStatus(404);
- \OCP\JSON::error(array('success' => false));
- exit();
- }
-
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK && $share->getPassword() !== null) {
- if (!self::authenticate($share, $password)) {
- \OC_Response::setStatus(403);
- \OCP\JSON::error(array('success' => false));
- exit();
- }
- }
-
- $basePath = $path;
-
- if ($relativePath !== null && Filesystem::isReadable($basePath . $relativePath)) {
- $path .= Filesystem::normalizePath($relativePath);
- }
-
- return array(
- 'share' => $share,
- 'basePath' => $basePath,
- 'realPath' => $path
- );
- }
-
- /**
- * Authenticate link item with the given password
- * or with the session if no password was given.
- * @param \OCP\Share\IShare $share
- * @param string $password optional password
- *
- * @return boolean true if authorized, false otherwise
- */
- public static function authenticate($share, $password = null) {
- $shareManager = \OC::$server->getShareManager();
-
- if ($password !== null) {
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) {
- if ($shareManager->checkPassword($share, $password)) {
- \OC::$server->getSession()->set('public_link_authenticated', (string)$share->getId());
- return true;
- }
- }
- } else {
- // not authenticated ?
- if (\OC::$server->getSession()->exists('public_link_authenticated')
- && \OC::$server->getSession()->get('public_link_authenticated') !== (string)$share->getId()) {
- return true;
- }
- }
- return false;
- }
-
- public static function getSharesFromItem($target) {
- $result = array();
- $owner = Filesystem::getOwner($target);
- Filesystem::initMountPoints($owner);
- $info = Filesystem::getFileInfo($target);
- $ownerView = new View('/'.$owner.'/files');
- if ( $owner !== User::getUser() ) {
- $path = $ownerView->getPath($info['fileid']);
- } else {
- $path = $target;
- }
-
-
- $ids = array();
- while ($path !== dirname($path)) {
- $info = $ownerView->getFileInfo($path);
- if ($info instanceof \OC\Files\FileInfo) {
- $ids[] = $info['fileid'];
- } else {
- \OCP\Util::writeLog('sharing', 'No fileinfo available for: ' . $path, \OCP\Util::WARN);
- }
- $path = dirname($path);
- }
-
- if (!empty($ids)) {
-
- $idList = array_chunk($ids, 99, true);
-
- foreach ($idList as $subList) {
- $statement = "SELECT `share_with`, `share_type`, `file_target` FROM `*PREFIX*share` WHERE `file_source` IN (" . implode(',', $subList) . ") AND `share_type` IN (0, 1, 2)";
- $query = \OCP\DB::prepare($statement);
- $r = $query->execute();
- $result = array_merge($result, $r->fetchAll());
- }
- }
-
- return $result;
- }
-
- /**
- * get the UID of the owner of the file and the path to the file relative to
- * owners files folder
- *
- * @param $filename
- * @return array
- * @throws \OC\User\NoUserException
- */
- public static function getUidAndFilename($filename) {
- $uid = Filesystem::getOwner($filename);
- $userManager = \OC::$server->getUserManager();
- // if the user with the UID doesn't exists, e.g. because the UID points
- // to a remote user with a federated cloud ID we use the current logged-in
- // user. We need a valid local user to create the share
- if (!$userManager->userExists($uid)) {
- $uid = User::getUser();
- }
- Filesystem::initMountPoints($uid);
- if ( $uid !== User::getUser() ) {
- $info = Filesystem::getFileInfo($filename);
- $ownerView = new View('/'.$uid.'/files');
- try {
- $filename = $ownerView->getPath($info['fileid']);
- } catch (NotFoundException $e) {
- $filename = null;
- }
- }
- return [$uid, $filename];
- }
-
- /**
- * Format a path to be relative to the /user/files/ directory
- * @param string $path the absolute path
- * @return string e.g. turns '/admin/files/test.txt' into 'test.txt'
- */
- public static function stripUserFilesPath($path) {
- $trimmed = ltrim($path, '/');
- $split = explode('/', $trimmed);
-
- // it is not a file relative to data/user/files
- if (count($split) < 3 || $split[1] !== 'files') {
- return false;
- }
-
- $sliced = array_slice($split, 2);
- $relPath = implode('/', $sliced);
-
- return $relPath;
+ 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++;
}
@@ -240,16 +46,29 @@ class Helper {
/**
* get default share folder
*
- * @param \OC\Files\View
+ * @param View|null $view
+ * @param string|null $userId
* @return string
*/
- public static function getShareFolder($view = null) {
+ public static function getShareFolder(?View $view = null, ?string $userId = null): string {
if ($view === null) {
$view = Filesystem::getView();
}
- $shareFolder = \OC::$server->getConfig()->getSystemValue('share_folder', '/');
+
+ $config = Server::get(IConfig::class);
+ $systemDefault = $config->getSystemValue('share_folder', '/');
+ $allowCustomShareFolder = $config->getSystemValueBool('sharing.allow_custom_share_folder', true);
+
+ // Init custom shareFolder
+ $shareFolder = $systemDefault;
+ if ($userId !== null && $allowCustomShareFolder) {
+ $shareFolder = $config->getUserValue($userId, Application::APP_ID, 'share_folder', $systemDefault);
+ }
+
+ // Verify and sanitize path
$shareFolder = Filesystem::normalizePath($shareFolder);
+ // Init path if folder doesn't exists
if (!$view->file_exists($shareFolder)) {
$dir = '';
$subdirs = explode('/', $shareFolder);
@@ -262,7 +81,6 @@ class Helper {
}
return $shareFolder;
-
}
/**
@@ -271,7 +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 97b1079f1eb..e90b9f5c23d 100644
--- a/apps/files_sharing/lib/Hooks.php
+++ b/apps/files_sharing/lib/Hooks.php
@@ -1,59 +1,32 @@
<?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 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 OCA\FederatedFileSharing\DiscoveryManager;
+use OC\Files\View;
+use OCP\Server;
class Hooks {
-
public static function deleteUser($params) {
- $manager = new External\Manager(
- \OC::$server->getDatabaseConnection(),
- \OC\Files\Filesystem::getMountManager(),
- \OC\Files\Filesystem::getLoader(),
- \OC::$server->getHTTPClientService(),
- \OC::$server->getNotificationManager(),
- \OC::$server->query(\OCP\OCS\IDiscoveryService::class),
- $params['uid']);
+ $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('OCA\Files_Sharing\ISharedStorage')) {
+ if ($mount->getStorage()->instanceOfStorage(ISharedStorage::class)) {
$mountPoint = $mount->getMountPoint();
$view->unlink($mountPoint);
}
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 b8abd5821d7..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 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: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_Sharing;
-interface ISharedStorage{
+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/LoadAdditionalListener.php b/apps/files_sharing/lib/Listener/LoadAdditionalListener.php
new file mode 100644
index 00000000000..b089c8309b7
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/LoadAdditionalListener.php
@@ -0,0 +1,35 @@
+<?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\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 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
new file mode 100644
index 00000000000..17fee71978f
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/LoadSidebarListener.php
@@ -0,0 +1,50 @@
+<?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\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
new file mode 100644
index 00000000000..7b11a472492
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/ShareInteractionListener.php
@@ -0,0 +1,71 @@
+<?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 OCP\Contacts\Events\ContactInteractedWithEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\EventDispatcher\IEventListener;
+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,
+ IShare::TYPE_EMAIL,
+ IShare::TYPE_REMOTE,
+ ];
+
+ public function __construct(
+ private IEventDispatcher $dispatcher,
+ private IUserManager $userManager,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof ShareCreatedEvent)) {
+ // Unrelated
+ return;
+ }
+
+ $share = $event->getShare();
+ if (!in_array($share->getShareType(), self::SUPPORTED_SHARE_TYPES, true)) {
+ $this->logger->debug('Share type does not allow to emit interaction event');
+ return;
+ }
+ $actor = $this->userManager->get($share->getSharedBy());
+ $sharedWith = $this->userManager->get($share->getSharedWith());
+ if ($actor === null) {
+ $this->logger->warning('Share was not created by a user, can\'t emit interaction event');
+ return;
+ }
+ $interactionEvent = new ContactInteractedWithEvent($actor);
+ switch ($share->getShareType()) {
+ case IShare::TYPE_USER:
+ $interactionEvent->setUid($share->getSharedWith());
+ if ($sharedWith !== null) {
+ $interactionEvent->setFederatedCloudId($sharedWith->getCloudId());
+ }
+ break;
+ case IShare::TYPE_EMAIL:
+ $interactionEvent->setEmail($share->getSharedWith());
+ break;
+ case IShare::TYPE_REMOTE:
+ $interactionEvent->setFederatedCloudId($share->getSharedWith());
+ break;
+ }
+
+ $this->dispatcher->dispatchTyped($interactionEvent);
+ }
+}
diff --git a/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.php
new file mode 100644
index 00000000000..281c96ca5e7
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/UserAddedToGroupListener.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\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Group\Events\UserAddedEvent;
+use OCP\IConfig;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+
+/** @template-implements IEventListener<UserAddedEvent> */
+class UserAddedToGroupListener implements IEventListener {
+
+ public function __construct(
+ private IManager $shareManager,
+ private IConfig $config,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof UserAddedEvent)) {
+ return;
+ }
+
+ $user = $event->getUser();
+ $group = $event->getGroup();
+
+ // This user doesn't have autoaccept so we can skip it all
+ if (!$this->hasAutoAccept($user->getUID())) {
+ return;
+ }
+
+ // Get all group shares this user has access to now to filter later
+ $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1);
+
+ foreach ($shares as $share) {
+ // If this is not the new group we can skip it
+ if ($share->getSharedWith() !== $group->getGID()) {
+ continue;
+ }
+
+ // Accept the share if needed
+ $this->shareManager->acceptShare($share, $user->getUID());
+ }
+ }
+
+
+ private function hasAutoAccept(string $userId): bool {
+ $defaultAcceptSystemConfig = $this->config->getSystemValueBool('sharing.enable_share_accept', false) ? 'no' : 'yes';
+ $acceptDefault = $this->config->getUserValue($userId, Application::APP_ID, 'default_accept', $defaultAcceptSystemConfig) === 'yes';
+ return (!$this->config->getSystemValueBool('sharing.force_share_accept', false) && $acceptDefault);
+ }
+}
diff --git a/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php b/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php
new file mode 100644
index 00000000000..0ac447436bd
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php
@@ -0,0 +1,60 @@
+<?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\AppInfo\Application;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\Share\Events\ShareCreatedEvent;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+
+/** @template-implements IEventListener<ShareCreatedEvent> */
+class UserShareAcceptanceListener implements IEventListener {
+
+ public function __construct(
+ private IConfig $config,
+ private IManager $shareManager,
+ private IGroupManager $groupManager,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof ShareCreatedEvent)) {
+ return;
+ }
+
+ $share = $event->getShare();
+
+ if ($share->getShareType() === IShare::TYPE_USER) {
+ $this->handleAutoAccept($share, $share->getSharedWith());
+ } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
+ $group = $this->groupManager->get($share->getSharedWith());
+
+ if ($group === null) {
+ return;
+ }
+
+ $users = $group->getUsers();
+ foreach ($users as $user) {
+ $this->handleAutoAccept($share, $user->getUID());
+ }
+ }
+ }
+
+ private function handleAutoAccept(IShare $share, string $userId) {
+ $defaultAcceptSystemConfig = $this->config->getSystemValueBool('sharing.enable_share_accept', false) ? 'no' : 'yes';
+ $acceptDefault = $this->config->getUserValue($userId, Application::APP_ID, 'default_accept', $defaultAcceptSystemConfig) === 'yes';
+ if (!$this->config->getSystemValueBool('sharing.force_share_accept', false) && $acceptDefault) {
+ $this->shareManager->acceptShare($share, $userId);
+ }
+ }
+}
diff --git a/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php b/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php
index faf9ef4756d..6671a78efff 100644
--- a/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php
+++ b/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php
@@ -1,26 +1,8 @@
<?php
+
/**
- *
- *
- * @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 74a5db4f308..e96940979bf 100644
--- a/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php
+++ b/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php
@@ -1,29 +1,11 @@
<?php
+
/**
- *
- *
- * @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;
-use OCA\FederatedFileSharing\FederatedShareProvider;
use OCA\Files_Sharing\Controller\ShareInfoController;
use OCA\Files_Sharing\Exceptions\S2SException;
use OCP\AppFramework\Controller;
@@ -34,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 c96b559daf5..8ea2eb59d73 100644
--- a/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php
+++ b/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php
@@ -1,45 +1,26 @@
<?php
+
+declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @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;
use OCA\Files_Sharing\Controller\ExternalSharesController;
-use OCA\Files_Sharing\Controller\ShareController;
+use OCA\Files_Sharing\Exceptions\S2SException;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Middleware;
+use OCP\AppFramework\Utility\IControllerMethodReflector;
use OCP\Files\NotFoundException;
use OCP\IConfig;
-use OCP\AppFramework\Utility\IControllerMethodReflector;
-use OCA\Files_Sharing\Exceptions\S2SException;
-use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\Share\IManager;
-use OCP\Share\Exceptions\ShareNotFound;
/**
* Checks whether the "sharing check" is enabled
@@ -48,40 +29,14 @@ use OCP\Share\Exceptions\ShareNotFound;
*/
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;
-
- /***
- * @param string $appName
- * @param IConfig $config
- * @param IAppManager $appManager
- * @param IControllerMethodReflector $reflector
- * @param IManager $shareManager
- * @param IRequest $request
- */
- public function __construct($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,23 +46,15 @@ class SharingCheckMiddleware extends Middleware {
* @param string $methodName
* @throws NotFoundException
* @throws S2SException
- * @throws ShareNotFound
*/
- public function beforeController($controller, $methodName) {
- if(!$this->isSharingEnabled()) {
+ public function beforeController($controller, $methodName): void {
+ if (!$this->isSharingEnabled()) {
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');
- } else if ($controller instanceof ShareController) {
- $token = $this->request->getParam('token');
- $share = $this->shareManager->getShareByToken($token);
- if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK
- && !$this->isLinkSharingEnabled()) {
- throw new NotFoundException('Link sharing is disabled');
- }
}
}
@@ -117,15 +64,15 @@ class SharingCheckMiddleware extends Middleware {
* @param Controller $controller
* @param string $methodName
* @param \Exception $exception
- * @return NotFoundResponse
+ * @return Response
* @throws \Exception
*/
- public function afterException($controller, $methodName, \Exception $exception) {
- if(is_a($exception, '\OCP\Files\NotFoundException')) {
+ public function afterException($controller, $methodName, \Exception $exception): Response {
+ if (is_a($exception, NotFoundException::class)) {
return new NotFoundResponse();
}
- if (is_a($exception, '\OCA\Files_Sharing\Exceptions\S2SException')) {
+ if (is_a($exception, S2SException::class)) {
return new JSONResponse($exception->getMessage(), 405);
}
@@ -136,15 +83,14 @@ class SharingCheckMiddleware extends Middleware {
* Checks for externalshares controller
* @return bool
*/
- private function externalSharesChecks() {
-
- if (!$this->reflector->hasAnnotation('NoIncomingFederatedSharingRequired') &&
- $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') !== 'yes') {
+ private function externalSharesChecks(): bool {
+ 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;
}
@@ -155,32 +101,13 @@ class SharingCheckMiddleware extends Middleware {
* Check whether sharing is enabled
* @return bool
*/
- private function isSharingEnabled() {
+ private function isSharingEnabled(): bool {
// FIXME: This check is done here since the route is globally defined and not inside the files_sharing app
// Check whether the sharing application is enabled
- if(!$this->appManager->isEnabledForUser($this->appName)) {
+ if (!$this->appManager->isEnabledForUser($this->appName)) {
return false;
}
return true;
}
-
- /**
- * Check if link sharing is allowed
- * @return bool
- */
- private function isLinkSharingEnabled() {
- // Check if the shareAPI is enabled
- if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') {
- return false;
- }
-
- // Check whether public sharing is enabled
- if($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
- return false;
- }
-
- return true;
- }
-
}
diff --git a/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php
index 07f739fb702..3718306e380 100644
--- a/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php
+++ b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php
@@ -1,33 +1,16 @@
<?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;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
-use OCP\Share;
+use OCP\Share\IShare;
/**
* Class OwncloudGuestShareType
@@ -36,16 +19,10 @@ use OCP\Share;
*/
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,15 +45,14 @@ class OwncloudGuestShareType implements IRepairStep {
$query = $this->connection->getQueryBuilder();
$query->update('share')
- ->set('share_type', $query->createNamedParameter(Share::SHARE_TYPE_GUEST))
- ->where($query->expr()->eq('share_type', $query->createNamedParameter(Share::SHARE_TYPE_EMAIL)));
+ ->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 in_array($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
new file mode 100644
index 00000000000..4da6aad4b33
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/SetAcceptedStatus.php
@@ -0,0 +1,56 @@
+<?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\Migration;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Share\IShare;
+
+class SetAcceptedStatus implements IRepairStep {
+
+ public function __construct(
+ private IDBConnection $connection,
+ private IConfig $config,
+ ) {
+ }
+
+ /**
+ * Returns the step's name
+ *
+ * @return string
+ * @since 9.1.0
+ */
+ public function getName(): string {
+ return 'Set existing shares as accepted';
+ }
+
+ /**
+ * @param IOutput $output
+ */
+ public function run(IOutput $output): void {
+ if (!$this->shouldRun()) {
+ return;
+ }
+
+ $query = $this->connection->getQueryBuilder();
+ $query
+ ->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->executeStatement();
+ }
+
+ protected function shouldRun() {
+ $appVersion = $this->config->getAppValue('files_sharing', 'installed_version', '0.0.0');
+ return version_compare($appVersion, '1.10.1', '<');
+ }
+}
diff --git a/apps/files_sharing/lib/Migration/SetPasswordColumn.php b/apps/files_sharing/lib/Migration/SetPasswordColumn.php
index e8631485f88..f60af2817d4 100644
--- a/apps/files_sharing/lib/Migration/SetPasswordColumn.php
+++ b/apps/files_sharing/lib/Migration/SetPasswordColumn.php
@@ -1,33 +1,16 @@
<?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;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
-use OCP\Share;
+use OCP\Share\IShare;
/**
* Class SetPasswordColumn
@@ -36,16 +19,10 @@ use OCP\Share;
*/
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,
+ ) {
}
/**
@@ -70,7 +47,7 @@ class SetPasswordColumn implements IRepairStep {
$query
->update('share')
->set('password', 'share_with')
- ->where($query->expr()->eq('share_type', $query->createNamedParameter(Share::SHARE_TYPE_LINK)))
+ ->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_LINK)))
->andWhere($query->expr()->isNotNull('share_with'));
$result = $query->execute();
@@ -83,15 +60,13 @@ class SetPasswordColumn implements IRepairStep {
$clearQuery
->update('share')
->set('share_with', $clearQuery->createNamedParameter(null))
- ->where($clearQuery->expr()->eq('share_type', $clearQuery->createNamedParameter(Share::SHARE_TYPE_LINK)));
+ ->where($clearQuery->expr()->eq('share_type', $clearQuery->createNamedParameter(IShare::TYPE_LINK)));
$clearQuery->execute();
-
}
protected function shouldRun() {
$appVersion = $this->config->getAppValue('files_sharing', 'installed_version', '0.0.0');
return version_compare($appVersion, '1.4.0', '<');
}
-
}
diff --git a/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php
new file mode 100644
index 00000000000..c9fe840d422
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version11300Date20201120141438.php
@@ -0,0 +1,124 @@
+<?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\Migration;
+
+use Closure;
+use Doctrine\DBAL\Types\Type;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version11300Date20201120141438 extends SimpleMigrationStep {
+
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
+ }
+
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if (!$schema->hasTable('share_external')) {
+ $table = $schema->createTable('share_external');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('parent', Types::BIGINT, [
+ 'notnull' => false,
+ 'default' => -1,
+ ]);
+ $table->addColumn('share_type', Types::INTEGER, [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $table->addColumn('remote', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 512,
+ ]);
+ $table->addColumn('remote_id', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 255,
+ 'default' => '',
+ ]);
+ $table->addColumn('share_token', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('password', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 64,
+ ]);
+ $table->addColumn('name', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('owner', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('user', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('mountpoint', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 4000,
+ ]);
+ $table->addColumn('mountpoint_hash', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 32,
+ ]);
+ $table->addColumn('accepted', Types::INTEGER, [
+ 'notnull' => true,
+ 'length' => 4,
+ 'default' => 0,
+ ]);
+ $table->setPrimaryKey(['id']);
+ $table->addUniqueIndex(['user', 'mountpoint_hash'], 'sh_external_mp');
+ } else {
+ $table = $schema->getTable('share_external');
+ $remoteIdColumn = $table->getColumn('remote_id');
+ if ($remoteIdColumn && $remoteIdColumn->getType()->getName() !== Types::STRING) {
+ $remoteIdColumn->setNotnull(false);
+ $remoteIdColumn->setType(Type::getType(Types::STRING));
+ $remoteIdColumn->setOptions(['length' => 255]);
+ $remoteIdColumn->setDefault('');
+ }
+ if (!$table->hasColumn('parent')) {
+ $table->addColumn('parent', Types::BIGINT, [
+ 'notnull' => false,
+ 'default' => -1,
+ ]);
+ }
+ if (!$table->hasColumn('share_type')) {
+ $table->addColumn('share_type', Types::INTEGER, [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ }
+ if ($table->hasColumn('lastscan')) {
+ $table->dropColumn('lastscan');
+ }
+ }
+
+ return $schema;
+ }
+
+ public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->update('share_external')
+ ->set('remote_id', $qb->createNamedParameter(''))
+ ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter('-1')));
+ $qb->execute();
+ }
+}
diff --git a/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php b/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php
new file mode 100644
index 00000000000..9bd07a19802
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version21000Date20201223143245.php
@@ -0,0 +1,51 @@
+<?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\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version21000Date20201223143245 extends SimpleMigrationStep {
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable('share_external')) {
+ $table = $schema->getTable('share_external');
+ $changed = false;
+ if (!$table->hasColumn('parent')) {
+ $table->addColumn('parent', Types::BIGINT, [
+ 'notnull' => false,
+ 'default' => -1,
+ ]);
+ $changed = true;
+ }
+ if (!$table->hasColumn('share_type')) {
+ $table->addColumn('share_type', Types::INTEGER, [
+ 'notnull' => false,
+ 'length' => 4,
+ ]);
+ $changed = true;
+ }
+ if ($table->hasColumn('lastscan')) {
+ $table->dropColumn('lastscan');
+ $changed = true;
+ }
+
+ if ($changed) {
+ return $schema;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php b/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php
new file mode 100644
index 00000000000..e82fb4a72d5
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version22000Date20210216084241.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 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\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version22000Date20210216084241 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $table = $schema->getTable('share_external');
+ if ($table->hasIndex('sh_external_user')) {
+ $table->dropIndex('sh_external_user');
+ }
+
+ return $schema;
+ }
+}
diff --git a/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php b/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php
new file mode 100644
index 00000000000..75da1de1d83
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version24000Date20220208195521.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version24000Date20220208195521 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ $schema = $schemaClosure();
+ $table = $schema->getTable('share');
+ $table->addColumn('password_expiration_time', Types::DATETIME, [
+ 'notnull' => false,
+ ]);
+ return $schema;
+ }
+
+}
diff --git a/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php b/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php
new file mode 100644
index 00000000000..03985bd50c7
--- /dev/null
+++ b/apps/files_sharing/lib/Migration/Version24000Date20220404142216.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version24000Date20220404142216 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $table = $schema->getTable('share_external');
+ $column = $table->getColumn('name');
+ if ($column->getLength() < 4000) {
+ $column->setLength(4000);
+ return $schema;
+ }
+ return null;
+ }
+}
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 fd4c537210f..b7b0582493e 100644
--- a/apps/files_sharing/lib/MountProvider.php
+++ b/apps/files_sharing/lib/MountProvider.php
@@ -1,118 +1,148 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @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 <pvince81@owncloud.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\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;
-
- /**
- * @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) {
- $this->config = $config;
- $this->shareManager = $shareManager;
- $this->logger = $logger;
+ public function __construct(
+ protected IConfig $config,
+ protected IManager $shareManager,
+ protected LoggerInterface $logger,
+ protected IEventDispatcher $eventDispatcher,
+ protected ICacheFactory $cacheFactory,
+ protected IMountManager $mountManager,
+ ) {
}
-
/**
* 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 $storageFactory
- * @return \OCP\Files\Mount\IMountPoint[]
+ * @param IUser $user
+ * @param IStorageFactory $loader
+ * @return IMountPoint[]
*/
- public function getMountsForUser(IUser $user, IStorageFactory $storageFactory) {
-
- $shares = $this->shareManager->getSharedWith($user->getUID(), \OCP\Share::SHARE_TYPE_USER, null, -1);
- $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), \OCP\Share::SHARE_TYPE_GROUP, null, -1));
- $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), \OCP\Share::SHARE_TYPE_CIRCLE, null, -1));
+ public function getMountsForUser(IUser $user, IStorageFactory $loader) {
+ $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 {
- $mounts[] = new SharedMount(
+ /** @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)) {
+ continue;
+ }
+
+ $owner = $parentShare->getShareOwner();
+ 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
- 'superShare' => $share[0],
+ 'superShare' => $parentShare,
// children/component of the superShare
'groupedShares' => $share[1],
+ 'ownerView' => $ownerViews[$owner],
+ 'sharingDisabledForUser' => $sharingDisabledForUser
],
- $storageFactory
+ $loader,
+ $view,
+ $foldersExistCache,
+ $this->eventDispatcher,
+ $user,
+ ($shareId <= $maxValidatedShare),
);
+
+ $newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
+
+ $event = new ShareMountedEvent($mount);
+ $this->eventDispatcher->dispatchTyped($event);
+
+ $mounts[$mount->getMountPoint()] = $mount;
+ foreach ($event->getAdditionalMounts() as $additionalMount) {
+ $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_filter($mounts);
+ return array_values(array_filter($mounts));
}
/**
* 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 = [];
@@ -127,11 +157,13 @@ class MountProvider implements IMountProvider {
$result = [];
// sort by stime, the super share will be based on the least recent share
foreach ($tmp as &$tmp2) {
- @usort($tmp2, function($a, $b) {
- if ($a->getShareTime() <= $b->getShareTime()) {
- return -1;
+ @usort($tmp2, function ($a, $b) {
+ $aTime = $a->getShareTime()->getTimestamp();
+ $bTime = $b->getShareTime()->getTimestamp();
+ if ($aTime === $bTime) {
+ return $a->getId() < $b->getId() ? -1 : 1;
}
- return 1;
+ return $aTime < $bTime ? -1 : 1;
});
$result[] = $tmp2;
}
@@ -145,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;
@@ -166,14 +198,45 @@ class MountProvider implements IMountProvider {
$superShare->setId($shares[0]->getId())
->setShareOwner($shares[0]->getShareOwner())
->setNodeId($shares[0]->getNodeId())
+ ->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());
@@ -198,7 +261,9 @@ class MountProvider implements IMountProvider {
}
}
- $superShare->setPermissions($permissions);
+ $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
new file mode 100644
index 00000000000..1cf0f845e7a
--- /dev/null
+++ b/apps/files_sharing/lib/Notification/Listener.php
@@ -0,0 +1,102 @@
+<?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\Notification;
+
+use OCP\IGroup;
+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 {
+
+ public function __construct(
+ protected INotificationManager $notificationManager,
+ protected IShareManager $shareManager,
+ protected IGroupManager $groupManager,
+ ) {
+ }
+
+ public function shareNotification(ShareCreatedEvent $event): void {
+ $share = $event->getShare();
+ $notification = $this->instantiateNotification($share);
+
+ if ($share->getShareType() === IShare::TYPE_USER) {
+ $notification->setSubject(Notifier::INCOMING_USER_SHARE)
+ ->setUser($share->getSharedWith());
+ $this->notificationManager->notify($notification);
+ } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
+ $notification->setSubject(Notifier::INCOMING_GROUP_SHARE);
+ $group = $this->groupManager->get($share->getSharedWith());
+
+ foreach ($group->getUsers() as $user) {
+ if ($user->getUID() === $share->getShareOwner()
+ || $user->getUID() === $share->getSharedBy()) {
+ continue;
+ }
+
+ $notification->setUser($user->getUID());
+ $this->notificationManager->notify($notification);
+ }
+ }
+ }
+
+ /**
+ * @param GenericEvent $event
+ */
+ public function userAddedToGroup(GenericEvent $event): void {
+ /** @var IGroup $group */
+ $group = $event->getSubject();
+ /** @var IUser $user */
+ $user = $event->getArgument('user');
+
+ $offset = 0;
+ while (true) {
+ $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, 50, $offset);
+ if (empty($shares)) {
+ break;
+ }
+
+ foreach ($shares as $share) {
+ if ($share->getSharedWith() !== $group->getGID()) {
+ continue;
+ }
+
+ if ($user->getUID() === $share->getShareOwner()
+ || $user->getUID() === $share->getSharedBy()) {
+ continue;
+ }
+
+ $notification = $this->instantiateNotification($share);
+ $notification->setSubject(Notifier::INCOMING_GROUP_SHARE)
+ ->setUser($user->getUID());
+ $this->notificationManager->notify($notification);
+ }
+ $offset += 50;
+ }
+ }
+
+ /**
+ * @param IShare $share
+ * @return INotification
+ */
+ protected function instantiateNotification(IShare $share): INotification {
+ $notification = $this->notificationManager->createNotification();
+ $notification
+ ->setApp('files_sharing')
+ ->setObject('share', $share->getFullId())
+ ->setDateTime($share->getShareTime());
+
+ return $notification;
+ }
+}
diff --git a/apps/files_sharing/lib/Notification/Notifier.php b/apps/files_sharing/lib/Notification/Notifier.php
new file mode 100644
index 00000000000..e4434ef0b37
--- /dev/null
+++ b/apps/files_sharing/lib/Notification/Notifier.php
@@ -0,0 +1,223 @@
+<?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\Notification;
+
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+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;
+
+class Notifier implements INotifier {
+ public const INCOMING_USER_SHARE = 'incoming_user_share';
+ public const INCOMING_GROUP_SHARE = 'incoming_group_share';
+
+ public function __construct(
+ protected IFactory $l10nFactory,
+ private IManager $shareManager,
+ private IRootFolder $rootFolder,
+ protected IGroupManager $groupManager,
+ protected IUserManager $userManager,
+ protected IURLGenerator $url,
+ ) {
+ }
+
+ /**
+ * Identifier of the notifier, only use [a-z0-9_]
+ *
+ * @return string
+ * @since 17.0.0
+ */
+ public function getID(): string {
+ return 'files_sharing';
+ }
+
+ /**
+ * Human readable name describing the notifier
+ *
+ * @return string
+ * @since 17.0.0
+ */
+ public function getName(): string {
+ return $this->l10nFactory->get('files_sharing')->t('File sharing');
+ }
+
+ /**
+ * @param INotification $notification
+ * @param string $languageCode The code of the language that should be used to prepare the notification
+ * @return INotification
+ * @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 UnknownNotificationException('Unhandled app or subject');
+ }
+
+ $l = $this->l10nFactory->get('files_sharing', $languageCode);
+ $attemptId = $notification->getObjectId();
+
+ try {
+ $share = $this->shareManager->getShareById($attemptId, $notification->getUser());
+ } catch (ShareNotFound $e) {
+ 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 {
+ $notification = $this->parseShareInvitation($share, $notification, $l);
+ }
+ return $notification;
+ }
+
+ protected function parseShareExpiration(IShare $share, INotification $notification, IL10N $l): INotification {
+ $node = $share->getNode();
+ $userFolder = $this->rootFolder->getUserFolder($notification->getUser());
+ $path = $userFolder->getRelativePath($node->getPath());
+
+ $notification
+ ->setParsedSubject($l->t('Share will expire tomorrow'))
+ ->setRichMessage(
+ $l->t('Your share of {node} will expire tomorrow'),
+ [
+ 'node' => [
+ 'type' => 'file',
+ 'id' => (string)$node->getId(),
+ 'name' => $node->getName(),
+ 'path' => (string)$path,
+ ],
+ ]
+ );
+
+ return $notification;
+ }
+
+ protected function parseShareInvitation(IShare $share, INotification $notification, IL10N $l): INotification {
+ if ($share->getShareType() === IShare::TYPE_USER) {
+ if ($share->getStatus() !== IShare::STATUS_PENDING) {
+ throw new AlreadyProcessedException();
+ }
+ } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
+ if ($share->getStatus() !== IShare::STATUS_PENDING) {
+ throw new AlreadyProcessedException();
+ }
+ } else {
+ throw new UnknownNotificationException('Invalid share type');
+ }
+
+ switch ($notification->getSubject()) {
+ case self::INCOMING_USER_SHARE:
+ if ($share->getSharedWith() !== $notification->getUser()) {
+ throw new AlreadyProcessedException();
+ }
+
+ $sharer = $this->userManager->get($share->getSharedBy());
+ if (!$sharer instanceof IUser) {
+ throw new \InvalidArgumentException('Temporary failure');
+ }
+
+ $subject = $l->t('You received {share} as a share by {user}');
+ $subjectParameters = [
+ 'share' => [
+ 'type' => 'highlight',
+ 'id' => $notification->getObjectId(),
+ 'name' => $share->getTarget(),
+ ],
+ 'user' => [
+ 'type' => 'user',
+ 'id' => $sharer->getUID(),
+ 'name' => $sharer->getDisplayName(),
+ ],
+ ];
+ break;
+
+ case self::INCOMING_GROUP_SHARE:
+ $user = $this->userManager->get($notification->getUser());
+ if (!$user instanceof IUser) {
+ throw new AlreadyProcessedException();
+ }
+
+ $group = $this->groupManager->get($share->getSharedWith());
+ if ($group === null || !$group->inGroup($user)) {
+ throw new AlreadyProcessedException();
+ }
+
+ if ($share->getPermissions() === 0) {
+ // Already rejected
+ throw new AlreadyProcessedException();
+ }
+
+ $sharer = $this->userManager->get($share->getSharedBy());
+ if (!$sharer instanceof IUser) {
+ throw new \InvalidArgumentException('Temporary failure');
+ }
+
+ $subject = $l->t('You received {share} to group {group} as a share by {user}');
+ $subjectParameters = [
+ 'share' => [
+ 'type' => 'highlight',
+ 'id' => $notification->getObjectId(),
+ 'name' => $share->getTarget(),
+ ],
+ 'group' => [
+ 'type' => 'user-group',
+ 'id' => $group->getGID(),
+ 'name' => $group->getDisplayName(),
+ ],
+ 'user' => [
+ 'type' => 'user',
+ 'id' => $sharer->getUID(),
+ 'name' => $sharer->getDisplayName(),
+ ],
+ ];
+ break;
+
+ default:
+ throw new UnknownNotificationException('Invalid subject');
+ }
+
+ $notification->setRichSubject($subject, $subjectParameters)
+ ->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg')));
+
+ $acceptAction = $notification->createAction();
+ $acceptAction->setParsedLabel($l->t('Accept'))
+ ->setLink($this->url->linkToOCSRouteAbsolute('files_sharing.ShareAPI.acceptShare', ['id' => $share->getId()]), 'POST')
+ ->setPrimary(true);
+ $notification->addParsedAction($acceptAction);
+
+ $rejectAction = $notification->createAction();
+ $rejectAction->setParsedLabel($l->t('Decline'))
+ ->setLink($this->url->linkToOCSRouteAbsolute('files_sharing.ShareAPI.deleteShare', ['id' => $share->getId()]), 'DELETE')
+ ->setPrimary(false);
+ $notification->addParsedAction($rejectAction);
+
+ return $notification;
+ }
+}
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 18ea879d5d8..28972c1b462 100644
--- a/apps/files_sharing/lib/Scanner.php
+++ b/apps/files_sharing/lib/Scanner.php
@@ -1,39 +1,22 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @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 <pvince81@owncloud.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;
@@ -45,7 +28,7 @@ class Scanner extends \OC\Files\Cache\Scanner {
*
* @param string $path path of the file for which to retrieve metadata
*
- * @return array an array of metadata of the file
+ * @return array|null an array of metadata of the file
*/
public function getData($path) {
$data = parent::getData($path);
@@ -62,8 +45,8 @@ class Scanner extends \OC\Files\Cache\Scanner {
return $this->sourceScanner;
}
if ($this->storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
- /** @var \OC\Files\Storage\Storage $storage */
- list($storage) = $this->storage->resolvePath('');
+ /** @var Storage $storage */
+ [$storage] = $this->storage->resolvePath('');
$this->sourceScanner = $storage->getScanner();
return $this->sourceScanner;
} else {
@@ -71,13 +54,13 @@ class Scanner extends \OC\Files\Cache\Scanner {
}
}
- public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) {
+ 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
new file mode 100644
index 00000000000..171131b1819
--- /dev/null
+++ b/apps/files_sharing/lib/Settings/Personal.php
@@ -0,0 +1,51 @@
+<?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\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;
+use OCP\Settings\ISettings;
+
+class Personal implements ISettings {
+
+ 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';
+ $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);
+
+ $this->initialState->provideInitialState('accept_default', $acceptDefault);
+ $this->initialState->provideInitialState('enforce_accept', $enforceAccept);
+ $this->initialState->provideInitialState('allow_custom_share_folder', $allowCustomDirectory);
+ $this->initialState->provideInitialState('default_share_folder', $defaultShareFolder);
+ $this->initialState->provideInitialState('share_folder', $userShareFolder);
+
+ return new TemplateResponse('files_sharing', 'Settings/personal');
+ }
+
+ public function getSection(): string {
+ return 'sharing';
+ }
+
+ public function getPriority(): int {
+ return 90;
+ }
+}
diff --git a/apps/files_sharing/lib/ShareBackend/File.php b/apps/files_sharing/lib/ShareBackend/File.php
index 83474546581..2aa52ef1b7f 100644
--- a/apps/files_sharing/lib/ShareBackend/File.php
+++ b/apps/files_sharing/lib/ShareBackend/File.php
@@ -1,73 +1,53 @@
<?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 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 <pvince81@owncloud.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;
-
-class File implements \OCP\Share_Backend_File_Dependent {
-
- const FORMAT_SHARED_STORAGE = 0;
- const FORMAT_GET_FOLDER_CONTENTS = 1;
- const FORMAT_FILE_APP_ROOT = 2;
- const FORMAT_OPENDIR = 3;
- const FORMAT_GET_ALL = 4;
- const FORMAT_PERMISSIONS = 5;
- const FORMAT_TARGET_NAMES = 6;
+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 Share_Backend_File_Dependent {
+ public const FORMAT_SHARED_STORAGE = 0;
+ public const FORMAT_GET_FOLDER_CONTENTS = 1;
+ public const FORMAT_FILE_APP_ROOT = 2;
+ public const FORMAT_OPENDIR = 3;
+ public const FORMAT_GET_ALL = 4;
+ public const FORMAT_PERMISSIONS = 5;
+ public const FORMAT_TARGET_NAMES = 6;
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 {
- $federatedSharingApp = new \OCA\FederatedFileSharing\AppInfo\Application();
- $this->federatedShareProvider = $federatedSharingApp->getFederatedShareProvider();
+ $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;
}
}
@@ -79,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;
}
}
@@ -89,22 +69,17 @@ class File implements \OCP\Share_Backend_File_Dependent {
/**
* create unique target
- * @param string $filePath
+ *
+ * @param string $itemSource
* @param string $shareWith
- * @param array $exclude (optional)
* @return string
*/
- public function generateTarget($filePath, $shareWith, $exclude = null) {
- $shareFolder = \OCA\Files_Sharing\Helper::getShareFolder();
- $target = \OC\Files\Filesystem::normalizePath($shareFolder . '/' . basename($filePath));
+ 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,26 +92,24 @@ class File implements \OCP\Share_Backend_File_Dependent {
}
}
- $excludeList = (is_array($exclude)) ? $exclude : array();
-
- return \OCA\Files_Sharing\Helper::generateUniqueTarget($target, $excludeList, $view);
+ return Helper::generateUniqueTarget($target, $view);
}
public function formatItems($items, $format, $parameters = null) {
if ($format === self::FORMAT_SHARED_STORAGE) {
// Only 1 item should come through for this format call
$item = array_shift($items);
- return array(
+ return [
'parent' => $item['parent'],
'path' => $item['path'],
'storage' => $item['storage'],
'permissions' => $item['permissions'],
'uid_owner' => $item['uid_owner'],
- );
- } else if ($format === self::FORMAT_GET_FOLDER_CONTENTS) {
- $files = array();
+ ];
+ } elseif ($format === self::FORMAT_GET_FOLDER_CONTENTS) {
+ $files = [];
foreach ($items as $item) {
- $file = array();
+ $file = [];
$file['fileid'] = $item['file_source'];
$file['storage'] = $item['storage'];
$file['path'] = $item['file_target'];
@@ -150,38 +123,38 @@ 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;
}
return $files;
- } else if ($format === self::FORMAT_OPENDIR) {
- $files = array();
+ } elseif ($format === self::FORMAT_OPENDIR) {
+ $files = [];
foreach ($items as $item) {
$files[] = basename($item['file_target']);
}
return $files;
- } else if ($format === self::FORMAT_GET_ALL) {
- $ids = array();
+ } elseif ($format === self::FORMAT_GET_ALL) {
+ $ids = [];
foreach ($items as $item) {
$ids[] = $item['file_source'];
}
return $ids;
- } else if ($format === self::FORMAT_PERMISSIONS) {
- $filePermissions = array();
+ } elseif ($format === self::FORMAT_PERMISSIONS) {
+ $filePermissions = [];
foreach ($items as $item) {
$filePermissions[$item['file_source']] = $item['permissions'];
}
return $filePermissions;
- } else if ($format === self::FORMAT_TARGET_NAMES) {
- $targets = array();
+ } elseif ($format === self::FORMAT_TARGET_NAMES) {
+ $targets = [];
foreach ($items as $item) {
$targets[] = $item['file_target'];
}
return $targets;
}
- return array();
+ return [];
}
/**
@@ -191,10 +164,14 @@ class File implements \OCP\Share_Backend_File_Dependent {
* @return boolean
*/
public function isShareTypeAllowed($shareType) {
- if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) {
+ if ($shareType === IShare::TYPE_REMOTE) {
return $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
}
+ if ($shareType === IShare::TYPE_REMOTE_GROUP) {
+ return $this->federatedShareProvider->isOutgoingServer2serverGroupShareEnabled();
+ }
+
return true;
}
@@ -207,8 +184,15 @@ class File implements \OCP\Share_Backend_File_Dependent {
if (isset($source['parent'])) {
$parent = $source['parent'];
while (isset($parent)) {
- $query = \OCP\DB::prepare('SELECT `parent`, `uid_owner` FROM `*PREFIX*share` WHERE `id` = ?', 1);
- $item = $query->execute(array($parent))->fetchRow();
+ $qb = Server::get(IDBConnection::class)->getQueryBuilder();
+ $qb->select('parent', 'uid_owner')
+ ->from('share')
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($parent))
+ );
+ $result = $qb->executeQuery();
+ $item = $result->fetch();
+ $result->closeCursor();
if (isset($item['parent'])) {
$parent = $item['parent'];
} else {
@@ -222,7 +206,7 @@ class File implements \OCP\Share_Backend_File_Dependent {
if (isset($fileOwner)) {
$source['fileOwner'] = $fileOwner;
} else {
- \OCP\Util::writeLog('files_sharing', "No owner found for reshare", \OCP\Util::ERROR);
+ 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 80b141326d3..df5529c3c4a 100644
--- a/apps/files_sharing/lib/ShareBackend/Folder.php
+++ b/apps/files_sharing/lib/ShareBackend/Folder.php
@@ -1,108 +1,61 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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 = array();
- $parent = $this->getParentId($itemSource);
- 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;
- $displayNameOwner = \OCP\User::getDisplayName($share['uid_owner']);
- $displayNameShareWith = \OCP\User::getDisplayName($share['share_with']);
- $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) {
- $query = \OCP\DB::prepare('SELECT `parent` FROM `*PREFIX*filecache` WHERE `fileid` = ?');
- $result = $query->execute(array($child));
- $row = $result->fetchRow();
- $parent = $row ? $row['parent'] : null;
-
- return $parent;
- }
+use OCP\IDBConnection;
+use OCP\Server;
+use OCP\Share_Backend_Collection;
+class Folder extends File implements Share_Backend_Collection {
public function getChildren($itemSource) {
- $children = array();
- $parents = array($itemSource);
- $query = \OCP\DB::prepare('SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = ?');
- $result = $query->execute(array('httpd/unix-directory'));
+ $children = [];
+ $parents = [$itemSource];
+
+ $qb = Server::get(IDBConnection::class)->getQueryBuilder();
+ $qb->select('id')
+ ->from('mimetypes')
+ ->where(
+ $qb->expr()->eq('mimetype', $qb->createNamedParameter('httpd/unix-directory'))
+ );
+ $result = $qb->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+
if ($row = $result->fetchRow()) {
- $mimetype = (int) $row['id'];
+ $mimetype = (int)$row['id'];
} else {
$mimetype = -1;
}
while (!empty($parents)) {
- $parents = "'".implode("','", $parents)."'";
- $query = \OCP\DB::prepare('SELECT `fileid`, `name`, `mimetype` FROM `*PREFIX*filecache`'
- .' WHERE `parent` IN ('.$parents.')');
- $result = $query->execute();
- $parents = array();
- while ($file = $result->fetchRow()) {
- $children[] = array('source' => $file['fileid'], 'file_path' => $file['name']);
+ $qb = Server::get(IDBConnection::class)->getQueryBuilder();
+
+ $parents = array_map(function ($parent) use ($qb) {
+ return $qb->createNamedParameter($parent);
+ }, $parents);
+
+ $qb->select('`fileid', 'name', '`mimetype')
+ ->from('filecache')
+ ->where(
+ $qb->expr()->in('parent', $parents)
+ );
+
+ $result = $qb->execute();
+
+ $parents = [];
+ 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'];
}
}
+ $result->closeCursor();
}
return $children;
}
-
}
diff --git a/apps/files_sharing/lib/SharedMount.php b/apps/files_sharing/lib/SharedMount.php
index 4f0dc89e997..692a6c8979b 100644
--- a/apps/files_sharing/lib/SharedMount.php
+++ b/apps/files_sharing/lib/SharedMount.php
@@ -1,29 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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 <pvince81@owncloud.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;
@@ -32,69 +12,91 @@ 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\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;
-
- /**
- * @var string
- */
- private $user;
-
- /** @var \OCP\Share\IShare */
+ /** @var IShare */
private $superShare;
- /** @var \OCP\Share\IShare[] */
+ /** @var IShare[] */
private $groupedShares;
- /**
- * @param string $storage
- * @param SharedMount[] $mountpoints
- * @param array|null $arguments
- * @param \OCP\Files\Storage\IStorageFactory $loader
- */
- public function __construct($storage, array $mountpoints, $arguments = null, $loader = null) {
- $this->user = $arguments['user'];
- $this->recipientView = new View('/' . $this->user . '/files');
-
+ public function __construct(
+ $storage,
+ array $mountpoints,
+ $arguments,
+ IStorageFactory $loader,
+ private View $recipientView,
+ CappedMemoryCache $folderExistCache,
+ private IEventDispatcher $eventDispatcher,
+ private IUser $user,
+ bool $alreadyVerified,
+ ) {
$this->superShare = $arguments['superShare'];
$this->groupedShares = $arguments['groupedShares'];
- $newMountPoint = $this->verifyMountPoint($this->superShare, $mountpoints);
- $absMountPoint = '/' . $this->user . '/files' . $newMountPoint;
- $arguments['ownerView'] = new View('/' . $this->superShare->getShareOwner() . '/files');
- parent::__construct($storage, $absMountPoint, $arguments, $loader);
+ $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, array $mountpoints) {
-
+ private function verifyMountPoint(
+ IShare $share,
+ array $mountpoints,
+ CappedMemoryCache $folderExistCache,
+ ) {
$mountPoint = basename($share->getTarget());
$parent = dirname($share->getTarget());
- if (!$this->recipientView->is_dir($parent)) {
- $parent = Helper::getShareFolder($this->recipientView);
+ $event = new VerifyMountPointEvent($share, $this->recipientView, $parent);
+ $this->eventDispatcher->dispatchTyped($event);
+ $parent = $event->getParent();
+
+ $cached = $folderExistCache->get($parent);
+ if ($cached) {
+ $parentExists = $cached;
+ } else {
+ $parentExists = $this->recipientView->is_dir($parent);
+ $folderExistCache->set($parent, $parentExists);
+ }
+ if (!$parentExists) {
+ $parent = Helper::getShareFolder($this->recipientView, $this->user->getUID());
}
$newMountPoint = $this->generateUniqueTarget(
- \OC\Files\Filesystem::normalizePath($parent . '/' . $mountPoint),
+ Filesystem::normalizePath($parent . '/' . $mountPoint),
$this->recipientView,
$mountpoints
);
@@ -110,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) {
@@ -118,8 +120,10 @@ class SharedMount extends MountPoint implements MoveableMount {
foreach ($this->groupedShares as $tmpShare) {
$tmpShare->setTarget($newPath);
- \OC::$server->getShareManager()->moveShare($tmpShare, $this->user);
+ Server::get(\OCP\Share\IManager::class)->moveShare($tmpShare, $this->user->getUID());
}
+
+ $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->user));
}
@@ -131,23 +135,15 @@ class SharedMount extends MountPoint implements MoveableMount {
*/
private function generateUniqueTarget($path, $view, array $mountpoints) {
$pathinfo = pathinfo($path);
- $ext = (isset($pathinfo['extension'])) ? '.' . $pathinfo['extension'] : '';
+ $ext = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : '';
$name = $pathinfo['filename'];
$dir = $pathinfo['dirname'];
- // Helper function to find existing mount points
- $mountpointExists = function ($path) use ($mountpoints) {
- foreach ($mountpoints as $mountpoint) {
- if ($mountpoint->getShare()->getTarget() === $path) {
- return true;
- }
- }
- return false;
- };
-
$i = 2;
- while ($view->file_exists($path) || $mountpointExists($path)) {
+ $absolutePath = $this->recipientView->getAbsolutePath($path) . '/';
+ while ($view->file_exists($path) || isset($mountpoints[$absolutePath])) {
$path = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
+ $absolutePath = $this->recipientView->getAbsolutePath($path) . '/';
$i++;
}
@@ -159,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, '/');
@@ -167,10 +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') {
- \OCP\Util::writeLog('file sharing',
- 'Can not strip userid and "files/" from path: ' . $path,
- \OCP\Util::ERROR);
- 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'
@@ -187,7 +181,6 @@ class SharedMount extends MountPoint implements MoveableMount {
* @return bool
*/
public function moveMount($target) {
-
$relTargetPath = $this->stripUserFilesPath($target);
$share = $this->storage->getShare();
@@ -198,9 +191,13 @@ class SharedMount extends MountPoint implements MoveableMount {
$this->setMountPoint($target);
$this->storage->setMountPoint($relTargetPath);
} catch (\Exception $e) {
- \OCP\Util::writeLog('file sharing',
+ Server::get(LoggerInterface::class)->error(
'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"',
- \OCP\Util::ERROR);
+ [
+ 'app' => 'files_sharing',
+ 'exception' => $e,
+ ]
+ );
}
return $result;
@@ -212,8 +209,8 @@ class SharedMount extends MountPoint implements MoveableMount {
* @return bool
*/
public function removeMount() {
- $mountManager = \OC\Files\Filesystem::getMountManager();
- /** @var $storage \OCA\Files_Sharing\SharedStorage */
+ $mountManager = Filesystem::getMountManager();
+ /** @var SharedStorage $storage */
$storage = $this->getStorage();
$result = $storage->unshareStorage();
$mountManager->removeMount($this->mountPoint);
@@ -222,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
@@ -244,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 f681854cd2b..e310c5f3138 100644
--- a/apps/files_sharing/lib/SharedStorage.php
+++ b/apps/files_sharing/lib/SharedStorage.php
@@ -1,61 +1,59 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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 <pvince81@owncloud.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\Filesystem;
-use OC\Files\Storage\Wrapper\PermissionsMask;
+use OC\Files\Cache\NullWatcher;
+use OC\Files\ObjectStore\HomeObjectStoreStorage;
+use OC\Files\Storage\Common;
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_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\Lock\ILockingProvider;
-use OC\User\NoUserException;
+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 {
-
- /** @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;
@@ -69,24 +67,42 @@ 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 = [];
- public function __construct($arguments) {
- $this->ownerView = $arguments['ownerView'];
- $this->logger = \OC::$server->getLogger();
+ /** @var boolean */
+ private $sharingDisabledForUser;
- $this->superShare = $arguments['superShare'];
- $this->groupedShares = $arguments['groupedShares'];
+ /** @var ?Folder $ownerUserFolder */
+ private $ownerUserFolder = null;
- $this->user = $arguments['user'];
+ private string $sourcePath = '';
+
+ 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 = $parameters['superShare'];
+ $this->groupedShares = $parameters['groupedShares'];
+
+ $this->user = $parameters['user'];
+ if (isset($parameters['sharingDisabledForUser'])) {
+ $this->sharingDisabledForUser = $parameters['sharingDisabledForUser'];
+ } else {
+ $this->sharingDisabledForUser = false;
+ }
parent::__construct([
'storage' => null,
@@ -109,19 +125,65 @@ 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 {
- Filesystem::initMountPoints($this->superShare->getShareOwner());
- $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId());
- list($this->nonMaskedStorage, $this->rootPath) = $this->ownerView->resolvePath($sourcePath);
- $this->storage = new PermissionsMask([
- 'storage' => $this->nonMaskedStorage,
- 'mask' => $this->superShare->getPermissions()
- ]);
+ if (self::$initDepth > 10) {
+ throw new \Exception('Maximum share depth reached');
+ }
+
+ /** @var IRootFolder $rootFolder */
+ $rootFolder = Server::get(IRootFolder::class);
+ $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner());
+ $sourceId = $this->superShare->getNodeId();
+ $ownerNodes = $this->ownerUserFolder->getById($sourceId);
+
+ if (count($ownerNodes) === 0) {
+ $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]);
+ $this->cache = new FailedCache();
+ $this->rootPath = '';
+ } else {
+ 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(),
+ ]);
+ }
} catch (NotFoundException $e) {
// original file not accessible or deleted, set FailedStorage
$this->storage = new FailedStorage(['exception' => $e]);
@@ -136,22 +198,27 @@ 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) {
- if ($class === '\OC\Files\Storage\Common') {
+ public function instanceOfStorage(string $class): bool {
+ if ($class === '\OC\Files\Storage\Common' || $class == Common::class) {
return true;
}
- if (in_array($class, ['\OC\Files\Storage\Home', '\OC\Files\ObjectStore\HomeObjectStoreStorage'])) {
+ if (in_array($class, [
+ '\OC\Files\Storage\Home',
+ '\OC\Files\ObjectStore\HomeObjectStoreStorage',
+ '\OCP\Files\IHomeStorage',
+ Home::class,
+ HomeObjectStoreStorage::class,
+ IHomeStorage::class
+ ])) {
return false;
}
return parent::instanceOfStorage($class);
@@ -164,47 +231,37 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto
return $this->superShare->getId();
}
- private function isValid() {
+ private function isValid(): bool {
return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE;
}
- /**
- * get id of the mount point
- *
- * @return string
- */
- public function getId() {
+ 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 = '') {
+ public function getPermissions(string $path = ''): int {
if (!$this->isValid()) {
return 0;
}
- $permissions = $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 (\OCP\Util::isSharingDisabledForUser()) {
- $permissions &= ~\OCP\Constants::PERMISSION_SHARE;
+ if ($this->sharingDisabledForUser) {
+ $permissions &= ~Constants::PERMISSION_SHARE;
}
return $permissions;
}
- public function isCreatable($path) {
- return ($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) {
+ public function isReadable(string $path): bool {
if (!$this->isValid()) {
return false;
}
@@ -213,87 +270,84 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto
}
/** @var IStorage $storage */
/** @var string $internalPath */
- list($storage, $internalPath) = $this->resolvePath($path);
+ [$storage, $internalPath] = $this->resolvePath($path);
return $storage->isReadable($internalPath);
}
- public function isUpdatable($path) {
- return ($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) {
- return ($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) {
- 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) & \OCP\Constants::PERMISSION_SHARE);
- }
-
- public function fopen($path, $mode) {
- if ($source = $this->getUnjailedPath($path)) {
- switch ($mode) {
- case 'r+':
- case 'rb+':
- case 'w+':
- case 'wb+':
- case 'x+':
- case 'xb+':
- case 'a+':
- case 'ab+':
- case 'w':
- case 'wb':
- case 'x':
- case 'xb':
- case 'a':
- case 'ab':
- $creatable = $this->isCreatable($path);
- $updatable = $this->isUpdatable($path);
- // if neither permissions given, no need to continue
- if (!$creatable && !$updatable) {
- return false;
+ return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
+ }
+
+ public function fopen(string $path, string $mode) {
+ $source = $this->getUnjailedPath($path);
+ switch ($mode) {
+ case 'r+':
+ case 'rb+':
+ case 'w+':
+ case 'wb+':
+ case 'x+':
+ case 'xb+':
+ case 'a+':
+ case 'ab+':
+ case 'w':
+ case 'wb':
+ case 'x':
+ case 'xb':
+ case 'a':
+ case 'ab':
+ $creatable = $this->isCreatable(dirname($path));
+ $updatable = $this->isUpdatable($path);
+ // if neither permissions given, no need to continue
+ if (!$creatable && !$updatable) {
+ if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
+ $updatable = $this->isUpdatable(dirname($path));
}
- $exists = $this->file_exists($path);
- // if a file exists, updatable permissions are required
- if ($exists && !$updatable) {
+ if (!$updatable) {
return false;
}
+ }
+
+ $exists = $this->file_exists($path);
+ // if a file exists, updatable permissions are required
+ if ($exists && !$updatable) {
+ return false;
+ }
- // part file is allowed if !$creatable but the final file is $updatable
- if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') {
- if (!$exists && !$creatable) {
- return false;
- }
+ // part file is allowed if !$creatable but the final file is $updatable
+ if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') {
+ if (!$exists && !$creatable) {
+ return false;
}
- }
- $info = array(
- 'target' => $this->getMountPoint() . $path,
- 'source' => $source,
- 'mode' => $mode,
- );
- \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info);
- return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode);
+ }
}
- return false;
+ $info = [
+ 'target' => $this->getMountPoint() . '/' . $path,
+ 'source' => $source,
+ 'mode' => $mode,
+ ];
+ Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info);
+ return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode);
}
- /**
- * see http://php.net/manual/en/function.rename.php
- *
- * @param string $path1
- * @param string $path2
- * @return bool
- */
- public function rename($path1, $path2) {
+ public function rename(string $source, string $target): bool {
$this->init();
- $isPartFile = pathinfo($path1, PATHINFO_EXTENSION) === 'part';
- $targetExists = $this->file_exists($path2);
- $sameFodler = dirname($path1) === dirname($path2);
+ $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part';
+ $targetExists = $this->file_exists($target);
+ $sameFolder = dirname($source) === dirname($target);
- if ($targetExists || ($sameFodler && !$isPartFile)) {
+ if ($targetExists || ($sameFolder && !$isPartFile)) {
if (!$this->isUpdatable('')) {
return false;
}
@@ -303,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));
}
/**
@@ -311,14 +365,11 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto
*
* @return string
*/
- public function getMountPoint() {
+ public function getMountPoint(): string {
return $this->superShare->getTarget();
}
- /**
- * @param string $path
- */
- public function setMountPoint($path) {
+ public function setMountPoint(string $path): void {
$this->superShare->setTarget($path);
foreach ($this->groupedShares as $share) {
@@ -331,14 +382,11 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto
*
* @return string
*/
- public function getSharedFrom() {
+ public function getSharedFrom(): string {
return $this->superShare->getShareOwner();
}
- /**
- * @return \OCP\Share\IShare
- */
- public function getShare() {
+ public function getShare(): IShare {
return $this->superShare;
}
@@ -347,111 +395,117 @@ class SharedStorage extends \OC\Files\Storage\Wrapper\Jail implements ISharedSto
*
* @return string
*/
- public function getItemType() {
+ public function getItemType(): string {
return $this->superShare->getNodeType();
}
- /**
- * @param string $path
- * @param null $storage
- * @return Cache
- */
- public function getCache($path = '', $storage = null) {
+ public function getCache(string $path = '', ?IStorage $storage = null): ICache {
if ($this->cache) {
return $this->cache;
}
if (!$storage) {
$storage = $this;
}
+ $sourceRoot = $this->getSourceRootInfo();
if ($this->storage instanceof FailedStorage) {
return new FailedCache();
}
- $this->cache = new \OCA\Files_Sharing\Cache($storage, $this->getSourceRootInfo(), $this->superShare);
+
+ $this->cache = new Cache(
+ $storage,
+ $sourceRoot,
+ 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) {
+ public function getOwner(string $path): string|false {
return $this->superShare->getShareOwner();
}
+ public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
+ if ($this->watcher) {
+ return $this->watcher;
+ }
+
+ // Get node information
+ $node = $this->getShare()->getNodeCacheEntry();
+ 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
+ $this->watcher = new NullWatcher();
+ return $this->watcher;
+ }
+
/**
* unshare complete storage, also the grouped shares
*
* @return bool
*/
- public function unshareStorage() {
+ 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 */
- list($targetStorage, $targetInternalPath) = $this->resolvePath($path);
+ 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
if ($path === '') {
- $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId());
+ $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
$this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
}
}
- /**
- * @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 */
- list($targetStorage, $targetInternalPath) = $this->resolvePath($path);
+ 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
if ($path === '') {
- $sourcePath = $this->ownerView->getPath($this->superShare->getNodeId());
+ $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath);
$this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true);
}
}
- /**
- * @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 */
- list($targetStorage, $targetInternalPath) = $this->resolvePath($path);
+ 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,
- 'last_checked' => 0
+ 'last_checked' => 0,
];
}
- /**
- * @param bool $available
- */
- public function setAvailability($available) {
+ public function setAvailability(bool $isAvailable): void {
// shares do not participate in availability logic
}
@@ -460,30 +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(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 de45b45ab37..24e82330d43 100644
--- a/apps/files_sharing/lib/Updater.php
+++ b/apps/files_sharing/lib/Updater.php
@@ -1,38 +1,29 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @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 {
/**
* @param array $params
*/
- static public function renameHook($params) {
+ public static function renameHook($params) {
self::renameChildren($params['oldpath'], $params['newpath']);
- self::moveShareToShare($params['newpath']);
+ self::moveShareInOrOutOfShare($params['newpath']);
}
/**
@@ -45,20 +36,53 @@ class Updater {
*
* @param string $path
*/
- static private 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();
-
- $shares = $shareManager->getSharesBy($userFolder->getOwner()->getUID(), \OCP\Share::SHARE_TYPE_USER, $src, false, -1);
- $shares = array_merge($shares, $shareManager->getSharesBy($userFolder->getOwner()->getUID(), \OCP\Share::SHARE_TYPE_GROUP, $src, false, -1));
+ $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));
+
+ 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)) {
@@ -66,19 +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 \OCP\Share\IShare $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);
- $shareManager->updateShare($share);
+ $share->setPermissions($newPermissions);
+ $shareManager->updateShare($share, onlyValid: false);
}
}
@@ -88,20 +127,19 @@ class Updater {
* @param string $oldPath old path relative to data/user/files
* @param string $newPath new path relative to data/user/files
*/
- static private function renameChildren($oldPath, $newPath) {
+ private static function renameChildren($oldPath, $newPath) {
+ $absNewPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $newPath);
+ $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $oldPath);
- $absNewPath = \OC\Files\Filesystem::normalizePath('/' . \OCP\User::getUser() . '/files/' . $newPath);
- $absOldPath = \OC\Files\Filesystem::normalizePath('/' . \OCP\User::getUser() . '/files/' . $oldPath);
-
- $mountManager = \OC\Files\Filesystem::getMountManager();
- $mountedShares = $mountManager->findIn('/' . \OCP\User::getUser() . '/files/' . $oldPath);
+ $mountManager = Filesystem::getMountManager();
+ $mountedShares = $mountManager->findIn('/' . \OC_User::getUser() . '/files/' . $oldPath);
foreach ($mountedShares as $mount) {
- if ($mount->getStorage()->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) {
+ /** @var MountPoint $mount */
+ if ($mount->getStorage()->instanceOfStorage(ISharedStorage::class)) {
$mountPoint = $mount->getMountPoint();
$target = str_replace($absOldPath, $absNewPath, $mountPoint);
$mount->moveMount($target);
}
}
}
-
}
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;
+ }
+}