From fb03244f9074a79a23128546a8474411c4972d7c Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Tue, 21 Nov 2023 17:33:21 +1300 Subject: [PATCH 1/4] MNT bump dependancies to support SS5 --- composer.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index b54c48e..f0d3caf 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,9 @@ "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", + "tijsverkoyen/akismet": "^1.1", + "silverstripe/spamprotection": "^4" }, "require-dev": { "phpunit/phpunit": "^9.5", @@ -37,4 +35,4 @@ "extra": [], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} From 8e504092b3c949339f4fb2824ff62e7237c77cb7 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Tue, 21 Nov 2023 17:35:04 +1300 Subject: [PATCH 2/4] MNT array syntax --- src/AkismetField.php | 13 +++++++------ src/AkismetSpamProtector.php | 16 ++++++++-------- src/Config/AkismetConfig.php | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/AkismetField.php b/src/AkismetField.php index 015af9b..a90c636 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,7 @@ protected function confirmationField() ->setForm($this->getForm()); } - public function Field($properties = array()) + public function Field($properties = [])) { $checkbox = $this->confirmationField(); if ($checkbox) { @@ -75,7 +75,7 @@ public function Field($properties = array()) } } - public function FieldHolder($properties = array()) + public function FieldHolder($properties = []) { $checkbox = $this->confirmationField(); if ($checkbox) { @@ -92,7 +92,7 @@ public function getSpamMappedData() return null; } - $result = array(); + $result = []; $data = $this->form->getData(); foreach ($this->fieldMapping as $fieldName => $mappedName) { @@ -143,11 +143,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..85b9832 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 * @@ -129,11 +129,11 @@ public function getService() 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, $url]); } - + 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) { From 43cccfcb771fdf00cdaf6e58c17b361dc8d6a80c Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Sun, 26 Nov 2023 08:26:32 +1300 Subject: [PATCH 3/4] feat: remove unsupported thirdparty dependancies --- .upgrade.yml | 3 +- _config/akismet.yml | 2 - composer.json | 1 - src/AkismetField.php | 4 +- src/AkismetSpamProtector.php | 4 +- src/Service/AkismetService.php | 163 ++++++++++++++++++++------ src/Service/AkismetServiceBackend.php | 12 -- tests/AkismetTestService.php | 6 +- tests/AkismetTestSubmission.php | 4 +- 9 files changed, 139 insertions(+), 60 deletions(-) delete mode 100644 src/Service/AkismetServiceBackend.php 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 f0d3caf..cf45b1f 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,6 @@ }, "require": { "silverstripe/framework": "^5", - "tijsverkoyen/akismet": "^1.1", "silverstripe/spamprotection": "^4" }, "require-dev": { diff --git a/src/AkismetField.php b/src/AkismetField.php index a90c636..dfbe2a0 100644 --- a/src/AkismetField.php +++ b/src/AkismetField.php @@ -67,7 +67,8 @@ protected function confirmationField() ->setForm($this->getForm()); } - public function Field($properties = [])) + + public function Field($properties = []) { $checkbox = $this->confirmationField(); if ($checkbox) { @@ -75,6 +76,7 @@ public function Field($properties = [])) } } + public function FieldHolder($properties = []) { $checkbox = $this->confirmationField(); diff --git a/src/AkismetSpamProtector.php b/src/AkismetSpamProtector.php index 85b9832..4ae08d1 100644 --- a/src/AkismetSpamProtector.php +++ b/src/AkismetSpamProtector.php @@ -124,14 +124,14 @@ 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, [$key, $url]); + return Injector::inst()->get(AkismetService::class, false, [$key]); } public function getFormField($name = null, $title = null, $value = null, $form = null, $rightTitle = null) diff --git a/src/Service/AkismetService.php b/src/Service/AkismetService.php index d72837b..22b8250 100644 --- a/src/Service/AkismetService.php +++ b/src/Service/AkismetService.php @@ -2,40 +2,133 @@ 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) + { + $data = [ + 'blog' => Director::protocolAndHost(), + 'user_ip' => $_SERVER['REMOTE_ADDR'], + '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) + { + $data = $this->buildData($content, $author, $email, $url, $permalink); + $response = $this->checkSpam($data); + + return (isset($response['spam']) && $response['spam']); + } + + + public function submitSpam($content, $author = null, $email = null, $url = null, $permalink = null) + { + $data = $this->buildData($content, $author, $email, $url, $permalink); + $this->post($this->endpoint . 'submit-spam', $data); + } + + + public function checkSpam($data) + { + $keys = array_intersect_key($_SERVER, 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'; } From cf0aa170687dc5fd310fba63e5f9a2e4c339ef98 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Tue, 5 Dec 2023 08:48:36 +1300 Subject: [PATCH 4/4] feat: decouple from _SERVER --- src/Service/AkismetService.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Service/AkismetService.php b/src/Service/AkismetService.php index 22b8250..eeedcce 100644 --- a/src/Service/AkismetService.php +++ b/src/Service/AkismetService.php @@ -29,13 +29,13 @@ public function verifyKey() } - public function buildData($content, $author = null, $email = null, $url = null, $permalink = null) + public function buildData($content, $author = null, $email = null, $url = null, $permalink = null, $server = []) { $data = [ 'blog' => Director::protocolAndHost(), - 'user_ip' => $_SERVER['REMOTE_ADDR'], - 'user_agent' => $_SERVER['HTTP_USER_AGENT'], - 'referrer' => (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : '', + '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, @@ -48,25 +48,33 @@ public function buildData($content, $author = null, $email = null, $url = null, } - public function isSpam($content, $author = null, $email = null, $url = null, $permalink = null) + public function isSpam($content, $author = null, $email = null, $url = null, $permalink = null, $server = null) { - $data = $this->buildData($content, $author, $email, $url, $permalink); - $response = $this->checkSpam($data); + 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) + 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) + public function checkSpam($data, $state = []) { - $keys = array_intersect_key($_SERVER, array_fill_keys([ + $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',