...
 
Commits (2)
......@@ -2,17 +2,18 @@
namespace Minds\Controllers\api\v2;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Security\RateLimits\Maps;
use Minds\Interfaces;
use Minds\Api\Factory;
class suggestions implements Interfaces\Api
{
public function get($pages)
{
$type = $pages[0] ?? 'user';
$type = $pages[0] ?? 'user';
$loggedInUser = Core\Session::getLoggedinUser();
if ($loggedInUser->getSubscriptionsCount() >= 5000) {
......
......@@ -4,6 +4,12 @@
*/
namespace Minds\Core\Subscriptions;
use Minds\Core\Subscriptions\Delegates\CacheDelegate;
use Minds\Core\Subscriptions\Delegates\CopyToElasticSearchDelegate;
use Minds\Core\Subscriptions\Delegates\EventsDelegate;
use Minds\Core\Subscriptions\Delegates\FeedsDelegate;
use Minds\Core\Subscriptions\Delegates\SendNotificationDelegate;
use Minds\Core\Suggestions\Delegates\CheckRateLimit;
use Minds\Entities\User;
class Manager
......@@ -30,6 +36,9 @@ class Manager
/** @var FeedsDelegate $feedsDelegate */
private $feedsDelegate;
/** @var CheckRateLimit */
private $checkRateLimitDelegate;
/** @var bool */
private $sendEvents = true;
......@@ -39,7 +48,8 @@ class Manager
$sendNotificationDelegate = null,
$cacheDelegate = null,
$eventsDelegate = null,
$feedsDelegate = null
$feedsDelegate = null,
$checkRateLimitDelegate = null
)
{
$this->repository = $repository ?: new Repository;
......@@ -48,6 +58,7 @@ class Manager
$this->cacheDelegate = $cacheDelegate ?: new Delegates\CacheDelegate;
$this->eventsDelegate = $eventsDelegate ?: new Delegates\EventsDelegate;
$this->feedsDelegate = $feedsDelegate ?: new Delegates\FeedsDelegate;
$this->checkRateLimitDelegate = $checkRateLimitDelegate ?: new CheckRateLimit();
}
public function setSubscriber($user)
......@@ -97,6 +108,7 @@ class Manager
$this->feedsDelegate->copy($subscription);
$this->copyToElasticSearchDelegate->copy($subscription);
$this->cacheDelegate->cache($subscription);
$this->checkRateLimitDelegate->incrementCache($this->subscriber->guid);
if ($this->sendEvents) {
$this->sendNotificationDelegate->send($subscription);
......
<?php
namespace Minds\Core\Suggestions\Delegates;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Di\Di;
use Minds\Core\Security\RateLimits\Maps;
class CheckRateLimit
{
/** @var abstractCacher */
private $cacher;
/** @var array */
private $maps;
const SUBSCRIBE_KEY = 'interaction:subscribe';
public function __construct($cacher = null, $maps = null)
{
$this->cacher = $cacher ?: Di::_()->get('Cache');
$this->maps = $maps ?: Maps::$maps;
}
/**
* @param string|int $userGuid
* @return bool false if about to get rate limited
* @throws \Exception
*/
public function check($userGuid)
{
if (!$userGuid) {
throw new \Exception('userGuid must be provided');
}
$threshold = $this->maps[static::SUBSCRIBE_KEY]['threshold'];
$cached = $this->cacher->get("subscriptions:user:$userGuid");
if ($cached != false && $cached >= $threshold - 10) {
return false;
}
return true;
}
/**
* @param string|int $userGuid
* @throws \Exception
*/
public function incrementCache($userGuid)
{
if (!$userGuid) {
throw new \Exception('userGuid must be provided');
}
$count = $this->cacher->get("subscriptions:user:$userGuid");
if (!$count)
$count = 0;
$this->cacher->set("subscriptions:user:$userGuid", ++$count, $this->maps[static::SUBSCRIBE_KEY]['period']);
}
}
......@@ -2,9 +2,11 @@
namespace Minds\Core\Suggestions;
use Minds\Core\EntitiesBuilder;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Suggestions\Delegates\CheckRateLimit;
use Minds\Entities\User;
class Manager
{
......@@ -14,20 +16,31 @@ class Manager
/** @var EntitiesBuilder $entitiesBuilder */
private $entitiesBuilder;
/** @var \Minds\Core\Subscriptions\Manager */
private $subscriptionsManager;
/** @var User $user */
private $user;
/** @var CheckRateLimit */
private $checkRateLimit;
/** @var string $type */
private $type;
public function __construct(
$repository = null,
$entitiesBuilder = null,
$subscriptionsManager = null
) {
$suggestedFeedsManager = null,
$subscriptionsManager = null,
$checkRateLimit = null
)
{
$this->repository = $repository ?: new Repository();
$this->entitiesBuilder = $entitiesBuilder ?: new EntitiesBuilder();
//$this->suggestedFeedsManager = $suggestedFeedsManager ?: Di::_()->get('Feeds\Suggested\Manager');
$this->subscriptionsManager = $subscriptionsManager ?: Di::_()->get('Subscriptions\Manager');
$this->checkRateLimit = $checkRateLimit ?: new CheckRateLimit();
}
/**
......@@ -72,6 +85,10 @@ class Manager
'paging-token' => '',
], $opts);
if (!$this->checkRateLimit->check($this->user->guid)) {
return new Response([]);
}
$opts['user_guid'] = $this->user->getGuid();
$response = $this->repository->getList($opts);
......
......@@ -2,10 +2,11 @@
namespace Spec\Minds\Core\Subscriptions;
use Minds\Core\Subscriptions\Delegates;
use Minds\Core\Subscriptions\Manager;
use Minds\Core\Subscriptions\Repository;
use Minds\Core\Subscriptions\Subscription;
use Minds\Core\Subscriptions\Delegates;
use Minds\Core\Suggestions\Delegates\CheckRateLimit;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
......@@ -19,6 +20,7 @@ class ManagerSpec extends ObjectBehavior
private $cacheDelegate;
private $eventsDelegate;
private $feedsDelegate;
private $checkRateLimitDelegate;
function let(
......@@ -27,7 +29,8 @@ class ManagerSpec extends ObjectBehavior
Delegates\SendNotificationDelegate $sendNotificationDelegate = null,
Delegates\CacheDelegate $cacheDelegate = null,
Delegates\EventsDelegate $eventsDelegate = null,
Delegates\FeedsDelegate $feedsDelegate = null
Delegates\FeedsDelegate $feedsDelegate = null,
CheckRateLimit $checkRateLimitDelegate = null
)
{
$this->beConstructedWith(
......@@ -37,7 +40,7 @@ class ManagerSpec extends ObjectBehavior
$cacheDelegate,
$eventsDelegate,
$feedsDelegate,
true
$checkRateLimitDelegate
);
$this->repository = $repository;
$this->copyToElasticSearchDelegate = $copyToElasticSearchDelegate;
......@@ -45,6 +48,7 @@ class ManagerSpec extends ObjectBehavior
$this->cacheDelegate = $cacheDelegate;
$this->eventsDelegate = $eventsDelegate;
$this->feedsDelegate = $feedsDelegate;
$this->checkRateLimitDelegate = $checkRateLimitDelegate;
}
function it_is_initializable()
......@@ -76,13 +80,13 @@ class ManagerSpec extends ObjectBehavior
$subscription = new Subscription;
$subscription->setActive(true);
$this->repository->add(Argument::that(function($sub) {
$this->repository->add(Argument::that(function ($sub) {
return $sub->getSubscriberGuid() == 123
&& $sub->getPublisherGuid() == 456;
}))
}))
->shouldBeCalled()
->willReturn($subscription);
$publisher = (new User)->set('guid', 456);
$this->setSubscriber((new User)->set('guid', 123));
......@@ -106,6 +110,10 @@ class ManagerSpec extends ObjectBehavior
$this->cacheDelegate->cache($subscription)
->shouldBeCalled();
// Call the Rate Limit delegate
$this->checkRateLimitDelegate->incrementCache(123)
->shouldBeCalled();
$newSubscription = $this->subscribe($publisher);
$newSubscription->isActive()
->shouldBe(true);
......@@ -118,13 +126,13 @@ class ManagerSpec extends ObjectBehavior
$subscription = new Subscription;
$subscription->setActive(false);
$this->repository->delete(Argument::that(function($sub) {
$this->repository->delete(Argument::that(function ($sub) {
return $sub->getSubscriberGuid() == 123
&& $sub->getPublisherGuid() == 456;
}))
}))
->shouldBeCalled()
->willReturn($subscription);
$publisher = (new User)->set('guid', 456);
$this->setSubscriber((new User)->set('guid', 123));
......@@ -143,7 +151,7 @@ class ManagerSpec extends ObjectBehavior
// Call the cache delegate
$this->cacheDelegate->cache($subscription)
->shouldBeCalled();
$newSubscription = $this->unSubscribe($publisher);
$newSubscription->isActive()
->shouldBe(false);
......
<?php
namespace Spec\Minds\Core\Suggestions\Delegates;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Data\cache\Redis;
use Minds\Core\Suggestions\Delegates\CheckRateLimit;
use PhpSpec\ObjectBehavior;
class CheckRateLimitSpec extends ObjectBehavior
{
/** @var abstractCacher */
private $cacher;
function let(Redis $cacher)
{
$this->cacher = $cacher;
$this->beConstructedWith($cacher);
}
function it_is_initializable()
{
$this->shouldHaveType(CheckRateLimit::class);
}
function it_should_throw_an_exception_if_performing_a_check_but_userGuid_isnt_set()
{
$this->shouldThrow(new \Exception('userGuid must be provided'))->during('check', [null]);
}
function it_should_perform_a_check_and_return_true()
{
$this->cacher->get("subscriptions:user:123")
->shouldBeCalled()
->willReturn(false);
$this->check(123)->shouldReturn(true);
}
function it_should_perform_a_check_and_return_false()
{
$this->cacher->get("subscriptions:user:123")
->shouldBeCalled()
->willReturn(41);
// returns false because we're near the subscribe threshold
$this->check(123)->shouldReturn(false);
}
function it_should_throw_an_exception_when_caching_the_response_but_userGuid_isnt_set()
{
$this->shouldThrow(new \Exception('userGuid must be provided'))->during('incrementCache', [null]);
}
function it_should_increment_the_cache()
{
$this->cacher->get("subscriptions:user:123")
->shouldBeCalled()
->willReturn(1);
$this->cacher->set('subscriptions:user:123', 2, 300)
->shouldBeCalled();
$this->incrementCache(123, 10);
}
}
......@@ -2,6 +2,7 @@
namespace Spec\Minds\Core\Suggestions;
use Minds\Core\Suggestions\Delegates\CheckRateLimit;
use Minds\Entities\User;
use Minds\Common\Repository\Response;
use Minds\Core\Suggestions\Manager;
......@@ -15,18 +16,25 @@ use Prophecy\Argument;
class ManagerSpec extends ObjectBehavior
{
/** @var Repository */
private $repository;
/** @var EntitiesBuilder */
private $entitiesBuilder;
/** @var CheckRateLimit */
private $checkRateLimit;
function let(
Repository $repository,
EntitiesBuilder $entitiesBuilder,
SubscriptionsManager $subscriptionsManager
SubscriptionsManager $subscriptionsManager,
CheckRateLimit $checkRateLimit
)
{
$this->beConstructedWith($repository, $entitiesBuilder, $subscriptionsManager);
$this->repository = $repository;
$this->entitiesBuilder = $entitiesBuilder;
$this->checkRateLimit = $checkRateLimit;
$this->beConstructedWith($repository, $entitiesBuilder, null, $subscriptionsManager, $checkRateLimit);
}
function it_is_initializable()
......@@ -43,11 +51,15 @@ class ManagerSpec extends ObjectBehavior
$response[] = (new Suggestion)
->setEntityGuid(789);
$this->checkRateLimit->check(123)
->shouldBeCalled()
->willReturn(true);
$this->repository->getList([
'limit' => 24,
'paging-token' => '',
'user_guid' => 123,
])
'limit' => 24,
'paging-token' => '',
'user_guid' => 123,
])
->shouldBeCalled()
->willReturn($response);
......@@ -61,7 +73,7 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn((new User)->set('guid', 789));
$newResponse = $this->getList([ 'limit' => 24 ]);
$newResponse = $this->getList(['limit' => 24]);
$newResponse[0]->getEntityGuid()
->shouldBe(456);
......@@ -74,6 +86,18 @@ class ManagerSpec extends ObjectBehavior
->shouldBe(789);
}
function it_shouldnt_return_a_list_of_suggested_users_if_close_too_close_to_the_rate_limit_threshold()
{
$this->checkRateLimit->check(123)
->shouldBeCalled()
->willReturn(false);
$this->setUser((new User)->set('guid', 123));
$newResponse = $this->getList(['limit' => 24]);
$newResponse->count()->shouldBe(0);
}
}