diff --git a/lib/Subscribers/SubscriberIPsRepository.php b/lib/Subscribers/SubscriberIPsRepository.php index 7dc260202c..7c2e7efc9c 100644 --- a/lib/Subscribers/SubscriberIPsRepository.php +++ b/lib/Subscribers/SubscriberIPsRepository.php @@ -4,6 +4,7 @@ namespace MailPoet\Subscribers; use MailPoet\Doctrine\Repository; use MailPoet\Entities\SubscriberIPEntity; +use MailPoetVendor\Carbon\Carbon; /** * @extends Repository @@ -12,4 +13,39 @@ class SubscriberIPsRepository extends Repository { protected function getEntityClassName() { return SubscriberIPEntity::class; } + + public function findOneByIPAndCreatedAtAfterTimeInSeconds(string $ip, int $seconds): ?SubscriberIPEntity { + return $this->entityManager->createQueryBuilder() + ->select('sip') + ->from(SubscriberIPEntity::class, 'sip') + ->where('sip.ip = :ip') + ->andWhere('sip.createdAt >= :timeThreshold') + ->setParameter('ip', $ip) + ->setParameter('timeThreshold', (new Carbon())->subSeconds($seconds)) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } + + public function getCountByIPAndCreatedAtAfterTimeInSeconds(string $ip, int $seconds): int { + return $this->entityManager->createQueryBuilder() + ->select('COUNT(sip)') + ->from(SubscriberIPEntity::class, 'sip') + ->where('sip.ip = :ip') + ->andWhere('sip.createdAt >= :timeThreshold') + ->setParameter('ip', $ip) + ->setParameter('timeThreshold', (new Carbon())->subSeconds($seconds)) + ->getQuery() + ->getSingleScalarResult(); + } + + public function deleteCreatedAtBeforeTimeInSeconds(int $seconds): int { + return (int)$this->entityManager->createQueryBuilder() + ->delete() + ->from(SubscriberIPEntity::class, 'sip') + ->where('sip.createdAt < :timeThreshold') + ->setParameter('timeThreshold', (new Carbon())->subSeconds($seconds)) + ->getQuery() + ->execute(); + } } diff --git a/lib/Subscription/Throttling.php b/lib/Subscription/Throttling.php index e1d684131f..9106393b30 100644 --- a/lib/Subscription/Throttling.php +++ b/lib/Subscription/Throttling.php @@ -2,42 +2,37 @@ namespace MailPoet\Subscription; -use MailPoet\Models\SubscriberIP; +use MailPoet\Entities\SubscriberIPEntity; +use MailPoet\Subscribers\SubscriberIPsRepository; use MailPoet\Util\Helpers; use MailPoet\WP\Functions as WPFunctions; class Throttling { + /** @var SubscriberIPsRepository */ + private $subscriberIPsRepository; + /** @var WPFunctions */ private $wp; - public function __construct(WPFunctions $wp) { + public function __construct(SubscriberIPsRepository $subscriberIPsRepository, WPFunctions $wp) { $this->wp = $wp; + $this->subscriberIPsRepository = $subscriberIPsRepository; } public function throttle() { $subscriptionLimitEnabled = $this->wp->applyFilters('mailpoet_subscription_limit_enabled', true); - $subscriptionLimitWindow = $this->wp->applyFilters('mailpoet_subscription_limit_window', DAY_IN_SECONDS); - $subscriptionLimitBase = $this->wp->applyFilters('mailpoet_subscription_limit_base', MINUTE_IN_SECONDS); + $subscriptionLimitWindow = (int)$this->wp->applyFilters('mailpoet_subscription_limit_window', DAY_IN_SECONDS); + $subscriptionLimitBase = (int)$this->wp->applyFilters('mailpoet_subscription_limit_base', MINUTE_IN_SECONDS); $subscriberIp = Helpers::getIP(); if ($subscriptionLimitEnabled && !$this->wp->isUserLoggedIn()) { if (!empty($subscriberIp)) { - $subscriptionCount = SubscriberIP::where('ip', $subscriberIp) - ->whereRaw( - '(`created_at` >= NOW() - INTERVAL ? SECOND)', - [(int)$subscriptionLimitWindow] - )->count(); - + $subscriptionCount = $this->subscriberIPsRepository->getCountByIPAndCreatedAtAfterTimeInSeconds($subscriberIp, $subscriptionLimitWindow); if ($subscriptionCount > 0) { $timeout = $subscriptionLimitBase * pow(2, $subscriptionCount - 1); - $existingUser = SubscriberIP::where('ip', $subscriberIp) - ->whereRaw( - '(`created_at` >= NOW() - INTERVAL ? SECOND)', - [(int)$timeout] - )->findOne(); - + $existingUser = $this->subscriberIPsRepository->findOneByIPAndCreatedAtAfterTimeInSeconds($subscriberIp, $timeout); if (!empty($existingUser)) { return $timeout; } @@ -45,21 +40,23 @@ class Throttling { } } - $ip = SubscriberIP::create(); - $ip->ip = $subscriberIp; - $ip->save(); + if ($subscriberIp !== null) { + $ip = new SubscriberIPEntity($subscriberIp); + $existingIp = $this->subscriberIPsRepository->findOneBy(['ip' => $ip->getIP(), 'createdAt' => $ip->getCreatedAt()]); + if (!$existingIp) { + $this->subscriberIPsRepository->persist($ip); + $this->subscriberIPsRepository->flush(); + } + } $this->purge(); return false; } - public function purge() { + public function purge(): void { $interval = $this->wp->applyFilters('mailpoet_subscription_purge_window', MONTH_IN_SECONDS); - return SubscriberIP::whereRaw( - '(`created_at` < NOW() - INTERVAL ? SECOND)', - [$interval] - )->deleteMany(); + $this->subscriberIPsRepository->deleteCreatedAtBeforeTimeInSeconds($interval); } public function secondsToTimeString($seconds): string { diff --git a/tests/integration/Subscription/ThrottlingTest.php b/tests/integration/Subscription/ThrottlingTest.php index 2eb6b51c94..cbe35bf8e0 100644 --- a/tests/integration/Subscription/ThrottlingTest.php +++ b/tests/integration/Subscription/ThrottlingTest.php @@ -2,7 +2,8 @@ namespace MailPoet\Subscription; -use MailPoet\Models\SubscriberIP; +use MailPoet\Entities\SubscriberIPEntity; +use MailPoet\Subscribers\SubscriberIPsRepository; use MailPoet\WP\Functions as WPFunctions; use MailPoetVendor\Carbon\Carbon; @@ -10,9 +11,13 @@ class ThrottlingTest extends \MailPoetTest { /** @var Throttling */ private $throttling; + /** @var SubscriberIPsRepository */ + private $subscriberIPsRepository; + protected function _before() { parent::_before(); $this->throttling = $this->diContainer->get(Throttling::class); + $this->subscriberIPsRepository = $this->diContainer->get(SubscriberIPsRepository::class); } public function testItProgressivelyThrottlesSubscriptions() { @@ -20,10 +25,7 @@ class ThrottlingTest extends \MailPoetTest { expect($this->throttling->throttle())->equals(false); expect($this->throttling->throttle())->equals(60); for ($i = 1; $i <= 10; $i++) { - $ip = SubscriberIP::create(); - $ip->ip = '127.0.0.1'; - $ip->createdAt = Carbon::now()->subMinutes($i); - $ip->save(); + $this->createSubscriberIP('127.0.0.1', Carbon::now()->subMinutes($i)); } expect($this->throttling->throttle())->equals(MINUTE_IN_SECONDS * pow(2, 10)); } @@ -49,18 +51,12 @@ class ThrottlingTest extends \MailPoetTest { } public function testItPurgesOldSubscriberIps() { - $ip = SubscriberIP::create(); - $ip->ip = '127.0.0.1'; - $ip->save(); + $this->createSubscriberIP('127.0.0.1', Carbon::now()); + $this->createSubscriberIP('127.0.0.1', Carbon::now()->subDays(30)->subSeconds(1)); - $ip2 = SubscriberIP::create(); - $ip2->ip = '127.0.0.1'; - $ip2->createdAt = Carbon::now()->subDays(30)->subSeconds(1); - $ip2->save(); - - expect(SubscriberIP::count())->equals(2); + expect($this->subscriberIPsRepository->countBy([]))->equals(2); $this->throttling->throttle(); - expect(SubscriberIP::count())->equals(1); + expect($this->subscriberIPsRepository->countBy([]))->equals(1); } public function testItConvertsSecondsToTimeString() { @@ -73,7 +69,15 @@ class ThrottlingTest extends \MailPoetTest { expect($this->throttling->secondsToTimeString(59))->equals('59 seconds'); } + private function createSubscriberIP(string $ip, Carbon $createdAt): SubscriberIPEntity { + $subscriberIP = new SubscriberIPEntity($ip); + $subscriberIP->setCreatedAt($createdAt); + $this->entityManager->persist($subscriberIP); + $this->entityManager->flush(); + return $subscriberIP; + } + public function _after() { - SubscriberIP::deleteMany(); + $this->truncateEntity(SubscriberIPEntity::class); } }