View source
<?php
interface IBotchaRecipeController {
public function getRecipe($id, $create = TRUE);
public function getRecipes($reset = FALSE);
public function save($recipe);
public function delete($recipe);
}
class BotchaRecipeController extends Controller implements IBotchaRecipeController {
protected $app_name = 'Botcha';
protected $controller_type = Botcha::CONTROLLER_TYPE_RECIPE;
protected function getModel() {
return parent::getModel();
}
public function getRecipe($id, $create = TRUE) {
$r = $this
->getModel()
->getRecipe($id);
$classname = $r->classname;
$recipe = new $classname($id);
$recipe
->setTitle($r->title)
->setDescription($r->description);
return $recipe;
}
public function getRecipes($reset = FALSE) {
$rs = array_keys($this
->getModel()
->getRecipes());
return $rs;
}
public function save($recipe) {
$this
->getModel()
->save($recipe);
return $this
->getRecipe($recipe->id, FALSE);
}
public function delete($recipe) {
$this
->getModel()
->delete($recipe);
}
}
abstract class BotchaRecipe {
public $id;
protected $description;
protected $settings = array();
protected $secret;
protected $method;
protected $css;
protected $js;
public $error_field;
public $error_text;
protected $recipebooks = array();
public function setRecipebook($rbid) {
$this->recipebooks[$rbid] = $rbid;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setSecret($secret) {
$this->secret = $secret;
return $this;
}
public function getSecret($build_id = NULL) {
if (empty($this->secret) || !empty($build_id)) {
$this->secret = md5($build_id . BOTCHA_SECRET);
}
return $this->secret;
}
public function setMethod($method) {
$this->method = $method;
return $this;
}
public function getMethod() {
return $this->method;
}
public function getSetting($key, $default = NULL) {
return !empty($this->settings[$key]) ? $this->settings[$key] : $default;
}
public function setSetting($key, $value) {
$this->settings[$key] = $value;
return $this;
}
public function __construct($id) {
$this->id = $id;
$this
->getInfo();
}
public function getInfo() {
$this->error_field = 'mail';
$this->error_text = t('You must be a human, not a spam bot, to submit forms on this website.') . ' ' . t('If you insist that you are a human, please try again.') . ' ' . t('If error persists, contact webmaster using contact link at the bottom of this page and give all the details of this error (your browser, version, OS).');
}
public function getDefaultSettings() {
return array(
'fields' => $this
->getFields(),
'css' => $this
->getCss(),
'js' => $this
->getJs(),
);
}
protected function getProperty(&$value, $getter_callback, $parameters = NULL) {
if (empty($value)) {
$value = $this
->{$getter_callback}($parameters);
}
return $value;
}
public function apply(&$form, &$form_state) {
$this
->prepare($form, $form_state);
$form_elements = $this
->generateFormElements();
foreach ($form_elements as $field_name => $field_properties) {
unset($field_properties['!valid_token']);
$form[$field_name] = $field_properties;
if ($this->method == 'build_id_submit') {
if (isset($_POST[$field_name])) {
$form_state['botcha_submit_values'][$field_name] = $_POST[$field_name];
}
if (isset($field_properties['#default_value'])) {
$form[$field_name]['#value'] = $field_properties['#default_value'];
$form_state['post'][$field_name] = $field_properties['#default_value'];
$_POST[$field_name] = $field_properties['#default_value'];
}
}
else {
}
}
}
protected function prepare($form, $form_state) {
if (!empty($_POST['form_build_id'])) {
$build_id = $_POST['form_build_id'];
$method = 'build_id_submit';
}
else {
$build_id = $form['#build_id'];
$method = 'build_id';
}
$secret = $this
->getSecret($build_id);
$this
->setSecret($secret)
->setMethod($method);
}
public function isSpam($form, $form_state) {
$this
->prepare($form, $form_state);
return FALSE;
}
public function handle($result, $form, $form_state) {
$this
->prepare($form, $form_state);
switch ($result) {
case 'success':
break;
case 'spam':
default:
form_set_error($this->error_field, $this->error_text);
break;
}
}
protected function getSeed() {
return md5(get_class($this) . substr($this->secret, 0, -4));
}
protected function getFields() {
$fields_count = $this
->getFieldCount();
$fields = array();
for ($i = 0; $i < $fields_count; $i++) {
$fields[$i] = $this
->getField($i);
}
return $fields;
}
protected function getField($delta) {
return array(
'name' => $this
->getProperty($this->settings['fields'][$delta]['name'], 'getFieldName', $delta),
'class' => $this
->getProperty($this->settings['fields'][$delta]['class'], 'getFieldClass', $delta),
'prefix' => $this
->getProperty($this->settings['fields'][$delta]['prefix'], 'getFieldPrefix', $delta),
);
}
public function getCss() {
}
public function getJs() {
}
protected function getFieldName($delta) {
return substr($this
->getProperty($this->seed, 'getSeed'), 0, 3) . '_name';
}
protected function getFieldClass($delta) {
return 'a' . substr($this
->getProperty($this->seed, 'getSeed'), 1, 4) . '_field';
}
protected function getFieldPrefix($delta) {
return substr($this
->getProperty($this->seed, 'getSeed'), 10, mt_rand(3, 6));
}
public function generateFormElements() {
$css = $this
->getProperty($this->settings['css'], 'getCss');
if (!empty($css)) {
drupal_add_css("{$this->settings['css']}", array(
'type' => 'inline',
));
}
return array();
}
}
class BotchaRecipeNoResubmit extends BotchaRecipe {
public function getInfo() {
parent::getInfo();
$this->description = t('Prevent form resubmission.' . ' Bots will try to resubmit old form prepared.' . ' Form is remembered, and only one submission is allowed.');
$this->error_text .= '<br />' . t('Form session reuse detected.') . ' ' . t('An old form was submitted again, which may happen' . ' if it was retrieved from browser history using "Back" button.') . '<br />' . t('Please try again - fill all entries on this page' . ' without going "Back".');
}
public function isSpam($form, $form_state) {
$isSpam = parent::isSpam($form, $form_state);
$build_id = isset($_POST['form_build_id']) ? $_POST['form_build_id'] : $form['#build_id'];
if ($cached = cache_get("botcha_{$build_id}", 'cache_form')) {
$data = $cached->data;
if (!isset($data['#cache_token']) || $data['#cache_token'] != $this
->getToken()) {
$isSpam = TRUE;
}
}
return $isSpam;
}
protected function getToken($value = '') {
if (empty($_SESSION['botcha_session'])) {
$_SESSION['botcha_session'] = session_id();
}
return drupal_hmac_base64($value, $_SESSION['botcha_session'] . drupal_get_private_key() . drupal_get_hash_salt());
}
public function apply(&$form, &$form_state) {
parent::apply($form, $form_state);
$build_id = $form['#build_id'];
$build_id_submit = isset($_POST['form_build_id']) ? $_POST['form_build_id'] : FALSE;
if ($build_id_submit != $build_id) {
$form_state['post']['form_build_id'] = $build_id;
}
$expire = variable_get('botcha_cache_expiration_timeout', 21600);
$data = array();
$data['#cache_token'] = $this
->getToken();
cache_set('botcha_' . $build_id, $data, 'cache_form', REQUEST_TIME + $expire);
}
public function handle($mode, $form, $form_state) {
parent::handle($mode, $form, $form_state);
$build_id = isset($_POST['form_build_id']) ? $_POST['form_build_id'] : $form['#build_id'];
$expire = 0;
$data = array();
cache_set('botcha_' . $build_id, $data, 'cache_form', REQUEST_TIME + $expire);
}
}
class BotchaRecipeUsingJsAbstract extends BotchaRecipe {
public function getInfo() {
parent::getInfo();
$this->error_text .= '<br />' . t('Please enable Javascript to use this form.');
}
protected function getFieldCount() {
return 1;
}
public function generateFormElements() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$form_elements = array(
$fields[0]['name'] => array(
'#type' => 'textfield',
'#title' => t('Enter your name'),
'#default_value' => $fields[0]['default_value'],
'#description' => t('Your first name.'),
'#prefix' => '<div class="' . $fields[0]['class'] . '">' . '<span class="description"> (' . t('If you\'re a human, don\'t change the following field') . ')</span>',
'#suffix' => '</div>' . '<noscript>' . t('Please enable Javascript to use this form.') . '</noscript>',
'#attributes' => array(
'class' => array(
$fields[0]['class'],
),
'autocomplete' => 'off',
),
'#weight' => -20,
'!valid_token' => $js['secure_token'],
),
);
$js_value = $this
->getProperty($this->settings['js']['value'], 'getJsValue');
if (!empty($js_value)) {
drupal_add_js($js_value, array(
'type' => 'inline',
'preprocess' => FALSE,
));
}
return array_merge(parent::generateFormElements(), $form_elements);
}
protected function getField($delta) {
return array_merge(parent::getField($delta), array(
'default_value' => $this
->getProperty($this->settings['fields'][$delta]['default_value'], 'getFieldDefault', $delta),
));
}
protected function getFieldDefault($delta) {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$field_prefix = $fields[$delta]['prefix'];
$chops_positions = array_keys($js['chops']);
$secure_token = $js['secure_token'];
return $field_prefix . substr($secure_token, $chops_positions[0], $chops_positions[1]);
}
public function getCss() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
return 'div.' . $fields[0]['class'] . ' { display: none; visibility: hidden; }';
}
public function getJs() {
return array(
'name' => $this
->getProperty($this->settings['js']['name'], 'getJsName'),
'pos' => $this
->getProperty($this->settings['js']['pos'], 'getJsPos'),
'match' => $this
->getProperty($this->settings['js']['match'], 'getJsMatch'),
'secure_token' => $this
->getProperty($this->settings['js']['secure_token'], 'getJsSecureToken'),
'chops' => $this
->getProperty($this->settings['js']['chops'], 'getJsChops'),
);
}
protected function getJsName() {
return 'a' . substr($this->secret, 0, 10) . substr($this
->getProperty($this->seed, 'getSeed'), 6, 8);
}
protected function getJsPos() {
return strlen($this
->getProperty($this->settings['fields'][0]['prefix'], 'getFieldPrefix'));
}
protected function getJsMatch() {
$chop_positions = array_keys($this
->getProperty($this->settings['js']['chops'], 'getJsChops'));
return substr($this
->getFieldDefault(0), 0, $this
->getJsPos() + mt_rand(2, $chop_positions[1]));
}
protected function getJsChops() {
$secure_token = $this
->getProperty($this->settings['js']['secure_token'], 'getJsSecureToken');
$js_chops = array();
$chop1 = 2;
$js_chops[$chop1] = substr($secure_token, 0, $chop1);
$chop2 = mt_rand(5, 8);
$js_chops[$chop2] = substr($secure_token, $chop1 + $chop2);
return $js_chops;
}
protected function getJsSecureToken() {
return substr($this
->getProperty($this->seed, 'getSeed'), 4, -2) . '_form';
}
}
class BotchaRecipeHoneypot extends BotchaRecipeUsingJsAbstract {
public function getInfo() {
parent::getInfo();
$this->description = t('Insert JS+CSS+honeypot field.') . ' ' . t('Bots will not run JS or will mess with the field') . ' ' . t('%parts are added to the form.', array(
'%parts' => t('Honeypot field') . ', CSS , JS',
)) . ' ' . t('CSS hides the input field.') . ' ' . t('JS enters key value into the field.');
}
public function getJsValue() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$js_tok1 = reset($js['chops']);
$js_tok2 = next($js['chops']);
return <<<END
(function (\$) {
Drupal.behaviors.{<span class="php-variable">$js</span>[<span class="php-string">'name'</span>]} = {
attach: function (context, settings) {
\$("input.{<span class="php-variable">$fields</span>[<span class="php-constant">0</span>][<span class="php-string">'class'</span>]}").each(function() {
f=\$(this)[0];
if (f.value.indexOf("{<span class="php-variable">$js</span>[<span class="php-string">'match'</span>]}")==0){f.value="{<span class="php-variable">$js_tok1</span>}"+f.value.substring({<span class="php-variable">$js</span>[<span class="php-string">'pos'</span>]})+"{<span class="php-variable">$js_tok2</span>}";}
});
}
};
})(jQuery);
END;
}
public function isSpam($form, $form_state) {
$isSpam = parent::isSpam($form, $form_state);
foreach ($this
->generateFormElements() as $field_name => $form_element) {
if (isset($form_element['!valid_token']) && isset($form_state['botcha_submit_values'][$field_name]) && $form_state['botcha_submit_values'][$field_name] !== $form_element['!valid_token']) {
$isSpam = TRUE;
break;
}
}
return $isSpam;
}
}
class BotchaRecipeHoneypot2 extends BotchaRecipeHoneypot {
protected function getFieldName($delta) {
switch ($delta) {
case 0:
default:
return parent::getFieldName($delta);
break;
}
}
public function generateFormElements() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$chops_positions = array_keys($js['chops']);
$css_tok2 = substr($js['secure_token'], $chops_positions[0] + $chops_positions[1]);
$form_elements = parent::generateFormElements();
$form_elements[$fields[0]['name']]['#attributes']['style'] = "font-family: \"a{$css_tok2}\"";
return $form_elements;
}
public function getJsValue() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$js_tok1 = reset($js['chops']);
$selector = "input.{$fields[0]['class']}";
return <<<END
(function (\$) {
Drupal.behaviors.{<span class="php-variable">$js</span>[<span class="php-string">'name'</span>]} = {
attach: function (context, settings) {
\$("{<span class="php-variable">$selector</span>}").each(function() {
f=\$(this)[0];
tok2 = f.style.fontFamily;
if(tok2.charAt(0) == "'" || tok2.charAt(0) == '"') tok2=tok2.substring(1, tok2.length-1);
tok2=tok2.substring(1, tok2.length);
if (f.value.indexOf("{<span class="php-variable">$js</span>[<span class="php-string">'match'</span>]}")==0){f.value="{<span class="php-variable">$js_tok1</span>}"+f.value.substring({<span class="php-variable">$js</span>[<span class="php-string">'pos'</span>]})+tok2;}
});
}
};
}(jQuery));
END;
}
}
class BotchaRecipeObscureUrl extends BotchaRecipeUsingJsAbstract {
public function getInfo() {
parent::getInfo();
$this->description = t('Insert a new field into form action URL.') . ' ' . t('Bots will not run JS and miss the field.') . ' ' . t('%parts is added to the form.', array(
'%parts' => 'JS',
)) . ' ' . t('JS enters key value into the field.');
}
public function isSpam($form, $form_state) {
$isSpam = parent::isSpam($form, $form_state);
foreach ($this->url_elements as $field => $value) {
$url_field = isset($_GET[$field]) ? $_GET[$field] : FALSE;
unset($_GET[$field]);
if (isset($value['!valid_token']) && $url_field !== $value['!valid_token']) {
$isSpam = TRUE;
break;
}
}
return $isSpam;
}
protected function prepare($form, $form_state) {
parent::prepare($form, $form_state);
$this->settings['form_id'] = $form['form_id']['#value'];
$this->settings['form_action'] = $form['#action'];
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$this->url_elements[$fields[2]['name']] = array(
'#type' => 'textfield',
'#default_value' => '',
'!valid_token' => $js['secure_token'],
);
}
protected function getFieldCount() {
return 3;
}
protected function getFieldName($delta) {
switch ($delta) {
case 2:
return substr($this
->getProperty($this->seed, 'getSeed'), 1, 4) . '_name';
break;
case 1:
return parent::getFieldName($delta);
break;
case 0:
default:
return 'botcha';
break;
}
}
public function getJsValue() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
$js = $this
->getProperty($this->settings['js'], 'getJs');
$chops_positions = array_keys($js['chops']);
$field2_name = $fields[2]['name'];
$form_id = str_replace('_', '-', $this
->getSetting('form_id'));
$submit = _botcha_url($this
->getSetting('form_action'), array(
'query' => array(
$field2_name => '__replace__',
),
));
$submit = preg_replace('/__replace__/', $js['chops'][$chops_positions[0]] . '\'+v+\'' . $js['chops'][$chops_positions[1]], $submit);
if (strpos($form_id, '-node-form') !== false) {
$js_form_id = 'node-form';
}
elseif (substr($form_id, 0, 7) == 'comment') {
$js_form_id = 'comment-form';
}
else {
$js_form_id = $form_id;
}
return <<<END
(function (\$) {
Drupal.behaviors.{<span class="php-variable">$js</span>[<span class="php-string">'name'</span>]} = {
attach: function (context, settings) {
\$("input.{<span class="php-variable">$fields</span>[<span class="php-constant">1</span>][<span class="php-string">'class'</span>]}").each(function() {
f=\$(this)[0];
if (f.value.indexOf("{<span class="php-variable">$js</span>[<span class="php-string">'match'</span>]}")==0){
v=f.value.substring({<span class="php-variable">$js</span>[<span class="php-string">'pos'</span>]});
\$("#{<span class="php-variable">$js_form_id</span>}").get(0).setAttribute('action', '{<span class="php-variable">$submit</span>}');
}
});
}
};
}(jQuery));
END;
}
public function generateFormElements() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
return array_merge(parent::generateFormElements(), array(
$fields[1]['name'] => array(
'#type' => 'hidden',
'#default_value' => $fields[1]['default_value'],
'#attributes' => array(
'class' => array(
$fields[1]['class'],
),
),
'#weight' => 20,
),
));
}
}
class BotchaRecipeTimegate extends BotchaRecipe {
protected $time;
public function getInfo() {
parent::getInfo();
$this->description = t('Check time spended for submitting a form.') . ' ' . t('Bots submit form too fast.') . ' ' . t('Form is marked with timestamp which is checked during submit.');
$this->error_text .= '<br />' . t('Form is submitted too fast.') . '<br />' . t('Please spend more time filling in the form.');
}
protected function getFieldCount() {
return 1;
}
protected function getFieldName($delta) {
switch ($delta) {
case 0:
default:
return 'timegate';
break;
}
}
public function generateFormElements() {
$fields = $this
->getProperty($this->settings['fields'], 'getFields');
return array_merge(parent::generateFormElements(), array(
$fields[0]['name'] => array(
'#type' => 'hidden',
'#title' => 'Timegate',
'#weight' => 5,
'#required' => FALSE,
'#default_value' => time(),
),
));
}
public function isSpam($form, $form_state) {
$isSpam = parent::isSpam($form, $form_state);
$absence = empty($form['timegate']);
$minimal_delay = variable_get('botcha_timegate', 5);
$form_generated = !empty($form_state['botcha_submit_values']['timegate']) ? $form_state['botcha_submit_values']['timegate'] : NULL;
$form_submitted = !empty($form_state['values']['timegate']) ? $form_state['values']['timegate'] : NULL;
if ($absence || (int) $form_submitted < (int) $form_generated + (int) $minimal_delay) {
$isSpam = TRUE;
}
return $isSpam;
}
}