diff --git a/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php b/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php new file mode 100644 index 0000000000000..9f534ec9113c4 --- /dev/null +++ b/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php @@ -0,0 +1,50 @@ +getRedirect('*/*/index', ['_secure' => true]); } catch (NoSuchEntityException $e) { $this->messageManager->addErrorMessage(__('Wrong email.')); + } catch (LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 1425d29c90186..26fa8966f93c1 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\ConfirmationEmailLogManagementInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; @@ -406,6 +407,11 @@ class AccountManagement implements AccountManagementInterface */ private Authenticate $authenticate; + /** + * @var ConfirmationEmailLogManagementInterface + */ + private ConfirmationEmailLogManagementInterface $confirmationEmailLogManagement; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -446,6 +452,7 @@ class AccountManagement implements AccountManagementInterface * @param Backend|null $eavValidator * @param CustomerLogger|null $customerLogger * @param Authenticate|null $authenticate + * @param ConfirmationEmailLogManagementInterface|null $confirmationEmailLogManagement * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -491,7 +498,8 @@ public function __construct( ?AuthenticationInterface $authentication = null, ?Backend $eavValidator = null, ?CustomerLogger $customerLogger = null, - ?Authenticate $authenticate = null + ?Authenticate $authenticate = null, + ?ConfirmationEmailLogManagementInterface $confirmationEmailLogManagement = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -536,6 +544,9 @@ public function __construct( $this->eavValidator = $eavValidator ?? $objectManager->get(Backend::class); $this->customerLogger = $customerLogger ?? $objectManager->get(CustomerLogger::class); $this->authenticate = $authenticate ?? $objectManager->get(Authenticate::class); + $this->confirmationEmailLogManagement = $confirmationEmailLogManagement ?? $objectManager->get( + ConfirmationEmailLogManagementInterface::class + ); } /** @@ -547,6 +558,9 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '') if (!$customer->getConfirmation()) { throw new InvalidTransitionException(__("Confirmation isn't needed.")); } + if (!$this->confirmationEmailLogManagement->canSend((int) $customer->getId())) { + throw new LocalizedException(__("You have reached the limit for confirmation emails.")); + } try { $this->getEmailNotification()->newAccount( @@ -621,6 +635,8 @@ private function activateCustomer($customer, $confirmationKey) ); } + $this->confirmationEmailLogManagement->deleteByCustomerId((int) $customer->getId()); + return $customer; } diff --git a/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php b/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php new file mode 100644 index 0000000000000..b563eb702f63a --- /dev/null +++ b/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php @@ -0,0 +1,159 @@ +resource->save($log); + return $log; + } + + /** + * @inheritdoc + */ + public function getByCustomerId(int $customerId): ?ConfirmationLogInterface + { + $log = $this->confirmationLogFactory->create(); + $this->resource->load($log, $customerId, 'customer_id'); + + return $log->getId() ? $log : null; + } + + /** + * @inheritdoc + */ + public function deleteByCustomerId(int $customerId): void + { + $log = $this->getByCustomerId($customerId); + if ($log) { + $this->resource->delete($log); + } + } + + /** + * @inheritdoc + */ + public function canSend(int $customerId): bool + { + $existingLog = $this->getByCustomerId($customerId); + + if ($existingLog) { + return $this->processExistingLog( + $existingLog, + $this->getConfig(self::XML_PATH_MAX_REQUEST), + $this->getConfig(self::XML_PATH_MIN_TIME_INTERVAL) * 60 + ); + } + + $this->addNewLogEntry($customerId); + return true; + } + + /** + * @inheritdoc + */ + public function getConfig(string $path): int + { + return (int) $this->scopeConfig->getValue($path, ScopeInterface::SCOPE_STORE); + } + + /** + * Processes an existing confirmation log by updating email counters and timestamps. + * Handles scenarios where the email limit has been exceeded. + * + * @param ConfirmationLogInterface $log + * @param int $maxEmailLimit + * @param int $minTimeBetweenEmails + * @return bool + */ + private function processExistingLog(ConfirmationLogInterface $log, int $maxEmailLimit, int $minTimeBetweenEmails): bool + { + $currentEmailCount = $log->getEmailSentCounter(); + $log->setEmailSentCounter($currentEmailCount + 1); + + if ($currentEmailCount >= $maxEmailLimit) { + return $this->handleMaxLimitReached($log, $minTimeBetweenEmails); + } + + $log->setLastEmailSentAt($this->dateTime->gmtDate()); + $this->save($log); + + return true; + } + + /** + * Handles the condition when the maximum limit of emails has been reached. + * + * @param ConfirmationLogInterface $log + * @param int $minTimeBetweenEmails + * @return bool + */ + private function handleMaxLimitReached(ConfirmationLogInterface $log, int $minTimeBetweenEmails): bool + { + $lastEmailTimestamp = strtotime($log->getLastEmailSentAt()); + $currentTimestamp = $this->dateTime->timestamp(); + + $timeDifference = $currentTimestamp - $lastEmailTimestamp; + + if ($timeDifference >= $minTimeBetweenEmails) { + $log->setEmailSentCounter(1); + $log->setLastEmailSentAt($this->dateTime->gmtDate()); + $this->save($log); + return true; + } + + return false; + } + + /** + * To add the confirmation log entry if no entry present for that customer + * + * @param int $customerId + * @return void + */ + private function addNewLogEntry(int $customerId): void + { + $confirmationLog = $this->confirmationLogFactory->create(); + $confirmationLog->setCustomerId($customerId); + $confirmationLog->setEmailSentCounter(1); + $confirmationLog->setLastEmailSentAt($this->dateTime->gmtDate()); + $this->save($confirmationLog); + } +} diff --git a/app/code/Magento/Customer/Model/ConfirmationLog.php b/app/code/Magento/Customer/Model/ConfirmationLog.php new file mode 100644 index 0000000000000..8ce29b3fbd4be --- /dev/null +++ b/app/code/Magento/Customer/Model/ConfirmationLog.php @@ -0,0 +1,69 @@ +_init(ResourceModel::class); + } + + /** + * @inheritdoc + */ + public function getCustomerId(): int + { + return (int) $this->getData(self::CUSTOMER_ID); + } + + /** + * @inheritdoc + */ + public function setCustomerId(int $customerId): ConfirmationLogInterface + { + return $this->setData(self::CUSTOMER_ID, $customerId); + } + + /** + * @inheritdoc + */ + public function getEmailSentCounter(): int + { + return (int) $this->getData(self::EMAIL_SENT_COUNTER); + } + + /** + * @inheritdoc + */ + public function setEmailSentCounter(int $counter): ConfirmationLogInterface + { + return $this->setData(self::EMAIL_SENT_COUNTER, $counter); + } + + /** + * @inheritdoc + */ + public function getLastEmailSentAt(): string + { + return $this->getData(self::LAST_EMAIL_SENT_AT); + } + + /** + * @inheritdoc + */ + public function setLastEmailSentAt(string $date): ConfirmationLogInterface + { + return $this->setData(self::LAST_EMAIL_SENT_AT, $date); + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php new file mode 100644 index 0000000000000..e391dae92cb04 --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php @@ -0,0 +1,17 @@ +_init('customer_confirmation_log', 'id'); + } +} + + diff --git a/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php new file mode 100644 index 0000000000000..26fb56ecfd0e7 --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php @@ -0,0 +1,20 @@ +_init(Model::class, ResourceModel::class); + } + +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php index d6dd51058a63c..9458da4c2e64f 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php @@ -32,6 +32,7 @@ class GroupTest extends TestCase /** @var Group */ protected $groupResourceModel; + /** @var ResourceConnection|MockObject */ protected $resource; diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index c260cb6572ac2..10feb51c7b853 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -115,8 +115,27 @@ Email template chosen based on theme fallback when "Default" option is selected. Magento\Config\Model\Config\Source\Email\Template + + 1 + + + + + Limit the number of confirmation email requests within a specified time interval. Use 0 to disable. + required-entry validate-zero-or-greater validate-digits + + 1 + + + + + Delay in minutes between password reset requests. Use 0 to disable. + required-entry validate-zero-or-greater validate-digits + + 1 + - +
@@ -124,7 +143,7 @@ ]]>
Magento\Config\Model\Config\Source\Email\Template
- + Magento\Config\Model\Config\Source\Yesno diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml index 6164cb5a2c7d1..927af2ed2b865 100644 --- a/app/code/Magento/Customer/etc/config.xml +++ b/app/code/Magento/Customer/etc/config.xml @@ -19,6 +19,8 @@ general customer_create_account_email_template customer_create_account_email_no_password_template + 3 + 30 customer_create_account_email_confirmation_template customer_create_account_email_confirmed_template 0 diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml index 0b84a3cc0c5a7..7f88602f1da21 100644 --- a/app/code/Magento/Customer/etc/db_schema.xml +++ b/app/code/Magento/Customer/etc/db_schema.xml @@ -561,4 +561,21 @@ + + + + + + + + + + + +
diff --git a/app/code/Magento/Customer/etc/db_schema_whitelist.json b/app/code/Magento/Customer/etc/db_schema_whitelist.json index 57cf2d0e87849..af76cefad77cf 100644 --- a/app/code/Magento/Customer/etc/db_schema_whitelist.json +++ b/app/code/Magento/Customer/etc/db_schema_whitelist.json @@ -369,5 +369,23 @@ "constraint": { "PRIMARY": true } + }, + "customer_confirmation_log": { + "id": { + "type": "integer", + "description": "Unique identifier for the confirmation log." + }, + "customer_id": { + "type": "integer", + "description": "ID of the associated customer." + }, + "email_sent_counter": { + "type": "integer", + "description": "Counter for the number of confirmation emails sent." + }, + "last_email_sent_at": { + "type": "timestamp", + "description": "Timestamp of the last confirmation email sent." + } } } diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index 00a8597d8c364..624163e1cb5be 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -65,6 +65,10 @@ + +