Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
use OCA\Guests\Capabilities;
use OCA\Guests\GroupBackend;
use OCA\Guests\Hooks;
use OCA\Guests\Listener\BeforeTemplateRenderedListener;
use OCA\Guests\Listener\BeforeUserManagementRenderedListener;
use OCA\Guests\Listener\LoadAdditionalScriptsListener;
use OCA\Guests\Listener\ShareAutoAcceptListener;
use OCA\Guests\Listener\TalkIntegrationListener;
use OCA\Guests\Listener\UserChangedListener;
use OCA\Guests\Notifications\Notifier;
use OCA\Guests\RestrictionManager;
Expand All @@ -33,6 +33,7 @@
use OCP\Share\Events\ShareCreatedEvent;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserFirstTimeLoggedInEvent;
use OCP\Util;
use Psr\Container\ContainerInterface;

class Application extends App implements IBootstrap {
Expand All @@ -47,7 +48,7 @@ public function register(IRegistrationContext $context): void {

$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class);
$context->registerEventListener(ShareCreatedEvent::class, ShareAutoAcceptListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, TalkIntegrationListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(BeforeUserManagementRenderedEvent::class, BeforeUserManagementRenderedListener::class);
$context->registerEventListener(UserChangedEvent::class, UserChangedListener::class);
}
Expand All @@ -62,6 +63,7 @@ public function boot(IBootContext $context): void {
$this->setupGuestRestrictions($context->getAppContainer(), $context->getServerContainer());
$this->setupNotifications($context->getAppContainer());
$context->getAppContainer()->get(RestrictionManager::class)->lateSetupRestrictions();
Util::addScript('guests', 'guests-init');
}

private function setupGuestManagement(ContainerInterface $container, ContainerInterface $server): void {
Expand Down
8 changes: 7 additions & 1 deletion lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Guests\Db\Transfer;
use OCA\Guests\Db\TransferMapper;
use OCA\Guests\GuestManager;
use OCA\Guests\Service\InviteService;
use OCA\Guests\TransferService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function __construct(
private IGroupManager $groupManager,
private TransferService $transferService,
private TransferMapper $transferMapper,
private InviteService $inviteService,
) {
parent::__construct($appName, $request);
}
Expand All @@ -55,7 +57,7 @@ public function __construct(
* @param array $groups
* @return DataResponse
*/
public function create(string $email, string $displayName, string $language, array $groups): DataResponse {
public function create(string $email, string $displayName, string $language, array $groups, bool $sendInvite = true): DataResponse {
$errorMessages = [];
$currentUser = $this->userSession->getUser();

Expand Down Expand Up @@ -154,6 +156,10 @@ public function create(string $email, string $displayName, string $language, arr
);
}

if ($sendInvite) {
$this->inviteService->sendInvite($currentUser->getUID(), $username);
}

return new DataResponse(
[
'message' => $this->l10n->t(
Expand Down
34 changes: 3 additions & 31 deletions lib/Hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

use OC\Files\Filesystem;
use OCA\Guests\AppInfo\Application;
use OCA\Guests\Service\InviteService;
use OCA\Guests\Storage\ReadOnlyJail;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\IAppContainer;
use OCP\Constants;
use OCP\Files\Storage\IStorage;
Expand All @@ -36,6 +36,7 @@ public function __construct(
private UserBackend $userBackend,
private IAppContainer $container,
private TransferService $transferService,
private InviteService $inviteService,
) {
}

Expand Down Expand Up @@ -76,38 +77,9 @@ public function handlePostShare(ShareCreatedEvent $event): void {
$this->logger->debug("checking if '$shareWith' has a password",
['app' => Application::APP_ID]);


$passwordToken = $this->config->getUserValue(
$shareWith,
'core',
'lostpassword',
null
);

$uid = $user->getUID();

try {
if ($passwordToken) {
// user has not yet activated his account

$decryptedToken = $this->crypto->decrypt($passwordToken, strtolower($targetUser->getEMailAddress()) . $this->config->getSystemValue('secret'));
[, $token] = explode(':', $decryptedToken);
$lang = $this->config->getUserValue($targetUser->getUID(), 'core', 'lang', '');
// send invitation
$this->mail->sendGuestInviteMail(
$uid,
$shareWith,
$share,
$token,
$lang
);
$share->setMailSend(false);
}
} catch (DoesNotExistException $ex) {
$this->logger->error("'$shareWith' does not exist", ['app' => Application::APP_ID]);
} catch (\Exception $e) {
$this->logger->error('Failed to send guest activation mail', ['app' => Application::APP_ID, 'exception' => $e]);
}
$this->inviteService->sendInvite($uid, $shareWith, $share);
}

public function setupReadonlyFilesystem(array $params): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,30 @@

namespace OCA\Guests\Listener;

use OCA\Guests\Config;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

/**
* @template-implements IEventListener<Event>
*/
class TalkIntegrationListener implements IEventListener {
class BeforeTemplateRenderedListener implements IEventListener {
public function __construct(
private Config $config,
private IInitialState $initialState,
) {
}

public function handle(Event $event): void {
if (!$event instanceof BeforeTemplateRenderedEvent) {
return;
}

$this->initialState->provideInitialState('canCreateGuests', $this->config->canCreateGuests());

if (!$event->isLoggedIn() || $event->getResponse()->getTemplateName() !== 'index') {
return;
}
Expand Down
110 changes: 75 additions & 35 deletions lib/Mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use OCP\Defaults;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
Expand Down Expand Up @@ -40,53 +41,83 @@ public function __construct(
* @param $uid
* @throws \Exception
*/
public function sendGuestInviteMail(string $uid, string $shareWith, Share\IShare $share, string $token, string $language = ''): void {
public function sendGuestInviteMail(string $uid, string $guest, string $token, string $language = '', ?Share\IShare $share = null): void {
if ($language === '') {
$language = null;
}
$l10n = $this->l10nFactory->get('guests', $language);

$passwordLink = $this->urlGenerator->linkToRouteAbsolute(
'core.lost.resetform',
['userId' => $shareWith, 'token' => $token]
['userId' => $guest, 'token' => $token]
);

$this->logger->debug("sending invite to $shareWith: $passwordLink", ['app' => 'guests']);

$targetUser = $this->userManager->get($shareWith);
$shareWithEmail = $targetUser->getEMailAddress();
if (!$shareWithEmail) {
$targetUser = $this->userManager->get($guest);
$guestEmail = $targetUser->getEMailAddress();
if (!$guestEmail) {
throw new \Exception('Guest user created without email');
}
$replyTo = $this->userManager->get($uid)->getEMailAddress();
$senderDisplayName = $this->userSession->getUser()->getDisplayName();

if (empty($share)) {
[ $subject, $emailTemplate ] = $this->composeInviteMessage($senderDisplayName, $guestEmail, $passwordLink, $l10n);
} else {
[ $subject, $emailTemplate ] = $this->composeShareMessage($share, $senderDisplayName, $guestEmail, $passwordLink, $l10n);
}

try {
$message = $this->mailer->createMessage();
$message->setTo([$guestEmail => $targetUser->getDisplayName()]);
$message->setSubject($subject);
$message->setHtmlBody($emailTemplate->renderHtml());
$message->setPlainBody($emailTemplate->renderText());
$message->setFrom([
Util::getDefaultEmailAddress('sharing-noreply')
=> $l10n->t('%s via %s', [
$senderDisplayName,
$this->defaults->getName()
]),
]);

if (!is_null($replyTo)) {
$message->setReplyTo([$replyTo]);
}

$this->mailer->send($message);
} catch (\Exception $e) {
throw new \Exception($l10n->t(
'Couldn\'t send reset email. Please contact your administrator.'
));
}
}

private function composeShareMessage(Share\IShare $share, string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array {
$filename = trim($share->getTarget(), '/');
$subject = $l10n->t('%s shared »%s« with you', [$senderDisplayName, $filename]);
$subject = $l10n->t('%s shared a file with you', [$senderDisplayName]);
$expiration = $share->getExpirationDate();

$link = $this->urlGenerator->linkToRouteAbsolute(
'files.viewcontroller.showFile', ['fileid' => $share->getNodeId(), 'direct' => 1]
);

$emailTemplate = $this->mailer->createEMailTemplate('guest.invite');
$emailTemplate = $this->mailer->createEMailTemplate('guest.share');

$emailTemplate->addHeader();
$emailTemplate->addHeading($l10n->t('Incoming share'));
$emailTemplate->addHeading($l10n->t('%s shared a file with you', [$senderDisplayName]));

$emailTemplate->addBodyText(
$l10n->t('Hey there,')
);

$emailTemplate->addBodyText(
$l10n->t('%s just shared »%s« with you.', [$senderDisplayName, $filename])
$l10n->t('%s just invited you and shared »%s« with you.', [$senderDisplayName, $filename])
);

$emailTemplate->addBodyText(
$l10n->t('You can access the shared file by activating your guest account.')
);
$emailTemplate->addBodyText(
$l10n->t('After your account is activated you can view the share by logging in with %s.', [$shareWithEmail])
$l10n->t('After your account is activated you can view the share by logging in with %s.', [$guestEmail])
);

if ($expiration) {
Expand All @@ -104,29 +135,38 @@ public function sendGuestInviteMail(string $uid, string $shareWith, Share\IShare
);
$emailTemplate->addFooter();

try {
$message = $this->mailer->createMessage();
$message->setTo([$shareWithEmail => $targetUser->getDisplayName()]);
$message->setSubject($subject);
$message->setHtmlBody($emailTemplate->renderHtml());
$message->setPlainBody($emailTemplate->renderText());
$message->setFrom([
Util::getDefaultEmailAddress('sharing-noreply')
=> $l10n->t('%s via %s', [
$senderDisplayName,
$this->defaults->getName()
]),
]);
return [ $subject, $emailTemplate ];
}

if (!is_null($replyTo)) {
$message->setReplyTo([$replyTo]);
}
private function composeInviteMessage(string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array {
$subject = $l10n->t('%s invited you as a guest', [$senderDisplayName]);

$this->mailer->send($message);
} catch (\Exception $e) {
throw new \Exception($l10n->t(
'Couldn\'t send reset email. Please contact your administrator.'
));
}
$emailTemplate = $this->mailer->createEMailTemplate('guest.invite');

$emailTemplate->addHeader();
$emailTemplate->addHeading($l10n->t('You have been invited'));

$emailTemplate->addBodyText(
$l10n->t('Hey there,')
);

$emailTemplate->addBodyText(
$l10n->t('%s just invited you.', [$senderDisplayName])
);

$emailTemplate->addBodyText(
$l10n->t('You can activate your guest account with the button below.')
);
$emailTemplate->addBodyText(
$l10n->t('After your account is activated you can log in with %s.', [$guestEmail])
);

$emailTemplate->addBodyButton(
$l10n->t('Activate account'),
$passwordLink
);
$emailTemplate->addFooter();

return [ $subject, $emailTemplate ];
}
}
60 changes: 60 additions & 0 deletions lib/Service/InviteService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Guests\Service;

use OCA\Guests\Mail;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IConfig;
use OCP\Security\ICrypto;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;

class InviteService {
public function __construct(
private LoggerInterface $logger,
private IConfig $config,
private ICrypto $crypto,
private Mail $mail,
) {
}

public function sendInvite(string $userId, string $guest, ?IShare $share = null): bool {
$passwordToken = $this->config->getUserValue($guest, 'core', 'lostpassword', null);

if (!$passwordToken) {
return false;
}

try {
// user has not yet activated his account
$decryptedToken = $this->crypto->decrypt($passwordToken, strtolower($guest) . $this->config->getSystemValue('secret'));
[, $token] = explode(':', $decryptedToken);
$lang = $this->config->getUserValue($guest, 'core', 'lang', '');
// send invitation
$this->mail->sendGuestInviteMail(
$userId,
$guest,
$token,
$lang,
$share
);

if ($share) {
$share->setMailSend(false);
}
} catch (DoesNotExistException $ex) {
$this->logger->error("'$guest' does not exist");
} catch (\Exception $e) {
$this->logger->error('Failed to send guest activation mail', ['exception' => $e]);
}

return true;
}
}
Loading
Loading