diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index b96a61288517f..d306177c85557 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -7,6 +7,7 @@ namespace OCA\ShareByMail; +use DateTime; use OC\Share20\DefaultShareProvider; use OC\Share20\Exception\InvalidShare; use OC\Share20\Share; @@ -98,14 +99,10 @@ public function create(IShare $share): IShare { // if the admin enforces a password for all mail shares we create a // random password and send it to the recipient - $password = $share->getPassword() ?: ''; - $passwordEnforced = $this->shareManager->shareApiLinkEnforcePassword(); - if ($passwordEnforced && empty($password)) { + $password = $share->getPassword(); + if ($password === null && $this->shareManager->shareApiLinkEnforcePassword()) { $password = $this->autoGeneratePassword($share); - } - - if (!empty($password)) { - $share->setPassword($this->hasher->hash($password)); + $share->setPasswordHash($this->hasher->hash($password)); } $shareId = $this->createMailShare($share); @@ -116,9 +113,7 @@ public function create(IShare $share): IShare { // Temporary set the clear password again to send it by mail // This need to be done after the share was created in the database // as the password is hashed in between. - if (!empty($password)) { - $data['password'] = $password; - } + $data['password'] = $password; return $this->createShareObject($data); } @@ -227,6 +222,11 @@ protected function createMailShare(IShare $share): string { if ($share->getToken() === '') { $share->setToken($this->generateToken()); } + + if ($share->getPassword() !== null && !$share->isPasswordHashed()) { + throw new RuntimeException('The password must be hashed already.'); + } + return $this->addShareToDB( $share->getNodeId(), $share->getNodeType(), @@ -241,6 +241,7 @@ protected function createMailShare(IShare $share): string { $share->getHideDownload(), $share->getLabel(), $share->getExpirationDate(), + $share->getShareTime(), $share->getNote(), $share->getAttributes(), $share->getMailSend(), @@ -699,6 +700,7 @@ protected function addShareToDB( ?bool $hideDownload, ?string $label, ?\DateTimeInterface $expirationTime, + ?DateTime $shareTime, ?string $note = '', ?IAttributes $attributes = null, ?bool $mailSend = true, @@ -717,7 +719,7 @@ protected function addShareToDB( ->setValue('password', $qb->createNamedParameter($password)) ->setValue('password_expiration_time', $qb->createNamedParameter($passwordExpirationTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)) ->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL)) - ->setValue('stime', $qb->createNamedParameter(time())) + ->setValue('stime', $qb->createNamedParameter($shareTime?->getTimestamp() ?? time())) ->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT)) ->setValue('label', $qb->createNamedParameter($label)) ->setValue('note', $qb->createNamedParameter($note)) @@ -767,6 +769,11 @@ public function update(IShare $share, ?string $plainTextPassword = null): IShare $expiration = \DateTime::createFromInterface($expiration); $expiration->setTimezone(new \DateTimeZone(date_default_timezone_get())); } + + if ($share->getPassword() !== null && !$share->isPasswordHashed()) { + throw new RuntimeException('The password must be hashed already.'); + } + $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) ->set('item_source', $qb->createNamedParameter($share->getNodeId())) @@ -1052,7 +1059,9 @@ protected function createShareObject(array $data): IShare { $shareTime->setTimestamp((int)$data['stime']); $share->setShareTime($shareTime); $share->setSharedWith($data['share_with'] ?? ''); - $share->setPassword($data['password']); + if (($password = $data['password']) !== null) { + $share->setPasswordHash($password); + } $passwordExpirationTime = \DateTime::createFromFormat('Y-m-d H:i:s', $data['password_expiration_time'] ?? ''); $share->setPasswordExpirationTime($passwordExpirationTime !== false ? $passwordExpirationTime : null); $share->setLabel($data['label'] ?? ''); diff --git a/apps/sharing/appinfo/info.xml b/apps/sharing/appinfo/info.xml index cd653bf56fa09..b46cd273f56f8 100644 --- a/apps/sharing/appinfo/info.xml +++ b/apps/sharing/appinfo/info.xml @@ -7,8 +7,8 @@ xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> sharing Sharing - TODO - TODO + This app provides APIs and occ commands to manage shares. + This app provides APIs and occ commands to manage shares. 2.0.0-dev.3 AGPL-3.0-or-later Kate Döen diff --git a/apps/sharing/lib/Command/AddShareRecipient.php b/apps/sharing/lib/Command/AddShareRecipient.php index 5742b0816087b..e45a347943a1d 100644 --- a/apps/sharing/lib/Command/AddShareRecipient.php +++ b/apps/sharing/lib/Command/AddShareRecipient.php @@ -16,7 +16,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -// TODO: Initiator missing. final class AddShareRecipient extends SharingBase { #[\Override] public function configure(): void { diff --git a/apps/sharing/openapi.json b/apps/sharing/openapi.json index 1073b158e7ac6..0a70734e06617 100644 --- a/apps/sharing/openapi.json +++ b/apps/sharing/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "sharing", "version": "0.0.1", - "description": "TODO", + "description": "This app provides APIs and occ commands to manage shares.", "license": { "name": "AGPL-3.0-or-later" } diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index b6da12c8b93f5..ce4e99120819d 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -20,7 +20,6 @@ - @@ -201,32 +200,17 @@ - CLASS]]> - CLASS]]> CLASS]]> DTEND]]> DTEND]]> - DTEND]]> - DTSTART]]> DTSTART]]> - DTSTART]]> - DUE]]> DUE]]> DUE]]> DURATION]]> DURATION]]> - DURATION]]> - RDATE]]> - RDATE]]> RDATE]]> RDATE]]> - RDATE]]> - RDATE]]> - RRULE]]> RRULE]]> - RRULE]]> - UID]]> - UID]]> UID]]> @@ -2423,9 +2407,6 @@ - - - diff --git a/core/Sharing/Recipient/GroupShareRecipientType.php b/core/Sharing/Recipient/GroupShareRecipientType.php index d2539c72399b2..01490505dcce4 100644 --- a/core/Sharing/Recipient/GroupShareRecipientType.php +++ b/core/Sharing/Recipient/GroupShareRecipientType.php @@ -20,7 +20,7 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventListener; -use OCP\Group\Events\GroupDeletedEvent; +use OCP\Group\Events\BeforeGroupDeletedEvent; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\Interaction\InteractionReceiver; @@ -30,7 +30,7 @@ use OCP\Share\IShare; /** - * @template-implements IEventListener + * @template-implements IEventListener */ final class GroupShareRecipientType extends AShareRecipientTypeSearchCollaborator implements IEventListener { public function __construct( @@ -39,7 +39,7 @@ public function __construct( private readonly IGroupManager $groupManager, private readonly ISharingManager $manager, ) { - $eventDispatcher->addServiceListener(GroupDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(BeforeGroupDeletedEvent::class, self::class); } #[\Override] diff --git a/core/Sharing/Recipient/UserShareRecipientType.php b/core/Sharing/Recipient/UserShareRecipientType.php index 3c69a7ab804b1..61927b0b1f8c0 100644 --- a/core/Sharing/Recipient/UserShareRecipientType.php +++ b/core/Sharing/Recipient/UserShareRecipientType.php @@ -26,10 +26,10 @@ use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Share\IShare; -use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\BeforeUserDeletedEvent; /** - * @template-implements IEventListener + * @template-implements IEventListener */ final class UserShareRecipientType extends AShareRecipientTypeSearchCollaborator implements IEventListener { @@ -39,7 +39,7 @@ public function __construct( private readonly IUserManager $userManager, private readonly ISharingManager $manager, ) { - $eventDispatcher->addServiceListener(UserDeletedEvent::class, self::class); + $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, self::class); } #[\Override] diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 255465c0ffb2e..01c8fc36d96b3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2366,7 +2366,6 @@ 'OC\\Share20\\UserRemovedListener' => $baseDir . '/lib/private/Share20/UserRemovedListener.php', 'OC\\Share\\Constants' => $baseDir . '/lib/private/Share/Constants.php', 'OC\\Sharing\\ClassMapper' => $baseDir . '/lib/private/Sharing/ClassMapper.php', - 'OC\\Sharing\\ISharingLegacyBackend' => $baseDir . '/lib/private/Sharing/ISharingLegacyBackend.php', 'OC\\Sharing\\SharingBackend' => $baseDir . '/lib/private/Sharing/SharingBackend.php', 'OC\\Sharing\\SharingManager' => $baseDir . '/lib/private/Sharing/SharingManager.php', 'OC\\Sharing\\SharingRegistry' => $baseDir . '/lib/private/Sharing/SharingRegistry.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 046d2cd20b693..0687a45a56211 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2407,7 +2407,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Share20\\UserRemovedListener' => __DIR__ . '/../../..' . '/lib/private/Share20/UserRemovedListener.php', 'OC\\Share\\Constants' => __DIR__ . '/../../..' . '/lib/private/Share/Constants.php', 'OC\\Sharing\\ClassMapper' => __DIR__ . '/../../..' . '/lib/private/Sharing/ClassMapper.php', - 'OC\\Sharing\\ISharingLegacyBackend' => __DIR__ . '/../../..' . '/lib/private/Sharing/ISharingLegacyBackend.php', 'OC\\Sharing\\SharingBackend' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingBackend.php', 'OC\\Sharing\\SharingManager' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingManager.php', 'OC\\Sharing\\SharingRegistry' => __DIR__ . '/../../..' . '/lib/private/Sharing/SharingRegistry.php', diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 7499db4046f07..a27c4ef89c536 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -98,11 +98,6 @@ public function setDisplayName(string $displayName): bool { return false; } - /** - * get all users in the group - * - * @return array - */ #[\Override] public function getUsers(): array { if ($this->usersLoaded) { diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index c7a47188f587b..0fff3fd0644bc 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -46,6 +46,7 @@ use OCP\Share\IShareProviderWithNotification; use OCP\Util; use Psr\Log\LoggerInterface; +use RuntimeException; use function str_starts_with; use function strlen; @@ -110,7 +111,10 @@ public function create(IShare $share) { if ($share->getShareType() === IShare::TYPE_USER) { //Set the UID of the user we share with $qb->setValue('share_with', $qb->createNamedParameter($share->getSharedWith())); - $qb->setValue('accepted', $qb->createNamedParameter(IShare::STATUS_PENDING)); + if ($share->getStatus() === null) { + $share->setStatus(IShare::STATUS_PENDING); + } + $qb->setValue('accepted', $qb->createNamedParameter($share->getStatus())); //If an expiration date is set store it if ($expirationDate !== null) { @@ -133,8 +137,11 @@ public function create(IShare $share) { $qb->setValue('token', $qb->createNamedParameter($share->getToken())); //If a password is set store it - if ($share->getPassword() !== null) { - $qb->setValue('password', $qb->createNamedParameter($share->getPassword())); + if (($password = $share->getPassword()) !== null) { + if (!$share->isPasswordHashed()) { + throw new RuntimeException('The password must be hashed already.'); + } + $qb->setValue('password', $qb->createNamedParameter($password)); } $qb->setValue('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)); @@ -185,9 +192,9 @@ public function create(IShare $share) { $qb->setValue('note', $qb->createNamedParameter($share->getNote())); } - // Set the time this share was created - $shareTime = $this->timeFactory->now(); + $shareTime = $share->getShareTime() ?? \DateTime::createFromImmutable($this->timeFactory->now()); $qb->setValue('stime', $qb->createNamedParameter($shareTime->getTimestamp())); + $share->setShareTime($shareTime); // insert the data and fetch the id of the share $qb->executeStatement(); @@ -197,8 +204,6 @@ public function create(IShare $share) { $share->setId((string)$id); $share->setProviderId($this->identifier()); - $share->setShareTime(\DateTime::createFromImmutable($shareTime)); - $mailSendValue = $share->getMailSend(); $share->setMailSend(($mailSendValue === null) ? true : $mailSendValue); @@ -287,10 +292,15 @@ public function update(IShare $share) { ->set('attributes', $qb->createNamedParameter($shareAttributes)) ->executeStatement(); } elseif ($share->getShareType() === IShare::TYPE_LINK) { + $password = $share->getPassword(); + if ($password !== null && !$share->isPasswordHashed()) { + throw new RuntimeException('The password must be hashed already.'); + } + $qb = $this->dbConn->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) - ->set('password', $qb->createNamedParameter($share->getPassword())) + ->set('password', $qb->createNamedParameter($password)) ->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) @@ -1129,7 +1139,9 @@ private function createShare($data): IShare { $share->setSharedWith($data['share_with']); $share->setSharedWithDisplayNameCallback(fn (IShare $share) => $this->groupManager->getDisplayName($share->getSharedWith())); } elseif ($share->getShareType() === IShare::TYPE_LINK) { - $share->setPassword($data['password']); + if (($password = $data['password']) !== null) { + $share->setPasswordHash($password); + } $share->setSendPasswordByTalk((bool)$data['password_by_talk']); $share->setToken($data['token']); } diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 7db6d8e4045da..5a0840693d736 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -567,9 +567,8 @@ public function createShare(IShare $share): IShare { $this->verifyPassword($share->getPassword()); // If a password is set. Hash it! - if ($share->getShareType() === IShare::TYPE_LINK - && $share->getPassword() !== null) { - $share->setPassword($this->hasher->hash($share->getPassword())); + if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) && $share->getPassword() !== null && !$share->isPasswordHashed()) { + $share->setPasswordHash($this->hasher->hash($share->getPassword())); } } @@ -588,9 +587,11 @@ public function createShare(IShare $share): IShare { } } - $target = $shareFolder . '/' . $share->getNode()->getName(); - $target = Filesystem::normalizePath($target); - $share->setTarget($target); + if ($share->getTarget() === null) { + $target = $shareFolder . '/' . $share->getNode()->getName(); + $target = Filesystem::normalizePath($target); + $share->setTarget($target); + } // Pre share event $event = new BeforeShareCreatedEvent($share); @@ -832,7 +833,7 @@ private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShar // If a password is set. Hash it! if (!empty($share->getPassword())) { - $share->setPassword($this->hasher->hash($share->getPassword())); + $share->setPasswordHash($this->hasher->hash($share->getPassword())); if ($share->getShareType() === IShare::TYPE_EMAIL) { // Shares shared by email have temporary passwords $this->setSharePasswordExpirationTime($share); @@ -850,7 +851,12 @@ private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShar } else { // Reset the password to the original one, as it is either the same // as the "new" password or a hashed version of it. - $share->setPassword($originalShare->getPassword()); + $password = $originalShare->getPassword(); + if ($password !== null && $originalShare->isPasswordHashed()) { + $share->setPasswordHash($password); + } else { + $share->setPassword($password); + } } return false; diff --git a/lib/private/Share20/Share.php b/lib/private/Share20/Share.php index 3ea440a885a68..35c70a1464bfc 100644 --- a/lib/private/Share20/Share.php +++ b/lib/private/Share20/Share.php @@ -16,6 +16,7 @@ use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\IUserManager; +use OCP\Security\IHasher; use OCP\Server; use OCP\Share\Exceptions\IllegalIDChangeException; use OCP\Share\IAttributes; @@ -57,6 +58,7 @@ class Share implements IShare { private $expireDate; /** @var string */ private $password; + private bool $isPasswordHashed = false; private ?\DateTimeInterface $passwordExpirationTime = null; /** @var bool */ private $sendPasswordByTalk = false; @@ -501,6 +503,18 @@ public function getShareOwner() { #[\Override] public function setPassword($password) { $this->password = $password; + $this->isPasswordHashed = false; + return $this; + } + + #[\Override] + public function setPasswordHash(string $passwordHash): IShare { + if (!Server::get(IHasher::class)->validate($passwordHash)) { + throw new \InvalidArgumentException(); + } + + $this->password = $passwordHash; + $this->isPasswordHashed = true; return $this; } @@ -512,6 +526,11 @@ public function getPassword() { return $this->password; } + #[\Override] + public function isPasswordHashed(): bool { + return $this->isPasswordHashed; + } + /** * @inheritdoc */ diff --git a/lib/private/Sharing/ISharingLegacyBackend.php b/lib/private/Sharing/ISharingLegacyBackend.php deleted file mode 100644 index 5aa9f45271e75..0000000000000 --- a/lib/private/Sharing/ISharingLegacyBackend.php +++ /dev/null @@ -1,59 +0,0 @@ -> - */ - public function getCompatibleSourceTypes(): array; - - /** - * @return list> - */ - public function getCompatibleRecipientTypes(): array; - - /** - * Update a share. - */ - public function updateShare(Share $share): void; - - /** - * Delete a share. - * - * @throws ShareNotFoundException - */ - public function deleteShare(string $id): void; - - /** - * Get a share. - * - * @throws ShareNotFoundException - */ - public function getShare(ShareAccessContext $accessContext, string $id): Share; - - /** - * Get multiple shares. - * - * @param ?class-string $filterSourceTypeClass - * @param ?positive-int $limit - * @return list - */ - public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?string $lastShareID, ?int $limit): array; -} diff --git a/lib/private/Sharing/SharingManager.php b/lib/private/Sharing/SharingManager.php index 328b919ec110f..5c44fe95f8204 100644 --- a/lib/private/Sharing/SharingManager.php +++ b/lib/private/Sharing/SharingManager.php @@ -50,8 +50,6 @@ use Random\Randomizer; use RuntimeException; -// TODO: Add accept/reject -// TODO: Add permission masking (reshares) // TODO: Test sharing to federated users, groups and circles // TODO: Implement share transfers // TODO: Cache share owner @@ -188,14 +186,7 @@ public function onOwnerDeleted(ShareAccessContext $accessContext, ShareUser $own // No need to update the last updated timestamp, because the share will be deleted anyway. - $ids = $this->backend->onOwnerDeleted($owner); - - $legacyBackend = $this->registry->getLegacyBackend(); - if ($legacyBackend instanceof ISharingLegacyBackend) { - foreach ($ids as $id) { - $legacyBackend->deleteShare($id); - } - } + $this->backend->onOwnerDeleted($owner); } #[\Override] @@ -759,11 +750,6 @@ public function deleteShare(ShareAccessContext $accessContext, Share $share): vo $this->validateShareEditPermissions($accessContext, $share); $this->backend->deleteShare($share->id); - - $legacyBackend = $this->registry->getLegacyBackend(); - if ($legacyBackend instanceof ISharingLegacyBackend) { - $legacyBackend->deleteShare($share->id); - } } #[\Override] @@ -988,27 +974,6 @@ private function processShareUpdates(array $shares): array { ); } } - - $legacyBackend = $this->registry->getLegacyBackend(); - if ($legacyBackend instanceof ISharingLegacyBackend) { - $compatibleSourceTypes = array_fill_keys($legacyBackend->getCompatibleSourceTypes(), true); - foreach ($share->sources as $source) { - if (!isset($compatibleSourceTypes[$source->class])) { - throw new RuntimeException('The legacy backend ' . $legacyBackend::class . ' does not support this source type: ' . $source->class); - } - } - - $compatibleRecipientTypes = array_fill_keys($legacyBackend->getCompatibleRecipientTypes(), true); - foreach ($share->recipients as $recipient) { - if (!isset($compatibleRecipientTypes[$recipient->class])) { - throw new RuntimeException( - 'The legacy backend ' . $legacyBackend::class . ' does not support this recipient type: ' . $recipient->class - ); - } - } - - $legacyBackend->updateShare($share); - } } return $shares; diff --git a/lib/private/Sharing/SharingRegistry.php b/lib/private/Sharing/SharingRegistry.php index d22e447f2c69a..d2c15b5ce9150 100644 --- a/lib/private/Sharing/SharingRegistry.php +++ b/lib/private/Sharing/SharingRegistry.php @@ -17,10 +17,7 @@ use NCU\Sharing\Source\IShareSourceType; use RuntimeException; -// TODO: Maybe add validate method to run all checks before using the manager final class SharingRegistry implements ISharingRegistry { - private ?ISharingLegacyBackend $legacyBackend = null; - /** @var array, IShareSourceType> */ private array $sourceTypes = []; @@ -59,7 +56,6 @@ final class SharingRegistry implements ISharingRegistry { #[\Override] public function clear(): void { - $this->legacyBackend = null; $this->sourceTypes = []; $this->recipientTypes = []; $this->propertyTypes = []; @@ -74,20 +70,6 @@ public function clear(): void { $this->permissionPresetCompatiblePermissionTypes = []; } - #[\Override] - public function registerLegacyBackend(ISharingLegacyBackend $legacyBackend): void { - if ($this->legacyBackend instanceof ISharingLegacyBackend) { - throw new RuntimeException('A sharing legacy backend is already registered'); - } - - $this->legacyBackend = $legacyBackend; - } - - #[\Override] - public function getLegacyBackend(): ?ISharingLegacyBackend { - return $this->legacyBackend; - } - #[\Override] public function registerSourceType(IShareSourceType $sourceType): void { $class = $sourceType::class; diff --git a/lib/private/Snowflake/SnowflakeGenerator.php b/lib/private/Snowflake/SnowflakeGenerator.php index 6e07bd72d6f61..a73253d246c7f 100644 --- a/lib/private/Snowflake/SnowflakeGenerator.php +++ b/lib/private/Snowflake/SnowflakeGenerator.php @@ -9,6 +9,7 @@ namespace OC\Snowflake; +use DateTimeImmutable; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IServerInfo; use OCP\Snowflake\ISnowflakeGenerator; @@ -31,9 +32,12 @@ public function __construct( } #[Override] - public function nextId(): string { + public function nextId(?DateTimeImmutable $timestamp = null): string { + $timestamp ??= $this->timeFactory->now(); + // Relative time - [$seconds, $milliseconds] = $this->getCurrentTime(); + $seconds = $timestamp->getTimestamp() - self::TS_OFFSET; + $milliseconds = (int)$timestamp->format('v'); $serverId = $this->serverInfo->getServerId(); $isCli = (int)$this->isCli(); // 1 bit @@ -124,14 +128,6 @@ private function convertToDecimal(array $bytes): string { return $digits; } - private function getCurrentTime(): array { - $time = $this->timeFactory->now(); - return [ - $time->getTimestamp() - self::TS_OFFSET, - (int)$time->format('v'), - ]; - } - private function isCli(): bool { return PHP_SAPI === 'cli'; } diff --git a/lib/public/IGroup.php b/lib/public/IGroup.php index 448b9f4c8f8ce..33da553d1ee38 100644 --- a/lib/public/IGroup.php +++ b/lib/public/IGroup.php @@ -41,7 +41,7 @@ public function setDisplayName(string $displayName): bool; /** * get all users in the group * - * @return IUser[] + * @return array * @since 8.0.0 */ public function getUsers(): array; diff --git a/lib/public/Share/IShare.php b/lib/public/Share/IShare.php index 5e5647f52aa10..6c92f30ef0ccd 100644 --- a/lib/public/Share/IShare.php +++ b/lib/public/Share/IShare.php @@ -453,6 +453,14 @@ public function getShareOwner(); */ public function setPassword($password); + /** + * Sets the password for the shared, but in it's already hashed form. + * Use {@see isPasswordHashed} to check if the return value of {@see getPassword} is already hashed. + * + * @since 35.0.0 + */ + public function setPasswordHash(string $passwordHash): IShare; + /** * Get the password of this share. * If this share is obtained via a shareprovider the password is @@ -463,6 +471,13 @@ public function setPassword($password); */ public function getPassword(); + /** + * Returns whether the return value of {@see getPassword} is already hashed. + * + * @since 35.0.0 + */ + public function isPasswordHashed(): bool; + /** * Returns whether the share is password protected by any means (e.g. password or OTP) * @return bool diff --git a/lib/public/Snowflake/ISnowflakeGenerator.php b/lib/public/Snowflake/ISnowflakeGenerator.php index ee9a02bc486fa..475d376788313 100644 --- a/lib/public/Snowflake/ISnowflakeGenerator.php +++ b/lib/public/Snowflake/ISnowflakeGenerator.php @@ -9,6 +9,7 @@ namespace OCP\Snowflake; +use DateTimeImmutable; use OCP\AppFramework\Attribute\Consumable; /** @@ -39,11 +40,12 @@ interface ISnowflakeGenerator { * * Each call to this method is guaranteed to return a different ID. * + * @param ?DateTimeImmutable $timestamp Generate the Snowflake ID for a specific time. This should only be used in very special cases. * @return non-empty-string * * @since 33.0 */ - public function nextId(): string; + public function nextId(?DateTimeImmutable $timestamp = null): string; /** * Return the smallest possible Snowflake ID for a given timestamp diff --git a/lib/unstable/Sharing/ISharingRegistry.php b/lib/unstable/Sharing/ISharingRegistry.php index 7e56c194f00c1..77bb566bdc59f 100644 --- a/lib/unstable/Sharing/ISharingRegistry.php +++ b/lib/unstable/Sharing/ISharingRegistry.php @@ -14,7 +14,6 @@ use NCU\Sharing\Property\ISharePropertyType; use NCU\Sharing\Recipient\IShareRecipientType; use NCU\Sharing\Source\IShareSourceType; -use OC\Sharing\ISharingLegacyBackend; use OCP\AppFramework\Attribute\Consumable; /** @@ -27,16 +26,6 @@ interface ISharingRegistry { */ public function clear(): void; - /** - * @experimental 35.0.0 - */ - public function registerLegacyBackend(ISharingLegacyBackend $legacyBackend): void; - - /** - * @experimental 35.0.0 - */ - public function getLegacyBackend(): ?ISharingLegacyBackend; - /** * @experimental 35.0.0 */ diff --git a/tests/lib/Sharing/AbstractSharingManagerTests.php b/tests/lib/Sharing/AbstractSharingManagerTests.php index 3b96003476921..349de677d0bfd 100644 --- a/tests/lib/Sharing/AbstractSharingManagerTests.php +++ b/tests/lib/Sharing/AbstractSharingManagerTests.php @@ -244,9 +244,10 @@ public function setUp(): void { #[\Override] protected function tearDown(): void { + $openTransaction = false; if ($this->dbConnection->inTransaction()) { $this->dbConnection->rollBack(); - $this->fail('Open transaction was not committed.'); + $openTransaction = true; } $accessContext = new ShareAccessContext(overrideChecks: true); @@ -257,11 +258,13 @@ protected function tearDown(): void { $this->manager->deleteShare($accessContext, $share); } + $this->dbConnection->commit(); + $this->owner->delete(); $this->user1->delete(); $this->user2->delete(); - $this->dbConnection->commit(); + $this->registry->clear(); foreach ([ 'sharing_share', @@ -279,9 +282,11 @@ protected function tearDown(): void { $this->assertEquals(0, $qb->executeQuery()->fetchOne(), $table); } - $this->registry->clear(); - parent::tearDown(); + + if ($openTransaction) { + $this->fail('Open transaction was not committed.'); + } } private function reloadShare(ShareAccessContext $accessContext, Share $share): Share { diff --git a/tests/lib/Snowflake/GeneratorTest.php b/tests/lib/Snowflake/GeneratorTest.php index 3323773105a0d..a3d60f7b93efe 100644 --- a/tests/lib/Snowflake/GeneratorTest.php +++ b/tests/lib/Snowflake/GeneratorTest.php @@ -99,6 +99,18 @@ public function testGeneratorWithFixedTime(string $date, int $expectedSeconds, i $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); } + #[DataProvider('provideSnowflakeData')] + public function testGeneratorWithTimestampParameter(string $date, int $expectedSeconds, int $expectedMilliseconds): void { + $dt = new \DateTimeImmutable($date); + + $generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo); + $data = $this->decoder->decode($generator->nextId($dt)); + + $this->assertEquals($expectedSeconds, $data->getCreatedAt()->format('U') - ISnowflakeGenerator::TS_OFFSET); + $this->assertEquals($expectedMilliseconds, (int)$data->getCreatedAt()->format('v')); + $this->assertEquals($this->serverInfo->getServerId(), $data->getServerId()); + } + public static function provideSnowflakeData(): array { $tests = [ ['2025-10-01 00:00:00.000000', 0, 0],