diff --git a/appinfo/info.xml b/appinfo/info.xml
index bf39775b75ae0ef44c8dfdb474ea8ea983a7daa1..318e406401361ee2ff7a3a4f72b22868fd70b15e 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -20,4 +20,7 @@
OCA\EmailRecovery\BackgroundJob\WeeklyRecoveryNotificationJob
+
+ OCA\EmailRecovery\Command\UpdateBlacklistedDomains
+
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 332e94a7a7ada01a4cbee9ba5ec990e1954364ee..277036d79f41b6af0ea4927c17d09de91d8125f0 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -28,6 +28,8 @@ namespace OCA\EmailRecovery\AppInfo;
use OCA\EmailRecovery\Listeners\BeforeTemplateRenderedListener;
use OCA\EmailRecovery\Listeners\UserConfigChangedListener;
+use OCA\EmailRecovery\Listeners\UserChangedListener;
+use OCA\EmailRecovery\Listeners\BeforeUserRegisteredListener;
use OCA\EmailRecovery\Notification\Notifier;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -36,6 +38,8 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\Notification\IManager as INotificationManager;
use OCP\User\Events\UserConfigChangedEvent;
+use OCP\User\Events\UserChangedEvent;
+use OCA\EcloudAccounts\Event\BeforeUserRegisteredEvent;
class Application extends App implements IBootstrap {
public const APP_ID = 'email-recovery';
@@ -45,8 +49,10 @@ class Application extends App implements IBootstrap {
}
public function register(IRegistrationContext $context): void {
+ $context->registerEventListener(BeforeUserRegisteredEvent::class, BeforeUserRegisteredListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(UserConfigChangedEvent::class, UserConfigChangedListener::class);
+ $context->registerEventListener(UserChangedEvent::class, UserChangedListener::class);
}
public function boot(IBootContext $context): void {
$context->injectFn([$this, 'registerNotifier']);
diff --git a/lib/Command/UpdateBlacklistedDomains.php b/lib/Command/UpdateBlacklistedDomains.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0bbed834579126e66b6e8be992a2239a1b452d6
--- /dev/null
+++ b/lib/Command/UpdateBlacklistedDomains.php
@@ -0,0 +1,39 @@
+blackListService = $blackListService;
+ $this->logger = $logger;
+ }
+
+ protected function configure() {
+ $this->setName(Application::APP_ID.':update-blacklisted-domains')->setDescription('Update blacklisted domains');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $this->blackListService->updateBlacklistedDomains();
+ $output->writeln('Updated blacklisted domains for creation.');
+ } catch (\Throwable $th) {
+ $this->logger->error('Error while updating blacklisted domains. ' . $th->getMessage());
+ $output->writeln('Error while updating blacklisted domains. '. $th->getMessage());
+ }
+ return 1;
+ }
+}
diff --git a/lib/Controller/EmailRecoveryController.php b/lib/Controller/EmailRecoveryController.php
index d572ed5ab32ad5baf6cfd99be6dadb9c3611acec..d9230c65595f0a82aeded45ab35b6145bb2e4d9e 100644
--- a/lib/Controller/EmailRecoveryController.php
+++ b/lib/Controller/EmailRecoveryController.php
@@ -41,9 +41,8 @@ use OCP\ILogger;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
-use OCP\Security\ICrypto;
use OCP\Security\VerificationToken\InvalidTokenException;
-use OCP\Security\VerificationToken\IVerificationToken;
+use OCA\EmailRecovery\AppInfo\Application;
class EmailRecoveryController extends Controller {
private IConfig $config;
@@ -52,11 +51,9 @@ class EmailRecoveryController extends Controller {
private IUserSession $userSession;
private RecoveryEmailService $recoveryEmailService;
private IUserManager $userManager;
- private IVerificationToken $verificationToken;
- private ICrypto $crypto;
public function __construct(
- string $appName,
+ string $appName,
IRequest $request,
IConfig $config,
ILogger $logger,
@@ -64,8 +61,6 @@ class EmailRecoveryController extends Controller {
IUserSession $userSession,
RecoveryEmailService $recoveryEmailService,
IUserManager $userManager,
- IVerificationToken $verificationToken,
- ICrypto $crypto
) {
parent::__construct($appName, $request);
$this->config = $config;
@@ -74,8 +69,6 @@ class EmailRecoveryController extends Controller {
$this->userSession = $userSession;
$this->recoveryEmailService = $recoveryEmailService;
$this->userManager = $userManager;
- $this->verificationToken = $verificationToken;
- $this->crypto = $crypto;
}
/**
@@ -85,8 +78,8 @@ class EmailRecoveryController extends Controller {
public function getRecoveryEmail() {
$response = new JSONResponse();
$userId = $this->userSession->getUser()->getUID();
- $recoveryEmail = $this->config->getUserValue($userId, $this->appName, 'recovery-email');
- $unverifiedRecoveryEmail = $this->config->getUserValue($userId, $this->appName, 'unverified-recovery-email');
+ $recoveryEmail = $this->config->getUserValue($userId, Application::APP_ID, 'recovery-email');
+ $unverifiedRecoveryEmail = $this->config->getUserValue($userId, Application::APP_ID, 'unverified-recovery-email');
$data = ["recoveryEmail" => $recoveryEmail, "unverifiedRecoveryEmail" => $unverifiedRecoveryEmail];
$response->setData($data);
return $response;
@@ -188,25 +181,14 @@ class EmailRecoveryController extends Controller {
}
} catch (Exception $e) {
$response->setStatus(500);
- if ($e instanceof InvalidRecoveryEmailException) {
+ if ($e instanceof InvalidRecoveryEmailException ||
+ $e instanceof SameRecoveryEmailAsEmailException ||
+ $e instanceof RecoveryEmailAlreadyFoundException ||
+ $e instanceof MurenaDomainDisallowedException ||
+ $e instanceof BlacklistedEmailException
+ ) {
$response->setStatus(400);
- $response->setData(['message' => $this->l->t('Invalid Recovery Email')]);
- }
- if ($e instanceof SameRecoveryEmailAsEmailException) {
- $response->setStatus(400);
- $response->setData(['message' => $this->l->t('Error! User email address cannot be saved as recovery email address!')]);
- }
- if ($e instanceof RecoveryEmailAlreadyFoundException) {
- $response->setStatus(400);
- $response->setData(['message' => $this->l->t('Recovery email address is already taken.')]);
- }
- if ($e instanceof MurenaDomainDisallowedException) {
- $response->setStatus(400);
- $response->setData(['message' => $this->l->t('You cannot set an email address with a Murena domain as recovery email address.')]);
- }
- if ($e instanceof BlacklistedEmailException) {
- $response->setStatus(400);
- $response->setData(['message' => $this->l->t('The domain of this email address is blacklisted. Please provide another recovery address.')]);
+ $response->setData(['message' => $e->getMessage()]);
}
}
}
diff --git a/lib/Listeners/BeforeUserRegisteredListener.php b/lib/Listeners/BeforeUserRegisteredListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e598bfcef53ac4fc5a82092d42e3ca1bf1e16ca
--- /dev/null
+++ b/lib/Listeners/BeforeUserRegisteredListener.php
@@ -0,0 +1,44 @@
+recoveryEmailService = $recoveryEmailService;
+ }
+
+ /**
+ * @param Event $event
+ */
+ public function handle(Event $event): void {
+ if ($event instanceof BeforeUserRegisteredEvent) {
+ $this->validateRecoveryEmail($event->getRecoveryEmail(), $event->getLanguage());
+ }
+ }
+
+ /**
+ * @param string $recoveryEmail
+ * @param string $language
+ */
+ protected function validateRecoveryEmail(string $recoveryEmail, string $language): void {
+ try {
+ $this->recoveryEmailService->validateRecoveryEmail('', $recoveryEmail, $language);
+ } catch (\Exception $e) {
+ throw new RecoveryEmailValidationException($e->getMessage());
+ }
+ }
+}
diff --git a/lib/Listeners/UserChangedListener.php b/lib/Listeners/UserChangedListener.php
new file mode 100644
index 0000000000000000000000000000000000000000..afc0558c5ceb643b45f25c17bb48b1d27ff6b24e
--- /dev/null
+++ b/lib/Listeners/UserChangedListener.php
@@ -0,0 +1,38 @@
+LDAPConnectionService = $LDAPConnectionService;
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof UserChangedEvent)) {
+ return;
+ }
+
+ $feature = $event->getFeature();
+ $user = $event->getUser();
+ $username = $user->getUID();
+
+ if ($feature === self::RECOVERY_EMAIL_FEATURE) {
+ $recoveryEmail = $event->getValue();
+ $recoveryEmailAttribute = [
+ 'recoveryMailAddress' => $recoveryEmail
+ ];
+
+ $this->LDAPConnectionService->updateAttributesInLDAP($username, $recoveryEmailAttribute);
+ }
+ }
+}
diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php
new file mode 100644
index 0000000000000000000000000000000000000000..891cf62f9368b163a7c19b96fc40cc52a667e391
--- /dev/null
+++ b/lib/Service/BlackListService.php
@@ -0,0 +1,138 @@
+logger = $logger;
+ $this->l10nFactory = $l10nFactory;
+ $this->LDAPConnectionService = $LDAPConnectionService;
+ $this->appData = $appData;
+ $this->appName = $appName;
+ }
+
+ public function mapActiveAttributesInLDAP(string $username, bool $isEnabled): void {
+ $userActiveAttributes = $this->getActiveAttributes($isEnabled);
+ $this->LDAPConnectionService->updateAttributesInLDAP($username, $userActiveAttributes);
+ }
+
+ private function getActiveAttributes(bool $isEnabled): array {
+ return [
+ 'active' => $isEnabled ? 'TRUE' : 'FALSE',
+ 'mailActive' => $isEnabled ? 'TRUE' : 'FALSE',
+ ];
+ }
+ /**
+ * Check if an email domain is blacklisted against a JSON list of disposable email domains.
+ *
+ * @param string $email The email address to check.
+ * @return bool True if the email domain is blacklisted, false otherwise.
+ */
+ public function isBlacklistedEmail(string $email): bool {
+ if (!$this->ensureDocumentsFolder()) {
+ return false;
+ }
+ $blacklistedDomains = $this->getBlacklistedDomainData();
+ if (empty($blacklistedDomains)) {
+ return false;
+ }
+ $emailParts = explode('@', $email);
+ $emailDomain = strtolower(end($emailParts));
+ return in_array($emailDomain, $blacklistedDomains);
+ }
+ /**
+ * Update the blacklisted domains data by fetching it from a URL and saving it locally.
+ *
+ * @return void
+ */
+ public function updateBlacklistedDomains(): void {
+ $blacklisted_domain_url = self::BLACKLISTED_DOMAINS_URL;
+ $json_data = file_get_contents($blacklisted_domain_url);
+ $this->setBlacklistedDomainsData($json_data);
+ }
+ /**
+ * Store blacklisted domain data in a file within AppData.
+ *
+ * @param string $data The data to be stored in the file.
+ * @return void
+ */
+ private function setBlacklistedDomainsData(string $data): void {
+ $file = $this->getBlacklistedDomainsFile();
+ $file->putContent($data);
+ }
+ /**
+ * Retrieve the blacklisted domain file path
+ *
+ * @return ISimpleFile
+ */
+ private function getBlacklistedDomainsFile(): ISimpleFile {
+ try {
+ $currentFolder = $this->appData->getFolder('/');
+ } catch (NotFoundException $e) {
+ $currentFolder = $this->appData->newFolder('/');
+ }
+ $filename = self::BLACKLISTED_DOMAINS_FILE_NAME;
+ if ($currentFolder->fileExists($filename)) {
+ return $currentFolder->getFile($filename);
+ }
+ return $currentFolder->newFile($filename);
+ }
+ /**
+ * Retrieve the blacklisted domain data.
+ *
+ * @return array The array of blacklisted domains.
+ */
+ public function getBlacklistedDomainData(): array {
+ $document = self::BLACKLISTED_DOMAINS_FILE_NAME;
+ $file = $this->getBlacklistedDomainsFile();
+ try {
+ $blacklistedDomainsInJson = $file->getContent();
+ if (empty($blacklistedDomainsInJson)) {
+ return [];
+ }
+ return json_decode($blacklistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR);
+ } catch (NotFoundException $e) {
+ $this->logger->warning('Blacklisted domains file ' . $document . ' not found!');
+ return [];
+ } catch (\Throwable $e) {
+ $this->logger->warning('Error decoding blacklisted domains file ' . $document . ': ' . $e->getMessage());
+ return [];
+ }
+ }
+
+ /**
+ * Ensure the specified folder exists within AppData.
+ *
+ * @return bool
+ */
+ private function ensureDocumentsFolder(): bool {
+ try {
+ $this->appData->getFolder('/');
+ } catch (NotFoundException $e) {
+ $this->logger->error($this->appName . ' AppData folder not found!');
+ return false;
+ } catch (\RuntimeException $e) {
+ $this->logger->error($this->appName . ' AppData folder not found! Runtime Error: ' . $e->getMessage());
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php
index 478d28bfaf16414bbd99fa2f804c32a1ee1b7f36..d6892c81cd2efd836d37d460b400a1abcb2579e9 100644
--- a/lib/Service/RecoveryEmailService.php
+++ b/lib/Service/RecoveryEmailService.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace OCA\EmailRecovery\Service;
use Exception;
-use OCA\EcloudAccounts\Service\LDAPConnectionService;
use OCA\EmailRecovery\Exception\BlacklistedEmailException;
use OCA\EmailRecovery\Exception\InvalidRecoveryEmailException;
use OCA\EmailRecovery\Exception\MurenaDomainDisallowedException;
@@ -24,7 +23,8 @@ use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Security\VerificationToken\IVerificationToken;
use OCP\Util;
-use OCA\EcloudAccounts\Service\BlackListService;
+use OCP\IL10N;
+use OCA\EcloudAccounts\Service\LDAPConnectionService;
class RecoveryEmailService {
private ILogger $logger;
@@ -42,8 +42,9 @@ class RecoveryEmailService {
protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes
private const ATTEMPT_KEY = "recovery_email_attempts";
private BlackListService $blackListService;
+ private IL10N $l;
- public function __construct(string $appName, ILogger $logger, IConfig $config, LDAPConnectionService $LDAPConnectionService, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService) {
+ public function __construct(string $appName, ILogger $logger, IConfig $config, LDAPConnectionService $LDAPConnectionService, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l) {
$this->logger = $logger;
$this->config = $config;
$this->appName = $appName;
@@ -57,6 +58,7 @@ class RecoveryEmailService {
$this->verificationToken = $verificationToken;
$this->curl = $curlService;
$this->blackListService = $blackListService;
+ $this->l = $l;
$commonServiceURL = $this->config->getSystemValue('common_services_url', '');
if (!empty($commonServiceURL)) {
@@ -107,29 +109,33 @@ class RecoveryEmailService {
return true;
}
- public function validateRecoveryEmail(string $username, string $recoveryEmail) : bool {
- $user = $this->userManager->get($username);
- $email = $user->getEMailAddress();
+ public function validateRecoveryEmail(string $username = '', string $recoveryEmail, string $language = 'en') : bool {
+ $email = '';
+ if ($username != '') {
+ $user = $this->userManager->get($username);
+ $email = $user->getEMailAddress();
+ }
+ $l = $this->l10nFactory->get($this->appName, $language);
if (!empty($recoveryEmail)) {
- if (!filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL)) {
+ if (!$this->isValidEmailFormat($recoveryEmail)) {
$this->logger->info("User $username's requested recovery email does not match email format");
- throw new InvalidRecoveryEmailException();
+ throw new InvalidRecoveryEmailException($l->t('Invalid Recovery Email'));
}
- if (strcmp($recoveryEmail, $email) === 0) {
+ if ($email != '' && strcmp($recoveryEmail, $email) === 0) {
$this->logger->info("User ID $username's requested recovery email is the same as email");
- throw new SameRecoveryEmailAsEmailException();
+ throw new SameRecoveryEmailAsEmailException($l->t('Error! User email address cannot be saved as recovery email address!'));
}
if ($this->isRecoveryEmailTaken($username, $recoveryEmail)) {
$this->logger->info("User ID $username's requested recovery email address is already taken");
- throw new RecoveryEmailAlreadyFoundException();
+ throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.'));
}
if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) {
$this->logger->info("User ID $username's requested recovery email address is disallowed.");
- throw new MurenaDomainDisallowedException();
+ throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.'));
}
if ($this->blackListService->isBlacklistedEmail($recoveryEmail)) {
$this->logger->info("User ID $username's requested recovery email address domain is blacklisted. Please provide another recovery address.");
- throw new BlacklistedEmailException();
+ throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.'));
}
}
return true;
@@ -157,12 +163,12 @@ class RecoveryEmailService {
return false;
}
- $usersWithEmailRecovery = $this->config->getUsersForUserValue('email-recovery', 'recovery-email', $recoveryEmail);
+ $usersWithEmailRecovery = $this->config->getUsersForUserValue($this->appName, 'recovery-email', $recoveryEmail);
if (count($usersWithEmailRecovery)) {
return true;
}
- $usersWithUnverifiedRecovery = $this->config->getUsersForUserValue('email-recovery', 'unverified-recovery-email', $recoveryEmail);
+ $usersWithUnverifiedRecovery = $this->config->getUsersForUserValue($this->appName, 'unverified-recovery-email', $recoveryEmail);
if (count($usersWithUnverifiedRecovery)) {
return true;
}
@@ -305,4 +311,14 @@ class RecoveryEmailService {
$this->manageEmailRestriction($email, 'DELETE', $url);
}
+ /**
+ * Check if a recovery email address is in valid format
+ *
+ * @param string $recoveryEmail The recovery email address to check.
+ *
+ * @return bool True if the recovery email address is valid, false otherwise.
+ */
+ public function isValidEmailFormat(string $recoveryEmail): bool {
+ return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false;
+ }
}