summaryrefslogtreecommitdiff
path: root/kolab.org/www/drupal-7.18/sites/all/modules/captcha/captcha.inc
blob: e8212b1ae272afd92eef77f0d422dc32c96a5405 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
<?php

/**
 * @file
 * General CAPTCHA functionality and helper functions.
 */

/**
 * Helper function for adding/updating a CAPTCHA point.
 *
 * @param $form_id the form ID to configure.
 * @param captcha_type the setting for the given form_id, can be:
 *   - 'none' to disable CAPTCHA,
 *   - 'default' to use the default challenge type
 *   - NULL to remove the entry for the CAPTCHA type
 *   - something of the form 'image_captcha/Image'
 *   - an object with attributes $captcha_type->module and $captcha_type->captcha_type
 * @return nothing
 */
function captcha_set_form_id_setting($form_id, $captcha_type) {
  // Handle 'none'.
  if ($captcha_type == 'none') {
    db_merge('captcha_points')
      ->key(array('form_id' => $form_id))
      ->fields(array('module' => NULL, 'captcha_type' => NULL))
      ->execute();
  }
  // Handle 'default'.
  elseif ($captcha_type == 'default') {
    db_merge('captcha_points')
      ->key(array('form_id' => $form_id))
      ->fields(array('module' => NULL, 'captcha_type' => 'default'))
      ->execute();
  }
  // Handle NULL.
  elseif ($captcha_type == NULL) {
    db_delete('captcha_points')->condition('form_id', $form_id)->execute();
  }
  // Handle a captcha_type object.
  elseif (is_object($captcha_type) && isset($captcha_type->module) && isset($captcha_type->captcha_type)) {
    db_merge('captcha_points')
      ->key(array('form_id' => $form_id))
      ->fields(array('module' => $captcha_type->module, 'captcha_type' => $captcha_type->captcha_type))
      ->execute();
  }
  // Handle a captcha_type string.
  elseif (is_string($captcha_type) && substr_count($captcha_type, '/') == 1) {
    list($module, $type) = explode('/', $captcha_type);
    db_merge('captcha_points')
      ->key(array('form_id' => $form_id))
      ->fields(array('module' => $module, 'captcha_type' => $type))
      ->execute();
  }
  else {
    drupal_set_message(t('Failed to set a CAPTCHA type for form %form_id: could not interpret value "@captcha_type"',
      array('%form_id' => $form_id, '@captcha_type' => (string)$captcha_type)), 'warning');
  }
}

/**
 * Get the CAPTCHA setting for a given form_id.
 *
 * @param $form_id the form_id to query for
 * @param $symbolic flag to return as (symbolic) strings instead of object.
 *
 * @return NULL if no setting is known
 *   or a captcha_point object with fields 'module' and 'captcha_type'.
 *   If argument $symbolic is true, returns (symbolic) as 'none', 'default'
 *   or in the form 'captcha/Math'.
 */
function captcha_get_form_id_setting($form_id, $symbolic=FALSE) {
  $result = db_query("SELECT module, captcha_type FROM {captcha_points} WHERE form_id = :form_id",
    array(':form_id' =>  $form_id));
  $captcha_point = $result->fetchObject();
  if (!$captcha_point) {
    $captcha_point = NULL;
  }
  elseif ($captcha_point->captcha_type == 'default') {
    if (!$symbolic) {
      list($module, $type) = explode('/', variable_get('captcha_default_challenge', 'captcha/Math'));
      $captcha_point->module = $module;
      $captcha_point->captcha_type = $type;
    }
    else {
      $captcha_point = 'default';
    }
  }
  elseif ($captcha_point->module == NULL && $captcha_point->captcha_type == NULL && $symbolic) {
    $captcha_point = 'none';
  }
  elseif ($symbolic) {
    $captcha_point = $captcha_point->module . '/' . $captcha_point->captcha_type;
  }
  return $captcha_point;
}


/**
 * Helper function for generating a new CAPTCHA session.
 *
 * @param $form_id the form_id of the form to add a CAPTCHA to.
 * @param $status the initial status of the CAPTHCA session.
 * @return the session ID of the new CAPTCHA session.
 */
function _captcha_generate_captcha_session($form_id=NULL, $status=CAPTCHA_STATUS_UNSOLVED) {
  global $user;
  // Initialize solution with random data.
  $solution = md5(mt_rand());
  // Insert an entry and thankfully receive the value of the autoincrement field 'csid'.
  $captcha_sid = db_insert('captcha_sessions')
  ->fields(array(
    'uid' => $user->uid,
    'sid' => session_id(),
    'ip_address' => ip_address(),
    'timestamp' => REQUEST_TIME,
    'form_id' => $form_id,
    'solution' => $solution,
    'status' => $status,
    'attempts' => 0,
  ))
  ->execute();
  return $captcha_sid;
}

