I've been looking at spam detection techniques for a while now, so when Zend_Form came onto the scene I thought that it would be nice to explore Zend's solution. Zend makes use of the Akismet web service for spam detection, a name which should be familiar to anyone who's signed up for a WordPress account, and here's a first stab at getting Zend_Form and Zend_Service_Akismet integrated.
The first thing we need is a bootstrap file. There's nothing magical happening in mine – it's a vanilla Zend Framework bootstrap – except for the three lines that enable the use of Zend_Form (which is currently still in the incubator.
index.php
ini_set( 'include_path', '/usr/share/php5/Zend/library'.PATH_SEPARATOR.
'/usr/share/php5/Zend/incubator/library'.PATH_SEPARATOR.
'/var/www/example/libs');
require_once('Zend/Loader.php');
Zend_Loader::registerAutoload();
// enable Zend_Form
$view = new Zend_View();
$view->addHelperPath('/usr/share/php5/Zend/incubator/library/Zend/View/Helper');
Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')->setView($view);
// dispatch
$front = Zend_Controller_Front::getInstance();
$front->setControllerDirectory('/var/www/example/controllers');
$front->dispatch();
The next thing we'll need is a set of views. I've created four views, even though we only need one.
// index.phtml <?php echo $this->form ?> // post.phtml <?php echo $this->form ?> // success.phtml <p>Success!</p> // spam.phtml <p>Spam!</p>
And now, a controller to create and handle the form.
class ExampleController extends Zend_Controller_Action
{
private $_formConfig = array(
'action' => '/example/post',
'method' => 'post',
'elements' => array(
'name' => array(
'text',
array(
'label' => 'Name',
'validators' => array(
array('stringLength', false, array(1))
),
'required' => true,
'akismet' => 'comment_author'
)
),
'email' => array(
'text',
array(
'label' => 'Email',
'validators' => array(
array('emailAddress', false)
),
'required' => true,
'akismet' => 'comment_author_email'
)
),
'comment' => array(
'textarea',
array(
'label' => 'Comment',
'validators' => array(
array('stringLength', false, array(1))
),
'required' => true,
'akismet' => 'comment_content'
)
),
'submit' => 'submit'
),
'akismetType' => 'comment'
);
public function indexAction ()
{
$form = new Zend_Form($this->_formConfig);
$this->view->form = $form;
}
public function postAction ()
{
$form = new Example_Form($this->_formConfig);
if ($form->isValid($_POST))
{
if ($form->isSpam($_POST))
{
$this->_redirect('/example/spam');
}
else
{
$this->_redirect('/example/success');
}
}
else
{
$this->view->form = $form;
}
}
public function successAction ()
{
}
public function spamAction ()
{
}
}
The code about is a standard implementation of Zend_Form, with two extra details. The first is the use of an 'akismet' key in element definitions. The section is the use of an 'akismetType' key in the form definition. These keys work simply: the value of 'akismetType' maps to Akismet's 'comment_type' parameter, and the 'akismet' keys map elements to Akismet parameters. For instance, in the email element above, the value that the user fills in will be sent to Akismet with the 'comment_author_email' parameter.
All very well and good, but Zend_Form will ignore these parameters unless we tell it to do otherwise. To do this, we'll override Zend_Form and give it a new function called isSpam().
class Example_Form extends Zend_Form
{
protected $_akismet;
function __construct ($options = NULL)
{
$data = array();
if (is_array($options) &&
array_key_exists('elements', $options) &&
is_array($options['elements']))
{
foreach ($options['elements'] as $field => $element)
{
if (is_array($element) &&
sizeof($element) > 1 &&
is_array($element[1]) &&
array_key_exists('akismet', $element[1]))
{
$data[$element[1]['akismet']] = $field;
}
}
}
if (is_array($options) &&
array_key_exists('akismetType', $options))
{
$data['comment_type'] = $options['akismetType'];
}
$this->_akismet = $data;
return parent::__construct($options);
}
public function isSpam ($data)
{
$akismetData = array();
foreach ($this->_akismet as $akisName => $postName)
{
if ($akisName != 'comment_type')
{
$akismetData[$akisName] = $data[$postName];
}
}
$akismetData['user_ip'] = $_SERVER['REMOTE_ADDR'];
$akismetData['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$akismet = new Zend_Service_Akismet(
'xxxxxxxxxx', 'http://blog.localhost/');
return $akismet->isSpam($akismetData);
}
}
This code warrants some explanation. The constructor, to begin with, processes the Askismet keys passed to it in the controller above and creates a mapping between element names and Akismet parameters. It then passes controll to Zend_Form::__construct(). When isSpam() is called, the mapping is used to extract the values from the user's posted data that will be used in the call to Akismet. The call itself is quite simple: you just need your API key (which you can purchase from their website for $500/year for a commercial license or for personal use you can just sign up to WordPress and one will be mailed to you) and your webapp's URL.
And that's it! The obvious drawback of my implementation is that you can only define Akismet mappings if you call Zend_Form::__construct() with a config array. You'd need to override Zend_Form_Element so that the Akismet key can be passed in Zend_Form_Element::__construct() or as a parameter to Zend_Form::addElement(). Also, there's no nice way at the moment to specify form-wide errors. This issue is currently being discussed on the Zend Framework mailing lists. Will keep you updated.
discuss this topic to forum
