honeypot.module 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. /**
  3. * @file
  4. *
  5. * Honeypot module, for deterring spam bots from completing Drupal forms.
  6. */
  7. /**
  8. * Implements hook_menu().
  9. */
  10. function honeypot_menu() {
  11. $items['admin/config/content/honeypot'] = array(
  12. 'title' => 'Honeypot configuration',
  13. 'description' => 'Configure Honeypot spam prevention and the forms on which Honeypot will be used.',
  14. 'page callback' => 'drupal_get_form',
  15. 'page arguments' => array('honeypot_admin_form'),
  16. 'access arguments' => array('administer honeypot'),
  17. 'file' => 'honeypot.admin.inc',
  18. );
  19. return $items;
  20. }
  21. /**
  22. * Implements hook_permission().
  23. */
  24. function honeypot_permission() {
  25. return array(
  26. 'administer honeypot' => array(
  27. 'title' => t('Administer Honeypot'),
  28. 'description' => t('Administer Honeypot-protected forms and settings'),
  29. ),
  30. 'bypass honeypot protection' => array(
  31. 'title' => t('Bypass Honeypot protection'),
  32. 'description' => t('Bypass Honeypot form protection.'),
  33. ),
  34. );
  35. }
  36. /**
  37. * Implements of hook_cron().
  38. */
  39. function honeypot_cron() {
  40. // Delete {honeypot_user} entries older than the value of honeypot_expire.
  41. db_delete('honeypot_user')
  42. ->condition('timestamp', time() - variable_get('honeypot_expire', 300), '<')
  43. ->execute();
  44. }
  45. /**
  46. * Implements hook_form_alter().
  47. *
  48. * Add Honeypot features to forms enabled in the Honeypot admin interface.
  49. */
  50. function honeypot_form_alter(&$form, &$form_state, $form_id) {
  51. // Don't use for maintenance mode forms (install, update, etc.).
  52. if (defined('MAINTENANCE_MODE')) {
  53. return;
  54. }
  55. $unprotected_forms = array(
  56. 'user_login',
  57. 'user_login_block',
  58. 'search_form',
  59. 'search_block_form',
  60. 'views_exposed_form',
  61. 'honeypot_admin_form',
  62. );
  63. // If configured to protect all forms, add protection to every form.
  64. if (variable_get('honeypot_protect_all_forms', 0) && !in_array($form_id, $unprotected_forms)) {
  65. // Don't protect system forms - only admins should have access, and system
  66. // forms may be programmatically submitted by drush and other modules.
  67. if (strpos($form_id, 'system_') === FALSE && strpos($form_id, 'search_') === FALSE && strpos($form_id, 'views_exposed_form_') === FALSE) {
  68. honeypot_add_form_protection($form, $form_state, array('honeypot', 'time_restriction'));
  69. }
  70. }
  71. // Otherwise add form protection to admin-configured forms.
  72. elseif ($forms_to_protect = honeypot_get_protected_forms()) {
  73. foreach ($forms_to_protect as $protect_form_id) {
  74. // For most forms, do a straight check on the form ID.
  75. if ($form_id == $protect_form_id) {
  76. honeypot_add_form_protection($form, $form_state, array('honeypot', 'time_restriction'));
  77. }
  78. // For webforms use a special check for variable form ID.
  79. elseif ($protect_form_id == 'webforms' && (strpos($form_id, 'webform_client_form') !== FALSE)) {
  80. honeypot_add_form_protection($form, $form_state, array('honeypot', 'time_restriction'));
  81. }
  82. }
  83. }
  84. }
  85. /**
  86. * Build an array of all the protected forms on the site, by form_id.
  87. *
  88. * @todo - Add in API call/hook to allow modules to add to this array.
  89. */
  90. function honeypot_get_protected_forms() {
  91. $forms = &drupal_static(__FUNCTION__);
  92. // If the data isn't already in memory, get from cache or look it up fresh.
  93. if (!isset($forms)) {
  94. if ($cache = cache_get('honeypot_protected_forms')) {
  95. $forms = $cache->data;
  96. }
  97. else {
  98. // Look up all the honeypot forms in the variables table.
  99. $result = db_query("SELECT name FROM {variable} WHERE name LIKE 'honeypot_form_%'")->fetchCol();
  100. // Add each form that's enabled to the $forms array.
  101. foreach ($result as $variable) {
  102. if (variable_get($variable, 0)) {
  103. $forms[] = substr($variable, 14);
  104. }
  105. }
  106. // Save the cached data.
  107. cache_set('honeypot_protected_forms', $forms, 'cache');
  108. }
  109. }
  110. return $forms;
  111. }
  112. /**
  113. * Form builder function to add different types of protection to forms.
  114. *
  115. * @param $options (array)
  116. * Array of options to be added to form. Currently accepts 'honeypot' and
  117. * 'time_restriction'.
  118. *
  119. * @return $form_elements
  120. * Returns elements to be placed in a form's elements array to prevent spam.
  121. */
  122. function honeypot_add_form_protection(&$form, &$form_state, $options = array()) {
  123. global $user;
  124. // Allow other modules to alter the protections applied to this form.
  125. drupal_alter('honeypot_form_protections', $options, $form);
  126. // Don't add any protections if the user can bypass the Honeypot.
  127. if (user_access('bypass honeypot protection')) {
  128. return;
  129. }
  130. // Build the honeypot element.
  131. if (in_array('honeypot', $options)) {
  132. // Get the element name (default is generic 'url').
  133. $honeypot_element = variable_get('honeypot_element_name', 'url');
  134. // Build the honeypot element.
  135. $honeypot_class = $honeypot_element . '-textfield';
  136. $form[$honeypot_element] = array(
  137. '#type' => 'textfield',
  138. '#title' => t('Leave this field blank'),
  139. '#size' => 20,
  140. '#weight' => 100,
  141. '#attributes' => array('autocomplete' => 'off'),
  142. '#element_validate' => array('_honeypot_honeypot_validate'),
  143. '#prefix' => '<div class="' . $honeypot_class . '">',
  144. '#suffix' => '</div>',
  145. '#attached' => array(
  146. 'css' => array(
  147. '.' . $honeypot_class . ' { display: none !important; }' => array('type' => 'inline'), // Hide honeypot.
  148. ),
  149. ),
  150. );
  151. }
  152. // Build the time restriction element (if it's not disabled).
  153. if (in_array('time_restriction', $options) && variable_get('honeypot_time_limit', 5) != 0) {
  154. // Set the current time in a hidden value to be checked later.
  155. $form['honeypot_time'] = array(
  156. '#type' => 'hidden',
  157. '#title' => t('Timestamp'),
  158. '#default_value' => time(),
  159. '#element_validate' => array('_honeypot_time_restriction_validate'),
  160. );
  161. // Disable page caching to make sure timestamp isn't cached.
  162. if (user_is_anonymous()) {
  163. $GLOBALS['conf']['cache'] = 0;
  164. }
  165. }
  166. // Allow other modules to react to addition of form protection.
  167. if (!empty($options)) {
  168. module_invoke_all('honeypot_add_form_protection', $options, $form);
  169. }
  170. }
  171. /**
  172. * Validate honeypot field.
  173. */
  174. function _honeypot_honeypot_validate($element, &$form_state) {
  175. // Get the honeypot field value.
  176. $honeypot_value = $element['#value'];
  177. // Make sure it's empty.
  178. if (!empty($honeypot_value)) {
  179. _honeypot_log($form_state['values']['form_id'], 'honeypot');
  180. form_set_error('', t('There was a problem with your form submission. Please refresh the page and try again.'));
  181. }
  182. }
  183. /**
  184. * Validate honeypot's time restriction field.
  185. */
  186. function _honeypot_time_restriction_validate($form, &$form_state) {
  187. // Get the time value.
  188. $honeypot_time = $form_state['values']['honeypot_time'];
  189. // Get the honeypot_time_limit.
  190. $time_limit = honeypot_get_time_limit($form_state['values']);
  191. // Make sure current time - (time_limit + form time value) is greater than 0.
  192. // If not, throw an error.
  193. if (time() < ($honeypot_time + $time_limit)) {
  194. _honeypot_log($form_state['values']['form_id'], 'honeypot_time');
  195. $time_limit = honeypot_get_time_limit();
  196. $form_state['values']['honeypot_time'] = time();
  197. form_set_error('', t('There was a problem with your form submission. Please wait @limit seconds and try again.', array('@limit' => $time_limit)));
  198. }
  199. }
  200. /**
  201. * Log blocked form submissions.
  202. *
  203. * @param $form_id
  204. * Form ID for the form on which submission was blocked.
  205. * @param $type
  206. * String indicating the reason the submission was blocked. Allowed values:
  207. * - honeypot: If honeypot field was filled in.
  208. * - honeypot_time: If form was completed before the configured time limit.
  209. */
  210. function _honeypot_log($form_id, $type) {
  211. honeypot_log_failure($form_id, $type);
  212. if (variable_get('honeypot_log', 0)) {
  213. $variables = array(
  214. '%form' => $form_id,
  215. '@cause' => ($type == 'honeypot') ? t('submission of a value in the honeypot field') : t('submission of the form in less than minimum required time'),
  216. );
  217. watchdog('honeypot', 'Blocked submission of %form due to @cause.', $variables);
  218. }
  219. return;
  220. }
  221. /**
  222. * Look up the time limit for the current user.
  223. *
  224. * @param $form_values
  225. * Array of form values (optional).
  226. */
  227. function honeypot_get_time_limit($form_values = array()) {
  228. global $user;
  229. $honeypot_time_limit = variable_get('honeypot_time_limit', 5);
  230. // Only calculate time limit if honeypot_time_limit has a value > 0.
  231. if ($honeypot_time_limit) {
  232. // Get value from {honeypot_user} table for authenticated users.
  233. if ($user->uid) {
  234. $number = db_query("SELECT COUNT(*) FROM {honeypot_user} WHERE uid = :uid", array(':uid' => $user->uid))->fetchField();
  235. }
  236. // Get value from {flood} table for anonymous users.
  237. else {
  238. $number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :hostname AND timestamp > :time", array(
  239. ':event' => 'honeypot',
  240. ':hostname' => ip_address(),
  241. ':time' => time() - variable_get('honeypot_expire', 300),
  242. ))->fetchField();
  243. }
  244. // Don't add more than 30 days' worth of extra time.
  245. $honeypot_time_limit = $honeypot_time_limit + (int) min($honeypot_time_limit + exp($number), 2592000);
  246. $additions = module_invoke_all('honeypot_time_limit', $honeypot_time_limit, $form_values, $number);
  247. if (count($additions)) {
  248. $honeypot_time_limit += array_sum($additions);
  249. }
  250. }
  251. return $honeypot_time_limit;
  252. }
  253. /**
  254. * Log the failed submision with timestamp.
  255. *
  256. * @param $form_id
  257. * Form ID for the rejected form submission.
  258. * @param $type
  259. * String indicating the reason the submission was blocked. Allowed values:
  260. * - honeypot: If honeypot field was filled in.
  261. * - honeypot_time: If form was completed before the configured time limit.
  262. */
  263. function honeypot_log_failure($form_id, $type) {
  264. global $user;
  265. // Log failed submissions for authenticated users.
  266. if ($user->uid) {
  267. db_insert('honeypot_user')
  268. ->fields(array(
  269. 'uid' => $user->uid,
  270. 'timestamp' => time(),
  271. ))
  272. ->execute();
  273. }
  274. // Register flood event for anonymous users.
  275. else {
  276. flood_register_event('honeypot');
  277. }
  278. // Allow other modules to react to honeypot rejections.
  279. module_invoke_all('honeypot_reject', $form_id, $user->uid, $type);
  280. }