forked from M1r1k/captcha
-
Notifications
You must be signed in to change notification settings - Fork 0
/
captcha.inc
342 lines (314 loc) · 11.1 KB
/
captcha.inc
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
<?php
/**
* @file
* General CAPTCHA functionality and helper functions.
*/
use Drupal\Component\Utility\Xss;
use Drupal\captcha\Entity\CaptchaPoint;
use Drupal\Core\Render\Element;
/**
* Helper function for adding/updating a CAPTCHA point.
*
* @param string $form_id
* the form ID to configure.
* @param string $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.
*/
function captcha_set_form_id_setting($form_id, $captcha_type) {
/* @var CaptchaPoint $captcha_point */
$captcha_point = CaptchaPoint::load($form_id);
if ($captcha_point) {
$captcha_point->setCaptchaType($captcha_type);
}
else {
$captcha_point = new CaptchaPoint(array('formId' => $form_id, 'captchaType' => $captcha_type), 'captcha_point');
}
$captcha_point->save();
}
/**
* Helper function for generating a new CAPTCHA session.
*
* @param string $form_id
* The form_id of the form to add a CAPTCHA to.
* @param int $status
* The initial status of the CAPTHCA session.
*
* @return string
* The session ID of the new CAPTCHA session.
*/
function _captcha_generate_captcha_session($form_id = NULL, $status = CAPTCHA_STATUS_UNSOLVED) {
$user = \Drupal::currentUser();
// 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->id(),
'sid' => session_id(),
'ip_address' => Drupal::request()->getClientIp(),
'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 string $captcha_sid
* The CAPTCHA session ID to update.
* @param string $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 = \Drupal::config('captcha.settings')->get('persistence');
// 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.
*
* @return string
* CAPTCHA description.
*/
function _captcha_get_description() {
$description = \Drupal::config('captcha.settings')->get('description');
return Xss::filter($description);
}
/**
* Parse or interpret the given captcha_type.
*
* @param string $captcha_type
* representation of the CAPTCHA type,
* e.g. 'default', 'none', 'captcha/Math', 'image_captcha/Image'.
*
* @return array
* 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 = \Drupal::config('captcha.settings')->get('default_challenge');
}
return explode('/', $captcha_type);
}
/**
* Helper function to get placement information for a given form_id.
*/
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;
$write_cache = FALSE;
// Try first level cache.
if ($placement_map === NULL) {
// If first level cache missed: try second level cache.
if ($cache = \Drupal::cache()->get('captcha_placement_map_cache')) {
$placement_map = $cache->data;
}
else {
// 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 = \Drupal::moduleHandler()->invokeAll('captcha_placement_map');
$write_cache = TRUE;
}
}
// 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 = array();
}
}
// Store calculated placement in cache.
$placement_map[$form_id] = $placement;
$write_cache = TRUE;
}
if ($write_cache) {
\Drupal::cache()->set('captcha_placement_map_cache', $placement_map);
}
return $placement;
}
/**
* Helper function for searching the buttons in a form.
*
* @param array $form
* The form to search button elements in.
*
* @return array
* 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(array $form) {
$buttons = array();
foreach (Element::children($form, FALSE) 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 recursively.
$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 before a given form element.
*
* @param array $form
* the form to add the CAPTCHA element to.
* @param array $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 array $captcha_element
* the CAPTCHA element to insert.
*/
function _captcha_insert_captcha_element(array &$form, array $placement, array $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;
}
}
}
}