├── .gitignore ├── .travis.yml ├── Component.php ├── README.md ├── Widget.php ├── composer.json ├── tests ├── ComponentTest.php ├── MessageValidatorTest.php ├── bootstrap.php └── phpunit.xml └── validators ├── MessageValidator.php └── UserValidator.php /.gitignore: -------------------------------------------------------------------------------- 1 | DS_Store 2 | Thumbs.db 3 | vendor/ 4 | composer.lock 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - hhvm 7 | 8 | sudo: false 9 | 10 | # cache vendor dirs 11 | cache: 12 | directories: 13 | - vendor 14 | - $HOME/.composer/cache 15 | 16 | install: 17 | - travis_retry composer self-update 18 | - travis_retry composer global require "fxp/composer-asset-plugin:~1.0.0" 19 | - travis_retry composer install --no-interaction 20 | 21 | script: ./vendor/bin/phpunit -c tests/phpunit.xml tests/ -------------------------------------------------------------------------------- /Component.php: -------------------------------------------------------------------------------- 1 | apiKey)) { 49 | throw new InvalidConfigException('CleanTalkApi configuration must have "apiKey" value'); 50 | } 51 | } 52 | 53 | /** 54 | * Check if user registration allow 55 | * @param string $email user email 56 | * @param string $nickName user nickName 57 | * @return array [bool, string] true, if user registration allow, false with text comment 58 | */ 59 | public function isAllowUser($email = '', $nickName = '') 60 | { 61 | $ctRequest = $this->createRequest(); 62 | $ctRequest->sender_email = $email; 63 | $ctRequest->sender_nickname = $nickName; 64 | 65 | $ctResult = $this->sendRequest($ctRequest, 'isAllowUser'); 66 | 67 | return [$ctResult->allow == 1, $ctResult->comment]; 68 | } 69 | 70 | /** 71 | * Check if user text message allow 72 | * @param string $email user email 73 | * @param string $nickName user nickName 74 | * @param string $message message 75 | * @return array [bool, string] true, if user message allow, false with text comment 76 | */ 77 | public function isAllowMessage($message, $email = '', $nickName = '') 78 | { 79 | $ctRequest = $this->createRequest(); 80 | $ctRequest->message = $message; 81 | $ctRequest->sender_email = $email; 82 | $ctRequest->sender_nickname = $nickName; 83 | 84 | $ctResult = $this->sendRequest($ctRequest, 'isAllowMessage'); 85 | 86 | return [$ctResult->allow == 1, $ctResult->comment]; 87 | } 88 | 89 | 90 | /** 91 | * Generate form Javascript check code 92 | * @return string 93 | */ 94 | public function getCheckJsCode() 95 | { 96 | return md5($this->apiKey . __FILE__); 97 | } 98 | 99 | /** 100 | * Set begin time of submitting form 101 | * @param string $id form id 102 | */ 103 | public function startFormSubmitTime($id) 104 | { 105 | Yii::$app->session->set(self::KEY_SESSION_FORM_SUBMIT . $id, time()); 106 | } 107 | 108 | /** 109 | * Get form submit time in seconds 110 | * @param string $id form id 111 | * @param boolean $clear clear value in session 112 | * @return int|null 113 | */ 114 | public function calcFormSubmitTime($id = null, $clear = true) 115 | { 116 | if ($id === null) { 117 | $id = Yii::$app->request->post('ct_formid'); 118 | } 119 | $startTime = Yii::$app->session->get(self::KEY_SESSION_FORM_SUBMIT . $id); 120 | if ($clear) { 121 | Yii::$app->session->remove(self::KEY_SESSION_FORM_SUBMIT . $id); 122 | } 123 | return $startTime > 0 ? time() - $startTime : null; 124 | } 125 | 126 | /** 127 | * Is javascript enabled 128 | * @return int 129 | */ 130 | public function isJavascriptEnable() 131 | { 132 | return Yii::$app->request->post('ct_checkjs') == $this->getCheckJsCode() ? 1 : 0; 133 | } 134 | 135 | /** 136 | * Create request for CleanTalk API. 137 | * @return \CleantalkRequest 138 | */ 139 | protected function createRequest() 140 | { 141 | $ctRequest = new CleantalkRequest(); 142 | $ctRequest->auth_key = $this->apiKey; 143 | $ctRequest->agent = self::AGENT_VERSION; 144 | $ctRequest->sender_ip = Yii::$app->request->getUserIP(); 145 | $ctRequest->submit_time = $this->calcFormSubmitTime(); 146 | $ctRequest->js_on = $this->isJavascriptEnable(); 147 | 148 | $ctRequest->sender_info = Json::encode( 149 | [ 150 | 'REFFERRER' => Yii::$app->request->getReferrer(), 151 | 'USER_AGENT' => Yii::$app->request->getUserAgent(), 152 | ] 153 | ); 154 | return $ctRequest; 155 | } 156 | 157 | /** 158 | * @param \CleantalkRequest $request 159 | * @param string $method 160 | * @return \CleantalkResponse CleanTalk API call result 161 | * @throws InvalidArgumentException 162 | */ 163 | protected function sendRequest($request, $method) 164 | { 165 | $ct = new Cleantalk(); 166 | $ct->server_url = $this->apiUrl; 167 | if ($method != 'isAllowMessage' && $method != 'isAllowUser') { 168 | throw new InvalidArgumentException('Method unknown'); 169 | } 170 | Yii::trace('Sending request to cleantalk:' . var_export($request, true), __METHOD__); 171 | 172 | $response = $ct->$method($request); 173 | 174 | if ($this->enableLog) { 175 | Yii::info(sprintf('Cleantalk response is allow=%d, inactive=%d, comment=%s', $response->allow, $response->inactive, $response->comment), __METHOD__); 176 | } 177 | return $response; 178 | } 179 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anti-spam extension by CleanTalk for Yii2 framework. 2 | 3 | No Captcha, no questions, no counting animals, no puzzles, no math. 4 | 5 | [![Build Status](https://travis-ci.org/CleanTalk/yii2-antispam.svg)](https://travis-ci.org/cleantalk/yii2-antispam) 6 | 7 | 8 | 9 | ## Requirements 10 | 11 | * Yii 2.0 or above 12 | * CleanTalk account https://cleantalk.org/register?product=anti-spam 13 | 14 | ##Installation 15 | 16 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 17 | 18 | Either run 19 | 20 | ``` 21 | php composer.phar require --prefer-dist cleantalk/yii2-antispam 22 | ``` 23 | 24 | or add 25 | 26 | ```json 27 | "cleantalk/yii2-antispam": "~1.0.0" 28 | ``` 29 | 30 | to the require section of your composer.json. 31 | 32 | ##Usage 33 | 34 | 1) Get access key on https://cleantalk.org/register?platform=yii2 35 | 36 | 2) Open your application configuration in protected/config/web.php and modify components section: 37 | 38 | ``` 39 | // application components 40 | 'components'=>[ 41 | ... 42 | 'antispam' => [ 43 | 'class' => 'cleantalk\antispam\Component', 44 | 'apiKey' => 'Your API KEY', 45 | ], 46 | ... 47 | ], 48 | ``` 49 | 50 | 3) Add validator in your model, for example ContactForm: 51 | 52 | ``` 53 | namespace app\models; 54 | 55 | use cleantalk\antispam\validators\MessageValidator; 56 | use Yii; 57 | use yii\base\Model; 58 | 59 | /** 60 | * ContactForm is the model behind the contact form. 61 | */ 62 | class ContactForm extends Model 63 | { 64 | public $name; 65 | public $email; 66 | public $body; 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function rules() 71 | { 72 | return [ 73 | // name, email, subject and body are required 74 | [['name', 'email', 'subject', 'body'], 'required'], 75 | // email has to be a valid email address 76 | ['email', 'email'], 77 | ['body', MessageValidator::className(), 'emailAttribute'=>'email', /*'nickNameAttribute'=>'name'*/] 78 | ]; 79 | } 80 | } 81 | ``` 82 | 83 | 4) In form view add widget for hidden Javascript checks: 84 | 85 | ``` 86 | 87 | ... 88 | 89 | ... 90 | 91 | ... 92 | 93 | 94 | ``` 95 | 96 | ## User registration validator 97 | 98 | See cleantalk\antispam\validators\UserValidator 99 | 100 | Example rules: 101 | ``` 102 | /** 103 | * @inheritdoc 104 | */ 105 | public function rules() 106 | { 107 | return [ 108 | [['name', 'email'], 'required'], 109 | ['email', 'email'], 110 | ['email', UserValidator::className(), 'nickNameAttribute'=>'name'] 111 | ]; 112 | } 113 | ``` 114 | 115 | ##License 116 | GNU General Public License 117 | 118 | ##Resources 119 | 120 | * https://cleantalk.org/ 121 | * https://github.com/CleanTalk/yii2-antispam 122 | -------------------------------------------------------------------------------- /Widget.php: -------------------------------------------------------------------------------- 1 | checkJsHtmlId = md5(rand(0, 1000)); 23 | } 24 | 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public function run() 30 | { 31 | $this->registerClientScript(); 32 | $this->getComponent()->startFormSubmitTime($this->checkJsHtmlId); 33 | return Html::hiddenInput('ct_checkjs', -1, ['id' => $this->checkJsHtmlId]) . 34 | Html::hiddenInput('ct_formid', $this->checkJsHtmlId); 35 | } 36 | 37 | protected function registerClientScript() 38 | { 39 | $js = 'setTimeout(function(){document.getElementById("' . $this->checkJsHtmlId . '").value="' . $this->getComponent()->getCheckJsCode() . '";}, 1000)'; 40 | $this->getView() 41 | ->registerJs($js); 42 | } 43 | 44 | /** 45 | * @return \cleantalk\antispam\Component 46 | */ 47 | protected function getComponent() 48 | { 49 | return Yii::$app->get($this->apiComponentId); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cleantalk/yii2-antispam", 3 | "description": "Anti-spam yii2 extension by CleanTalk with protection against spam bots and manual spam", 4 | "keywords": [ 5 | "yii2", 6 | "cleantalk", 7 | "antispam" 8 | ], 9 | "type": "yii2-extension", 10 | "require": { 11 | "cleantalk/php-antispam": "~2.0.0" 12 | }, 13 | "require-dev": { 14 | "yiisoft/yii2": "~2.0.0", 15 | "phpunit/phpunit": "~4.6.9" 16 | }, 17 | "license": "GPL-3.0", 18 | "authors": [ 19 | { 20 | "name": "psrustik", 21 | "email": "psrustik@gmail.com" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-4": {"cleantalk\\antispam\\": ""} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/ComponentTest.php: -------------------------------------------------------------------------------- 1 | component = Yii::createObject(['class' => CleantalkComponent::className(), 'apiKey' => CLEANTALK_TEST_API_KEY]); 28 | } 29 | 30 | /** 31 | * @expectedException \yii\base\InvalidConfigException 32 | */ 33 | public function testInit() 34 | { 35 | Yii::createObject(['class' => CleantalkComponent::className(), 'apiKey' => null]); 36 | } 37 | 38 | public function testIsAllowUser() 39 | { 40 | $mock = $this->getSendRequestMock( 41 | array( 42 | 'allow' => 1, 43 | 'comment' => '' 44 | ) 45 | ); 46 | list($result, $comment) = $mock->isAllowUser('allow@email.ru', 'Ivan Petrov'); 47 | $this->assertTrue($result); 48 | 49 | $mock = $this->getSendRequestMock( 50 | array( 51 | 'allow' => 0, 52 | 'comment' => 'Mock deny' 53 | ) 54 | ); 55 | list($result, $comment) = $mock->isAllowUser('deny@email.ru', 'Ivan Petrov'); 56 | $this->assertFalse($result); 57 | $this->assertEquals('Mock deny', $comment); 58 | 59 | $mock = $this->getSendRequestMock( 60 | array( 61 | 'inactive' => 1, 62 | 'comment' => 'Mock deny' 63 | ) 64 | ); 65 | list($result, $comment) = $mock->isAllowUser('deny@email.ru', 'Ivan Petrov'); 66 | $this->assertFalse($result); 67 | } 68 | 69 | public function testIsAllowMessage() 70 | { 71 | $mock = $this->getSendRequestMock( 72 | array( 73 | 'allow' => 1, 74 | 'comment' => '' 75 | ) 76 | ); 77 | 78 | list($result, $comment) = $mock->isAllowMessage('message1'); 79 | $this->assertTrue($result); 80 | 81 | $mock = $this->getSendRequestMock( 82 | array( 83 | 'allow' => 0, 84 | 'comment' => 'Mock deny' 85 | ) 86 | ); 87 | list($result, $comment) = $mock->isAllowMessage('bad message'); 88 | $this->assertFalse($result); 89 | $this->assertEquals('Mock deny', $comment); 90 | } 91 | 92 | public function testGetCheckJsCode() 93 | { 94 | $this->assertRegExp('#\w+#', $this->component->getCheckJsCode()); 95 | } 96 | 97 | 98 | public function testStartFormSubmitTime() 99 | { 100 | $this->component->startFormSubmitTime(''); 101 | sleep(2); 102 | $time = $this->component->calcFormSubmitTime(''); 103 | $this->assertGreaterThanOrEqual(1, $time); 104 | } 105 | 106 | 107 | public function testIsJavascriptEnable() 108 | { 109 | Yii::$app->request->setBodyParams(['ct_checkjs' => $this->component->getCheckJsCode()]); 110 | $this->assertEquals(1, $this->component->isJavascriptEnable()); 111 | 112 | Yii::$app->request->setBodyParams([]); 113 | $this->assertEquals(0, $this->component->isJavascriptEnable()); 114 | } 115 | 116 | public function testCreateRequest() 117 | { 118 | $class = new ReflectionClass($this->component); 119 | $method = $class->getMethod('createRequest'); 120 | $method->setAccessible(true); 121 | $request = $method->invoke($this->component); 122 | $this->assertInstanceOf('CleantalkRequest', $request); 123 | } 124 | 125 | /** 126 | * @expectedException InvalidArgumentException 127 | */ 128 | public function testSendRequest() 129 | { 130 | $class = new ReflectionClass($this->component); 131 | $method = $class->getMethod('sendRequest'); 132 | $method->setAccessible(true); 133 | $method->invoke($this->component, new CleantalkRequest(), 'ololo'); 134 | } 135 | 136 | protected function getSendRequestMock($response) 137 | { 138 | $mock = $this->getMock(CleantalkComponent::className(), ['sendRequest'], [['apiKey' => CLEANTALK_TEST_API_KEY]]); 139 | $mock->expects($this->once()) 140 | ->method('sendRequest') 141 | ->will( 142 | $this->returnValue( 143 | new CleantalkResponse( 144 | $response 145 | ) 146 | ) 147 | ); 148 | return $mock; 149 | } 150 | } -------------------------------------------------------------------------------- /tests/MessageValidatorTest.php: -------------------------------------------------------------------------------- 1 | getMock(CleantalkComponent::className(), ['sendRequest'], [['apiKey' => CLEANTALK_TEST_API_KEY]]); 19 | $mock->expects($this->once()) 20 | ->method('sendRequest') 21 | ->will( 22 | $this->returnValue( 23 | new CleantalkResponse( 24 | [ 25 | 'allow' => $allow, 26 | 'comment' => $message, 27 | ] 28 | ) 29 | ) 30 | ); 31 | 32 | Yii::$app->set('antispam', $mock); 33 | } 34 | 35 | public function testValidator() 36 | { 37 | $this->setResponse(0, 'Forbidden'); 38 | 39 | $model = new FakeModel(); 40 | $model->message = 'example1'; 41 | $model->validate(); 42 | $this->assertTrue($model->hasErrors('message')); 43 | } 44 | 45 | public function testValidatorAllow() 46 | { 47 | $this->setResponse(1, ''); 48 | 49 | $model = new FakeModel(); 50 | $model->message = 'example1'; 51 | $model->validate(); 52 | $this->assertFalse($model->hasErrors('message')); 53 | } 54 | } 55 | 56 | class FakeModel extends Model 57 | { 58 | public $message; 59 | 60 | public function rules() 61 | { 62 | return [ 63 | ['message', MessageValidator::className()] 64 | ]; 65 | } 66 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'test-yii2-antispam', 'basePath' => __DIR__]); -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | ../ 10 | 11 | ../vendor 12 | ../tests 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /validators/MessageValidator.php: -------------------------------------------------------------------------------- 1 | get($this->apiComponentId); 30 | $email = $nick = ''; 31 | if ($this->emailAttribute) { 32 | $email = $model->{$this->emailAttribute}; 33 | } 34 | if ($this->nickNameAttribute) { 35 | $nick = $model->{$this->nickNameAttribute}; 36 | } 37 | list($valid, $comment) = $api->isAllowMessage($model->$attribute, $email, $nick); 38 | if (!$valid) { 39 | if ($this->message !== null) { 40 | $comment = $this->message; 41 | } 42 | $this->addError($model, $attribute, $comment); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /validators/UserValidator.php: -------------------------------------------------------------------------------- 1 | get($this->apiComponentId); 28 | $nick = ''; 29 | if ($this->nickNameAttribute) { 30 | $nick = $model->{$this->nickNameAttribute}; 31 | } 32 | list($valid, $comment) = $api->isAllowUser($model->$attribute, $nick); 33 | if (!$valid) { 34 | if ($this->message !== null) { 35 | $comment = $this->message; 36 | } 37 | $this->addError($model, $attribute, $comment); 38 | } 39 | } 40 | } --------------------------------------------------------------------------------