diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index cbec91c3d..4fcc5c4af 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -1,10 +1,11 @@ isAppWhitelisted($app)) { return true; - } else { - if ($url === '/apps/files_external/api/v1/mounts') { - // fake successful response - echo '[]'; - exit; - } - $this->logger->notice("Blocking access to non-whitelisted app ($app) for guest", ['app' => 'guests']); - return false; } + + if ($url === '/apps/files_external/api/v1/mounts') { + // fake successful response + echo '[]'; + exit; + } + + $this->logger->notice('Blocking access to non-whitelisted app (' . $app . ') for guest', ['app' => 'guests']); + return false; } return true; @@ -92,40 +94,69 @@ private function getRequestedApp(string|false $url): string { if (substr($url, 0, $this->baseUrlLength) === $this->baseUrl) { $url = substr($url, $this->baseUrlLength); } + if (str_starts_with($url, '/index.php/')) { $url = substr($url, 10); } + if (str_starts_with($url, '/apps/')) { // empty string / 'apps' / $app / rest of the route [, , $app,] = explode('/', $url, 4); return \OC_App::cleanAppId($app); - } elseif ($url === '/cron.php') { + } + + if ($url === '/cron.php') { return 'core'; - } elseif (str_starts_with($url, '/core/')) { + } + + if (str_starts_with($url, '/core/')) { return 'core'; - } elseif (str_starts_with($url, '/js/')) { + } + + if (str_starts_with($url, '/js/')) { return 'core'; - } elseif (str_starts_with($url, '/css/')) { + } + + if (str_starts_with($url, '/css/')) { return 'core'; - } elseif (str_starts_with($url, '/login')) { + } + + if (str_starts_with($url, '/login')) { return 'core'; - } elseif (str_starts_with($url, '/logout')) { + } + + if (str_starts_with($url, '/logout')) { return 'core'; - } elseif (str_starts_with($url, '/f/')) { + } + + if (str_starts_with($url, '/f/')) { return 'files'; - } elseif (str_starts_with($url, '/webdav/')) { + } + + if (str_starts_with($url, '/webdav/')) { return 'dav'; - } elseif (str_starts_with($url, '/dav/')) { + } + + if (str_starts_with($url, '/dav/')) { return 'dav'; - } elseif (str_starts_with($url, '/call/')) { + } + + if (str_starts_with($url, '/call/')) { return 'spreed'; - } elseif (str_starts_with($url, '/settings/')) { + } + + if (str_starts_with($url, '/settings/')) { return 'settings'; - } elseif (str_starts_with($url, '/avatar/')) { + } + + if (str_starts_with($url, '/avatar/')) { return 'avatar'; - } elseif (str_starts_with($url, '/heartbeat')) { + } + + if (str_starts_with($url, '/heartbeat')) { return 'heartbeat'; } + return 'core'; } diff --git a/lib/BackgroundJob/TransferJob.php b/lib/BackgroundJob/TransferJob.php index 8f6706cba..e06ba0eac 100644 --- a/lib/BackgroundJob/TransferJob.php +++ b/lib/BackgroundJob/TransferJob.php @@ -74,6 +74,7 @@ private function fail(Transfer $transfer, ?IUser $targetUser = null): void { if (!$targetUser instanceof IUser) { return; } + $result = $targetUser->delete(); // Rollback created user if (!$result) { $this->logger->error('Failed to delete target user', ['user' => $targetUser->getUID()]); @@ -86,6 +87,7 @@ public function run($argument): void { $transfer = $this->transferMapper->getById($id); $transfer->setStatus(Transfer::STATUS_STARTED); + $this->transferMapper->update($transfer); $source = $transfer->getSource(); @@ -99,7 +101,7 @@ public function run($argument): void { } if ($this->userManager->userExists($target)) { - $this->logger->error("Cannot transfer guest user \"$source\", target user \"$target\" already exists"); + $this->logger->error('Cannot transfer guest user "' . $source . '", target user "' . $target . '" already exists'); $this->fail($transfer); return; } @@ -119,8 +121,8 @@ public function run($argument): void { try { $this->transferService->transfer($sourceUser, $targetUser); - } catch (\Throwable $th) { - $this->logger->error($th->getMessage(), ['exception' => $th]); + } catch (\Throwable $throwable) { + $this->logger->error($throwable->getMessage(), ['exception' => $throwable]); $this->fail($transfer, $targetUser); return; } diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 436167676..b92b11309 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -20,6 +20,7 @@ class Capabilities implements ICapability { /** * @inheritDoc + * @return array> */ public function getCapabilities(): array { return [ diff --git a/lib/Command/AddCommand.php b/lib/Command/AddCommand.php index 8882b04a7..703ca1165 100644 --- a/lib/Command/AddCommand.php +++ b/lib/Command/AddCommand.php @@ -134,7 +134,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input->getOption('language') ?? '', $password ); - if ($user instanceof IUser) { $output->writeln('The guest account user "' . $user->getUID() . '" was created successfully'); return self::SUCCESS; diff --git a/lib/Command/ListCommand.php b/lib/Command/ListCommand.php index 0fc9371eb..e7c516005 100644 --- a/lib/Command/ListCommand.php +++ b/lib/Command/ListCommand.php @@ -35,12 +35,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $guests = $this->guestManager->getGuestsInfo(); $outputType = $input->getOption('output'); - if (count($guests) === 0) { + if ($guests === []) { if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { $output->writeln('[]'); } else { $output->writeln('No guests created'); } + return self::SUCCESS; } @@ -52,6 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $table->setRows($guests); $table->render(); } + return self::SUCCESS; } } diff --git a/lib/Config.php b/lib/Config.php index 278afbdbc..83cd8b076 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -65,6 +65,7 @@ public function setAppWhitelist(array|string $whitelist): void { if (is_array($whitelist)) { $whitelist = implode(',', $whitelist); } + $this->appConfig->setAppValueString('whitelist', $whitelist); } @@ -87,10 +88,10 @@ public function canCreateGuests(): bool { // Check if we have a group restriction // and if the user belong to that group $groupRestriction = $this->getCreateRestrictedToGroup(); - if (!empty($groupRestriction)) { + if ($groupRestriction !== []) { $userGroups = $this->groupManager->getUserGroupIds($user); $groupRestriction = array_intersect($userGroups, $groupRestriction); - if (empty($groupRestriction)) { + if ($groupRestriction === []) { return false; } } diff --git a/lib/Controller/APIController.php b/lib/Controller/APIController.php index 84ab60d0b..cd6371345 100644 --- a/lib/Controller/APIController.php +++ b/lib/Controller/APIController.php @@ -65,7 +65,7 @@ public function languages(): DataResponse { // Put appropriate languages into appropriate arrays, to print them sorted // common languages -> divider -> other languages if (in_array($lang, Factory::COMMON_LANGUAGE_CODES)) { - $commonLanguages[array_search($lang, Factory::COMMON_LANGUAGE_CODES)] = $ln; + $commonLanguages[array_search($lang, Factory::COMMON_LANGUAGE_CODES, true)] = $ln; } else { $languages[] = $ln; } @@ -79,10 +79,12 @@ public function languages(): DataResponse { // If a doesn't have a name, but b does, list b before a return 1; } + if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) { // If a does have a name, but b doesn't, list a before b return -1; } + // Otherwise compare the names return strcmp((string)$a['name'], (string)$b['name']); }); @@ -102,6 +104,7 @@ public function groups(): DataResponse { } else { $groups = $this->subAdmin->getSubAdminsGroups($user); } + $groups = array_values(array_map(fn (IGroup $group): array => [ 'gid' => $group->getGID(), 'name' => $group->getDisplayName(), diff --git a/lib/Controller/UsersController.php b/lib/Controller/UsersController.php index f91ceeb00..96c1cf7e7 100644 --- a/lib/Controller/UsersController.php +++ b/lib/Controller/UsersController.php @@ -87,23 +87,25 @@ public function create(string $email, string $displayName, string $language, arr if (!$group) { return new DataResponse( [ - 'errorMessages' => ["Group $groupId not found"], + 'errorMessages' => ['Group ' . $groupId . ' not found'], ], Http::STATUS_BAD_REQUEST ); } - if (!($this->subAdmin->isSubAdminOfGroup($currentUser, $group) || $this->groupManager->isAdmin($currentUser->getUID()))) { + + if (!$this->subAdmin->isSubAdminOfGroup($currentUser, $group) && !$this->groupManager->isAdmin($currentUser->getUID())) { return new DataResponse( [ - 'errorMessages' => ["You are not allowed to add users to group $groupId"], + 'errorMessages' => ['You are not allowed to add users to group ' . $groupId], ], Http::STATUS_FORBIDDEN ); } + $groupObjects[] = $group; } - if (empty($email) || !$this->mailer->validateMailAddress($email)) { + if ($email === '' || !$this->mailer->validateMailAddress($email)) { $errorMessages['email'] = $this->l10n->t( 'Invalid mail address' ); @@ -122,7 +124,7 @@ public function create(string $email, string $displayName, string $language, arr ); } - if (!empty($errorMessages)) { + if ($errorMessages !== []) { return new DataResponse( [ 'errorMessages' => $errorMessages, @@ -132,18 +134,26 @@ public function create(string $email, string $displayName, string $language, arr } try { - $this->guestManager->createGuest($this->userSession->getUser(), $username, $email, $displayName, $language); - $guestUser = $this->userManager->get($username); + $guestUser = $this->guestManager->createGuest($this->userSession->getUser(), $username, $email, $displayName, $language); + if (!$guestUser instanceof IUser) { + return new DataResponse( + [ + 'errorMessages' => ['email' => 'Unable to create guest user ' . $username], + ], + Http::STATUS_UNPROCESSABLE_ENTITY + ); + } if ($this->userManager instanceof PublicEmitter) { $this->userManager->emit('\OC\User', 'assignedUserId', [$username]); } + foreach ($groupObjects as $group) { $group->addUser($guestUser); } - } catch (\Exception $e) { + } catch (\Exception $exception) { return new DataResponse( [ - 'errorMessages' => ['email' => $e->getMessage()], + 'errorMessages' => ['email' => $exception->getMessage()], ], Http::STATUS_UNPROCESSABLE_ENTITY ); diff --git a/lib/Db/Transfer.php b/lib/Db/Transfer.php index a034549ac..23523b813 100644 --- a/lib/Db/Transfer.php +++ b/lib/Db/Transfer.php @@ -26,6 +26,7 @@ */ class Transfer extends Entity { public const STATUS_WAITING = 'waiting'; + public const STATUS_STARTED = 'started'; /** @var string */ diff --git a/lib/FilteredNavigationManager.php b/lib/FilteredNavigationManager.php index d04f1c72e..70e7f7e98 100644 --- a/lib/FilteredNavigationManager.php +++ b/lib/FilteredNavigationManager.php @@ -28,6 +28,9 @@ public function getAll(string $type = 'link'): array { return array_filter($items, $this->isEntryWhitelisted(...)); } + /** + * @param array $item + */ private function isEntryWhitelisted(array $item): bool { return $this->whitelist->isUrlAllowed($this->user, $item['href']); } diff --git a/lib/GroupBackend.php b/lib/GroupBackend.php index eac2281e1..03730fae3 100644 --- a/lib/GroupBackend.php +++ b/lib/GroupBackend.php @@ -34,7 +34,7 @@ public function __construct( } private function getMembers(): array { - if (empty($this->guestMembers)) { + if ($this->guestMembers === []) { $this->guestMembers = $this->guestManager->listGuests(); } @@ -111,6 +111,7 @@ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): arra if ($this->guestManager->isGuest() && $this->config->hideOtherUsers()) { return [$this->userSession->getUser()->getUID()]; } + return $offset === 0 ? $this->getMembers() : []; } @@ -122,8 +123,10 @@ public function countUsersInGroup(string $gid, string $search = ''): int { if ($this->guestManager->isGuest() && $this->config->hideOtherUsers()) { return 1; } + return count($this->getMembers()); } + return 0; } @@ -131,6 +134,7 @@ public function getGroupDetails(string $gid): array { if ($gid === $this->groupName) { return ['displayName' => 'Guests']; } + return []; } diff --git a/lib/GuestManager.php b/lib/GuestManager.php index b5ce2fc14..0cd2fcfc6 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -41,15 +41,16 @@ public function isGuest(IUser|string|null $user = null): bool { $user = $this->userSession->getUser(); return ($user !== null) && $this->userBackend->userExists($user->getUID()); } + if (is_string($user)) { return $this->userBackend->userExists($user); - } elseif ($user instanceof IUser) { - return $this->userBackend->userExists($user->getUID()); } - return false; + + + return $this->userBackend->userExists($user->getUID()); } - public function createGuest(?IUser $createdBy, string $userId, string $email, string $displayName = '', string $language = '', ?string $initialPassword = null) : IUser { + public function createGuest(?IUser $createdBy, string $userId, string $email, string $displayName = '', string $language = '', ?string $initialPassword = null) : ?IUser { if ($initialPassword === null) { $passwordEvent = new GenerateSecurePasswordEvent(); $this->eventDispatcher->dispatchTyped($passwordEvent); @@ -58,23 +59,26 @@ public function createGuest(?IUser $createdBy, string $userId, string $email, st $password = $initialPassword; } - /** @var IUser */ $user = $this->userManager->createUserFromBackend( $userId, $password, $this->userBackend ); + if (!$user instanceof IUser) { + return null; + } + $user->setSystemEMailAddress($email); - if ($createdBy) { + if ($createdBy instanceof IUser) { $this->config->setUserValue($userId, 'guests', 'created_by', $createdBy->getUID()); } - if ($displayName) { + if ($displayName !== '') { $user->setDisplayName($displayName); } - if ($language) { + if ($language !== '') { $this->config->setUserValue($userId, 'core', 'lang', $language); } @@ -111,6 +115,15 @@ public function listGuests(): array { return $this->userBackend->getUsers(); } + /** + * @return list + */ public function getGuestsInfo(): array { $displayNames = $this->userBackend->getDisplayNames(); $guests = array_keys($displayNames); @@ -134,6 +147,10 @@ public function getGuestsInfo(): array { }, $guests); } + /** + * @param list $guests + * @return array + */ private function getShareCountForUsers(array $guests): array { $query = $this->connection->getQueryBuilder(); $query->select('share_with', $query->func()->count('*', 'count')) @@ -143,13 +160,23 @@ private function getShareCountForUsers(array $guests): array { $result = $query->executeQuery(); $data = []; while ($row = $result->fetch()) { - $data[$row['share_with']] = $row['count']; + $data[$row['share_with']] = (int)$row['count']; } + $result->closeCursor(); return $data; } + /** + * @return array{shares: list} + */ public function getGuestInfo(string $userId): array { $shares = array_merge( $this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1, 0), diff --git a/lib/Hooks.php b/lib/Hooks.php index 69c3fd5b3..0005be3bf 100644 --- a/lib/Hooks.php +++ b/lib/Hooks.php @@ -17,7 +17,6 @@ use OCP\Constants; use OCP\Files\Storage\IStorage; use OCP\IConfig; -use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; use OCP\Share\Events\ShareCreatedEvent; @@ -45,7 +44,7 @@ public function handlePostShare(ShareCreatedEvent $event): void { if (!$isGuest) { $this->logger->debug( - "ignoring user '$shareWith', not a guest", + "ignoring user '" . $shareWith . "', not a guest", ['app' => Application::APP_ID] ); return; @@ -69,7 +68,7 @@ public function handlePostShare(ShareCreatedEvent $event): void { ); } - $this->logger->debug("checking if '$shareWith' has a password", + $this->logger->debug("checking if '" . $shareWith . "' has a password", ['app' => Application::APP_ID]); $uid = $user->getUID(); @@ -77,19 +76,23 @@ public function handlePostShare(ShareCreatedEvent $event): void { $this->inviteService->sendInvite($uid, $shareWith, $share); } + /** + * @param array $params + */ public function setupReadonlyFilesystem(array $params): void { $uid = $params['user']; $user = $this->userManager->get($uid); if ($user && $this->guestManager->isGuest($user)) { Filesystem::addStorageWrapper('guests.readonly', function ($mountPoint, IStorage $storage) use ($uid): ReadOnlyJail|IStorage { - if ($mountPoint === "/$uid/") { + if ($mountPoint === sprintf('/%s/', $uid)) { return new ReadOnlyJail([ 'storage' => $storage, 'mask' => Constants::PERMISSION_READ, 'path' => 'files' ]); } + return $storage; }); } @@ -100,7 +103,6 @@ public function handleFirstLogin(UserFirstTimeLoggedInEvent $event): void { return; } - /** @var IUser $user */ $user = $event->getUser(); $this->logger->debug('User ' . $user->getUID() . ' logged in for the very first time. Checking guests data import.'); @@ -115,14 +117,14 @@ public function handleFirstLogin(UserFirstTimeLoggedInEvent $event): void { return; } - if (strtolower($email) === strtolower((string)$user->getUID())) { + if (strtolower($email) === strtolower($user->getUID())) { // This is the guest user, logging in for the very first time return; } $guestUser = $this->userManager->get($email); if ($guestUser === null) { - $this->logger->warning("Guest user $email does not exist (anymore)"); + $this->logger->warning('Guest user ' . $email . ' does not exist (anymore)'); return; } diff --git a/lib/Listener/BeforeTemplateRenderedListener.php b/lib/Listener/BeforeTemplateRenderedListener.php index 46d63f28b..5ea01f575 100644 --- a/lib/Listener/BeforeTemplateRenderedListener.php +++ b/lib/Listener/BeforeTemplateRenderedListener.php @@ -36,6 +36,7 @@ public function handle(Event $event): void { if (!$this->config->canCreateGuests()) { return; } + Util::addScript('guests', 'guests-init'); Util::addScript('guests', 'guests-contactsmenu'); diff --git a/lib/Mail.php b/lib/Mail.php index e9335e424..ead0be5b3 100644 --- a/lib/Mail.php +++ b/lib/Mail.php @@ -16,6 +16,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Share\IShare; use OCP\Util; @@ -42,6 +43,7 @@ public function sendGuestInviteMail(string $uid, string $guest, string $token, s if ($language === '') { $language = null; } + $l10n = $this->l10nFactory->get('guests', $language); $passwordLink = $this->urlGenerator->linkToRouteAbsolute( @@ -54,6 +56,7 @@ public function sendGuestInviteMail(string $uid, string $guest, string $token, s if (!$guestEmail) { throw new \Exception('Guest user created without email'); } + $replyTo = $this->userManager->get($uid)->getEMailAddress(); $senderDisplayName = $this->userSession->getUser()->getDisplayName(); @@ -84,11 +87,14 @@ public function sendGuestInviteMail(string $uid, string $guest, string $token, s $this->mailer->send($message); } catch (\Exception) { throw new \Exception($l10n->t( - 'Couldn\'t send reset email. Please contact your administrator.' + "Couldn't send reset email. Please contact your administrator." )); } } + /** + * @return array{string, IEMailTemplate} + */ private function composeShareMessage(IShare $share, string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array { $filename = trim($share->getTarget(), '/'); $subject = $l10n->t('%s shared a file with you', [$senderDisplayName]); @@ -135,6 +141,9 @@ private function composeShareMessage(IShare $share, string $senderDisplayName, s return [ $subject, $emailTemplate ]; } + /** + * @return array{string, IEMailTemplate} + */ private function composeInviteMessage(string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array { $subject = $l10n->t('%s invited you as a guest', [$senderDisplayName]); diff --git a/lib/Migration/OwncloudGuestsMigration.php b/lib/Migration/OwncloudGuestsMigration.php index 9b807300a..21063c309 100644 --- a/lib/Migration/OwncloudGuestsMigration.php +++ b/lib/Migration/OwncloudGuestsMigration.php @@ -36,9 +36,9 @@ public function run(IOutput $output): void { if (!$this->shouldRun()) { $this->logger->info('Skipping owncloud guests migration, no owncloud guests found'); return; - } else { - $this->runStep($output); } + + $this->runStep($output); } protected function shouldRun(): bool { @@ -48,7 +48,7 @@ protected function shouldRun(): bool { return false; } - $this->logger->info("Found $count owncloud guests to migrate"); + $this->logger->info('Found ' . $count . ' owncloud guests to migrate'); return true; } @@ -79,14 +79,15 @@ protected function runStep(IOutput $output): void { if ($guestUser) { $guestUser->setQuota('0 B'); } else { - $output->warning("Could not set quota for guest $userID"); + $output->warning('Could not set quota for guest ' . $userID); } } else { - $output->warning("Could not get hashed password for guest $userID, skipping"); + $output->warning('Could not get hashed password for guest ' . $userID . ', skipping'); } } else { - $output->warning("Failed to get user data for guest $userID, skipping"); + $output->warning('Failed to get user data for guest ' . $userID . ', skipping'); } + $output->advance(); } } diff --git a/lib/Migration/Version1Date20180615130406.php b/lib/Migration/Version1Date20180615130406.php index 3542d3aca..24fc75f25 100644 --- a/lib/Migration/Version1Date20180615130406.php +++ b/lib/Migration/Version1Date20180615130406.php @@ -1,10 +1,11 @@ factory->get(Application::APP_ID)->t('Guests'); } + /** + * @return array> + */ private function getRichMessageParams(string $source, string $target): array { $sourceUser = $this->userManager->get($source); $targetUser = $this->userManager->get($target); diff --git a/lib/RestrictionManager.php b/lib/RestrictionManager.php index c67323726..e4a68f6d0 100644 --- a/lib/RestrictionManager.php +++ b/lib/RestrictionManager.php @@ -70,22 +70,17 @@ public function setupRestrictions(): void { } public function lateSetupRestrictions(): void { - if ($this->guestManager->isGuest($this->userSession->getUser())) { - if ($this->config->hideOtherUsers()) { - $this->server->getContactsManager()->clear(); - - $this->userBackend->setAllowListing(false); - - /** @var AppConfigOverwrite $appConfig */ - $appConfig = $this->server->get(AppConfigOverwrite::class); - $appConfig->setOverwrite([ - 'core' => [ - 'shareapi_only_share_with_group_members' => 'yes' - ] - ]); - - $this->server->registerService(AppConfig::class, fn () => $appConfig); - } + if ($this->guestManager->isGuest($this->userSession->getUser()) && $this->config->hideOtherUsers()) { + $this->server->getContactsManager()->clear(); + $this->userBackend->setAllowListing(false); + /** @var AppConfigOverwrite $appConfig */ + $appConfig = $this->server->get(AppConfigOverwrite::class); + $appConfig->setOverwrite([ + 'core' => [ + 'shareapi_only_share_with_group_members' => 'yes' + ] + ]); + $this->server->registerService(AppConfig::class, fn () => $appConfig); } } } diff --git a/lib/Service/InviteService.php b/lib/Service/InviteService.php index 1316a02ac..588046894 100644 --- a/lib/Service/InviteService.php +++ b/lib/Service/InviteService.php @@ -46,11 +46,11 @@ public function sendInvite(string $userId, string $guest, ?IShare $share = null) $share ); - if ($share) { + if ($share instanceof IShare) { $share->setMailSend(false); } } catch (DoesNotExistException) { - $this->logger->error("'$guest' does not exist"); + $this->logger->error('"' . $guest . '" does not exist'); } catch (\Exception $e) { $this->logger->error('Failed to send guest activation mail', ['exception' => $e]); } diff --git a/lib/Storage/DirMask.php b/lib/Storage/DirMask.php index a6bd3c9a9..125511819 100644 --- a/lib/Storage/DirMask.php +++ b/lib/Storage/DirMask.php @@ -25,6 +25,7 @@ class DirMask extends PermissionsMask { * @var string the dir that should be masked */ private readonly string $path; + /** * @var int remember length */ @@ -91,6 +92,7 @@ public function rename($source, $target): bool { if (!$this->isUpdatable($source)) { return false; } + if ($this->file_exists($target)) { if ($this->isUpdatable($target)) { return $this->storage->rename($source, $target); @@ -100,10 +102,12 @@ public function rename($source, $target): bool { if ($parent === '.') { $parent = ''; } + if ($this->isCreatable($parent)) { return $this->storage->rename($source, $target); } } + return false; } @@ -111,6 +115,7 @@ public function copy($source, $target): bool { if (!$this->isReadable($source)) { return false; } + if ($this->file_exists($target)) { if ($this->isUpdatable($target)) { return $this->storage->copy($source, $target); @@ -120,10 +125,12 @@ public function copy($source, $target): bool { if ($parent === '.') { $parent = ''; } + if ($this->isCreatable($parent)) { return $this->storage->copy($source, $target); } } + return false; } @@ -180,6 +187,7 @@ public function getCache($path = '', $storage = null): ICache { if (!$storage) { $storage = $this; } + $sourceCache = $this->storage->getCache($path, $storage); return new DirMaskCache($sourceCache, $this->mask, $this->checkPath(...)); } diff --git a/lib/TransferService.php b/lib/TransferService.php index 178d92774..3451319c7 100644 --- a/lib/TransferService.php +++ b/lib/TransferService.php @@ -39,11 +39,11 @@ public function transfer(IUser $guestUser, IUser $user): void { try { /** @var OwnershipTransferService $ownershipTransferService */ $ownershipTransferService = $this->container->get(OwnershipTransferService::class); - } catch (QueryException $e) { + } catch (QueryException $queryException) { $this->logger->error('Could not resolve ownership transfer service to import guest user data', [ - 'exception' => $e, + 'exception' => $queryException, ]); - throw $e; + throw $queryException; } try { @@ -55,11 +55,11 @@ public function transfer(IUser $guestUser, IUser $user): void { true, true ); - } catch (TransferOwnershipException $e) { + } catch (TransferOwnershipException $transferOwnershipException) { $this->logger->error('Could not import guest user data', [ - 'exception' => $e, + 'exception' => $transferOwnershipException, ]); - throw $e; + throw $transferOwnershipException; } // Update incomming shares diff --git a/lib/UserBackend.php b/lib/UserBackend.php index 4a34f56c3..e5e0a9306 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -39,6 +39,7 @@ class UserBackend extends ABackend implements IPasswordHashBackend { private CappedMemoryCache $cache; + private bool $allowListing = true; public function __construct( @@ -75,7 +76,7 @@ public function createUser(string $uid, string $password): bool { // Clear cache unset($this->cache[$uid]); - return $result ? true : false; + return (bool)$result; } return false; @@ -99,7 +100,7 @@ public function deleteUser($uid): bool { unset($this->cache[$uid]); } - return $result ? true : false; + return (bool)$result; } /** @@ -117,7 +118,7 @@ public function setPassword(string $uid, string $password): bool { ->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid)))); $result = $query->executeStatement(); - return $result ? true : false; + return (bool)$result; } return false; @@ -127,6 +128,7 @@ public function getPasswordHash(string $userId): ?string { if (!$this->userExists($userId)) { return null; } + $qb = $this->dbConn->getQueryBuilder(); $qb->select('password') ->from('guests_users') @@ -136,6 +138,7 @@ public function getPasswordHash(string $userId): ?string { if ($hash === false) { return null; } + return $hash; } @@ -143,6 +146,7 @@ public function setPasswordHash(string $userId, string $passwordHash): bool { if (!$this->hasher->validate($passwordHash)) { throw new InvalidArgumentException(); } + $qb = $this->dbConn->getQueryBuilder(); $qb->update('guests_users') ->set('password', $qb->createNamedParameter($passwordHash)) @@ -191,41 +195,40 @@ public function getDisplayName($uid): string { public function getDisplayNames($search = '', $limit = null, $offset = null): array { if (!$this->allowListing) { return []; - } else { - $query = $this->dbConn->getQueryBuilder(); - - if ($search === '') { - $query->select('uid', 'displayname') - ->from('guests_users', 'u') - ->orderBy('uid_lower', 'ASC') - ->setMaxResults($limit) - ->setFirstResult($offset); - } else { - $query->select('uid', 'displayname') - ->from('guests_users', 'u') - ->leftJoin('u', 'preferences', 'p', $query->expr()->andX( - $query->expr()->eq('userid', 'uid'), - $query->expr()->eq('appid', $query->expr()->literal('settings')), - $query->expr()->eq('configkey', $query->expr()->literal('email'))) - ) - // sqlite doesn't like re-using a single named parameter here - ->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) - ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) - ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) - ->orderBy($query->func()->lower('displayname'), 'ASC') - ->orderBy('uid_lower', 'ASC') - ->setMaxResults($limit) - ->setFirstResult($offset); - } + } - $result = $query->executeQuery(); - $displayNames = []; - while ($row = $result->fetch()) { - $displayNames[(string)$row['uid']] = (string)$row['displayname']; - } + $query = $this->dbConn->getQueryBuilder(); + if ($search === '') { + $query->select('uid', 'displayname') + ->from('guests_users', 'u') + ->orderBy('uid_lower', 'ASC') + ->setMaxResults($limit) + ->setFirstResult($offset); + } else { + $query->select('uid', 'displayname') + ->from('guests_users', 'u') + ->leftJoin('u', 'preferences', 'p', $query->expr()->andX( + $query->expr()->eq('userid', 'uid'), + $query->expr()->eq('appid', $query->expr()->literal('settings')), + $query->expr()->eq('configkey', $query->expr()->literal('email'))) + ) + // sqlite doesn't like re-using a single named parameter here + ->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) + ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) + ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%'))) + ->orderBy($query->func()->lower('displayname'), 'ASC') + ->orderBy('uid_lower', 'ASC') + ->setMaxResults($limit) + ->setFirstResult($offset); + } - return $displayNames; + $result = $query->executeQuery(); + $displayNames = []; + while ($row = $result->fetch()) { + $displayNames[(string)$row['uid']] = (string)$row['displayname']; } + + return $displayNames; } /** @@ -255,9 +258,10 @@ public function checkPassword(string $loginName, string $password) { $storedHash = $row['password']; $newHash = ''; if ($this->hasher->verify($password, $storedHash, $newHash)) { - if (!empty($newHash)) { + if ($newHash !== '') { $this->setPassword($loginName, $password); } + return (string)$row['uid']; } } diff --git a/rector.php b/rector.php index bd8e90773..f1c95c239 100644 --- a/rector.php +++ b/rector.php @@ -9,6 +9,7 @@ use Rector\Config\RectorConfig; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector; return RectorConfig::configure() ->withPaths([ @@ -23,7 +24,18 @@ ) ->withPreparedSets( deadCode: true, + codeQuality: true, + codingStyle: true, typeDeclarations: true, + typeDeclarationDocblocks: true, + privatization: true, + instanceOf: true, + earlyReturn: true, + rectorPreset: true, + phpunitCodeQuality: true, + doctrineCodeQuality: true, + symfonyCodeQuality: true, + symfonyConfigs: true, )->withPhpSets( php81: true, )->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [ @@ -33,4 +45,7 @@ ->withSets([ NextcloudSets::NEXTCLOUD_30, PHPUnitSetList::PHPUNIT_100, + ]) + ->withRules([ + SafeDeclareStrictTypesRector::class, ]); diff --git a/tests/integration/features/bootstrap/GuestsContext.php b/tests/integration/features/bootstrap/GuestsContext.php index b2f2442b2..4a31a2f72 100644 --- a/tests/integration/features/bootstrap/GuestsContext.php +++ b/tests/integration/features/bootstrap/GuestsContext.php @@ -18,6 +18,10 @@ * Guests context. */ class GuestsContext implements Context, SnippetAcceptingContext { + public $baseUrl; + + public $response; + use Webdav; private array $createdGuests = []; @@ -32,26 +36,23 @@ public function prepareUserNameAsFrontend(string $guestDisplayName, $guestEmail) * @param string $user */ public function userCreatedAGuestUser($user, string $guestDisplayName, string $guestEmail): void { - $fullUrl = substr($this->baseUrl, 0, -4) . '/index.php/apps/guests/users'; + $fullUrl = substr((string)$this->baseUrl, 0, -4) . '/index.php/apps/guests/users'; //Replicating frontend behaviour $userName = $this->prepareUserNameAsFrontend($guestDisplayName, $guestEmail); $fullUrl = $fullUrl . '?displayName=' . $guestDisplayName . '&email=' . $guestEmail . '&username=' . $userName; $client = new Client(); $options = []; - if ($user === 'admin') { - $options['auth'] = $this->adminUser; - } else { - $options['auth'] = [$user, $this->regularUser]; - } + $options['auth'] = $user === 'admin' ? $this->adminUser : [$user, $this->regularUser]; $request = $client->createRequest('PUT', $fullUrl, $options); $request->addHeader('Content-Type', 'application/x-www-form-urlencoded'); try { $this->response = $client->send($request); - } catch (BadResponseException $e) { + } catch (BadResponseException $badResponseException) { // 4xx and 5xx responses cause an exception - $this->response = $e->getResponse(); + $this->response = $badResponseException->getResponse(); } + $this->createdGuests[$guestDisplayName] = $guestEmail; } @@ -110,8 +111,8 @@ public function guestUserSetsItsPassword(string $guestDisplayName): void { ]; try { $this->response = $client->send($client->createRequest('POST', $urlSetPasswd, $options)); - } catch (ClientException $ex) { - $this->response = $ex->getResponse(); + } catch (ClientException $clientException) { + $this->response = $clientException->getResponse(); } } @@ -120,7 +121,7 @@ public function guestUserSetsItsPassword(string $guestDisplayName): void { * @AfterScenario */ public function cleanupGuests(): void { - foreach ($this->createdGuests as $displayName => $email) { + foreach (array_keys($this->createdGuests) as $displayName) { $this->deleteGuestUser($displayName); } } diff --git a/tests/unit/AppWhitelistTest.php b/tests/unit/AppWhitelistTest.php index 2dcb32e3a..378fec800 100644 --- a/tests/unit/AppWhitelistTest.php +++ b/tests/unit/AppWhitelistTest.php @@ -21,9 +21,13 @@ class AppWhitelistTest extends TestCase { private Config&MockObject $config; + private GuestManager&MockObject $guestManager; + private IL10N&MockObject $l10n; + private IAppManager&MockObject $appManager; + private IURLGenerator&MockObject $urlGenerator; private ?AppWhitelist $appWhitelist = null; @@ -56,7 +60,7 @@ public function testIsUrlAllowed(): void { ->willReturn(true); $this->guestManager->method('isGuest') ->willReturn(true); - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->assertFalse($this->appWhitelist->isUrlAllowed($user, '/apps/news/...')); $this->assertTrue($this->appWhitelist->isUrlAllowed($user, '/apps/foo/...')); @@ -69,7 +73,7 @@ public function testIsUrlAllowedNoWhitelist(): void { ->willReturn(false); $this->guestManager->method('isGuest') ->willReturn(true); - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->assertTrue($this->appWhitelist->isUrlAllowed($user, '/apps/news/...')); $this->assertTrue($this->appWhitelist->isUrlAllowed($user, '/apps/foo/...')); diff --git a/tests/unit/Command/AddCommandTest.php b/tests/unit/Command/AddCommandTest.php index 70c716d11..c3fad58da 100644 --- a/tests/unit/Command/AddCommandTest.php +++ b/tests/unit/Command/AddCommandTest.php @@ -21,11 +21,15 @@ class AddCommandTest extends TestCase { /** @var IUserManager|MockObject */ private $userManager; + /** @var GuestManager|MockObject */ private $guestManager; + /** @var IMailer|MockObject */ private $mailer; + private ?AddCommand $command = null; + private ?CommandTester $commandTester = null; protected function setUp(): void { @@ -41,9 +45,13 @@ protected function setUp(): void { $this->guestManager ); $this->command->setApplication(new Application()); + $this->commandTester = new CommandTester($this->command); } + /** + * @return array>|array>> + */ public function createGuestDataProvider() { return [ [ @@ -68,7 +76,7 @@ public function createGuestDataProvider() { * @dataProvider createGuestDataProvider */ public function testCreateGuest($commandArgs): void { - $createdByUser = $this->createMock(IUser::class); + $createdByUser = $this->createStub(IUser::class); $password = 'guest-password'; if (isset($commandArgs['--generate-password'])) { @@ -107,6 +115,7 @@ public function testCreateGuest($commandArgs): void { $this->commandTester->setInputs(['guest-password', 'guest-password']); $this->commandTester->execute($commandArgs); + $output = $this->commandTester->getDisplay(); $this->assertStringContainsString('The guest account user "guestid" was created successfully', $output); } diff --git a/tests/unit/ConfigTest.php b/tests/unit/ConfigTest.php index 797a52456..6180bd149 100644 --- a/tests/unit/ConfigTest.php +++ b/tests/unit/ConfigTest.php @@ -23,12 +23,16 @@ class ConfigTest extends TestCase { /** @var IConfig|MockObject */ private $config; + /** @var IAppConfig|MockObject */ private $appConfig; + /** @var ISubAdmin|MockObject */ private $subAdmin; + /** @var IUserSession|MockObject */ private $userSession; + /** @var IGroupManager|MockObject */ private $groupManager; @@ -152,7 +156,7 @@ public function testCanCreateGuestsNoUser(): void { } public function testCanCreateGuestsWithGroupRestrictionNoMatch(): void { - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->userSession->method('getUser') ->willReturn($user); @@ -168,7 +172,7 @@ public function testCanCreateGuestsWithGroupRestrictionNoMatch(): void { } public function testCanCreateGuestsWithGroupRestrictionWithMatch(): void { - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->userSession->method('getUser') ->willReturn($user); @@ -188,7 +192,7 @@ public function testCanCreateGuestsWithGroupRestrictionWithMatch(): void { } public function testCanCreateGuestsWithSharingRestrictedButIsSubAdmin(): void { - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->userSession->method('getUser') ->willReturn($user); @@ -208,7 +212,7 @@ public function testCanCreateGuestsWithSharingRestrictedButIsSubAdmin(): void { } public function testCanCreateGuestsWithSharingRestrictedNotSubAdmin(): void { - $user = $this->createMock(IUser::class); + $user = $this->createStub(IUser::class); $this->userSession->method('getUser') ->willReturn($user); diff --git a/tests/unit/Controller/UsersControllerTest.php b/tests/unit/Controller/UsersControllerTest.php index b6858a198..fc5d155d8 100644 --- a/tests/unit/Controller/UsersControllerTest.php +++ b/tests/unit/Controller/UsersControllerTest.php @@ -32,19 +32,33 @@ class UsersControllerTest extends TestCase { private IRequest&MockObject $request; + private IUserManager&MockObject $userManager; + private IL10N&MockObject $l10n; + private Config $guestsConfig; + private IMailer&MockObject $mailer; + private GuestManager&MockObject $guestManager; + private IUserSession&MockObject $userSession; + private ISubAdmin&MockObject $subAdmin; + private IGroupManager&MockObject $groupManager; + private TransferService&MockObject $transferService; + private TransferMapper&MockObject $transferMapper; + private IAppConfig&MockObject $appConfig; + private IConfig&MockObject $config; + private InviteService&MockObject $inviteService; + private UsersController $controller; protected function setUp(): void { @@ -96,7 +110,7 @@ protected function setUp(): void { * Test that creation fails when the current user is a guest. */ public function testCreateWhenCurrentUserIsGuest(): void { - $currentUser = $this->createMock(IUser::class); + $currentUser = $this->createStub(IUser::class); $this->userSession->method('getUser') ->willReturn($currentUser); @@ -200,7 +214,7 @@ public function testCreateSucceedsForAdminDespiteGroupRestrictions(): void { $this->userManager->method('userExists') ->willReturn(false); - $guestUser = $this->createMock(IUser::class); + $guestUser = $this->createStub(IUser::class); $this->guestManager->method('createGuest') ->willReturn($guestUser); @@ -247,7 +261,7 @@ public function testCreateSucceedsWhenUserInAllowedGroup(): void { $this->userManager->method('userExists') ->willReturn(false); - $guestUser = $this->createMock(IUser::class); + $guestUser = $this->createStub(IUser::class); $this->guestManager->method('createGuest') ->willReturn($guestUser); @@ -309,7 +323,7 @@ public function testCreateSucceedsForSubAdminWhenSharingIsRestricted(): void { $this->userManager->method('userExists') ->willReturn(false); - $guestUser = $this->createMock(IUser::class); + $guestUser = $this->createStub(IUser::class); $this->guestManager->method('createGuest') ->willReturn($guestUser); @@ -395,7 +409,7 @@ public function testCreateSucceedsWhenNoRestrictions(): void { $this->userManager->method('userExists') ->willReturn(false); - $guestUser = $this->createMock(IUser::class); + $guestUser = $this->createStub(IUser::class); $this->guestManager->method('createGuest') ->willReturn($guestUser); @@ -495,7 +509,7 @@ public function testCreateWithGroupButNotSubAdmin(): void { ->with($currentUser) ->willReturn(false); - $group = $this->createMock(IGroup::class); + $group = $this->createStub(IGroup::class); $this->groupManager->method('get') ->with('testgroup') ->willReturn($group); @@ -577,7 +591,7 @@ public function testCreateWithExistingEmailUser(): void { ->with('existing@example.com') ->willReturn(true); - $existingUser = $this->createMock(IUser::class); + $existingUser = $this->createStub(IUser::class); $this->userManager->method('getByEmail') ->with('existing@example.com') ->willReturn([$existingUser]); @@ -641,7 +655,7 @@ public function testCreateSuccessWithGroupsAsSubadmin(): void { $group1 = $this->createMock(IGroup::class); $group2 = $this->createMock(IGroup::class); - $this->groupManager->method('get') + $this->groupManager->expects($this->exactly(2))->method('get') ->willReturnMap([ ['group1', $group1], ['group2', $group2], @@ -668,7 +682,7 @@ public function testCreateSuccessWithGroupsAsSubadmin(): void { ->with('new@example.com') ->willReturn(false); - $guestUser = $this->createMock(IUser::class); + $guestUser = $this->createStub(IUser::class); $this->userManager->method('get') ->with('new@example.com') ->willReturn($guestUser); diff --git a/tests/unit/GroupBackendTest.php b/tests/unit/GroupBackendTest.php index c364b2d3b..2fbfcc5fc 100644 --- a/tests/unit/GroupBackendTest.php +++ b/tests/unit/GroupBackendTest.php @@ -19,8 +19,10 @@ class GroupBackendTest extends TestCase { /** @var GuestManager|MockObject */ private $guestManager; + /** @var Config|MockObject */ private $config; + /** @var IUserSession|MockObject */ private $userSession; @@ -49,6 +51,9 @@ private function setUID(string $uid): void { ->willReturn($user); } + /** + * @param string[] $uids + */ private function setGuests(array $uids): void { $this->guestManager->method('listGuests') ->willReturn($uids); diff --git a/tests/unit/GuestManagerTest.php b/tests/unit/GuestManagerTest.php index bdd5c45f5..0dc1f59e9 100644 --- a/tests/unit/GuestManagerTest.php +++ b/tests/unit/GuestManagerTest.php @@ -25,20 +25,28 @@ class GuestManagerTest extends TestCase { /** @var UserBackend|MockObject */ private $userBackend; + /** @var IUserSession|MockObject */ private $userSession; + /** @var IUserManager|MockObject */ private $userManager; + /** @var IConfig|MockObject */ private $config; + /** @var ISecureRandom|MockObject */ private $random; + /** @var ICrypto|MockObject */ private $crypto; + /** @var IManager|MockObject */ private $shareManager; + /** @var IDBConnection|MockObject */ private $conneciton; + /** @var IEventDispatcher|MockObject */ private $eventDispatcher; @@ -131,6 +139,7 @@ public function testCreateGuest(): void { if (!isset($setValues[$app])) { $setValues[$app] = []; } + $setValues[$app][$key] = $value; }); diff --git a/tests/unit/Migration/OwncloudGuestMigrationTest.php b/tests/unit/Migration/OwncloudGuestMigrationTest.php index a50ba9be7..9e742cc17 100644 --- a/tests/unit/Migration/OwncloudGuestMigrationTest.php +++ b/tests/unit/Migration/OwncloudGuestMigrationTest.php @@ -25,9 +25,13 @@ */ class OwncloudGuestMigrationTest extends TestCase { private IConfig $config; + private IUserManager $userManager; + private BufferedOutput $consoleOutput; + private ConsoleOutput $migrationOutput; + private GuestManager $guestManager; protected function setUp():void { diff --git a/vendor-bin/rector/composer.lock b/vendor-bin/rector/composer.lock index 5f6b688f7..d053cd497 100644 --- a/vendor-bin/rector/composer.lock +++ b/vendor-bin/rector/composer.lock @@ -122,11 +122,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.36", + "version": "2.1.38", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2132e5e2361d11d40af4c17faa16f043269a4cf3", - "reference": "2132e5e2361d11d40af4c17faa16f043269a4cf3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", "shasum": "" }, "require": { @@ -171,7 +171,7 @@ "type": "github" } ], - "time": "2026-01-21T13:58:26+00:00" + "time": "2026-01-30T17:12:46+00:00" }, { "name": "psr/clock", @@ -376,21 +376,21 @@ }, { "name": "rector/rector", - "version": "2.3.4", + "version": "2.3.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2" + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9227d7a24b0f23ae941057509364f948d5da9ab2", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b", + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.36" + "phpstan/phpstan": "^2.1.38" }, "conflict": { "rector/rector-doctrine": "*", @@ -424,7 +424,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.4" + "source": "https://github.com/rectorphp/rector/tree/2.3.6" }, "funding": [ { @@ -432,7 +432,7 @@ "type": "github" } ], - "time": "2026-01-21T14:49:03+00:00" + "time": "2026-02-06T14:25:06+00:00" }, { "name": "webmozart/assert",