Commit 6435d79c authored by Richard Mansfield's avatar Richard Mansfield
Browse files

Move some of the antispam form stuff into pieforms for easier reuse


Signed-off-by: default avatarRichard Mansfield <richardm@catalyst.net.nz>
parent 8c2ee780
......@@ -47,22 +47,9 @@ else {
$email = '';
}
// we're in the middle of processing the form, so read the time
// from the form rather than getting a new one
if ($_POST) {
$time = $_POST['timestamp'];
}
else {
$time = time();
}
$fields = array('name', 'email', 'subject', 'message', 'userid', 'submit', 'invisiblefield', 'invisiblesubmit');
$hashed_fields = hash_fieldnames($fields, $time);
$elements = array(
'name' => array(
'type' => 'text',
'name' => $hashed_fields['name'],
'title' => get_string('name'),
'defaultvalue' => $name,
'rules' => array(
......@@ -71,7 +58,6 @@ $elements = array(
),
'email' => array(
'type' => 'text',
'name' => $hashed_fields['email'],
'title' => get_string('email'),
'defaultvalue' => $email,
'rules' => array(
......@@ -81,13 +67,11 @@ $elements = array(
),
'subject' => array(
'type' => 'text',
'name' => $hashed_fields['subject'],
'title' => get_string('subject'),
'defaultvalue' => '',
),
'message' => array(
'type' => 'textarea',
'name' => $hashed_fields['message'],
'rows' => 10,
'cols' => 60,
'title' => get_string('message'),
......@@ -98,122 +82,68 @@ $elements = array(
)
);
$elements['invisiblefield'] = array(
'type' => 'text',
'name' => $hashed_fields['invisiblefield'],
'title' => get_string('spamtrap'),
'defaultvalue' => '',
'class' => 'dontshow',
);
$elements['userid'] = array(
'type' => 'hidden',
'name' => $hashed_fields['userid'],
'value' => $userid,
);
$elements['timestamp'] = array(
'type' => 'hidden',
'value' => $time,
);
$elements['invisiblesubmit'] = array(
'type' => 'submit',
'name' => $hashed_fields['invisiblesubmit'],
'value' => get_string('spamtrap'),
'class' => 'dontshow',
);
$elements['submit'] = array(
'type' => 'submit',
'name' => $hashed_fields['submit'],
'value' => get_string('sendmessage'),
);
// swap the name and email fields at random
if (rand(0,1)) {
$name = array_shift($elements);
$email = array_shift($elements);
array_unshift($elements, $email, $name);
}
$contactform = pieform(array(
'name' => 'contactus',
'method' => 'post',
'action' => '',
'elements' => $elements
'name' => 'contactus',
'method' => 'post',
'action' => '',
'elements' => $elements,
'spam' => array(
'secret' => get_config('formsecret'),
'mintime' => 5,
'hash' => array('name', 'email', 'subject', 'message', 'userid', 'submit'),
'reorder' => array('name', 'email'),
),
));
function contactus_validate(Pieform $form, $values) {
global $SESSION;
$error = false;
$currenttime = time();
// read the timestamp field
$timestamp = $values['timestamp'];
// recompute the field names
$fields = array('name', 'email', 'subject', 'message', 'userid', 'submit', 'invisiblefield', 'invisiblesubmit');
$hashed = hash_fieldnames($fields, $timestamp);
// make sure the submission is less than a day, and more than 5 seconds old
if ($currenttime - $timestamp < 5 || $currenttime - $timestamp > 86400) {
$error = true;
}
// make sure the real submit button was used. If it wasn't, it won't exist.
elseif (!isset($values[$hashed['submit']]) || isset($values[$hashed['invisiblesubmit']])) {
$error = true;
}
// make sure the invisible field is empty
elseif (!isset($values[$hashed['invisiblefield']]) || $values[$hashed['invisiblefield']] != '') {
$error = true;
}
// make sure all the other data fields exist
elseif (!(isset($values[$hashed['name']]) && isset($values[$hashed['email']]) &&
isset($values[$hashed['subject']]) && isset($values[$hashed['message']]))) {
$error = true;
}
else {
$spamtrap = new_spam_trap(array(
array(
'type' => 'name',
'value' => $values[$hashed['name']],
),
array(
'type' => 'email',
'value' => $values[$hashed['email']],
),
array(
'type' => 'subject',
'value' => $values[$hashed['subject']],
),
array(
'type' => 'body',
'value' => $values[$hashed['message']],
),
));
if ($spamtrap->is_spam()) {
$error = true;
}
}
if ($error) {
$spamtrap = new_spam_trap(array(
array(
'type' => 'name',
'value' => $values['name'],
),
array(
'type' => 'email',
'value' => $values['email'],
),
array(
'type' => 'subject',
'value' => $values['subject'],
),
array(
'type' => 'body',
'value' => $values['message'],
),
));
if ($form->spam_error() || $spamtrap->is_spam()) {
$msg = get_string('formerror');
$emailcontact = get_config('emailcontact');
if (!empty($emailcontact)) {
$msg .= ' ' . get_string('formerroremail', 'mahara', $emailcontact, $emailcontact);
}
$SESSION->add_error_msg($msg);
$form->set_error($hashed['submit'], '');
$form->set_error('submit', '');
}
}
function contactus_submit(Pieform $form, $values) {
global $SESSION;
// read the timestamp field
$timestamp = $values['timestamp'];
// recompute the field names
$fields = array('name', 'email', 'subject', 'message', 'userid', 'submit', 'invisiblefield', 'invisiblesubmit');
$hashed = hash_fieldnames($fields, $timestamp);
$data = new StdClass;
$data->fromname = $values[$hashed['name']];
$data->fromemail = $values[$hashed['email']];
$data->subject = $values[$hashed['subject']];
$data->message = $values[$hashed['message']];
if ($values[$hashed['userid']]) {
$data->fromuser = $values[$hashed['userid']];
$data->fromname = $values['name'];
$data->fromemail = $values['email'];
$data->subject = $values['subject'];
$data->message = $values['message'];
if ($values['userid']) {
$data->fromuser = $values['userid'];
}
require_once('activity.php');
activity_occurred('contactus', $data);
......
......@@ -27,27 +27,6 @@
defined('INTERNAL') || die();
function get_ip() {
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $_SERVER['REMOTE_ADDR'];
}
function hash_fieldnames($names, $time) {
$ip = get_ip();
$secret = get_config('formsecret');
$hashed = array();
foreach ($names as $name) {
// prefix the hash with an underscore to ensure it is always a valid pieforms element name
$hashed[$name] = '_' . sha1($name . $time . $ip . $secret);
}
return $hashed;
}
function available_spam_traps() {
$results = array();
$handle = opendir(get_config('docroot') . 'lib/antispam');
......
......@@ -239,6 +239,77 @@ class Pieform {/*{{{*/
throw new PieformException('Forms must have a list of elements');
}
if (isset($this->data['spam'])) {
if (empty($this->data['spam']['secret']) || !isset($this->data['elements']['submit'])) {
// @todo don't rely on submit element
throw new PieformException('Forms with spam config must have a secret and submit element');
}
if (!empty($this->data['jsform'])) {
throw new PieformException('Antispam forms not tested with jsform yet');
}
$this->time = isset($_POST['timestamp']) ? $_POST['timestamp'] : time();
$spamelements1 = array(
'invisiblefield' => array(
'type' => 'text',
'title' => get_string('spamtrap'),
'defaultvalue' => '',
'class' => 'dontshow',
),
);
$spamelements2 = array(
'timestamp' => array(
'type' => 'hidden',
'value' => $this->time,
),
'invisiblesubmit' => array(
'type' => 'submit',
'value' => get_string('spamtrap'),
'class' => 'dontshow',
),
);
$insert = rand(0, count($this->data['elements']));
$this->data['elements'] = array_merge(
array_slice($this->data['elements'], 0, $insert, true),
$spamelements1,
array_slice($this->data['elements'], $insert, count($this->data['elements']) - $insert, true),
$spamelements2
);
// Min & max number of seconds between page load & submission
if (!isset($this->data['spam']['mintime'])) {
$this->data['spam']['mintime'] = 0.01;
}
if (!isset($this->data['spam']['maxtime'])) {
$this->data['spam']['maxtime'] = 86400;
}
if (empty($this->data['spam']['hash'])) {
$this->data['spam']['hash'] = array();
}
$this->data['spam']['hash'][] = 'invisiblefield';
$this->data['spam']['hash'][] = 'invisiblesubmit';
$this->hash_fieldnames();
if (isset($this->data['spam']['reorder'])) {
// Reorder form fields randomly
$order = $this->data['spam']['reorder'];
shuffle($order);
$order = array_combine($this->data['spam']['reorder'], $order);
$temp = array();
foreach (array_keys($this->data['elements']) as $k) {
if (isset($order[$k])) {
$temp[$order[$k]] = $this->data['elements'][$order[$k]];
}
else {
$temp[$k] = $this->data['elements'][$k];
}
}
$this->data['elements'] = $temp;
}
$this->spamerror = false;
}
// Get references to all the elements in the form, excluding fieldsets
foreach ($this->data['elements'] as $name => &$element) {
// The name can be in the element itself. This is compatibility for
......@@ -267,7 +338,8 @@ class Pieform {/*{{{*/
$this->elementrefs[$name] = &$element;
}
$element['name'] = $name;
$element['name'] = isset($this->hashedfields[$name]) ? $this->hashedfields[$name] : $name;
}
unset($element);
......@@ -412,9 +484,9 @@ class Pieform {/*{{{*/
// Submit the form if things went OK
if ($this->data['submit'] && !$this->has_errors()) {
$submitted = false;
foreach ($this->elementrefs as $element) {
foreach ($this->elementrefs as $name => $element) {
if (!empty($element['submitelement']) && isset($global[$element['name']])) {
$function = "{$this->data['successcallback']}_{$element['name']}";
$function = "{$this->data['successcallback']}_{$name}";
if (function_exists($function)) {
$function($this, $values);
$submitted = true;
......@@ -857,7 +929,7 @@ EOF;
* @throws PieformException If the element could not be found
*/
public function set_error($name, $message) {/*{{{*/
foreach ($this->data['elements'] as &$element) {
foreach ($this->data['elements'] as $key => &$element) {
if ($element['type'] == 'fieldset') {
foreach ($element['elements'] as &$subelement) {
if ($subelement['name'] == $name) {
......@@ -867,7 +939,7 @@ EOF;
}
}
else {
if ($element['name'] == $name) {
if ($key == $name) {
$element['error'] = $message;
return;
}
......@@ -1143,7 +1215,7 @@ EOF;
private function get_submitted_values() {/*{{{*/
$result = array();
$global = ($this->data['method'] == 'get') ? $_GET : $_POST;
foreach ($this->elementrefs as $element) {
foreach ($this->elementrefs as $name => $element) {
if ($element['type'] != 'markup') {
if (
(empty($element['submitelement']) && empty($element['cancelelement'])) ||
......@@ -1152,7 +1224,7 @@ EOF;
&& isset($global[$element['name']])
)
) {
$result[$element['name']] = $this->get_value($element);
$result[$name] = $this->get_value($element);
}
}
}
......@@ -1179,10 +1251,10 @@ EOF;
}
// Perform rule validation
foreach ($this->elementrefs as $element) {
foreach ($this->elementrefs as $name => $element) {
if (isset($element['rules']) && is_array($element['rules'])) {
foreach ($element['rules'] as $rule => $data) {
if (!$this->get_error($element['name'])) {
if (!$this->get_error($name)) {
// See if this element has a function that describes
// how this rule should apply to it
$function = 'pieform_element_' . $element['type'] . '_rule_' . $rule;
......@@ -1196,14 +1268,30 @@ EOF;
}
}
}
if ($error = $function($this, $values[$element['name']], $element, $data)) {
$this->set_error($element['name'], $error);
if ($error = $function($this, $values[$name], $element, $data)) {
$this->set_error($name, $error);
}
}
}
}
}
if (isset($this->data['spam'])) {
// make sure the user waited long enough but not too long before submitting the form
$elapsed = time() - $values['timestamp'];
if ($elapsed < $this->data['spam']['mintime'] || $elapsed > $this->data['spam']['maxtime']) {
$this->spamerror = true;
}
// make sure the real submit button was used. If it wasn't, it won't exist.
else if (!isset($values['submit']) || isset($values['invisiblesubmit'])) {
$this->spamerror = true;
}
// make sure the invisible field is empty
else if (!isset($values['invisiblefield']) || $values['invisiblefield'] != '') {
$this->spamerror = true;
}
}
// Then user specific validation if a function is available for that
$function = $this->data['validatecallback'];
if (is_callable($function)) {
......@@ -1424,6 +1512,31 @@ EOF;
);
}/*}}}*/
private function hash_fieldnames() {/*{{{*/
// Mess with field names to make it harder for bots to fill in the form
$ip = self::get_ip();
$secret = $this->data['spam']['secret'];
$this->hashedfields = array();
foreach ($this->data['spam']['hash'] as $name) {
// prefix the hash with an underscore to ensure it is always a valid pieforms element name
$this->hashedfields[$name] = '_' . sha1($name . $this->time . $ip . $secret);
}
}/*}}}*/
private static function get_ip() {/*{{{*/
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $_SERVER['REMOTE_ADDR'];
}/*}}}*/
public function spam_error() {/*{{{*/
return $this->spamerror;
}/*}}}*/
}/*}}}*/
......
......@@ -161,22 +161,9 @@ if (isset($key)) {
// Default page - show the registration form
// we're in the middle of processing the form, so read the time
// from the form rather than getting a new one
if ($_POST) {
$time = $_POST['timestamp'];
}
else {
$time = time();
}
$fields = array('firstname', 'lastname', 'email', 'institution', 'tandc', 'submit', 'invisiblefield', 'invisiblesubmit');
$hashed_fields = hash_fieldnames($fields, $time);
$elements = array(
'firstname' => array(
'type' => 'text',
'name' => $hashed_fields['firstname'],
'title' => get_string('firstname'),
'rules' => array(
'required' => true
......@@ -184,7 +171,6 @@ $elements = array(
),
'lastname' => array(
'type' => 'text',
'name' => $hashed_fields['lastname'],
'title' => get_string('lastname'),
'rules' => array(
'required' => true
......@@ -192,7 +178,6 @@ $elements = array(
),
'email' => array(
'type' => 'text',
'name' => $hashed_fields['email'],
'title' => get_string('emailaddress'),
'rules' => array(
'required' => true,
......@@ -219,7 +204,6 @@ if (count($institutions) > 1) {
natcasesort($options);
$elements['institution'] = array(
'type' => 'select',
'name' => $hashed_fields['institution'],
'title' => get_string('institution'),
'options' => $options,
'rules' => array(
......@@ -233,7 +217,6 @@ else if ($institutions) { // Only one option - probably mahara ('No Institution'
$elements['institution'] = array(
'type' => 'hidden',
'name' => $hashed_fields['institution'],
'value' => $institution->name
);
}
......@@ -250,7 +233,6 @@ $elements['tandctext'] = array(
);
$elements['tandc'] = array(
'type' => 'radio',
'name' => $hashed_fields['tandc'],
'title' => get_string('iagreetothetermsandconditions', 'auth.internal'),
'options' => array(
'yes' => get_string('yes'),
......@@ -262,35 +244,17 @@ $elements['tandc'] = array(
),
'separator' => ' &nbsp; '
);
$elements['invisiblefield'] = array(
'type' => 'text',
'name' => $hashed_fields['invisiblefield'],
'title' => get_string('spamtrap'),
'defaultvalue' => '',
'class' => 'dontshow',
);
$elements['timestamp'] = array(
'type' => 'hidden',
'value' => $time,
);
$elements['invisiblesubmit'] = array(
'type' => 'submit',
'name' => $hashed_fields['invisiblesubmit'],
'value' => get_string('spamtrap'),
'class' => 'dontshow',
);
$elements['submit'] = array(
'type' => 'submit',
'name' => $hashed_fields['submit'],
'value' => get_string('register'),
);
// swap the name and email fields at random
if (rand(0,1)) {
$firstname = array_shift($elements);
$lastname = array_shift($elements);
$email = array_shift($elements);
array_unshift($elements, $email, $firstname, $lastname);
$emailelement = $elements['email'];
unset($elements['email']);
$elements = array('email' => $emailelement) + $elements;
}
$form = array(
......@@ -301,7 +265,12 @@ $form = array(
'action' => '',
'showdescriptiononerror' => false,
'renderer' => 'table',
'elements' => $elements
'elements' => $elements,
'spam' => array(
'secret' => get_config('formsecret'),
'mintime' => 5,
'hash' => array('firstname', 'lastname', 'email', 'institution', 'tandc', 'submit'),
),
);
/**
......@@ -312,82 +281,55 @@ $form = array(
*/
function register_validate(Pieform $form, $values) {
global $SESSION;
$error = false;
$currenttime = time();
// read the timestamp field
$timestamp = $values['timestamp'];
// recompute the field names
$fields = array('firstname', 'lastname', 'email', 'institution', 'tandc', 'submit', 'invisiblefield', 'invisiblesubmit');
$hashed = hash_fieldnames($fields, $timestamp);
// make sure the submission is less than a day, and more than 5 seconds old
if ($currenttime - $timestamp < 5 || $currenttime - $timestamp > 86400) {
$error = true;
}
// make sure the real submit button was used. If it wasn't, it won't exist.
elseif (!isset($values[$hashed['submit']]) || isset($values[$hashed['invisiblesubmit']])) {
$error = true;
}
// make sure the invisible field is empty
elseif (!isset($values[$hashed['invisiblefield']]) || $values[$hashed['invisiblefield']] != '') {
$error = true;
}
// make sure all the other data fields exist
elseif (!(isset($values[$hashed['firstname']]) && isset($values[$hashed['lastname']]) &&
isset($values[$hashed['email']]) && isset($values[$hashed['tandc']]) &&
isset($values[$hashed['institution']]))) {
$error = true;
}
else {
$spamtrap = new_spam_trap(array(
array(
'type' => 'name',
'value' => $values[$hashed['firstname']],
),
array(
'type' => 'name',
'value' => $values[$hashed['lastname']],
),
array(
'type' => 'email',
'value' => $values[$hashed['email']],
),
));
if ($spamtrap->is_spam()) {
$error = true;
}
}
if ($error) {
$spamtrap = new_spam_trap(array(
array(
'type' => 'name',
'value' => $values['firstname'],
),
array(
'type' => 'name',
'value' =>