From f41c61e490c50992951df9783ed062e0ef382572 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 19 Apr 2019 09:22:46 -0400 Subject: [PATCH] Upgrades and improvements to CommentFilterAkismet class --- .../Fieldtype/FieldtypeComments/Comment.php | 29 ++- .../FieldtypeComments/CommentFilter.php | 30 ++- .../CommentFilterAkismet.module | 178 ++++++++++++++---- 3 files changed, 193 insertions(+), 44 deletions(-) diff --git a/wire/modules/Fieldtype/FieldtypeComments/Comment.php b/wire/modules/Fieldtype/FieldtypeComments/Comment.php index 6f98d308..c452173b 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/Comment.php +++ b/wire/modules/Fieldtype/FieldtypeComments/Comment.php @@ -12,7 +12,8 @@ * @property int $parent_id * @property string $text * @property int $sort - * @property int $status + * @property int $status + * @property int|null $prevStatus * @property int $flags * @property int $created * @property string $email @@ -49,6 +50,12 @@ class Comment extends WireData { */ const statusApproved = 1; + /** + * Status for comment that's been approved and featured + * + */ + const statusFeatured = 2; + /** * Status for Comment to indicate pending deletion * @@ -150,7 +157,7 @@ class Comment extends WireData { $this->set('website', ''); $this->set('ip', ''); $this->set('user_agent', ''); - $this->set('created_users_id', $this->config->guestUserID); + $this->set('created_users_id', $this->config->guestUserPageID); $this->set('code', ''); // approval code $this->set('subcode', ''); // subscriber code (for later user modifications to comment) $this->set('upvotes', 0); @@ -161,8 +168,8 @@ class Comment extends WireData { public function get($key) { if($key == 'user' || $key == 'createdUser') { - if(!$this->settings['created_users_id']) return $this->users->get($this->config->guestUserID); - return $this->users->get($this->settings['created_users_id']); + if(!$this->created_users_id) return $this->users->get($this->config->guestUserPageID); + return $this->users->get($this->created_users_id); } else if($key == 'gravatar') { return $this->gravatar(); @@ -187,7 +194,10 @@ class Comment extends WireData { } else if($key == 'editUrl' || $key == 'editURL') { return $this->editUrl(); - } + + } else if($key == 'prevStatus') { + return $this->prevStatus; + } return parent::get($key); } @@ -454,9 +464,12 @@ class Comment extends WireData { * */ public function url($http = false) { - $fragment = "#Comment$this->id"; - if(!$this->page || !$this->page->id) return $fragment; - return ($http ? $this->page->httpUrl() : $this->page->url) . $fragment; + if($this->page && $this->page->id) { + $url = $http ? $this->page->httpUrl() : $this->page->url; + } else { + $url = $http ? $this->wire('config')->urls->httpRoot : $this->wire('config')->urls->root; + } + return $url . "#Comment$this->id"; } /** diff --git a/wire/modules/Fieldtype/FieldtypeComments/CommentFilter.php b/wire/modules/Fieldtype/FieldtypeComments/CommentFilter.php index c12dd39b..970ddca4 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/CommentFilter.php +++ b/wire/modules/Fieldtype/FieldtypeComments/CommentFilter.php @@ -12,31 +12,55 @@ * * ProcessWire 3.x, Copyright 2016 by Ryan Cramer * https://processwire.com + * + * @property string $appUserAgent + * @property string $charset + * @property string $homeURL @deprecated + * @property string $apiKey * * */ abstract class CommentFilter extends WireData { + /** + * @var Comment + * + */ protected $comment; public function __construct() { $this->set('appUserAgent', 'ProcessWire'); $this->set('charset', 'utf-8'); - $this->set('homeURL', 'http://' . $this->config->httpHost); $this->set('apiKey', ''); } public function init() { + /** @var Paths $urls */ + $urls = $this->wire('config')->urls; + $this->set('homeURL', $urls->httpRoot); } public function setComment(Comment $comment) { $this->comment = $comment; - $this->set('pageUrl', $this->homeURL . $this->wire('page')->url); - if(!$comment->ip) $comment->ip = $_SERVER['REMOTE_ADDR']; + $page = $comment->getPage(); + if(!$page || !$page->id) $page = $this->wire('page'); + $this->set('pageUrl', $page->httpUrl); + if(!$comment->ip) $comment->ip = $this->wire('session')->getIP(); if(!$comment->user_agent) $comment->user_agent = $_SERVER['HTTP_USER_AGENT']; } + /** + * Send an HTTP POST request + * + * @param $request + * @param $host + * @param $path + * @param int $port + * @return array|string + * @deprecated no longer in use (replaced with WireHttp) + * + */ protected function httpPost($request, $host, $path, $port = 80) { // from ksd_http_post() - http://akismet.com/development/api/ diff --git a/wire/modules/Fieldtype/FieldtypeComments/CommentFilterAkismet.module b/wire/modules/Fieldtype/FieldtypeComments/CommentFilterAkismet.module index 18b814b1..17b8846b 100644 --- a/wire/modules/Fieldtype/FieldtypeComments/CommentFilterAkismet.module +++ b/wire/modules/Fieldtype/FieldtypeComments/CommentFilterAkismet.module @@ -5,7 +5,7 @@ * * Implementation of a CommentFilter class specific to the Akismet filtering service. * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2019 by Ryan Cramer * https://processwire.com * * @@ -16,6 +16,12 @@ require_once($dirname . "/CommentFilter.php"); /** * Uses the Akismet service to identify comment spam. Module plugin for the Comments Fieldtype. + * + * @property string $apiKey + * @property string $validKey + * @property Comment $comment + * @property string $pageUrl + * * */ class CommentFilterAkismet extends CommentFilter implements Module, ConfigurableModule { @@ -23,7 +29,7 @@ class CommentFilterAkismet extends CommentFilter implements Module, Configurable public static function getModuleInfo() { return array( 'title' => __('Comment Filter: Akismet', __FILE__), - 'version' => 102, + 'version' => 200, 'summary' => __('Uses the Akismet service to identify comment spam. Module plugin for the Comments Fieldtype.', __FILE__), 'permanent' => false, 'singular' => false, @@ -34,61 +40,166 @@ class CommentFilterAkismet extends CommentFilter implements Module, Configurable public function __construct() { parent::__construct(); - $this->set('appUserAgent', "ProcessWire/2 | AkismetCommentFilter/1"); - $this->set('apiKey', ''); + $this->set('appUserAgent', "ProcessWire/3 | AkismetCommentFilter/2"); + $this->set('apiKey', ''); + $this->set('validKey', ''); + } + + protected function akismetHttpPost($action, array $data) { + + $http = new WireHttp(); + $http->setHeader('content-type', "application/x-www-form-urlencoded; charset={$this->charset}"); + $http->setHeader('user-agent', $this->appUserAgent); + + $response = $http->post('https://' . $this->apiKey . ".rest.akismet.com/1.1/$action", $data); + + $result = $response; + if($action === 'comment-check') $result = $result === 'true' ? 'SPAM' : 'not spam'; + $msg = "$action: $result "; + $comment = $this->comment; + + if($comment) { + if($comment->id) $msg .= "comment $comment->id "; + $msg .= "by $comment->email "; + $page = $this->comment->getPage(); + if($page && $page->id) $msg .= "on page $page->path"; + } + + $this->saveLog($msg); + + return trim($response); } - protected function verifyKey() { - if(!$this->comment) throw new WireException("No Comment provided to CommentFilter"); - if(!$this->apiKey) throw new WireException("apiKey must be set to use this filter"); - $request = "key={$this->apiKey}&blog=" . urlencode($this->homeURL); - $response = $this->httpPost($request, 'rest.akismet.com', '/1.1/verify-key'); - if($response[1] == 'valid') return true; - if($response[1] == 'invalid') throw new WireException("Invalid Akismet Key $request, " . print_r($response, true)); - // some other error + /** + * Verify Akismet API key + * + * @param string $apiKey + * @return bool + * + */ + protected function verifyKey($apiKey = '') { + + if(empty($apiKey)) $apiKey = $this->apiKey; + + if(empty($apiKey)) { + if(strlen($this->validKey)) $this->wire('modules')->saveConfig($this, 'validKey', ''); + return false; + } + + if($apiKey === $this->validKey) return true; + + $request = array( + 'key' => $this->apiKey, + 'blog' => $this->wire('config')->urls->httpRoot + ); + + $response = $this->akismetHttpPost('verify-key', $request); + + if($response === 'valid') { + $this->validKey = $apiKey; + $this->wire('modules')->saveConfig($this, array( + 'apiKey' => $apiKey, + 'validKey' => $apiKey, + )); + $msg = "Akismet API key has been validated"; + $this->message($msg); + $this->saveLog($msg); + return true; + + } else if($response === 'invalid') { + $msg = 'Invalid Akismet API key provided'; + if(strlen($this->validKey)) $this->wire('modules')->saveConfig($this, 'validKey', ''); + $this->error($msg); + $this->saveLog("$msg: $apiKey"); + return false; + } + return false; } - protected function buildRequest() { - $request = - "blog=" . urlencode($this->homeURL) . - "&user_ip={$this->comment->ip}" . - "&user_agent=" . urlencode($this->comment->user_agent) . - // "&referrer=" . urlencode($this->referrer) . - "&permalink=" . urlencode($this->pageUrl) . - "&comment_type=comment" . - "&comment_author=" . urlencode($this->comment->cite) . - "&comment_author_email=" . urlencode($this->comment->email) . - "&comment_author_url=" . urlencode($this->comment->website) . - "&comment_content=" . urlencode($this->comment->text); - return $request; + /** + * Build Akismet POST request + * + * @return array + * @throws WireException + * + */ + public function buildRequest() { + return array( + 'blog' => $this->wire('config')->urls->httpRoot, + 'user_ip' => $this->comment->ip, + 'user_agent' => $this->comment->user_agent, + 'permalink' => $this->comment->httpUrl(), + 'comment_type' => 'comment', + 'comment_author' => $this->comment->cite, + 'comment_author_email' => $this->comment->email, + 'comment_author_url' => $this->comment->website, + 'comment_content' => $this->comment->text + ); } + + /** + * Check if comment is spam + * + * @return bool + * + */ public function checkSpam() { + if($this->comment->status == Comment::statusSpam) return true; - $this->verifyKey(); + if(!$this->verifyKey()) return false; + $request = $this->buildRequest(); - $response = $this->httpPost($request, $this->apiKey . ".rest.akismet.com", '/1.1/comment-check'); - $isSpam = $response[1] == 'true'; + $response = $this->akismetHttpPost('comment-check', $request); + $isSpam = $response === 'true'; $this->setIsSpam($isSpam); - //print_r($response); + return $isSpam; } + /** + * Tell Akismet comment is spam + * + */ public function submitSpam() { - $this->verifyKey(); + if(!$this->verifyKey()) return false; $request = $this->buildRequest(); - $this->httpPost($request, $this->apiKey . ".rest.akismet.com", '/1.1/submit-spam'); + $this->akismetHttpPost('submit-spam', $request); $this->message("Notified Akismet of spam that it didn't identify"); + return true; } + /** + * Tell Akismet comment is not spam and they made an error + * + */ public function submitHam() { - $this->verifyKey(); + if(!$this->verifyKey()) return false; $request = $this->buildRequest(); - $this->httpPost($request, $this->apiKey . ".rest.akismet.com", '/1.1/submit-ham'); + $this->akismetHttpPost('submit-ham', $request); $this->message("Notified Akismet of a spam false positive (ham)"); + return true; } + /** + * Save log entry + * + * @param string $msg + * + */ + public function saveLog($msg) { + $this->wire('log')->save('comment-filter-akismet', $msg); + } + + + /** + * Configure module + * + * @param array $data + * @return InputfieldWrapper + * + */ public function getModuleConfigInputfields(array $data) { $inputfields = $this->wire(new InputfieldWrapper()); @@ -102,6 +213,7 @@ class CommentFilterAkismet extends CommentFilter implements Module, Configurable $f->description = $this->_('If you want to have comments automatically identified as spam, the Comments fieldtype can utilize the Akismet service to do this. In order to use it, you must enter an Akismet API key obtained from akismet.com. Use of this service is optional but recommended.'); // Akismet description $inputfields->append($f); + $this->verifyKey($data[$name]); return $inputfields;