-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathhoneypot.module
338 lines (300 loc) · 12.9 KB
/
honeypot.module
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
<?php
/**
* @file
* Honeypot module, for deterring spam bots from completing Drupal forms.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function honeypot_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.honeypot':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Honeypot module uses both the honeypot and timestamp methods of deterring spam bots from completing forms on your Drupal site. These methods are effective against many spam bots, and are not as intrusive as CAPTCHAs or other methods which punish the user. For more information, see the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/8/modules/honeypot']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Configuring Honeypot') . '</dt>';
$output .= '<dd>' . t('All settings for this module are on the Honeypot configuration page, under the Configuration section, in the Content authoring settings. You can visit the configuration page directly from the Honeypot configuration link below. The configuration settings are described in the <a href=":url">online documentation for the Honeypot module</a>.', [':url' => 'https://www.drupal.org/docs/8/modules/honeypot/using-honeypot']) . '</dd>';
$output .= '<dt>' . t('Setting up Honeypot in your own forms') . '</dt>';
$output .= '<dd>' . t("Honeypot protection can be bypassed for certain user roles. For instance, site administrators, who just might be able to fill out a form in less than 5 seconds. And, Honeypot protection can be enabled only for certain forms. Or, it can protect all forms on the site. Finally, honeypot protection can be used in any of your own forms by simply including a little code snippet included on the module's project page.") . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_cron().
*/
function honeypot_cron() {
// Delete {honeypot_user} entries older than the value of honeypot_expire.
$expire_limit = \Drupal::config('honeypot.settings')->get('expire');
\Drupal::database()->delete('honeypot_user')
->condition('timestamp', \Drupal::time()->getRequestTime() - $expire_limit, '<')
->execute();
}
/**
* Implements hook_form_alter().
*
* Add Honeypot features to forms enabled in the Honeypot admin interface.
*/
function honeypot_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Don't use for maintenance mode forms (install, update, etc.).
if (defined('MAINTENANCE_MODE')) {
return;
}
// Add a tag to all forms, so that if they are cached and honeypot
// configuration is changed, the cached forms are invalidated and honeypot
// protection can be re-evaluated.
$form['#cache']['tags'][] = 'config:honeypot.settings';
// Get list of unprotected forms and setting for whether to protect all forms.
$unprotected_forms = \Drupal::config('honeypot.settings')->get('unprotected_forms');
$protect_all_forms = \Drupal::config('honeypot.settings')->get('protect_all_forms');
// If configured to protect all forms, add protection to every form.
if ($protect_all_forms && !in_array($form_id, $unprotected_forms)) {
// Don't protect system forms - only admins should have access, and system
// forms may be programmatically submitted by drush and other modules.
if (preg_match('/[^a-zA-Z]system_/', $form_id) === 0 && preg_match('/[^a-zA-Z]search_/', $form_id) === 0 && preg_match('/[^a-zA-Z]views_exposed_form_/', $form_id) === 0) {
honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']);
}
}
// Otherwise add form protection to admin-configured forms.
elseif ($forms_to_protect = honeypot_get_protected_forms()) {
foreach ($forms_to_protect as $protect_form_id) {
// For most forms, do a straight check on the form ID.
if ($form_id == $protect_form_id) {
honeypot_add_form_protection($form, $form_state, ['honeypot', 'time_restriction']);
}
}
}
}
/**
* Build an array of all the protected forms on the site, by form_id.
*/
function honeypot_get_protected_forms() {
$forms = &drupal_static(__FUNCTION__);
// If the data isn't already in memory, get from cache or look it up fresh.
if (!isset($forms)) {
if ($cache = \Drupal::cache()->get('honeypot_protected_forms')) {
$forms = $cache->data;
}
else {
$form_settings = \Drupal::config('honeypot.settings')->get('form_settings');
if (!empty($form_settings)) {
// Add each form that's enabled to the $forms array.
foreach ($form_settings as $form_id => $enabled) {
if ($enabled) {
$forms[] = $form_id;
}
}
}
else {
$forms = [];
}
// Save the cached data.
\Drupal::cache()->set('honeypot_protected_forms', $forms);
}
}
return $forms;
}
/**
* Form builder function to add different types of protection to forms.
*
* @param array $options
* Array of options to be added to form. Currently accepts 'honeypot' and
* 'time_restriction'.
*/
function honeypot_add_form_protection(&$form, FormStateInterface $form_state, array $options = []) {
$account = \Drupal::currentUser();
// Allow other modules to alter the protections applied to this form.
\Drupal::moduleHandler()->alter('honeypot_form_protections', $options, $form);
// Don't add any protections if the user can bypass the Honeypot.
if ($account->hasPermission('bypass honeypot protection')) {
return;
}
// Build the honeypot element.
if (in_array('honeypot', $options)) {
// Get the element name (default is generic 'url').
$honeypot_element = \Drupal::config('honeypot.settings')->get('element_name');
// Build the honeypot element.
$honeypot_class = $honeypot_element . '-textfield';
$form[$honeypot_element] = [
'#theme_wrappers' => [
0 => 'form_element',
'container' => [
'#id' => NULL,
'#attributes' => [
'class' => [
$honeypot_class,
],
'style' => [
'display: none !important;',
],
],
],
],
'#type' => 'textfield',
'#title' => t('Leave this field blank'),
'#size' => 20,
'#weight' => 100,
'#attributes' => ['autocomplete' => 'off'],
'#element_validate' => ['_honeypot_honeypot_validate'],
];
}
// Set the time restriction for this form (if it's not disabled).
if (in_array('time_restriction', $options) && \Drupal::config('honeypot.settings')->get('time_limit') != 0) {
// Set the current time in a hidden value to be checked later.
$input = $form_state->getUserInput();
if (empty($input['honeypot_time'])) {
$identifier = Crypt::randomBytesBase64();
\Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, time(), 3600 * 24);
}
else {
$identifier = $input['honeypot_time'];
}
$form['honeypot_time'] = [
'#type' => 'hidden',
'#title' => t('Timestamp'),
'#default_value' => $identifier,
'#element_validate' => ['_honeypot_time_restriction_validate'],
'#cache' => [
'max-age' => 0,
],
];
// Disable page caching to make sure timestamp isn't cached.
$account = \Drupal::currentUser();
if ($account->id() == 0) {
// TODO D8 - Use DIC? See: http://drupal.org/node/1539454
// Should this now set 'omit_vary_cookie' instead?
Drupal::service('page_cache_kill_switch')->trigger();
}
}
// Allow other modules to react to addition of form protection.
if (!empty($options)) {
\Drupal::moduleHandler()->invokeAll('honeypot_add_form_protection', [$options, $form]);
}
}
/**
* Validate honeypot field.
*/
function _honeypot_honeypot_validate($element, FormStateInterface $form_state) {
// Get the honeypot field value.
$honeypot_value = $element['#value'];
// Make sure it's empty.
if (!empty($honeypot_value) || $honeypot_value == '0') {
_honeypot_log($form_state->getValue('form_id'), 'honeypot');
$form_state->setErrorByName('', t('There was a problem with your form submission. Please refresh the page and try again.'));
}
}
/**
* Validate honeypot's time restriction field.
*/
function _honeypot_time_restriction_validate($element, FormStateInterface $form_state) {
if ($form_state->isProgrammed()) {
// Don't do anything if the form was submitted programmatically.
return;
}
$triggering_element = $form_state->getTriggeringElement();
// Don't do anything if the triggering element is a preview button.
if ($triggering_element['#value'] == t('Preview')) {
return;
}
// Get the time value.
$identifier = $form_state->getValue('honeypot_time', FALSE);
$honeypot_time = \Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->get($identifier, 0);
// Get the honeypot_time_limit.
$time_limit = honeypot_get_time_limit($form_state->getValues());
// Make sure current time - (time_limit + form time value) is greater than 0.
// If not, throw an error.
if (!$honeypot_time || \Drupal::time()->getRequestTime() < ($honeypot_time + $time_limit)) {
_honeypot_log($form_state->getValue('form_id'), 'honeypot_time');
$time_limit = honeypot_get_time_limit();
\Drupal::service('keyvalue.expirable')->get('honeypot_time_restriction')->setWithExpire($identifier, \Drupal::time()->getRequestTime(), 3600 * 24);
$form_state->setErrorByName('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', ['@limit' => $time_limit]));
}
}
/**
* Log blocked form submissions.
*
* @param string $form_id
* Form ID for the form on which submission was blocked.
* @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
*/
function _honeypot_log($form_id, $type) {
honeypot_log_failure($form_id, $type);
if (\Drupal::config('honeypot.settings')->get('log')) {
$variables = [
'%form' => $form_id,
'@cause' => ($type == 'honeypot') ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
];
\Drupal::logger('honeypot')->notice(t('Blocked submission of %form due to @cause.', $variables));
}
}
/**
* Look up the time limit for the current user.
*
* @param array $form_values
* Array of form values (optional).
*/
function honeypot_get_time_limit(array $form_values = []) {
$account = \Drupal::currentUser();
$honeypot_time_limit = \Drupal::config('honeypot.settings')->get('time_limit');
// Only calculate time limit if honeypot_time_limit has a value > 0.
if ($honeypot_time_limit) {
$expire_time = \Drupal::config('honeypot.settings')->get('expire');
// Query the {honeypot_user} table to determine the number of failed
// submissions for the current user.
$uid = $account->id();
$query = \Drupal::database()->select('honeypot_user', 'hu')
->condition('uid', $uid)
->condition('timestamp', \Drupal::time()->getRequestTime() - $expire_time, '>');
// For anonymous users, take the hostname into account.
if ($uid === 0) {
$hostname = \Drupal::request()->getClientIp();
$query->condition('hostname', $hostname);
}
$number = $query->countQuery()->execute()->fetchField();
// Don't add more than 30 days' worth of extra time.
$honeypot_time_limit = (int) min($honeypot_time_limit + exp($number) - 1, $expire_time);
// TODO - Only accepts two args.
$additions = \Drupal::moduleHandler()->invokeAll('honeypot_time_limit', [
$honeypot_time_limit,
$form_values,
$number,
]);
if (count($additions)) {
$honeypot_time_limit += array_sum($additions);
}
}
return $honeypot_time_limit;
}
/**
* Log the failed submission with timestamp and hostname.
*
* @param string $form_id
* Form ID for the rejected form submission.
* @param string $type
* String indicating the reason the submission was blocked. Allowed values:
* - honeypot: If honeypot field was filled in.
* - honeypot_time: If form was completed before the configured time limit.
*/
function honeypot_log_failure($form_id, $type) {
$account = \Drupal::currentUser();
$uid = $account->id();
// Log failed submissions.
\Drupal::database()->insert('honeypot_user')
->fields([
'uid' => $uid,
'hostname' => Drupal::request()->getClientIp(),
'timestamp' => \Drupal::time()->getRequestTime(),
])
->execute();
// Allow other modules to react to honeypot rejections.
// TODO - Only accepts two args.
\Drupal::moduleHandler()->invokeAll('honeypot_reject', [$form_id, $uid, $type]);
}