diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b26ea43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.php_cs +composer.lock +vendor diff --git a/LICENSE b/LICENSE index 64c8910..c7616d6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 TJ +Copyright (c) 2016 CMTT (https://cmtt.ru) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/composer.json b/composer.json index 4376c24..bd8cb73 100644 --- a/composer.json +++ b/composer.json @@ -13,14 +13,16 @@ } ], "require": { - "php": ">=5.4.0", "maknz/slack": "~1.7", + "php": ">=5.4.0", + "psr/cache": "~1.0", "psr/log": "~1.0" }, "suggest": { - "monolog/monolog": "Monolog sends your logs to files, sockets, inboxes, databases and various web services" + "monolog/monolog": "Monolog sends your logs to files, sockets, inboxes, databases and various web services", + "tedivm/stash": "Stash is a PHP caching library with a support of PSR-6, Memcached, Redis, APC and other cache drivers" }, "autoload": { "psr-4": {"TJ\\": "src/"} } -} \ No newline at end of file +} diff --git a/example.php b/example.php index 1945c58..ca4d69a 100644 --- a/example.php +++ b/example.php @@ -14,10 +14,11 @@ // Uncomment if you want to use Memcached for antiflood protection /* -$memcached = new Memcached(); -$memcached->addServer('localhost', '11211'); +$driver = new Stash\Driver\Memcache(['servers' => ['127.0.0.1', '11211']]); -$newrphus->setMemcached($memcached); +$pool = new Stash\Pool($driver); + +$newrphus->setCache($pool); */ // You can add multiple additional fields to message (optional) @@ -33,4 +34,4 @@ // And customize Slack message text (optional) $newrphus->setMessageText("New misprint: {$_POST['misprintText']}"); -$result = $newrphus->report($_POST['misprintText'], $_POST['misprintUrl']); \ No newline at end of file +$result = $newrphus->report($_POST['misprintText'], $_POST['misprintUrl']); diff --git a/src/Newrphus.php b/src/Newrphus.php index 795cce8..d0f5e78 100644 --- a/src/Newrphus.php +++ b/src/Newrphus.php @@ -3,6 +3,7 @@ use Exception; use Maknz\Slack\Client as Slack; +use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; /** @@ -21,7 +22,7 @@ class Newrphus * How many misprints will be accepted from one IP address * per 10 minutes before it will be banned * - * @var integer + * @var int */ public $attemptsThreshold = 10; @@ -66,10 +67,19 @@ class Newrphus /** * Memcached instance * + * @deprecated + * * @var Memcached */ protected $memcached; + /** + * PSR-6 compatible cache pool + * + * @var CacheItemPoolInterface + */ + protected $cache; + /** * Logger instance * @@ -80,8 +90,9 @@ class Newrphus /** * Slack options setter * - * @param array $slackSettings e.g. [ 'endpoint' => 'https://hook.slack.com/...', 'channel' => '#misprints' ] - * @return TJ\Newrphus + * @param array $slackSettings e.g. [ 'endpoint' => 'https://hook.slack.com/...', 'channel' => '#misprints' ] + * + * @return Newrphus */ public function setSlackSettings($slackSettings) { @@ -93,8 +104,9 @@ public function setSlackSettings($slackSettings) /** * PSR-3 compatible logger setter * - * @param LoggerInterface $logger - * @return TJ\Newrphus + * @param LoggerInterface $logger + * + * @return Newrphus */ public function setLogger(LoggerInterface $logger) { @@ -106,8 +118,11 @@ public function setLogger(LoggerInterface $logger) /** * Memcached setter * - * @param Memcached $memcached - * @return TJ\Newrphus + * @deprecated + * + * @param Memcached $memcached + * + * @return Newrphus */ public function setMemcached($memcached) { @@ -117,29 +132,39 @@ public function setMemcached($memcached) } /** - * Add field to Slack message + * Cache setter + * + * @param CacheItemPoolInterface $cache * - * @param string $title - * @param string $value - * @param boolean $short - * @return TJ\Newrphus + * @return Newrphus */ - public function addField($title, $value, $short = false) + public function setCache($cache) { - array_push($this->fields, [ - 'title' => $title, - 'value' => $value, - 'short' => (bool) $short - ]); + $this->cache = $cache; return $this; } /** - * Custom notification text setter + * Slack mesage text setter * - * @param string $notificationText - * @return TJ\Newrphus + * @param string $messageText + * + * @return Newrphus + */ + public function setMessageText($messageText) + { + $this->messageText = $messageText; + + return $this; + } + + /** + * Custom Slack notification text setter + * + * @param string $notificationText + * + * @return Newrphus */ public function setNotificationText($notificationText) { @@ -149,14 +174,21 @@ public function setNotificationText($notificationText) } /** - * Slack mesage text setter + * Add custom field to Slack message * - * @param string $messageText - * @return TJ\Newrphus + * @param string $title + * @param string $value + * @param bool $short + * + * @return Newrphus */ - public function setMessageText($messageText) + public function addField($title, $value, $short = false) { - $this->messageText = $messageText; + array_push($this->fields, [ + 'title' => $title, + 'value' => $value, + 'short' => (bool) $short + ]); return $this; } @@ -164,9 +196,10 @@ public function setMessageText($messageText) /** * Report about misprint * - * @param string misprintText - * @param string misprintUrl - * @return boolean + * @param string misprintText Used for antflood protection and as a fallback if message doesn't provided + * @param string misprintUrl URL where misprint was found + * + * @return bool */ public function report($misprintText, $misprintUrl = null) { @@ -186,37 +219,67 @@ public function report($misprintText, $misprintUrl = null) /** * Flood protection with Memcached * - * @param string $misprintHash + * @param string $misprintHash + * * @throws Exception if report is flood-positive - * @return boolean + * + * @return bool */ protected function floodProtect($misprintHash) { - if (!$this->memcached) { + if (!$this->memcached && !$this->cache) { return false; } $ip = $this->getIP(); + if ($ip !== false) { - $mcIpHash = 'newrphus:byIP:' . md5($ip); - $attemptsCount = $this->memcached->get($mcIpHash); - if ($this->memcached->getResultCode() === 0) { - if ($attemptsCount > $this->attemptsThreshold) { + $ipHash = 'newrphus/byIP/' . md5($ip); + $textHash = 'newrphus/byText/' . $misprintHash; + + if ($this->cache) { + $ipItem = $this->cache->getItem($ipHash); + $textItem = $this->cache->getItem($textHash); + + $attemptsCount = $ipItem->get(); + if ($attemptsCount !== null && (int) $attemptsCount > $this->attemptsThreshold) { throw new Exception("Too many attempts", 429); + } else { + $ipItem->lock(); + + if ($ipItem->isMiss()) { + $ipItem->expiresAfter(300); + } + + $this->cache->save($ipItem->set((int) $attemptsCount + 1)); + } + + if ($textItem->isHit()) { + throw new Exception("This misprint already was sent", 202); + } else { + $this->cache->save($textItem->expiresAfter(300)->set(true)); } - $this->memcached->increment($mcIpHash); } else { - $this->memcached->set($mcIpHash, 1, 300); - } - } + $attemptsCount = $this->memcached->get($ipHash); - $mcTextHash = 'newrphus:byText:' . $misprintHash; - $this->memcached->get($mcTextHash); - if ($this->memcached->getResultCode() === 0) { - throw new Exception("This misprint already was sent", 202); - } + if ($this->memcached->getResultCode() === 0) { + if ($attemptsCount > $this->attemptsThreshold) { + throw new Exception("Too many attempts", 429); + } - $this->memcached->set($mcTextHash, true, 300); + $this->memcached->increment($ipHash); + } else { + $this->memcached->set($ipHash, 1, 300); + } + + $this->memcached->get($textHash); + if ($this->memcached->getResultCode() === 0) { + throw new Exception("This misprint already was sent", 202); + } + + $this->memcached->set($textHash, true, 300); + } + } return true; } @@ -224,7 +287,7 @@ protected function floodProtect($misprintHash) /** * Get user IP address * - * @return string|boolean + * @return string|bool */ protected function getIP() { @@ -242,9 +305,10 @@ protected function getIP() /** * Send misprint report to Slack * - * @param string $misprintText - * @param string $misprintUrl - * @return boolean + * @param string $misprintText + * @param string $misprintUrl + * + * @return bool */ protected function sendToSlack($misprintText, $misprintUrl) {