Adding TypePad Anti Spam to your Contact Form with Zend Framework
About 2 weeks ago, I posted a quick tutorial on how to implement Akismet.com’s anti-spam checks to your contact form using the excellent Zend Framework. I was fortunate enough to have Anil Dash from Six Apart (the creators of TypePad) drop by and post a comment asking me if I’d be interested in giving TypePad’s Antispam service a try.
I’m happy to report that TypePad Antispam is just as effective as Akismet, with the bonus of there being no restrictions on usage (that I could find in any case).
With all of that in mind – I simply copied the current Zend/Service/Akismet.php class – renamed it to Zend/Service/TypePadAntiSpam.php and did a simple search and replace. The two services are so alike in implementation, that’s all it took.
The code posted below makes the following assumptions :
- You have a TypePad Antispam API key
- You have the Zend Framework installed and configured correctly : click here for documentation
- You have written the form HTML, the form has been submitted and all input data has been sanitized (make sure to read the comments).
- You know how to use the php mail() function or have an equivalent method
Firstly the TypePad Antispam Class. Simply save this file to the following folder inside your Zend Framework installation : Zend/Service/TypePadAntiSpam.php (should be in the same folder as Akismet.php)
<?php
/**
* Please note that this is NOT an official Zend Framework package.
* This is essentially a copy-paste-modification of the original Zend Framework's Service/Akismet.php class to
* work with the TypePad Anti Spam service. If you find this class useful or find an error etc, please leave a
* comment at http://calisza.wordpress.com - all feedback is welcome.
*
* All original/offical headers have been left intact. Thanks to all the devs who have made the Zend Framework
* the wonderful product that it is.
*/
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Service
* @subpackage TypePadAntiSpam
* @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/**
* @see Zend_Version
*/
require_once 'Zend/Version.php';
/**
* @see Zend_Service_Abstract
*/
require_once 'Zend/Service/Abstract.php';
/**
* Typepad Anti Spam REST service implementation
*
* @uses Zend_Service_Abstract
* @category Zend
* @package Zend_Service
* @subpackage TypePadAntiSpam
* @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Service_TypePadAntiSpam extends Zend_Service_Abstract
{
/**
* TypePadAntiSpam API key
* @var string
*/
protected $_apiKey;
/**
* Blog URL
* @var string
*/
protected $_blogUrl;
/**
* Charset used for encoding
* @var string
*/
protected $_charset = 'UTF-8';
/**
* TCP/IP port to use in requests
* @var int
*/
protected $_port = 80;
/**
* User Agent string to send in requests
* @var string
*/
protected $_userAgent;
/**
* Constructor
*
* @param string $apiKey TypePadAntiSpam API key
* @param string $blog Blog URL
* @return void
*/
public function __construct($apiKey, $blog)
{
$this->setBlogUrl($blog)
->setApiKey($apiKey)
->setUserAgent('Zend Framework/' . Zend_Version::VERSION . ' | TypePadAntiSpam/1.1');
}
/**
* Retrieve blog URL
*
* @return string
*/
public function getBlogUrl()
{
return $this->_blogUrl;
}
/**
* Set blog URL
*
* @param string $blogUrl
* @return Zend_Service_TypePadAntiSpam
* @throws Zend_Service_Exception if invalid URL provided
*/
public function setBlogUrl($blogUrl)
{
require_once 'Zend/Uri.php';
if (!Zend_Uri::check($blogUrl)) {
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Invalid url provided for blog');
}
$this->_blogUrl = $blogUrl;
return $this;
}
/**
* Retrieve API key
*
* @return string
*/
public function getApiKey()
{
return $this->_apiKey;
}
/**
* Set API key
*
* @param string $apiKey
* @return Zend_Service_TypePadAntiSpam
*/
public function setApiKey($apiKey)
{
$this->_apiKey = $apiKey;
return $this;
}
/**
* Retrieve charset
*
* @return string
*/
public function getCharset()
{
return $this->_charset;
}
/**
* Set charset
*
* @param string $charset
* @return Zend_Service_TypePadAntiSpam
*/
public function setCharset($charset)
{
$this->_charset = $charset;
return $this;
}
/**
* Retrieve TCP/IP port
*
* @return int
*/
public function getPort()
{
return $this->_port;
}
/**
* Set TCP/IP port
*
* @param int $port
* @return Zend_Service_TypePadAntiSpam
* @throws Zend_Service_Exception if non-integer value provided
*/
public function setPort($port)
{
if (!is_int($port)) {
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Invalid port');
}
$this->_port = $port;
return $this;
}
/**
* Retrieve User Agent string
*
* @return string
*/
public function getUserAgent()
{
return $this->_userAgent;
}
/**
* Set User Agent
*
* Should be of form "Some user agent/version | TypePadAntiSpam/version"
*
* @param string $userAgent
* @return Zend_Service_TypePadAntiSpam
* @throws Zend_Service_Exception with invalid user agent string
*/
public function setUserAgent($userAgent)
{
if (!is_string($userAgent)
|| !preg_match(":^[^\n/]*/[^ ]* \| TypePadAntiSpam/[0-9\.]*$:i", $userAgent))
{
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Invalid User Agent string; must be of format "Application name/version | TypePadAntiSpam/version"');
}
$this->_userAgent = $userAgent;
return $this;
}
/**
* Post a request
*
* @param string $host
* @param string $path
* @param array $params
* @return mixed
*/
protected function _post($host, $path, array $params)
{
$uri = 'http://' . $host . ':' . $this->getPort() . $path;
$client = self::getHttpClient();
$client->setUri($uri);
$client->setConfig(array(
'useragent' => $this->getUserAgent(),
));
$client->setHeaders(array(
'Host' => $host,
'Content-Type' => 'application/x-www-form-urlencoded; charset=' . $this->getCharset()
));
$client->setParameterPost($params);
$client->setMethod(Zend_Http_Client::POST);
return $client->request();
}
/**
* Verify an API key
*
* @param string $key Optional; API key to verify
* @param string $blog Optional; blog URL against which to verify key
* @return boolean
*/
public function verifyKey($key = null, $blog = null)
{
if (null === $key) {
$key = $this->getApiKey();
}
if (null === $blog) {
$blog = $this->getBlogUrl();
}
$response = $this->_post('api.antispam.typepad.com', '/1.1/verify-key', array(
'key' => $key,
'blog' => $blog
));
return ('valid' == $response->getBody());
}
/**
* Perform an API call
*
* @param string $path
* @param array $params
* @return Zend_Http_Response
* @throws Zend_Service_Exception if missing user_ip or user_agent fields
*/
protected function _makeApiCall($path, $params)
{
if (empty($params['user_ip']) || empty($params['user_agent'])) {
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Missing required TypePadAntiSpam fields (user_ip and user_agent are required)');
}
if (!isset($params['blog'])) {
$params['blog'] = $this->getBlogUrl();
}
return $this->_post($this->getApiKey() . '.api.antispam.typepad.com', $path, $params);
}
/**
* Check a comment for spam
*
* Checks a comment to see if it is spam. $params should be an associative
* array with one or more of the following keys (unless noted, all keys are
* optional):
* - blog: URL of the blog. If not provided, uses value returned by {@link getBlogUrl()}
* - user_ip (required): IP address of comment submitter
* - user_agent (required): User Agent used by comment submitter
* - referrer: contents of HTTP_REFERER header
* - permalink: location of the entry to which the comment was submitted
* - comment_type: typically, one of 'blank', 'comment', 'trackback', or 'pingback', but may be any value
* - comment_author: name submitted with the content
* - comment_author_email: email submitted with the content
* - comment_author_url: URL submitted with the content
* - comment_content: actual content
*
* Additionally, TypePadAntiSpam suggests returning the key/value pairs in the
* $_SERVER array, and these may be included in the $params.
*
* This method implements the TypePadAntiSpam comment-check REST method.
*
* @param array $params
* @return boolean
* @throws Zend_Service_Exception with invalid API key
*/
public function isSpam($params)
{
$response = $this->_makeApiCall('/1.1/comment-check', $params);
$return = trim($response->getBody());
if ('invalid' == $return) {
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Invalid API key');
}
if ('true' == $return) {
return true;
}
return false;
}
/**
* Submit spam
*
* Takes the same arguments as {@link isSpam()}.
*
* Submits known spam content to TypePadAntiSpam to help train it.
*
* This method implements TypePadAntiSpam's submit-spam REST method.
*
* @param array $params
* @return void
* @throws Zend_Service_Exception with invalid API key
*/
public function submitSpam($params)
{
$response = $this->_makeApiCall('/1.1/submit-spam', $params);
$value = trim($response->getBody());
if ('invalid' == $value) {
require_once 'Zend/Service/Exception.php';
throw new Zend_Service_Exception('Invalid API key');
}
}
/**
* Submit ham
*
* Takes the same arguments as {@link isSpam()}.
*
* Submits a comment that has been falsely categorized as spam by TypePadAntiSpam
* as a false positive, telling TypePadAntiSpam's filters not to filter such
* comments as spam in the future.
*
* Unlike {@link submitSpam()} and {@link isSpam()}, a valid API key is
* never necessary; as a result, this method never throws an exception
* (unless an exception happens with the HTTP client layer).
*
* this method implements TypePadAntiSpam's submit-ham REST method.
*
* @param array $params
* @return void
*/
public function submitHam($params)
{
$response = $this->_makeApiCall('/1.1/submit-ham', $params);
}
}
Next, some code to implement our new class :
<?php
/*
* Basic function to check for spam
* @param items : associative array for containing form field values
* @return boolean : true if spam, false if clean
*/
function spamCheck($items){
require_once 'Zend/Service/TypePadAntiSpam.php'; // include the required class file - change path if necessary
$url = "http://url.to.my.blog.or.form"; // url associated with API key
$api = "432dsjk890"; // TypePad Antispam API key
$spam = new Zend_Service_TypePadAntiSpam($api, $url ); // create new instance of our TypePadAntiSpam Service class
if ($spam->verifyKey()){ // make sure the API key if valid before performing check
$params = array(); // check the comments for the isSpam() method in Zend/Service/TypePadAntiSpam.php for more information on available parameters
$params["user_ip"] = $_SERVER['REMOTE_ADDR']; // required by TypePadAntiSpam
$params["user_agent"] = $_SERVER['HTTP_USER_AGENT']; // required by TypePadAntiSpam
$params["referrer"] = $_SERVER[ 'HTTP_REFERER'];
$params["comment_type"] = "comment";
$params["comment_author"] = $items["name"];
$params["comment_author_email"] = $items["email"];
$params["comment_content"] = $items["comments"];
return $spam->isSpam($params); // submits api call and returns true if spam, false if clean
} else {
return false;
}
}
// to make use of our spam check function try the following :
$items = sanitize($_POST); // sanitize is your own built-in function to sanitize user submitted data
// only mail the form contents if not spam
if (!spamCheck($items)){
// insert code to mail form contents here
}
?>
That should do it. You should now have a robust, easy to use anti-spam solution for your contact forms.
