botcha_recipebook.controller.inc in BOTCHA Spam Prevention 7.2
Same filename and directory in other branches
Controller layer of the BotchaRecipebook objects.
File
controller/botcha_recipebook.controller.incView source
<?php
/**
* @file
* Controller layer of the BotchaRecipebook objects.
*/
class BotchaRecipebookAbstract {
/**
* Identifier of the recipe book.
*/
public $id;
/**
* A title of the book.
*/
public $title;
/**
* Description of the recipe book.
*/
public $description;
/**
* List of recipes.
* @var BotchaRecipe
*/
protected $recipes = array();
/**
* List of forms.
* @var BotchaForm
*/
protected $forms = array();
public static function getRecipebook($id, $create = TRUE) {
$none = TRUE;
if ($id != 'none') {
$rb = BotchaRecipebookModel::getRecipebook($id);
if ($rb || $create) {
$none = FALSE;
}
}
if ($none) {
$recipebook = new BotchaRecipebookNone($id);
}
else {
$recipebook = new BotchaRecipebook($id);
if ($rb) {
$recipebook
->setTitle($rb->title)
->setDescription($rb->description);
}
}
return $recipebook;
}
protected function __construct($id) {
$this->id = $id;
}
public function isApplicable($form, $form_state) {
$form_id = $form['form_id']['#value'];
$isApplicable = FALSE;
if (!user_access('skip BOTCHA')) {
$isApplicable = TRUE;
}
switch ($form_id) {
case 'user_register':
// Only change the registration form. There is also 'user_register' form
// at /admin/config/people/user/create path, but we leave it alone.
if (FALSE === strpos($form['#action'], 'user/register')) {
if (!variable_get('botcha_allow_on_admin_pages', FALSE)) {
$isApplicable = FALSE;
}
}
break;
}
return $isApplicable;
}
/* @todo Remove it.
protected function addAdminLinks(&$form) {
$form_id = $form['form_id']['#value'];
if (variable_get('botcha_administration_mode', FALSE)
&& user_access('administer BOTCHA settings')
&& (arg(0) != 'admin'
|| variable_get('botcha_allow_on_admin_pages', FALSE)
|| ($form_id == 'user_register'))) {
// Add BOTCHA administration tools.
$botcha_element = $this->createAdminLinksFieldset($form_id);
// Get placement in form and insert in form.
// @todo BotchaRecipebook isApplicable Make away with a dependency from botcha.inc.
$botcha_placement = _botcha_get_botcha_placement($form_id, $form);
_botcha_insert_botcha_element($form, $botcha_placement, $botcha_element);
}
}
protected function createAdminLinksFieldset($form_id) {
// For administrators: show BOTCHA info and offer link to configure it.
return array(
'#type' => 'fieldset',
'#title' => t('BOTCHA'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
// @todo Abstract it.
'#attributes' => array('class' => array('botcha-admin-links')),
//'#attributes' => array('class' => 'botcha-admin-links'),
);
}
*
*/
public function save() {
// Clean session to fetch new values.
Botcha::clean();
}
public function delete() {
Botcha::unsetRecipebook($this);
// Delete recipe book from DB.
BotchaRecipebookModel::delete($this);
// Clean session to fetch new values.
Botcha::clean();
}
function setTitle($title) {
$this->title = $title;
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
function getTitle() {
return $this->title;
}
function setDescription($description) {
$this->description = $description;
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
function getDescription() {
return $this->description;
}
function setRecipe($recipe_id) {
$this->recipes[$recipe_id] = $recipe_id;
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
function unsetRecipe($recipe_id) {
unset($this->recipes[$recipe_id]);
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
/**
* @todo BotchaRecipebook getRecipes Description.
* @return array
*/
function getRecipes() {
if (!isset($this->recipes)) {
$rs = BotchaModel::getRecipebooksRecipes(array(
'mode' => 'recipe',
'recipebooks' => $this->id,
));
foreach ($rs as $recipe_id) {
$this
->setRecipe($recipe_id);
}
}
$recipes = array();
foreach ($this->recipes as $recipe_id) {
$recipes[$recipe_id] = Botcha::getRecipe($recipe_id, FALSE);
}
return $recipes;
}
function setForm($form_id) {
$this->forms[$form_id] = $form_id;
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
function unsetForm($form_id) {
unset($this->forms[$form_id]);
// Save changed state.
Botcha::setRecipebook($this);
return $this;
}
/**
* @todo BotchaRecipebook getForms Description.
* @return BotchaForm
*/
function getForms() {
if (!isset($this->forms)) {
$fs = BotchaModel::getRecipebooksForms(array(
'mode' => 'form',
'recipebooks' => $this->id,
));
foreach ($fs as $form_id) {
$this
->setForm($form_id);
}
}
$forms = array();
foreach ($this->forms as $form_id) {
$forms[$form_id] = Botcha::getForm($form_id, FALSE);
}
return $forms;
}
/**
* Get the list of recipes by status of spam checking.
*
* @param string $status
* @return array
*/
function getRecipesByStatus($status) {
$recipes_list = array();
foreach ($this
->getRecipes() as $recipe_id => $recipe) {
if ($recipe
->getStatus() == $status) {
$recipes_list[$recipe_id] = $recipe;
}
}
return $recipes_list;
}
/**
* Handle form depending on the result of spam check.
*
* @param string $result
* This parameter is string and not boolean to have a chance to easily implement
* new results of spam check (such as 'postponed', 'suspected' or other).
* @param array $form
* @param array $form_state
*/
function handle($result, $form, $form_state) {
$recipes_success = $this
->getRecipesByStatus('success');
$recipes_success_count = count($recipes_success);
$recipes_spam = $this
->getRecipesByStatus('spam');
$recipes_spam_count = count($recipes_spam);
// !!~ @todo Recipebook handle Reduce code duplication.
switch ($result) {
case 'success':
variable_set('botcha_form_passed_counter', $recipes_success_count);
// Show good submissions in log.
if (BOTCHA_LOGLEVEL >= 3) {
watchdog(BOTCHA_LOG, '%form_id post approved by BOTCHA.!more', array(
'%form_id' => $form['form_id']['#value'],
'!more' => '' . (BOTCHA_LOGLEVEL >= 3 ? ' Checked ' . count($this->recipes) . ' botchas (' . join(', ', array_keys($this->recipes)) . ').' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'POST=<pre>' . print_r(_botcha_filter_value($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_value($_GET), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'SERVER=<pre>' . print_r($_SERVER, 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . ' form=<pre>' . print_r(_botcha_filter_form_log($form), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . ' values=<pre>' . print_r(_botcha_filter_value($form_state['values']), 1) . '</pre>' : ''),
), WATCHDOG_INFO);
}
$rules_event_name = 'botcha_form_approved';
break;
case 'spam':
default:
variable_set('botcha_form_blocked_counter', $recipes_spam_count);
// Prepare a list of failed recipes.
foreach ($recipes_spam as $recipe_spam) {
$recipe_spam_ids[$recipe_spam->id] = $recipe_spam->id;
}
// Just using the first failed recipe to reject form submission.
$recipe_spam = reset($recipes_spam);
form_set_error($recipe_spam->error_field, $recipe_spam->error_text);
// Show blocked submissions in log.
if (BOTCHA_LOGLEVEL >= 1) {
watchdog(BOTCHA_LOG, '%form_id post blocked by BOTCHA: submission looks like from a spambot.!more', array(
'%form_id' => $form['form_id']['#value'],
'!more' => '' . (BOTCHA_LOGLEVEL >= 2 ? '<br /><br />' . ' Failed ' . $recipes_spam_count . ' of ' . count($this->recipes) . ' recipes [' . implode(', ', $recipe_spam_ids) . '] from "' . $this->id . '" recipe book.' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'POST=<pre>' . print_r(_botcha_filter_value($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_value($_GET), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'SERVER=<pre>' . print_r($_SERVER, 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . ' form=<pre>' . print_r(_botcha_filter_form_log($form), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . ' values=<pre>' . print_r(_botcha_filter_value($form_state['values']), 1) . '</pre>' : ''),
), WATCHDOG_WARNING);
}
$rules_event_name = 'botcha_form_rejected';
break;
}
// Invoke rules event.
if (module_exists('rules')) {
$arguments = array(
// 'form' => &$form,
// 'form_state' => &$form_state,
'form_id' => $form['form_id']['#value'],
'total_recipes' => count($this->recipes),
'passed_recipes' => $recipes_success_count,
'passed_recipes_names' => join(', ', array_keys($recipes_success)),
// !!~ @todo handle Add last recipe name.
//'last_recipe_name' => $recipe->name,
// !!~ @todo Recipebook handle Add a reason of fail to rules event invokation.
//'fail' => $fail,
'fail' => 'FAIL',
'failed_field' => 'mail',
);
// !!? Do we need per recipe rules event invoking?
rules_invoke_event_by_args($rules_event_name, $arguments);
}
// Clean $_SESSION.
Botcha::clean();
}
protected function getRecipeSecret($value) {
return md5($value . BOTCHA_SECRET);
}
/**
* Spam check.
* Currently the logic is as follows: if we could find a recipe that failed
* spam check - then we consider this form submission as spam and decline it.
*
* @param array $form
* @param array $form_state
* @return boolean
*/
function isSpam($form, $form_state) {
$isSpam = FALSE;
$recipes = $this
->getRecipes();
foreach ($recipes as $recipe) {
$recipe
->setStatus($recipe
->isSpam($form, $form_state) ? 'spam' : 'success');
// Do per recipe handling right here. Global handling will be done later.
$recipe
->handle($recipe
->getStatus(), $form, $form_state);
// One is enough to block the form.
$isSpam = $isSpam || $recipe
->getStatus() == 'spam';
}
return $isSpam;
}
protected function getCsss() {
$csss = array();
foreach ($this
->getRecipes() as $recipe) {
if ($css = $recipe
->getCss()) {
$csss[] = $css;
}
}
return $csss;
}
protected function getJss() {
$jss = array();
foreach ($this
->getRecipes() as $recipe) {
if ($recipe instanceof BotchaRecipeUsingJsAbstract) {
$jss[] = $recipe
->getJsValue();
}
}
return $jss;
}
function apply(&$form, &$form_state) {
$form_state['no_cache'] = TRUE;
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';
}
$this
->applyForBuildId($form, $form_state, $method, $build_id);
// User_login forms open session in validate hooks instead of submit
// we should be the first to validate - add our hook to the beginning
if (is_array($form['#validate'])) {
// Workaround since array_unshift'ing by reference was deprecated.
// @see http://www.php.net/manual/ru/function.array-unshift.php#40270
array_unshift($form['#validate'], '');
$form['#validate'][0] = '_botcha_form_validate';
}
else {
$form['#validate'] = array(
'_botcha_form_validate',
);
}
$form_state['#botcha'] = $this->id;
// Logging.
$csss = $this
->getCsss();
$jss = $this
->getJss();
if (BOTCHA_LOGLEVEL >= 4) {
watchdog(BOTCHA_LOG, '%form_id form prepared by BOTCHA: added recipes - !botchas!more', array(
'%form_id' => $form['form_id']['#value'],
'!botchas' => join(', ', $this->recipes),
'!more' => '' . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'POST=<pre>' . print_r(_botcha_filter_value($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_value($_GET), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'SERVER=<pre>' . print_r($_SERVER, 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'form=<pre>' . print_r(_botcha_filter_form_log($form), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 && count($jss) ? '<br /><br />' . 'JS=<pre>' . join("\n", $jss) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 && count($csss) ? '<br /><br />' . 'CSS=<pre>' . join("\n", $csss) . '</pre>' : ''),
), WATCHDOG_NOTICE);
}
}
protected function applyForBuildId(&$form, &$form_state, $method, $build_id) {
$secret = $this
->getRecipeSecret($build_id);
$recipes = $this
->getRecipes();
foreach ($recipes as $recipe) {
// Set necessary parameters.
$recipe
->setSecret($secret)
->setMethod($method);
// Apply recipe to the concrete form.
$recipe
->applyRecipe($form, $form_state);
}
}
}
class BotchaRecipebook extends BotchaRecipebookAbstract {
/* @todo Remove it.
protected function createAdminLinksFieldset($form_id) {
$botcha_element = parent::createAdminLinksFieldset($form_id);
$recipebook = Botcha::getForm($form_id, FALSE)->getRecipebook();
$botcha_element['#title'] = t('BOTCHA: protection enabled (@recipebook recipe book)', array('@recipebook' => $recipebook->id));
$botcha_element['#description'] = t('Untrusted users will have form %form_id protected by BOTCHA (!recipebook_settings, !general_settings).',
array(
'%form_id' => $form_id,
'!recipebook_settings' => l(t('Recipe book settings'), Botcha::BOTCHA_ADMIN_PATH . "/recipebook/{$recipebook->id}"),
'!general_settings' => l(t('General BOTCHA settings'), Botcha::BOTCHA_ADMIN_PATH),
)
);
$botcha_element['protection'] = array(
'#type' => 'item',
'#title' => t('Enabled protection'),
'#markup' => t('Form is protected by "@recipebook" recipe book (!edit, !disable)', array(
//'#value' => t('Form is protected by "@recipebook" recipe book (!edit, !disable)', array(
'@recipebook' => $recipebook->id,
'!edit' => l(t('edit'), Botcha::BOTCHA_ADMIN_PATH . "/form/{$form_id}", array('query' => drupal_get_destination(), 'html' => TRUE)),
'!disable' => l(t('disable'), Botcha::BOTCHA_ADMIN_PATH . "/form/{$form_id}/disable", array('query' => drupal_get_destination(), 'html' => TRUE)),
)),
);
return $botcha_element;
}
*
*/
public function save() {
// Save recipe book to DB.
BotchaRecipebookModel::save($this);
parent::save();
}
}
/**
* Dummy class, created for data consistency and for interface unifying.
* When there is no recipe book binded to form, this class is used as a handler.
* It has no logic at all - by design.
*/
class BotchaRecipebookNone extends BotchaRecipebookAbstract {
// @todo Refactor this since it is duplication.
public function __construct($id = NULL) {
$this->id = !empty($id) ? $id : 'none';
}
/* @todo Remove it.
protected function addAdminLinks(&$form) {
$form_id = $form['form_id']['#value'];
// Apply only to allowed forms.
// @todo Move it to new abstraction: form exceptions.
if (!in_array($form_id, array('update_script_selection_form', 'user_login', 'user_login_block'))) {
parent::addAdminLinks($form);
}
}
protected function createAdminLinksFieldset($form_id) {
$botcha_element = parent::createAdminLinksFieldset($form_id);
$botcha_element['#title'] = t('BOTCHA: no protection enabled');
$botcha_element['add_botcha'] = array(
// @todo Abstract it.
'#markup' => l(t('Add BOTCHA protection on form'), Botcha::BOTCHA_ADMIN_PATH . "/form/add", array('query' => array_merge(drupal_get_destination(), array('botcha_form_id' => $form_id)), 'html' => TRUE)),
//'#value' => l(t('Add BOTCHA protection on form'), "admin/user/botcha/form/add", array('query' => drupal_get_destination() . "&botcha_form_id=$form_id", 'html' => TRUE)),
);
return $botcha_element;
}
*
*/
public function save() {
// @todo ?Refactor this with delete() : unify it.
Botcha::unsetRecipebook($this);
parent::save();
}
}
Classes
Name | Description |
---|---|
BotchaRecipebook | |
BotchaRecipebookAbstract | @file Controller layer of the BotchaRecipebook objects. |
BotchaRecipebookNone | Dummy class, created for data consistency and for interface unifying. When there is no recipe book binded to form, this class is used as a handler. It has no logic at all - by design. |