? .bzr
? captcha-rewrite-diffr1.42-r15.patch
? captcha-rewrite-r14.patch
? captcha-rewrite.patch
Index: captcha.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/captcha/captcha.info,v
retrieving revision 1.3
diff -u -b -B -u -p -r1.3 captcha.info
--- captcha.info	18 Jun 2007 22:53:32 -0000	1.3
+++ captcha.info	21 Jun 2007 23:28:19 -0000
@@ -1,4 +1,5 @@
-; $Id: captcha.info,v 1.3 2007/06/18 22:53:32 dww Exp $
+; $Id$
 name = Captcha
-description = "Implements a captcha to registration, comment, contact and node entry forms."
-package = SPAM Control
+description = "Implements a captcha challenge to, for example, registration, comment and contact forms."
+package = Spam control
+version = "$Name$"
Index: captcha.install
===================================================================
RCS file: captcha.install
diff -N captcha.install
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ captcha.install	21 Jun 2007 23:28:19 -0000
@@ -0,0 +1,50 @@
+<?php
+// $Id$
+
+/**
+ * Create tables on install
+ */
+function captcha_install() {
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+      db_query("CREATE TABLE {captcha_points} (
+        form_id varchar(128) NOT NULL,
+        captcha_type varchar(64) default NULL,
+        PRIMARY KEY (form_id)
+        ) /*!40100 DEFAULT CHARACTER SET utf8 */;"
+      );
+      $succes = TRUE;
+      break;
+    case 'pgsql':
+      db_query("CREATE TABLE {captcha_points} (
+        form_id varchar(128) NOT NULL,
+        captcha_type varchar(64) default NULL,
+        PRIMARY KEY (form_id)
+        );"
+      );
+      $succes = TRUE;
+      break;
+    default:
+      drupal_set_message(t('Unsupported database.'), 'error');
+      $succes = FALSE;
+  }
+  if ($succes) {
+    // insert some defaults
+    $form_ids = array('comment_form', 'contact_mail_user', 'contact_mail_page',
+      'user_register', 'user_pass');
+    foreach($form_ids as $form_id) {
+      db_query("INSERT INTO {captcha_points} (form_id, captcha_type) VALUES ('%s', NULL)", $form_id);
+    }
+    drupal_set_message(t('The installation of the captcha_points table and some default entries was successful'), 'status');
+  } else {
+    drupal_set_message(t('The installation of the captcha module was unsuccessful'), 'error');
+  }
+}
+
+/**
+ * Remove tables on uninstall.
+ */
+function captcha_uninstall() {
+  db_query("DROP TABLE {captcha_points}");
+}
Index: captcha.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/captcha/captcha.module,v
retrieving revision 1.42
diff -u -b -B -u -p -r1.42 captcha.module
--- captcha.module	21 Jun 2007 11:47:48 -0000	1.42
+++ captcha.module	21 Jun 2007 23:28:19 -0000
@@ -1,17 +1,18 @@
 <?php
-// $Id: captcha.module,v 1.42 2007/06/21 11:47:48 wundo Exp $
+// $Id$
+
+define('CAPTCHA_DESCRIPTION', 'This question is used to make sure you are a human visitor and to prevent spam submissions.');
 
 /**
  * Implementation of hook_help(). 
  */
 function captcha_help($section = 'admin/help#captcha') {
-  $output = '';
-
+  // TODO: More help needed here.
   switch ($section) {
   case 'admin/help#captcha':
-    $output .= '<p>'. t('Adds a Captcha to various forms to help prevent spam submissions.') .'</p>';
-    // TODO: More help needed here.
-    break;
+      return '<p>' . t('Adds a Captcha to various forms to help prevent spam submissions.') .'</p>';
+    case 'admin/settings/captcha':
+      return '<p>' . t('Captchas are meant to counter spam submissions. Captchas are extra fields on a form presented to the anonymous visitors. These fields present a challenge, which should be easy for a human to solve, but hard enough to keep automated scripts and bots out.') . '</p>';
   }
   return $output;
 }
@@ -26,136 +27,173 @@ function captcha_menu($may_cache) {
       'path' => 'admin/settings/captcha',
       'title' => t('Captcha'),
       'description' => t('Administer how and where Captchas are used.'),
-      'callback' => 'drupal_get_form',
-      'callback arguments' => array('captcha_admin_settings'),
+      'callback' => 'captcha_admin',
       'access' => user_access('administer site configuration'),
       'type' => MENU_NORMAL_ITEM,
     );
