diff --git a/.upgrade.yml b/.upgrade.yml index 1842e7f..b49aa41 100644 --- a/.upgrade.yml +++ b/.upgrade.yml @@ -2,6 +2,5 @@ mappings: AkismetConfig: SilverStripe\Akismet\Config\AkismetConfig AkismetProcessor: SilverStripe\Akismet\Config\AkismetProcessor AkismetService: SilverStripe\Akismet\Service\AkismetService - AkismetServiceBackend: SilverStripe\Akismet\Service\AkismetServiceBackend AkismetField: SilverStripe\Akismet\AkismetField - AkismetSpamProtector: SilverStripe\Akismet\AkismetSpamProtector \ No newline at end of file + AkismetSpamProtector: SilverStripe\Akismet\AkismetSpamProtector diff --git a/_config/akismet.yml b/_config/akismet.yml index 243caff..eceb04f 100644 --- a/_config/akismet.yml +++ b/_config/akismet.yml @@ -7,8 +7,6 @@ SilverStripe\SpamProtection\Extension\FormSpamProtectionExtension: default_spam_protector: SilverStripe\Akismet\AkismetSpamProtector SilverStripe\Core\Injector\Injector: - SilverStripe\Akismet\Service\AkismetService: - class: SilverStripe\Akismet\Service\AkismetServiceBackend SilverStripe\Control\Director: properties: Middlewares: diff --git a/composer.json b/composer.json index b54c48e..cf45b1f 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,8 @@ "issues": "https://github.com/silverstripe/silverstripe-akismet/issues" }, "require": { - "php": "^7.4 || ^8.0", - "silverstripe/framework": "^4.10", - "silverstripe/cms": "^4.0", - "tijsverkoyen/akismet": "1.1.0", - "silverstripe/spamprotection": "^3.0" + "silverstripe/framework": "^5", + "silverstripe/spamprotection": "^4" }, "require-dev": { "phpunit/phpunit": "^9.5", @@ -37,4 +34,4 @@ "extra": [], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/src/AkismetField.php b/src/AkismetField.php index 015af9b..dfbe2a0 100644 --- a/src/AkismetField.php +++ b/src/AkismetField.php @@ -33,7 +33,7 @@ class AkismetField extends FormField /** * @var array */ - private $fieldMapping = array(); + private $fieldMapping = []; /** * @@ -67,7 +67,8 @@ protected function confirmationField() ->setForm($this->getForm()); } - public function Field($properties = array()) + + public function Field($properties = []) { $checkbox = $this->confirmationField(); if ($checkbox) { @@ -75,7 +76,8 @@ public function Field($properties = array()) } } - public function FieldHolder($properties = array()) + + public function FieldHolder($properties = []) { $checkbox = $this->confirmationField(); if ($checkbox) { @@ -92,7 +94,7 @@ public function getSpamMappedData() return null; } - $result = array(); + $result = []; $data = $this->form->getData(); foreach ($this->fieldMapping as $fieldName => $mappedName) { @@ -143,11 +145,12 @@ public function validate($validator) if (Config::inst()->get(AkismetSpamProtector::class, 'save_spam')) { // In order to save spam but still display the spam message, we must mock a form message // without failing the validation - $errors = array(array( + $errors = [[ 'fieldName' => $this->name, 'message' => $errorMessage, 'messageType' => 'error', - )); + ]]; + $formName = $this->getForm()->FormName(); $this->getForm()->sessionMessage($errorMessage, ValidationResult::TYPE_GOOD); diff --git a/src/AkismetSpamProtector.php b/src/AkismetSpamProtector.php index f275d55..4ae08d1 100644 --- a/src/AkismetSpamProtector.php +++ b/src/AkismetSpamProtector.php @@ -70,11 +70,11 @@ class AkismetSpamProtector implements SpamProtector * @config */ private static $save_spam = false; - + /** * @var array */ - private $fieldMapping = array(); + private $fieldMapping = []; /** * Set the API key @@ -87,7 +87,7 @@ public function setApiKey($key) $this->apiKey = $key; return $this; } - + /** * Get the API key. Priority is given first to explicitly set values on a singleton, then to configuration values * and finally to environment values. @@ -106,7 +106,7 @@ public function getApiKey() if (!empty($key)) { return $key; } - + // Check environment as last resort if ($envApiKey = Environment::getEnv('SS_AKISMET_API_KEY')) { return $envApiKey; @@ -114,7 +114,7 @@ public function getApiKey() return ''; } - + /** * Retrieves Akismet API object, or null if not configured * @@ -124,16 +124,16 @@ public function getService() { // Get API key and URL $key = $this->getApiKey(); + if (empty($key)) { user_error("AkismetSpamProtector is incorrectly configured. Please specify an API key.", E_USER_WARNING); return null; } - $url = Director::protocolAndHost(); - + // Generate API object - return Injector::inst()->get(AkismetService::class, false, array($key, $url)); + return Injector::inst()->get(AkismetService::class, false, [$key]); } - + public function getFormField($name = null, $title = null, $value = null, $form = null, $rightTitle = null) { return AkismetField::create($name, $title, $value, $form, $rightTitle) diff --git a/src/Config/AkismetConfig.php b/src/Config/AkismetConfig.php index 9fa8f49..524c132 100644 --- a/src/Config/AkismetConfig.php +++ b/src/Config/AkismetConfig.php @@ -11,9 +11,9 @@ */ class AkismetConfig extends DataExtension { - private static $db = array( + private static $db = [ 'AkismetKey' => 'Varchar' - ); + ]; public function updateCMSFields(FieldList $fields) { diff --git a/src/Service/AkismetService.php b/src/Service/AkismetService.php index d72837b..eeedcce 100644 --- a/src/Service/AkismetService.php +++ b/src/Service/AkismetService.php @@ -2,40 +2,141 @@ namespace SilverStripe\Akismet\Service; -/** - * Describes TijsVerkoyen\Akismet\Akismet - */ -interface AkismetService +use SilverStripe\Control\Director; +use Exception; + +class AkismetService { - /** - * Check if the comment is spam or not - * This is basically the core of everything. This call takes a number of - * arguments and characteristics about the submitted content and then - * returns a thumbs up or thumbs down. - * Almost everything is optional, but performance can drop dramatically if - * you exclude certain elements. - * REMARK: If you are having trouble triggering you can send - * "viagra-test-123" as the author and it will trigger a true response, - * always. - * - * @param string[optional] $content The content that was submitted. - * @param string[optional] $author The name. - * @param string[optional] $email The email address. - * @param string[optional] $url The URL. - * @param string[optional] $permalink The permanent location of the entry - * the comment was submitted to. - * @param string[optional] $type The type, can be blank, comment, - * trackback, pingback, or a made up - * value like "registration". - * @return bool If the comment is spam true will be - * returned, otherwise false. - */ - public function isSpam( - $content, - $author = null, - $email = null, - $url = null, - $permalink = null, - $type = null - ); + private $apiKey; + + private $endpoint; + + public function __construct($apiKey) + { + $this->apiKey = $apiKey; + $this->endpoint = sprintf('https://%s.rest.akismet.com/1.1/', $apiKey); + } + + + public function verifyKey() + { + $response = $this->post($this->endpoint . 'verify-key', [ + 'key' => $this->apiKey, + 'blog' => Director::protocolAndHost() + ]); + + return 'valid' == trim(strtolower($response['body'])); + } + + + public function buildData($content, $author = null, $email = null, $url = null, $permalink = null, $server = []) + { + $data = [ + 'blog' => Director::protocolAndHost(), + 'user_ip' => (isset($server['REMOTE_ADDR'])) ? $server['REMOTE_ADDR'] : '', + 'user_agent' => (isset($server['HTTP_USER_AGENT'])) ? $server['HTTP_USER_AGENT'] : '', + 'referrer' => (isset($server['HTTP_REFERER'])) ? $server['HTTP_REFERER'] : '', + 'permalink' => $permalink, + 'comment_type' => 'comment', + 'comment_author' => $author, + 'comment_author_email' => $email, + 'comment_author_url' => $url, + 'comment_content' => $content, + ]; + + return $data; + } + + + public function isSpam($content, $author = null, $email = null, $url = null, $permalink = null, $server = null) + { + if (is_null($server)) { + $server = $_SERVER; + } + + $data = $this->buildData($content, $author, $email, $url, $permalink, $server); + $response = $this->checkSpam($data, $server); + + return (isset($response['spam']) && $response['spam']); + } + + + public function submitSpam($content, $author = null, $email = null, $url = null, $permalink = null, $server = null) + { + if (is_null($server)) { + $server = $_SERVER; + } + + $data = $this->buildData($content, $author, $email, $url, $permalink); + $this->post($this->endpoint . 'submit-spam', $data); + } + + + public function checkSpam($data, $state = []) + { + $keys = array_intersect_key($state, array_fill_keys([ + 'HTTP_HOST', 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING', + 'HTTP_ACCEPT_CHARSET', 'HTTP_KEEP_ALIVE', 'HTTP_REFERER', 'HTTP_CONNECTION', 'HTTP_FORWARDED', + 'HTTP_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', + 'REMOTE_ADDR', 'REMOTE_HOST', 'REMOTE_PORT', 'SERVER_PROTOCOL', 'REQUEST_METHOD'], + 0 + )); + + $data = array_merge($keys, $data); + $response = $this->post($this->endpoint . 'comment-check', $data); + $response['error'] = $response['discard'] = $response['spam'] = null; + + $body = trim(strtolower($response['body'])); + + if ('true' == $body) { + $response['spam'] = true; + + if ( array_key_exists('x-akismet-pro-tip', $response['akismet_headers']) && $response['akismet_headers']['x-akismet-pro-tip'] == 'discard' ) { + $response['discard'] = true; + } + } else if ('false' == $body) { + $response['spam'] = false; + } else if (array_key_exists('x-akismet-debug-help', $response['akismet_headers'])) { + $response['error'] = $response['akismet_headers']['x-akismet-debug-help']; + } + + return $response; + } + + protected function post($endpoint, $data) + { + $response = []; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $endpoint); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 6); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + $curl_response = curl_exec($ch); + + if (false === $curl_response) { + throw new Exception('There was an error sending the Akismet request.'); + } + + $response['info'] = curl_getinfo($ch); + $response['info']['request_header'] .= http_build_query($data); + $response['header'] = substr($curl_response, 0, $response['info']['header_size']); + $response['body'] = substr($curl_response, $response['info']['header_size']); + + $response['akismet_headers'] = []; + + foreach (explode("\n", $response['header']) as $header) { + if (stripos($header, 'x-akismet') === 0) { + list($key, $value) = explode(':', $header, 2); + $response['akismet_headers'][strtolower($key)] = $value; + } + } + + curl_close($ch); + + return $response; + } } diff --git a/src/Service/AkismetServiceBackend.php b/src/Service/AkismetServiceBackend.php deleted file mode 100644 index 37e566c..0000000 --- a/src/Service/AkismetServiceBackend.php +++ /dev/null @@ -1,12 +0,0 @@ - 'Varchar', 'Email' => 'Varchar', 'Content' => 'Text', 'IsSpam' => 'Boolean', - ); + ]; private static $default_sort = 'ID'; }