/**
 * Helper function for updating the solution in the CAPTCHA session table.
 *
 * @param $captcha_sid the CAPTCHA session ID to update.
 * @param $solution the new solution to associate with the given CAPTCHA session.
 */
function _captcha_update_captcha_session($captcha_sid, $solution) {
  db_update('captcha_sessions')
    ->condition('csid', $captcha_sid)
    ->fields(array(
      'timestamp' => REQUEST_TIME,
      'solution' => $solution,
    ))
    ->execute();
}

/**
 * Helper function for checking if CAPTCHA is required for user,
 * based on the CAPTCHA persistence setting, the CAPTCHA session ID and
 * user session info.
 */
function _captcha_required_for_user($captcha_sid, $form_id) {
  // Get the CAPTCHA persistence setting.
  $captcha_persistence = variable_get('captcha_persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE);

  // First check: should we always add a CAPTCHA?
  if ($captcha_persistence == CAPTCHA_PERSISTENCE_SHOW_ALWAYS) {
    return TRUE;
  }

  // Get the status of the current CAPTCHA session.
  $captcha_session_status = db_query('SELECT status FROM {captcha_sessions} WHERE csid = :csid', array(':csid' => $captcha_sid))->fetchField();
  // Second check: if the current session is already solved: omit further CAPTCHAs.
  if ($captcha_session_status == CAPTCHA_STATUS_SOLVED) {
    return FALSE;
  }

  // Third check: look at the persistence level (per form instance, per form or per user).
  if ($captcha_persistence == CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) {
    return TRUE;
  }
  else {
    $captcha_success_form_ids = isset($_SESSION['captcha_success_form_ids']) ? (array)($_SESSION['captcha_success_form_ids']) : array();
    switch ($captcha_persistence) {
      case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL:
        return (count($captcha_success_form_ids) == 0);
      case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE:
        return !isset($captcha_success_form_ids[$form_id]);
    }
  }

  // We should never get to this point, but to be sure, we return TRUE.
  return TRUE;
}


/**
 * Get the CAPTCHA description as configured on the general CAPTCHA
 * settings page.
 *
 * If the locale module is enabled, the description will be returned
 * for the current language the page is rendered for. This language
 * can optionally been overriden with the $lang_code argument.
 *
 * @param $lang_code an optional language code to get the descripion for.
 * @return a string with (localized) CAPTCHA description.
 */
function _captcha_get_description($lang_code=NULL) {
  // If no language code is given: use the language of the current page.
  global $language;
  $lang_code = isset($lang_code) ? $lang_code : $language->language;
  // The hardcoded but localizable default.
  $default = t('This question is for testing whether you are a human visitor and to prevent automated spam submissions.', array(), array('langcode' => $lang_code));
  // Look up the configured CAPTCHA description or fall back on the (localized) default.
  if (module_exists('locale')) {
    $description = variable_get("captcha_description_$lang_code", $default);
  }
  else {
    $description = variable_get('captcha_description', $default);
  }
  return filter_xss_admin($description);
}

/**
 * Parse or interpret the given captcha_type.
 * @param $captcha_type string representation of the CAPTCHA type,
 *      e.g. 'default', 'none', 'captcha/Math', 'image_captcha/Image'
 * @return list($captcha_module, $captcha_type)
 */
function _captcha_parse_captcha_type($captcha_type) {
  if ($captcha_type == 'none') {
    return array(NULL, NULL);
  }
  if ($captcha_type == 'default') {
    $captcha_type = variable_get('captcha_default_challenge', 'captcha/Math');
  }
  return explode('/', $captcha_type);
}

/**
 * Helper function to get placement information for a given form_id.
 * @param $form_id the form_id to get the placement information for.
 * @param $form if a form corresponding to the given form_id, if there
 *   is no placement info for the given form_id, this form is examined to
 *   guess the placement.
 * @return placement info array (@see _captcha_insert_captcha_element() for more
 *   info about the fields 'path', 'key' and 'weight'.
 */
function _captcha_get_captcha_placement($form_id, $form) {
  // Get CAPTCHA placement map from cache. Two levels of cache:
  // static variable in this function and storage in the variables table.
  static $placement_map = NULL;
  // Try first level cache.
  if ($placement_map === NULL) {
    // If first level cache missed: try second level cache.
    $placement_map = variable_get('captcha_placement_map_cache', NULL);

    if ($placement_map === NULL) {
      // If second level cache missed: initialize the placement map
      // and let other modules hook into this with the hook_captcha_placement_map hook.
      // By default however, probably all Drupal core forms are already correctly
      // handled with the best effort guess based on the 'actions' element (see below).
      $placement_map = module_invoke_all('captcha_placement_map');
    }
  }

  // Query the placement map.
  if (array_key_exists($form_id, $placement_map)) {
    $placement = $placement_map[$form_id];
  }
  // If no placement info is available in placement map: make a best effort guess.
  else {
    // If there is an "actions" button group, a good placement is just before that.
    if (isset($form['actions']) && isset($form['actions']['#type']) && $form['actions']['#type'] === 'actions') {
      $placement = array(
        'path' => array(),
        'key' => 'actions',
        // #type 'actions' defaults to 100.
        'weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99),
      );
    }
    else {
      // Search the form for buttons and guess placement from it.
      $buttons = _captcha_search_buttons($form);
      if (count($buttons)) {
        // Pick first button.
        // TODO: make this more sofisticated? Use cases needed.
        $placement = $buttons[0];
      }
      else {
        // Use NULL when no buttons were found.
        $placement = NULL;
      }
    }

    // Store calculated placement in cache.
    $placement_map[$form_id] = $placement;
    variable_set('captcha_placement_map_cache', $placement_map);
  }

  return $placement;
}

/**
 * Helper function for searching the buttons in a form.
 *
 * @param $form the form to search button elements in
 * @return an array of paths to the buttons.
 *   A path is an array of keys leading to the button, the last
 *   item in the path is the weight of the button element
 *   (or NULL if undefined).
 */
function _captcha_search_buttons($form) {
  $buttons = array();
  foreach (element_children($form) as $key) {
    // Look for submit or button type elements.
    if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) {
      $weight = isset($form[$key]['#weight']) ? $form[$key]['#weight'] : NULL;
      $buttons[] = array(
        'path' => array(),
        'key' => $key,
        'weight' => $weight,
      );
    }
    // Process children recurively.
    $children_buttons = _captcha_search_buttons($form[$key]);
    foreach ($children_buttons as $b) {
      $b['path'] = array_merge(array($key), $b['path']);
      $buttons[] = $b;
    }
  }
  return $buttons;
}