-    $items[] = array(
-      'path' => 'admin/settings/captcha/captcha',
-      'title' => t('Captcha'),
-      'description' => t('Administer how and where Captchas are used.'),
-      'access' => user_access('administer site configuration'),
-      'type' => MENU_DEFAULT_LOCAL_TASK,
-    );
-    $items[] = array(
-      'path' => 'admin/settings/captcha/points',
-      'title' => t('Captcha Points'),
-      'description' => t('Administer in what forms Captchas are applied.'),
-      'callback' => 'drupal_get_form',
-      'callback arguments' => array('captcha_admin_points'),
-      'access' => user_access('administer site configuration'),
-      'type' => MENU_LOCAL_TASK,
-    );
   }
   return $items;
 }
 
-
+/**
+ * Return an array with the available captcha types
+ */
 function _captcha_supported_challenges_types(){
   static $captcha_challenges = NULL;
-  
+  // if cached: return cache
   if($captcha_challenges != NULL) {
   	return $captcha_challenges; 
   }	
-  
   $captcha_challenges['none'] = 'none';
-  
   foreach(module_implements('captcha') as $module) {
     $result = call_user_func_array($module .'_captcha', 'list');
     if (isset($result)) { //&& is_array($result)) {
       foreach($result as $challenge) {
-      	$captcha_challenges[$module.'/'.$challenge] = $module.'/'.$challenge;
+        $captcha_challenges["$module::$challenge"] = "$module::$challenge";
       }
     }
   }
-  
-  
   return $captcha_challenges;
 }
 
