diff --git a/README.md b/README.md index 0e8010f..40a977c 100644 --- a/README.md +++ b/README.md @@ -4,91 +4,70 @@ php-antispam [![Latest Stable Version](https://poser.pugx.org/cleantalk/php-antispam/v)](https://packagist.org/packages/cleantalk/php-antispam) -A PHP API for antispam service cleantalk.org. Invisible protection from spam, no captches, no puzzles, no animals and no math. +## The Invisible protection from spam, no captcha, no recaptcha, no puzzles, no math captcha. +_API for antispam service cleantalk.org_ -## How API stops spam? -API uses several simple tests to stop spammers. - * Spam bots signatures. - * Blacklists checks by Email, IP, web-sites domain names. - * JavaScript availability. - * Relevance test for the comment. +#### Requirements +* PHP 5.6 and above +* CURL support -## How API works? -API sends a comment's text and several previous approved comments to the servers. Servers evaluates the relevance of the comment's text on the topic, tests on spam and finaly provides a solution - to publish or put on manual moderation of comments. If a comment is placed on manual moderation, the plugin adds to the text of a comment explaining the reason for the ban server publishing. +### How we stop spam? +PHP Anti-Spam library providing invisible spam protection for your websites, registration forms, and comment sections. CleanTalk API offers an effective CAPTCHA alternative that silently blocks spam without interrupting your users' experience. -## Requirements +When users submit forms on your website form, the form data is securely sent to CleanTalk’s cloud servers. CleanTalk analyzes submissions using advanced heuristics. CleanTalk then returns a real-time verdict— legitimate requests or spam. - * PHP 5.6 and above - * CURL support +You are free to do anything with spam, or just allow as to block spam (we will interrupt desirable request). -You can unpack the archive with the plugin to the root of the site or install it using the composer +## Interesting? Let's make some settings (it will take few minutes) + + +### Step 1 - install our SDK (2 variants ability) + +Through composer install **OR** through download zip arhive and unzip it to root directory (with your index.php) ```php composer require cleantalk/php-antispam ``` - -### Sample SPAM test for text comment - -Notice: You can use the example PHP file from ./examples/form_with_handler -```php -handle(); +``` -//require_once "lib/cleantalk-php-patch.php"; -- PHP-FPM +### Step 2.1 - add js lib to your html template +```html + +``` +_Need for gathering frontend data._ -$cleantalk_antispam = new CleantalkAntispam($apikey, $email_field, $user_name_field, $message_field, $type_form); -$api_result = $cleantalk_antispam->handle(); -if ($api_result) { // the check fired - if ($api_result->account_status !== 1) { - // something wrong with your key or license, to know why read $api_result->codes - echo 'Allowed. Spam protection disabled.'; // or do nothing - } else { - if ($api_result->allow === 1) { - echo 'Allowed. Spam protection OK.'; // or do nothing - } else { - die('Blocked. Spam protection OK. Reason: ' . $api_result->comment); // or make your own handler - } - } +### Step 3 - do whatever you want with cloud result +For example add die block for spam. +```php +if ($api_result && $api_result->allow === 0) { + die('Blocked. Spam protection OK. Reason: ' . $api_result->comment); + // or make your own actions/logs/messages ... } -// your further code flow here -?> - -
- - -
- - -
- - -
- -
- -frontendScript(); ?> ``` -## API Response description -API returns (`$api_result`) PHP object: - * allow (0|1) - allow to publish or not, in other words spam or ham - * comment (string) - server comment for requests. - * id (string MD5 HEX hash) - unique request idenifier. - * errno (int) - error number. errno == 0 if requests successfull. - * errstr (string) - comment for error issue, errstr == null if requests successfull. - * account_status - 0 account disabled, 1 account enabled, -1 unknown status. +### Step 4 (not required) - we prepare for you special troubleshooting method +To find possible problems, just add follow snippet after getVerdict method. +```php +// TROUBLESHOOTING: logging the suggestions +error_log($cleantalk_antispam->whatsWrong(true)); +``` +In [example file](https://github.com/CleanTalk/php-antispam/blob/dev/examples/form_with_handler/form_with_handler.php) you can see context. + +### Step 5 (not required) - if you have any question, please, feel free to ask it in issue here or in our tiket system + +## Examples +- [api response description](https://github.com/CleanTalk/php-antispam/tree/dev/examples/api_response_description.md) +- [example with form handler](https://github.com/CleanTalk/php-antispam/blob/dev/examples/form_with_handler/form_with_handler.php) + ## Don't want to deal with all this? Universal solution for any CMS or custom website: https://github.com/CleanTalk/php-uni diff --git a/cleantalk-antispam.php b/cleantalk-antispam.php index b56e377..26928a4 100644 --- a/cleantalk-antispam.php +++ b/cleantalk-antispam.php @@ -1,8 +1,8 @@ valid; - -echo "Access key validation result:"; -echo CleantalkAPI::method__notice_validate_key($auth_key, 'php-api'); -echo "\n"; - -if (!$is_valid) { - echo "Access key is not valid. Please check access key in the config.\n"; - exit; -} - -// The facility in which to store the query parameters -$ct_request = new CleantalkRequest(); - -$ct_request->auth_key = $auth_key; -$ct_request->message = 'stop_word'; -$ct_request->sender_email = 'stop_email@example.com'; -$ct_request->sender_nickname = 'John Dow'; -$ct_request->example = str_repeat('Just text ', 10); -$ct_request->agent = 'php-api'; -$ct_request->sender_ip = '178.32.183.43'; -$ct_request->event_token = isset($_POST['ct_bot_detector_event_token']) ? $_POST['ct_bot_detector_event_token'] : null; - -$ct = new Cleantalk(); -$ct->server_url = $config_url; - -// Check -$ct_result = $ct->isAllowMessage($ct_request); - -if ( $ct_result->allow == 1 ) { - echo 'Comment allowed. Reason ' . $ct_result->comment; -} else { - echo 'Comment blocked. Reason ' . $ct_result->comment; -} diff --git a/examples/form_with_handler/form_with_handler.php b/examples/form_with_handler/form_with_handler.php index df2ff7b..929b1ab 100644 --- a/examples/form_with_handler/form_with_handler.php +++ b/examples/form_with_handler/form_with_handler.php @@ -1,47 +1,147 @@ handle(); -if ($api_result) { // the check fired - if ($api_result->account_status !== 1) { - // something wrong with your key or license, to know why read $api_result->codes - echo 'Allowed. Spam protection disabled.'; // or do nothing - } else { - if ($api_result->allow === 1) { - echo 'Allowed. Spam protection OK.'; // or do nothing - } else { - die('Blocked. Spam protection OK. Reason: ' . $api_result->comment); // or make your own handler +require_once '../../../php-antispam/cleantalk-antispam.php'; + +use CleanTalk\CleantalkAntispam; + +?> + + + + + + Contact Form + + + + + + + +

Contact Us

+ + handle(); + if ($api_result->allow === 0) { + $statusMessage = 'Spam detected - ' . $api_result->comment; + $messageType = 'error'; + } + + // TROUBLESHOOTING: logging the suggestions + error_log($cleantalk_antispam->whatsWrong(true)); + } + // END OF HANDLE CLEANTALK ANTISPAM + + if ($messageType !== 'error') { + $logEntry = date('Y-m-d H:i:s') . " | Name: $name | Email: $email | Message: $message\n"; + if (file_put_contents('./contacts.log', $logEntry, FILE_APPEND)) { + $statusMessage = 'Thank you for your message! We will get back to you soon.'; + $messageType = 'success'; + $name = $email = $message = ''; // Clear form data after successful submission + } } } -} -// your further code flow here -?> + ?> -
- - -
- - -
- - -
- -
+ +
+ +
+ + +
+
+ + +
-frontendScript(); ?> +
+ + +
+ +
+ + +
+ + +
+ + diff --git a/lib/Cleantalk.php b/lib/Cleantalk.php deleted file mode 100644 index ffa487c..0000000 --- a/lib/Cleantalk.php +++ /dev/null @@ -1,635 +0,0 @@ -filterRequest($request); - $filtered_request = $this->createMsg('check_message', $request); - - $this->sender_ip = $filtered_request->sender_ip; - $this->sender_email = $filtered_request->sender_email; - - return $this->httpRequest($filtered_request); - } - - /** - * Function checks whether it is possible to publish the message - * - * @param CleantalkRequest $request - * - * @return CleantalkResponse - * @throws TransportException - */ - public function isAllowUser(CleantalkRequest $request) - { - $request = $this->filterRequest($request); - $filtered_request = $this->createMsg('check_newuser', $request); - - $this->sender_ip = $filtered_request->sender_ip; - $this->sender_email = $filtered_request->sender_email; - - return $this->httpRequest($filtered_request); - } - - /** - * Function sends the results of manual moderation - * - * @param CleantalkRequest $request - * - * @return CleantalkResponse - * @throws TransportException - */ - public function sendFeedback(CleantalkRequest $request) - { - $request = $this->filterRequest($request); - $filtered_request = $this->createMsg('send_feedback', $request); - - $this->sender_ip = $filtered_request->sender_ip; - $this->sender_email = $filtered_request->sender_email; - - return $this->httpRequest($filtered_request); - } - - /** - * Function checks if visitor is bot or not based on the Bot-detector event token. - * - * @param CleantalkRequest $request - * - * @return CleantalkResponse - * @throws TransportException - */ - public function checkBot(CleantalkRequest $request) - { - $request = $this->filterRequest($request); - $filtered_request = $this->createMsg('check_bot', $request); - - return $this->httpRequest($filtered_request); - } - - /** - * Filter request params - * - * @param CleantalkRequest $request - * - * @return CleantalkRequest - */ - private function filterRequest(CleantalkRequest $request) - { - // general and optional - foreach ( $request as $param => $value ) { - if ( $param == 'js_on' ) { - if ( ! is_int($value) ) { - $request->$param = null; - } - } - if ( $param == 'submit_time' ) { - if ( ! is_int($value) ) { - $request->$param = null; - } - } - if ( $param == 'message' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } // Should be array, but servers understand only JSON - if ( $param == 'example' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } // Should be array, but servers understand only JSON - if ( $param == 'sender_info' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } // Should be array, but servers understand only JSON - if ( $param == 'post_info' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } // Should be array, but servers understand only JSON - if ( $param == 'agent' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } - if ( $param == 'sender_nickname' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } - if ( $param == 'phone' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } - if ( $param == 'sender_email' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } - if ( $param == 'sender_ip' ) { - if ( ! is_string($value) ) { - $request->$param = null; - } - } - } - - return $request; - } - - /** - * Compress data and encode to base64 - * - * @param string $data - * @psalm-suppress UnusedMethod - * @return string - */ - private function compressData($data = '') - { - if ( strlen($data) > $this->dataMaxSise && function_exists('gzencode') && function_exists('base64_encode') ) { - $localData = gzencode($data, $this->compressRate, FORCE_GZIP); - - if ( $localData === false ) { - return $data; - } - - $localData = base64_encode($localData); - - if ( $localData === false ) { - return $data; - } - - return $localData; - } - - return $data; - } - - /** - * Create msg for cleantalk server - * - * @param string $method - * @param CleantalkRequest $request - * - * @return CleantalkRequest - */ - private function createMsg($method, CleantalkRequest $request) - { - switch ($method) { - case 'check_message': - // Convert strings to UTF8 - $request->message = CleantalkHelper::stringToUTF8($request->message, $this->data_codepage); - $request->example = CleantalkHelper::stringToUTF8($request->example, $this->data_codepage); - $request->sender_email = CleantalkHelper::stringToUTF8($request->sender_email, $this->data_codepage); - $request->sender_nickname = CleantalkHelper::stringToUTF8( - $request->sender_nickname, - $this->data_codepage - ); - - // $request->message = $this->compressData($request->message); - // $request->example = $this->compressData($request->example); - break; - - case 'check_newuser': - // Convert strings to UTF8 - $request->sender_email = CleantalkHelper::stringToUTF8($request->sender_email, $this->data_codepage); - $request->sender_nickname = CleantalkHelper::stringToUTF8( - $request->sender_nickname, - $this->data_codepage - ); - break; - - case 'send_feedback': - if ( is_array($request->feedback) ) { - $request->feedback = implode(';', $request->feedback); - } - break; - } - - $request->method_name = $method; - - // Removing non UTF8 characters from request, because non UTF8 or malformed characters break json_encode(). - foreach ( $request as $param => $value ) { - if ( is_array($request->$param) ) { - $request->$param = CleantalkHelper::removeNonUTF8FromArray($value); - } - if ( is_string($request->$param) || is_int($request->$param) ) { - $request->$param = CleantalkHelper::removeNonUTF8FromString($value); - } - } - - return $request; - } - - /** - * Send JSON request to servers - * - * @param $data - * @param $url - * @param int $server_timeout - * - * @return boolean|CleantalkResponse - */ - private function sendRequest($data, $url, $server_timeout = 3) - { - // Convert to array - $data = (array)json_decode(json_encode($data), true); - - //Cleaning from 'null' values - $tmp_data = array(); - foreach ( $data as $key => $value ) { - if ( $value !== null ) { - $tmp_data[$key] = $value; - } - } - $data = $tmp_data; - unset($key, $value, $tmp_data); - - // Convert to JSON - $data = json_encode($data); - - if ( isset($this->api_version) ) { - $url = $url . $this->api_version; - } - - // Switching to secure connection - if ( $this->ssl_on && ! preg_match("/^https:/", $url) ) { - $url = preg_replace("/^(http)/i", "$1s", $url); - } - - $result = false; - $curl_error = null; - if ( function_exists('curl_init') ) { - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_TIMEOUT, $server_timeout); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // receive server response ... - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); // resolve 'Expect: 100-continue' issue - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disabling CA cert verivication and - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // Disabling common name verification - - if ( $this->ssl_on && $this->ssl_path != '' ) { - curl_setopt($ch, CURLOPT_CAINFO, $this->ssl_path); - } - - $result = curl_exec($ch); - if ( ! $result ) { - $curl_error = curl_error($ch); - // Use SSL next time, if error occurs. - if ( ! $this->ssl_on ) { - $this->ssl_on = true; - return $this->sendRequest($data, $url, $server_timeout); - } - } - - curl_close($ch); - } - - if ( ! $result ) { - $allow_url_fopen = ini_get('allow_url_fopen'); - if ( function_exists('file_get_contents') && !empty($allow_url_fopen) && $allow_url_fopen == '1' ) { - $opts = array( - 'http' => - array( - 'method' => 'POST', - 'header' => "Content-Type: text/html\r\n", - 'content' => $data, - 'timeout' => $server_timeout - ) - ); - - $context = stream_context_create($opts); - $result = @file_get_contents($url, false, $context); - } - } - - if ( !is_string($result) || ! CleantalkHelper::is_json($result) ) { - $response = null; - $response['errno'] = 1; - if ( $curl_error ) { - $response['errstr'] = sprintf("CURL error: '%s'", $curl_error); - } else { - $response['errstr'] = 'No CURL support compiled in'; - } - $response['errstr'] .= ' or disabled allow_url_fopen in php.ini.'; - $response = json_decode(json_encode($response)); - - return $response; - } - - $errstr = null; - $response = json_decode($result); - if ( is_object($response) ) { - $response->errno = 0; - $response->errstr = $errstr; - } else { - $errstr = 'Unknown response from ' . $url . '.' . ' ' . $result; - - $response = null; - $response['errno'] = 1; - $response['errstr'] = $errstr; - $response = json_decode(json_encode($response)); - } - - - return $response; - } - - /** - * httpRequest - * - * @param $msg - * - * @return CleantalkResponse - * @throws TransportException - */ - private function httpRequest($msg) - { - // Wiping session cookies from request - - $ct_tmp = apache_request_headers(); - - if ( isset($ct_tmp['Cookie']) ) { - $cookie_name = 'Cookie'; - } elseif ( isset($ct_tmp['cookie']) ) { - $cookie_name = 'cookie'; - } else { - $cookie_name = 'COOKIE'; - } - - if ( isset($ct_tmp[$cookie_name]) ) { - unset($ct_tmp[$cookie_name]); - } - - $msg->all_headers = ! empty($ct_tmp) ? json_encode($ct_tmp) : ''; - - // Using current server without changing it - if ( ! empty($this->work_url) && ($this->server_changed + $this->server_ttl > time()) ) { - $url = ! empty($this->work_url) ? $this->work_url : $this->server_url; - $result = $this->sendRequest($msg, $url, $this->server_timeout); - } else { - $result = false; - } - - // Changing server - if ($result === false || $result->errno != 0) { - // Split server url to parts - preg_match("@^(https?://)([^/:]+)(.*)@i", $this->server_url, $matches); - - $url_prefix = isset($matches[1]) ? $matches[1] : ''; - $url_host = isset($matches[2]) ? $matches[2] : ''; - $url_suffix = isset($matches[3]) ? $matches[3] : ''; - - if ( empty($url_host) ) { - throw TransportException::fromUrlHostError($url_host); - } elseif ( null !== $servers = $this->get_servers_ip($url_host) ) { - // Loop until find work server - foreach ( $servers as $server ) { - $this->work_url = $url_prefix . $server['ip'] . $url_suffix; - $this->server_ttl = $server['ttl']; - - $result = $this->sendRequest($msg, $this->work_url, $this->server_timeout); - - if ( $result !== false && $result->errno === 0 ) { - $this->server_change = true; - break; - } - } - } else { - throw TransportException::fromUrlHostError($url_host); - } - } - - $response = new CleantalkResponse(null, $result); - - $response->sender_ip = $this->sender_ip; - $response->sender_email = $this->sender_email; - - if ( ! empty($this->data_codepage) && $this->data_codepage !== 'UTF-8' ) { - if ( ! empty($response->comment) ) { - $response->comment = CleantalkHelper::stringFromUTF8($response->comment, $this->data_codepage); - } - if ( ! empty($response->errstr) ) { - $response->errstr = CleantalkHelper::stringFromUTF8($response->errstr, $this->data_codepage); - } - if ( ! empty($response->sms_error_text) ) { - $response->sms_error_text = CleantalkHelper::stringFromUTF8($response->sms_error_text, $this->data_codepage); - } - } - - return $response; - } - - /** - * Function DNS request - * - * @param $host - * - * @return array|null - */ - private function get_servers_ip($host) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - if ( ! isset($host) ) { - return null; - } - - $servers = array(); - - // Get DNS records about URL - if ( function_exists('dns_get_record') ) { - $records = dns_get_record($host, DNS_A); - if ( $records !== false ) { - foreach ( $records as $server ) { - $servers[] = $server; - } - } - } - - // Another try if first failed - if ( count($servers) == 0 && function_exists('gethostbynamel') ) { - $records = gethostbynamel($host); - if ( $records !== false ) { - foreach ( $records as $server ) { - $servers[] = array( - "ip" => $server, - "host" => $host, - "ttl" => $this->server_ttl - ); - } - } - } - - // If couldn't get records - if ( count($servers) == 0 ) { - $servers[] = array( - "ip" => null, - "host" => $host, - "ttl" => $this->server_ttl - ); - // If records recieved - } else { - $tmp = array(); - $fast_server_found = false; - - foreach ( $servers as $server ) { - if ( $fast_server_found ) { - $ping = $this->max_server_timeout; - } else { - $ping = $this->httpPing($server['ip']); - $ping = $ping * 1000; - } - - $tmp[(int)$ping] = $server; - - $fast_server_found = $ping < $this->min_server_timeout ? true : false; - } - - ksort($tmp); - $response = $tmp; - } - - return empty($response) ? null : $response; - } - - /** - * Function to check response time - * param string - * @return float - */ - private function httpPing($host) - { - // Skip localhost ping cause it raise error at fsockopen. - // And return minimun value - if ( $host == 'localhost' ) { - return 0.001; - } - - $starttime = microtime(true); - $file = @fsockopen($host, 443, $errno, $errstr, $this->max_server_timeout / 1000); - $stoptime = microtime(true); - - if ( ! $file ) { - $status = $this->max_server_timeout / 1000; // Site is down - } else { - fclose($file); - $status = ($stoptime - $starttime); - $status = round($status, 4); - } - - return $status; - } -} diff --git a/lib/CleantalkAPI.php b/lib/CleantalkAPI.php deleted file mode 100644 index 70291da..0000000 --- a/lib/CleantalkAPI.php +++ /dev/null @@ -1,299 +0,0 @@ - 'get_api_key', - 'product_name' => 'antispam', - 'email' => $email, - 'website' => $host, - 'platform' => $platform, - 'timezone' => $timezone, - 'http_accept_language' => $language, - 'user_ip' => $ip, - 'hoster_whitelabel' => $white_label, - 'hoster_api_key' => $hoster_api_key, - ); - - $result = self::send_request($request); - $result = $do_check ? self::check_response($result, 'get_api_key') : $result; - - return $result; - } - - /** - * Function gets spam report - * - * @param string $host website host - * @param integer $period report days - * @param bool $do_check do_check - * - * @return string - */ - public static function method__get_antispam_report($host, $period = 1, $do_check = true) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $request = array( - 'method_name' => 'get_antispam_report', - 'hostname' => $host, - 'period' => $period - ); - - $result = self::send_request($request); - $result = $do_check ? self::check_response($result, 'get_antispam_report') : $result; - - return $result; - } - - /** - * Function gets spam statistics - * - * @param string $api_key - * @param bool $do_check - * - * @return string - */ - public static function method__get_antispam_report_breif($api_key, $do_check = true) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $request = array( - 'method_name' => 'get_antispam_report_breif', - 'auth_key' => $api_key, - ); - - $result = self::send_request($request); - $result = $do_check ? self::check_response($result, 'get_antispam_report_breif') : $result; - - return $result; - } - - /** - * Function gets information about renew notice - * - * @param string $api_key - * @param string $path_to_cms - * @param bool $do_check - * - * @return string - */ - public static function method__notice_validate_key($api_key = '', $path_to_cms = '', $do_check = true) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $request = array( - 'method_name' => 'notice_validate_key', - 'auth_key' => $api_key, - 'path_to_cms' => $path_to_cms - ); - - $result = self::send_request($request); - $result = $do_check ? self::check_response($result, 'notice_validate_key') : $result; - - return $result; - } - - /** - * Function gets information about renew notice - * - * @param string api_key - * - * @return string - */ - public static function method__notice_paid_till($api_key, $do_check = true) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $request = array( - 'method_name' => 'notice_paid_till', - 'auth_key' => $api_key - ); - - $result = self::send_request($request); - $result = $do_check ? self::check_response($result, 'notice_paid_till') : $result; - - return $result; - } - - /** - * Function sends raw request to API server - * - * @param array $data to send - * @param string $url of API server - * @param int $timeout is data have to be JSON encoded or not - * @param bool $ssl should use ssl - * @psalm-suppress PossiblyUnusedParam - * @return string JSON encoded string - */ - public static function send_request($data, $url = self::URL, $timeout = 5, $ssl = false) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - // Possibility to switch API url - $url = defined('CLEANTALK_API_URL') ? CLEANTALK_API_URL : $url; - - // Adding agent version to data - if ( defined('CLEANTALK_AGENT') ) { - $data['agent'] = CLEANTALK_AGENT; - } - - // Make URL string - $data_string = http_build_query($data); - $data_string = str_replace("&", "&", $data_string); - - if ( function_exists('curl_init') ) { - $ch = curl_init(); - - // Set diff options - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); - - // Switch on/off SSL - if ( $ssl === true ) { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - } else { - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - } - - // Make a request - $result = curl_exec($ch); - $errors = curl_error($ch); - curl_close($ch); - - // Get cURL error if result failed - if ( $result === false ) { - // And retry with SSL enabled - if ( $ssl === false ) { - return self::send_request($data, $url, $timeout, true); - } - } - } else { - $errors = 'CURL_NOT_INSTALLED'; - } - - // Trying to use file_get_contents() to make a API call - if ( ! empty($errors) && ini_get('allow_url_fopen') ) { - $opts = array( - 'http' => array( - 'method' => "POST", - 'timeout' => $timeout, - 'content' => $data_string, - ) - ); - $context = stream_context_create($opts); - $result = file_get_contents($url, false, $context); - } else { - $errors .= '_AND_ALLOW_URL_FOPEN_IS_DISABLED'; - } - - if ( empty($result) && ! empty($errors) ) { - $json_error = json_encode(array('error' => true, 'error_string' => $errors)); - return false !== $json_error ? $json_error : 'CURL_ERROR'; - } - - return $result; - } - - /** - * Function checks server response - * - * @param string $result - * @param string|null $method_name - * - * @return string JSON encoded string - */ - public static function check_response($result, $method_name = null) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $out = array('error' => false); - - // Errors handling - - // Bad connection - if ( empty($result) ) { - $out = array( - 'error' => true, - 'error_string' => 'CONNECTION_ERROR' - ); - } - - // JSON decode errors - $result = json_decode($result, true); - if ( empty($result) ) { - $out = array( - 'error' => true, - 'error_string' => 'JSON_DECODE_ERROR' - ); - } - - // cURL error - if ( ! empty($result['error']) ) { - $out = array( - 'error' => true, - 'error_string' => 'CONNECTION_ERROR: ' . $result['error_string'], - ); - } - - // Server errors - if ( $result && (isset($result['error_no']) || isset($result['error_message'])) ) { - $out = array( - 'error' => true, - 'error_string' => "SERVER_ERROR NO: {$result['error_no']} MSG: {$result['error_message']}", - 'error_no' => $result['error_no'], - 'error_message' => $result['error_message'] - ); - } - - // Patches for different methods - if ( !$out['error'] ) { - // mehod_name = notice_validate_key - if ( $method_name == 'notice_validate_key' && isset($result['valid']) ) { - $out = $result; - } - - // Other methods - if ( isset($result['data']) && is_array($result['data']) ) { - $out = $result['data']; - } - } - - // method_name = get_antispam_report_breif - if ( $method_name == 'get_antispam_report_breif' ) { - if ( !$out['error'] ) { - $result = $result['data']; - } - - for ( $tmp = array(), $i = 0; $i < 7; $i++ ) { - $tmp[date('Y-m-d', time() - 86400 * 7 + 86400 * $i)] = 0; - } - - $result['spam_stat'] = array_merge($tmp, isset($result['spam_stat']) ? $result['spam_stat'] : array()); - $result['top5_spam_ip'] = isset($result['top5_spam_ip']) ? $result['top5_spam_ip'] : array(); - $out = array_merge($result, $out); - } - - $out = json_encode($out); - - return false !== $out ? $out : 'JSON_ENCODE_ERROR'; - } -} diff --git a/lib/CleantalkAntispam.php b/lib/CleantalkAntispam.php index 5226ad2..88c2abe 100644 --- a/lib/CleantalkAntispam.php +++ b/lib/CleantalkAntispam.php @@ -1,73 +1,626 @@ apikey = $apikey; - $this->email_field = $email_field; - $this->user_name_field = $user_name_field; - $this->message_field = $message_field; - $this->type_form = $type_form; - } - - /** - * @return CleantalkResponse|null - * @throws TransportException + const MODERATE_URL = 'https://moderate.cleantalk.org/api2.0'; + const BOT_DETECTOR_LIBRARY_URL = 'https://moderate.cleantalk.org/ct-bot-detector-wrapper.js'; + const EVENT_TOKEN_FIELD_NAME = 'ct_bot_detector_event_token'; + const EMAIL_ADDRESS_REGEXP = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; + + /** + * @var string Access key for CleanTalk API + */ + private $access_key; + /** + * @var string Event token for CleanTalk API + */ + private $event_token; + /** + * @var bool + */ + private $block_no_js_visitor = false; + /** + * @var string Nickname of the sender + */ + private $nickname; + /** + * @var string Email of the sender + */ + private $email; + + /** + * @var string Message content + */ + private $message; + + /** + * @var string IP address of the sender + */ + private $ip; + + /** + * @var string Method name for the CleanTalk API request + */ + private $method_name; + + /** + * @var CleantalkVerdict Verdict object to store the result of the CleanTalk check + */ + private $verdict; + + /** + * @var array Suggestions for improving the data quality + */ + private $improvement_suggestions = array(); + + /** + * @var array Stack of method calls for fluid interface + */ + private $fluid_call_stack = array(); + + /** + * @var CleantalkResponse Response object from the CleanTalk API + */ + private $cleantalk_response; + + /** + * @var false|string CleanTalk request data in JSON format + */ + private $cleantalk_request_data; + + /** + * @var array Data container to get the data from the form, usually is $_POST + */ + private $data_container; + + /** + * ------------ Construction kit methods ------------ + */ + + /** + * Constructor for the CleanTalkCheck class. + * + * @param string $accessKey Access key for CleanTalk API + * @psalm-suppress PossiblyUnusedParam + */ + public function __construct($accessKey, $email = '', $user_name_field = '', $message_field = '', $type_form = '') + { + $this->access_key = $accessKey; + $this->verdict = new CleantalkVerdict(); + //default data preparing + $this->data_container = $_POST; + if (isset($this->data_container[static::EVENT_TOKEN_FIELD_NAME])) { + $this->event_token = $this->data_container[static::EVENT_TOKEN_FIELD_NAME]; + } + $this->ip = Helper::ipGet(); + if ($email) { + $this->email = $email; + } else { + $this->setEmailAutomatically(); + } + } + + private function setEmailAutomatically() + { + foreach ($this->data_container as $_key => $value) { + if (preg_match(static::EMAIL_ADDRESS_REGEXP, $value)) { + $this->email = $value; + break; + } + } + } + + /** + * ------------ Frontend methods ------------ + */ + + /** + * Get the frontend HTML code for the CleanTalk bot detector. + * + * @param bool $warn_if_js_disabled Flag to include a warning if JavaScript is disabled + * @return string HTML code + */ + public static function getFrontendHTMLCode($warn_if_js_disabled = false) + { + $warn = $warn_if_js_disabled ? '' : ''; + $html = '%s'; + return sprintf($html, static::BOT_DETECTOR_LIBRARY_URL, $warn); + } + + /** + * ------------ Internal processing methods ------------ + */ + + /** + * Get the response from the CleanTalk API. + * + * @return CleantalkResponse Response object from the CleanTalk API + */ + private function getCleanTalkResponse() + { + $http = new Request(); + $this->cleantalk_request_data = $this->prepareCleanTalkRequestData(); + $response_raw = $http->setUrl(static::MODERATE_URL) + ->setData($this->cleantalk_request_data) + ->request(); + + /** @psalm-suppress InvalidArgument */ + return new CleantalkResponse(@json_decode($response_raw), null); + } + + /** + * Prepare the request data for the CleanTalk API. + * + * @return string JSON encoded request data + */ + private function prepareCleanTalkRequestData() + { + $data = array( + 'method_name' => $this->method_name, + 'auth_key' => $this->access_key, + 'message' => $this->message, + 'sender_nickname' => $this->nickname, + 'sender_email' => $this->email, + 'sender_ip' => $this->ip, + 'js_on' => !empty($this->event_token) ? 1 : 0, + 'event_token' => $this->event_token, + 'agent' => 'php-cleantalk-check', + 'sender_info' => @json_encode( + array( + 'REFFERRER' => !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', + ) + ), + ); + return @json_encode($data); + } + + /** + * Get the CleanTalk request data. + * + * @return string JSON encoded request data + */ + public function getCleanTalkRequestData() + { + if (empty($this->cleantalk_request_data)) { + return ''; + } + $data = @json_decode($this->cleantalk_request_data); + return $data ?: ''; + } + + /** + * Verify the data before sending the request to CleanTalk API. + */ + private function verifyData() + { + try { + $this->checkAccessKey(); + } catch (\Exception $e) { + $this->verdict->error = $e->getMessage(); + $this->verdict->allow = true; + } + + try { + $this->checkEventToken(); + } catch (\Exception $e) { + if ($this->block_no_js_visitor) { + $this->verdict->error = $e->getMessage(); + $this->verdict->allow = false; + $this->verdict->comment = 'Please, enable JavaScript to process the form.'; + } else { + $this->verdict->allow = true; + } + } + } + + /** + * Check if the access key is valid. + * + * @throws \Exception If the access key is invalid + */ + private function checkAccessKey() + { + if (empty($this->access_key)) { + throw new \Exception('Access key is empty'); + } + + if (!is_string($this->access_key)) { + throw new \Exception('Access key is not a string'); + } + } + + /** + * Check if the event token is valid. + * + * @throws \Exception If the event token is invalid + */ + private function checkEventToken() + { + if (empty($this->event_token)) { + throw new \Exception('Event token is empty'); + } + + if (!is_string($this->event_token)) { + throw new \Exception('Event token is not a string'); + } + + if (strlen($this->event_token) !== 64) { + throw new \Exception('Event token is not valid'); + } + } + + /** + * Perform actions before returning the verdict. + * + * @return CleantalkVerdict Verdict object with the result of the CleanTalk check + */ + private function beforeReturnVerdict() + { + $this->setImprovementSuggestions(); + return $this->verdict; + } + + /** + * ------------ Fluid public calls ------------ + */ + + /** + * Set the email of the sender. + * + * @param string $email Email of the sender + * @return $this + */ + public function setEmail($email) + { + $this->fluidCallStack(__FUNCTION__); + $this->email = is_string($email) ? $email : null; + return $this; + } + + /** + * Set the nickname of the sender. + * + * @param string $nickname Nickname of the sender + * @return $this + */ + public function setNickName($nickname) + { + $this->fluidCallStack(__FUNCTION__); + $this->nickname = is_string($nickname) ? $nickname : null; + return $this; + } + + /** + * Set the message content. + * + * @param string $message Message content + * @return $this + */ + public function setMessage($message) + { + $this->fluidCallStack(__FUNCTION__); + $this->message = is_string($message) ? $message : null; + return $this; + } + + /** + * Set the IP address of the sender. + * + * @param string|null $ip IP address of the sender + * @return $this + */ + public function setIP($ip = null) + { + $this->fluidCallStack(__FUNCTION__); + if (!Helper::ipValidate($ip)) { + $this->setImprovementSuggestion('critical', 'IP address is not valid, the value has been set form the request', 'setIP()'); + } else { + $this->ip = $ip; + } + + return $this; + } + + /** + * Enable blocking of visitors without JavaScript. + * + * @return $this + */ + public function setDoBlockNoJSVisitor() + { + $this->fluidCallStack(__FUNCTION__); + $this->block_no_js_visitor = true; + return $this; + } + + /** + * Set the event token. + * + * @param string|null $event_token Event token + * @return $this + */ + public function setEventToken($event_token = null) + { + $this->fluidCallStack(__FUNCTION__); + $this->event_token = $event_token; + return $this; + } + + /** + * Use the registration check method. + * + * @return $this + */ + public function useRegistrationCheck() + { + $this->fluidCallStack(__FUNCTION__); + $this->method_name = 'check_newuser'; + return $this; + } + + /** + * Use the contact form check method. + * + * @return $this + */ + public function useContactFormCheck() + { + $this->fluidCallStack(__FUNCTION__); + $this->method_name = 'check_message'; + return $this; + } + + /** + * Set own data container. + * @param $data_container + * + * @return $this + */ + public function setCustomFormDataContainer($data_container) + { + $this->data_container = $data_container; + return $this; + } + + /** + * Get the verdict from the CleanTalk API. + * + * @return CleantalkVerdict Verdict object with the result of the CleanTalk check */ public function handle() { - if ( count($_POST) === 0 ) { - $_SESSION['ct_submit_time'] = time(); + $this->verifyData(); - return null; + if ($this->verdict->error) { + return $this->beforeReturnVerdict(); } - $sender_email = isset($_POST[$this->email_field]) ? $_POST[$this->email_field] : ''; - $sender_nickname = isset($_POST[$this->user_name_field]) ? $_POST[$this->user_name_field] : ''; - $message = isset($_POST[$this->message_field]) ? $_POST[$this->message_field] : ''; - $sender_ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; + if (empty($this->method_name)) { + $this->method_name = empty($this->message) ? 'check_newuser' : 'check_message'; + } - $ct_request = new CleantalkRequest(); + $this->cleantalk_response = $this->getCleanTalkResponse(); - $ct_request->auth_key = $this->apikey; - $ct_request->agent = 'php-api'; - $ct_request->sender_email = $sender_email; - $ct_request->sender_ip = $sender_ip; - $ct_request->sender_nickname = $sender_nickname; - $ct_request->message = $message; - $ct_request->submit_time = isset($_SESSION['ct_submit_time']) ? $_SESSION['ct_submit_time'] : null; - $ct_request->event_token = isset($_POST['ct_bot_detector_event_token']) ? $_POST['ct_bot_detector_event_token'] : null; + if ($this->cleantalk_response->error) { + /** @psalm-suppress InvalidOperand */ + $this->verdict->error = 'CleanTalk moderate server error: ' . $this->cleantalk_response->error; + return $this->beforeReturnVerdict(); + } + + $this->verdict->allow = $this->cleantalk_response->allow; + $this->verdict->comment = $this->cleantalk_response->comment; + $this->verdict->request_link = !empty($this->cleantalk_response->id) + ? 'https://cleantalk.org/my/show_requests?request_id=' . $this->cleantalk_response->id + : null + ; + + return $this->beforeReturnVerdict(); + } - $ct = new Cleantalk(); - $ct->server_url = $ct_request::CLEANTALK_API_URL; + /** + * ------------ Improvement suggestions feature ------------ + */ - // Check - if ( $this->type_form === 'signup' ) { - $ct_result = $ct->isAllowUser($ct_request); + /** + * Set suggestions for improving the data quality. + */ + private function setImprovementSuggestions() + { + if ( $this->cleantalk_response instanceof CleantalkResponse ) { + if (empty($this->cleantalk_response->comment)) { + $this->setImprovementSuggestion( + 'critical', + 'Something is wrong with connection to the CleanTalk server ' . static::MODERATE_URL, + 'getCleanTalkResponse()' + ); + return; + } + if ($this->cleantalk_response->error) { + $this->setImprovementSuggestion( + 'critical', + 'Please, check the error message from the CleanTalk server', + 'getCleanTalkResponse()' + ); + return; + } + if ($this->cleantalk_response->account_status !== 1) { + $this->setImprovementSuggestion( + 'critical', + 'Something wrong with your CleanTalk license, visit your CleanTalk dashboard to check the license status', + 'getCleanTalkResponse()' + ); + return; + } } - if ( $this->type_form === 'contact' ) { - $ct_result = $ct->isAllowMessage($ct_request); + + if (!empty($this->verdict->error)) { + $this->setImprovementSuggestion('critical', 'Error occurred due the CleanTalk check: ' . $this->verdict->error); } - return $ct_result; + if (empty($this->ip)) { + $fluid_method = 'setIP'; + $stack = !$this->fluidCallExist($fluid_method) + ? "interface method ->$fluid_method() has not been called" + : "interface method ->$fluid_method() has been called, but provided var is invalid or automatic IP get has failed"; + $this->setImprovementSuggestion( + 'average', + 'Please, provide the visitor IP address to improve check quality.', + $stack + ); + } + + if (empty($this->event_token)) { + $fluid_method = 'setEventToken'; + $stack = !$this->fluidCallExist($fluid_method) + ? "interface method ->$fluid_method() has not been called, make sure the form data contains the field " . static::EVENT_TOKEN_FIELD_NAME + : "interface method ->$fluid_method() has been called, but provided var is invalid"; + $common_token_message = 'Event token is not provided. Most likely the visitor has JavaScript disabled.'; + $this->setImprovementSuggestion('critical', $common_token_message, $stack); + + $fluid_method = 'setDoBlockNoJSVisitor'; + if ($this->fluidCallExist($fluid_method)) { + $stack = "seen the call ->$fluid_method()"; + $common_token_message .= ' All the visitors without token are BLOCKED due the current setting to block users without JS.'; + } else { + $stack = "interface method ->$fluid_method() has not been called"; + $common_token_message .= ' All the visitors without token are PASSED due the current setting to pass users without JS.'; + } + $this->setImprovementSuggestion('critical', $common_token_message, $stack); + } + + if (empty($this->access_key)) { + $this->setImprovementSuggestion( + 'critical', + 'Please, provide the access key via constructor call ' . __CLASS__ . '()', + 'construct()' + ); + } + + if (empty($this->email)) { + $fluid_method = 'setEmail'; + $stack = !$this->fluidCallExist($fluid_method) + ? "interface method ->$fluid_method() has not been called" + : "interface method ->$fluid_method() has been called, but provided var is invalid"; + $this->setImprovementSuggestion( + 'average', + 'Please, provide the email field content to improve check quality.', + $stack + ); + } + + if (empty($this->nickname)) { + $fluid_method = 'setNickName'; + $stack = !$this->fluidCallExist($fluid_method) + ? "interface method ->$fluid_method() has not been called" + : "interface method ->$fluid_method() has been called, but provided var is invalid"; + $this->setImprovementSuggestion( + 'average', + 'Please, provide the nickname field content to improve check quality.', + $stack + ); + } + + if (empty($this->message)) { + if ($this->fluidCallExist('useContactFormCheck')) { + if ($this->fluidCallExist('setMessage')) { + $stack = 'seen the call ->setMessage(), but provided value is invalid'; + } else { + $stack = 'seen the call ->useContactFormCheck(), but interface method ->setMessage() has not been called'; + } + $this->setImprovementSuggestion( + 'average', + 'Please, provide the message field to improve check quality.', + $stack + ); + } + } } - public function frontendScript() + /** + * Set an improvement suggestion. + * + * @param string $level Severity level of the suggestion + * @param string $message Suggestion message + * @param string|null $stack Call stack information + */ + private function setImprovementSuggestion($level, $message, $stack = null) { - echo ''; + $this->improvement_suggestions[$level][] = array('stack' => $stack, 'message' => $message); + } + + /** + * Get the improvement suggestions. + * + * @return array Improvement suggestions + */ + private function getImprovementSuggestions() + { + if (empty($this->improvement_suggestions)) { + return array('Everything looks well!'); + } + ksort($this->improvement_suggestions, SORT_STRING); + return $this->improvement_suggestions; + } + + /** + * Add a method to the fluid call stack. + * + * @param string $method Method name + */ + private function fluidCallStack($method) + { + $this->fluid_call_stack[] = $method; + } + + /** + * Check if a method exists in the fluid call stack. + * + * @param string $method Method name + * @return bool True if the method exists in the stack, false otherwise + */ + private function fluidCallExist($method) + { + return in_array($method, $this->fluid_call_stack); + } + + /** + * Get the suggestions for improving the data quality. + * @param $return_as_json + * + * @return false|string + */ + public function whatsWrong($return_as_json = false) + { + $array = array( + 'suggestions' => $this->getImprovementSuggestions(), + 'request_data' => $this->getCleanTalkRequestData(), + 'verdict' => $this->verdict instanceof CleantalkVerdict ? $this->verdict->getArray() : null, + ); + + if (empty($array['verdict'])) { + $array['verdict'] = 'Verdict is not processed. Maybe you forgot to call ->getVerdict() method before'; + } + + if ($return_as_json) { + return @json_encode($array); + } + + /** @psalm-suppress InvalidScalarArgument */ + $suggestions = var_export($array['suggestions'], 1); + /** @psalm-suppress InvalidScalarArgument */ + $data = var_export($array['request_data'], 1); + /** @psalm-suppress InvalidScalarArgument */ + $verdict = var_export($array['verdict'], 1); + echo "
Suggestions
$suggestions
"; + echo "
Request Data
$data
"; + echo "
Verdict
$verdict
"; + return ''; } } diff --git a/lib/CleantalkHelper.php b/lib/CleantalkHelper.php deleted file mode 100644 index cea3dfb..0000000 --- a/lib/CleantalkHelper.php +++ /dev/null @@ -1,415 +0,0 @@ - array( - 'ipv4' => array( - '103.21.244.0/22', - '103.22.200.0/22', - '103.31.4.0/22', - '104.16.0.0/12', - '108.162.192.0/18', - '131.0.72.0/22', - '141.101.64.0/18', - '162.158.0.0/15', - '172.64.0.0/13', - '173.245.48.0/20', - '185.93.231.18/20', // User fix - '185.220.101.46/20', // User fix - '188.114.96.0/20', - '190.93.240.0/20', - '197.234.240.0/22', - '198.41.128.0/17', - ), - 'ipv6' => array( - '2400:cb00::/32', - '2405:8100::/32', - '2405:b500::/32', - '2606:4700::/32', - '2803:f800::/32', - '2c0f:f248::/32', - '2a06:98c0::/29', - ), - ), - ); - - private static $private_networks = array( - '10.0.0.0/8', - '100.64.0.0/10', - '172.16.0.0/12', - '192.168.0.0/16', - '127.0.0.1/32', - ); - - /** - * Getting arrays of IP (REMOTE_ADDR, X-Forwarded-For, X-Real-Ip, Cf_Connecting_Ip) - * @returns array ('remote_addr' => 'val', ['x_forwarded_for' => 'val', ['x_real_ip' => 'val', ['cloud_flare' => 'val']]]) - * @phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - */ - public static function ip_get($ips_input = array('real', 'remote_addr', 'x_forwarded_for', 'x_real_ip', 'cloud_flare'), $v4_only = true) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $ips = array(); - foreach ( $ips_input as $ip_type ) { - $ips[$ip_type] = ''; - } - unset($ip_type); - - $headers = apache_request_headers(); - - // REMOTE_ADDR - if ( isset($ips['remote_addr']) ) { - $ips['remote_addr'] = $_SERVER['REMOTE_ADDR']; - } - - // X-Forwarded-For - if ( isset($ips['x_forwarded_for']) ) { - if ( isset($headers['X-Forwarded-For']) ) { - $tmp = explode(",", trim($headers['X-Forwarded-For'])); - $ips['x_forwarded_for'] = trim($tmp[0]); - } - } - - // X-Real-Ip - if ( isset($ips['x_real_ip']) ) { - if ( isset($headers['X-Real-Ip']) ) { - $tmp = explode(",", trim($headers['X-Real-Ip'])); - $ips['x_real_ip'] = trim($tmp[0]); - } - } - - // Cloud Flare - if ( isset($ips['cloud_flare']) ) { - if ( isset($headers['Cf-Connecting-Ip']) ) { - if ( self::ip_mask_match($ips['remote_addr'], self::$cdn_pool['cloud_flare']['ipv4']) ) { - $ips['cloud_flare'] = $headers['Cf-Connecting-Ip']; - } - } - } - - // Getting real IP from REMOTE_ADDR or Cf_Connecting_Ip if set or from (X-Forwarded-For, X-Real-Ip) if REMOTE_ADDR is local. - if ( isset($ips['real']) ) { - $ips['real'] = $_SERVER['REMOTE_ADDR']; - - // Cloud Flare - if ( isset($headers['Cf-Connecting-Ip']) ) { - if ( self::ip_mask_match($ips['real'], self::$cdn_pool['cloud_flare']['ipv4']) ) { - $ips['real'] = $headers['Cf-Connecting-Ip']; - } - // Incapsula proxy - } elseif ( isset($headers['Incap-Client-Ip']) ) { - $ips['real'] = $headers['Incap-Client-Ip']; - // Private networks. Looking for X-Forwarded-For and X-Real-Ip - } elseif ( self::ip_mask_match($ips['real'], self::$private_networks) ) { - if ( isset($headers['X-Forwarded-For']) ) { - $tmp = explode(",", trim($headers['X-Forwarded-For'])); - $ips['real'] = trim($tmp[0]); - } elseif ( isset($headers['X-Real-Ip']) ) { - $tmp = explode(",", trim($headers['X-Real-Ip'])); - $ips['real'] = trim($tmp[0]); - } - } - } - - // Validating IPs - $result = array(); - foreach ( $ips as $key => $ip ) { - if ( $v4_only ) { - if ( self::ip_validate($ip) == 'v4' ) { - $result[$key] = $ip; - } - } else { - if ( self::ip_validate($ip) ) { - $result[$key] = $ip; - } - } - } - - $result = array_unique($result); - - return count($ips_input) > 1 - ? $result - : (reset($result) !== false - ? reset($result) - : null); - } - - /** - * Check if the IP belong to mask. Recursively if array given - * @param $ip string - * @param $cidr mixed (string|array of strings) - */ - public static function ip_mask_match($ip, $cidr) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - if ( is_array($cidr) ) { - foreach ( $cidr as $curr_mask ) { - if ( self::ip_mask_match($ip, $curr_mask) ) { - return true; - } - } - unset($curr_mask); - - return false; - } - $exploded = explode('/', $cidr); - $net = $exploded[0]; - $mask = 4294967295 << (32 - (int)$exploded[1]); - - return (ip2long($ip) & $mask) == (ip2long($net) & $mask); - } - - /** - * Validating IPv4, IPv6 - * @param $ip string - * @returns string 'v4' || (string) 'v6' || (bool) false - */ - public static function ip_validate($ip) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - if ( ! $ip ) { - return false; - } // NULL || FALSE || '' || so on... - if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ) { - return 'v4'; - } // IPv4 - if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ) { - return 'v6'; - } // IPv6 - - return false; // Unknown - } - - /** - * Function sends raw http request - * - * May use 4 presets(combining possible): - * get_code - getting only HTTP response code - * dont_wait_for_answer - async requests - * get - GET-request - * ssl - use SSL - * - * @param $url string - * @param $data array - * @param $presets string|array - * @param $opts array - * - * @return mixed (array || array('error' => true)) - */ - public static function http__request($url, $data = array(), $presets = null, $opts = array()) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - $out = array(); - if ( function_exists('curl_init') ) { - $ch = curl_init(); - - // Obligatory options - $opts = array( - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => 1, - CURLOPT_CONNECTTIMEOUT_MS => 3000, - CURLOPT_FORBID_REUSE => true, - CURLOPT_USERAGENT => 'Cleantalk Antispam ' . defined('CLEANTALK_AGENT') - ? CLEANTALK_AGENT - : 'UNKNOWN_AGENT', - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => str_replace("&", "&", http_build_query($data)), - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => 0, - CURLOPT_HTTPHEADER => array('Expect:'), - // Fix for large data and old servers http://php.net/manual/ru/function.curl-setopt.php#82418 - ); - - // Use presets - $presets = is_array($presets) ? $presets : array($presets); - foreach ( $presets as $preset ) { - switch ($preset) { - // Get headers only - case 'get_code': - $opts[CURLOPT_HEADER] = true; - $opts[CURLOPT_NOBODY] = true; - break; - - // Make a request, don't wait for an answer - case 'dont_wait_for_answer': - $opts[CURLOPT_CONNECTTIMEOUT_MS] = 1000; - $opts[CURLOPT_TIMEOUT_MS] = 500; - break; - - case 'get': - $opts[CURLOPT_URL] .= '?' . str_replace("&", "&", http_build_query($data)); - $opts[CURLOPT_POST] = false; - $opts[CURLOPT_POSTFIELDS] = null; - break; - - case 'ssl': - $opts[CURLOPT_SSL_VERIFYPEER] = true; - $opts[CURLOPT_SSL_VERIFYHOST] = 2; - break; - - default: - break; - } - } - unset($preset); - - curl_setopt_array($ch, $opts); - $result = @curl_exec($ch); - - if ( in_array('dont_wait_for_answer', $presets) ) { - return true; - } - - if ( is_string($result) ) { - $result = explode(PHP_EOL, $result); - if ( in_array('get_code', $presets) ) { - $result = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - } - curl_close($ch); - - return $result; - } else { - $error = array('error' => true, 'error_string' => curl_error($ch)); - } - } else { - $error = array('error' => true, 'error_string' => 'CURL_NOT_INSTALLED'); - } - - /** Fix for get_code preset */ - if ( - $presets && ($presets == 'get_code' - || (is_array($presets) && in_array('get_code', $presets))) - && $error['error_string'] == 'CURL_NOT_INSTALLED' - ) { - $headers = get_headers($url); - $out = (int)preg_replace('/.*(\d{3}).*/', '$1', $headers[0]); - } - - return $out; - } - - /** - * Checks if the string is JSON type - * - * @param $string string - * - * @return bool - */ - public static function is_json($string) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - { - return is_string($string) && is_array(json_decode($string, true)) ? true : false; - } - - /** - * Function removing non UTF8 characters from array||string - * - * @param $data array|string - * - * @return array|string - */ - public static function removeNonUTF8FromArray($data) - { - foreach ( $data as $key => $val ) { - if ( is_array($val) ) { - $data[$key] = self::removeNonUTF8FromArray($val); - } else { - $data[$key] = self::removeNonUTF8FromString($val); - } - } - - return $data; - } - - /** - * Function removing non UTF8 characters from array||string - * param mixed(array||string) - * return mixed(array||string) - */ - public static function removeNonUTF8FromString($data) - { - if ( ! preg_match('//u', $data) ) { - $data = 'Nulled. Not UTF8 encoded or malformed.'; - } - - return $data; - } - - /** - * Function convert array to UTF8 and removes non UTF8 characters - * param array - * param string - * @return array - */ - public static function arrayToUTF8($array, $data_codepage = null) - { - foreach ( $array as $key => $val ) { - if ( is_array($val) ) { - $array[$key] = self::arrayToUTF8($val, $data_codepage); - } else { - $array[$key] = self::stringToUTF8($val, $data_codepage); - } - } - - return $array; - } - - /** - * Function convert string to UTF8 and removes non UTF8 characters - * @param $str string|null - * @param $data_codepage string - * @return string - */ - public static function stringToUTF8($str, $data_codepage = null) - { - //php8 deprecated fix - if ( is_null($str) ) { - $str = ''; - } - if ( - ! preg_match('//u', $str) - && function_exists('mb_detect_encoding') - && function_exists('mb_convert_encoding') - ) { - if ( $data_codepage !== null ) { - return mb_convert_encoding($str, 'UTF-8', $data_codepage); - } - - $encoding = mb_detect_encoding($str); - - if ( $encoding ) { - return mb_convert_encoding($str, 'UTF-8', $encoding); - } - } - - return $str; - } - - /** - * Function convert from UTF8 - * - * @param array|object|string $obj - * @param string $data_codepage - * - * @return mixed (array|object|string) - */ - public static function stringFromUTF8($obj, $data_codepage = null) - { - // Array || object - if ( is_array($obj) || is_object($obj) ) { - foreach ( $obj as $_key => &$val ) { - $val = self::stringFromUTF8($val, $data_codepage); - } - unset($val); - //String - } else { - if ( $data_codepage !== null && preg_match('//u', $obj) ) { - if ( function_exists('mb_convert_encoding') ) { - $obj = mb_convert_encoding($obj, $data_codepage, 'UTF-8'); - } elseif ( version_compare(phpversion(), '8.3', '<') ) { - $obj = @utf8_decode($obj); - } - } - } - - return $obj; - } -} diff --git a/lib/CleantalkRequest.php b/lib/CleantalkRequest.php deleted file mode 100644 index 76315b9..0000000 --- a/lib/CleantalkRequest.php +++ /dev/null @@ -1,179 +0,0 @@ - 0 ) { - foreach ( $params as $param => $value ) { - $this->{$param} = $value; - } - } - } -} diff --git a/lib/CleantalkResponse.php b/lib/CleantalkResponse.php deleted file mode 100644 index 4e5f69a..0000000 --- a/lib/CleantalkResponse.php +++ /dev/null @@ -1,209 +0,0 @@ - 0 ) { - foreach ( $response as $param => $value ) { - $this->{$param} = $value; - } - } else { - $this->errno = $obj->errno; - $this->errstr = $obj->errstr; - - $this->errstr = isset($this->errstr) - ? preg_replace("/.+(\*\*\*.+\*\*\*).+/", "$1", $this->errstr) - : ''; - - $this->stop_words = isset($obj->stop_words) ? CleantalkHelper::stringFromUTF8( - $obj->stop_words, - 'ISO-8859-1' - ) : null; - $this->comment = isset($obj->comment) ? CleantalkHelper::stringFromUTF8( - $obj->comment, - 'ISO-8859-1' - ) : null; - $this->blacklisted = (isset($obj->blacklisted)) ? $obj->blacklisted : null; - $this->allow = (isset($obj->allow)) ? $obj->allow : 0; - $this->id = (isset($obj->id)) ? $obj->id : null; - $this->fast_submit = (isset($obj->fast_submit)) ? $obj->fast_submit : 0; - $this->spam = (isset($obj->spam)) ? $obj->spam : 0; - $this->js_disabled = (isset($obj->js_disabled)) ? $obj->js_disabled : 0; - $this->sms_allow = (isset($obj->sms_allow)) ? $obj->sms_allow : null; - $this->sms = (isset($obj->sms)) ? $obj->sms : null; - $this->sms_error_code = (isset($obj->sms_error_code)) ? $obj->sms_error_code : null; - $this->sms_error_text = (isset($obj->sms_error_text)) ? $obj->sms_error_text : null; - $this->stop_queue = (isset($obj->stop_queue)) ? $obj->stop_queue : 0; - $this->inactive = (isset($obj->inactive)) ? $obj->inactive : 0; - $this->account_status = (isset($obj->account_status)) ? $obj->account_status : -1; - $this->received = (isset($obj->received)) ? $obj->received : -1; - $this->codes = (isset($obj->codes)) ? explode(' ', $obj->codes) : array(); - - $this->bot_expectation = (isset($obj->bot_expectation)) ? $obj->bot_expectation : 0.0; - $this->ip_frequency_24hour = (isset($obj->ip_frequency_24hour)) ? $obj->ip_frequency_24hour : 0; - $this->ip_frequency_1hour = (isset($obj->ip_frequency_1hour)) ? $obj->ip_frequency_1hour : 0; - $this->ip_frequency_10min = (isset($obj->ip_frequency_10min)) ? $obj->ip_frequency_10min : 0; - - - if ( $this->errno !== 0 && $this->errstr !== null && $this->comment === null ) { - $this->comment = '*** ' . $this->errstr . ' Antispam service cleantalk.org ***'; - } - } - } -} diff --git a/lib/CleantalkVerdict.php b/lib/CleantalkVerdict.php new file mode 100644 index 0000000..53195d0 --- /dev/null +++ b/lib/CleantalkVerdict.php @@ -0,0 +1,21 @@ +getJSON(), true); + } +} diff --git a/lib/HTTP/CleantalkResponse.php b/lib/HTTP/CleantalkResponse.php new file mode 100644 index 0000000..103dbaf --- /dev/null +++ b/lib/HTTP/CleantalkResponse.php @@ -0,0 +1,170 @@ +errno = isset($obj->errno) ? $obj->errno : 0; + $this->errstr = isset($obj->errstr) ? + preg_replace("/.+(\*\*\*.+\*\*\*).+/", "$1", htmlspecialchars($obj->errstr)) : + null; + $this->stop_words = isset($obj->stop_words) ? Helper::fromUTF8($obj->stop_words, 'ISO-8859-1') : null; + $this->comment = isset($obj->comment) ? $obj->comment : null; + $this->blacklisted = isset($obj->blacklisted) ? $obj->blacklisted : null; + $this->allow = isset($obj->allow) ? $obj->allow : 1; + $this->id = isset($obj->id) ? $obj->id : null; + $this->fast_submit = isset($obj->fast_submit) ? $obj->fast_submit : 0; + $this->spam = isset($obj->spam) ? $obj->spam : 0; + $this->js_disabled = isset($obj->js_disabled) ? $obj->js_disabled : 0; + $this->sms_allow = isset($obj->sms_allow) ? $obj->sms_allow : null; + $this->sms = isset($obj->sms) ? $obj->sms : null; + $this->sms_error_code = isset($obj->sms_error_code) ? $obj->sms_error_code : null; + $this->sms_error_text = isset($obj->sms_error_text) ? htmlspecialchars($obj->sms_error_text) : ''; + $this->stop_queue = isset($obj->stop_queue) ? $obj->stop_queue : 0; + $this->inactive = isset($obj->inactive) ? $obj->inactive : 0; + $this->account_status = isset($obj->account_status) ? $obj->account_status : -1; + $this->received = isset($obj->received) ? $obj->received : -1; + $this->codes = isset($obj->codes) ? explode(' ', $obj->codes) : array(); + + if ( $this->errno !== 0 && $this->errstr !== null && $this->comment === null ) { + $this->comment = '*** ' . $this->errstr . ' Anti-Spam service cleantalk.org ***'; + } + + $this->failed_connections_urls_string = !empty($failed_urls) ? $failed_urls : ''; + } +} diff --git a/lib/HTTP/Helper.php b/lib/HTTP/Helper.php new file mode 100644 index 0000000..b9a9e0b --- /dev/null +++ b/lib/HTTP/Helper.php @@ -0,0 +1,550 @@ + array( + '10.0.0.0/8', + '100.64.0.0/10', + '172.16.0.0/12', + '192.168.0.0/16', + '127.0.0.1/32', + ), + 'v6' => array( + '0:0:0:0:0:0:0:1/128', // localhost + '0:0:0:0:0:0:a:1/128', // ::ffff:127.0.0.1 + ), + ); + + /** + * Getting arrays of IP (REMOTE_ADDR, X-Forwarded-For, X-Real-Ip, Cf_Connecting_Ip) + * + * @param string $ip_type_to_get Type of IP you want to receive + * @param bool $v4_only + * + * @return string|null + * + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress ComplexMethod + * @psalm-suppress FalsableReturnStatement + */ + public static function ipGet($ip_type_to_get = 'real', $v4_only = true, $headers = array()) + { + $out = null; + switch ($ip_type_to_get) { + // Cloud Flare + case 'cloud_flare': + $headers = $headers ?: self::httpGetHeaders(); + if ( + isset($headers['Cf-Connecting-Ip']) && + (isset($headers['Cf-Ray']) || isset($headers['X-Wpe-Request-Id'])) && + ! isset($headers['X-Gt-Clientip']) + ) { + if (isset($headers['Cf-Pseudo-Ipv4'], $headers['Cf-Pseudo-Ipv6'])) { + $source = $headers['Cf-Pseudo-Ipv6']; + } else { + $source = $headers['Cf-Connecting-Ip']; + } + $tmp = strpos($source, ',') !== false + ? explode(',', $source) + : (array)$source; + if ( isset($tmp[0]) ) { + $ip_version = self::ipValidate(trim($tmp[0])); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only + ? self::ipV6Normalize(trim($tmp[0])) + : trim($tmp[0]); + } + } + } + break; + + // GTranslate + case 'gtranslate': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Gt-Clientip'], $headers['X-Gt-Viewer-Ip'])) { + $ip_version = self::ipValidate($headers['X-Gt-Viewer-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['X-Gt-Viewer-Ip'] + ) : $headers['X-Gt-Viewer-Ip']; + } + } + break; + + // ezoic + case 'ezoic': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Middleton'], $headers['X-Middleton-Ip'])) { + $ip_version = self::ipValidate($headers['X-Middleton-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['X-Middleton-Ip'] + ) : $headers['X-Middleton-Ip']; + } + } + break; + + // Sucury + case 'sucury': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Sucuri-Clientip'])) { + $ip_version = self::ipValidate($headers['X-Sucuri-Clientip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['X-Sucuri-Clientip'] + ) : $headers['X-Sucuri-Clientip']; + } + } + break; + + // X-Forwarded-By + case 'x_forwarded_by': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Forwarded-By'], $headers['X-Client-Ip'])) { + $ip_version = self::ipValidate($headers['X-Client-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['X-Client-Ip'] + ) : $headers['X-Client-Ip']; + } + } + break; + + // Stackpath + case 'stackpath': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Sp-Edge-Host'], $headers['X-Sp-Forwarded-Ip'])) { + $ip_version = self::ipValidate($headers['X-Sp-Forwarded-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['X-Sp-Forwarded-Ip'] + ) : $headers['X-Sp-Forwarded-Ip']; + } + } + break; + + // Ico-X-Forwarded-For + case 'ico_x_forwarded_for': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['Ico-X-Forwarded-For'], $headers['X-Forwarded-Host'])) { + $ip_version = self::ipValidate($headers['Ico-X-Forwarded-For']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['Ico-X-Forwarded-For'] + ) : $headers['Ico-X-Forwarded-For']; + } + } + break; + + // OVH + case 'ovh': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Cdn-Any-Ip'], $headers['Remote-Ip'])) { + $ip_version = self::ipValidate($headers['Remote-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['Remote-Ip'] + ) : $headers['Remote-Ip']; + } + } + break; + + // Incapsula proxy + case 'incapsula': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['Incap-Client-Ip'], $headers['X-Forwarded-For'])) { + $ip_version = self::ipValidate($headers['Incap-Client-Ip']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $headers['Incap-Client-Ip'] + ) : $headers['Incap-Client-Ip']; + } + } + break; + + // Incapsula proxy like "X-Clientside":"10.10.10.10:62967 -> 192.168.1.1:443" + case 'clientside': + $headers = $headers ?: self::httpGetHeaders(); + if ( + isset($headers['X-Clientside']) + && (preg_match('/^([0-9a-f.:]+):\d+ -> ([0-9a-f.:]+):\d+$/', $headers['X-Clientside'], $matches) + && isset($matches[1])) + ) { + $ip_version = self::ipValidate($matches[1]); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize($matches[1]) : $matches[1]; + } + } + break; + + // Remote addr + case 'remote_addr': + $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; + if (!empty($remote_addr)) { + $ip_version = self::ipValidate($_SERVER['REMOTE_ADDR']); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize( + $_SERVER['REMOTE_ADDR'] + ) : $_SERVER['REMOTE_ADDR']; + } + } + break; + + // X-Forwarded-For + case 'x_forwarded_for': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Forwarded-For'])) { + $tmp = explode(',', trim($headers['X-Forwarded-For'])); + $tmp = trim($tmp[0]); + $ip_version = self::ipValidate($tmp); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize($tmp) : $tmp; + } + } + break; + + // X-Real-Ip + case 'x_real_ip': + $headers = $headers ?: self::httpGetHeaders(); + if (isset($headers['X-Real-Ip'])) { + $tmp = explode(",", trim($headers['X-Real-Ip'])); + $tmp = trim($tmp[0]); + $ip_version = self::ipValidate($tmp); + if ($ip_version) { + $out = $ip_version === 'v6' && ! $v4_only ? self::ipV6Normalize($tmp) : $tmp; + } + } + break; + + // Real + // Getting real IP from REMOTE_ADDR or Cf_Connecting_Ip if set or from (X-Forwarded-For, X-Real-Ip) if REMOTE_ADDR is local. + case 'real': + // Detect IP type + $out = self::ipGet('cloud_flare', $v4_only, $headers); + $out = $out ?: self::ipGet('sucury', $v4_only, $headers); + $out = $out ?: self::ipGet('gtranslate', $v4_only, $headers); + $out = $out ?: self::ipGet('ezoic', $v4_only, $headers); + $out = $out ?: self::ipGet('stackpath', $v4_only, $headers); + $out = $out ?: self::ipGet('x_forwarded_by', $v4_only, $headers); + $out = $out ?: self::ipGet('ico_x_forwarded_for', $v4_only, $headers); + $out = $out ?: self::ipGet('ovh', $v4_only, $headers); + $out = $out ?: self::ipGet('incapsula', $v4_only, $headers); + $out = $out ?: self::ipGet('clientside', $v4_only, $headers); + + $ip_version = self::ipValidate($out); + + // Is private network + if ( + ! $out || + ( + is_string($ip_version) && ( + self::ipIsPrivateNetwork($out, $ip_version) || + ( + $ip_version === self::ipValidate($_SERVER['SERVER_ADDR']) && + self::ipMaskMatch($out, $_SERVER['SERVER_ADDR'] . '/24', $ip_version) + ) + ) + ) + ) { + //@todo Remove local IP from x-forwarded-for and x-real-ip + $out = $out ?: self::ipGet('x_forwarded_for', $v4_only, $headers); + $out = $out ?: self::ipGet('x_real_ip', $v4_only, $headers); + } + + $out = $out ?: self::ipGet('remote_addr', $v4_only, $headers); + + break; + + default: + $out = self::ipGet('real', $v4_only, $headers); + } + + if ( is_string($out) ) { + $ip_version = self::ipValidate($out); + + if ( ! $ip_version ) { + $out = null; + } + + if ( $ip_version === 'v6' && $v4_only ) { + $out = null; + } + } + + return $out; + } + + /** + * Checks if the IP is in private range + * + * @param string $ip + * @param string $ip_type + * + * @return bool + */ + public static function ipIsPrivateNetwork($ip, $ip_type = 'v4') + { + return self::ipMaskMatch($ip, self::$private_networks[$ip_type], $ip_type); + } + + /** + * Check if the IP belong to mask. Recursive. + * Octet by octet for IPv4 + * Hextet by hextet for IPv6 + * + * @param string $ip + * @param string|array $cidr work to compare with + * @param string $ip_type IPv6 or IPv4 + * @param int $xtet_count Recursive counter. Determs current part of address to check. + * + * @return bool + * @psalm-suppress InvalidScalarArgument + */ + public static function ipMaskMatch($ip, $cidr, $ip_type = 'v4', $xtet_count = 0) + { + if (is_array($cidr)) { + foreach ($cidr as $curr_mask) { + if (self::ipMaskMatch($ip, $curr_mask, $ip_type)) { + return true; + } + } + + return false; + } + + if ( ! self::ipValidate($ip) || ! self::cidrValidate($cidr) ) { + return false; + } + + $xtet_base = ($ip_type === 'v4') ? 8 : 16; + + // Calculate mask + $exploded = explode('/', $cidr); + + if ( ! isset($exploded[0], $exploded[1]) ) { + return false; + } + + $net_ip = $exploded[0]; + $mask = (int)$exploded[1]; + + // Exit condition + $xtet_end = ceil($mask / $xtet_base); + if ($xtet_count == $xtet_end) { + return true; + } + + // Length of bits for comparison + $mask = $mask - $xtet_base * $xtet_count >= $xtet_base ? $xtet_base : $mask - $xtet_base * $xtet_count; + + // Explode by octets/hextets from IP and Net + $net_ip_xtets = explode($ip_type === 'v4' ? '.' : ':', $net_ip); + $ip_xtets = explode($ip_type === 'v4' ? '.' : ':', $ip); + + // Standartizing. Getting current octets/hextets. Adding leading zeros. + $net_xtet = str_pad( + decbin( + ($ip_type === 'v4' && (int)$net_ip_xtets[$xtet_count]) ? $net_ip_xtets[$xtet_count] : @hexdec( + $net_ip_xtets[$xtet_count] + ) + ), + $xtet_base, + 0, + STR_PAD_LEFT + ); + $ip_xtet = str_pad( + decbin( + ($ip_type === 'v4' && (int)$ip_xtets[$xtet_count]) ? $ip_xtets[$xtet_count] : @hexdec( + $ip_xtets[$xtet_count] + ) + ), + $xtet_base, + 0, + STR_PAD_LEFT + ); + + // Comparing bit by bit + for ($i = 0, $result = true; $mask != 0; $mask--, $i++) { + if ($ip_xtet[$i] != $net_xtet[$i]) { + $result = false; + break; + } + } + + // Recursing. Moving to next octet/hextet. + if ($result) { + $result = self::ipMaskMatch($ip, $cidr, $ip_type, $xtet_count + 1); + } + + return $result; + } + + /** + * Validating IPv4, IPv6 + * + * @param string|null|false $ip + * + * @return string|bool + */ + public static function ipValidate($ip) + { + if ( ! $ip ) { // NULL || FALSE || '' || so on... + return false; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $ip != '0.0.0.0') { // IPv4 + return 'v4'; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && self::ipV6Reduce($ip) != '0::0') { // IPv6 + return 'v6'; + } + + return false; // Unknown + } + + /** + * Validate CIDR + * + * @param string $cidr expects string like 1.1.1.1/32 + * + * @return bool + */ + public static function cidrValidate($cidr) + { + $cidr = explode('/', $cidr); + + return isset($cidr[0], $cidr[1]) && self::ipValidate($cidr[0]) && preg_match('@\d{1,2}@', $cidr[1]); + } + + /** + * Expand IPv6 + * + * @param string $ip + * + * @return string IPv6 + */ + public static function ipV6Normalize($ip) + { + $ip = trim($ip); + // Searching for ::ffff:xx.xx.xx.xx patterns and turn it to IPv6 + if (preg_match('/^::ffff:([0-9]{1,3}\.?){4}$/', $ip)) { + $ip = dechex((int)sprintf("%u", ip2long(substr($ip, 7)))); + $ip = '0:0:0:0:0:0:' . (strlen($ip) > 4 ? substr('abcde', 0, -4) : '0') . ':' . substr($ip, -4, 4); + // Normalizing hextets number + } elseif (strpos($ip, '::') !== false) { + $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip); + $ip = strpos($ip, ':') === 0 ? '0' . $ip : $ip; + $ip = strpos(strrev($ip), ':') === 0 ? $ip . '0' : $ip; + } + // Simplifyng hextets + if (preg_match('/:0(?=[a-z0-9]+)/', $ip)) { + $ip = preg_replace('/:0(?=[a-z0-9]+)/', ':', strtolower($ip)); + $ip = self::ipV6Normalize($ip); + } + + return $ip; + } + + /** + * Reduce IPv6 + * + * @param string $ip + * + * @return string IPv6 + */ + public static function ipV6Reduce($ip) + { + if (strpos($ip, ':') !== false) { + $ip = preg_replace('/:0{1,4}/', ':', $ip); + $ip = preg_replace('/:{2,}/', '::', $ip); + $ip = strpos($ip, '0') === 0 && substr($ip, 1) !== false ? substr($ip, 1) : $ip; + } + + return $ip; + } + + /** + * Gets every HTTP_ headers from $_SERVER + * + * If Apache web server is missing then making + * Patch for apache_request_headers() + * + * returns array + */ + public static function httpGetHeaders() + { + // If headers already return them + $headers = array(); + foreach ($_SERVER as $key => $val) { + if (0 === stripos($key, 'http_')) { + $server_key = preg_replace('/^http_/i', '', $key); + $key_parts = explode('_', $server_key); + if (strlen($server_key) > 2) { + foreach ($key_parts as $part_index => $part) { + if ($part === '') { + continue; + } + + $key_parts[$part_index] = function_exists('mb_strtolower') ? mb_strtolower( + $part + ) : strtolower( + $part + ); + $key_parts[$part_index][0] = strtoupper($key_parts[$part_index][0]); + } + $server_key = implode('-', $key_parts); + } + $headers[$server_key] = $val; + } + } + + return $headers; + } + + /** + * Function convert from UTF8 + * + * @param array|object|string $obj + * @param string $data_codepage + * + * @return mixed (array|object|string) + */ + public static function fromUTF8($obj, $data_codepage = null) + { + // Array || object + if (is_array($obj) || is_object($obj)) { + foreach ($obj as $_key => &$val) { + $val = self::fromUTF8($val, $data_codepage); + } + unset($val); + //String + } else { + if ($data_codepage !== null && preg_match('//u', $obj)) { + if ( function_exists('mb_convert_encoding') ) { + $obj = mb_convert_encoding($obj, $data_codepage, 'UTF-8'); + } elseif (version_compare(phpversion(), '8.3', '<')) { + $obj = @utf8_decode($obj); + } + } + } + + return $obj; + } +} diff --git a/lib/HTTP/Request.php b/lib/HTTP/Request.php new file mode 100644 index 0000000..e1e4d52 --- /dev/null +++ b/lib/HTTP/Request.php @@ -0,0 +1,583 @@ + $url, + * CURLOPT_TIMEOUT => 15, + * CURLOPT_LOW_SPEED_TIME => 10, + * CURLOPT_RETURNTRANSFER => true, + * ) + */ + protected $options = []; + + /** + * @var array [callable] Callback function to process after the request is performed without error to process received data + * If passed will be fired for both single and multi requests + */ + protected $callbacks = []; + + /** + * @var Response|array + */ + public $response; + + /** + * @param mixed $url + * + * @return Request + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * @param mixed $data + * + * @return Request + */ + public function setData($data) + { + // If $data scalar converting it to array + $this->data = ! empty($data) && ! self::isJson($data) && is_scalar($data) + ? array((string)$data => 1) + : $data; + + return $this; + } + + /** + * Set one or more presets which change the way of the processing Request::request + * + * @param mixed $presets Array with presets + * Example: array('get_code', 'async') + * Or space separated string with presets + * Example: 'get_code async get' + * + * May use the following presets(combining is possible): + * dont_follow_redirects - ignore 300-family response code and don't follow redirects + * get_code - getting only HTTP response code + * async - async requests. Sends request and return 'true' value. Doesn't wait for response. + * get - makes GET-type request instead of default POST-type + * ssl - uses SSL + * cache - allow caching for this request + * retry_with_socket - make another request with socket if cURL failed to retrieve data + * + * @return Request + */ + public function setPresets($presets) + { + // Prepare $presets to process + $this->presets = ! is_array($presets) + ? explode(' ', $presets) + : $presets; + + return $this; + } + + /** + * @param mixed $options + * + * @return Request + */ + public function setOptions($options) + { + $this->options = $options; + + return $this; + } + + /** + * Set callback and additional arguments which will be passed to callback function + * + * @param callable $callback + * @param array $arguments + * @param int $priority + * @param bool $pass_response + * + * @return Request + * @psalm-suppress UnusedVariable + */ + public function addCallback($callback, $arguments = array(), $priority = null, $pass_response = false) + { + $priority = $priority ?: 100; + if ( isset($this->callbacks[$priority]) ) { + return $this->addCallback($callback, $arguments, ++$priority); + } + + $this->callbacks[$priority] = [ + 'function' => $callback, + 'arguments' => $arguments, + 'pass_response' => $pass_response, + ]; + + return $this; + } + + /** + * Function sends raw http request + * + * @return array|bool (array || array('error' => true)) + */ + public function request() + { + // Return the error if cURL is not installed + if ( ! function_exists('curl_init') ) { + return array('error' => 'CURL_NOT_INSTALLED'); + } + + if ( empty($this->url) ) { + return array('error' => 'URL_IS_NOT_SET'); + } + + $this->convertOptionsTocURLFormat(); + $this->appendOptionsObligatory(); + $this->processPresets(); + + // Call cURL multi request if many URLs passed + $this->response = is_array($this->url) + ? $this->requestMulti() + : $this->requestSingle(); + + // Process the error. Unavailable for multiple URLs. + if ( + ! is_array($this->url) && + ! is_array($this->response) && $this->response->getError() && + in_array('retry_with_socket', $this->presets, true) + ) { + $this->response = $this->requestWithSocket(); + if ( $this->response->getError() ) { + return $this->response->getError(); + } + } + + return $this->runCallbacks(); + } + + /** + * @return Response + */ + protected function requestSingle() + { + // Make a request + $ch = curl_init(); + + curl_setopt_array($ch, $this->options); + + $request_result = curl_exec($ch); // Gather request result + $curl_info = curl_getinfo($ch); // Gather HTTP response information + + // Do not catch timeout error for async requests. + if ( in_array('async', $this->presets, true) ) { + $request_result = true; + } + + if ( $request_result === false ) { + $request_result = array('error' => curl_error($ch)); + } + + curl_close($ch); + + + return new Response($request_result, $curl_info); + } + + + /** + * Do multi curl requests without processing it. + * + * @return array + * + * @psalm-suppress PossiblyInvalidArgument + */ + protected function requestMulti() + { + $this->response = []; + + if ( ! is_array($this->url) ) { + return $this->response; + } + + $urls_count = count($this->url); + $curl_arr = array(); + $mh = curl_multi_init(); + + for ( $i = 0; $i < $urls_count; $i++ ) { + $this->options[CURLOPT_URL] = $this->url[$i]; + $curl_arr[$i] = curl_init($this->url[$i]); + + curl_setopt_array($curl_arr[$i], $this->options); + curl_multi_add_handle($mh, $curl_arr[$i]); + } + + do { + curl_multi_exec($mh, $running); + usleep(1000); + } while ( $running > 0 ); + + for ( $i = 0; $i < $urls_count; $i++ ) { + $curl_info = curl_getinfo($curl_arr[$i]); // Gather HTTP response information + $received_data = curl_multi_getcontent($curl_arr[$i]); + + // Do not catch timeout error for async requests. + if ( in_array('async', $this->presets, true) ) { + $received_data = true; + } + + if ( $received_data === '' ) { + $received_data = array('error' => curl_error($curl_arr[$i])); + } + + $this->response[$this->url[$i]] = new Response($received_data, $curl_info); + } + + return $this->response; + } + + /** + * Make a request with socket, exactly with file_get_contents() + * + * @return Response + * + * @psalm-suppress PossiblyInvalidArgument + * @psalm-suppress PossiblyInvalidCast + */ + private function requestWithSocket() + { + if ( ! ini_get('allow_url_fopen') ) { + return new Response(['error' => 'ALLOW_URL_FOPEN_IS_DISABLED'], []); + } + + $context = stream_context_create( + [ + 'http' => [ + 'method' => 'GET', //in_array('get', $this->presets, true) ? 'GET' : 'POST', + 'timeout' => $this->options[CURLOPT_TIMEOUT], + 'content' => $this->data, + ], + ] + ); + + $response_content = @file_get_contents($this->url, false, $context) + ?: ['error' => 'FAILED_TO_USE_FILE_GET_CONTENTS']; + + return new Response($response_content, []); + } + + // Process with callback if passed. Save the processed result. + protected function runCallbacks() + { + $return_value = []; + + // Cast to array to process result from $this->requestSingle as $this->requestMulti results + $responses = is_object($this->response) + ? [$this->response] + : $this->response; + + // Sort callback to keep the priority order + ksort($this->callbacks); + + foreach ( $responses as $url => &$response ) { + // Skip the processing if the error occurred in this specific result + if ( $response->getError() ) { + $return_value[] = $response->getError(); + continue; + } + + // Get content to process + $content = $response->getContentProcessed(); + + // Perform all provided callback functions to each request result + if ( ! empty($this->callbacks) ) { + foreach ( $this->callbacks as $callback ) { + if ( is_callable($callback['function']) ) { + // Run callback + $content = call_user_func_array( + $callback['function'], + array_merge( + array( + $callback['pass_response'] ? $response : $content, // Pass Response or content + $url + ), + $callback['arguments'] + ) + ); + + // Foolproof + if ( ! $content instanceof Response ) { + $response->setProcessed($content); + } + } + } + } + + $return_value[$url] = $content instanceof Response ? $content->getContentProcessed() : $content; + } + unset($response); + + // Return a single content if it was a single request + return is_array($this->response) && count($this->response) > 1 + ? $return_value + : reset($return_value); + } + + /** + * Convert given options from simple naming like 'timeout' or 'ssl' + * to sophisticated and standardized cURL defined constants + * + * !! Called only after we make sure that cURL is exists !! + */ + private function convertOptionsTocURLFormat() + { + $temp_options = []; + foreach ( $this->options as $option_name => &$option_value ) { + switch ( $option_name ) { + case 'timeout': + $temp_options[CURLOPT_TIMEOUT] = $option_value; // String + unset($this->options[$option_name]); + break; + case 'sslverify': + if ( $option_value ) { + $temp_options[CURLOPT_SSL_VERIFYPEER] = (bool)$option_value; // Boolean + $temp_options[CURLOPT_SSL_VERIFYHOST] = (int)(bool)$option_value; // Int 0|1 + unset($this->options[$option_name]); + } + break; + case 'sslcertificates': + $temp_options[CURLOPT_CAINFO] = $option_name; // String + unset($this->options[$option_name]); + break; + case 'headers': + $temp_options[CURLOPT_HTTPHEADER] = $option_name; // String[] + unset($this->options[$option_name]); + break; + case 'user-agent': + $temp_options[CURLOPT_USERAGENT] = $option_name; // String + unset($this->options[$option_name]); + break; + + // Unset unsupported string names in options + default: + if ( ! is_int($option_name) ) { + unset($this->options[$option_name]); + } + break; + } + } + unset($option_value); + + $this->options = array_replace($this->options, $temp_options); + } + + /** + * Set default options to make a request + */ + protected function appendOptionsObligatory() + { + // Merging OBLIGATORY options with GIVEN options + $this->options = array_replace( + array( + CURLOPT_URL => ! is_array($this->url) ? $this->url : null, + CURLOPT_TIMEOUT => 50, + CURLOPT_LOW_SPEED_TIME => 25, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 5000, + CURLOPT_FORBID_REUSE => true, + CURLOPT_USERAGENT => self::AGENT, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $this->data, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HTTPHEADER => array( + 'Expect:', + // Fix for large data and old servers http://php.net/manual/ru/function.curl-setopt.php#82418 + 'Expires: ' . date(DATE_RFC822, mktime(0, 0, 0, 1, 1, 1971)), + 'Cache-Control: no-store, no-cache, must-revalidate', + 'Cache-Control: post-check=0, pre-check=0', + 'Pragma: no-cache', + ), + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + ), + $this->options + ); + } + + /** + * Append options considering passed presets + */ + protected function processPresets() + { + foreach ( $this->presets as $preset ) { + switch ( $preset ) { + // Do not follow redirects + case 'dont_follow_redirects': + $this->options[CURLOPT_FOLLOWLOCATION] = false; + $this->options[CURLOPT_MAXREDIRS] = 0; + break; + + // Get headers only + case 'get_code': + $this->options[CURLOPT_HEADER] = true; + $this->options[CURLOPT_NOBODY] = true; + $this->addCallback( + static function (Response $response, $_url) { + return $response->getResponseCode(); + }, + array(), + 60, + true + ); + break; + + // Get headers only + case 'split_to_array': + $this->addCallback( + static function ($response_content, $_url) { + return explode(PHP_EOL, $response_content); + }, + array(), + 50 + ); + break; + + // Make a request, don't wait for an answer + case 'async': + $this->options[CURLOPT_CONNECTTIMEOUT] = 3; + $this->options[CURLOPT_TIMEOUT] = 3; + break; + + case 'get': + $this->options[CURLOPT_CUSTOMREQUEST] = 'GET'; + $this->options[CURLOPT_POST] = false; + $this->options[CURLOPT_POSTFIELDS] = null; + // Append parameter in a different way for single and multiple requests + if ( is_array($this->url) ) { + $this->url = array_map(function ($elem) { + return self::appendParametersToURL($elem, $this->data); + }, $this->url); + } else { + $this->options[CURLOPT_URL] = self::appendParametersToURL( + $this->options[CURLOPT_URL], + $this->data + ); + } + break; + + case 'ssl': + $this->options[CURLOPT_SSL_VERIFYPEER] = true; + $this->options[CURLOPT_SSL_VERIFYHOST] = 2; + if ( defined('APBCT_CASERT_PATH') && APBCT_CASERT_PATH ) { + $this->options[CURLOPT_CAINFO] = APBCT_CASERT_PATH; + } + break; + + case 'no_cache': + // Append parameter in a different way for single and multiple requests + if ( is_array($this->url) ) { + $this->url = array_map(static function ($elem) { + return self::appendParametersToURL($elem, ['apbct_no_cache' => mt_rand()]); + }, $this->url); + } else { + $this->options[CURLOPT_URL] = self::appendParametersToURL( + $this->options[CURLOPT_URL], + ['apbct_no_cache' => mt_rand()] + ); + } + break; + case 'api3.0': + // api3.0 methods requires 'Content-Type: application/json' http header + $this->options[CURLOPT_HTTPHEADER][] = 'Content-Type: application/json'; + } + } + } + + /** + * Appends given parameter(s) to URL considering other parameters + * Adds ? or & before the append + * + * @param string $url + * @param string|array $parameters + * + * @return string + */ + public static function appendParametersToURL($url, $parameters) + { + if ( empty($parameters) ) { + return $url; + } + + $parameters = is_array($parameters) + ? http_build_query($parameters) + : $parameters; + + $url .= strpos($url, '?') === false + ? ('?' . $parameters) + : ('&' . $parameters); + + return $url; + } + + /** + * Checks if the string is JSON type + * + * @param string $string + * + * @return bool + */ + public static function isJson($string) + { + return is_string($string) && is_array(json_decode($string, true)); + } +} diff --git a/lib/HTTP/Response.php b/lib/HTTP/Response.php new file mode 100644 index 0000000..4aaac43 --- /dev/null +++ b/lib/HTTP/Response.php @@ -0,0 +1,79 @@ +raw = $raw; + $this->processed = $raw; + $this->info = $info; + $this->error = ! empty($raw['error']) + ? $raw + : null; + if ( isset($this->info['http_code']) ) { + $this->response_code = (int)$this->info['http_code']; + } + } + + /** + * @return mixed + */ + public function getError() + { + return $this->error; + } + + /** + * @return mixed + */ + public function getResponseCode() + { + return $this->response_code; + } + + /** + * @return mixed + */ + public function getContentRaw() + { + return $this->raw; + } + + /** + * @return mixed + */ + public function getContentProcessed() + { + return $this->processed; + } + + /** + * @param mixed $processed + */ + public function setProcessed($processed) + { + $this->processed = $processed; + } + + /** + * @return mixed + */ + public function getInfo() + { + return $this->info; + } +} diff --git a/lib/TransportException.php b/lib/TransportException.php deleted file mode 100644 index 7309bd3..0000000 --- a/lib/TransportException.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Blacklisted - - - -

CleanTalk. Spam protection


-%ERROR_TEXT% -

-

« Back

- diff --git a/tests/CleantalkTest.php b/tests/CleantalkTest.php index f25599e..3d75530 100644 --- a/tests/CleantalkTest.php +++ b/tests/CleantalkTest.php @@ -3,45 +3,6 @@ use Cleantalk\Cleantalk; use Cleantalk\CleantalkRequest; -require_once "lib/Cleantalk.php"; -require_once "lib/CleantalkRequest.php"; -require_once "lib/CleantalkResponse.php"; -require_once "lib/CleantalkHelper.php"; -require_once "lib/CleantalkAPI.php"; -require_once "lib/cleantalk-php-patch.php"; - - class CleantalkTest extends \PHPUnit\Framework\TestCase { - protected $ct; - - protected $ct_request; - - public function setUp() - { - $this->ct = new Cleantalk(); - $this->ct->server_url = 'https://moderate.cleantalk.org'; - $this->ct_request = new CleantalkRequest(); - $this->ct_request->auth_key = getenv("CLEANTALK_TEST_API_KEY"); - } - - public function testIsAllowMessage() - { - $this->ct_request->sender_email = 's@cleantalk.org'; - $this->ct_request->message = 'stop_word bad message'; - $result = $this->ct->isAllowMessage($this->ct_request); - $this->assertEquals(0, $result->allow); - - $this->ct_request->message = ''; - $this->ct_request->sender_email = ''; - } - - public function testIsAllowUser() - { - $this->ct_request->sender_email = 's@cleantalk.org'; - $result = $this->ct->isAllowUser($this->ct_request); - $this->assertEquals(0, $result->allow); - - $this->ct_request->sender_email = ''; - } }