You are here

botcha_recipebook.controller.inc in BOTCHA Spam Prevention 6.2

Same filename and directory in other branches
  1. 7.2 controller/botcha_recipebook.controller.inc

Controller layer of the BotchaRecipebook objects.

File

controller/botcha_recipebook.controller.inc
View 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/user/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 = array_keys(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 = array_keys(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_form_values_log($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_form_values_log($_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_form_values_log($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_form_values_log($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_form_values_log($_GET), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'SERVER=<pre>' . print_r($_SERVER, 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . ' values=<pre>' . print_r(_botcha_filter_form_values_log($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 Add last recipe name.

        //'last_recipe_name' => $recipe->name,

        // !!~ @todo 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($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;

    // We are going to store changes of the recipes states.
    $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) {

    // @todo Abstract it.

    //$form_state['no_cache'] = TRUE;
    $form += array(
      '#input' => TRUE,
    );

    // '#input'=1 hacks FAPI to call #process handler on the form
    $form['#process'][] = 'botcha_fprocess';
    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_form_values_log($_POST), 1) . '</pre>' : '') . (BOTCHA_LOGLEVEL >= 5 ? '<br /><br />' . 'GET=<pre>' . print_r(_botcha_filter_form_values_log($_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);

    // We are going to store any changes of the recipes states.
    $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'),
        // @todo Abstract it.
        //'#markup' => t('Form is protected by "@recipebook" recipe book (!edit, !delete)', array(
        '#value' => t('Form is protected by "@recipebook" recipe book (!edit, !delete)', array(
          '@recipebook' => $recipebook->id,
          '!edit' => l(t('edit'), Botcha::BOTCHA_ADMIN_PATH . "/form/$form_id", array('query' => drupal_get_destination(), 'html' => TRUE)),
          '!delete' => l(t('delete'), Botcha::BOTCHA_ADMIN_PATH . "/form/$form_id/delete", 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 {
  public function __construct($id = NULL) {
    $this->id = 'none';
    $this->title = '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'))) {
        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'), Botcha::BOTCHA_ADMIN_PATH . "/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

Namesort descending 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.