From c50fe2a9c9786247ef31aad855813b377ed33e89 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Tue, 2 Jul 2019 14:58:48 +0200 Subject: [PATCH] Send emails when remote wipe starts/finishes Signed-off-by: Christoph Wurst --- core/Application.php | 14 +- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Listeners/RemoteWipeEmailListener.php | 171 +++++++++++++ .../Listeners/RemoteWipeEmailListenerTest.php | 241 ++++++++++++++++++ 5 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 lib/private/Authentication/Listeners/RemoteWipeEmailListener.php create mode 100644 tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php diff --git a/core/Application.php b/core/Application.php index 97ebae774d..9655a8e1a4 100644 --- a/core/Application.php +++ b/core/Application.php @@ -31,6 +31,7 @@ namespace OC\Core; use OC\Authentication\Events\RemoteWipeFinished; use OC\Authentication\Events\RemoteWipeStarted; use OC\Authentication\Listeners\RemoteWipeActivityListener; +use OC\Authentication\Listeners\RemoteWipeEmailListener; use OC\Authentication\Listeners\RemoteWipeNotificationsListener; use OC\Authentication\Notifications\Notifier as AuthenticationNotifier; use OC\Core\Notification\RemoveLinkSharesNotifier; @@ -64,19 +65,19 @@ class Application extends App { $eventDispatcher = $server->query(IEventDispatcher::class); $notificationManager = $server->getNotificationManager(); - $notificationManager->registerNotifier(function() use ($server) { + $notificationManager->registerNotifier(function () use ($server) { return new RemoveLinkSharesNotifier( $server->getL10NFactory() ); - }, function() { + }, function () { return [ 'id' => 'core', 'name' => 'core', ]; }); - $notificationManager->registerNotifier(function() use ($server) { + $notificationManager->registerNotifier(function () use ($server) { return $server->query(AuthenticationNotifier::class); - }, function() { + }, function () { return [ 'id' => 'auth', 'name' => 'authentication notifier', @@ -84,7 +85,7 @@ class Application extends App { }); $eventDispatcher->addListener(IDBConnection::CHECK_MISSING_INDEXES_EVENT, - function(GenericEvent $event) use ($container) { + function (GenericEvent $event) use ($container) { /** @var MissingIndexInformation $subject */ $subject = $event->getSubject(); @@ -165,7 +166,10 @@ class Application extends App { $eventDispatcher->addServiceListener(RemoteWipeStarted::class, RemoteWipeActivityListener::class); $eventDispatcher->addServiceListener(RemoteWipeStarted::class, RemoteWipeNotificationsListener::class); + $eventDispatcher->addServiceListener(RemoteWipeStarted::class, RemoteWipeEmailListener::class); $eventDispatcher->addServiceListener(RemoteWipeFinished::class, RemoteWipeActivityListener::class); $eventDispatcher->addServiceListener(RemoteWipeFinished::class, RemoteWipeNotificationsListener::class); + $eventDispatcher->addServiceListener(RemoteWipeFinished::class, RemoteWipeEmailListener::class); } + } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 80c03a6e63..f608ce0b77 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -522,6 +522,7 @@ return array( 'OC\\Authentication\\Exceptions\\UserAlreadyLoggedInException' => $baseDir . '/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php', 'OC\\Authentication\\Exceptions\\WipeTokenException' => $baseDir . '/lib/private/Authentication/Exceptions/WipeTokenException.php', 'OC\\Authentication\\Listeners\\RemoteWipeActivityListener' => $baseDir . '/lib/private/Authentication/Listeners/RemoteWipeActivityListener.php', + 'OC\\Authentication\\Listeners\\RemoteWipeEmailListener' => $baseDir . '/lib/private/Authentication/Listeners/RemoteWipeEmailListener.php', 'OC\\Authentication\\Listeners\\RemoteWipeNotificationsListener' => $baseDir . '/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php', 'OC\\Authentication\\LoginCredentials\\Credentials' => $baseDir . '/lib/private/Authentication/LoginCredentials/Credentials.php', 'OC\\Authentication\\LoginCredentials\\Store' => $baseDir . '/lib/private/Authentication/LoginCredentials/Store.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 0eaa457464..fc4fc585e6 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -556,6 +556,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Exceptions\\UserAlreadyLoggedInException' => __DIR__ . '/../../..' . '/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php', 'OC\\Authentication\\Exceptions\\WipeTokenException' => __DIR__ . '/../../..' . '/lib/private/Authentication/Exceptions/WipeTokenException.php', 'OC\\Authentication\\Listeners\\RemoteWipeActivityListener' => __DIR__ . '/../../..' . '/lib/private/Authentication/Listeners/RemoteWipeActivityListener.php', + 'OC\\Authentication\\Listeners\\RemoteWipeEmailListener' => __DIR__ . '/../../..' . '/lib/private/Authentication/Listeners/RemoteWipeEmailListener.php', 'OC\\Authentication\\Listeners\\RemoteWipeNotificationsListener' => __DIR__ . '/../../..' . '/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php', 'OC\\Authentication\\LoginCredentials\\Credentials' => __DIR__ . '/../../..' . '/lib/private/Authentication/LoginCredentials/Credentials.php', 'OC\\Authentication\\LoginCredentials\\Store' => __DIR__ . '/../../..' . '/lib/private/Authentication/LoginCredentials/Store.php', diff --git a/lib/private/Authentication/Listeners/RemoteWipeEmailListener.php b/lib/private/Authentication/Listeners/RemoteWipeEmailListener.php new file mode 100644 index 0000000000..be43174738 --- /dev/null +++ b/lib/private/Authentication/Listeners/RemoteWipeEmailListener.php @@ -0,0 +1,171 @@ + + * + * @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 . + */ + +namespace OC\Authentication\Listeners; + +use Exception; +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory as IL10nFactory; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use function substr; + +class RemoteWipeEmailListener implements IEventListener { + + /** @var IMailer */ + private $mailer; + + /** @var IUserManager */ + private $userManager; + + /** @var IL10N */ + private $l10n; + + /** @var ILogger */ + private $logger; + + public function __construct(IMailer $mailer, + IUserManager $userManager, + IL10nFactory $l10nFactory, + ILogger $logger) { + $this->mailer = $mailer; + $this->userManager = $userManager; + $this->l10n = $l10nFactory->get('core'); + $this->logger = $logger; + } + + /** + * @param Event $event + */ + public function handle(Event $event): void { + if ($event instanceof RemoteWipeStarted) { + $uid = $event->getToken()->getUID(); + $user = $this->userManager->get($uid); + if ($user === null) { + $this->logger->warning("not sending a wipe started email because user <$uid> does not exist (anymore)"); + return; + } + if ($user->getEMailAddress() === null) { + $this->logger->info("not sending a wipe started email because user <$uid> has no email set"); + return; + } + + try { + $this->mailer->send( + $this->getWipingStartedMessage($event, $user) + ); + } catch (Exception $e) { + $this->logger->logException($e, [ + 'message' => "Could not send remote wipe started email to <$uid>", + 'level' => ILogger::ERROR, + ]); + } + } else if ($event instanceof RemoteWipeFinished) { + $uid = $event->getToken()->getUID(); + $user = $this->userManager->get($uid); + if ($user === null) { + $this->logger->warning("not sending a wipe finished email because user <$uid> does not exist (anymore)"); + return; + } + if ($user->getEMailAddress() === null) { + $this->logger->info("not sending a wipe finished email because user <$uid> has no email set"); + return; + } + + try { + $this->mailer->send( + $this->getWipingFinishedMessage($event, $user) + ); + } catch (Exception $e) { + $this->logger->logException($e, [ + 'message' => "Could not send remote wipe finished email to <$uid>", + 'level' => ILogger::ERROR, + ]); + } + } + } + + private function getWipingStartedMessage(RemoteWipeStarted $event, IUser $user): IMessage { + $message = $this->mailer->createMessage(); + $emailTemplate = $this->mailer->createEMailTemplate('auth.RemoteWipeStarted'); + $plainHeading = $this->l10n->t('Wiping of device %s has started', [$event->getToken()->getName()]); + $htmlHeading = $this->l10n->t('Wiping of device »%s« has started', [$event->getToken()->getName()]); + $emailTemplate->setSubject( + $this->l10n->t( + '»%s« started remote wipe', + [ + substr($event->getToken()->getName(), 0, 15) + ] + ) + ); + $emailTemplate->addHeader(); + $emailTemplate->addHeading( + $htmlHeading, + $plainHeading + ); + $emailTemplate->addBodyText( + $this->l10n->t('Device or application »%s« has started the remote wipe process. You will receive another email once the process has finished', [$event->getToken()->getName()]) + ); + $emailTemplate->addFooter(); + $message->setTo([$user->getEMailAddress()]); + $message->useTemplate($emailTemplate); + + return $message; + } + + private function getWipingFinishedMessage(RemoteWipeFinished $event, IUser $user): IMessage { + $message = $this->mailer->createMessage(); + $emailTemplate = $this->mailer->createEMailTemplate('auth.RemoteWipeFinished'); + $plainHeading = $this->l10n->t('Wiping of device %s has finished', [$event->getToken()->getName()]); + $htmlHeading = $this->l10n->t('Wiping of device »%s« has finished', [$event->getToken()->getName()]); + $emailTemplate->setSubject( + $this->l10n->t( + '»%s« finished remote wipe', + [ + substr($event->getToken()->getName(), 0, 15) + ] + ) + ); + $emailTemplate->addHeader(); + $emailTemplate->addHeading( + $htmlHeading, + $plainHeading + ); + $emailTemplate->addBodyText( + $this->l10n->t('Device or application »%s« has finished the remote wipe process.', [$event->getToken()->getName()]) + ); + $emailTemplate->addFooter(); + $message->setTo([$user->getEMailAddress()]); + $message->useTemplate($emailTemplate); + + return $message; + } + +} diff --git a/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php b/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php new file mode 100644 index 0000000000..ea7740057b --- /dev/null +++ b/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php @@ -0,0 +1,241 @@ + + * + * @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 . + */ + +namespace lib\Authentication\Listeners; + +use Exception; +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Listeners\RemoteWipeEmailListener; +use OC\Authentication\Token\IToken; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RemoteWipeEmailListenerTest extends TestCase { + + /** @var IMailer|MockObject */ + private $mailer; + + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var IFactory|MockObject */ + private $l10nFactory; + + /** @var IL10N|MockObject */ + private $l10n; + + /** @var ILogger|MockObject */ + private $logger; + + /** @var IEventListener */ + private $listener; + + protected function setUp() { + parent::setUp(); + + $this->mailer = $this->createMock(IMailer::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->logger = $this->createMock(ILogger::class); + + $this->l10nFactory->method('get')->with('core')->willReturn($this->l10n); + $this->l10n->method('t')->willReturnArgument(0); + + $this->listener = new RemoteWipeEmailListener( + $this->mailer, + $this->userManager, + $this->l10nFactory, + $this->logger + ); + } + + + public function testHandleUnrelated() { + $event = new Event(); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedInvalidUser() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedNoEmailSet() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedTransmissionError() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $this->mailer->expects($this->once()) + ->method('send') + ->willThrowException(new Exception()); + $this->logger->expects($this->once()) + ->method('logException'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStarted() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $message = $this->createMock(IMessage::class); + $this->mailer->expects($this->once()) + ->method('createMessage') + ->willReturn($message); + $message->expects($this->once()) + ->method('setTo') + ->with($this->equalTo(['user@domain.org'])); + $this->mailer->expects($this->once()) + ->method('send') + ->with($message); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedInvalidUser() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedNoEmailSet() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedTransmissionError() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $this->mailer->expects($this->once()) + ->method('send') + ->willThrowException(new Exception()); + $this->logger->expects($this->once()) + ->method('logException'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinished() { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $message = $this->createMock(IMessage::class); + $this->mailer->expects($this->once()) + ->method('createMessage') + ->willReturn($message); + $message->expects($this->once()) + ->method('setTo') + ->with($this->equalTo(['user@domain.org'])); + $this->mailer->expects($this->once()) + ->method('send') + ->with($message); + + $this->listener->handle($event); + } + +}