-function _captcha_points() {
-  $captcha_points = variable_get('captcha_points',NULL);
-  if($captcha_points == NULL) {
-    $captcha_points = array(
-      'comment_form' => t('Comment form'),
-      'contact_mail_user' => t('User contact form'),
-      'contact_mail_page' => t('Sitewide contact form'),
-    );
-    variable_set('captcha_points',serialize($captcha_points));
+/**
+ * General Captcha settings handler
+ *
+ * If arguments are given: first argument is used as form_id, the second one
+ * is interpreted as action (such as disable, delete and enable) to execute on
+ * the form_id.
+ * Otherwise: returns the general captcha configuration form.
+ */
+function captcha_admin($form_id='', $op='') {
+  // if $form_id and action $op given: do the action
+  if ($form_id) {
+    switch ($op) {
+      case 'disable':
+        // disable the captcha point: set the captcha_type to NULL
+        db_query("UPDATE {captcha_points} SET captcha_type = NULL WHERE form_id = '%s'", $form_id);
+        drupal_set_message(t('Disabled captcha point %form_id.', array('%form_id'=>$form_id)));
+        // TODO: return to referer page?
+        drupal_goto('admin/settings/captcha');
+        break;
+      case 'delete':
+        db_query("DELETE FROM {captcha_points} WHERE form_id = '%s'", $form_id);
+        drupal_set_message(t('Deleted captcha point %form_id.', array('%form_id'=>$form_id)));
+        drupal_goto('admin/settings/captcha');
+        break;
+      case 'enable':
+        db_query("DELETE FROM {captcha_points} WHERE form_id = '%s'", $form_id);
+        db_query("INSERT INTO {captcha_points} (form_id, captcha_type) VALUES ('%s', NULL)", $form_id);
+        drupal_goto("admin/settings/captcha/$form_id");
+        break;
   }
-  else {
-    $captcha_points = unserialize($captcha_points);
   }
-  return $captcha_points;
+  // no $form_id or legal action given: generate general captcha settings form
+  return drupal_get_form('captcha_admin_settings', $form_id);
 }
 
-function captcha_admin_points() {
-  $captcha_points = _captcha_points();
-  $output = '';
-  
-  if(function_exists('form_store_get_all')) {
-    $form = array('#description' => t('Enable/Disable captcha points.'));
-    $known_forms = form_store_get_all();
-    $form['captcha_points']['#tree'] = true;
-    foreach($known_forms as $item) {
-      $form['captcha_points'][$item->form_id] = array(
+/**
+ * Form builder function for the general captcha configuration
+ */
+function captcha_admin_settings($form_id='') {
+  $form['#action'] = url('admin/settings/captcha');
+  // field for the captcha adminstration mode
+  $form['captcha_administration_mode'] = array(
         '#type' => 'checkbox',
-        '#title' => $item->form_id.' - '.$item->description,
-        '#default_value' => isset($captcha_points[$item->form_id]),
+    '#title' => t('Add captcha adminstration links to forms.'),
+    '#default_value' => variable_get('captcha_administration_mode', FALSE),
+    '#description' => t('This option is very helpfull to enable/disable captchas on forms. When enabled, users with the "administer site configuration" permission will see captcha adminstration links on all forms.'),
+  );
+  // field set with form_id -> captcha_type configuration
+  $form["captcha_types"] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Select captcha types'),
+    '#description' => t('Select what kind of captcha challenge you want for each form.'),
+    '#tree' => TRUE,
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+    '#theme' => 'captcha_admin_settings_captcha_points',
+  );
+  // list all possible form_id's
+  $captcha_challenges = _captcha_supported_challenges_types();
+  $result = db_query("SELECT * FROM {captcha_points} ORDER BY form_id");
+  while ($captcha_point = db_fetch_object($result)) {
+    $form['captcha_types'][$captcha_point->form_id]['form_id'] = array(
+      '#value' => $captcha_point->form_id,
+    );
+    // select widget for captcha_type
+    $form['captcha_types'][$captcha_point->form_id]['captcha_type'] = array(
+      '#type' => 'select',
+      '#default_value' => $captcha_point->captcha_type,
+      '#options' => $captcha_challenges,
+    );
+    // if a form_id was given as argument of this form builder, highlight the captcha_type select widget
+    if ($form_id == $captcha_point->form_id) {
+      $form['captcha_types'][$captcha_point->form_id]['captcha_type']['#attributes'] = array('class'=>'error');
+    }
+    // additional operations
+    $form['captcha_types'][$captcha_point->form_id]['operations'] = array(
+      '#value' => implode(", ", array(
+        l(t('delete'), "admin/settings/captcha/{$captcha_point->form_id}/delete"),
+      ))
       );
     }
-    $output = system_settings_form($form);
-  }
-  else {
-    drupal_set_message(t('Unable to find Form Store module, Captcha needs it to allow you to change the capcha points.'),'error');
-  }
-
-  return $output;
+  // field for setting the additional captcha description
+  $form["captcha_description"] = array(
+    '#type' => 'textfield',
+    '#title' => t('Captcha description'),
+    '#description' => t('This description should explain to the anonymous visitor why the captcha needs to be solved.'),
+    '#default_value' => t(variable_get('captcha_description', CAPTCHA_DESCRIPTION)),
+  );
+  // field for captcha persistency
+  $form["captcha_persist"] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Persistent captchas'),
+    '#description' => t('If checked, a captcha will be presented to the user each time (s)he wants to submit concerning form. If not checked, once the user has successfully solved a captcha for a form, (s)he will not be presented a captcha for subsequent instances of that form.'),
+    '#default_value' => variable_get('captcha_persist', FALSE),
+  );
+  // submit button
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+  return $form;
 }
 
-function captcha_admin_points_submit($form_id, $form_values) {
-  if($form_id == 'captcha_admin_points') {
-    $known_forms = form_store_get_all();
-    $captcha_points = array();
-    foreach($form_values['captcha_points'] as $key => $selected) {
-      if($selected)
-        $captcha_points[$key] = $known_forms[$key]->description;
-    }
-    variable_set('captcha_points',serialize($captcha_points));
-    drupal_set_message(t('Your captcha points were sucessful updated, please configure the challenge options in the other tab.'),'status');
+/**
+ * Custom theme function for a table of (form_id -> captcha_type) settings
+ */
+function theme_captcha_admin_settings_captcha_points($form) {
+  foreach (element_children($form) as $key) {
+    $row = array();
+    $row[] = drupal_render($form[$key]['form_id']);
+    $row[] = drupal_render($form[$key]['captcha_type']);
+    $row[] = drupal_render($form[$key]['operations']);
+    $rows[] = $row;
   }
+  $header = array('form_id', t('Captcha type'), t('Operations'));
+  $output = theme('table', $header, $rows);
+  return $output;
 }
 
 /**
- * Helper function generates admin settings page.
+ * Submission function for captcha_admin_settings form
  */
-function captcha_admin_settings() {
-  $captcha_points = _captcha_points();
-  $roles = variable_get('captcha_roles',user_roles());
-  $captcha_challenges = _captcha_supported_challenges_types();
-  
-  foreach($roles as $role) {
-    $varsuffix = strtr($role, ' ', '_') .'_captcha';
-    $form[$varsuffix] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Captcha points for the role @role', array('@role' => $role)),
-      '#description' => t('Select what kind of challenge you want to pose to the user in each captcha point.'),
-      '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
-    );
-    foreach($captcha_points as $captcha_point => $captcha_point_description) {
-      $varname = $captcha_point .'_'. $varsuffix;
-      $form[$varsuffix][$varname] = array(
-        '#type' => 'select',
-        '#title' => t('@point', array('@point' => $captcha_point_description)),
-        '#default_value' => variable_get($varname, 'captcha/Math'),
-        '#options' => $captcha_challenges,
-      );
+function captcha_admin_settings_submit($form_id, $form_values) {
+  if ($form_id == 'captcha_admin_settings') {
+    variable_set('captcha_administration_mode', $form_values['captcha_administration_mode']);
+    foreach($form_values['captcha_types'] as $form_id => $data) {
+      $captcha_type = $data['captcha_type'];
+      if ($captcha_type == 'none') {
+        db_query("UPDATE {captcha_points} SET captcha_type = NULL WHERE form_id = '%s'", $form_id);
+      }
+      else {
+        db_query("UPDATE {captcha_points} SET captcha_type = '%s' WHERE form_id = '%s'", $captcha_type, $form_id);
     }
   }
-  
-  return system_settings_form($form);
+    variable_set('captcha_description', $form_values['captcha_description']);
+    variable_set('captcha_persist', $form_values['captcha_persist']);
+    drupal_set_message(t('Your captcha settings were saved.'), 'status');
+  }
 }
 
 /**
@@ -165,89 +203,115 @@ function captcha_form_alter($form_id, &$
   global $captcha;
   global $user;
 
-  $seed = $form['form_token']['#default_value'];
-  if($_SESSION['captcha'][$seed]['success'] === TRUE)
-    return;
-
-  foreach($user->roles as $role) {	
-  	$candidate_trigger = $form_id .'_'. strtr($role, ' ', '_') .'_captcha';
-  	$captcha_type = variable_get($candidate_trigger,NULL);
-
-  	if($captcha_type != NULL) {
-  	  //Found one valid captcha challenge for this point.
-  	  break;
+  if (user_access('administer site configuration') && variable_get('captcha_administration_mode', FALSE)) {
+    // For administrators: show captcha info and offer link to configure it
+    $captcha_type = db_result(db_query("SELECT captcha_type FROM {captcha_points} WHERE form_id = '%s'", $form_id));
+    if ($captcha_type) {
+      $form['captcha'] = array(
+        '#type' => 'item',
+        '#title' => t('Captcha administration'),
+        '#description' => t('The captcha challenge %captcha_type is enabled here for anonymous visitors: !disable.', array(
+          '%captcha_type' => $captcha_type,
+          '!disable' => l(t('disable captcha'), "admin/settings/captcha/$form_id/disable", array(), drupal_get_destination()))),
+      );
   	}
+    else {
+      $form['captcha'] = array(
+        '#type' => 'item',
+        '#title' => t('Captcha administration'),
+        '#description' => l(t('Place a captcha challenge here for anonymous visitors.'), "admin/settings/captcha/$form_id/enable"),
+      );
   }
-
-  if($captcha_type == 'none') {
-    //One of the user roles doesn't have captcha challenge for this point, aborting.
+  }
+  elseif ($user->uid) {
+    // captcha is only for anonymous users, so return
     return;
   }
-  elseif($captcha_type == NULL) {
-  	//captcha_type null;
+  else {
+    $seed = $form['form_token']['#default_value'];
+    // do not present captcha if not captcha-persistent and user has already solved a captcha for this form
+    if(!variable_get('captcha_persist', FALSE) && ($_SESSION['captcha'][$seed]['success'] === TRUE)) {
   	return;
   }
-     
-  $challenge = explode('/',$captcha_type);
-   
-  $result = module_invoke($challenge[0], 'captcha', 'generate', $challenge[1]);
+    // get captcha type and return if no captcha set
+    $captcha_type = db_result(db_query("SELECT captcha_type FROM {captcha_points} form_id = '%s'", $form_id));
+    if (!$captcha_type) {
+      return;
+    }
+    // get captcha module
+    list($captcha_module, $captcha_type) = explode('::', $captcha_type);
+    $result = module_invoke($captcha_module, 'captcha', 'generate', $captcha_type);
   if (!$result) {
-     //The user selected module returned nothing, maybe it is disabled or it's wrong, we should watchdog that and the quit.
-     watchdog('captcha',
-              t('Problems when trying to retrieve captcha %challenge challenge from %module',
-                array('%challenge' => $challenge[1], '%module' => $challenge[0])),
-              WATCHDOG_ERROR);
      return;
   }
-  
-  $form = array_merge($form,$result['form']);
-  
-  if(!isset($form['#validate']))
+    $captcha_description = variable_get('captcha_description', CAPTCHA_DESCRIPTION);
+    if ($captcha_description) {
+      $form['captcha'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Captcha'),
+        '#description' => t($captcha_description),
+      );
+    }
+    else {
+      $form['captcha'] = array();
+    }
+    $form['captcha'] = array_merge($form['captcha'], $result['form']);
+    // handle validate and prerender attributes
+    if(!isset($form['#validate'])) {
     $form['#validate'] = array();
+    }
   $form['#validate'] += array('captcha_validate' => array());
   $form['#pre_render'] = array('captcha_prerender');
-
+    // save captcha data in session
   $_SESSION['captcha'][$seed]['success'] = FALSE;
   $_SESSION['captcha'][$seed]['form_id'] = $form_id;
   $_SESSION['captcha'][$seed]['form'] = $result['form'];
-  $_SESSION['captcha'][$seed]['new_value'] = $result['value'];
+    $_SESSION['captcha'][$seed]['new_captcha_solution'] = $result['value'];
   $_SESSION['captcha'][$seed]['preprocess'] = isset($result['preprocess'])? $result['preprocess'] : FALSE;
-  $_SESSION['captcha'][$seed]['module'] = $challenge[0];
-  $_SESSION['captcha'][$seed]['type'] = $challenge[1];
+    $_SESSION['captcha'][$seed]['module'] = $captcha_module;
+    $_SESSION['captcha'][$seed]['type'] = $captcha_type;
+  }
+  //handle position of captcha: just above (last) submit button
+  foreach(element_children($form) as $key) {
+    if ($form[$key]['#type'] == 'submit') {
+      $submit_key = $key;
+    }
+  }
+  if (!$form[$submit_key]['#weight']) {
+    $form[$submit_key]['#weight'] = 30;
+  }
+  $form['captcha']['#weight'] = $form[$submit_key]['#weight'] - 1;
 }
 
-function captcha_validate($form_id, $form_item) {
-  $seed = $form_item['form_token'];
-
+/**
+ * Implementation of form #validate.
+ */
+function captcha_validate($form_id, $form_values) {
+  $seed = $form_values['form_token'];
+  // get answer and preprocess if needed
+  $captcha_answer = $form_values['captcha_challenge'];
   if($_SESSION['captcha'][$seed]['preprocess']) {
-    $value = module_invoke($_SESSION['captcha'][$seed]['module'],'captcha','process',$_SESSION['captcha'][$seed]['type'],$form_item['captcha_challenge']);
-  }
-  else {
-  	$value = $form_item['captcha_challenge'];
+    $captcha_answer = module_invoke($_SESSION['captcha'][$seed]['module'], 'captcha', 'process', $_SESSION['captcha'][$seed]['type'], $captcha_answer);
   } 
-
-  if($value == $_SESSION['captcha'][$seed]['value']) {
+  // check answer
+  if($captcha_answer == $_SESSION['captcha'][$seed]['captcha_solution']) {
   	$_SESSION['captcha'][$seed]['success'] = TRUE;
     return;
   }
-  form_set_error('captcha_response', t('The answer you entered to the captcha challenge is incorrect.'));  
+  else {
+    form_set_error('captcha_challenge', t('The answer you entered to the captcha challenge is incorrect.'));
+  }
 }
 
 /**
  * Implementation of form #pre_render.
- *
  */
 function captcha_prerender($form_id, $form) {
-  $seed = $form['#post']['form_token'];
-  $_SESSION['captcha'][$seed]['value'] = $_SESSION['captcha'][$seed]['new_value'];
-  if($_SESSION['captcha'][$seed]['success']) {
-    unset($form['captcha_challenge']);
-    unset($_SESSION['captcha'][$seed]);
-  }
+  $seed = $form['form_token']['#default_value'];
+  // store the captcha solution in the session array
+  $_SESSION['captcha'][$seed]['captcha_solution'] = $_SESSION['captcha'][$seed]['new_captcha_solution'];
 }
 
-
-
 /**
  * Default implementation of hook_captcha
  */
@@ -264,12 +328,11 @@ function captcha_captcha() {
         $answer = mt_rand(1, 20);
         $x = mt_rand(1, $answer);
         $y = $answer - $x;
- 
-        $result['value'] = (string)($answer);
+        $result['value'] = "$answer";
         $result['form']['captcha_challenge'] = array (
           '#type' => 'textfield',
-          '#title' => t('Math Question: What is %problem?', array('%problem' => $x .' + '. $y)),
-          '#description' => t('Please solve the math problem above and type in the result. e.g. for 1+1, type 2.'),
+          '#title' => t('Math Question: What is %problem?', array('%problem' => "$x + $y")),
+          '#description' => t('Solve this simple math problem and enter the result. E.g. for 1+3, type 4.'),
           '#weight' => 0,
           '#required' => TRUE,
         );