/**
 * Helper function to insert a CAPTCHA element in a form before a given form element.
 * @param $form the form to add the CAPTCHA element to.
 * @param $placement information where the CAPTCHA element should be inserted.
 *   $placement should be an associative array with fields:
 *     - 'path': path (array of path items) of the container in the form where the
 *       CAPTCHA element should be inserted.
 *     - 'key': the key of the element before which the CAPTCHA element
 *       should be inserted. If the field 'key' is undefined or NULL, the CAPTCHA will
 *       just be appended in the container.
 *     - 'weight': if 'key' is not NULL: should be the weight of the element defined by 'key'.
 *       If 'key' is NULL and weight is not NULL: set the weight property of the CAPTCHA element
 *       to this value.
 * @param $captcha_element the CAPTCHA element to insert.
 */
function _captcha_insert_captcha_element(&$form, $placement, $captcha_element) {
  // Get path, target and target weight or use defaults if not available.
  $target_key = isset($placement['key']) ? $placement['key'] : NULL;
  $target_weight = isset($placement['weight']) ? $placement['weight'] : NULL;
  $path = isset($placement['path']) ? $placement['path'] : array();

  // Walk through the form along the path.
  $form_stepper = &$form;
  foreach ($path as $step) {
    if (isset($form_stepper[$step])) {
      $form_stepper = & $form_stepper[$step];
    }
    else {
      // Given path is invalid: stop stepping and
      // continue in best effort (append instead of insert).
      $target_key = NULL;
      break;
    }
  }

  // If no target is available: just append the CAPTCHA element to the container.
  if ($target_key == NULL || !array_key_exists($target_key, $form_stepper)) {
    // Optionally, set weight of CAPTCHA element.
    if ($target_weight != NULL) {
      $captcha_element['#weight'] = $target_weight;
    }
    $form_stepper['captcha'] =  $captcha_element;
  }
  // If there is a target available: make sure the CAPTCHA element comes right before it.
  else {
    // If target has a weight: set weight of CAPTCHA element a bit smaller
    // and just append the CAPTCHA: sorting will fix the ordering anyway.
    if ($target_weight != NULL) {
      $captcha_element['#weight'] = $target_weight - .1;
      $form_stepper['captcha'] =  $captcha_element;
    }
    else {
      // If we can't play with weights: insert the CAPTCHA element at the right position.
      // Because PHP lacks a function for this (array_splice() comes close,
      // but it does not preserve the key of the inserted element), we do it by hand:
      // chop of the end, append the CAPTCHA element and put the end back.
      $offset = array_search($target_key, array_keys($form_stepper));
      $end = array_splice($form_stepper, $offset);
      $form_stepper['captcha'] =  $captcha_element;
      foreach ($end as $k => $v) {
        $form_stepper[$k] = $v;
      }
    }
  }
}