diff options
Diffstat (limited to 'apps/files_sharing/lib')
117 files changed, 12839 insertions, 5146 deletions
diff --git a/apps/files_sharing/lib/Activity/Filter.php b/apps/files_sharing/lib/Activity/Filter.php new file mode 100644 index 00000000000..4f3c4a7c914 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Filter.php @@ -0,0 +1,78 @@ +<?php + +/** + * 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 { + public const TYPE_REMOTE_SHARE = 'remote_share'; + public const TYPE_SHARED = 'shared'; + + public function __construct( + protected IL10N $l, + protected IURLGenerator $url, + ) { + } + + /** + * @return string Lowercase a-z only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'files_sharing'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('File shares'); + } + + /** + * @return int + * @since 11.0.0 + */ + public function getPriority() { + return 31; + } + + /** + * @return string Full URL to an icon, empty string when none is given + * @since 11.0.0 + */ + public function getIcon() { + return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg')); + } + + /** + * @param string[] $types + * @return string[] An array of allowed apps from which activities should be displayed + * @since 11.0.0 + */ + public function filterTypes(array $types) { + return array_intersect([ + self::TYPE_SHARED, + self::TYPE_REMOTE_SHARE, + 'file_downloaded', + ], $types); + } + + /** + * @return string[] An array of allowed apps from which activities should be displayed + * @since 11.0.0 + */ + public function allowedApps() { + 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 new file mode 100644 index 00000000000..7428af382fc --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/Base.php @@ -0,0 +1,185 @@ +<?php + +/** + * 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\IUserManager; +use OCP\L10N\IFactory; + +abstract class Base implements IProvider { + /** @var IL10N */ + protected $l; + + /** @var array */ + protected $displayNames = []; + + public function __construct( + protected IFactory $languageFactory, + protected IURLGenerator $url, + protected IManager $activityManager, + protected IUserManager $userManager, + protected ICloudIdManager $cloudIdManager, + protected IContactsManager $contactsManager, + protected IEventMerger $eventMerger, + ) { + } + + /** + * @param string $language + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws UnknownActivityException + * @since 11.0.0 + */ + public function parse($language, IEvent $event, ?IEvent $previousEvent = null) { + if ($event->getApp() !== 'files_sharing') { + throw new UnknownActivityException(); + } + + $this->l = $this->languageFactory->get('files_sharing', $language); + + if ($this->activityManager->isFormattingFilteredObject()) { + try { + return $this->parseShortVersion($event); + } catch (\InvalidArgumentException $e) { + // Ignore and simply use the long version... + } + } + + return $this->parseLongVersion($event, $previousEvent); + } + + /** + * @param IEvent $event + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + abstract protected function parseShortVersion(IEvent $event); + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + abstract protected function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null); + + /** + * @throws \InvalidArgumentException + */ + protected function setSubjects(IEvent $event, string $subject, array $parameters): void { + $event->setRichSubject($subject, $parameters); + } + + /** + * @param array|string $parameter + * @param IEvent|null $event + * @return array + * @throws \InvalidArgumentException + */ + protected function getFile($parameter, ?IEvent $event = null) { + if (is_array($parameter)) { + $path = reset($parameter); + $id = (string)key($parameter); + } elseif ($event !== null) { + $path = $parameter; + $id = (string)$event->getObjectId(); + } else { + throw new \InvalidArgumentException('Could not generate file parameter'); + } + + return [ + 'type' => 'file', + 'id' => $id, + 'name' => basename($path), + 'path' => trim($path, '/'), + 'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $id]), + ]; + } + + /** + * @param string $uid + * @param string $overwriteDisplayName - overwrite display name, only if user is not local + * + * @return array + */ + 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' => (($overwriteDisplayName !== '') ? $overwriteDisplayName : $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 new file mode 100644 index 00000000000..bddf2d30f73 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/Downloads.php @@ -0,0 +1,124 @@ +<?php + +/** + * 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'; + + 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 + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + 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) { + $subject = $this->l->t('Downloaded via public link'); + } elseif ($event->getSubject() === self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED + || $event->getSubject() === self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED) { + $subject = $this->l->t('Downloaded by {email}'); + } else { + throw new \InvalidArgumentException(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/download.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/download.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseLongVersion(IEvent $event, ?IEvent $previousEvent = null) { + $parsedParameters = $this->getParsedParameters($event); + + if ($event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED + || $event->getSubject() === self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED) { + if (!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(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/download.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/download.svg'))); + } + + return $event; + } + + /** + * @param IEvent $event + * @return array + * @throws \InvalidArgumentException + */ + protected function getParsedParameters(IEvent $event) { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + 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), + ]; + case self::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED: + case self::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED: + return [ + 'file' => $this->getFile($parameters[0], $event), + 'email' => [ + 'type' => 'email', + 'id' => $parameters[1], + 'name' => $parameters[1], + ], + ]; + } + + throw new \InvalidArgumentException(); + } +} diff --git a/apps/files_sharing/lib/Activity/Providers/Groups.php b/apps/files_sharing/lib/Activity/Providers/Groups.php new file mode 100644 index 00000000000..d0086c05ced --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/Groups.php @@ -0,0 +1,162 @@ +<?php + +/** + * 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'; + + 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 + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseShortVersion(IEvent $event) { + $parsedParameters = $this->getParsedParameters($event); + + if ($event->getSubject() === self::SUBJECT_SHARED_GROUP_SELF) { + $subject = $this->l->t('Shared with group {group}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) { + $subject = $this->l->t('Removed share for group {group}'); + } elseif ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) { + $subject = $this->l->t('{actor} shared with group {group}'); + } 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(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + 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}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_GROUP_SELF) { + $subject = $this->l->t('You removed group {group} from {file}'); + } elseif ($event->getSubject() === self::SUBJECT_RESHARED_GROUP_BY) { + $subject = $this->l->t('{actor} shared {file} with group {group}'); + } 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(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + protected function getParsedParameters(IEvent $event) { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + switch ($subject) { + case self::SUBJECT_RESHARED_GROUP_BY: + case self::SUBJECT_UNSHARED_GROUP_BY: + return [ + 'file' => $this->getFile($parameters[0], $event), + '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' => $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 new file mode 100644 index 00000000000..15ffaf2cdb0 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/PublicLinks.php @@ -0,0 +1,111 @@ +<?php + +/** + * 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 { + 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 + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseShortVersion(IEvent $event) { + $parsedParameters = $this->getParsedParameters($event); + + if ($event->getSubject() === self::SUBJECT_SHARED_LINK_SELF) { + $subject = $this->l->t('Shared as public link'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) { + $subject = $this->l->t('Removed public link'); + } elseif ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) { + $subject = $this->l->t('Public link expired'); + } elseif ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) { + $subject = $this->l->t('{actor} shared as public link'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) { + $subject = $this->l->t('{actor} removed public link'); + } elseif ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) { + $subject = $this->l->t('Public link of {actor} expired'); + } else { + throw new \InvalidArgumentException(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + 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'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_SELF) { + $subject = $this->l->t('You removed public link for {file}'); + } elseif ($event->getSubject() === self::SUBJECT_LINK_EXPIRED) { + $subject = $this->l->t('Public link expired for {file}'); + } elseif ($event->getSubject() === self::SUBJECT_RESHARED_LINK_BY) { + $subject = $this->l->t('{actor} shared {file} as public link'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_LINK_BY) { + $subject = $this->l->t('{actor} removed public link for {file}'); + } elseif ($event->getSubject() === self::SUBJECT_LINK_BY_EXPIRED) { + $subject = $this->l->t('Public link of {actor} for {file} expired'); + } else { + throw new \InvalidArgumentException(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + protected function getParsedParameters(IEvent $event) { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + switch ($subject) { + case self::SUBJECT_SHARED_LINK_SELF: + case self::SUBJECT_UNSHARED_LINK_SELF: + case self::SUBJECT_LINK_EXPIRED: + return [ + 'file' => $this->getFile($parameters[0], $event), + ]; + case self::SUBJECT_RESHARED_LINK_BY: + case self::SUBJECT_UNSHARED_LINK_BY: + case self::SUBJECT_LINK_BY_EXPIRED: + return [ + 'file' => $this->getFile($parameters[0], $event), + 'actor' => $this->getUser($parameters[1]), + ]; + } + return []; + } +} diff --git a/apps/files_sharing/lib/Activity/Providers/RemoteShares.php b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php new file mode 100644 index 00000000000..750d0747b62 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/RemoteShares.php @@ -0,0 +1,122 @@ +<?php + +/** + * 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'; + + public function __construct(IFactory $languageFactory, + IURLGenerator $url, + IManager $activityManager, + IUserManager $userManager, + ICloudIdManager $cloudIdManager, + IContactsManager $contactsManager, + IEventMerger $eventMerger) { + parent::__construct($languageFactory, $url, $activityManager, $userManager, $cloudIdManager, $contactsManager, $eventMerger); + } + + /** + * @param IEvent $event + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseShortVersion(IEvent $event) { + $parsedParameters = $this->getParsedParameters($event); + + if ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_ACCEPTED) { + $subject = $this->l->t('{user} accepted the remote share'); + } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) { + $subject = $this->l->t('{user} declined the remote share'); + } else { + throw new \InvalidArgumentException(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + 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}'); + } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_ACCEPTED) { + $subject = $this->l->t('{user} accepted the remote share of {file}'); + } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_DECLINED) { + $subject = $this->l->t('{user} declined the remote share of {file}'); + } elseif ($event->getSubject() === self::SUBJECT_REMOTE_SHARE_UNSHARED) { + $subject = $this->l->t('{user} unshared {file} from you'); + } else { + throw new \InvalidArgumentException(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + protected function getParsedParameters(IEvent $event) { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + switch ($subject) { + case self::SUBJECT_REMOTE_SHARE_RECEIVED: + case self::SUBJECT_REMOTE_SHARE_UNSHARED: + $displayName = (count($parameters) > 2) ? $parameters[2] : ''; + return [ + 'file' => [ + 'type' => 'pending-federated-share', + 'id' => $parameters[1], + 'name' => $parameters[1], + ], + 'user' => $this->getUser($parameters[0], $displayName) + ]; + case self::SUBJECT_REMOTE_SHARE_ACCEPTED: + case self::SUBJECT_REMOTE_SHARE_DECLINED: + $fileParameter = $parameters[1]; + if (!is_array($fileParameter)) { + $fileParameter = [$event->getObjectId() => $event->getObjectName()]; + } + return [ + 'file' => $this->getFile($fileParameter), + 'user' => $this->getUser($parameters[0]), + ]; + } + throw new \InvalidArgumentException(); + } +} diff --git a/apps/files_sharing/lib/Activity/Providers/Users.php b/apps/files_sharing/lib/Activity/Providers/Users.php new file mode 100644 index 00000000000..5c833ffae93 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Providers/Users.php @@ -0,0 +1,143 @@ +<?php + +/** + * 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'; + + public const SUBJECT_EXPIRED_USER = 'expired_user'; + public const SUBJECT_EXPIRED = 'expired'; + + /** + * @param IEvent $event + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parseShortVersion(IEvent $event) { + $parsedParameters = $this->getParsedParameters($event); + + if ($event->getSubject() === self::SUBJECT_SHARED_USER_SELF) { + $subject = $this->l->t('Shared with {user}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) { + $subject = $this->l->t('Removed share for {user}'); + } 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}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) { + $subject = $this->l->t('{actor} removed share for {user}'); + } elseif ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) { + $subject = $this->l->t('Shared by {actor}'); + } 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(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + /** + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + 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}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_SELF) { + $subject = $this->l->t('You removed {user} from {file}'); + } 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}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARED_USER_BY) { + $subject = $this->l->t('{actor} removed {user} from {file}'); + } elseif ($event->getSubject() === self::SUBJECT_SHARED_WITH_BY) { + $subject = $this->l->t('{actor} shared {file} with you'); + } 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(); + } + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg'))); + } + $this->setSubjects($event, $subject, $parsedParameters); + + return $event; + } + + protected function getParsedParameters(IEvent $event) { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + 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]), + ]; + case self::SUBJECT_RESHARED_USER_BY: + case self::SUBJECT_UNSHARED_USER_BY: + return [ + 'file' => $this->getFile($parameters[0], $event), + 'user' => $this->getUser($parameters[2]), + 'actor' => $this->getUser($parameters[1]), + ]; + } + return []; + } +} diff --git a/apps/files_sharing/lib/Activity/Settings/PublicLinks.php b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php new file mode 100644 index 00000000000..0d3d00d2a7b --- /dev/null +++ b/apps/files_sharing/lib/Activity/Settings/PublicLinks.php @@ -0,0 +1,67 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Activity\Settings; + +class PublicLinks extends ShareActivitySettings { + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'public_links'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('A file or folder shared by mail or by public link was <strong>downloaded</strong>'); + } + + /** + * @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/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 new file mode 100644 index 00000000000..c04364bef20 --- /dev/null +++ b/apps/files_sharing/lib/Activity/Settings/RemoteShare.php @@ -0,0 +1,67 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Activity\Settings; + +class RemoteShare extends ShareActivitySettings { + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'remote_share'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('A file or folder was shared from <strong>another server</strong>'); + } + + /** + * @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 11; + } + + /** + * @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/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 new file mode 100644 index 00000000000..3717512eebd --- /dev/null +++ b/apps/files_sharing/lib/Activity/Settings/Shared.php @@ -0,0 +1,67 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Activity\Settings; + +class Shared extends ShareActivitySettings { + /** + * @return string Lowercase a-z and underscore only identifier + * @since 11.0.0 + */ + public function getIdentifier() { + return 'shared'; + } + + /** + * @return string A translated string + * @since 11.0.0 + */ + public function getName() { + return $this->l->t('A file or folder has been <strong>shared</strong>'); + } + + /** + * @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 10; + } + + /** + * @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/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php new file mode 100644 index 00000000000..8ddb3afaf33 --- /dev/null +++ b/apps/files_sharing/lib/AppInfo/Application.php @@ -0,0 +1,157 @@ +<?php + +/** + * 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 OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; +use OCP\Files\Config\IMountProviderCollection; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\Events\Node\BeforeNodeReadEvent; +use OCP\Group\Events\GroupChangedEvent; +use OCP\Group\Events\GroupDeletedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\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; + +class Application extends App implements IBootstrap { + public const APP_ID = 'files_sharing'; + + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + } + + 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) + ); + }); + + /** + * Middleware + */ + $context->registerMiddleWare(SharingCheckMiddleware::class); + $context->registerMiddleWare(OCSShareAPIMiddleware::class); + $context->registerMiddleWare(ShareInfoMiddleware::class); + + $context->registerCapability(Capabilities::class); + + $context->registerNotifierService(Notifier::class); + $context->registerEventListener(UserChangedEvent::class, DisplayNameCache::class); + $context->registerEventListener(UserDeletedEvent::class, DisplayNameCache::class); + $context->registerEventListener(GroupChangedEvent::class, GroupDisplayNameCache::class); + $context->registerEventListener(GroupDeletedEvent::class, GroupDisplayNameCache::class); + + // Sidebar and files scripts + $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); + $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); + $context->registerEventListener(ShareCreatedEvent::class, ShareInteractionListener::class); + $context->registerEventListener(ShareCreatedEvent::class, UserShareAcceptanceListener::class); + $context->registerEventListener(UserAddedEvent::class, UserAddedToGroupListener::class); + + // Publish activity for public download + $context->registerEventListener(BeforeNodeReadEvent::class, BeforeNodeReadListener::class); + $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeNodeReadListener::class); + + // Handle download events for view only checks. Priority higher than 0 to run early. + $context->registerEventListener(BeforeZipCreatedEvent::class, BeforeZipCreatedListener::class, 5); + $context->registerEventListener(BeforeDirectFileDownloadEvent::class, BeforeDirectFileDownloadListener::class, 5); + + // File request auth + $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class); + + $context->registerConfigLexicon(ConfigLexicon::class); + } + + public function boot(IBootContext $context): void { + $context->injectFn([$this, 'registerMountProviders']); + $context->injectFn([$this, 'registerEventsScripts']); + + 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 new file mode 100644 index 00000000000..f9042fc0765 --- /dev/null +++ b/apps/files_sharing/lib/Cache.php @@ -0,0 +1,199 @@ +<?php + +/** + * 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 + * + * don't use this class directly if you need to get metadata, use \OC\Files\Filesystem::getFileInfo instead + */ +class Cache extends CacheJail { + private bool $rootUnchanged = true; + private ?string $ownerDisplayName = null; + private $numericId; + private DisplayNameCache $displayNameCache; + + /** + * @param SharedStorage $storage + */ + public function __construct( + private $storage, + private ICacheEntry $sourceRootInfo, + CacheDependencies $dependencies, + private IShare $share, + ) { + $this->numericId = $this->sourceRootInfo->getStorageId(); + $this->displayNameCache = $dependencies->getDisplayNameCache(); + + parent::__construct( + null, + '', + $dependencies, + ); + } + + protected function getRoot() { + 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 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 ?? ''; + } + return $this->root; + } + + public function getGetUnjailedRoot(): string { + return $this->sourceRootInfo->getPath(); + } + + public function getCache(): ICache { + if (is_null($this->cache)) { + $sourceStorage = $this->storage->getSourceStorage(); + if ($sourceStorage) { + $this->cache = $sourceStorage->getCache(); + } else { + // don't set $this->cache here since sourceStorage will be set later + return new FailedCache(); + } + } + return $this->cache; + } + + public function getNumericStorageId() { + if (isset($this->numericId)) { + return $this->numericId; + } else { + return -1; + } + } + + public function get($file) { + if ($this->rootUnchanged && ($file === '' || $file === $this->sourceRootInfo->getId())) { + return $this->formatCacheEntry(clone $this->sourceRootInfo, ''); + } + return parent::get($file); + } + + public function update($id, array $data) { + $this->rootUnchanged = false; + parent::update($id, $data); + } + + public function insert($file, array $data) { + $this->rootUnchanged = false; + return parent::insert($file, $data); + } + + public function remove($file) { + $this->rootUnchanged = false; + parent::remove($file); + } + + 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 = $entry['path'] ?? ''; + $entry['path'] = $this->getJailedPath($path); + } else { + $entry['path'] = $path; + } + + 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->share->getShareOwner(); + $entry['displayname_owner'] = $this->getOwnerDisplayName(); + if ($path === '') { + $entry['is_share_mount_point'] = true; + } + return $entry; + } + + private function getOwnerDisplayName() { + if (!$this->ownerDisplayName) { + $uid = $this->share->getShareOwner(); + $this->ownerDisplayName = $this->displayNameCache->getDisplayName($uid) ?? $uid; + } + return $this->ownerDisplayName; + } + + /** + * remove all entries for files that are stored on the storage from the cache + */ + 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 new file mode 100644 index 00000000000..06aa1271c8f --- /dev/null +++ b/apps/files_sharing/lib/Capabilities.php @@ -0,0 +1,193 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OC\Core\AppInfo\ConfigLexicon; +use OCP\App\IAppManager; +use OCP\Capabilities\ICapability; +use OCP\Constants; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\Share\IManager; + +/** + * Class Capabilities + * + * @package OCA\Files_Sharing + */ +class Capabilities implements ICapability { + public function __construct( + private IConfig $config, + private readonly IAppConfig $appConfig, + private IManager $shareManager, + private IAppManager $appManager, + ) { + } + + /** + * Return this classes capabilities + * + * @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->shareManager->shareApiEnabled()) { + $res['api_enabled'] = false; + $res['public'] = ['enabled' => false]; + $res['user'] = ['send_mail' => false]; + $res['resharing'] = false; + } else { + $res['api_enabled'] = true; + + $public = []; + $public['enabled'] = $this->shareManager->shareApiAllowLinks(); + if ($public['enabled']) { + $public['password'] = []; + $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['multiple_links'] = true; + $public['expire_date']['enabled'] = $this->shareManager->shareApiLinkDefaultExpireDate(); + if ($public['expire_date']['enabled']) { + $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->shareManager->shareApiLinkAllowPublicUpload(); + $public['upload_files_drop'] = $public['upload']; + $public['custom_tokens'] = $this->shareManager->allowCustomTokens(); + } + $res['public'] = $public; + + $res['resharing'] = $this->config->getAppValue('core', 'shareapi_allow_resharing', 'yes') === 'yes'; + + $res['user']['send_mail'] = false; + $res['user']['expire_date']['enabled'] = true; + + // 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->shareManager->allowGroupSharing(); + + $res['group'] = []; + $res['group']['enabled'] = $this->shareManager->allowGroupSharing(); + $res['group']['expire_date']['enabled'] = true; + $res['default_permissions'] = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL); + } + + //Federated sharing + 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 [ + 'files_sharing' => $res, + ]; + } +} diff --git a/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php new file mode 100644 index 00000000000..803dfd6325f --- /dev/null +++ b/apps/files_sharing/lib/Collaboration/ShareRecipientSorter.php @@ -0,0 +1,85 @@ +<?php + +/** + * 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\IRootFolder; +use OCP\Files\Node; +use OCP\IUserSession; +use OCP\Share\IManager; + +class ShareRecipientSorter implements ISorter { + + public function __construct( + private IManager $shareManager, + private IRootFolder $rootFolder, + private IUserSession $userSession, + ) { + } + + 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') { + return; + } + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + /** @var Node[] $nodes */ + $node = $userFolder->getFirstNodeById((int)$context['itemId']); + if (!$node) { + return; + } + $al = $this->shareManager->getAccessList($node); + + foreach ($sortArray as $type => &$byType) { + 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) { + return [$i++, $element]; + }, $byType); + + usort($workArray, function ($a, $b) use ($al, $type) { + $result = $this->compare($a[1], $b[1], $al[$type]); + if ($result === 0) { + $result = $a[0] - $b[0]; + } + return $result; + }); + + // and remove the index values again + $byType = array_column($workArray, 1); + } + } + + /** + * @param array $a + * @param array $b + * @param array $al + * @return int + */ + protected function compare(array $a, array $b, array $al) { + $a = $a['value']['shareWith']; + $b = $b['value']['shareWith']; + + $valueA = (int)in_array($a, $al, true); + $valueB = (int)in_array($b, $al, true); + + return $valueB - $valueA; + } +} diff --git a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php new file mode 100644 index 00000000000..809481e5c0f --- /dev/null +++ b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php @@ -0,0 +1,169 @@ +<?php + +/** + * 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; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Cleanup 'shared::' storage entries that have no matching entries in the + * shares_external table. + */ +class CleanupRemoteStorages extends Command { + + public function __construct( + protected IDBConnection $connection, + private ICloudIdManager $cloudIdManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('sharing:cleanup-remote-storages') + ->setDescription('Cleanup shared storage entries that have no matching entry in the shares_external table') + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'only show which storages would be deleted' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $remoteStorages = $this->getRemoteStorages(); + + $output->writeln(count($remoteStorages) . ' remote storage(s) need(s) to be checked'); + + $remoteShareIds = $this->getRemoteShareIds(); + + $output->writeln(count($remoteShareIds) . ' remote share(s) exist'); + + foreach ($remoteShareIds as $id => $remoteShareId) { + if (isset($remoteStorages[$remoteShareId])) { + if ($input->getOption('dry-run') || $output->isVerbose()) { + $output->writeln("<info>$remoteShareId belongs to remote share $id</info>"); + } + + unset($remoteStorages[$remoteShareId]); + } else { + $output->writeln("<comment>$remoteShareId for share $id has no matching storage, yet</comment>"); + } + } + + if (empty($remoteStorages)) { + $output->writeln('<info>no storages deleted</info>'); + } else { + $dryRun = $input->getOption('dry-run'); + foreach ($remoteStorages as $id => $numericId) { + if ($dryRun) { + $output->writeln("<error>$id [$numericId] can be deleted</error>"); + $this->countFiles($numericId, $output); + } else { + $this->deleteStorage($id, $numericId, $output); + } + } + } + return 0; + } + + public function countFiles($numericId, OutputInterface $output) { + $queryBuilder = $this->connection->getQueryBuilder(); + $queryBuilder->select($queryBuilder->func()->count('fileid')) + ->from('filecache') + ->where($queryBuilder->expr()->eq( + 'storage', + $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR) + ); + $result = $queryBuilder->executeQuery(); + $count = $result->fetchOne(); + $result->closeCursor(); + $output->writeln("$count files can be deleted for storage $numericId"); + } + + public function deleteStorage($id, $numericId, OutputInterface $output) { + $queryBuilder = $this->connection->getQueryBuilder(); + $queryBuilder->delete('storages') + ->where($queryBuilder->expr()->eq( + 'id', + $queryBuilder->createNamedParameter($id, IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR) + ); + $output->write("deleting $id [$numericId] ... "); + $count = $queryBuilder->executeStatement(); + $output->writeln("deleted $count storage"); + $this->deleteFiles($numericId, $output); + } + + public function deleteFiles($numericId, OutputInterface $output) { + $queryBuilder = $this->connection->getQueryBuilder(); + $queryBuilder->delete('filecache') + ->where($queryBuilder->expr()->eq( + 'storage', + $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR), + IQueryBuilder::PARAM_STR) + ); + $output->write("deleting files for storage $numericId ... "); + $count = $queryBuilder->executeStatement(); + $output->writeln("deleted $count files"); + } + + public function getRemoteStorages() { + $queryBuilder = $this->connection->getQueryBuilder(); + $queryBuilder->select(['id', 'numeric_id']) + ->from('storages') + ->where($queryBuilder->expr()->like( + 'id', + // match all 'shared::' + 32 characters storages + $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::') . str_repeat('_', 32)), + IQueryBuilder::PARAM_STR) + ) + ->andWhere($queryBuilder->expr()->notLike( + 'id', + // but not the ones starting with a '/', they are for normal shares + $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::/') . '%'), + IQueryBuilder::PARAM_STR) + ) + ->orderBy('numeric_id'); + $result = $queryBuilder->executeQuery(); + + $remoteStorages = []; + + 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', 'owner', 'remote']) + ->from('share_external'); + $result = $queryBuilder->executeQuery(); + + $remoteShareIds = []; + + 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 new file mode 100644 index 00000000000..fa828a9d2c2 --- /dev/null +++ b/apps/files_sharing/lib/Controller/ExternalSharesController.php @@ -0,0 +1,62 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +/** + * Class ExternalSharesController + * + * @package OCA\Files_Sharing\Controller + */ +class ExternalSharesController extends Controller { + public function __construct( + string $appName, + IRequest $request, + private \OCA\Files_Sharing\External\Manager $externalManager, + ) { + parent::__construct($appName, $request); + } + + /** + * @NoOutgoingFederatedSharingRequired + * + * @return JSONResponse + */ + #[NoAdminRequired] + public function index() { + return new JSONResponse($this->externalManager->getOpenShares()); + } + + /** + * @NoOutgoingFederatedSharingRequired + * + * @param int $id + * @return JSONResponse + */ + #[NoAdminRequired] + public function create($id) { + $this->externalManager->acceptShare($id); + return new JSONResponse(); + } + + /** + * @NoOutgoingFederatedSharingRequired + * + * @param integer $id + * @return JSONResponse + */ + #[NoAdminRequired] + public function destroy($id) { + $this->externalManager->declineShare($id); + return new JSONResponse(); + } +} diff --git a/apps/files_sharing/lib/Controller/PublicPreviewController.php b/apps/files_sharing/lib/Controller/PublicPreviewController.php new file mode 100644 index 00000000000..d917f6e0ebb --- /dev/null +++ b/apps/files_sharing/lib/Controller/PublicPreviewController.php @@ -0,0 +1,206 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Controller; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\PublicShareController; +use OCP\Constants; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\IPreview; +use OCP\IRequest; +use OCP\ISession; +use OCP\Preview\IMimeIconProvider; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; +use OCP\Share\IShare; + +class PublicPreviewController extends PublicShareController { + + /** @var IShare */ + private $share; + + public function __construct( + string $appName, + IRequest $request, + private ShareManager $shareManager, + ISession $session, + private IPreview $previewManager, + private IMimeIconProvider $mimeIconProvider, + ) { + parent::__construct($appName, $request, $session); + } + + protected function getPasswordHash(): ?string { + return $this->share->getPassword(); + } + + public function isValidToken(): bool { + try { + $this->share = $this->shareManager->getShareByToken($this->getToken()); + return true; + } catch (ShareNotFound $e) { + return false; + } + } + + protected function isPasswordProtected(): bool { + return $this->share->getPassword() !== null; + } + + + /** + * 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{}> + * + * 200: Preview returned + * 303: Redirect to the mime icon url if mimeFallback is true + * 400: Getting preview is not possible + * 403: Getting preview is not allowed + * 404: Share or preview not found + */ + #[PublicPage] + #[NoCSRFRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getPreview( + string $token, + string $file = '', + int $x = 32, + int $y = 32, + $a = false, + bool $mimeFallback = false, + ) { + $cacheForSeconds = 60 * 60 * 24; // 1 day + + if ($token === '' || $x === 0 || $y === 0) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + try { + $share = $this->shareManager->getShareByToken($token); + } catch (ShareNotFound $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + 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) { + $file = $node->get($file); + } else { + $file = $node; + } + + $f = $this->previewManager->getPreview($file, $x, $y, !$a); + $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); + } + } + + /** + * @NoSameSiteCookieRequired + * + * 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 + */ + #[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); + } + + // No share no image + try { + $share = $this->shareManager->getShareByToken($token); + } catch (ShareNotFound $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + // No permissions no image + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + // Password protected shares have no direct link! + if ($share->getPassword() !== null) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + if (!$share->canSeeContent()) { + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + try { + $node = $share->getNode(); + if ($node instanceof Folder) { + // Direct link only works for single files + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $f = $this->previewManager->getPreview($node, -1, -1, false); + $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) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + } +} diff --git a/apps/files_sharing/lib/Controller/RemoteController.php b/apps/files_sharing/lib/Controller/RemoteController.php new file mode 100644 index 00000000000..8c15cd8463e --- /dev/null +++ b/apps/files_sharing/lib/Controller/RemoteController.php @@ -0,0 +1,179 @@ +<?php + +/** + * 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\IRequest; +use Psr\Log\LoggerInterface; + +/** + * @psalm-import-type Files_SharingRemoteShare from ResponseDefinitions + */ +class RemoteController extends OCSController { + /** + * Remote constructor. + * + * @param string $appName + * @param IRequest $request + * @param Manager $externalManager + */ + public function __construct( + $appName, + IRequest $request, + private Manager $externalManager, + private LoggerInterface $logger, + ) { + parent::__construct($appName, $request); + } + + /** + * Get list of pending remote shares + * + * @return DataResponse<Http::STATUS_OK, list<Files_SharingRemoteShare>, array{}> + * + * 200: Pending remote shares returned + */ + #[NoAdminRequired] + public function getOpenShares() { + return new DataResponse($this->externalManager->getOpenShares()); + } + + /** + * Accept a remote share + * + * @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(); + } + + $this->logger->error('Could not accept federated share with id: ' . $id, + ['app' => 'files_sharing']); + + throw new OCSNotFoundException('wrong share ID, share does not exist.'); + } + + /** + * Decline a remote share + * + * @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(); + } + + // 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 does not exist.'); + } + + /** + * @param array $share Share with info from the share_external table + * @return array enriched share info with data from the filecache + */ + private static function extendShareInfo($share) { + $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(); + $share['type'] = $info->getType(); + $share['file_id'] = $info->getId(); + + return $share; + } + + /** + * Get a list of accepted remote shares + * + * @return DataResponse<Http::STATUS_OK, list<Files_SharingRemoteShare>, array{}> + * + * 200: Accepted remote shares returned + */ + #[NoAdminRequired] + public function getShares() { + $shares = $this->externalManager->getAcceptedShares(); + $shares = array_map(self::extendShareInfo(...), $shares); + + return new DataResponse($shares); + } + + /** + * Get info of a remote share + * + * @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); + + if ($shareInfo === false) { + throw new OCSNotFoundException('share does not exist'); + } else { + $shareInfo = self::extendShareInfo($shareInfo); + return new DataResponse($shareInfo); + } + } + + /** + * Unshare a remote share + * + * @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); + + if ($shareInfo === false) { + throw new OCSNotFoundException('Share does not exist'); + } + + $mountPoint = '/' . \OC_User::getUser() . '/files' . $shareInfo['mountpoint']; + + if ($this->externalManager->removeShare($mountPoint) === true) { + return new DataResponse(); + } else { + throw new OCSForbiddenException('Could not unshare'); + } + } +} 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 new file mode 100644 index 00000000000..095a8a75963 --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -0,0 +1,2295 @@ +<?php + +declare(strict_types=1); +/** + * 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\IPreview; +use OCP\IRequest; +use OCP\ITagManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use OCP\Mail\IMailer; +use OCP\Server; +use OCP\Share\Exceptions\GenericShareException; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\Exceptions\ShareTokenException; +use OCP\Share\IManager; +use OCP\Share\IProviderFactory; +use OCP\Share\IShare; +use OCP\Share\IShareProviderWithNotification; +use OCP\UserStatus\IManager as IUserStatusManager; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +/** + * @package OCA\Files_Sharing\API + * + * @psalm-import-type Files_SharingShare from ResponseDefinitions + */ +class ShareAPIController extends OCSController { + + private ?Node $lockedNode = null; + private array $trustedServerCache = []; + + /** + * Share20OCS constructor. + */ + public function __construct( + string $appName, + IRequest $request, + 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); + } + + /** + * Convert an IShare to an array for OCS output + * + * @param IShare $share + * @param Node|null $recipientNode + * @return Files_SharingShare + * @throws NotFoundException In case the node can't be resolved. + * + * @suppress PhanUndeclaredClassMethod + */ + 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->userId); + if ($recipientNode) { + $node = $recipientNode; + } else { + $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'; + } + + // 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() === IShare::TYPE_USER) { + $sharedWith = $this->userManager->get($share->getSharedWith()); + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $sharedWith !== null ? $sharedWith->getDisplayName() : $share->getSharedWith(); + $result['share_with_displayname_unique'] = $sharedWith !== null ? ( + !empty($sharedWith->getSystemEMailAddress()) ? $sharedWith->getSystemEMailAddress() : $sharedWith->getUID() + ) : $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(); + } 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'] = '(' . $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()]); + } elseif ($share->getShareType() === IShare::TYPE_REMOTE) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getCachedFederatedDisplayName($share->getSharedWith()); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD'); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_EMAIL) { + $result['share_with'] = $share->getSharedWith(); + $result['password'] = $share->getPassword(); + $result['password_expiration_time'] = $share->getPasswordExpirationTime() !== null ? $share->getPasswordExpirationTime()->format(\DateTime::ATOM) : null; + $result['send_password_by_talk'] = $share->getSendPasswordByTalk(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL'); + $result['token'] = $share->getToken(); + } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) { + // getSharedWith() returns either "name (type, owner)" or + // "name (type, owner) [id]", depending on the 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 + * not we return the full name. + * + * @param string $query + * @param string $property + * @return string + */ + 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 && $r['FN']) { + return $r['FN']; + } + } + } + + 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 + * + * @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 + * + * 200: Share returned + */ + #[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 does not exist')); + } + + try { + if ($this->canAccessShare($share)) { + $share = $this->formatShare($share); + + 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 does not exist')); + } + + /** + * Delete a share + * + * @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 + * + * 200: Share deleted successfully + */ + #[NoAdminRequired] + public function deleteShare(string $id): DataResponse { + try { + $share = $this->getShareById($id); + } catch (ShareNotFound $e) { + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); + } + + try { + $this->lock($share->getNode()); + } catch (LockedException $e) { + throw new OCSNotFoundException($this->l->t('Could not delete share')); + } + + if (!$this->canAccessShare($share)) { + throw new OCSNotFoundException($this->l->t('Wrong share ID, share does not exist')); + } + + // 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); + } + + return new DataResponse(); + } + + /** + * Create a share + * + * @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<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( + ?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->userId); + try { + /** @var \OC\Files\Node\Node $node */ + $node = $userFolder->get($path); + } catch (NotFoundException $e) { + 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($node); + + try { + $this->lock($share->getNode()); + } catch (LockedException $e) { + throw new OCSNotFoundException($this->l->t('Could not create share')); + } + + // 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; + } + + // 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 ($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); + } + } + + $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 account to share with')); + } + $share->setSharedWith($shareWith); + $share->setPermissions($permissions); + } elseif ($shareType === IShare::TYPE_GROUP) { + if (!$this->shareManager->allowGroupSharing()) { + throw new OCSNotFoundException($this->l->t('Group sharing is disabled by the administrator')); + } + + // Valid group is required to share + if ($shareWith === null || !$this->groupManager->groupExists($shareWith)) { + throw new OCSNotFoundException($this->l->t('Please specify a valid group')); + } + $share->setSharedWith($shareWith); + $share->setPermissions($permissions); + } 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')); + } + + $this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload); + $share->setPermissions($permissions); + + // Set password + if ($password !== '') { + $share->setPassword($password); + } + + // 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); + } + + 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 %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); + $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); + $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 = Circles::detailsCircle($shareWith); + + // Valid team is required to share + if ($circle === null) { + 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); + $this->checkInheritedAttributes($share); + + if ($note !== '') { + $share->setNote($note); + } + + try { + $share = $this->shareManager->createShare($share); + } 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) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSForbiddenException('Failed to create share.', $e); + } + + $output = $this->formatShare($share); + + return new DataResponse($output); + } + + /** + * @param null|Node $node + * @param boolean $includeTags + * + * @return list<Files_SharingShare> + */ + 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 ($filteredShares as $share) { + if ($this->canAccessShare($share)) { + try { + $formatted[] = $this->formatShare($share); + } catch (NotFoundException $e) { + // Ignore this share + } + } + } + + if ($includeTags) { + $formatted = $this->populateTags($formatted); + } + + return $formatted; + } + + /** + * @param Node $folder + * + * @return list<Files_SharingShare> + * @throws OCSBadRequestException + * @throws NotFoundException + */ + private function getSharesInDir(Node $folder): array { + if (!($folder instanceof Folder)) { + throw new OCSBadRequestException($this->l->t('Not a directory')); + } + + $nodes = $folder->getDirectoryListing(); + + /** @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 { + $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 + } + } + + if (!$resharingRight) { + $formatted = $miniFormatted; + } + + return $formatted; + } + + /** + * Get shares of the current user + * + * @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 + * + * @return DataResponse<Http::STATUS_OK, list<Files_SharingShare>, array{}> + * @throws OCSNotFoundException The folder was not found or is inaccessible + * + * 200: Shares returned + */ + #[NoAdminRequired] + public function getShares( + 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 { + $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 node')); + } + } + + $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 ($this->hasPermission($permissions, Constants::PERMISSION_READ) + && $this->shareManager->outgoingServer2ServerSharesAllowed() + && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) { + $permissions |= Constants::PERMISSION_SHARE; + } + + return $permissions; + } + + /** + * Helper to check for legacy "publicUpload" handling. + * If the value is set to `true` or `false` then true or false are returned. + * Otherwise null is returned to indicate that the option was not (or wrong) set. + * + * @param null|string $legacyPublicUpload The value of `publicUpload` + */ + private function getLegacyPublicUpload(?string $legacyPublicUpload): ?bool { + if ($legacyPublicUpload === 'true') { + return true; + } elseif ($legacyPublicUpload === 'false') { + return false; + } + // Not set at all + return null; + } + + /** + * For link and email shares validate that only allowed combinations are set. + * + * @throw OCSBadRequestException If permission combination is invalid. + * @throw OCSForbiddenException If public upload was forbidden by the administrator. + */ + private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void { + if ($legacyPublicUpload && ($node instanceof File)) { + throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders')); + } + + // We need at least READ or CREATE (file drop) + if (!$this->hasPermission($permissions, Constants::PERMISSION_READ) + && !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) { + throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions')); + } + + // UPDATE and DELETE require a READ permission + if (!$this->hasPermission($permissions, Constants::PERMISSION_READ) + && ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) { + throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set')); + } + + // Check if public uploading was disabled + if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE) + && !$this->shareManager->shareApiLinkAllowPublicUpload()) { + throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator')); + } + } + + /** + * @param string $viewer + * @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 { + $share->getNode(); + } catch (NotFoundException $e) { + /* + * 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 (!$resharingRight) { + $formatted = $miniFormatted; + } + + // fix eventual missing display name from federated shares + $formatted = $this->fixMissingDisplayName($formatted); + + if ($includeTags) { + $formatted = $this->populateTags($formatted); + } + + return $formatted; + } + + + /** + * Get all shares relative to a file, including parent folders shares rights + * + * @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( + 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 does not exist')); + } + + $this->lock($share->getNode()); + + 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 edit incoming shares')); + } + + 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 ($note !== null) { + $share->setNote($note); + } + + if ($attributes !== null) { + $share = $this->setShareAttributes($share, $attributes); + } + + // Handle mail send + if ($sendMail === 'true' || $sendMail === 'false') { + $share->setMailSend($sendMail === 'true'); + } + + /** + * 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 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); + } elseif ($password !== null) { + $share->setPassword($password); + } + + if ($label !== null) { + if (strlen($label) > 255) { + throw new OCSBadRequestException('Maximum label length is 255'); + } + $share->setLabel($label); + } + + 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 ($token !== null) { + if (!$this->shareManager->allowCustomTokens()) { + throw new OCSForbiddenException($this->l->t('Custom share link tokens have been disabled by the administrator')); + } + if (!$this->validateToken($token)) { + throw new OCSBadRequestException($this->l->t('Tokens must contain at least 1 character and may only contain letters, numbers, or a hyphen')); + } + $share->setToken($token); + } + } + + // NOT A LINK SHARE + 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); + } + } + + 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 + ]; + + 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; + } + } + } + + $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 { + $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) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new OCSBadRequestException('Failed to accept share.', $e); + } + + return new DataResponse(); + } + + /** + * 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(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->userId + || $share->getSharedBy() === $this->userId) { + return true; + } + + // If the share is shared with you, you can access it! + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $this->userId) { + return true; + } + + // Have reshare rights on the shared file/folder ? + // Does the currentUser have access to the shared file? + $userFolder = $this->rootFolder->getUserFolder($this->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->userId); + if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) { + return true; + } + } + + 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; + } + + /** + * Make sure that the passed date is valid ISO 8601 + * So YYYY-MM-DD + * If not throw an exception + * + * @param string $expireDate + * + * @throws \Exception + * @return \DateTime + */ + private function parseDate(string $expireDate): \DateTime { + try { + $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($this->l->t('Invalid date. Format must be YYYY-MM-DD')); + } + + return $date; + } + + /** + * Since we have multiple providers but the OCS Share API v1 does + * not support this we need to check all backends. + * + * @param string $id + * @return IShare + * @throws ShareNotFound + */ + private function getShareById(string $id): IShare { + $share = null; + + // First check if it is an internal share. + try { + $share = $this->shareManager->getShareById('ocinternal:' . $id, $this->userId); + return $share; + } catch (ShareNotFound $e) { + // Do nothing, just try the other share type + } + + + try { + 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) { + // Do nothing, just try the other share type + } + + try { + if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) { + $share = $this->shareManager->getShareById('sciencemesh:' . $id, $this->userId); + return $share; + } + } catch (ShareNotFound $e) { + // Do nothing, just try the other share type + } + + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + throw new ShareNotFound(); + } + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id, $this->userId); + + return $share; + } + + /** + * Lock a Node + * + * @param Node $node + * @throws LockedException + */ + 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 new file mode 100644 index 00000000000..5a776379fce --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -0,0 +1,403 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing\Controller; + +use OC\Security\CSP\ContentSecurityPolicy; +use 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\IPreview; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Security\Events\GenerateSecurePasswordEvent; +use OCP\Security\ISecureRandom; +use OCP\Security\PasswordContext; +use OCP\Share; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; +use OCP\Share\IPublicShareTemplateFactory; +use OCP\Share\IShare; + +/** + * @package OCA\Files_Sharing\Controllers + */ +#[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); + } + + /** + * Show the authentication page + * The form has to submit to the authenticate method route + */ + #[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; + } + + /** + * The template to show when authentication failed + */ + protected function showAuthFailed(): TemplateResponse { + $templateParameters = ['share' => $this->share, 'wrongpw' => true]; + + $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; + } + + /** + * The template to show after user identification + */ + protected function showIdentificationResult(bool $success = false): TemplateResponse { + $templateParameters = ['share' => $this->share, 'identityOk' => $success]; + + $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; + } + + /** + * Validate the identity token of a public share + * + * @param ?string $identityToken + * @return bool + */ + 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 IShare|string $share the Share instance if available, + * otherwise token + * @param int $errorCode + * @param string $errorMessage + * + * @throws HintException + * @throws \OC\ServerNotAvailableException + * + * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent + */ + protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') { + $itemType = $itemSource = $uidOwner = ''; + $token = $share; + $exception = null; + if ($share instanceof IShare) { + try { + $token = $share->getToken(); + $uidOwner = $share->getSharedBy(); + $itemType = $share->getNodeType(); + $itemSource = $share->getNodeId(); + } catch (\Exception $e) { + // we log what we know and pass on the exception afterwards + $exception = $e; + } + } + + \OC_Hook::emit(Share::class, 'share_link_access', [ + 'itemType' => $itemType, + 'itemSource' => $itemSource, + 'uidOwner' => $uidOwner, + 'token' => $token, + 'errorCode' => $errorCode, + 'errorMessage' => $errorMessage + ]); + + 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(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(); + } + + /** + * @param string $path + * @return TemplateResponse + * @throws NotFoundException + * @throws \Exception + */ + #[PublicPage] + #[NoCSRFRequired] + public function showShare($path = ''): TemplateResponse { + \OC_User::setIncognitoMode(true); + + // Check whether share exists + try { + $share = $this->shareManager->getShareByToken($this->getToken()); + } catch (ShareNotFound $e) { + // The share does not exists, we do not emit an ShareLinkAccessedEvent + $this->emitAccessShareHook($this->getToken(), 404, 'Share not found'); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); + } + + if (!$this->validateShare($share)) { + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); + } + + $shareNode = $share->getNode(); + + try { + $templateProvider = $this->publicShareTemplateFactory->getProvider($share); + $response = $templateProvider->renderPage($share, $this->getToken(), $path); + } catch (NotFoundException $e) { + $this->emitAccessShareHook($share, 404, 'Share not found'); + $this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found'); + throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available')); + } + + // We can't get the path of a file share + try { + if ($shareNode instanceof File && $path !== '') { + $this->emitAccessShareHook($share, 404, 'Share not found'); + $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; + } + + + $this->emitAccessShareHook($share); + $this->emitShareAccessEvent($share, self::SHARE_ACCESS); + + return $response; + } + + /** + * @NoSameSiteCookieRequired + * + * @param string $token + * @param string|null $files + * @param string $path + * @return void|Response + * @throws NotFoundException + * @deprecated 31.0.0 Users are encouraged to use the DAV endpoint + */ + #[PublicPage] + #[NoCSRFRequired] + public function downloadShare($token, $files = null, $path = '') { + \OC_User::setIncognitoMode(true); + + $share = $this->shareManager->getShareByToken($token); + + if (!($share->getPermissions() & Constants::PERMISSION_READ)) { + return new DataResponse('Share has no read permission'); + } + + $attributes = $share->getAttributes(); + if ($attributes?->getAttribute('permissions', 'download') === false) { + return new DataResponse('Share has no download permission'); + } + + if (!$this->validateShare($share)) { + throw new NotFoundException(); + } + + $node = $share->getNode(); + if ($node instanceof Folder) { + // Directory share + + // Try to get the path + if ($path !== '') { + try { + $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(); + } + } + + if ($node instanceof Folder) { + if ($files === null || $files === '') { + if ($share->getHideDownload()) { + throw new NotFoundException('Downloading a folder'); + } + } + } + } + + $this->emitAccessShareHook($share); + $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD); + + $davUrl = '/public.php/dav/files/' . $token . '/?accept=zip'; + if ($files !== null) { + $davUrl .= '&files=' . $files; + } + return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl)); + } +} diff --git a/apps/files_sharing/lib/Controller/ShareInfoController.php b/apps/files_sharing/lib/Controller/ShareInfoController.php new file mode 100644 index 00000000000..b7e79aec830 --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareInfoController.php @@ -0,0 +1,154 @@ +<?php + +/** + * 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\IRequest; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; + +/** + * @psalm-import-type Files_SharingShareInfo from ResponseDefinitions + */ +class ShareInfoController extends ApiController { + + /** + * ShareInfoController constructor. + * + * @param string $appName + * @param IRequest $request + * @param IManager $shareManager + */ + public function __construct( + string $appName, + IRequest $request, + private IManager $shareManager, + ) { + parent::__construct($appName, $request); + } + + /** + * 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{}> + * + * 200: Share info returned + * 403: Getting share info is not allowed + * 404: Share not found + */ + #[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) { + $response = new JSONResponse([], Http::STATUS_NOT_FOUND); + $response->throttle(['token' => $t]); + return $response; + } + + if ($share->getPassword() && !$this->shareManager->checkPassword($share, $password)) { + $response = new JSONResponse([], Http::STATUS_FORBIDDEN); + $response->throttle(['token' => $t]); + return $response; + } + + if (!($share->getPermissions() & Constants::PERMISSION_READ)) { + $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, $permissionMask, $depth)); + } + + /** + * @return Files_SharingShareInfo + */ + private function parseNode(Node $node, int $permissionMask, int $depth): array { + if ($node instanceof File) { + return $this->parseFile($node, $permissionMask); + } + /** @var Folder $node */ + return $this->parseFolder($node, $permissionMask, $depth); + } + + /** + * @return Files_SharingShareInfo + */ + private function parseFile(File $file, int $permissionMask): array { + return $this->format($file, $permissionMask); + } + + /** + * @return Files_SharingShareInfo + */ + private function parseFolder(Folder $folder, int $permissionMask, int $depth): array { + $data = $this->format($folder, $permissionMask); + + if ($depth === 0) { + return $data; + } + + $data['children'] = []; + + $nodes = $folder->getDirectoryListing(); + foreach ($nodes as $node) { + $data['children'][] = $this->parseNode($node, $permissionMask, $depth <= -1 ? -1 : $depth - 1); + } + + return $data; + } + + /** + * @return Files_SharingShareInfo + */ + private function format(Node $node, int $permissionMask): array { + $entry = []; + + $entry['id'] = $node->getId(); + $entry['parentId'] = $node->getParent()->getId(); + $entry['mtime'] = $node->getMTime(); + + $entry['name'] = $node->getName(); + $entry['permissions'] = $node->getPermissions() & $permissionMask; + $entry['mimetype'] = $node->getMimetype(); + $entry['size'] = $node->getSize(); + $entry['type'] = $node->getType(); + $entry['etag'] = $node->getEtag(); + + return $entry; + } +} diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php new file mode 100644 index 00000000000..0c458ce9662 --- /dev/null +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -0,0 +1,397 @@ +<?php + +declare(strict_types=1); +/** + * 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\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\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 int */ + protected $offset = 0; + + /** @var int */ + protected $limit = 10; + + /** @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 = []; + + public function __construct( + string $appName, + IRequest $request, + protected ?string $userId, + protected IConfig $config, + protected IURLGenerator $urlGenerator, + protected IManager $shareManager, + protected ISearch $collaboratorSearch, + ) { + parent::__construct($appName, $request); + } + + /** + * Search for sharees + * + * @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 + */ + #[NoAdminRequired] + public function search(string $search = '', ?string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse { + + // only search for string larger than a given threshold + $threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0); + 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 = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT); + if ($maxResults > 0) { + $perPage = min($perPage, $maxResults); + } + if ($perPage <= 0) { + throw new OCSBadRequestException('Invalid perPage argument'); + } + if ($page <= 0) { + throw new OCSBadRequestException('Invalid page'); + } + + $shareTypes = [ + IShare::TYPE_USER, + ]; + + if ($itemType === null) { + throw new OCSBadRequestException('Missing itemType'); + } elseif ($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; + } + + if ($this->shareManager->shareProviderExists(IShare::TYPE_SCIENCEMESH)) { + $shareTypes[] = IShare::TYPE_SCIENCEMESH; + } + } else { + if ($this->shareManager->allowGroupSharing()) { + $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 ($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->limit = $perPage; + $this->offset = $perPage * ($page - 1); + + // 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'])) { + $result['exact'] = array_merge($this->result['exact'], $result['exact']); + } + $this->result = array_merge($this->result, $result); + $response = new DataResponse($this->result); + + if ($hasMoreResults) { + $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(string $itemType): bool { + try { + // FIXME: static foo makes unit testing unnecessarily difficult + $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; + } + } + + + /** + * Generates a bunch of pagination links for the current page + * + * @param int $page Current page + * @param array $params Parameters for the URL + * @return string + */ + 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; + return '<' . $url . http_build_query($params) . '>; rel="next"'; + } + + /** + * @return bool + */ + 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 new file mode 100644 index 00000000000..63f057e3bf4 --- /dev/null +++ b/apps/files_sharing/lib/DeleteOrphanedSharesJob.php @@ -0,0 +1,133 @@ +<?php + +declare(strict_types=1); +/** + * 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 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 { + + use TTransactional; + + private const CHUNK_SIZE = 1000; + + private const INTERVAL = 24 * 60 * 60; + + /** + * sets the correct interval for this timed job + */ + 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); + } + + /** + * Makes the background job do its work + * + * @param array $argument unused argument + */ + public function run($argument) { + 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); + + $deleteQb = $this->db->getQueryBuilder(); + $deleteQb->delete('share') + ->where( + $deleteQb->expr()->in('file_source', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY) + ); + + $chunks = array_chunk($sourceFiles, self::CHUNK_SIZE); + foreach ($chunks as $chunk) { + $deletedFiles = $this->findMissingSources($chunk); + $this->atomic(function () use ($deletedFiles, $deleteQb) { + $deleteQb->setParameter('ids', $deletedFiles, IQueryBuilder::PARAM_INT_ARRAY); + $deleted = $deleteQb->executeStatement(); + $this->logger->debug('{deleted} orphaned share(s) deleted', [ + 'app' => 'DeleteOrphanedSharesJob', + 'deleted' => $deleted, + ]); + return $deleted; + }, $this->db); + } + } + + 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 new file mode 100644 index 00000000000..a68a8fc05d4 --- /dev/null +++ b/apps/files_sharing/lib/Exceptions/BrokenPath.php @@ -0,0 +1,17 @@ +<?php + +/** + * 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 new file mode 100644 index 00000000000..10360820432 --- /dev/null +++ b/apps/files_sharing/lib/Exceptions/S2SException.php @@ -0,0 +1,13 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing\Exceptions; + +/** + * S2S sharing not allowed + */ +class S2SException extends \Exception { +} 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 new file mode 100644 index 00000000000..b1c6c592e80 --- /dev/null +++ b/apps/files_sharing/lib/ExpireSharesJob.php @@ -0,0 +1,79 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\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 { + + 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) { + //Current time + $now = new \DateTime(); + $now = $now->format('Y-m-d H:i:s'); + + /* + * Expire file link shares only (for now) + */ + $qb = $this->db->getQueryBuilder(); + $qb->select('id', 'share_type') + ->from('share') + ->where( + $qb->expr()->andX( + $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()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)) + ) + ); + + $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 new file mode 100644 index 00000000000..027f682d818 --- /dev/null +++ b/apps/files_sharing/lib/External/Cache.php @@ -0,0 +1,51 @@ +<?php + +/** + * 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 { + private $remote; + private $remoteUser; + + /** + * @param Storage $storage + * @param ICloudId $cloudId + */ + public function __construct( + private $storage, + private ICloudId $cloudId, + ) { + [, $remote] = explode('://', $this->cloudId->getRemote(), 2); + $this->remote = $remote; + $this->remoteUser = $this->cloudId->getUser(); + parent::__construct($this->storage); + } + + public function get($file) { + $result = parent::get($file); + if (!$result) { + return false; + } + $result['displayname_owner'] = $this->cloudId->getDisplayId(); + if (!$file || $file === '') { + $result['is_share_mount_point'] = true; + $mountPoint = rtrim($this->storage->getMountPoint()); + $result['name'] = basename($mountPoint); + } + return $result; + } + + public function getFolderContentsById($fileId) { + $results = parent::getFolderContentsById($fileId); + foreach ($results as &$file) { + $file['displayname_owner'] = $this->cloudId->getDisplayId(); + } + return $results; + } +} diff --git a/apps/files_sharing/lib/External/Manager.php b/apps/files_sharing/lib/External/Manager.php new file mode 100644 index 00000000000..ff4781eba0f --- /dev/null +++ b/apps/files_sharing/lib/External/Manager.php @@ -0,0 +1,838 @@ +<?php + +/** + * 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 { + public const STORAGE = '\OCA\Files_Sharing\External\Storage'; + + /** @var string|null */ + private $uid; + + 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; + } + + /** + * add new server-to-server share + * + * @param string $remote + * @param string $token + * @param string $password + * @param string $name + * @param string $owner + * @param int $shareType + * @param boolean $accepted + * @param string $user + * @param string $remoteId + * @param int $parent + * @return Mount|null + * @throws \Doctrine\DBAL\Exception + */ + 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 !== 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, + // using the original share item name. + $tmpMountPointName = '{{TemporaryMountPointName#' . $name . '}}'; + $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, + 'share_type' => $shareType, + ]; + + $i = 1; + while (!$this->connection->insertIfNotExist('*PREFIX*share_external', $data, ['user', 'mountpoint_hash'])) { + // The external share already exists for the user + $data['mountpoint'] = $tmpMountPointName . '-' . $i; + $data['mountpoint_hash'] = md5($data['mountpoint']); + $i++; + } + return null; + } + + $mountPoint = Files::buildNotExistingFileName('/', $name); + $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`, `parent`, `share_type`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + '); + $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 by token + * + * @param string $token + * @return mixed share of false + */ + private function fetchShareByToken($token) { + $getShare = $this->connection->prepare(' + SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`, `parent`, `share_type`, `password`, `mountpoint_hash` + FROM `*PREFIX*share_external` + WHERE `share_token` = ?'); + $result = $getShare->execute([$token]); + $share = $result->fetch(); + $result->closeCursor(); + return $share; + } + + 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(); + } + + /** + * accept server-to-server share + * + * @param int $id + * @return bool True if the share could be accepted, false otherwise + */ + public function acceptShare(int $id, ?string $user = null) { + // If we're auto-accepting a share, we need to know the user id + // as there is no session available while processing the share + // from the remote server request. + $user = $user ?? $this->uid; + if ($user === null) { + $this->logger->error('No user specified for accepting share'); + return false; + } + + $share = $this->getShare($id, $user); + $result = false; + + if ($share) { + \OC_Util::setupFS($user); + $shareFolder = Helper::getShareFolder(null, $user); + $mountPoint = Files::buildNotExistingFileName($shareFolder, $share['name']); + $mountPoint = Filesystem::normalizePath($mountPoint); + $hash = md5($mountPoint); + $userShareAccepted = false; + + 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` = ?'); + $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'); + $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, $user); + + return $result; + } + + /** + * decline server-to-server share + * + * @param int $id + * @return bool True if the share could be declined, false otherwise + */ + public function declineShare(int $id, ?string $user = null) { + $user = $user ?? $this->uid; + if ($user === null) { + $this->logger->error('No user specified for declining share'); + return false; + } + + $share = $this->getShare($id, $user); + $result = false; + + if ($share && (int)$share['share_type'] === IShare::TYPE_USER) { + $removeShare = $this->connection->prepare(' + DELETE FROM `*PREFIX*share_external` WHERE `id` = ? AND `user` = ?'); + $removeShare->execute([$id, $user]); + $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); + + $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 $result; + } + + 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($user) + ->setObject('remote_share', (string)$remoteShare); + $this->notificationManager->markProcessed($filter); + } + + /** + * inform remote server whether server-to-server share was accepted/declined + * + * @param string $remote + * @param string $token + * @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 = $federationEndpoints['share'] ?? '/ocs/v2.php/cloud/shares'; + + $url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=' . Share::RESPONSE_FORMAT; + $fields = ['token' => $token]; + + $client = $this->clientService->newClient(); + + try { + $response = $client->post( + $url, + [ + 'body' => $fields, + 'connect_timeout' => 10, + ] + ); + } catch (\Exception $e) { + return false; + } + + $status = json_decode($response->getBody(), true); + + return ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200); + } + + /** + * 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 + * @return string + */ + protected function stripPath($path) { + $prefix = '/' . $this->uid . '/files'; + return rtrim(substr($path, strlen($prefix)), '/'); + } + + public function getMount($data, ?string $user = null) { + $user = $user ?? $this->uid; + $data['manager'] = $this; + $mountPoint = '/' . $user . '/files' . $data['mountpoint']; + $data['mountpoint'] = $mountPoint; + $data['certificateManager'] = \OC::$server->getCertificateManager(); + return new Mount(self::STORAGE, $mountPoint, $data, $this, $this->storageLoader); + } + + /** + * @param array $data + * @return Mount + */ + protected function mountShare($data, ?string $user = null) { + $mount = $this->getMount($data, $user); + $this->mountManager->addMount($mount); + return $mount; + } + + /** + * @return \OC\Files\Mount\Manager + */ + public function getMountManager() { + return $this->mountManager; + } + + /** + * @param string $source + * @param string $target + * @return bool + */ + public function setMountPoint($source, $target) { + $source = $this->stripPath($source); + $target = $this->stripPath($target); + $sourceHash = md5($source); + $targetHash = md5($target); + + $query = $this->connection->prepare(' + UPDATE `*PREFIX*share_external` + SET `mountpoint` = ?, `mountpoint_hash` = ? + WHERE `mountpoint_hash` = ? + AND `user` = ? + '); + $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): 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); + + 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); + } + + $this->removeReShares($id); + } catch (\Doctrine\DBAL\Exception $ex) { + $this->logger->emergency('Could not update share', ['exception' => $ex]); + return false; + } + + return true; + } + + /** + * remove re-shares from share table and mapping in the federated_reshares table + * + * @param $mountPointId + */ + protected function removeReShares($mountPointId) { + $selectQuery = $this->connection->getQueryBuilder(); + $query = $this->connection->getQueryBuilder(); + $selectQuery->select('id')->from('share') + ->where($selectQuery->expr()->eq('file_source', $query->createNamedParameter($mountPointId))); + $select = $selectQuery->getSQL(); + + + $query->delete('federated_reshares') + ->where($query->expr()->in('share_id', $query->createFunction($select))); + $query->execute(); + + $deleteReShares = $this->connection->getQueryBuilder(); + $deleteReShares->delete('share') + ->where($deleteReShares->expr()->eq('file_source', $deleteReShares->createNamedParameter($mountPointId))); + $deleteReShares->execute(); + } + + /** + * remove all shares for user $uid if the user was deleted + * + * @param string $uid + */ + 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; + } + + 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 list<Files_SharingRemoteShare> list of open server-to-server shares + */ + public function getOpenShares() { + return $this->getShares(false); + } + + /** + * return a list of shares which are accepted by the user + * + * @return list<Files_SharingRemoteShare> list of accepted server-to-server shares + */ + public function getAcceptedShares() { + return $this->getShares(true); + } + + /** + * return a list of shares for the user + * + * @param bool|null $accepted True for accepted only, + * false for not accepted, + * null for all shares of the user + * @return list<Files_SharingRemoteShare> list of open server-to-server shares + */ + private function getShares($accepted) { + // Not allowing providing a user here, + // as we only want to retrieve shares for the current user. + $user = $this->userManager->get($this->uid); + $groups = $this->groupManager->getUserGroups($user); + $userGroups = []; + foreach ($groups as $group) { + $userGroups[] = $group->getGID(); + } + + $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'); + + 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 new file mode 100644 index 00000000000..f50c379f85f --- /dev/null +++ b/apps/files_sharing/lib/External/Mount.php @@ -0,0 +1,63 @@ +<?php + +/** + * 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, ISharedMountPoint { + + /** + * @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, + protected $manager, + $loader = null, + ) { + parent::__construct($storage, $mountpoint, $options, $loader, null, null, MountProvider::class); + } + + /** + * Move the mount point to $target + * + * @param string $target the target mount point + * @return bool + */ + public function moveMount($target) { + $result = $this->manager->setMountPoint($this->mountPoint, $target); + $this->setMountPoint($target); + + return $result; + } + + /** + * Remove the mount points + */ + public function removeMount(): bool { + return $this->manager->removeShare($this->mountPoint); + } + + /** + * Get the type of mount point, used to distinguish things like shares and external storage + * in the web interface + * + * @return string + */ + public function getMountType() { + return 'shared'; + } +} diff --git a/apps/files_sharing/lib/External/MountProvider.php b/apps/files_sharing/lib/External/MountProvider.php new file mode 100644 index 00000000000..a5781d5d35a --- /dev/null +++ b/apps/files_sharing/lib/External/MountProvider.php @@ -0,0 +1,68 @@ +<?php + +/** + * 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 { + public const STORAGE = '\OCA\Files_Sharing\External\Storage'; + + /** + * @var callable + */ + private $managerProvider; + + /** + * @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( + private IDBConnection $connection, + callable $managerProvider, + private ICloudIdManager $cloudIdManager, + ) { + $this->managerProvider = $managerProvider; + } + + public function getMount(IUser $user, $data, IStorageFactory $storageFactory) { + $managerProvider = $this->managerProvider; + $manager = $managerProvider(); + $data['manager'] = $manager; + $mountPoint = '/' . $user->getUID() . '/files/' . ltrim($data['mountpoint'], '/'); + $data['mountpoint'] = $mountPoint; + $data['cloudId'] = $this->cloudIdManager->getCloudId($data['owner'], $data['remote']); + $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) { + $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 = $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 new file mode 100644 index 00000000000..0d57248595b --- /dev/null +++ b/apps/files_sharing/lib/External/Scanner.php @@ -0,0 +1,55 @@ +<?php + +/** + * 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 Storage */ + protected $storage; + + public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) { + // Disable locking for federated shares + parent::scan($path, $recursive, $reuse, false); + } + + /** + * Scan a single file and store it in the cache. + * If an exception happened while accessing the external storage, + * the storage will be checked for availability and removed + * if it is not available any more. + * + * @param string $file file to scan + * @param int $reuseExisting + * @param int $parentId + * @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 + * @param array|null $data the metadata for the file, as returned by the storage + * @return array|null an array of metadata of the scanned file + */ + public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { + try { + return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data); + } catch (ForbiddenException $e) { + $this->storage->checkStorageAvailability(); + } catch (NotFoundException $e) { + // if the storage isn't found, the call to + // checkStorageAvailable() will verify it and remove it + // if appropriate + $this->storage->checkStorageAvailability(); + } catch (StorageInvalidException $e) { + $this->storage->checkStorageAvailability(); + } catch (StorageNotAvailableException $e) { + $this->storage->checkStorageAvailability(); + } + } +} diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php new file mode 100644 index 00000000000..a9781b91a6c --- /dev/null +++ b/apps/files_sharing/lib/External/Storage.php @@ -0,0 +1,429 @@ +<?php + +declare(strict_types=1); +/** + * 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; +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; + + /** + * @param array{HttpClientService: IClientService, manager: ExternalShareManager, cloudId: ICloudId, mountpoint: string, token: string, password: ?string}|array $options + */ + public function __construct($options) { + $this->memcacheFactory = Server::get(ICacheFactory::class); + $this->httpClient = $options['HttpClientService']; + $this->manager = $options['manager']; + $this->cloudId = $options['cloudId']; + $this->logger = Server::get(LoggerInterface::class); + $discoveryService = Server::get(IOCMDiscoveryService::class); + $this->config = Server::get(IConfig::class); + + // use default path to webdav if not found on discovery + try { + $ocmProvider = $discoveryService->discover($this->cloudId->getRemote()); + $webDavEndpoint = $ocmProvider->extractProtocolEntry('file', 'webdav'); + $remote = $ocmProvider->getEndPoint(); + } catch (OCMProviderException|OCMArgumentException $e) { + $this->logger->notice('exception while retrieving webdav endpoint', ['exception' => $e]); + $webDavEndpoint = '/public.php/webdav'; + $remote = $this->cloudId->getRemote(); + } + + $host = parse_url($remote, PHP_URL_HOST); + $port = parse_url($remote, PHP_URL_PORT); + $host .= ($port === null) ? '' : ':' . $port; // we add port if available + + // in case remote NC is on a sub folder and using deprecated ocm provider + $tmpPath = rtrim(parse_url($this->cloudId->getRemote(), PHP_URL_PATH) ?? '', '/'); + if (!str_starts_with($webDavEndpoint, $tmpPath)) { + $webDavEndpoint = $tmpPath . $webDavEndpoint; + } + + $this->mountPoint = $options['mountpoint']; + $this->token = $options['token']; + + 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(string $path = '', ?IStorage $storage = null): IWatcher { + if (!$storage) { + $storage = $this; + } + if (!isset($this->watcher)) { + $this->watcher = new Watcher($storage); + $this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE); + } + return $this->watcher; + } + + public function getRemoteUser(): string { + return $this->cloudId->getUser(); + } + + public function getRemote(): string { + return $this->cloudId->getRemote(); + } + + public function getMountPoint(): string { + return $this->mountPoint; + } + + public function getToken(): string { + return $this->token; + } + + public function getPassword(): ?string { + return $this->password; + } + + public function getId(): string { + return 'shared::' . md5($this->token . '@' . $this->getRemote()); + } + + public function getCache(string $path = '', ?IStorage $storage = null): ICache { + if (is_null($this->cache)) { + $this->cache = new Cache($this, $this->cloudId); + } + return $this->cache; + } + + 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; + } + + 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) { + return false; + } + $this->updateChecked = true; + try { + return parent::hasUpdated('', $time); + } catch (StorageInvalidException $e) { + // check if it needs to be removed + $this->checkStorageAvailability(); + throw $e; + } catch (StorageNotAvailableException $e) { + // check if it needs to be removed or just temp unavailable + $this->checkStorageAvailability(); + throw $e; + } + } + + public function test(): bool { + try { + return parent::test(); + } catch (StorageInvalidException $e) { + // check if it needs to be removed + $this->checkStorageAvailability(); + throw $e; + } catch (StorageNotAvailableException $e) { + // check if it needs to be removed or just temp unavailable + $this->checkStorageAvailability(); + throw $e; + } + } + + /** + * Check whether this storage is permanently or temporarily + * unavailable + * + * @throws StorageNotAvailableException + * @throws StorageInvalidException + */ + public function checkStorageAvailability() { + // see if we can find out why the share is unavailable + try { + $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()) { + // valid Nextcloud instance means that the public share no longer exists + // since this is permanent (re-sharing the file will create a new token) + // we remove the invalid storage + $this->manager->removeShare($this->mountPoint); + $this->manager->getMountManager()->removeMount($this->mountPoint); + throw new StorageInvalidException('Remote share not found', 0, $e); + } else { + // Nextcloud instance is gone, likely to be a temporary server configuration error + throw new StorageNotAvailableException('No nextcloud instance found at remote', 0, $e); + } + } catch (ForbiddenException $e) { + // auth error, remove share for now (provide a dialog in the future) + $this->manager->removeShare($this->mountPoint); + $this->manager->getMountManager()->removeMount($this->mountPoint); + throw new StorageInvalidException('Auth error when getting remote share'); + } catch (\GuzzleHttp\Exception\ConnectException $e) { + throw new StorageNotAvailableException('Failed to connect to remote instance', 0, $e); + } catch (\GuzzleHttp\Exception\RequestException $e) { + throw new StorageNotAvailableException('Error while sending request to remote instance', 0, $e); + } + } + + public function file_exists(string $path): bool { + if ($path === '') { + return true; + } else { + return parent::file_exists($path); + } + } + + /** + * Check if the configured remote is a valid federated share provider + * + * @return bool + */ + protected function testRemote(): bool { + try { + 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; + } + } + + private function testRemoteUrl(string $url): bool { + $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url'); + $cached = $cache->get($url); + if ($cached !== null) { + return (bool)$cached; + } + + $client = $this->httpClient->newClient(); + try { + $result = $client->get($url, $this->getDefaultRequestOptions())->getBody(); + $data = json_decode($result); + $returnValue = (is_object($data) && !empty($data->version)); + } catch (ConnectException|ClientException|RequestException $e) { + $returnValue = false; + $this->logger->warning('Failed to test remote URL', ['exception' => $e]); + } + + $cache->set($url, $returnValue, 60 * 60 * 24); + return $returnValue; + } + + /** + * Check whether the remote is an ownCloud/Nextcloud. This is needed since some sharing + * features are not standardized. + * + * @throws LocalServerException + */ + public function remoteIsOwnCloud(): bool { + if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) { + return false; + } + return true; + } + + /** + * @return mixed + * @throws ForbiddenException + * @throws NotFoundException + * @throws \Exception + */ + public function getShareInfo(int $depth = -1) { + $remote = $this->getRemote(); + $token = $this->getToken(); + $password = $this->getPassword(); + + 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 = Server::get(IClientService::class)->newClient(); + try { + $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(); + } + if ($e->getCode() === Http::STATUS_NOT_FOUND) { + throw new NotFoundException(); + } + // 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(); + } + + return json_decode($response->getBody(), true); + } + + public function getOwner(string $path): string|false { + return $this->cloudId->getDisplayId(); + } + + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { + return false; + } + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); + } + + public function getPermissions(string $path): int { + $response = $this->propfind($path); + if ($response === false) { + return 0; + } + + $ocsPermissions = $response['{http://open-collaboration-services.org/ns}share-permissions'] ?? null; + $ocmPermissions = $response['{http://open-cloud-mesh.org/ns}share-permissions'] ?? null; + $ocPermissions = $response['{http://owncloud.org/ns}permissions'] ?? null; + // old federated sharing permissions + if ($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 + $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 new file mode 100644 index 00000000000..f3616feabba --- /dev/null +++ b/apps/files_sharing/lib/External/Watcher.php @@ -0,0 +1,19 @@ +<?php + +/** + * 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 { + /** + * remove deleted files in $path from the cache + * + * @param string $path + */ + public function cleanFolder($path) { + // not needed, the scanner takes care of this + } +} diff --git a/apps/files_sharing/lib/Helper.php b/apps/files_sharing/lib/Helper.php new file mode 100644 index 00000000000..92e874b73db --- /dev/null +++ b/apps/files_sharing/lib/Helper.php @@ -0,0 +1,94 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OC\Files\Filesystem; +use OC\Files\View; +use OCA\Files_Sharing\AppInfo\Application; +use OCP\IConfig; +use OCP\Server; +use OCP\Util; + +class Helper { + public static function registerHooks() { + Util::connectHook('OC_Filesystem', 'post_rename', '\OCA\Files_Sharing\Updater', 'renameHook'); + Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren'); + + 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 View $view + * @return string $path + */ + public static function generateUniqueTarget($path, $view) { + $pathinfo = pathinfo($path); + $ext = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : ''; + $name = $pathinfo['filename']; + $dir = $pathinfo['dirname']; + $i = 2; + while ($view->file_exists($path)) { + $path = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext); + $i++; + } + + return $path; + } + + /** + * get default share folder + * + * @param View|null $view + * @param string|null $userId + * @return string + */ + public static function getShareFolder(?View $view = null, ?string $userId = null): string { + if ($view === null) { + $view = Filesystem::getView(); + } + + $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); + foreach ($subdirs as $subdir) { + $dir = $dir . '/' . $subdir; + if (!$view->is_dir($dir)) { + $view->mkdir($dir); + } + } + } + + return $shareFolder; + } + + /** + * set default share folder + * + * @param string $shareFolder + */ + public static function setShareFolder($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 new file mode 100644 index 00000000000..e90b9f5c23d --- /dev/null +++ b/apps/files_sharing/lib/Hooks.php @@ -0,0 +1,35 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OC\Files\Filesystem; +use OC\Files\View; +use OCP\Server; + +class Hooks { + public static function deleteUser($params) { + $manager = Server::get(External\Manager::class); + + $manager->removeUserShares($params['uid']); + } + + public static function unshareChildren($params) { + $path = Filesystem::getView()->getAbsolutePath($params['path']); + $view = new View('/'); + + // find share mount points within $path and unmount them + $mountManager = Filesystem::getMountManager(); + $mountedShares = $mountManager->findIn($path); + foreach ($mountedShares as $mount) { + 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 new file mode 100644 index 00000000000..9bd3e4c9476 --- /dev/null +++ b/apps/files_sharing/lib/ISharedStorage.php @@ -0,0 +1,16 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OCP\Files\Storage\IStorage; + +/** + * @deprecated 30.0.0 use `\OCP\Files\Storage\ISharedStorage` instead + */ +interface ISharedStorage extends IStorage { +} diff --git a/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php b/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php new file mode 100644 index 00000000000..717edd4869e --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\ViewOnly; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; +use OCP\Files\IRootFolder; +use OCP\IUserSession; + +/** + * @template-implements IEventListener<BeforeDirectFileDownloadEvent|Event> + */ +class BeforeDirectFileDownloadListener implements IEventListener { + + public function __construct( + private IUserSession $userSession, + private IRootFolder $rootFolder, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeDirectFileDownloadEvent)) { + return; + } + + $pathsToCheck = [$event->getPath()]; + // Check only for user/group shares. Don't restrict e.g. share links + $user = $this->userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $this->rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setSuccessful(false); + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + } + } + } +} diff --git a/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php b/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php new file mode 100644 index 00000000000..d19bc8dfae9 --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeNodeReadListener.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\Activity\Providers\Downloads; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\Events\Node\BeforeNodeReadEvent; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IRequest; +use OCP\ISession; +use OCP\Share\IShare; + +/** + * @template-implements IEventListener<BeforeNodeReadEvent|BeforeZipCreatedEvent|Event> + */ +class BeforeNodeReadListener implements IEventListener { + private ICache $cache; + + public function __construct( + private ISession $session, + private IRootFolder $rootFolder, + private \OCP\Activity\IManager $activityManager, + private IRequest $request, + ICacheFactory $cacheFactory, + ) { + $this->cache = $cacheFactory->createDistributed('files_sharing_activity_events'); + } + + public function handle(Event $event): void { + if ($event instanceof BeforeZipCreatedEvent) { + $this->handleBeforeZipCreatedEvent($event); + } elseif ($event instanceof BeforeNodeReadEvent) { + $this->handleBeforeNodeReadEvent($event); + } + } + + public function handleBeforeZipCreatedEvent(BeforeZipCreatedEvent $event): void { + $files = $event->getFiles(); + if (count($files) !== 0) { + /* No need to do anything, activity will be triggered for each file in the zip by the BeforeNodeReadEvent */ + return; + } + + $node = $event->getFolder(); + if (!($node instanceof Folder)) { + return; + } + + try { + $storage = $node->getStorage(); + } catch (NotFoundException) { + return; + } + + if (!$storage->instanceOfStorage(ISharedStorage::class)) { + return; + } + + /** @var ISharedStorage $storage */ + $share = $storage->getShare(); + + if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) { + return; + } + + /* Cache that that folder download activity was published */ + $this->cache->set($this->request->getId(), $node->getPath(), 3600); + + $this->singleFileDownloaded($share, $node); + } + + public function handleBeforeNodeReadEvent(BeforeNodeReadEvent $event): void { + $node = $event->getNode(); + if (!($node instanceof File)) { + return; + } + + try { + $storage = $node->getStorage(); + } catch (NotFoundException) { + return; + } + + if (!$storage->instanceOfStorage(ISharedStorage::class)) { + return; + } + + /** @var ISharedStorage $storage */ + $share = $storage->getShare(); + + if (!in_array($share->getShareType(), [IShare::TYPE_EMAIL, IShare::TYPE_LINK])) { + return; + } + + $path = $this->cache->get($this->request->getId()); + if (is_string($path) && str_starts_with($node->getPath(), $path)) { + /* An activity was published for a containing folder already */ + return; + } + + /* Avoid publishing several activities for one video playing */ + $cacheKey = $node->getId() . $node->getPath() . $this->session->getId(); + if (($this->request->getHeader('range') !== '') && ($this->cache->get($cacheKey) === 'true')) { + /* This is a range request and an activity for the same file was published in the same session */ + return; + } + $this->cache->set($cacheKey, 'true', 3600); + + $this->singleFileDownloaded($share, $node); + } + + /** + * create activity if a single file or folder was downloaded from a link share + */ + protected function singleFileDownloaded(IShare $share, File|Folder $node): void { + $fileId = $node->getId(); + + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); + $userNode = $userFolder->getFirstNodeById($fileId); + $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); + $userPath = $userFolder->getRelativePath($userNode?->getPath() ?? '') ?? ''; + $ownerPath = $ownerFolder->getRelativePath($node->getPath()) ?? ''; + + $parameters = [$userPath]; + + if ($share->getShareType() === IShare::TYPE_EMAIL) { + if ($node instanceof File) { + $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED; + } else { + $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED; + } + $parameters[] = $share->getSharedWith(); + } elseif ($share->getShareType() === IShare::TYPE_LINK) { + if ($node instanceof File) { + $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED; + } else { + $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED; + } + $remoteAddress = $this->request->getRemoteAddress(); + $dateTime = new \DateTime(); + $dateTime = $dateTime->format('Y-m-d H'); + $remoteAddressHash = md5($dateTime . '-' . $remoteAddress); + $parameters[] = $remoteAddressHash; + } else { + return; + } + + $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath); + + if ($share->getShareOwner() !== $share->getSharedBy()) { + $parameters[0] = $ownerPath; + $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath); + } + } + + /** + * publish activity + */ + protected function publishActivity( + string $subject, + array $parameters, + string $affectedUser, + int $fileId, + string $filePath, + ): void { + $event = $this->activityManager->generateEvent(); + $event->setApp('files_sharing') + ->setType('public_links') + ->setSubject($subject, $parameters) + ->setAffectedUser($affectedUser) + ->setObject('files', $fileId, $filePath); + $this->activityManager->publish($event); + } +} diff --git a/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php b/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php new file mode 100644 index 00000000000..1fc62bfe0fa --- /dev/null +++ b/apps/files_sharing/lib/Listener/BeforeZipCreatedListener.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Listener; + +use OCA\Files_Sharing\ViewOnly; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\BeforeZipCreatedEvent; +use OCP\Files\IRootFolder; +use OCP\IUserSession; + +/** + * @template-implements IEventListener<BeforeZipCreatedEvent|Event> + */ +class BeforeZipCreatedListener implements IEventListener { + + public function __construct( + private IUserSession $userSession, + private IRootFolder $rootFolder, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof BeforeZipCreatedEvent)) { + return; + } + + $dir = $event->getDirectory(); + $files = $event->getFiles(); + + $pathsToCheck = []; + foreach ($files as $file) { + $pathsToCheck[] = $dir . '/' . $file; + } + + // Check only for user/group shares. Don't restrict e.g. share links + $user = $this->userSession->getUser(); + if ($user) { + $viewOnlyHandler = new ViewOnly( + $this->rootFolder->getUserFolder($user->getUID()) + ); + if (!$viewOnlyHandler->check($pathsToCheck)) { + $event->setErrorMessage('Access to this resource or one of its sub-items has been denied.'); + $event->setSuccessful(false); + } else { + $event->setSuccessful(true); + } + } else { + $event->setSuccessful(true); + } + } +} diff --git a/apps/files_sharing/lib/Listener/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 new file mode 100644 index 00000000000..6671a78efff --- /dev/null +++ b/apps/files_sharing/lib/Middleware/OCSShareAPIMiddleware.php @@ -0,0 +1,52 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Middleware; + +use OCA\Files_Sharing\Controller\ShareAPIController; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\IL10N; +use OCP\Share\IManager; + +class OCSShareAPIMiddleware extends Middleware { + public function __construct( + private IManager $shareManager, + private IL10N $l, + ) { + } + + /** + * @param Controller $controller + * @param string $methodName + * + * @throws OCSNotFoundException + */ + public function beforeController($controller, $methodName) { + if ($controller instanceof ShareAPIController) { + if (!$this->shareManager->shareApiEnabled()) { + throw new OCSNotFoundException($this->l->t('Share API is disabled')); + } + } + } + + /** + * @param Controller $controller + * @param string $methodName + * @param Response $response + * @return Response + */ + public function afterController($controller, $methodName, Response $response) { + if ($controller instanceof ShareAPIController) { + /** @var ShareAPIController $controller */ + $controller->cleanup(); + } + + return $response; + } +} diff --git a/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php b/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php new file mode 100644 index 00000000000..e96940979bf --- /dev/null +++ b/apps/files_sharing/lib/Middleware/ShareInfoMiddleware.php @@ -0,0 +1,87 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Sharing\Middleware; + +use OCA\Files_Sharing\Controller\ShareInfoController; +use OCA\Files_Sharing\Exceptions\S2SException; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\Share\IManager; + +class ShareInfoMiddleware extends Middleware { + public function __construct( + private IManager $shareManager, + ) { + } + + /** + * @param Controller $controller + * @param string $methodName + * @throws S2SException + */ + public function beforeController($controller, $methodName) { + if (!($controller instanceof ShareInfoController)) { + return; + } + + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + throw new S2SException(); + } + } + + /** + * @param Controller $controller + * @param string $methodName + * @param \Exception $exception + * @throws \Exception + * @return Response + */ + public function afterException($controller, $methodName, \Exception $exception) { + if (!($controller instanceof ShareInfoController)) { + throw $exception; + } + + if ($exception instanceof S2SException) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + throw $exception; + } + + /** + * @param Controller $controller + * @param string $methodName + * @param Response $response + * @return Response + */ + public function afterController($controller, $methodName, Response $response) { + if (!($controller instanceof ShareInfoController)) { + return $response; + } + + if (!($response instanceof JSONResponse)) { + return $response; + } + + $data = $response->getData(); + $status = 'error'; + + if ($response->getStatus() === Http::STATUS_OK) { + $status = 'success'; + } + + $response->setData([ + 'data' => $data, + 'status' => $status, + ]); + + return $response; + } +} diff --git a/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php b/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php new file mode 100644 index 00000000000..8ea2eb59d73 --- /dev/null +++ b/apps/files_sharing/lib/Middleware/SharingCheckMiddleware.php @@ -0,0 +1,113 @@ +<?php + +declare(strict_types=1); +/** + * 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\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\IRequest; +use OCP\Share\IManager; + +/** + * Checks whether the "sharing check" is enabled + * + * @package OCA\Files_Sharing\Middleware + */ +class SharingCheckMiddleware extends Middleware { + + public function __construct( + protected string $appName, + protected IConfig $config, + protected IAppManager $appManager, + protected IControllerMethodReflector $reflector, + protected IManager $shareManager, + protected IRequest $request, + ) { + } + + /** + * Check if sharing is enabled before the controllers is executed + * + * @param Controller $controller + * @param string $methodName + * @throws NotFoundException + * @throws S2SException + */ + public function beforeController($controller, $methodName): void { + if (!$this->isSharingEnabled()) { + throw new NotFoundException('Sharing is disabled.'); + } + + if ($controller instanceof ExternalSharesController + && !$this->externalSharesChecks()) { + throw new S2SException('Federated sharing not allowed'); + } + } + + /** + * Return 404 page in case of a not found exception + * + * @param Controller $controller + * @param string $methodName + * @param \Exception $exception + * @return Response + * @throws \Exception + */ + public function afterException($controller, $methodName, \Exception $exception): Response { + if (is_a($exception, NotFoundException::class)) { + return new NotFoundResponse(); + } + + if (is_a($exception, S2SException::class)) { + return new JSONResponse($exception->getMessage(), 405); + } + + throw $exception; + } + + /** + * Checks for externalshares controller + * @return bool + */ + 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') { + return false; + } + + return true; + } + + /** + * Check whether sharing is enabled + * @return bool + */ + 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)) { + return false; + } + + return true; + } +} diff --git a/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php new file mode 100644 index 00000000000..3718306e380 --- /dev/null +++ b/apps/files_sharing/lib/Migration/OwncloudGuestShareType.php @@ -0,0 +1,58 @@ +<?php + +/** + * 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\IShare; + +/** + * Class OwncloudGuestShareType + * + * @package OCA\Files_Sharing\Migration + */ +class OwncloudGuestShareType 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() { + return 'Fix the share type of guest shares when migrating from ownCloud'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) { + if (!$this->shouldRun()) { + return; + } + + $query = $this->connection->getQueryBuilder(); + $query->update('share') + ->set('share_type', $query->createNamedParameter(IShare::TYPE_GUEST)) + ->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_EMAIL))); + $query->execute(); + } + + protected function shouldRun() { + $appVersion = $this->config->getAppValue('files_sharing', 'installed_version', '0.0.0'); + return $appVersion === '0.10.0' + || $this->config->getAppValue('core', 'vendor', '') === 'owncloud'; + } +} 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 new file mode 100644 index 00000000000..f60af2817d4 --- /dev/null +++ b/apps/files_sharing/lib/Migration/SetPasswordColumn.php @@ -0,0 +1,72 @@ +<?php + +/** + * 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\IShare; + +/** + * Class SetPasswordColumn + * + * @package OCA\Files_Sharing\Migration + */ +class SetPasswordColumn 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() { + return 'Copy the share password into the dedicated column'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) { + if (!$this->shouldRun()) { + return; + } + + $query = $this->connection->getQueryBuilder(); + $query + ->update('share') + ->set('password', 'share_with') + ->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_LINK))) + ->andWhere($query->expr()->isNotNull('share_with')); + $result = $query->execute(); + + if ($result === 0) { + // No link updated, no need to run the second query + return; + } + + $clearQuery = $this->connection->getQueryBuilder(); + $clearQuery + ->update('share') + ->set('share_with', $clearQuery->createNamedParameter(null)) + ->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 new file mode 100644 index 00000000000..b7b0582493e --- /dev/null +++ b/apps/files_sharing/lib/MountProvider.php @@ -0,0 +1,273 @@ +<?php + +/** + * 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\IUser; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Psr\Log\LoggerInterface; + +class MountProvider implements IMountProvider { + /** + * @param IConfig $config + * @param IManager $shareManager + * @param LoggerInterface $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 IUser $user + * @param IStorageFactory $loader + * @return IMountPoint[] + */ + 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 (IShare $share) use ($user) { + return $share->getPermissions() > 0 && $share->getShareOwner() !== $user->getUID() && $share->getSharedBy() !== $user->getUID(); + }); + + $superShares = $this->buildSuperShares($shares, $user); + + $otherMounts = $this->mountManager->getAll(); + $mounts = []; + $view = new View('/' . $user->getUID() . '/files'); + $ownerViews = []; + $sharingDisabledForUser = $this->shareManager->sharingDisabledForUser($user->getUID()); + /** @var CappedMemoryCache<bool> $folderExistCache */ + $foldersExistCache = new CappedMemoryCache(); + + $validShareCache = $this->cacheFactory->createLocal('share-valid-mountpoint-max'); + $maxValidatedShare = $validShareCache->get($user->getUID()) ?? 0; + $newMaxValidatedShare = $maxValidatedShare; + + foreach ($superShares as $share) { + try { + /** @var 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', + array_merge($mounts, $otherMounts), + [ + 'user' => $user->getUID(), + // parent share + 'superShare' => $parentShare, + // children/component of the superShare + 'groupedShares' => $share[1], + 'ownerView' => $ownerViews[$owner], + 'sharingDisabledForUser' => $sharingDisabledForUser + ], + $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->error( + 'Error while trying to create shared mount', + [ + 'app' => 'files_sharing', + 'exception' => $e, + ], + ); + } + } + + $validShareCache->set($user->getUID(), $newMaxValidatedShare, 24 * 60 * 60); + + // array_filter removes the null values from the array + return array_values(array_filter($mounts)); + } + + /** + * Groups shares by path (nodeId) and target path + * + * @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 = []; + + foreach ($shares as $share) { + if (!isset($tmp[$share->getNodeId()])) { + $tmp[$share->getNodeId()] = []; + } + $tmp[$share->getNodeId()][] = $share; + } + + $result = []; + // sort by stime, the super share will be based on the least recent share + foreach ($tmp as &$tmp2) { + @usort($tmp2, function ($a, $b) { + $aTime = $a->getShareTime()->getTimestamp(); + $bTime = $b->getShareTime()->getTimestamp(); + if ($aTime === $bTime) { + return $a->getId() < $b->getId() ? -1 : 1; + } + return $aTime < $bTime ? -1 : 1; + }); + $result[] = $tmp2; + } + + return array_values($result); + } + + /** + * Build super shares (virtual share) by grouping them by node id and target, + * then for each group compute the super share and return it along with the matching + * grouped shares. The most permissive permissions are used based on the permissions + * of all shares within the group. + * + * @param IShare[] $allShares + * @param IUser $user user + * @return array Tuple of [superShare, groupedShares] + */ + private function buildSuperShares(array $allShares, IUser $user) { + $result = []; + + $groupedShares = $this->groupShares($allShares); + + /** @var IShare[] $shares */ + foreach ($groupedShares as $shares) { + if (count($shares) === 0) { + continue; + } + + $superShare = $this->shareManager->newShare(); + + // compute super share based on first entry of the group + $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 + // 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) { + $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()) { + $share->setTarget($superShare->getTarget()); + try { + $this->shareManager->moveShare($share, $user->getUID()); + } catch (\InvalidArgumentException $e) { + // ignore as it is not important and we don't want to + // block FS setup + + // the subsequent code anyway only uses the target of the + // super share + + // such issue can usually happen when dealing with + // null groups which usually appear with group backend + // caching inconsistencies + $this->logger->debug( + 'Could not adjust share target for share ' . $share->getId() . ' to make it consistent: ' . $e->getMessage(), + ['app' => 'files_sharing'] + ); + } + } + if (!is_null($share->getNodeCacheEntry())) { + $superShare->setNodeCacheEntry($share->getNodeCacheEntry()); + } + } + + $superShare->setPermissions($superPermissions); + $superShare->setStatus($status); + $superShare->setAttributes($superAttributes); + + $result[] = [$superShare, $shares]; + } + + return $result; + } +} 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 new file mode 100644 index 00000000000..28972c1b462 --- /dev/null +++ b/apps/files_sharing/lib/Scanner.php @@ -0,0 +1,66 @@ +<?php + +/** + * 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\ObjectStoreScanner; +use OC\Files\Storage\Storage; + +/** + * Scanner for SharedStorage + */ +class Scanner extends \OC\Files\Cache\Scanner { + /** + * @var SharedStorage $storage + */ + protected $storage; + + private $sourceScanner; + + /** + * Returns metadata from the shared storage, but + * with permissions from the source storage. + * + * @param string $path path of the file for which to retrieve metadata + * + * @return array|null an array of metadata of the file + */ + public function getData($path) { + $data = parent::getData($path); + if ($data === null) { + return null; + } + $internalPath = $this->storage->getUnjailedPath($path); + $data['permissions'] = $this->storage->getSourceStorage()->getPermissions($internalPath); + return $data; + } + + private function getSourceScanner() { + if ($this->sourceScanner) { + return $this->sourceScanner; + } + if ($this->storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { + /** @var Storage $storage */ + [$storage] = $this->storage->resolvePath(''); + $this->sourceScanner = $storage->getScanner(); + return $this->sourceScanner; + } else { + return null; + } + } + + public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) { + $sourceScanner = $this->getSourceScanner(); + 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/share/file.php b/apps/files_sharing/lib/ShareBackend/File.php index 113675508fb..2aa52ef1b7f 100644 --- a/apps/files_sharing/lib/share/file.php +++ b/apps/files_sharing/lib/ShareBackend/File.php @@ -1,54 +1,53 @@ <?php + /** - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - -class OC_Share_Backend_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; +namespace OCA\Files_Sharing\ShareBackend; + +use OC\Files\Filesystem; +use OC\Files\View; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\Files_Sharing\Helper; +use OCP\Files\NotFoundException; +use OCP\IDBConnection; +use OCP\Server; +use OCP\Share\IShare; +use OCP\Share_Backend_File_Dependent; +use Psr\Log\LoggerInterface; + +class File implements 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; + public function __construct( + private ?FederatedShareProvider $federatedShareProvider = null, + ) { + if ($federatedShareProvider) { + $this->federatedShareProvider = $federatedShareProvider; + } else { + $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; } } @@ -60,9 +59,9 @@ class OC_Share_Backend_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; } } @@ -70,22 +69,17 @@ class OC_Share_Backend_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)); - - // for group shares we return the target right away - if ($shareWith === false) { - return $target; - } + public function generateTarget($itemSource, $shareWith) { + $shareFolder = Helper::getShareFolder(); + $target = Filesystem::normalizePath($shareFolder . '/' . basename($itemSource)); - \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 = ''; @@ -98,26 +92,24 @@ class OC_Share_Backend_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) { + 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']; @@ -131,38 +123,38 @@ class OC_Share_Backend_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 []; } /** @@ -172,8 +164,12 @@ class OC_Share_Backend_File implements OCP\Share_Backend_File_Dependent { * @return boolean */ public function isShareTypeAllowed($shareType) { - if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) { - return \OCA\Files_Sharing\Helper::isOutgoingServer2serverShareEnabled(); + if ($shareType === IShare::TYPE_REMOTE) { + return $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); + } + + if ($shareType === IShare::TYPE_REMOTE_GROUP) { + return $this->federatedShareProvider->isOutgoingServer2serverGroupShareEnabled(); } return true; @@ -188,8 +184,15 @@ class OC_Share_Backend_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 { @@ -203,7 +206,7 @@ class OC_Share_Backend_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 new file mode 100644 index 00000000000..df5529c3c4a --- /dev/null +++ b/apps/files_sharing/lib/ShareBackend/Folder.php @@ -0,0 +1,61 @@ +<?php + +/** + * 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 OCP\IDBConnection; +use OCP\Server; +use OCP\Share_Backend_Collection; + +class Folder extends File implements Share_Backend_Collection { + public function getChildren($itemSource) { + $children = []; + $parents = [$itemSource]; + + $qb = 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']; + } else { + $mimetype = -1; + } + while (!empty($parents)) { + $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) { + $parents[] = $file['fileid']; + } + } + $result->closeCursor(); + } + return $children; + } +} diff --git a/apps/files_sharing/lib/SharedMount.php b/apps/files_sharing/lib/SharedMount.php new file mode 100644 index 00000000000..692a6c8979b --- /dev/null +++ b/apps/files_sharing/lib/SharedMount.php @@ -0,0 +1,270 @@ +<?php + +/** + * 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\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, ISharedMountPoint { + /** + * @var SharedStorage $storage + */ + protected $storage = null; + + /** @var IShare */ + private $superShare; + + /** @var IShare[] */ + private $groupedShares; + + 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']; + + $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 IShare $share + * @param SharedMount[] $mountpoints + * @param CappedMemoryCache<bool> $folderExistCache + * @return string + */ + private function verifyMountPoint( + IShare $share, + array $mountpoints, + CappedMemoryCache $folderExistCache, + ) { + $mountPoint = basename($share->getTarget()); + $parent = dirname($share->getTarget()); + + $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( + Filesystem::normalizePath($parent . '/' . $mountPoint), + $this->recipientView, + $mountpoints + ); + + if ($newMountPoint !== $share->getTarget()) { + $this->updateFileTarget($newMountPoint, $share); + } + + return $newMountPoint; + } + + /** + * update fileTarget in the database if the mount point changed + * + * @param string $newPath + * @param IShare $share + * @return bool + */ + private function updateFileTarget($newPath, &$share) { + $share->setTarget($newPath); + + foreach ($this->groupedShares as $tmpShare) { + $tmpShare->setTarget($newPath); + Server::get(\OCP\Share\IManager::class)->moveShare($tmpShare, $this->user->getUID()); + } + + $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent($this->user)); + } + + + /** + * @param string $path + * @param View $view + * @param SharedMount[] $mountpoints + * @return mixed + */ + private function generateUniqueTarget($path, $view, array $mountpoints) { + $pathinfo = pathinfo($path); + $ext = isset($pathinfo['extension']) ? '.' . $pathinfo['extension'] : ''; + $name = $pathinfo['filename']; + $dir = $pathinfo['dirname']; + + $i = 2; + $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++; + } + + return $path; + } + + /** + * 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' + * @throws BrokenPath + */ + protected 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') { + 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' + $sliced = array_slice($split, 2); + $relPath = implode('/', $sliced); + + return '/' . $relPath; + } + + /** + * Move the mount point to $target + * + * @param string $target the target mount point + * @return bool + */ + public function moveMount($target) { + $relTargetPath = $this->stripUserFilesPath($target); + $share = $this->storage->getShare(); + + $result = true; + + try { + $this->updateFileTarget($relTargetPath, $share); + $this->setMountPoint($target); + $this->storage->setMountPoint($relTargetPath); + } catch (\Exception $e) { + Server::get(LoggerInterface::class)->error( + 'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"', + [ + 'app' => 'files_sharing', + 'exception' => $e, + ] + ); + } + + return $result; + } + + /** + * Remove the mount points + * + * @return bool + */ + public function removeMount() { + $mountManager = Filesystem::getMountManager(); + /** @var SharedStorage $storage */ + $storage = $this->getStorage(); + $result = $storage->unshareStorage(); + $mountManager->removeMount($this->mountPoint); + + return $result; + } + + /** + * @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 + */ + public function getStorageRootId() { + return $this->getShare()->getNodeId(); + } + + /** + * @return int + */ + public function getNumericStorageId() { + if (!is_null($this->getShare()->getNodeCacheEntry())) { + return $this->getShare()->getNodeCacheEntry()->getStorageId(); + } else { + $builder = Server::get(IDBConnection::class)->getQueryBuilder(); + + $query = $builder->select('storage') + ->from('filecache') + ->where($builder->expr()->eq('fileid', $builder->createNamedParameter($this->getStorageRootId()))); + + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + if ($row) { + return (int)$row['storage']; + } + return -1; + } + } + + public function getMountType() { + return 'shared'; + } +} diff --git a/apps/files_sharing/lib/SharedStorage.php b/apps/files_sharing/lib/SharedStorage.php new file mode 100644 index 00000000000..e310c5f3138 --- /dev/null +++ b/apps/files_sharing/lib/SharedStorage.php @@ -0,0 +1,564 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\Files_Sharing; + +use OC\Files\Cache\CacheDependencies; +use OC\Files\Cache\CacheEntry; +use OC\Files\Cache\FailedCache; +use OC\Files\Cache\NullWatcher; +use OC\Files\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 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 Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage { + /** @var IShare */ + private $superShare; + + /** @var IShare[] */ + private $groupedShares; + + /** + * @var View + */ + private $ownerView; + + private $initialized = false; + + /** + * @var ICacheEntry + */ + private $sourceRootInfo; + + /** @var string */ + private $user; + + private LoggerInterface $logger; + + /** @var IStorage */ + private $nonMaskedStorage; + + private array $mountOptions = []; + + /** @var boolean */ + private $sharingDisabledForUser; + + /** @var ?Folder $ownerUserFolder */ + private $ownerUserFolder = null; + + 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, + 'root' => null, + ]); + } + + /** + * @return ICacheEntry + */ + private function getSourceRootInfo() { + if (is_null($this->sourceRootInfo)) { + if (is_null($this->superShare->getNodeCacheEntry())) { + $this->init(); + $this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath); + } else { + $this->sourceRootInfo = $this->superShare->getNodeCacheEntry(); + } + } + return $this->sourceRootInfo; + } + + /** + * @psalm-assert Storage $this->storage + */ + private function init() { + if ($this->initialized) { + if (!$this->storage) { + // marked as initialized but no storage set + // this is probably because some code path has caused recursion during the share setup + // we setup a "failed storage" so `getWrapperStorage` doesn't return null. + // If the share setup completes after this the "failed storage" will be overwritten by the correct one + $this->logger->warning('Possible share setup recursion detected'); + $this->storage = new FailedStorage(['exception' => new \Exception('Possible share setup recursion detected')]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } + return; + } + + $this->initialized = true; + self::$initDepth++; + + try { + if (self::$initDepth > 10) { + throw new \Exception('Maximum share depth reached'); + } + + /** @var IRootFolder $rootFolder */ + $rootFolder = 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]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } catch (NoUserException $e) { + // sharer user deleted, set FailedStorage + $this->storage = new FailedStorage(['exception' => $e]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + } catch (\Exception $e) { + $this->storage = new FailedStorage(['exception' => $e]); + $this->cache = new FailedCache(); + $this->rootPath = ''; + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + + if (!$this->nonMaskedStorage) { + $this->nonMaskedStorage = $this->storage; + } + self::$initDepth--; + } + + 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', + '\OCP\Files\IHomeStorage', + Home::class, + HomeObjectStoreStorage::class, + IHomeStorage::class + ])) { + return false; + } + return parent::instanceOfStorage($class); + } + + /** + * @return string + */ + public function getShareId() { + return $this->superShare->getId(); + } + + private function isValid(): bool { + return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; + } + + public function getId(): string { + return 'shared::' . $this->getMountPoint(); + } + + public function getPermissions(string $path = ''): int { + if (!$this->isValid()) { + return 0; + } + $permissions = parent::getPermissions($path) & $this->superShare->getPermissions(); + + // part files and the mount point always have delete permissions + if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') { + $permissions |= Constants::PERMISSION_DELETE; + } + + if ($this->sharingDisabledForUser) { + $permissions &= ~Constants::PERMISSION_SHARE; + } + + return $permissions; + } + + public function isCreatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); + } + + public function isReadable(string $path): bool { + if (!$this->isValid()) { + return false; + } + if (!$this->file_exists($path)) { + return false; + } + /** @var IStorage $storage */ + /** @var string $internalPath */ + [$storage, $internalPath] = $this->resolvePath($path); + return $storage->isReadable($internalPath); + } + + public function isUpdatable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); + } + + public function isDeletable(string $path): bool { + return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); + } + + public function isSharable(string $path): bool { + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { + 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)); + } + + 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; + } + } + } + $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); + } + + public function rename(string $source, string $target): bool { + $this->init(); + $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part'; + $targetExists = $this->file_exists($target); + $sameFolder = dirname($source) === dirname($target); + + if ($targetExists || ($sameFolder && !$isPartFile)) { + if (!$this->isUpdatable('')) { + return false; + } + } else { + if (!$this->isCreatable('')) { + return false; + } + } + + return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); + } + + /** + * return mount point of share, relative to data/user/files + * + * @return string + */ + public function getMountPoint(): string { + return $this->superShare->getTarget(); + } + + public function setMountPoint(string $path): void { + $this->superShare->setTarget($path); + + foreach ($this->groupedShares as $share) { + $share->setTarget($path); + } + } + + /** + * get the user who shared the file + * + * @return string + */ + public function getSharedFrom(): string { + return $this->superShare->getShareOwner(); + } + + public function getShare(): IShare { + return $this->superShare; + } + + /** + * return share type, can be "file" or "folder" + * + * @return string + */ + public function getItemType(): string { + return $this->superShare->getNodeType(); + } + + 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 Cache( + $storage, + $sourceRoot, + Server::get(CacheDependencies::class), + $this->getShare() + ); + return $this->cache; + } + + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { + if (!$storage) { + $storage = $this; + } + return new Scanner($storage); + } + + 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(): bool { + foreach ($this->groupedShares as $share) { + Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user); + } + return true; + } + + 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->ownerUserFolder->getRelativePath($this->sourcePath); + $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); + } + } + + 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->ownerUserFolder->getRelativePath($this->sourcePath); + $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); + } + } + + public function changeLock(string $path, int $type, ILockingProvider $provider): void { + /** @var ILockingStorage $targetStorage */ + [$targetStorage, $targetInternalPath] = $this->resolvePath($path); + $targetStorage->changeLock($targetInternalPath, $type, $provider); + } + + public function getAvailability(): array { + // shares do not participate in availability logic + return [ + 'available' => true, + 'last_checked' => 0, + ]; + } + + public function setAvailability(bool $isAvailable): void { + // shares do not participate in availability logic + } + + public function getSourceStorage() { + $this->init(); + return $this->nonMaskedStorage; + } + + 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(string $path): string|false { + $info = [ + 'target' => $this->getMountPoint() . '/' . $path, + 'source' => $this->getUnjailedPath($path), + ]; + Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); + return parent::file_get_contents($path); + } + + public function file_put_contents(string $path, mixed $data): int|float|false { + $info = [ + 'target' => $this->getMountPoint() . '/' . $path, + 'source' => $this->getUnjailedPath($path), + ]; + Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); + return parent::file_put_contents($path, $data); + } + + 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 new file mode 100644 index 00000000000..24e82330d43 --- /dev/null +++ b/apps/files_sharing/lib/Updater.php @@ -0,0 +1,145 @@ +<?php + +/** + * 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 + */ + public static function renameHook($params) { + self::renameChildren($params['oldpath'], $params['newpath']); + self::moveShareInOrOutOfShare($params['newpath']); + } + + /** + * Fix for https://github.com/owncloud/core/issues/20769 + * + * The owner is allowed to move their files (if they are shared) into a receiving folder + * In this case we need to update the parent of the moved share. Since they are + * effectively handing over ownership of the file the rest of the code needs to know + * they need to build up the reshare tree. + * + * @param string $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 = 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)) { + return; + } + + // Check if the destination is inside a share + $mountManager = Server::get(IMountManager::class); + $dstMount = $mountManager->find($src->getPath()); + + //Ownership is moved over + foreach ($shares as $share) { + if ( + $share->getShareType() !== IShare::TYPE_USER + && $share->getShareType() !== IShare::TYPE_GROUP + && $share->getShareType() !== IShare::TYPE_ROOM + ) { + continue; + } + + if ($dstMount instanceof SharedMount) { + if (!($dstMount->getShare()->getPermissions() & Constants::PERMISSION_SHARE)) { + $shareManager->deleteShare($share); + continue; + } + $newOwner = $dstMount->getShare()->getShareOwner(); + $newPermissions = $share->getPermissions() & $dstMount->getShare()->getPermissions(); + } else { + $newOwner = $userFolder->getOwner()->getUID(); + $newPermissions = $share->getPermissions(); + } + + $share->setShareOwner($newOwner); + $share->setPermissions($newPermissions); + $shareManager->updateShare($share, onlyValid: false); + } + } + + /** + * rename mount point from the children if the parent was renamed + * + * @param string $oldPath old path relative to data/user/files + * @param string $newPath new path relative to data/user/files + */ + private static function renameChildren($oldPath, $newPath) { + $absNewPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $newPath); + $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files/' . $oldPath); + + $mountManager = Filesystem::getMountManager(); + $mountedShares = $mountManager->findIn('/' . \OC_User::getUser() . '/files/' . $oldPath); + foreach ($mountedShares as $mount) { + /** @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; + } +} diff --git a/apps/files_sharing/lib/activity.php b/apps/files_sharing/lib/activity.php deleted file mode 100644 index 721379eb78f..00000000000 --- a/apps/files_sharing/lib/activity.php +++ /dev/null @@ -1,483 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OCP\Activity\IExtension; -use OCP\Activity\IManager; -use OCP\IL10N; -use OCP\IURLGenerator; -use OCP\L10N\IFactory; - -class Activity implements IExtension { - const FILES_SHARING_APP = 'files_sharing'; - /** - * Filter with all sharing related activities - */ - const FILTER_SHARES = 'shares'; - - /** - * Activity types known to this extension - */ - const TYPE_PUBLIC_LINKS = 'public_links'; - const TYPE_REMOTE_SHARE = 'remote_share'; - const TYPE_SHARED = 'shared'; - - /** - * Subject keys for translation of the subjections - */ - const SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED = 'public_shared_file_downloaded'; - const SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED = 'public_shared_folder_downloaded'; - - 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'; - - 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_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'; - - 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'; - - const SUBJECT_SHARED_EMAIL = 'shared_with_email'; - const SUBJECT_SHARED_WITH_BY = 'shared_with_by'; - const SUBJECT_UNSHARED_BY = 'unshared_by'; - - /** @var IFactory */ - protected $languageFactory; - - /** @var IURLGenerator */ - protected $URLGenerator; - - /** @var IManager */ - protected $activityManager; - - /** - * @param IFactory $languageFactory - * @param IURLGenerator $URLGenerator - * @param IManager $activityManager - */ - public function __construct(IFactory $languageFactory, IURLGenerator $URLGenerator, IManager $activityManager) { - $this->languageFactory = $languageFactory; - $this->URLGenerator = $URLGenerator; - $this->activityManager = $activityManager; - } - - protected function getL10N($languageCode = null) { - return $this->languageFactory->get(self::FILES_SHARING_APP, $languageCode); - } - - /** - * The extension can return an array of additional notification types. - * If no additional types are to be added false is to be returned - * - * @param string $languageCode - * @return array|false - */ - public function getNotificationTypes($languageCode) { - $l = $this->getL10N($languageCode); - - return array( - self::TYPE_SHARED => (string) $l->t('A file or folder has been <strong>shared</strong>'), - self::TYPE_REMOTE_SHARE => (string) $l->t('A file or folder was shared from <strong>another server</strong>'), - self::TYPE_PUBLIC_LINKS => (string) $l->t('A public shared file or folder was <strong>downloaded</strong>'), - ); - } - - /** - * For a given method additional types to be displayed in the settings can be returned. - * In case no additional types are to be added false is to be returned. - * - * @param string $method - * @return array|false - */ - public function getDefaultTypes($method) { - $defaultTypes = [ - self::TYPE_SHARED, - self::TYPE_REMOTE_SHARE, - ]; - - if ($method === self::METHOD_STREAM) { - $defaultTypes[] = self::TYPE_PUBLIC_LINKS; - } - - return $defaultTypes; - } - - /** - * A string naming the css class for the icon to be used can be returned. - * If no icon is known for the given type false is to be returned. - * - * @param string $type - * @return string|false - */ - public function getTypeIcon($type) { - switch ($type) { - case self::TYPE_SHARED: - case self::TYPE_REMOTE_SHARE: - return 'icon-share'; - case self::TYPE_PUBLIC_LINKS: - return 'icon-download'; - } - - return false; - } - - /** - * The extension can translate a given message to the requested languages. - * If no translation is available false is to be returned. - * - * @param string $app - * @param string $text - * @param array $params - * @param boolean $stripPath - * @param boolean $highlightParams - * @param string $languageCode - * @return string|false - */ - public function translate($app, $text, $params, $stripPath, $highlightParams, $languageCode) { - if ($app !== self::FILES_SHARING_APP) { - return false; - } - - $l = $this->getL10N($languageCode); - - if ($this->activityManager->isFormattingFilteredObject()) { - $translation = $this->translateShort($text, $l, $params); - if ($translation !== false) { - return $translation; - } - } - - return $this->translateLong($text, $l, $params); - } - - /** - * @param string $text - * @param IL10N $l - * @param array $params - * @return bool|string - */ - protected function translateLong($text, IL10N $l, array $params) { - - switch ($text) { - case self::SUBJECT_REMOTE_SHARE_RECEIVED: - if (sizeof($params) === 2) { - // New activity ownCloud 8.2+ - return (string) $l->t('You received a new remote share %2$s from %1$s', $params); - } - return (string) $l->t('You received a new remote share from %s', $params); - case self::SUBJECT_REMOTE_SHARE_ACCEPTED: - return (string) $l->t('%1$s accepted remote share %2$s', $params); - case self::SUBJECT_REMOTE_SHARE_DECLINED: - return (string) $l->t('%1$s declined remote share %2$s', $params); - case self::SUBJECT_REMOTE_SHARE_UNSHARED: - return (string) $l->t('%1$s unshared %2$s from you', $params); - case self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED: - return (string) $l->t('Public shared folder %1$s was downloaded', $params); - case self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED: - return (string) $l->t('Public shared file %1$s was downloaded', $params); - - case self::SUBJECT_SHARED_USER_SELF: - return (string) $l->t('You shared %1$s with %2$s', $params); - case self::SUBJECT_RESHARED_USER_BY: - return (string) $l->t('%2$s shared %1$s with %3$s', $params); - case self::SUBJECT_UNSHARED_USER_SELF: - return (string) $l->t('You removed the share of %2$s for %1$s', $params); - case self::SUBJECT_UNSHARED_USER_BY: - return (string) $l->t('%2$s removed the share of %3$s for %1$s', $params); - - case self::SUBJECT_SHARED_GROUP_SELF: - return (string) $l->t('You shared %1$s with group %2$s', $params); - case self::SUBJECT_RESHARED_GROUP_BY: - return (string) $l->t('%2$s shared %1$s with group %3$s', $params); - case self::SUBJECT_UNSHARED_GROUP_SELF: - return (string) $l->t('You removed the share of group %2$s for %1$s', $params); - case self::SUBJECT_UNSHARED_GROUP_BY: - return (string) $l->t('%2$s removed the share of group %3$s for %1$s', $params); - - case self::SUBJECT_RESHARED_LINK_BY: - return (string) $l->t('%2$s shared %1$s via link', $params); - case self::SUBJECT_SHARED_LINK_SELF: - return (string) $l->t('You shared %1$s via link', $params); - case self::SUBJECT_UNSHARED_LINK_SELF: - return (string) $l->t('You removed the public link for %1$s', $params); - case self::SUBJECT_UNSHARED_LINK_BY: - return (string) $l->t('%2$s removed the public link for %1$s', $params); - case self::SUBJECT_LINK_EXPIRED: - return (string) $l->t('Your public link for %1$s expired', $params); - case self::SUBJECT_LINK_BY_EXPIRED: - return (string) $l->t('The public link of %2$s for %1$s expired', $params); - - case self::SUBJECT_SHARED_WITH_BY: - return (string) $l->t('%2$s shared %1$s with you', $params); - case self::SUBJECT_UNSHARED_BY: - return (string) $l->t('%2$s removed the share for %1$s', $params); - case self::SUBJECT_SHARED_EMAIL: - return (string) $l->t('You shared %1$s with %2$s', $params); - } - - return false; - } - - /** - * @param string $text - * @param IL10N $l - * @param array $params - * @return bool|string - */ - protected function translateShort($text, IL10N $l, array $params) { - switch ($text) { - case self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED: - case self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED: - return (string) $l->t('Downloaded via public link'); - - case self::SUBJECT_SHARED_USER_SELF: - return (string) $l->t('Shared with %2$s', $params); - case self::SUBJECT_RESHARED_USER_BY: - return (string) $l->t('Shared with %3$s by %2$s', $params); - case self::SUBJECT_UNSHARED_USER_SELF: - return (string) $l->t('Removed share for %2$s', $params); - case self::SUBJECT_UNSHARED_USER_BY: - return (string) $l->t('%2$s removed share for %3$s', $params); - - case self::SUBJECT_SHARED_GROUP_SELF: - return (string) $l->t('Shared with group %2$s', $params); - case self::SUBJECT_RESHARED_GROUP_BY: - return (string) $l->t('Shared with group %3$s by %2$s', $params); - case self::SUBJECT_UNSHARED_GROUP_SELF: - return (string) $l->t('Removed share of group %2$s', $params); - case self::SUBJECT_UNSHARED_GROUP_BY: - return (string) $l->t('%2$s removed share of group %3$s', $params); - - case self::SUBJECT_RESHARED_LINK_BY: - return (string) $l->t('Shared via link by %2$s', $params); - case self::SUBJECT_SHARED_LINK_SELF: - return (string) $l->t('Shared via public link'); - case self::SUBJECT_UNSHARED_LINK_SELF: - return (string) $l->t('Removed public link'); - case self::SUBJECT_UNSHARED_LINK_BY: - return (string) $l->t('%2$s removed public link'); - case self::SUBJECT_LINK_EXPIRED: - return (string) $l->t('Public link expired', $params); - case self::SUBJECT_LINK_BY_EXPIRED: - return (string) $l->t('Public link of %2$s expired', $params); - - case self::SUBJECT_SHARED_WITH_BY: - return (string) $l->t('Shared by %2$s', $params); - case self::SUBJECT_SHARED_EMAIL: - return (string) $l->t('Shared with %2$s', $params); - - default: - return false; - } - } - - /** - * The extension can define the type of parameters for translation - * - * Currently known types are: - * * file => will strip away the path of the file and add a tooltip with it - * * username => will add the avatar of the user - * - * @param string $app - * @param string $text - * @return array|false - */ - public function getSpecialParameterList($app, $text) { - if ($app === self::FILES_SHARING_APP) { - switch ($text) { - case self::SUBJECT_REMOTE_SHARE_RECEIVED: - case self::SUBJECT_REMOTE_SHARE_UNSHARED: - return array( - 0 => 'federated_cloud_id', - //1 => 'file', in theory its a file, but it does not exist yet/anymore - ); - case self::SUBJECT_REMOTE_SHARE_ACCEPTED: - case self::SUBJECT_REMOTE_SHARE_DECLINED: - return array( - 0 => 'federated_cloud_id', - 1 => 'file', - ); - case self::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED: - case self::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED: - return array( - 0 => 'file', - ); - case self::SUBJECT_SHARED_LINK_SELF: - case self::SUBJECT_UNSHARED_LINK_SELF: - case self::SUBJECT_LINK_EXPIRED: - return [0 => 'file']; - case self::SUBJECT_RESHARED_LINK_BY: - return [ - 0 => 'file', - 1 => 'username', - 2 => '', - ]; - case self::SUBJECT_SHARED_EMAIL: - return array( - 0 => 'file', - 1 => '',// 'email' is neither supported nor planned for now - ); - - case self::SUBJECT_SHARED_USER_SELF: - case self::SUBJECT_SHARED_WITH_BY: - case self::SUBJECT_UNSHARED_BY: - case self::SUBJECT_UNSHARED_LINK_BY: - case self::SUBJECT_LINK_BY_EXPIRED: - case self::SUBJECT_UNSHARED_USER_SELF: - return [0 => 'file', 1 => 'username']; - case self::SUBJECT_RESHARED_USER_BY: - case self::SUBJECT_UNSHARED_USER_BY: - return [ - 0 => 'file', - 1 => 'username', - 2 => 'username', - ]; - - case self::SUBJECT_SHARED_GROUP_SELF: - case self::SUBJECT_UNSHARED_GROUP_SELF: - return [ - 0 => 'file', - 1 => 'group', - ]; - - case self::SUBJECT_RESHARED_GROUP_BY: - case self::SUBJECT_UNSHARED_GROUP_BY: - return [ - 0 => 'file', - 1 => 'username', - 2 => 'group', - ]; - } - } - - return false; - } - - /** - * The extension can define the parameter grouping by returning the index as integer. - * In case no grouping is required false is to be returned. - * - * @param array $activity - * @return integer|false - */ - public function getGroupParameter($activity) { - if ($activity['app'] === self::FILES_SHARING_APP) { - switch ($activity['subject']) { - case self::SUBJECT_SHARED_LINK_SELF: - case self::SUBJECT_UNSHARED_LINK_SELF: - case self::SUBJECT_LINK_EXPIRED: - case self::SUBJECT_SHARED_WITH_BY: - case self::SUBJECT_UNSHARED_BY: - // Group by file name - return 0; - case self::SUBJECT_SHARED_USER_SELF: - case self::SUBJECT_SHARED_GROUP_SELF: - // Group by user/group - return 1; - } - } - - return false; - } - - /** - * The extension can define additional navigation entries. The array returned has to contain two keys 'top' - * and 'apps' which hold arrays with the relevant entries. - * If no further entries are to be added false is no be returned. - * - * @return array|false - */ - public function getNavigation() { - $l = $this->getL10N(); - return [ - 'apps' => [], - 'top' => [ - self::FILTER_SHARES => [ - 'id' => self::FILTER_SHARES, - 'name' => (string) $l->t('Shares'), - 'url' => $this->URLGenerator->linkToRoute('activity.Activities.showList', ['filter' => self::FILTER_SHARES]), - ], - ], - ]; - } - - /** - * The extension can check if a custom filter (given by a query string like filter=abc) is valid or not. - * - * @param string $filterValue - * @return boolean - */ - public function isFilterValid($filterValue) { - return $filterValue === self::FILTER_SHARES; - } - - /** - * The extension can filter the types based on the filter if required. - * In case no filter is to be applied false is to be returned unchanged. - * - * @param array $types - * @param string $filter - * @return array|false - */ - public function filterNotificationTypes($types, $filter) { - switch ($filter) { - case self::FILTER_SHARES: - return array_intersect([self::TYPE_SHARED, self::TYPE_REMOTE_SHARE], $types); - } - return false; - } - - /** - * For a given filter the extension can specify the sql query conditions including parameters for that query. - * In case the extension does not know the filter false is to be returned. - * The query condition and the parameters are to be returned as array with two elements. - * E.g. return array('`app` = ? and `message` like ?', array('mail', 'ownCloud%')); - * - * @param string $filter - * @return array|false - */ - public function getQueryForFilter($filter) { - if ($filter === self::FILTER_SHARES) { - return [ - '`app` = ?', - [self::FILES_SHARING_APP,], - ]; - } - return false; - } - -} diff --git a/apps/files_sharing/lib/cache.php b/apps/files_sharing/lib/cache.php deleted file mode 100644 index 4dcdaa892ad..00000000000 --- a/apps/files_sharing/lib/cache.php +++ /dev/null @@ -1,105 +0,0 @@ -<?php -/** - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Cache; - -use OC\Files\Cache\Wrapper\CacheJail; -use OCP\Files\Cache\ICacheEntry; -use OCP\Files\Storage\IStorage; - -/** - * Metadata cache for shared files - * - * don't use this class directly if you need to get metadata, use \OC\Files\Filesystem::getFileInfo instead - */ -class Shared_Cache extends CacheJail { - /** - * @var \OC\Files\Storage\Shared - */ - private $storage; - - /** - * @var IStorage - */ - private $sourceStorage; - - /** - * @var ICacheEntry - */ - private $sourceRootInfo; - - /** - * @var \OCP\Files\Cache\ICache - */ - private $sourceCache; - - /** - * @param \OC\Files\Storage\Shared $storage - * @param IStorage $sourceStorage - * @param ICacheEntry $sourceRootInfo - */ - public function __construct($storage, IStorage $sourceStorage, ICacheEntry $sourceRootInfo) { - $this->storage = $storage; - $this->sourceStorage = $sourceStorage; - $this->sourceRootInfo = $sourceRootInfo; - $this->sourceCache = $sourceStorage->getCache(); - parent::__construct( - $this->sourceCache, - $this->sourceRootInfo->getPath() - ); - } - - public function getNumericStorageId() { - if (isset($this->numericId)) { - return $this->numericId; - } else { - return false; - } - } - - protected function formatCacheEntry($entry) { - $path = $entry['path']; - $entry = parent::formatCacheEntry($entry); - $sharePermissions = $this->storage->getPermissions($path); - if (isset($entry['permissions'])) { - $entry['permissions'] &= $sharePermissions; - } else { - $entry['permissions'] = $sharePermissions; - } - $entry['uid_owner'] = $this->storage->getOwner($path); - $entry['displayname_owner'] = \OC_User::getDisplayName($entry['uid_owner']); - if ($path === '') { - $entry['is_share_mount_point'] = true; - } - return $entry; - } - - /** - * remove all entries for files that are stored on the storage from the cache - */ - public function clear() { - // Not a valid action for Shared Cache - } -} diff --git a/apps/files_sharing/lib/capabilities.php b/apps/files_sharing/lib/capabilities.php deleted file mode 100644 index 913f248b174..00000000000 --- a/apps/files_sharing/lib/capabilities.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -/** - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\Files_Sharing; - -use OCP\Capabilities\ICapability; -use \OCP\IConfig; - -/** - * Class Capabilities - * - * @package OCA\Files_Sharing - */ -class Capabilities implements ICapability { - - /** @var IConfig */ - private $config; - - public function __construct(IConfig $config) { - $this->config = $config; - } - - /** - * Return this classes capabilities - * - * @return array - */ - public function getCapabilities() { - $res = []; - - if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') { - $res['api_enabled'] = false; - $res['public'] = ['enabled' => false]; - $res['user'] = ['send_mail' => false]; - $res['resharing'] = false; - } else { - $res['api_enabled'] = true; - - $public = []; - $public['enabled'] = $this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes'; - if ($public['enabled']) { - $public['password'] = []; - $public['password']['enforced'] = ($this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes'); - - $public['expire_date'] = []; - $public['expire_date']['enabled'] = $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes'; - 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['send_mail'] = $this->config->getAppValue('core', 'shareapi_allow_public_notification', 'no') === 'yes'; - $public['upload'] = $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes'; - } - $res["public"] = $public; - - $res['user']['send_mail'] = $this->config->getAppValue('core', 'shareapi_allow_mail_notification', 'no') === 'yes'; - - $res['resharing'] = $this->config->getAppValue('core', 'shareapi_allow_resharing', 'yes') === 'yes'; - } - - //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' - ]; - - return [ - 'files_sharing' => $res, - ]; - } -} diff --git a/apps/files_sharing/lib/controllers/externalsharescontroller.php b/apps/files_sharing/lib/controllers/externalsharescontroller.php deleted file mode 100644 index beefb4d7027..00000000000 --- a/apps/files_sharing/lib/controllers/externalsharescontroller.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\Controllers; - -use OCP\AppFramework\Controller; -use OCP\IRequest; -use OCP\AppFramework\Http\JSONResponse; -use OCP\Http\Client\IClientService; -use OCP\AppFramework\Http\DataResponse; - -/** - * Class ExternalSharesController - * - * @package OCA\Files_Sharing\Controllers - */ -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) { - parent::__construct($appName, $request); - $this->externalManager = $externalManager; - $this->clientService = $clientService; - } - - /** - * @NoAdminRequired - * @NoOutgoingFederatedSharingRequired - * - * @return JSONResponse - */ - public function index() { - return new JSONResponse($this->externalManager->getOpenShares()); - } - - /** - * @NoAdminRequired - * @NoOutgoingFederatedSharingRequired - * - * @param int $id - * @return JSONResponse - */ - public function create($id) { - $this->externalManager->acceptShare($id); - return new JSONResponse(); - } - - /** - * @NoAdminRequired - * @NoOutgoingFederatedSharingRequired - * - * @param integer $id - * @return JSONResponse - */ - 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/controllers/sharecontroller.php b/apps/files_sharing/lib/controllers/sharecontroller.php deleted file mode 100644 index ea024b6016a..00000000000 --- a/apps/files_sharing/lib/controllers/sharecontroller.php +++ /dev/null @@ -1,494 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Georg Ehrke <georg@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\Controllers; - -use OC; -use OC_Files; -use OC_Util; -use OCP; -use OCP\Template; -use OCP\Share; -use OCP\AppFramework\Controller; -use OCP\IRequest; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Http\RedirectResponse; -use OCP\AppFramework\Http\NotFoundResponse; -use OCP\IURLGenerator; -use OCP\IConfig; -use OCP\ILogger; -use OCP\IUserManager; -use OCP\ISession; -use OCP\IPreview; -use OCA\Files_Sharing\Helper; -use OCP\Util; -use OCA\Files_Sharing\Activity; -use \OCP\Files\NotFoundException; -use OCP\Files\IRootFolder; -use OCP\Share\Exceptions\ShareNotFound; - -/** - * 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; - - /** - * @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 - */ - 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) { - 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; - } - - /** - * @PublicPage - * @NoCSRFRequired - * - * @param string $token - * @return TemplateResponse|RedirectResponse - */ - public function showAuthenticate($token) { - $share = $this->shareManager->getShareByToken($token); - - if($this->linkShareAuth($share)) { - return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', array('token' => $token))); - } - - return new TemplateResponse($this->appName, 'authenticate', array(), 'guest'); - } - - /** - * @PublicPage - * @UseSession - * - * Authenticates against password-protected shares - * @param string $token - * @param string $password - * @return RedirectResponse|TemplateResponse - */ - public function authenticate($token, $password = '') { - - // Check whether share exists - try { - $share = $this->shareManager->getShareByToken($token); - } catch (ShareNotFound $e) { - return new NotFoundResponse(); - } - - $authenticate = $this->linkShareAuth($share, $password); - - if($authenticate === true) { - return new RedirectResponse($this->urlGenerator->linkToRoute('files_sharing.sharecontroller.showShare', array('token' => $token))); - } - - return new TemplateResponse($this->appName, 'authenticate', array('wrongpw' => true), 'guest'); - } - - /** - * 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 - * - * @param \OCP\Share\IShare $share - * @param string|null $password - * @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; - } - } - return true; - } - - /** - * throws hooks when a share is attempted to be accessed - * - * @param \OCP\Share\IShare|string $share the Share instance if available, - * otherwise token - * @param int $errorCode - * @param string $errorMessage - * @throws OC\HintException - * @throws OC\ServerNotAvailableException - */ - protected function emitAccessShareHook($share, $errorCode = 200, $errorMessage = '') { - $itemType = $itemSource = $uidOwner = ''; - $token = $share; - $exception = null; - if($share instanceof \OCP\Share\IShare) { - try { - $token = $share->getToken(); - $uidOwner = $share->getSharedBy(); - $itemType = $share->getNodeType(); - $itemSource = $share->getNodeId(); - } catch (\Exception $e) { - // we log what we know and pass on the exception afterwards - $exception = $e; - } - } - \OC_Hook::emit('OCP\Share', 'share_link_access', [ - 'itemType' => $itemType, - 'itemSource' => $itemSource, - 'uidOwner' => $uidOwner, - 'token' => $token, - 'errorCode' => $errorCode, - 'errorMessage' => $errorMessage, - ]); - if(!is_null($exception)) { - throw $exception; - } - } - - /** - * Validate the permissions of the share - * - * @param Share\IShare $share - * @return bool - */ - private function validateShare(\OCP\Share\IShare $share) { - return $share->getNode()->isReadable() && $share->getNode()->isShareable(); - } - - /** - * @PublicPage - * @NoCSRFRequired - * - * @param string $token - * @param string $path - * @return TemplateResponse|RedirectResponse - * @throws NotFoundException - */ - public function showShare($token, $path = '') { - \OC_User::setIncognitoMode(true); - - // Check whether share exists - try { - $share = $this->shareManager->getShareByToken($token); - } catch (ShareNotFound $e) { - $this->emitAccessShareHook($token, 404, 'Share not found'); - return new NotFoundResponse(); - } - - // 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(); - } - // We can't get the path of a file share - try { - if ($share->getNode() instanceof \OCP\Files\File && $path !== '') { - $this->emitAccessShareHook($share, 404, 'Share not found'); - throw new NotFoundException(); - } - } catch (\Exception $e) { - $this->emitAccessShareHook($share, 404, 'Share not found'); - throw $e; - } - - $rootFolder = null; - if ($share->getNode() instanceof \OCP\Files\Folder) { - /** @var \OCP\Files\Folder $rootFolder */ - $rootFolder = $share->getNode(); - - try { - $path = $rootFolder->get($path); - } catch (\OCP\Files\NotFoundException $e) { - $this->emitAccessShareHook($share, 404, 'Share not found'); - throw new NotFoundException(); - } - } - - $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'] = Helper::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 - if ($share->getNode() instanceof \OCP\Files\Folder) { - $shareTmpl['dir'] = $rootFolder->getRelativePath($path->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_UNKNOWN) { - $freeSpace = max($freeSpace, 0); - } else { - $freeSpace = (INF > 0) ? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188 - } - - $uploadLimit = Util::uploadLimit(); - $maxUploadFilesize = min($freeSpace, $uploadLimit); - - $folder = new Template('files', 'list', ''); - $folder->assign('dir', $rootFolder->getRelativePath($path->getPath())); - $folder->assign('dirToken', $token); - $folder->assign('permissions', \OCP\Constants::PERMISSION_READ); - $folder->assign('isPublic', true); - $folder->assign('publicUploadEnabled', 'no'); - $folder->assign('uploadMaxFilesize', $maxUploadFilesize); - $folder->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize)); - $folder->assign('freeSpace', $freeSpace); - $folder->assign('uploadLimit', $uploadLimit); // PHP upload limit - $folder->assign('usedSpacePercent', 0); - $folder->assign('trash', false); - $shareTmpl['folder'] = $folder->fetchPage(); - } - - $shareTmpl['downloadURL'] = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.downloadShare', array('token' => $token)); - $shareTmpl['maxSizeAnimateGif'] = $this->config->getSystemValue('max_filesize_animated_gifs_public_sharing', 10); - $shareTmpl['previewEnabled'] = $this->config->getSystemValue('enable_previews', true); - - $csp = new OCP\AppFramework\Http\ContentSecurityPolicy(); - $csp->addAllowedFrameDomain('\'self\''); - $response = new TemplateResponse($this->appName, 'public', $shareTmpl, 'base'); - $response->setContentSecurityPolicy($csp); - - $this->emitAccessShareHook($share); - - return $response; - } - - /** - * @PublicPage - * @NoCSRFRequired - * - * @param string $token - * @param string $files - * @param string $path - * @param string $downloadStartSecret - * @return void|RedirectResponse - */ - public function downloadShare($token, $files = null, $path = '', $downloadStartSecret = '') { - \OC_User::setIncognitoMode(true); - - $share = $this->shareManager->getShareByToken($token); - - // 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])); - } - - $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]; - } - } - - $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 - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType(Activity::TYPE_PUBLIC_LINKS) - ->setSubject(Activity::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED, [$userFolder->getRelativePath($share->getNode()->getPath())]) - ->setAffectedUser($share->getShareOwner()) - ->setObject('files', $share->getNode()->getId(), $userFolder->getRelativePath($share->getNode()->getPath())); - $this->activityManager->publish($event); - } - // Directory share - else { - /** @var \OCP\Files\Folder $node */ - $node = $share->getNode(); - - // Try to get the path - if ($path !== '') { - try { - $node = $node->get($path); - } catch (NotFoundException $e) { - $this->emitAccessShareHook($share, 404, 'Share not found'); - return new NotFoundResponse(); - } - } - - $originalSharePath = $userFolder->getRelativePath($node->getPath()); - - if ($node instanceof \OCP\Files\File) { - // Single file download - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType(Activity::TYPE_PUBLIC_LINKS) - ->setSubject(Activity::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED, [$userFolder->getRelativePath($node->getPath())]) - ->setAffectedUser($share->getShareOwner()) - ->setObject('files', $node->getId(), $userFolder->getRelativePath($node->getPath())); - $this->activityManager->publish($event); - } else if (!empty($files_list)) { - /** @var \OCP\Files\Folder $node */ - - // Subset of files is downloaded - foreach ($files_list as $file) { - $subNode = $node->get($file); - - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType(Activity::TYPE_PUBLIC_LINKS) - ->setAffectedUser($share->getShareOwner()) - ->setObject('files', $subNode->getId(), $userFolder->getRelativePath($subNode->getPath())); - - if ($subNode instanceof \OCP\Files\File) { - $event->setSubject(Activity::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED, [$userFolder->getRelativePath($subNode->getPath())]); - } else { - $event->setSubject(Activity::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED, [$userFolder->getRelativePath($subNode->getPath())]); - } - - $this->activityManager->publish($event); - } - } else { - // The folder is downloaded - $event = $this->activityManager->generateEvent(); - $event->setApp('files_sharing') - ->setType(Activity::TYPE_PUBLIC_LINKS) - ->setSubject(Activity::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED, [$userFolder->getRelativePath($node->getPath())]) - ->setAffectedUser($share->getShareOwner()) - ->setObject('files', $node->getId(), $userFolder->getRelativePath($node->getPath())); - $this->activityManager->publish($event); - } - } - - /* 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); - - // 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['REQUEST_METHOD'] == 'HEAD'); - 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['REQUEST_METHOD'] == 'HEAD'); - exit(); - } - } -} diff --git a/apps/files_sharing/lib/deleteorphanedsharesjob.php b/apps/files_sharing/lib/deleteorphanedsharesjob.php deleted file mode 100644 index 72bf6b222e7..00000000000 --- a/apps/files_sharing/lib/deleteorphanedsharesjob.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_sharing\Lib; - -use OC\BackgroundJob\TimedJob; - -/** - * 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; - - /** - * sets the correct interval for this timed job - */ - public function __construct(){ - $this->interval = $this->defaultIntervalMin * 60; - } - - /** - * Makes the background job do its work - * - * @param array $argument unused argument - */ - public function run($argument) { - $connection = \OC::$server->getDatabaseConnection(); - $logger = \OC::$server->getLogger(); - - $sql = - 'DELETE FROM `*PREFIX*share` ' . - 'WHERE `item_type` in (\'file\', \'folder\') ' . - 'AND NOT EXISTS (SELECT `fileid` FROM `*PREFIX*filecache` WHERE `file_source` = `fileid`)'; - - $deletedEntries = $connection->executeUpdate($sql); - $logger->debug("$deletedEntries orphaned share(s) deleted", ['app' => 'DeleteOrphanedSharesJob']); - } - -} diff --git a/apps/files_sharing/lib/exceptions/brokenpath.php b/apps/files_sharing/lib/exceptions/brokenpath.php deleted file mode 100644 index 4639d47c4a6..00000000000 --- a/apps/files_sharing/lib/exceptions/brokenpath.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -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 deleted file mode 100644 index 1ed6a9e2179..00000000000 --- a/apps/files_sharing/lib/exceptions/s2sexception.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\Exceptions; - -/** - * S2S sharing not allowed - */ -class S2SException extends \Exception { -} diff --git a/apps/files_sharing/lib/expiresharesjob.php b/apps/files_sharing/lib/expiresharesjob.php deleted file mode 100644 index 479f407e68f..00000000000 --- a/apps/files_sharing/lib/expiresharesjob.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php -/** - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OC\BackgroundJob\TimedJob; - -/** - * Delete all shares that are expired - */ -class ExpireSharesJob extends TimedJob { - - /** - * sets the correct interval for this timed job - */ - public function __construct() { - // Run once a day - $this->setInterval(24 * 60 * 60); - } - - /** - * Makes the background job do its work - * - * @param array $argument unused argument - */ - public function run($argument) { - $connection = \OC::$server->getDatabaseConnection(); - $logger = \OC::$server->getLogger(); - - //Current time - $now = new \DateTime(); - $now = $now->format('Y-m-d H:i:s'); - - /* - * Expire file link shares only (for now) - */ - $qb = $connection->getQueryBuilder(); - $qb->select('id', 'file_source', 'uid_owner', 'item_type') - ->from('share') - ->where( - $qb->expr()->andX( - $qb->expr()->eq('share_type', $qb->expr()->literal(\OCP\Share::SHARE_TYPE_LINK)), - $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')) - ) - ) - ); - - $shares = $qb->execute(); - while($share = $shares->fetch()) { - \OCP\Share::unshare($share['item_type'], $share['file_source'], \OCP\Share::SHARE_TYPE_LINK, null, $share['uid_owner']); - } - $shares->closeCursor(); - } - -} diff --git a/apps/files_sharing/lib/external/cache.php b/apps/files_sharing/lib/external/cache.php deleted file mode 100644 index da9bf83cdfa..00000000000 --- a/apps/files_sharing/lib/external/cache.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -class Cache extends \OC\Files\Cache\Cache { - private $remote; - private $remoteUser; - private $storage; - - /** - * @param \OCA\Files_Sharing\External\Storage $storage - * @param string $remote - * @param string $remoteUser - */ - public function __construct($storage, $remote, $remoteUser) { - $this->storage = $storage; - list(, $remote) = explode('://', $remote, 2); - $this->remote = $remote; - $this->remoteUser = $remoteUser; - parent::__construct($storage); - } - - public function get($file) { - $result = parent::get($file); - if (!$result) { - return false; - } - $result['displayname_owner'] = $this->remoteUser . '@' . $this->remote; - if (!$file || $file === '') { - $result['is_share_mount_point'] = true; - $mountPoint = rtrim($this->storage->getMountPoint()); - $result['name'] = basename($mountPoint); - } - return $result; - } - - public function getFolderContentsById($id) { - $results = parent::getFolderContentsById($id); - foreach ($results as &$file) { - $file['displayname_owner'] = $this->remoteUser . '@' . $this->remote; - } - return $results; - } -} diff --git a/apps/files_sharing/lib/external/manager.php b/apps/files_sharing/lib/external/manager.php deleted file mode 100644 index 71d6788cb52..00000000000 --- a/apps/files_sharing/lib/external/manager.php +++ /dev/null @@ -1,419 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -use OC\Files\Filesystem; -use OCA\FederatedFileSharing\DiscoveryManager; -use OCP\Files; -use OCP\Notification\IManager; - -class Manager { - const STORAGE = '\OCA\Files_Sharing\External\Storage'; - - /** - * @var string - */ - private $uid; - - /** - * @var \OCP\IDBConnection - */ - private $connection; - - /** - * @var \OC\Files\Mount\Manager - */ - private $mountManager; - - /** - * @var \OCP\Files\Storage\IStorageFactory - */ - private $storageLoader; - - /** - * @var \OC\HTTPHelper - */ - private $httpHelper; - - /** - * @var IManager - */ - private $notificationManager; - /** @var DiscoveryManager */ - private $discoveryManager; - - /** - * @param \OCP\IDBConnection $connection - * @param \OC\Files\Mount\Manager $mountManager - * @param \OCP\Files\Storage\IStorageFactory $storageLoader - * @param \OC\HTTPHelper $httpHelper - * @param IManager $notificationManager - * @param DiscoveryManager $discoveryManager - * @param string $uid - */ - public function __construct(\OCP\IDBConnection $connection, - \OC\Files\Mount\Manager $mountManager, - \OCP\Files\Storage\IStorageFactory $storageLoader, - \OC\HTTPHelper $httpHelper, - IManager $notificationManager, - DiscoveryManager $discoveryManager, - $uid) { - $this->connection = $connection; - $this->mountManager = $mountManager; - $this->storageLoader = $storageLoader; - $this->httpHelper = $httpHelper; - $this->uid = $uid; - $this->notificationManager = $notificationManager; - $this->discoveryManager = $discoveryManager; - } - - /** - * add new server-to-server share - * - * @param string $remote - * @param string $token - * @param string $password - * @param string $name - * @param string $owner - * @param boolean $accepted - * @param string $user - * @param int $remoteId - * @return Mount|null - */ - public function addShare($remote, $token, $password, $name, $owner, $accepted=false, $user = null, $remoteId = -1) { - - $user = $user ? $user : $this->uid; - $accepted = $accepted ? 1 : 0; - $name = Filesystem::normalizePath('/' . $name); - - if (!$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, - // using the original share item name. - $tmpMountPointName = '{{TemporaryMountPointName#' . $name . '}}'; - $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, - ]; - - $i = 1; - while (!$this->connection->insertIfNotExist('*PREFIX*share_external', $data, ['user', 'mountpoint_hash'])) { - // The external share already exists for the user - $data['mountpoint'] = $tmpMountPointName . '-' . $i; - $data['mountpoint_hash'] = md5($data['mountpoint']); - $i++; - } - return null; - } - - $mountPoint = Files::buildNotExistingFileName('/', $name); - $mountPoint = Filesystem::normalizePath('/' . $mountPoint); - $hash = md5($mountPoint); - - $query = $this->connection->prepare(' - INSERT INTO `*PREFIX*share_external` - (`remote`, `share_token`, `password`, `name`, `owner`, `user`, `mountpoint`, `mountpoint_hash`, `accepted`, `remote_id`) - 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); - } - - /** - * get share - * - * @param int $id share id - * @return mixed share of false - */ - public function getShare($id) { - $getShare = $this->connection->prepare(' - SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted` - FROM `*PREFIX*share_external` - WHERE `id` = ? AND `user` = ?'); - $result = $getShare->execute(array($id, $this->uid)); - - return $result ? $getShare->fetch() : false; - } - - /** - * accept server-to-server share - * - * @param int $id - * @return bool True if the share could be accepted, false otherwise - */ - public function acceptShare($id) { - - $share = $this->getShare($id); - - if ($share) { - $mountPoint = Files::buildNotExistingFileName('/', $share['name']); - $mountPoint = Filesystem::normalizePath('/' . $mountPoint); - $hash = md5($mountPoint); - - $acceptShare = $this->connection->prepare(' - UPDATE `*PREFIX*share_external` - SET `accepted` = ?, - `mountpoint` = ?, - `mountpoint_hash` = ? - WHERE `id` = ? AND `user` = ?'); - $acceptShare->execute(array(1, $mountPoint, $hash, $id, $this->uid)); - $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept'); - - \OC_Hook::emit('OCP\Share', 'federated_share_added', ['server' => $share['remote']]); - - $this->processNotification($id); - return true; - } - - return false; - } - - /** - * decline server-to-server share - * - * @param int $id - * @return bool True if the share could be declined, false otherwise - */ - public function declineShare($id) { - - $share = $this->getShare($id); - - if ($share) { - $removeShare = $this->connection->prepare(' - DELETE FROM `*PREFIX*share_external` WHERE `id` = ? AND `user` = ?'); - $removeShare->execute(array($id, $this->uid)); - $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); - - $this->processNotification($id); - return true; - } - - return false; - } - - /** - * @param int $remoteShare - */ - public function processNotification($remoteShare) { - $filter = $this->notificationManager->createNotification(); - $filter->setApp('files_sharing') - ->setUser($this->uid) - ->setObject('remote_share', (int) $remoteShare); - $this->notificationManager->markProcessed($filter); - } - - /** - * inform remote server whether server-to-server share was accepted/declined - * - * @param string $remote - * @param string $token - * @param int $remoteId Share id on the remote host - * @param string $feedback - * @return boolean - */ - private function sendFeedbackToRemote($remote, $token, $remoteId, $feedback) { - - $url = rtrim($remote, '/') . $this->discoveryManager->getShareEndpoint($remote) . '/' . $remoteId . '/' . $feedback . '?format=' . \OCP\Share::RESPONSE_FORMAT; - $fields = array('token' => $token); - - $result = $this->httpHelper->post($url, $fields); - $status = json_decode($result['result'], true); - - return ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)); - } - - /** - * remove '/user/files' from the path and trailing slashes - * - * @param string $path - * @return string - */ - protected function stripPath($path) { - $prefix = '/' . $this->uid . '/files'; - return rtrim(substr($path, strlen($prefix)), '/'); - } - - public function getMount($data) { - $data['manager'] = $this; - $mountPoint = '/' . $this->uid . '/files' . $data['mountpoint']; - $data['mountpoint'] = $mountPoint; - $data['certificateManager'] = \OC::$server->getCertificateManager($this->uid); - return new Mount(self::STORAGE, $mountPoint, $data, $this, $this->storageLoader); - } - - /** - * @param array $data - * @return Mount - */ - protected function mountShare($data) { - $mount = $this->getMount($data); - $this->mountManager->addMount($mount); - return $mount; - } - - /** - * @return \OC\Files\Mount\Manager - */ - public function getMountManager() { - return $this->mountManager; - } - - /** - * @param string $source - * @param string $target - * @return bool - */ - public function setMountPoint($source, $target) { - $source = $this->stripPath($source); - $target = $this->stripPath($target); - $sourceHash = md5($source); - $targetHash = md5($target); - - $query = $this->connection->prepare(' - UPDATE `*PREFIX*share_external` - SET `mountpoint` = ?, `mountpoint_hash` = ? - WHERE `mountpoint_hash` = ? - AND `user` = ? - '); - $result = (bool)$query->execute(array($target, $targetHash, $sourceHash, $this->uid)); - - return $result; - } - - public function removeShare($mountPoint) { - $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) { - $share = $getShare->fetch(); - $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); - } - - $query = $this->connection->prepare(' - DELETE FROM `*PREFIX*share_external` - WHERE `mountpoint_hash` = ? - AND `user` = ? - '); - return (bool)$query->execute(array($hash, $this->uid)); - } - - /** - * 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) { - $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline'); - } - } - - $query = $this->connection->prepare(' - DELETE FROM `*PREFIX*share_external` - WHERE `user` = ? - '); - return (bool)$query->execute(array($uid)); - } - - /** - * return a list of shares which are not yet accepted by the user - * - * @return array list of open server-to-server shares - */ - public function getOpenShares() { - return $this->getShares(false); - } - - /** - * return a list of shares wich are accepted by the user - * - * @return array list of accepted server-to-server shares - */ - public function getAcceptedShares() { - return $this->getShares(true); - } - - /** - * return a list of shares for the user - * - * @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 - */ - 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; - } - $query .= ' ORDER BY `id` ASC'; - - $shares = $this->connection->prepare($query); - $result = $shares->execute($parameters); - - return $result ? $shares->fetchAll() : []; - } -} diff --git a/apps/files_sharing/lib/external/mount.php b/apps/files_sharing/lib/external/mount.php deleted file mode 100644 index 9cf66d3f8f0..00000000000 --- a/apps/files_sharing/lib/external/mount.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -use OC\Files\Mount\MountPoint; -use OC\Files\Mount\MoveableMount; - -class Mount extends MountPoint implements MoveableMount { - - /** - * @var \OCA\Files_Sharing\External\Manager - */ - protected $manager; - - /** - * @param string|\OC\Files\Storage\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; - } - - /** - * Move the mount point to $target - * - * @param string $target the target mount point - * @return bool - */ - public function moveMount($target) { - $result = $this->manager->setMountPoint($this->mountPoint, $target); - $this->setMountPoint($target); - - return $result; - } - - /** - * Remove the mount points - * - * @return mixed - * @return bool - */ - public function removeMount() { - return $this->manager->removeShare($this->mountPoint); - } -} diff --git a/apps/files_sharing/lib/external/mountprovider.php b/apps/files_sharing/lib/external/mountprovider.php deleted file mode 100644 index 67d85f27d81..00000000000 --- a/apps/files_sharing/lib/external/mountprovider.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -use OCP\Files\Config\IMountProvider; -use OCP\Files\Storage\IStorageFactory; -use OCP\IDBConnection; -use OCP\IUser; - -class MountProvider implements IMountProvider { - const STORAGE = '\OCA\Files_Sharing\External\Storage'; - - /** - * @var \OCP\IDBConnection - */ - private $connection; - - /** - * @var callable - */ - private $managerProvider; - - /** - * @param \OCP\IDBConnection $connection - * @param callable $managerProvider due to setup order we need a callable that return the manager instead of the manager itself - */ - public function __construct(IDBConnection $connection, callable $managerProvider) { - $this->connection = $connection; - $this->managerProvider = $managerProvider; - } - - public function getMount(IUser $user, $data, IStorageFactory $storageFactory) { - $managerProvider = $this->managerProvider; - $manager = $managerProvider(); - $data['manager'] = $manager; - $mountPoint = '/' . $user->getUID() . '/files/' . ltrim($data['mountpoint'], '/'); - $data['mountpoint'] = $mountPoint; - $data['certificateManager'] = \OC::$server->getCertificateManager($user->getUID()); - 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]); - $mounts = []; - while ($row = $query->fetch()) { - $row['manager'] = $this; - $row['token'] = $row['share_token']; - $mounts[] = $this->getMount($user, $row, $loader); - } - return $mounts; - } -} diff --git a/apps/files_sharing/lib/external/scanner.php b/apps/files_sharing/lib/external/scanner.php deleted file mode 100644 index 1cc6cf8f5f9..00000000000 --- a/apps/files_sharing/lib/external/scanner.php +++ /dev/null @@ -1,127 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Olivier Paroz <github@oparoz.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -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 */ - 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(); - } - - /** - * Scan a single file and store it in the cache. - * If an exception happened while accessing the external storage, - * the storage will be checked for availability and removed - * if it is not available any more. - * - * @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 bool $lock set to false to disable getting an additional read lock during scanning - * @return array an array of metadata of the scanned file - */ - public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) { - try { - return parent::scanFile($file, $reuseExisting); - } catch (ForbiddenException $e) { - $this->storage->checkStorageAvailability(); - } catch (NotFoundException $e) { - // if the storage isn't found, the call to - // checkStorageAvailable() will verify it and remove it - // if appropriate - $this->storage->checkStorageAvailability(); - } catch (StorageInvalidException $e) { - $this->storage->checkStorageAvailability(); - } catch (StorageNotAvailableException $e) { - $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 deleted file mode 100644 index 8fe7af66044..00000000000 --- a/apps/files_sharing/lib/external/storage.php +++ /dev/null @@ -1,323 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\External; - -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ConnectException; -use OC\Files\Storage\DAV; -use OC\ForbiddenException; -use OCA\FederatedFileSharing\DiscoveryManager; -use OCA\Files_Sharing\ISharedStorage; -use OCP\Files\NotFoundException; -use OCP\Files\StorageInvalidException; -use OCP\Files\StorageNotAvailableException; - -class Storage extends DAV implements ISharedStorage { - /** @var string */ - private $remoteUser; - /** @var string */ - private $remote; - /** @var string */ - private $mountPoint; - /** @var string */ - private $token; - /** @var \OCP\ICacheFactory */ - private $memcacheFactory; - /** @var \OCP\Http\Client\IClientService */ - private $httpClient; - /** @var \OCP\ICertificateManager */ - private $certificateManager; - /** @var bool */ - private $updateChecked = false; - - /** - * @var \OCA\Files_Sharing\External\Manager - */ - private $manager; - - public function __construct($options) { - $this->memcacheFactory = \OC::$server->getMemCacheFactory(); - $this->httpClient = \OC::$server->getHTTPClientService(); - $discoveryManager = new DiscoveryManager( - $this->memcacheFactory, - \OC::$server->getHTTPClientService() - ); - - $this->manager = $options['manager']; - $this->certificateManager = $options['certificateManager']; - $this->remote = $options['remote']; - $this->remoteUser = $options['owner']; - list($protocol, $remote) = explode('://', $this->remote); - if (strpos($remote, '/')) { - list($host, $root) = explode('/', $remote, 2); - } else { - $host = $remote; - $root = ''; - } - $secure = $protocol === 'https'; - $root = rtrim($root, '/') . $discoveryManager->getWebDavEndpoint($this->remote); - $this->mountPoint = $options['mountpoint']; - $this->token = $options['token']; - parent::__construct(array( - 'secure' => $secure, - 'host' => $host, - 'root' => $root, - 'user' => $options['token'], - 'password' => (string)$options['password'] - )); - - $this->getWatcher()->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE); - } - - public function getRemoteUser() { - return $this->remoteUser; - } - - public function getRemote() { - return $this->remote; - } - - public function getMountPoint() { - return $this->mountPoint; - } - - public function getToken() { - return $this->token; - } - - public function getPassword() { - return $this->password; - } - - /** - * @brief get id of the mount point - * @return string - */ - public function getId() { - return 'shared::' . md5($this->token . '@' . $this->remote); - } - - public function getCache($path = '', $storage = null) { - if (is_null($this->cache)) { - $this->cache = new Cache($this, $this->remote, $this->remoteUser); - } - return $this->cache; - } - - /** - * @param string $path - * @param \OC\Files\Storage\Storage $storage - * @return \OCA\Files_Sharing\External\Scanner - */ - public function getScanner($path = '', $storage = null) { - if (!$storage) { - $storage = $this; - } - if (!isset($this->scanner)) { - $this->scanner = new Scanner($storage); - } - 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) { - // 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) { - return false; - } - $this->updateChecked = true; - try { - return parent::hasUpdated('', $time); - } catch (StorageInvalidException $e) { - // check if it needs to be removed - $this->checkStorageAvailability(); - throw $e; - } catch (StorageNotAvailableException $e) { - // check if it needs to be removed or just temp unavailable - $this->checkStorageAvailability(); - throw $e; - } - } - - /** - * Check whether this storage is permanently or temporarily - * unavailable - * - * @throws \OCP\Files\StorageNotAvailableException - * @throws \OCP\Files\StorageInvalidException - */ - public function checkStorageAvailability() { - // see if we can find out why the share is unavailable - try { - $this->getShareInfo(); - } catch (NotFoundException $e) { - // a 404 can either mean that the share no longer exists or there is no ownCloud on the remote - if ($this->testRemote()) { - // valid ownCloud instance means that the public share no longer exists - // since this is permanent (re-sharing the file will create a new token) - // we remove the invalid storage - $this->manager->removeShare($this->mountPoint); - $this->manager->getMountManager()->removeMount($this->mountPoint); - throw new StorageInvalidException(); - } else { - // ownCloud instance is gone, likely to be a temporary server configuration error - throw new StorageNotAvailableException(); - } - } 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(); - } catch (\GuzzleHttp\Exception\ConnectException $e) { - throw new StorageNotAvailableException(); - } catch (\GuzzleHttp\Exception\RequestException $e) { - throw new StorageNotAvailableException(); - } catch (\Exception $e) { - throw $e; - } - } - - public function file_exists($path) { - if ($path === '') { - return true; - } else { - return parent::file_exists($path); - } - } - - /** - * check if the configured remote is a valid federated share provider - * - * @return bool - */ - protected function testRemote() { - try { - return $this->testRemoteUrl($this->remote . '/ocs-provider/index.php') - || $this->testRemoteUrl($this->remote . '/ocs-provider/') - || $this->testRemoteUrl($this->remote . '/status.php'); - } catch (\Exception $e) { - return false; - } - } - - /** - * @param string $url - * @return bool - */ - private function testRemoteUrl($url) { - $cache = $this->memcacheFactory->create('files_sharing_remote_url'); - if($cache->hasKey($url)) { - return (bool)$cache->get($url); - } - - $client = $this->httpClient->newClient(); - try { - $result = $client->get($url)->getBody(); - $data = json_decode($result); - $returnValue = (is_object($data) && !empty($data->version)); - } catch (ConnectException $e) { - $returnValue = false; - } catch (ClientException $e) { - $returnValue = false; - } - - $cache->set($url, $returnValue); - return $returnValue; - } - - /** - * Whether the remote is an ownCloud, used since some sharing features are not - * standardized. Let's use this to detect whether to use it. - * - * @return bool - */ - public function remoteIsOwnCloud() { - if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) { - return false; - } - return true; - } - - /** - * @return mixed - * @throws ForbiddenException - * @throws NotFoundException - * @throws \Exception - */ - public function getShareInfo() { - $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']; - } - - $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token; - - // TODO: DI - $client = \OC::$server->getHTTPClientService()->newClient(); - try { - $response = $client->post($url, ['body' => ['password' => $password]]); - } catch (\GuzzleHttp\Exception\RequestException $e) { - if ($e->getCode() === 401 || $e->getCode() === 403) { - throw new ForbiddenException(); - } - if ($e->getCode() === 404) { - throw new NotFoundException(); - } - // 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(); - } - - return json_decode($response->getBody(), true); - } - - public function getOwner($path) { - list(, $remote) = explode('://', $this->remote, 2); - return $this->remoteUser . '@' . $remote; - } - - public function isSharable($path) { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { - return false; - } - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE); - } - -} diff --git a/apps/files_sharing/lib/helper.php b/apps/files_sharing/lib/helper.php deleted file mode 100644 index e857974ae74..00000000000 --- a/apps/files_sharing/lib/helper.php +++ /dev/null @@ -1,335 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\Files_Sharing; - -use OC\Files\Filesystem; -use OC\Files\View; -use OCP\Files\NotFoundException; -use OCP\User; - -class Helper { - - public static function registerHooks() { - \OCP\Util::connectHook('OC_Filesystem', 'delete', '\OC\Files\Cache\Shared_Updater', 'deleteHook'); - \OCP\Util::connectHook('OC_Filesystem', 'post_rename', '\OC\Files\Cache\Shared_Updater', 'renameHook'); - \OCP\Util::connectHook('OC_Filesystem', 'post_delete', '\OCA\Files_Sharing\Hooks', 'unshareChildren'); - \OCP\Util::connectHook('OC_Appconfig', 'post_set_value', '\OCA\Files\Share\Maintainer', 'configChangeHook'); - - \OCP\Util::connectHook('OCP\Share', 'post_shared', '\OC\Files\Cache\Shared_Updater', 'postShareHook'); - \OCP\Util::connectHook('OCP\Share', 'post_unshare', '\OC\Files\Cache\Shared_Updater', 'postUnshareHook'); - \OCP\Util::connectHook('OCP\Share', 'post_unshareFromSelf', '\OC\Files\Cache\Shared_Updater', 'postUnshareFromSelfHook'); - - \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); - - $linkItem = \OCP\Share::getShareByToken($token, !$password); - if($linkItem === false || ($linkItem['item_type'] !== 'file' && $linkItem['item_type'] !== 'folder')) { - \OC_Response::setStatus(404); - \OCP\Util::writeLog('core-preview', 'Passed token parameter is not valid', \OCP\Util::DEBUG); - exit; - } - - if(!isset($linkItem['uid_owner']) || !isset($linkItem['file_source'])) { - \OC_Response::setStatus(500); - \OCP\Util::writeLog('core-preview', 'Passed token seems to be valid, but it does not contain all necessary information . ("' . $token . '")', \OCP\Util::WARN); - exit; - } - - $rootLinkItem = \OCP\Share::resolveReShare($linkItem); - $path = null; - if (isset($rootLinkItem['uid_owner'])) { - \OCP\JSON::checkUserExists($rootLinkItem['uid_owner']); - \OC_Util::tearDownFS(); - \OC_Util::setupFS($rootLinkItem['uid_owner']); - } - - try { - $path = Filesystem::getPath($linkItem['file_source']); - } 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 (!isset($linkItem['item_type'])) { - \OCP\Util::writeLog('share', 'No item type set for share id: ' . $linkItem['id'], \OCP\Util::ERROR); - \OC_Response::setStatus(404); - \OCP\JSON::error(array('success' => false)); - exit(); - } - - if (isset($linkItem['share_with']) && (int)$linkItem['share_type'] === \OCP\Share::SHARE_TYPE_LINK) { - if (!self::authenticate($linkItem, $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( - 'linkItem' => $linkItem, - 'basePath' => $basePath, - 'realPath' => $path - ); - } - - /** - * Authenticate link item with the given password - * or with the session if no password was given. - * @param array $linkItem link item array - * @param string $password optional password - * - * @return boolean true if authorized, false otherwise - */ - public static function authenticate($linkItem, $password = null) { - if ($password !== null) { - if ($linkItem['share_type'] == \OCP\Share::SHARE_TYPE_LINK) { - // Check Password - $newHash = ''; - if(\OC::$server->getHasher()->verify($password, $linkItem['share_with'], $newHash)) { - // Save item id in session for future requests - \OC::$server->getSession()->set('public_link_authenticated', $linkItem['id']); - - /** - * FIXME: Migrate old hashes to new hash format - * Due to the fact that there is no reasonable functionality to update the password - * of an existing share no migration is yet performed there. - * The only possibility is to update the existing share which will result in a new - * share ID and is a major hack. - * - * In the future the migration should be performed once there is a proper method - * to update the share's password. (for example `$share->updatePassword($password)` - * - * @link https://github.com/owncloud/core/issues/10671 - */ - if(!empty($newHash)) { - - } - } else { - return false; - } - } else { - \OCP\Util::writeLog('share', 'Unknown share type '.$linkItem['share_type'] - .' for share id '.$linkItem['id'], \OCP\Util::ERROR); - return false; - } - - } - else { - // not authenticated ? - if ( ! \OC::$server->getSession()->exists('public_link_authenticated') - || \OC::$server->getSession()->get('public_link_authenticated') !== $linkItem['id']) { - return false; - } - } - return true; - } - - 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; - } - - /** - * 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) { - $pathinfo = pathinfo($path); - $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); - $i++; - } - - return $path; - } - - /** - * allow users from other ownCloud instances to mount public links share by this instance - * @return bool - */ - public static function isOutgoingServer2serverShareEnabled() { - $appConfig = \OC::$server->getAppConfig(); - $result = $appConfig->getValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); - return ($result === 'yes') ? true : false; - } - - /** - * allow user to mount public links from onther ownClouds - * @return bool - */ - public static function isIncomingServer2serverShareEnabled() { - $appConfig = \OC::$server->getAppConfig(); - $result = $appConfig->getValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); - return ($result === 'yes') ? true : false; - } - - /** - * get default share folder - * - * @return string - */ - public static function getShareFolder() { - $shareFolder = \OC::$server->getConfig()->getSystemValue('share_folder', '/'); - $shareFolder = Filesystem::normalizePath($shareFolder); - - if (!Filesystem::file_exists($shareFolder)) { - $dir = ''; - $subdirs = explode('/', $shareFolder); - foreach ($subdirs as $subdir) { - $dir = $dir . '/' . $subdir; - if (!Filesystem::is_dir($dir)) { - Filesystem::mkdir($dir); - } - } - } - - return $shareFolder; - - } - - /** - * set default share folder - * - * @param string $shareFolder - */ - public static function setShareFolder($shareFolder) { - \OC::$server->getConfig()->setSystemValue('share_folder', $shareFolder); - } - -} diff --git a/apps/files_sharing/lib/hooks.php b/apps/files_sharing/lib/hooks.php deleted file mode 100644 index 4882ffcb001..00000000000 --- a/apps/files_sharing/lib/hooks.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OC\Files\Filesystem; -use OCA\FederatedFileSharing\DiscoveryManager; - -class Hooks { - - public static function deleteUser($params) { - $discoveryManager = new DiscoveryManager( - \OC::$server->getMemCacheFactory(), - \OC::$server->getHTTPClientService() - ); - $manager = new External\Manager( - \OC::$server->getDatabaseConnection(), - \OC\Files\Filesystem::getMountManager(), - \OC\Files\Filesystem::getLoader(), - \OC::$server->getHTTPHelper(), - \OC::$server->getNotificationManager(), - $discoveryManager, - $params['uid']); - - $manager->removeUserShares($params['uid']); - } - - public static function unshareChildren($params) { - $path = Filesystem::getView()->getAbsolutePath($params['path']); - $view = new \OC\Files\View('/'); - - // find share mount points within $path and unmount them - $mountManager = \OC\Files\Filesystem::getMountManager(); - $mountedShares = $mountManager->findIn($path); - foreach ($mountedShares as $mount) { - if ($mount->getStorage()->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) { - $mountPoint = $mount->getMountPoint(); - $view->unlink($mountPoint); - } - } - } -} diff --git a/apps/files_sharing/lib/isharedstorage.php b/apps/files_sharing/lib/isharedstorage.php deleted file mode 100644 index bb47b011f48..00000000000 --- a/apps/files_sharing/lib/isharedstorage.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -interface ISharedStorage{ - -} diff --git a/apps/files_sharing/lib/maintainer.php b/apps/files_sharing/lib/maintainer.php deleted file mode 100644 index a728f6bbb67..00000000000 --- a/apps/files_sharing/lib/maintainer.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files\Share; - -/** - * Maintains stuff around the sharing functionality - * - * for example: on disable of "allow links" it removes all link shares - */ - -class Maintainer { - - /** - * Keeps track of the "allow links" config setting - * and removes all link shares if the config option is set to "no" - * - * @param array $params array with app, key, value as named values - */ - static public function configChangeHook($params) { - if($params['app'] === 'core' && $params['key'] === 'shareapi_allow_links' && $params['value'] === 'no') { - \OCP\Share::removeAllLinkShares(); - } - } - -} diff --git a/apps/files_sharing/lib/middleware/sharingcheckmiddleware.php b/apps/files_sharing/lib/middleware/sharingcheckmiddleware.php deleted file mode 100644 index a4a968b37fa..00000000000 --- a/apps/files_sharing/lib/middleware/sharingcheckmiddleware.php +++ /dev/null @@ -1,161 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing\Middleware; - -use OCP\App\IAppManager; -use OCP\AppFramework\Http\NotFoundResponse; -use OCP\AppFramework\Middleware; -use OCP\Files\NotFoundException; -use OCP\IConfig; -use OCP\AppFramework\Utility\IControllerMethodReflector; -use OCA\Files_Sharing\Exceptions\S2SException; -use OCP\AppFramework\Http\JSONResponse; - -/** - * Checks whether the "sharing check" is enabled - * - * @package OCA\Files_Sharing\Middleware - */ -class SharingCheckMiddleware extends Middleware { - - /** @var string */ - protected $appName; - /** @var IConfig */ - protected $config; - /** @var IAppManager */ - protected $appManager; - /** @var IControllerMethodReflector */ - protected $reflector; - - /*** - * @param string $appName - * @param IConfig $config - * @param IAppManager $appManager - */ - public function __construct($appName, - IConfig $config, - IAppManager $appManager, - IControllerMethodReflector $reflector - ) { - $this->appName = $appName; - $this->config = $config; - $this->appManager = $appManager; - $this->reflector = $reflector; - } - - /** - * Check if sharing is enabled before the controllers is executed - * - * @param \OCP\AppFramework\Controller $controller - * @param string $methodName - * @throws NotFoundException - */ - public function beforeController($controller, $methodName) { - if(!$this->isSharingEnabled()) { - throw new NotFoundException('Sharing is disabled.'); - } - - if ($controller instanceof \OCA\Files_Sharing\Controllers\ExternalSharesController && - !$this->externalSharesChecks()) { - throw new S2SException('Federated sharing not allowed'); - } else if ($controller instanceof \OCA\Files_Sharing\Controllers\ShareController && - !$this->isLinkSharingEnabled()) { - throw new NotFoundException('Link sharing is disabled'); - } - } - - /** - * Return 404 page in case of a not found exception - * - * @param \OCP\AppFramework\Controller $controller - * @param string $methodName - * @param \Exception $exception - * @return NotFoundResponse - * @throws \Exception - */ - public function afterException($controller, $methodName, \Exception $exception) { - if(is_a($exception, '\OCP\Files\NotFoundException')) { - return new NotFoundResponse(); - } - - if (is_a($exception, '\OCA\Files_Sharing\Exceptions\S2SException')) { - return new JSONResponse($exception->getMessage(), 405); - } - - throw $exception; - } - - /** - * 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') { - return false; - } - - if (!$this->reflector->hasAnnotation('NoOutgoingFederatedSharingRequired') && - $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') !== 'yes') { - return false; - } - - return true; - } - - /** - * Check whether sharing is enabled - * @return bool - */ - private function isSharingEnabled() { - // 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)) { - 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.php b/apps/files_sharing/lib/migration.php deleted file mode 100644 index 766a7ebd52a..00000000000 --- a/apps/files_sharing/lib/migration.php +++ /dev/null @@ -1,274 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use Doctrine\DBAL\Connection; -use OCP\IDBConnection; -use OC\Cache\CappedMemoryCache; - -/** - * Class Migration - * - * @package OCA\Files_Sharing - * @group DB - */ -class Migration { - - /** @var IDBConnection */ - private $connection; - - /** @var array with all shares we already saw */ - private $shareCache; - - /** @var string */ - private $table = 'share'; - - public function __construct(IDBConnection $connection) { - $this->connection = $connection; - - // We cache up to 10k share items (~20MB) - $this->shareCache = new CappedMemoryCache(10000); - } - - /** - * move all re-shares to the owner in order to have a flat list of shares - * upgrade from oC 8.2 to 9.0 with the new sharing - */ - public function removeReShares() { - - $stmt = $this->getReShares(); - - $owners = []; - while($share = $stmt->fetch()) { - - $this->shareCache[$share['id']] = $share; - - $owners[$share['id']] = [ - 'owner' => $this->findOwner($share), - 'initiator' => $share['uid_owner'], - 'type' => $share['share_type'], - ]; - - if (count($owners) === 1000) { - $this->updateOwners($owners); - $owners = []; - } - } - - $stmt->closeCursor(); - - if (count($owners)) { - $this->updateOwners($owners); - } - } - - /** - * update all owner information so that all shares have an owner - * and an initiator for the upgrade from oC 8.2 to 9.0 with the new sharing - */ - public function updateInitiatorInfo() { - while (true) { - $shares = $this->getMissingInitiator(1000); - - if (empty($shares)) { - break; - } - - $owners = []; - foreach ($shares as $share) { - $owners[$share['id']] = [ - 'owner' => $share['uid_owner'], - 'initiator' => $share['uid_owner'], - 'type' => $share['share_type'], - ]; - } - $this->updateOwners($owners); - } - } - - /** - * find the owner of a re-shared file/folder - * - * @param array $share - * @return array - */ - private function findOwner($share) { - $currentShare = $share; - while(!is_null($currentShare['parent'])) { - if (isset($this->shareCache[$currentShare['parent']])) { - $currentShare = $this->shareCache[$currentShare['parent']]; - } else { - $currentShare = $this->getShare((int)$currentShare['parent']); - $this->shareCache[$currentShare['id']] = $currentShare; - } - } - - return $currentShare['uid_owner']; - } - - /** - * Get $n re-shares from the database - * - * @param int $n The max number of shares to fetch - * @return \Doctrine\DBAL\Driver\Statement - */ - private function getReShares() { - $query = $this->connection->getQueryBuilder(); - $query->select(['id', 'parent', 'uid_owner', 'share_type']) - ->from($this->table) - ->where($query->expr()->in( - 'share_type', - $query->createNamedParameter( - [ - \OCP\Share::SHARE_TYPE_USER, - \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK, - \OCP\Share::SHARE_TYPE_REMOTE, - ], - Connection::PARAM_INT_ARRAY - ) - )) - ->andWhere($query->expr()->in( - 'item_type', - $query->createNamedParameter( - ['file', 'folder'], - Connection::PARAM_STR_ARRAY - ) - )) - ->andWhere($query->expr()->isNotNull('parent')) - ->orderBy('id', 'asc'); - return $query->execute(); - - - $shares = $result->fetchAll(); - $result->closeCursor(); - - $ordered = []; - foreach ($shares as $share) { - $ordered[(int)$share['id']] = $share; - } - - return $ordered; - } - - /** - * Get $n re-shares from the database - * - * @param int $n The max number of shares to fetch - * @return array - */ - private function getMissingInitiator($n = 1000) { - $query = $this->connection->getQueryBuilder(); - $query->select(['id', 'uid_owner', 'share_type']) - ->from($this->table) - ->where($query->expr()->in( - 'share_type', - $query->createNamedParameter( - [ - \OCP\Share::SHARE_TYPE_USER, - \OCP\Share::SHARE_TYPE_GROUP, - \OCP\Share::SHARE_TYPE_LINK, - \OCP\Share::SHARE_TYPE_REMOTE, - ], - Connection::PARAM_INT_ARRAY - ) - )) - ->andWhere($query->expr()->in( - 'item_type', - $query->createNamedParameter( - ['file', 'folder'], - Connection::PARAM_STR_ARRAY - ) - )) - ->andWhere($query->expr()->isNull('uid_initiator')) - ->orderBy('id', 'asc') - ->setMaxResults($n); - $result = $query->execute(); - $shares = $result->fetchAll(); - $result->closeCursor(); - - $ordered = []; - foreach ($shares as $share) { - $ordered[(int)$share['id']] = $share; - } - - return $ordered; - } - - /** - * get a specific share - * - * @param int $id - * @return array - */ - private function getShare($id) { - $query = $this->connection->getQueryBuilder(); - $query->select(['id', 'parent', 'uid_owner']) - ->from($this->table) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))); - $result = $query->execute(); - $share = $result->fetchAll(); - $result->closeCursor(); - - return $share[0]; - } - - /** - * update database with the new owners - * - * @param array $owners - * @throws \Exception - */ - private function updateOwners($owners) { - - $this->connection->beginTransaction(); - - try { - - foreach ($owners as $id => $owner) { - $query = $this->connection->getQueryBuilder(); - $query->update($this->table) - ->set('uid_owner', $query->createNamedParameter($owner['owner'])) - ->set('uid_initiator', $query->createNamedParameter($owner['initiator'])); - - - if ((int)$owner['type'] !== \OCP\Share::SHARE_TYPE_LINK) { - $query->set('parent', $query->createNamedParameter(null)); - } - - $query->where($query->expr()->eq('id', $query->createNamedParameter($id))); - - $query->execute(); - } - - $this->connection->commit(); - - } catch (\Exception $e) { - $this->connection->rollBack(); - throw $e; - } - - } - -} diff --git a/apps/files_sharing/lib/mountprovider.php b/apps/files_sharing/lib/mountprovider.php deleted file mode 100644 index 4a60e44bb26..00000000000 --- a/apps/files_sharing/lib/mountprovider.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OC\Files\Filesystem; -use OC\User\NoUserException; -use OCP\Files\Config\IMountProvider; -use OCP\Files\Storage\IStorageFactory; -use OCP\IConfig; -use OCP\IUser; - -class MountProvider implements IMountProvider { - /** - * @var \OCP\IConfig - */ - protected $config; - - /** - * @param \OCP\IConfig $config - */ - public function __construct(IConfig $config) { - $this->config = $config; - } - - - /** - * 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[] - */ - public function getMountsForUser(IUser $user, IStorageFactory $storageFactory) { - $shares = \OCP\Share::getItemsSharedWithUser('file', $user->getUID()); - $shares = array_filter($shares, function ($share) { - return $share['permissions'] > 0; - }); - $shares = array_map(function ($share) use ($user, $storageFactory) { - - return new SharedMount( - '\OC\Files\Storage\Shared', - '/' . $user->getUID() . '/' . $share['file_target'], - array( - 'share' => $share, - 'user' => $user->getUID() - ), - $storageFactory - ); - }, $shares); - // array_filter removes the null values from the array - return array_filter($shares); - } -} diff --git a/apps/files_sharing/lib/notifier.php b/apps/files_sharing/lib/notifier.php deleted file mode 100644 index 27e4e2565f2..00000000000 --- a/apps/files_sharing/lib/notifier.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - - -use OCP\Notification\INotification; -use OCP\Notification\INotifier; - -class Notifier implements INotifier { - /** @var \OCP\L10N\IFactory */ - protected $factory; - - /** - * @param \OCP\L10N\IFactory $factory - */ - public function __construct(\OCP\L10N\IFactory $factory) { - $this->factory = $factory; - } - - /** - * @param INotification $notification - * @param string $languageCode The code of the language that should be used to prepare the notification - * @return INotification - */ - public function prepare(INotification $notification, $languageCode) { - if ($notification->getApp() !== 'files_sharing') { - // Not my app => throw - throw new \InvalidArgumentException(); - } - - // Read the language from the notification - $l = $this->factory->get('files_sharing', $languageCode); - - switch ($notification->getSubject()) { - // Deal with known subjects - case 'remote_share': - $params = $notification->getSubjectParameters(); - $notification->setParsedSubject( - (string) $l->t('You received "/%2$s" as a remote share from %1$s', $params) - ); - - // Deal with the actions for a known subject - foreach ($notification->getActions() as $action) { - switch ($action->getLabel()) { - case 'accept': - $action->setParsedLabel( - (string) $l->t('Accept') - ) - ->setPrimary(true); - break; - - case 'decline': - $action->setParsedLabel( - (string) $l->t('Decline') - ); - break; - } - - $notification->addParsedAction($action); - } - return $notification; - - default: - // Unknown subject => Unknown notification => throw - throw new \InvalidArgumentException(); - } - } -} diff --git a/apps/files_sharing/lib/scanner.php b/apps/files_sharing/lib/scanner.php deleted file mode 100644 index 8b32d014a08..00000000000 --- a/apps/files_sharing/lib/scanner.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Cache; - -use OC\Files\ObjectStore\NoopScanner; -use OC\Files\Storage\Shared; - -/** - * Scanner for SharedStorage - */ -class SharedScanner extends Scanner { - private $sourceScanner; - - /** - * Returns metadata from the shared storage, but - * with permissions from the source storage. - * - * @param string $path path of the file for which to retrieve metadata - * - * @return array an array of metadata of the file - */ - public function getData($path) { - $data = parent::getData($path); - $sourcePath = $this->storage->getSourcePath($path); - list($sourceStorage, $internalPath) = \OC\Files\Filesystem::resolvePath($sourcePath); - $data['permissions'] = $sourceStorage->getPermissions($internalPath); - return $data; - } - - private function getSourceScanner() { - if ($this->sourceScanner) { - return $this->sourceScanner; - } - if ($this->storage->instanceOfStorage('\OC\Files\Storage\Shared')) { - /** @var \OC\Files\Storage\Storage $storage */ - list($storage) = $this->storage->resolvePath(''); - $this->sourceScanner = $storage->getScanner(); - return $this->sourceScanner; - } else { - return null; - } - } - - public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) { - $sourceScanner = $this->getSourceScanner(); - if ($sourceScanner instanceof NoopScanner) { - return []; - } else { - return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock); - } - } -} - diff --git a/apps/files_sharing/lib/share/folder.php b/apps/files_sharing/lib/share/folder.php deleted file mode 100644 index 1004f049866..00000000000 --- a/apps/files_sharing/lib/share/folder.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -class OC_Share_Backend_Folder extends OC_Share_Backend_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; - } - - 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')); - if ($row = $result->fetchRow()) { - $mimetype = $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']); - // If a child folder is found look inside it - if ($file['mimetype'] == $mimetype) { - $parents[] = $file['fileid']; - } - } - } - return $children; - } - -} diff --git a/apps/files_sharing/lib/sharedmount.php b/apps/files_sharing/lib/sharedmount.php deleted file mode 100644 index 1e554cc5948..00000000000 --- a/apps/files_sharing/lib/sharedmount.php +++ /dev/null @@ -1,212 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OC\Files\Mount\MountPoint; -use OC\Files\Mount\MoveableMount; -use OC\Files\View; - -/** - * Shared mount points can be moved by the user - */ -class SharedMount extends MountPoint implements MoveableMount { - /** - * @var \OC\Files\Storage\Shared $storage - */ - protected $storage = null; - - /** - * @var \OC\Files\View - */ - private $recipientView; - - /** - * @var string - */ - private $user; - - /** - * @param string $storage - * @param string $mountpoint - * @param array|null $arguments - * @param \OCP\Files\Storage\IStorageFactory $loader - */ - public function __construct($storage, $mountpoint, $arguments = null, $loader = null) { - $this->user = $arguments['user']; - $this->recipientView = new View('/' . $this->user . '/files'); - $newMountPoint = $this->verifyMountPoint($arguments['share']); - $absMountPoint = '/' . $this->user . '/files' . $newMountPoint; - $arguments['ownerView'] = new View('/' . $arguments['share']['uid_owner'] . '/files'); - parent::__construct($storage, $absMountPoint, $arguments, $loader); - } - - /** - * check if the parent folder exists otherwise move the mount point up - * - * @param array $share - * @return string - */ - private function verifyMountPoint(&$share) { - - $mountPoint = basename($share['file_target']); - $parent = dirname($share['file_target']); - - if (!$this->recipientView->is_dir($parent)) { - $parent = Helper::getShareFolder(); - } - - $newMountPoint = \OCA\Files_Sharing\Helper::generateUniqueTarget( - \OC\Files\Filesystem::normalizePath($parent . '/' . $mountPoint), - [], - $this->recipientView - ); - - if ($newMountPoint !== $share['file_target']) { - $this->updateFileTarget($newMountPoint, $share); - $share['file_target'] = $newMountPoint; - $share['unique_name'] = true; - } - - return $newMountPoint; - } - - /** - * update fileTarget in the database if the mount point changed - * - * @param string $newPath - * @param array $share reference to the share which should be modified - * @return bool - */ - private function updateFileTarget($newPath, &$share) { - // if the user renames a mount point from a group share we need to create a new db entry - // for the unique name - if ($share['share_type'] === \OCP\Share::SHARE_TYPE_GROUP && empty($share['unique_name'])) { - $query = \OCP\DB::prepare('INSERT INTO `*PREFIX*share` (`item_type`, `item_source`, `item_target`,' - .' `share_type`, `share_with`, `uid_owner`, `permissions`, `stime`, `file_source`,' - .' `file_target`, `token`, `parent`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'); - $arguments = array($share['item_type'], $share['item_source'], $share['item_target'], - 2, $this->user, $share['uid_owner'], $share['permissions'], $share['stime'], $share['file_source'], - $newPath, $share['token'], $share['id']); - } else { - // rename mount point - $query = \OCP\DB::prepare( - 'Update `*PREFIX*share` - SET `file_target` = ? - WHERE `id` = ?' - ); - $arguments = array($newPath, $share['id']); - } - - $result = $query->execute($arguments); - - return $result === 1 ? true : false; - } - - /** - * 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' - * @throws \OCA\Files_Sharing\Exceptions\BrokenPath - */ - protected 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') { - \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); - } - - // skip 'user' and 'files' - $sliced = array_slice($split, 2); - $relPath = implode('/', $sliced); - - return '/' . $relPath; - } - - /** - * Move the mount point to $target - * - * @param string $target the target mount point - * @return bool - */ - public function moveMount($target) { - - $relTargetPath = $this->stripUserFilesPath($target); - $share = $this->storage->getShare(); - - $result = true; - - if (!empty($share['grouped'])) { - foreach ($share['grouped'] as $s) { - $result = $this->updateFileTarget($relTargetPath, $s) && $result; - } - } else { - $result = $this->updateFileTarget($relTargetPath, $share) && $result; - } - - if ($result) { - $this->setMountPoint($target); - $this->storage->setUniqueName(); - $this->storage->setMountPoint($relTargetPath); - - } else { - \OCP\Util::writeLog('file sharing', - 'Could not rename mount point for shared folder "' . $this->getMountPoint() . '" to "' . $target . '"', - \OCP\Util::ERROR); - } - - return $result; - } - - /** - * Remove the mount points - * - * @return bool - */ - public function removeMount() { - $mountManager = \OC\Files\Filesystem::getMountManager(); - /** @var $storage \OC\Files\Storage\Shared */ - $storage = $this->getStorage(); - $result = $storage->unshareStorage(); - $mountManager->removeMount($this->mountPoint); - - return $result; - } - - /** - * @return array - */ - public function getShare() { - /** @var $storage \OC\Files\Storage\Shared */ - $storage = $this->getStorage(); - return $storage->getShare(); - } -} diff --git a/apps/files_sharing/lib/sharedpropagator.php b/apps/files_sharing/lib/sharedpropagator.php deleted file mode 100644 index 29735934499..00000000000 --- a/apps/files_sharing/lib/sharedpropagator.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\Files_Sharing; - -use OC\Files\Cache\Propagator; - -class SharedPropagator extends Propagator { - /** - * @var \OC\Files\Storage\Shared - */ - protected $storage; - - /** - * @param string $internalPath - * @param int $time - * @param int $sizeDifference - * @return \array[] all propagated entries - */ - public function propagateChange($internalPath, $time, $sizeDifference = 0) { - $source = $this->storage->getSourcePath($internalPath); - /** @var \OC\Files\Storage\Storage $storage */ - list($storage, $sourceInternalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->getPropagator()->propagateChange($sourceInternalPath, $time, $sizeDifference); - } -} diff --git a/apps/files_sharing/lib/sharedstorage.php b/apps/files_sharing/lib/sharedstorage.php deleted file mode 100644 index 8f4888d20e2..00000000000 --- a/apps/files_sharing/lib/sharedstorage.php +++ /dev/null @@ -1,748 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author scambra <sergio@entrecables.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Storage; - -use OC\Files\Filesystem; -use OC\Files\Cache\FailedCache; -use OCA\Files_Sharing\ISharedStorage; -use OCP\Constants; -use OCP\Files\Cache\ICacheEntry; -use OCP\Files\Storage\IStorage; -use OCP\Lock\ILockingProvider; - -/** - * Convert target path to source path and pass the function call to the correct storage provider - */ -class Shared extends \OC\Files\Storage\Common implements ISharedStorage { - - private $share; // the shared resource - private $files = array(); - - /** - * @var \OC\Files\View - */ - private $ownerView; - - /** - * @var string - */ - private $user; - - private $initialized = false; - - /** - * @var ICacheEntry - */ - private $sourceRootInfo; - - /** - * @var IStorage - */ - private $sourceStorage; - - /** - * @var \OCP\ILogger - */ - private $logger; - - public function __construct($arguments) { - $this->share = $arguments['share']; - $this->ownerView = $arguments['ownerView']; - $this->user = $arguments['user']; - $this->logger = \OC::$server->getLogger(); - } - - private function init() { - if ($this->initialized) { - return; - } - $this->initialized = true; - try { - Filesystem::initMountPoints($this->share['uid_owner']); - $sourcePath = $this->ownerView->getPath($this->share['file_source']); - list($this->sourceStorage, $sourceInternalPath) = $this->ownerView->resolvePath($sourcePath); - $this->sourceRootInfo = $this->sourceStorage->getCache()->get($sourceInternalPath); - } catch (\Exception $e) { - $this->logger->logException($e); - } - } - - private function isValid() { - $this->init(); - return $this->sourceRootInfo && ($this->sourceRootInfo->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; - } - - /** - * get id of the mount point - * - * @return string - */ - public function getId() { - return 'shared::' . $this->getMountPoint(); - } - - /** - * get file cache of the shared item source - * - * @return int - */ - public function getSourceId() { - return (int)$this->share['file_source']; - } - - /** - * Get the source file path, permissions, and owner for a shared file - * - * @param string $target Shared target file path - * @return array Returns array with the keys path, permissions, and owner or false if not found - */ - public function getFile($target) { - $this->init(); - if (!isset($this->files[$target])) { - // Check for partial files - if (pathinfo($target, PATHINFO_EXTENSION) === 'part') { - $source = \OC_Share_Backend_File::getSource(substr($target, 0, -5), $this->getShare()); - if ($source) { - $source['path'] .= '.part'; - // All partial files have delete permission - $source['permissions'] |= \OCP\Constants::PERMISSION_DELETE; - } - } else { - $source = \OC_Share_Backend_File::getSource($target, $this->getShare()); - } - $this->files[$target] = $source; - } - return $this->files[$target]; - } - - /** - * Get the source file path for a shared file - * - * @param string $target Shared target file path - * @return string|false source file path or false if not found - */ - public function getSourcePath($target) { - if (!$this->isValid()){ - return false; - } - $source = $this->getFile($target); - if ($source) { - if (!isset($source['fullPath'])) { - \OC\Files\Filesystem::initMountPoints($source['fileOwner']); - $mount = \OC\Files\Filesystem::getMountByNumericId($source['storage']); - if (is_array($mount) && !empty($mount)) { - $this->files[$target]['fullPath'] = $mount[key($mount)]->getMountPoint() . $source['path']; - } else { - $this->files[$target]['fullPath'] = false; - \OCP\Util::writeLog('files_sharing', "Unable to get mount for shared storage '" . $source['storage'] . "' user '" . $source['fileOwner'] . "'", \OCP\Util::ERROR); - } - } - return $this->files[$target]['fullPath']; - } - return false; - } - - /** - * Get the permissions granted for a shared file - * - * @param string $target Shared target file path - * @return int CRUDS permissions granted - */ - public function getPermissions($target = '') { - if (!$this->isValid()) { - return 0; - } - $permissions = $this->share['permissions']; - // part files and the mount point always have delete permissions - if ($target === '' || pathinfo($target, PATHINFO_EXTENSION) === 'part') { - $permissions |= \OCP\Constants::PERMISSION_DELETE; - } - - if (\OCP\Util::isSharingDisabledForUser()) { - $permissions &= ~\OCP\Constants::PERMISSION_SHARE; - } - - return $permissions; - } - - public function mkdir($path) { - if ($path == '' || $path == '/' || !$this->isCreatable(dirname($path))) { - return false; - } else if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->mkdir($internalPath); - } - return false; - } - - /** - * Delete the directory if DELETE permission is granted - * - * @param string $path - * @return boolean - */ - public function rmdir($path) { - - // never delete a share mount point - if (empty($path)) { - return false; - } - - if (($source = $this->getSourcePath($path)) && $this->isDeletable($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->rmdir($internalPath); - } - return false; - } - - public function opendir($path) { - $source = $this->getSourcePath($path); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->opendir($internalPath); - } - - public function is_dir($path) { - $source = $this->getSourcePath($path); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->is_dir($internalPath); - } - - public function is_file($path) { - if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->is_file($internalPath); - } - return false; - } - - public function stat($path) { - if ($path == '' || $path == '/') { - $stat['size'] = $this->filesize($path); - $stat['mtime'] = $this->filemtime($path); - return $stat; - } else if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->stat($internalPath); - } - return false; - } - - public function filetype($path) { - if ($path == '' || $path == '/') { - return 'dir'; - } else if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->filetype($internalPath); - } - return false; - } - - public function filesize($path) { - $source = $this->getSourcePath($path); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->filesize($internalPath); - } - - public function isCreatable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_CREATE); - } - - public function isReadable($path) { - if (!$this->isValid()) { - return false; - } - if (!$this->file_exists($path)) { - return false; - } - list($storage, $internalPath) = $this->resolvePath($path); - return $storage->isReadable($internalPath); - } - - public function isUpdatable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_UPDATE); - } - - public function isDeletable($path) { - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_DELETE); - } - - public function isSharable($path) { - if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) { - return false; - } - return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE); - } - - public function file_exists($path) { - if ($path == '' || $path == '/') { - return true; - } else if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->file_exists($internalPath); - } - return false; - } - - public function filemtime($path) { - $source = $this->getSourcePath($path); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->filemtime($internalPath); - } - - public function file_get_contents($path) { - $source = $this->getSourcePath($path); - if ($source) { - $info = array( - 'target' => $this->getMountPoint() . $path, - 'source' => $source, - ); - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->file_get_contents($internalPath); - } - } - - public function file_put_contents($path, $data) { - if ($source = $this->getSourcePath($path)) { - // Check if permission is granted - if (($this->file_exists($path) && !$this->isUpdatable($path)) - || ($this->is_dir($path) && !$this->isCreatable($path)) - ) { - return false; - } - $info = array( - 'target' => $this->getMountPoint() . '/' . $path, - 'source' => $source, - ); - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - $result = $storage->file_put_contents($internalPath, $data); - return $result; - } - return false; - } - - /** - * Delete the file if DELETE permission is granted - * - * @param string $path - * @return boolean - */ - public function unlink($path) { - - // never delete a share mount point - if (empty($path)) { - return false; - } - if ($source = $this->getSourcePath($path)) { - if ($this->isDeletable($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->unlink($internalPath); - } - } - return false; - } - - public function rename($path1, $path2) { - $this->init(); - // we need the paths relative to data/user/files - $relPath1 = $this->getMountPoint() . '/' . $path1; - $relPath2 = $this->getMountPoint() . '/' . $path2; - $pathinfo = pathinfo($relPath1); - - $isPartFile = (isset($pathinfo['extension']) && $pathinfo['extension'] === 'part'); - $targetExists = $this->file_exists($path2); - $sameFolder = (dirname($relPath1) === dirname($relPath2)); - if ($targetExists || ($sameFolder && !$isPartFile)) { - // note that renaming a share mount point is always allowed - if (!$this->isUpdatable('')) { - return false; - } - } else { - if (!$this->isCreatable('')) { - return false; - } - } - - - /** - * @var \OC\Files\Storage\Storage $sourceStorage - */ - list($sourceStorage, $sourceInternalPath) = $this->resolvePath($path1); - /** - * @var \OC\Files\Storage\Storage $targetStorage - */ - list($targetStorage, $targetInternalPath) = $this->resolvePath($path2); - - return $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } - - public function copy($path1, $path2) { - // Copy the file if CREATE permission is granted - if ($this->isCreatable(dirname($path2))) { - /** - * @var \OC\Files\Storage\Storage $sourceStorage - */ - list($sourceStorage, $sourceInternalPath) = $this->resolvePath($path1); - /** - * @var \OC\Files\Storage\Storage $targetStorage - */ - list($targetStorage, $targetInternalPath) = $this->resolvePath($path2); - - return $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } - return false; - } - - public function fopen($path, $mode) { - if ($source = $this->getSourcePath($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; - } - - $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; - } - } - } - $info = array( - 'target' => $this->getMountPoint() . $path, - 'source' => $source, - 'mode' => $mode, - ); - \OCP\Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->fopen($internalPath, $mode); - } - return false; - } - - public function getMimeType($path) { - if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->getMimeType($internalPath); - } - return false; - } - - public function free_space($path) { - $source = $this->getSourcePath($path); - if ($source) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->free_space($internalPath); - } - return \OCP\Files\FileInfo::SPACE_UNKNOWN; - } - - public function getLocalFile($path) { - if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->getLocalFile($internalPath); - } - return false; - } - - public function touch($path, $mtime = null) { - if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->touch($internalPath, $mtime); - } - return false; - } - - /** - * return mount point of share, relative to data/user/files - * - * @return string - */ - public function getMountPoint() { - return $this->share['file_target']; - } - - public function setMountPoint($path) { - $this->share['file_target'] = $path; - } - - public function getShareType() { - return $this->share['share_type']; - } - - /** - * does the group share already has a user specific unique name - * - * @return bool - */ - public function uniqueNameSet() { - return (isset($this->share['unique_name']) && $this->share['unique_name']); - } - - /** - * the share now uses a unique name of this user - * - * @brief the share now uses a unique name of this user - */ - public function setUniqueName() { - $this->share['unique_name'] = true; - } - - /** - * get share ID - * - * @return integer unique share ID - */ - public function getShareId() { - return $this->share['id']; - } - - /** - * get the user who shared the file - * - * @return string - */ - public function getSharedFrom() { - return $this->share['uid_owner']; - } - - /** - * @return array - */ - public function getShare() { - return $this->share; - } - - /** - * return share type, can be "file" or "folder" - * - * @return string - */ - public function getItemType() { - return $this->share['item_type']; - } - - public function hasUpdated($path, $time) { - return $this->filemtime($path) > $time; - } - - public function getCache($path = '', $storage = null) { - $this->init(); - if (is_null($this->sourceStorage)) { - return new FailedCache(false); - } - if (!$storage) { - $storage = $this; - } - return new \OC\Files\Cache\Shared_Cache($storage, $this->sourceStorage, $this->sourceRootInfo); - } - - public function getScanner($path = '', $storage = null) { - if (!$storage) { - $storage = $this; - } - return new \OC\Files\Cache\SharedScanner($storage); - } - - public function getPropagator($storage = null) { - if (!$storage) { - $storage = $this; - } - return new \OCA\Files_Sharing\SharedPropagator($storage); - } - - public function getOwner($path) { - if ($path == '') { - $path = $this->getMountPoint(); - } - $source = $this->getFile($path); - if ($source) { - return $source['fileOwner']; - } - return false; - } - - public function getETag($path) { - if ($source = $this->getSourcePath($path)) { - list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($source); - return $storage->getETag($internalPath); - } - return null; - } - - /** - * unshare complete storage, also the grouped shares - * - * @return bool - */ - public function unshareStorage() { - $result = true; - if (!empty($this->share['grouped'])) { - foreach ($this->share['grouped'] as $share) { - $result = $result && \OCP\Share::unshareFromSelf($share['item_type'], $share['file_target']); - } - } - $result = $result && \OCP\Share::unshareFromSelf($this->getItemType(), $this->getMountPoint()); - - return $result; - } - - /** - * Resolve the path for the source of the share - * - * @param string $path - * @return array - */ - public function resolvePath($path) { - $source = $this->getSourcePath($path); - return \OC\Files\Filesystem::resolvePath($source); - } - - /** - * @param \OCP\Files\Storage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function copyFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) { - /** @var \OCP\Files\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $this->resolvePath($targetInternalPath); - return $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } - - /** - * @param \OCP\Files\Storage $sourceStorage - * @param string $sourceInternalPath - * @param string $targetInternalPath - * @return bool - */ - public function moveFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) { - /** @var \OCP\Files\Storage $targetStorage */ - list($targetStorage, $targetInternalPath) = $this->resolvePath($targetInternalPath); - return $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); - } - - /** - * @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); - $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->share['file_source']); - $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); - $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->share['file_source']); - $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); - $targetStorage->changeLock($targetInternalPath, $type, $provider); - } - - /** - * @return array [ available, last_checked ] - */ - public function getAvailability() { - // shares do not participate in availability logic - return [ - 'available' => true, - 'last_checked' => 0 - ]; - } - - /** - * @param bool $available - */ - public function setAvailability($available) { - // shares do not participate in availability logic - } - - public function isLocal() { - $this->init(); - $ownerPath = $this->ownerView->getPath($this->share['item_source']); - list($targetStorage) = $this->ownerView->resolvePath($ownerPath); - return $targetStorage->isLocal(); - } - - public function getSourceStorage() { - return $this->sourceStorage; - } -} diff --git a/apps/files_sharing/lib/updater.php b/apps/files_sharing/lib/updater.php deleted file mode 100644 index 10da2462807..00000000000 --- a/apps/files_sharing/lib/updater.php +++ /dev/null @@ -1,219 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OC\Files\Cache; - -class Shared_Updater { - - /** - * Walk up the users file tree and update the etags. - * - * @param string $user user id - * @param string $path share mount point path, relative to the user's "files" folder - */ - static private function correctUsersFolder($user, $path) { - // $path points to the mount point which is a virtual folder, so we start with - // the parent - $path = '/' . ltrim($path, '/'); - $path = '/files' . dirname($path); - \OC\Files\Filesystem::initMountPoints($user); - $view = new \OC\Files\View('/' . $user); - if ($view->file_exists($path)) { - while ($path !== dirname($path)) { - $etag = $view->getETag($path); - $view->putFileInfo($path, array('etag' => $etag)); - $path = dirname($path); - } - } else { - \OCP\Util::writeLog('files_sharing', 'can not update etags on ' . $path . ' for user ' . $user . '. Path does not exists', \OCP\Util::DEBUG); - } - } - - /** - * @param array $params - */ - static public function renameHook($params) { - self::renameChildren($params['oldpath'], $params['newpath']); - self::moveShareToShare($params['newpath']); - } - - /** - * Fix for https://github.com/owncloud/core/issues/20769 - * - * The owner is allowed to move their files (if they are shared) into a receiving folder - * In this case we need to update the parent of the moved share. Since they are - * effectively handing over ownership of the file the rest of the code needs to know - * they need to build up the reshare tree. - * - * @param string $path - */ - static private function moveShareToShare($path) { - $userFolder = \OC::$server->getUserFolder(); - - // If the user folder can't be constructed (e.g. link share) just return. - if ($userFolder === null) { - return; - } - - $src = $userFolder->get($path); - - $type = $src instanceof \OCP\Files\File ? 'file' : 'folder'; - $shares = \OCP\Share::getItemShared($type, $src->getId()); - - // If the path we move is not a share we don't care - if (empty($shares)) { - return; - } - - // Check if the destination is inside a share - $mountManager = \OC::$server->getMountManager(); - $dstMount = $mountManager->find($src->getPath()); - if (!($dstMount instanceof \OCA\Files_Sharing\SharedMount)) { - return; - } - - $parenShare = $dstMount->getShare(); - - foreach ($shares as $share) { - $qb = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - $qb->update('share') - ->set('parent', $qb->createNamedParameter($parenShare['id'])) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share['id']))) - ->execute(); - } - } - - /** - * @param array $params - */ - static public function deleteHook($params) { - $path = $params['path']; - } - - /** - * update etags if a file was shared - * @param array $params - */ - static public function postShareHook($params) { - - if ($params['itemType'] === 'folder' || $params['itemType'] === 'file') { - - $shareWith = $params['shareWith']; - $shareType = $params['shareType']; - - if ($shareType === \OCP\Share::SHARE_TYPE_USER) { - self::correctUsersFolder($shareWith, $params['fileTarget']); - } elseif ($shareType === \OCP\Share::SHARE_TYPE_GROUP) { - foreach (\OC_Group::usersInGroup($shareWith) as $user) { - self::correctUsersFolder($user, $params['fileTarget']); - } - } - } - } - - /** - * update etags if a file was unshared - * - * @param array $params - */ - static public function postUnshareHook($params) { - - // only update etags for file/folders shared to local users/groups - if (($params['itemType'] === 'file' || $params['itemType'] === 'folder') && - $params['shareType'] !== \OCP\Share::SHARE_TYPE_LINK && - $params['shareType'] !== \OCP\Share::SHARE_TYPE_REMOTE) { - - $deletedShares = isset($params['deletedShares']) ? $params['deletedShares'] : array(); - - foreach ($deletedShares as $share) { - if ($share['shareType'] === \OCP\Share::SHARE_TYPE_GROUP) { - foreach (\OC_Group::usersInGroup($share['shareWith']) as $user) { - self::correctUsersFolder($user, $share['fileTarget']); - } - } else { - self::correctUsersFolder($share['shareWith'], $share['fileTarget']); - } - } - } - } - - /** - * update etags if file was unshared from self - * @param array $params - */ - static public function postUnshareFromSelfHook($params) { - if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') { - foreach ($params['unsharedItems'] as $item) { - if ($item['shareType'] === \OCP\Share::SHARE_TYPE_GROUP) { - foreach (\OC_Group::usersInGroup($item['shareWith']) as $user) { - self::correctUsersFolder($user, $item['fileTarget']); - } - } else { - self::correctUsersFolder($item['shareWith'], $item['fileTarget']); - } - } - } - } - - /** - * clean up oc_share table from files which are no longer exists - * - * This fixes issues from updates from files_sharing < 0.3.5.6 (ownCloud 4.5) - * It will just be called during the update of the app - */ - static public function fixBrokenSharesOnAppUpdate() { - // delete all shares where the original file no longer exists - $findAndRemoveShares = \OCP\DB::prepare('DELETE FROM `*PREFIX*share` ' . - 'WHERE `item_type` IN (\'file\', \'folder\') ' . - 'AND `file_source` NOT IN (SELECT `fileid` FROM `*PREFIX*filecache`)' - ); - $findAndRemoveShares->execute(array()); - } - - /** - * rename mount point from the children if the parent was renamed - * - * @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) { - - $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); - foreach ($mountedShares as $mount) { - if ($mount->getStorage()->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) { - $mountPoint = $mount->getMountPoint(); - $target = str_replace($absOldPath, $absNewPath, $mountPoint); - $mount->moveMount($target); - } - } - } - -} |