botcha_recipebook.controller.inc in BOTCHA Spam Prevention 6.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/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
| 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. | 
