diff options
Diffstat (limited to 'lib/private/Share20/Manager.php')
-rw-r--r-- | lib/private/Share20/Manager.php | 256 |
1 files changed, 147 insertions, 109 deletions
diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 219f3d86380..855bb173d56 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -7,11 +7,13 @@ */ namespace OC\Share20; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\Mount\MoveableMount; use OC\KnownUser\KnownUserService; use OC\Share20\Exception\ProviderException; use OCA\Files_Sharing\AppInfo\Application; use OCA\Files_Sharing\SharedStorage; +use OCA\ShareByMail\ShareByMailProvider; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\Folder; @@ -21,6 +23,7 @@ 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; @@ -34,6 +37,7 @@ use OCP\Mail\IMailer; use OCP\Security\Events\ValidatePasswordPolicyEvent; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; +use OCP\Security\PasswordContext; use OCP\Share; use OCP\Share\Events\BeforeShareDeletedEvent; use OCP\Share\Events\ShareAcceptedEvent; @@ -43,11 +47,13 @@ use OCP\Share\Events\ShareDeletedFromSelfEvent; use OCP\Share\Exceptions\AlreadySharedException; 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\IShareProvider; use OCP\Share\IShareProviderSupportsAccept; +use OCP\Share\IShareProviderSupportsAllSharesInFolder; use OCP\Share\IShareProviderWithNotification; use Psr\Log\LoggerInterface; @@ -78,6 +84,7 @@ class Manager implements IManager { private KnownUserService $knownUserService, private ShareDisableChecker $shareDisableChecker, private IDateTimeZone $dateTimeZone, + private IAppConfig $appConfig, ) { $this->l = $this->l10nFactory->get('lib'); // The constructor of LegacyHooks registers the listeners of share events @@ -113,7 +120,8 @@ class Manager implements IManager { // Let others verify the password try { - $this->dispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password)); + $event = new ValidatePasswordPolicyEvent($password, PasswordContext::SHARING); + $this->dispatcher->dispatchTyped($event); } catch (HintException $e) { /* Wrap in a 400 bad request error */ throw new HintException($e->getMessage(), $e->getHint(), 400, $e); @@ -176,8 +184,8 @@ class Manager implements IManager { } // Cannot share with yourself - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $share->getSharedBy()) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $share->getSharedBy()) { throw new \InvalidArgumentException($this->l->t('Cannot share with yourself')); } @@ -187,8 +195,8 @@ class Manager implements IManager { } // And it should be a file or a folder - if (!($share->getNode() instanceof \OCP\Files\File) && - !($share->getNode() instanceof \OCP\Files\Folder)) { + if (!($share->getNode() instanceof \OCP\Files\File) + && !($share->getNode() instanceof \OCP\Files\Folder)) { throw new \InvalidArgumentException($this->l->t('Shared path must be either a file or a folder')); } @@ -212,6 +220,17 @@ class Manager implements IManager { throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing')); } + // Permissions must be valid + if ($share->getPermissions() < 0 || $share->getPermissions() > \OCP\Constants::PERMISSION_ALL) { + throw new \InvalidArgumentException($this->l->t('Valid permissions are required for sharing')); + } + + // Single file shares should never have delete or create permissions + if (($share->getNode() instanceof File) + && (($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE)) !== 0)) { + throw new \InvalidArgumentException($this->l->t('File shares cannot have create or delete permissions')); + } + $permissions = 0; $nodesForUser = $userFolder->getById($share->getNodeId()); foreach ($nodesForUser as $node) { @@ -234,8 +253,8 @@ class Manager implements IManager { // Link shares are allowed to have no read permissions to allow upload to hidden folders $noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL; - if (!$noReadPermissionRequired && - ($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) { + if (!$noReadPermissionRequired + && ($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) { throw new \InvalidArgumentException($this->l->t('Shares need at least read permissions')); } @@ -544,8 +563,8 @@ class Manager implements IManager { } // Check if public upload is allowed - if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload() && - ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) { + if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload() + && ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) { throw new \InvalidArgumentException($this->l->t('Public upload is not allowed')); } } @@ -562,13 +581,10 @@ class Manager implements IManager { * @param IShare $share */ protected function setLinkParent(IShare $share) { - // No sense in checking if the method is not there. - if (method_exists($share, 'setParent')) { - $storage = $share->getNode()->getStorage(); - if ($storage->instanceOfStorage(SharedStorage::class)) { - /** @var \OCA\Files_Sharing\SharedStorage $storage */ - $share->setParent($storage->getShareId()); - } + $storage = $share->getNode()->getStorage(); + if ($storage->instanceOfStorage(SharedStorage::class)) { + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $share->setParent((int)$storage->getShareId()); } } @@ -581,7 +597,11 @@ class Manager implements IManager { $mounts = $this->mountManager->findIn($path->getPath()); foreach ($mounts as $mount) { if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) { - throw new \InvalidArgumentException($this->l->t('Path contains files shared with you')); + // Using a flat sharing model ensures the file owner can always see who has access. + // Allowing parent folder sharing would require tracking inherited access, which adds complexity + // and hurts performance/scalability. + // So we forbid sharing a parent folder of a share you received. + throw new \InvalidArgumentException($this->l->t('You cannot share a folder that contains other shares')); } } } @@ -659,41 +679,7 @@ class Manager implements IManager { $this->linkCreateChecks($share); $this->setLinkParent($share); - // Initial token length - $tokenLength = \OC\Share\Helper::getTokenLength(); - - do { - $tokenExists = false; - - for ($i = 0; $i <= 2; $i++) { - // Generate a new token - $token = $this->secureRandom->generate( - $tokenLength, - \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE - ); - - try { - // Try to fetch a share with the generated token - $this->getShareByToken($token); - $tokenExists = true; // Token exists, we need to try again - } catch (\OCP\Share\Exceptions\ShareNotFound $e) { - // Token is unique, exit the loop - $tokenExists = false; - break; - } - } - - // If we've reached the maximum attempts and the token still exists, increase the token length - if ($tokenExists) { - $tokenLength++; - - // Check if the token length exceeds the maximum allowed length - if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) { - throw new \Exception('Unable to generate a unique share token. Maximum token length exceeded.'); - } - } - } while ($tokenExists); - + $token = $this->generateToken(); // Set the unique token $share->setToken($token); @@ -711,18 +697,18 @@ class Manager implements IManager { } // Cannot share with the owner - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $share->getShareOwner()) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $share->getShareOwner()) { throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner')); } // Generate the target - $defaultShareFolder = $this->config->getSystemValue('share_folder', '/'); - $allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true); - if ($allowCustomShareFolder) { - $shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $defaultShareFolder); - } else { - $shareFolder = $defaultShareFolder; + $shareFolder = $this->config->getSystemValue('share_folder', '/'); + if ($share->getShareType() === IShare::TYPE_USER) { + $allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true); + if ($allowCustomShareFolder) { + $shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $shareFolder); + } } $target = $shareFolder . '/' . $share->getNode()->getName(); @@ -804,14 +790,14 @@ class Manager implements IManager { } // We can only change the recipient on user shares - if ($share->getSharedWith() !== $originalShare->getSharedWith() && - $share->getShareType() !== IShare::TYPE_USER) { + if ($share->getSharedWith() !== $originalShare->getSharedWith() + && $share->getShareType() !== IShare::TYPE_USER) { throw new \InvalidArgumentException($this->l->t('Can only update recipient on user shares')); } // Cannot share with the owner - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $share->getShareOwner()) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $share->getShareOwner()) { throw new \InvalidArgumentException($this->l->t('Cannot share with the share owner')); } @@ -882,6 +868,7 @@ class Manager implements IManager { // Now update the share! $provider = $this->factory->getProviderForType($share->getShareType()); if ($share->getShareType() === IShare::TYPE_EMAIL) { + /** @var ShareByMailProvider $provider */ $share = $provider->update($share, $plainTextPassword); } else { $share = $provider->update($share); @@ -962,11 +949,11 @@ class Manager implements IManager { * @return boolean whether the password was updated or not. */ private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) { - $passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) && - (($share->getPassword() !== null && $originalShare->getPassword() === null) || - ($share->getPassword() === null && $originalShare->getPassword() !== null) || - ($share->getPassword() !== null && $originalShare->getPassword() !== null && - !$this->hasher->verify($share->getPassword(), $originalShare->getPassword()))); + $passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) + && (($share->getPassword() !== null && $originalShare->getPassword() === null) + || ($share->getPassword() === null && $originalShare->getPassword() !== null) + || ($share->getPassword() !== null && $originalShare->getPassword() !== null + && !$this->hasher->verify($share->getPassword(), $originalShare->getPassword()))); // Password updated. if ($passwordsAreDifferent) { @@ -1019,7 +1006,6 @@ class Manager implements IManager { /** * Delete all the children of this share - * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in * * @param IShare $share * @return IShare[] List of deleted shares @@ -1083,7 +1069,12 @@ class Manager implements IManager { foreach ($userIds as $userId) { foreach ($shareTypes as $shareType) { - $provider = $this->factory->getProviderForType($shareType); + try { + $provider = $this->factory->getProviderForType($shareType); + } catch (ProviderException $e) { + continue; + } + if ($node instanceof Folder) { /* We need to get all shares by this user to get subshares */ $shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0); @@ -1222,11 +1213,13 @@ class Manager implements IManager { $shares = []; foreach ($providers as $provider) { if ($isOwnerless) { - foreach ($node->getDirectoryListing() as $childNode) { - $data = $provider->getSharesByPath($childNode); - $fid = $childNode->getId(); - $shares[$fid] ??= []; - $shares[$fid] = array_merge($shares[$fid], $data); + // If the provider does not implement the additional interface, + // we lack a performant way of querying all shares and therefore ignore the provider. + if ($provider instanceof IShareProviderSupportsAllSharesInFolder) { + foreach ($provider->getAllSharesInFolder($node) as $fid => $data) { + $shares[$fid] ??= []; + $shares[$fid] = array_merge($shares[$fid], $data); + } } } else { foreach ($provider->getSharesInFolder($userId, $node, $reshares) as $fid => $data) { @@ -1243,9 +1236,9 @@ class Manager implements IManager { * @inheritdoc */ public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0, bool $onlyValid = true) { - if ($path !== null && - !($path instanceof \OCP\Files\File) && - !($path instanceof \OCP\Files\Folder)) { + if ($path !== null + && !($path instanceof \OCP\Files\File) + && !($path instanceof \OCP\Files\Folder)) { throw new \InvalidArgumentException($this->l->t('Invalid path')); } @@ -1493,15 +1486,6 @@ class Manager implements IManager { $this->deleteShare($share); throw new ShareNotFound($this->l->t('The requested share does not exist anymore')); } - - try { - $share->getNode(); - // Ignore share, file is still accessible - } catch (NotFoundException) { - // Access lost, but maybe only temporarily, so don't delete the share right away - throw new ShareNotFound($this->l->t('The requested share does not exist anymore')); - } - if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') { $uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]); foreach ($uids as $uid) { @@ -1567,8 +1551,14 @@ class Manager implements IManager { * @inheritdoc */ public function groupDeleted($gid) { - $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP); - $provider->groupDeleted($gid); + foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) { + try { + $provider = $this->factory->getProviderForType($type); + } catch (ProviderException $e) { + continue; + } + $provider->groupDeleted($gid); + } $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); if ($excludedGroups === '') { @@ -1588,8 +1578,14 @@ class Manager implements IManager { * @inheritdoc */ public function userDeletedFromGroup($uid, $gid) { - $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP); - $provider->userDeletedFromGroup($uid, $gid); + foreach ([IShare::TYPE_GROUP, IShare::TYPE_REMOTE_GROUP] as $type) { + try { + $provider = $this->factory->getProviderForType($type); + } catch (ProviderException $e) { + continue; + } + $provider->userDeletedFromGroup($uid, $gid); + } } /** @@ -1801,8 +1797,8 @@ class Manager implements IManager { * @return bool */ public function shareApiLinkDefaultExpireDateEnforced() { - return $this->shareApiLinkDefaultExpireDate() && - $this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes'; + return $this->shareApiLinkDefaultExpireDate() + && $this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes'; } @@ -1839,8 +1835,8 @@ class Manager implements IManager { * @return bool */ public function shareApiInternalDefaultExpireDateEnforced(): bool { - return $this->shareApiInternalDefaultExpireDate() && - $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes'; + return $this->shareApiInternalDefaultExpireDate() + && $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes'; } /** @@ -1849,8 +1845,8 @@ class Manager implements IManager { * @return bool */ public function shareApiRemoteDefaultExpireDateEnforced(): bool { - return $this->shareApiRemoteDefaultExpireDate() && - $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes'; + return $this->shareApiRemoteDefaultExpireDate() + && $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes'; } /** @@ -1918,13 +1914,13 @@ class Manager implements IManager { } public function limitEnumerationToGroups(): bool { - return $this->allowEnumeration() && - $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + return $this->allowEnumeration() + && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; } public function limitEnumerationToPhone(): bool { - return $this->allowEnumeration() && - $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + return $this->allowEnumeration() + && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; } public function allowEnumerationFullMatch(): bool { @@ -1939,6 +1935,14 @@ class Manager implements IManager { return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; } + public function allowCustomTokens(): bool { + return $this->appConfig->getValueBool('core', ConfigLexicon::SHARE_CUSTOM_TOKEN); + } + + public function allowViewWithoutDownload(): bool { + return $this->appConfig->getValueBool('core', 'shareapi_allow_view_without_download', true); + } + public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool { if ($this->allowEnumerationFullMatch()) { return true; @@ -1976,14 +1980,9 @@ class Manager implements IManager { } /** - * Copied from \OC_Util::isSharingDisabledForUser - * - * TODO: Deprecate function from OC_Util - * - * @param string $userId - * @return bool + * Check if sharing is disabled for the current user */ - public function sharingDisabledForUser($userId) { + public function sharingDisabledForUser(?string $userId): bool { return $this->shareDisableChecker->sharingDisabledForUser($userId); } @@ -2025,4 +2024,43 @@ class Manager implements IManager { yield from $provider->getAllShares(); } } + + public function generateToken(): string { + // Initial token length + $tokenLength = \OC\Share\Helper::getTokenLength(); + + do { + $tokenExists = false; + + for ($i = 0; $i <= 2; $i++) { + // Generate a new token + $token = $this->secureRandom->generate( + $tokenLength, + ISecureRandom::CHAR_HUMAN_READABLE, + ); + + try { + // Try to fetch a share with the generated token + $this->getShareByToken($token); + $tokenExists = true; // Token exists, we need to try again + } catch (ShareNotFound $e) { + // Token is unique, exit the loop + $tokenExists = false; + break; + } + } + + // If we've reached the maximum attempts and the token still exists, increase the token length + if ($tokenExists) { + $tokenLength++; + + // Check if the token length exceeds the maximum allowed length + if ($tokenLength > \OC\Share\Constants::MAX_TOKEN_LENGTH) { + throw new ShareTokenException('Unable to generate a unique share token. Maximum token length exceeded.'); + } + } + } while ($tokenExists); + + return $token; + } } |