diff --git a/appinfo/routes.php b/appinfo/routes.php
index 11acc1bf..18683d47 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -58,6 +58,11 @@
'url' => '/api/v1/users/{userId}',
'verb' => 'GET'
],
+ [
+ 'name' => 'users#transfer',
+ 'url' => '/api/v1/transfer',
+ 'verb' => 'PUT'
+ ],
[
'name' => 'API#languages',
'url' => '/api/v1/languages',
diff --git a/img/account-arrow-right.svg b/img/account-arrow-right.svg
new file mode 100644
index 00000000..efed9e4a
--- /dev/null
+++ b/img/account-arrow-right.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/BackgroundJob/TransferJob.php b/lib/BackgroundJob/TransferJob.php
new file mode 100644
index 00000000..f74f9e6b
--- /dev/null
+++ b/lib/BackgroundJob/TransferJob.php
@@ -0,0 +1,147 @@
+notificationManager->createNotification();
+ $notification
+ ->setApp(Application::APP_ID)
+ ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'account-arrow-right.svg')))
+ ->setUser($transfer->getAuthor())
+ ->setDateTime($this->time->getDateTime())
+ ->setObject('guest-transfer', (string)$transfer->getId())
+ ->setSubject('guest-transfer-fail', [
+ 'source' => $transfer->getSource(),
+ 'target' => $transfer->getTarget(),
+ ]);
+ $this->notificationManager->notify($notification);
+ }
+
+ private function notifySuccess(Transfer $transfer): void {
+ $notification = $this->notificationManager->createNotification();
+ $notification
+ ->setApp(Application::APP_ID)
+ ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'account-arrow-right.svg')))
+ ->setUser($transfer->getAuthor())
+ ->setDateTime($this->time->getDateTime())
+ ->setObject('guest-transfer', (string)$transfer->getId())
+ ->setSubject('guest-transfer-done', [
+ 'source' => $transfer->getSource(),
+ 'target' => $transfer->getTarget(),
+ ]);
+ $this->notificationManager->notify($notification);
+ }
+
+ private function fail(Transfer $transfer, ?IUser $targetUser = null): void {
+ $this->notifyFailure($transfer);
+ $this->transferMapper->delete($transfer);
+ if (!($targetUser instanceof IUser)) {
+ return;
+ }
+ $result = $targetUser->delete(); // Rollback created user
+ if (!$result) {
+ $this->logger->error('Failed to delete target user', ['user' => $targetUser->getUID()]);
+ }
+ }
+
+ public function run($argument): void {
+ /** @var int $id */
+ $id = $argument['id'];
+
+ $transfer = $this->transferMapper->getById($id);
+ $transfer->setStatus(Transfer::STATUS_STARTED);
+ $this->transferMapper->update($transfer);
+
+ $source = $transfer->getSource();
+ $target = $transfer->getTarget();
+
+ $sourceUser = $this->userManager->get($source);
+ if (!($sourceUser instanceof IUser)) {
+ $this->logger->error('Failed to transfer missing guest user: ' . $source);
+ $this->fail($transfer);
+ return;
+ }
+
+ if ($this->userManager->userExists($target)) {
+ $this->logger->error("Cannot transfer guest user \"$source\", target user \"$target\" already exists");
+ $this->fail($transfer);
+ return;
+ }
+
+ $targetUser = $this->userManager->createUser(
+ $target,
+ $this->secureRandom->generate(20), // Password hash will be copied to target user from source user
+ );
+
+ if (!($targetUser instanceof IUser)) {
+ $this->logger->error('Failed to create new user: ' . $target);
+ $this->fail($transfer);
+ return;
+ }
+
+ $targetUser->setSystemEMailAddress($sourceUser->getUID()); // Guest user id is an email
+
+ try {
+ $this->transferService->transfer($sourceUser, $targetUser);
+ } catch (\Throwable $th) {
+ $this->logger->error($th->getMessage(), ['exception' => $th]);
+ $this->fail($transfer, $targetUser);
+ return;
+ }
+
+ $passwordHash = $sourceUser->getPasswordHash();
+ if (empty($passwordHash)) {
+ $this->logger->error('Invalid guest password hash', ['guest' => $sourceUser->getUID(), 'passwordHash' => $passwordHash]);
+ $this->fail($transfer, $targetUser);
+ return;
+ }
+
+ $setPasswordHashResult = $targetUser->setPasswordHash($passwordHash); // Copy password hash after transfer to prevent login before completion
+ if (!$setPasswordHashResult) {
+ $this->logger->error('Failed to set password hash on target user', ['user' => $targetUser->getUID()]);
+ $this->fail($transfer, $targetUser);
+ return;
+ }
+
+ $result = $sourceUser->delete();
+ if (!$result) {
+ $this->logger->error('Failed to delete guest user', ['user' => $sourceUser->getUID()]);
+ }
+ $this->notifySuccess($transfer);
+ $this->transferMapper->delete($transfer);
+ }
+}
diff --git a/lib/Controller/UsersController.php b/lib/Controller/UsersController.php
index 67a57afe..38f4f5ec 100644
--- a/lib/Controller/UsersController.php
+++ b/lib/Controller/UsersController.php
@@ -1,10 +1,21 @@
request = $request;
- $this->userManager = $userManager;
- $this->l10n = $l10n;
- $this->mailer = $mailer;
- $this->guestManager = $guestManager;
- $this->userSession = $userSession;
- $this->config = $config;
- $this->subAdmin = $subAdmin;
- $this->groupManager = $groupManager;
}
/**
@@ -199,4 +174,58 @@ public function get(string $userId): DataResponse {
return new DataResponse($guests);
}
+
+ /**
+ * Transfer guest to a full account
+ */
+ public function transfer(string $guestUserId, string $targetUserId): DataResponse {
+ $author = $this->userSession->getUser();
+ if (!($author instanceof IUser)) {
+ return new DataResponse([
+ 'message' => $this->l10n->t('Failed to authorize')
+ ], Http::STATUS_UNAUTHORIZED);
+ }
+
+ $sourceUser = $this->userManager->get($guestUserId);
+ if (!($sourceUser instanceof IUser)) {
+ return new DataResponse([
+ 'message' => $this->l10n->t('Guest does not exist')
+ ], Http::STATUS_NOT_FOUND);
+ }
+
+ if ($this->userManager->userExists($targetUserId)) {
+ return new DataResponse([
+ 'message' => $this->l10n->t('User already exists')
+ ], Http::STATUS_CONFLICT);
+ }
+
+ if (!$this->guestManager->isGuest($sourceUser)) {
+ return new DataResponse([
+ 'message' => $this->l10n->t('User is not a guest'),
+ ], Http::STATUS_CONFLICT);
+ }
+
+ try {
+ $transfer = $this->transferMapper->getBySource($sourceUser->getUID());
+ } catch (DoesNotExistException $e) {
+ // Allow as this just means there is no pending transfer
+ }
+
+ try {
+ $transfer = $this->transferMapper->getByTarget($targetUserId);
+ } catch (DoesNotExistException $e) {
+ // Allow as this just means there is no pending transfer
+ }
+
+ if (!empty($transfer)) {
+ return new DataResponse([
+ 'status' => $transfer->getStatus(),
+ 'source' => $transfer->getSource(),
+ 'target' => $transfer->getTarget(),
+ ], Http::STATUS_ACCEPTED);
+ }
+
+ $this->transferService->addTransferJob($author, $sourceUser, $targetUserId);
+ return new DataResponse([], Http::STATUS_CREATED);
+ }
}
diff --git a/lib/Db/Transfer.php b/lib/Db/Transfer.php
new file mode 100644
index 00000000..840e29ec
--- /dev/null
+++ b/lib/Db/Transfer.php
@@ -0,0 +1,46 @@
+addType('author', 'string');
+ $this->addType('source', 'string');
+ $this->addType('target', 'string');
+ $this->addType('status', 'string');
+ }
+}
diff --git a/lib/Db/TransferMapper.php b/lib/Db/TransferMapper.php
new file mode 100644
index 00000000..879bbe1c
--- /dev/null
+++ b/lib/Db/TransferMapper.php
@@ -0,0 +1,68 @@
+
+ */
+class TransferMapper extends QBMapper {
+ public const TABLE_NAME = 'guests_transfers';
+
+ public function __construct(IDBConnection $db) {
+ parent::__construct(
+ $db,
+ static::TABLE_NAME,
+ Transfer::class,
+ );
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function getById(int $id): Transfer {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from(static::TABLE_NAME)
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
+
+ return $this->findEntity($qb);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function getBySource(string $userId): Transfer {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from(static::TABLE_NAME)
+ ->where($qb->expr()->eq('source', $qb->createNamedParameter($userId)));
+
+ return $this->findEntity($qb);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function getByTarget(string $userId): Transfer {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from(static::TABLE_NAME)
+ ->where($qb->expr()->eq('target', $qb->createNamedParameter($userId)));
+
+ return $this->findEntity($qb);
+ }
+}
diff --git a/lib/GuestManager.php b/lib/GuestManager.php
index a28e9811..cc8c288f 100644
--- a/lib/GuestManager.php
+++ b/lib/GuestManager.php
@@ -35,53 +35,17 @@
use OCP\Share\IShare;
class GuestManager {
- /** @var IConfig */
- private $config;
-
- /** @var UserBackend */
- private $userBackend;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var ISecureRandom */
- private $secureRandom;
-
- /** @var ICrypto */
- private $crypto;
-
- /** @var IManager */
- private $shareManager;
-
- /** @var IDBConnection */
- private $connection;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
public function __construct(
- IConfig $config,
- UserBackend $userBackend,
- ISecureRandom $secureRandom,
- ICrypto $crypto,
- IManager $shareManager,
- IDBConnection $connection,
- IUserSession $userSession,
- IEventDispatcher $eventDispatcher,
- IUserManager $userManager
+ private IConfig $config,
+ private UserBackend $userBackend,
+ private ISecureRandom $secureRandom,
+ private ICrypto $crypto,
+ private IManager $shareManager,
+ private IDBConnection $connection,
+ private IUserSession $userSession,
+ private IEventDispatcher $eventDispatcher,
+ private IUserManager $userManager,
) {
- $this->config = $config;
- $this->userBackend = $userBackend;
- $this->secureRandom = $secureRandom;
- $this->crypto = $crypto;
- $this->shareManager = $shareManager;
- $this->connection = $connection;
- $this->userSession = $userSession;
- $this->eventDispatcher = $eventDispatcher;
- $this->userManager = $userManager;
}
/**
diff --git a/lib/Hooks.php b/lib/Hooks.php
index 8ed6a3e9..944fb99b 100644
--- a/lib/Hooks.php
+++ b/lib/Hooks.php
@@ -23,85 +23,34 @@
namespace OCA\Guests;
use OC\Files\Filesystem;
-use OCA\Files\Exception\TransferOwnershipException;
-use OCA\Files\Service\OwnershipTransferService;
use OCA\Guests\AppInfo\Application;
use OCA\Guests\Storage\ReadOnlyJail;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\IAppContainer;
-use OCP\AppFramework\QueryException;
use OCP\Constants;
use OCP\Files\Storage\IStorage;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
-use OCP\Notification\IManager as INotificationManager;
use OCP\Security\ICrypto;
use OCP\Share\Events\ShareCreatedEvent;
-use OCP\Share\IManager as IShareManager;
-use OCP\Share\IShare;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use Psr\Log\LoggerInterface;
class Hooks {
-
- /** @var LoggerInterface */
- private $logger;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var Mail */
- private $mail;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var ICrypto */
- private $crypto;
-
- /** @var GuestManager */
- private $guestManager;
-
- /** @var UserBackend */
- private $userBackend;
-
- /** @var IAppContainer */
- private $container;
-
- /** @var INotificationManager */
- private $notificationManager;
- /** @var IShareManager */
- private $shareManager;
-
- /** @var IConfig */
- private $config;
-
public function __construct(
- LoggerInterface $logger,
- IUserSession $userSession,
- Mail $mail,
- IUserManager $userManager,
- IConfig $config,
- ICrypto $crypto,
- GuestManager $guestManager,
- UserBackend $userBackend,
- IAppContainer $container,
- INotificationManager $notificationManager,
- IShareManager $shareManager
+ private LoggerInterface $logger,
+ private IUserSession $userSession,
+ private Mail $mail,
+ private IUserManager $userManager,
+ private IConfig $config,
+ private ICrypto $crypto,
+ private GuestManager $guestManager,
+ private UserBackend $userBackend,
+ private IAppContainer $container,
+ private TransferService $transferService,
) {
- $this->logger = $logger;
- $this->userSession = $userSession;
- $this->mail = $mail;
- $this->userManager = $userManager;
- $this->config = $config;
- $this->crypto = $crypto;
- $this->guestManager = $guestManager;
- $this->userBackend = $userBackend;
- $this->container = $container;
- $this->notificationManager = $notificationManager;
- $this->shareManager = $shareManager;
}
public function handlePostShare(ShareCreatedEvent $event): void {
@@ -195,7 +144,7 @@ public function setupReadonlyFilesystem(array $params): void {
}
public function handleFirstLogin(UserFirstTimeLoggedInEvent $event): void {
- if ($this->config->getSystemValue('migrate_guest_user_data', false) === false) {
+ if (!$this->config->getSystemValueBool('migrate_guest_user_data', false)) {
return;
}
@@ -219,58 +168,20 @@ public function handleFirstLogin(UserFirstTimeLoggedInEvent $event): void {
return;
}
- try {
- /** @var OwnershipTransferService $ownershipTransferService */
- $ownershipTransferService = $this->container->get(OwnershipTransferService::class);
- } catch (QueryException $e) {
- $this->logger->error('Could not resolve ownership transfer service to import guest user data', [
- 'exception' => $e,
- ]);
- return;
- }
-
$guestUser = $this->userManager->get($email);
if ($guestUser === null) {
$this->logger->warning("Guest user $email does not exist (anymore)");
return;
}
- try {
- $ownershipTransferService->transfer(
- $guestUser,
- $user,
- '/',
- null,
- true,
- true
- );
- } catch (TransferOwnershipException $e) {
- $this->logger->error('Could not import guest user data', [
- 'exception' => $e,
- ]);
- }
- // Update incomming shares
- $shares = $this->shareManager->getSharedWith($guestUser->getUID(), IShare::TYPE_USER);
- foreach ($shares as $share) {
- $share->setSharedWith($user->getUID());
- $this->shareManager->updateShare($share);
- }
+ $this->transferService->transfer($guestUser, $user);
- if ($this->config->getSystemValue('remove_guest_account_on_conversion', false) === false) {
+ if (!$this->config->getSystemValueBool('remove_guest_account_on_conversion', false)) {
// Disable previous account
$guestUser->setEnabled(false);
} else {
// Remove previous account
$guestUser->delete();
}
-
- $notification = $this->notificationManager->createNotification();
- $notification
- ->setApp(Application::APP_ID)
- ->setSubject('data_migrated_to_system_user')
- ->setObject('user', $email)
- ->setDateTime(new \DateTime())
- ->setUser($user->getUID());
- $this->notificationManager->notify($notification);
}
}
diff --git a/lib/Migration/Version4000Date20240619221613.php b/lib/Migration/Version4000Date20240619221613.php
new file mode 100644
index 00000000..61e98efc
--- /dev/null
+++ b/lib/Migration/Version4000Date20240619221613.php
@@ -0,0 +1,57 @@
+hasTable(TransferMapper::TABLE_NAME)) {
+ return null;
+ }
+
+ $table = $schema->createTable(TransferMapper::TABLE_NAME);
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('author', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('source', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('target', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('status', Types::STRING, [
+ 'notnull' => true,
+ ]);
+ $table->setPrimaryKey(['id']);
+
+ return $schema;
+ }
+}
diff --git a/lib/Notifications/Notifier.php b/lib/Notifications/Notifier.php
index 4173c10d..0765cf79 100644
--- a/lib/Notifications/Notifier.php
+++ b/lib/Notifications/Notifier.php
@@ -3,24 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2019 Christoph Wurst
- *
- * @author 2019 Christoph Wurst
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Guests\Notifications;
@@ -28,22 +12,17 @@
use InvalidArgumentException;
use OCA\Guests\AppInfo\Application;
use OCP\IURLGenerator;
+use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class Notifier implements INotifier {
-
- /** @var IFactory */
- private $factory;
-
- /** @var IURLGenerator */
- private $url;
-
- public function __construct(IFactory $factory,
- IURLGenerator $url) {
- $this->factory = $factory;
- $this->url = $url;
+ public function __construct(
+ private IFactory $factory,
+ private IURLGenerator $url,
+ private IUserManager $userManager,
+ ) {
}
public function getID(): string {
@@ -54,6 +33,26 @@ public function getName(): string {
return $this->factory->get(Application::APP_ID)->t('Guests');
}
+ private function getRichMessageParams(string $source, string $target): array {
+ $sourceUser = $this->userManager->get($source);
+ $targetUser = $this->userManager->get($target);
+ return [
+ 'guest' => [
+ 'type' => $sourceUser ? 'guest' : 'highlight',
+ 'id' => $sourceUser?->getUID() ?? $source,
+ 'name' => $sourceUser?->getDisplayName() ?? $source,
+ ],
+ 'user' => [
+ 'type' => $targetUser ? 'user' : 'highlight',
+ 'id' => $targetUser?->getUID() ?? $source,
+ 'name' => $targetUser?->getDisplayName() ?? $target,
+ ],
+ ];
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
// Not my app => throw
@@ -74,6 +73,26 @@ public function prepare(INotification $notification, string $languageCode): INot
return $notification;
+ case 'guest-transfer-fail':
+ $params = $notification->getSubjectParameters();
+ $notification
+ ->setRichSubject($l->t('Guest transfer failed'))
+ ->setRichMessage(
+ $l->t('Failed to transfer guest {guest} to {user}'),
+ $this->getRichMessageParams($params['source'], $params['target']),
+ );
+ return $notification;
+
+ case 'guest-transfer-done':
+ $params = $notification->getSubjectParameters();
+ $notification
+ ->setRichSubject($l->t('Guest transfer done'))
+ ->setRichMessage(
+ $l->t('Transfer of guest {guest} to {user} completed'),
+ $this->getRichMessageParams($params['source'], $params['target']),
+ );
+ return $notification;
+
default:
// Unknown subject => Unknown notification => throw
throw new InvalidArgumentException();
diff --git a/lib/TransferService.php b/lib/TransferService.php
new file mode 100644
index 00000000..4ff0ac3f
--- /dev/null
+++ b/lib/TransferService.php
@@ -0,0 +1,98 @@
+container->get(OwnershipTransferService::class);
+ } catch (QueryException $e) {
+ $this->logger->error('Could not resolve ownership transfer service to import guest user data', [
+ 'exception' => $e,
+ ]);
+ throw $e;
+ }
+
+ try {
+ $ownershipTransferService->transfer(
+ $guestUser,
+ $user,
+ '/',
+ null,
+ true,
+ true
+ );
+ } catch (TransferOwnershipException $e) {
+ $this->logger->error('Could not import guest user data', [
+ 'exception' => $e,
+ ]);
+ throw $e;
+ }
+
+ // Update incomming shares
+ $shares = $this->shareManager->getSharedWith($guestUser->getUID(), IShare::TYPE_USER);
+ foreach ($shares as $share) {
+ $share->setSharedWith($user->getUID());
+ $this->shareManager->updateShare($share);
+ }
+
+ $notification = $this->notificationManager->createNotification();
+ $notification
+ ->setApp(Application::APP_ID)
+ ->setSubject('data_migrated_to_system_user')
+ ->setObject('user', $user->getEMailAddress())
+ ->setDateTime(new \DateTime())
+ ->setUser($user->getUID());
+ $this->notificationManager->notify($notification);
+ }
+
+ /**
+ * @param string $target Target user id
+ */
+ public function addTransferJob(IUser $author, IUser $source, string $target): void {
+ $transfer = new Transfer();
+ $transfer->setAuthor($author->getUID());
+ $transfer->setSource($source->getUID());
+ $transfer->setTarget($target);
+ $transfer->setStatus(Transfer::STATUS_WAITING);
+ /** @var Transfer $transfer */
+ $transfer = $this->transferMapper->insert($transfer);
+
+ $this->jobList->add(TransferJob::class, [
+ 'id' => $transfer->getId(),
+ ]);
+ }
+}