diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 8924f9d668..7569b72f0f 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -158,6 +158,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Subscribers\RequiredCustomFieldValidator::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\SubscriberActions::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\InactiveSubscribersController::class); + $container->autowire(\MailPoet\Subscribers\LinkTokens::class); // Segments $container->autowire(\MailPoet\Segments\SubscribersListings::class)->setPublic(true); $container->autowire(\MailPoet\Segments\WooCommerce::class)->setPublic(true); diff --git a/lib/Subscribers/LinkTokens.php b/lib/Subscribers/LinkTokens.php new file mode 100644 index 0000000000..b38c38590e --- /dev/null +++ b/lib/Subscribers/LinkTokens.php @@ -0,0 +1,44 @@ +link_token === null) { + $subscriber->link_token = $this->generateToken($subscriber->email); + // `$subscriber->save()` fails if the subscriber has subscriptions, segments or custom fields + \ORM::rawExecute(sprintf('UPDATE %s SET link_token = ? WHERE email = ?', Subscriber::$_table), [$subscriber->link_token, $subscriber->email]); + } + return $subscriber->link_token; + } + + function verifyToken(Subscriber $subscriber, $token) { + $database_token = $subscriber->getLinkToken(); + $request_token = substr($token, 0, strlen($database_token)); + return call_user_func( + 'hash_equals', + $database_token, + $request_token + ); + } + + /** + * Only for backward compatibility for old tokens + */ + private function generateToken($email = null, $length = self::OBSOLETE_LINK_TOKEN_LENGTH) { + if ($email !== null) { + $auth_key = ''; + if (defined('AUTH_KEY')) { + $auth_key = AUTH_KEY; + } + return substr(md5($auth_key . $email), 0, $length); + } + return false; + } + +} \ No newline at end of file diff --git a/tests/integration/Models/SubscriberTest.php b/tests/integration/Models/SubscriberTest.php index 440b0a3259..f7cea44f49 100644 --- a/tests/integration/Models/SubscriberTest.php +++ b/tests/integration/Models/SubscriberTest.php @@ -672,32 +672,6 @@ class SubscriberTest extends \MailPoetTest { expect($total)->equals(1); } - function testItGeneratesSubscriberToken() { - $token = Subscriber::generateToken($this->test_data['email']); - expect(strlen($token))->equals(6); - } - - function testItVerifiesSubscriberToken() { - $subscriber = Subscriber::createOrUpdate([ - 'email' => $this->test_data['email'], - ]); - $token = $subscriber->getLinkToken(); - expect($subscriber->verifyToken($token))->true(); - expect($subscriber->verifyToken('faketoken'))->false(); - } - - function testItVerifiesOldVersionOfSubscriberToken() { - $subscriber = Subscriber::createOrUpdate([ - 'email' => $this->test_data['email'], - ]); - $subscriber->link_token = 'abcdef'; - $token = $subscriber->getLinkToken(); - expect($subscriber->verifyToken($token))->true(); - expect($subscriber->verifyToken('abcdefghijk'))->true(); - expect($subscriber->verifyToken('faketoken'))->false(); - expect($subscriber->verifyToken('fake'))->false(); - } - function testItBulkDeletesSubscribers() { $segment = Segment::createOrUpdate( [ diff --git a/tests/integration/Subscribers/LinkTokensTest.php b/tests/integration/Subscribers/LinkTokensTest.php new file mode 100644 index 0000000000..d4397bf50e --- /dev/null +++ b/tests/integration/Subscribers/LinkTokensTest.php @@ -0,0 +1,54 @@ +link_tokens = new LinkTokens; + } + + function testItGeneratesSubscriberToken() { + $subscriber1 = $this->makeSubscriber(['email' => 'demo1@fake.loc']); + $subscriber2 = $this->makeSubscriber(['email' => 'demo2@fake.loc']); + $token1 = $this->link_tokens->getToken($subscriber1); + $token2 = $this->link_tokens->getToken($subscriber2); + expect(strlen($token1))->equals(6); + expect(strlen($token2))->equals(6); + expect($token1 != $token2)->equals(true); + } + + function testItGetsSubscriberToken() { + $subscriber1 = $this->makeSubscriber(['email' => 'demo1@fake.loc', 'link_token' => 'already-existing-token']); + $subscriber2 = $this->makeSubscriber(['email' => 'demo2@fake.loc']); + expect($this->link_tokens->getToken($subscriber1))->equals('already-existing-token'); + expect(strlen($this->link_tokens->getToken($subscriber2)))->equals(6); + } + + function testItVerifiesSubscriberToken() { + $subscriber = $this->makeSubscriber([ + 'email' => 'demo@fake.loc', + ]); + $token = $this->link_tokens->getToken($subscriber); + expect($this->link_tokens->verifyToken($subscriber, $token))->true(); + expect($this->link_tokens->verifyToken($subscriber, 'faketoken'))->false(); + } + + function _after() { + \ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); + } + + private function makeSubscriber($data) { + $subscriber = Subscriber::create(); + $subscriber->hydrate($data); + $subscriber->save(); + return $subscriber; + } + +}