captcha.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <?php
  2. /**
  3. * @file
  4. * General CAPTCHA functionality and helper functions.
  5. */
  6. /**
  7. * Helper function for adding/updating a CAPTCHA point.
  8. *
  9. * @param string $form_id
  10. * the form ID to configure.
  11. *
  12. * @param string $captcha_type
  13. * the setting for the given form_id, can be:
  14. * - 'none' to disable CAPTCHA,
  15. * - 'default' to use the default challenge type
  16. * - NULL to remove the entry for the CAPTCHA type
  17. * - something of the form 'image_captcha/Image'
  18. * - an object with attributes $captcha_type->module and $captcha_type->captcha_type
  19. */
  20. function captcha_set_form_id_setting($form_id, $captcha_type) {
  21. // Handle 'none'.
  22. if ($captcha_type == 'none') {
  23. db_merge('captcha_points')
  24. ->key(array('form_id' => $form_id))
  25. ->fields(array('module' => NULL, 'captcha_type' => NULL))
  26. ->execute();
  27. }
  28. // Handle 'default'.
  29. elseif ($captcha_type == 'default') {
  30. db_merge('captcha_points')
  31. ->key(array('form_id' => $form_id))
  32. ->fields(array('module' => NULL, 'captcha_type' => 'default'))
  33. ->execute();
  34. }
  35. // Handle NULL.
  36. elseif ($captcha_type == NULL) {
  37. db_delete('captcha_points')->condition('form_id', $form_id)->execute();
  38. }
  39. // Handle a captcha_type object.
  40. elseif (is_object($captcha_type) && !empty($captcha_type->module) && !empty($captcha_type->captcha_type)) {
  41. db_merge('captcha_points')
  42. ->key(array('form_id' => $form_id))
  43. ->fields(array('module' => $captcha_type->module, 'captcha_type' => $captcha_type->captcha_type))
  44. ->execute();
  45. }
  46. // Handle a captcha_type string.
  47. elseif (is_string($captcha_type) && substr_count($captcha_type, '/') == 1) {
  48. list($module, $type) = explode('/', $captcha_type);
  49. db_merge('captcha_points')
  50. ->key(array('form_id' => $form_id))
  51. ->fields(array('module' => $module, 'captcha_type' => $type))
  52. ->execute();
  53. }
  54. else {
  55. drupal_set_message(
  56. t('Failed to set a CAPTCHA type for form %form_id: could not interpret value "@captcha_type"',
  57. array(
  58. '%form_id' => $form_id,
  59. '@captcha_type' => (string) $captcha_type,
  60. )
  61. ),
  62. 'warning'
  63. );
  64. }
  65. }
  66. /**
  67. * Get the CAPTCHA setting for a given form_id.
  68. *
  69. * @param string $form_id
  70. * the form_id to query for
  71. *
  72. * @param bool $symbolic
  73. * flag to return as (symbolic) strings instead of object.
  74. *
  75. * @return NULL
  76. * if no setting is known
  77. * or a captcha_point object with fields 'module' and 'captcha_type'.
  78. * If argument $symbolic is true, returns (symbolic) as 'none', 'default'
  79. * or in the form 'captcha/Math'.
  80. */
  81. function captcha_get_form_id_setting($form_id, $symbolic = FALSE) {
  82. // Fetch setting from database.
  83. if (module_exists('ctools')) {
  84. ctools_include('export');
  85. $object = ctools_export_load_object('captcha_points', 'names', array($form_id));
  86. $captcha_point = array_pop($object);
  87. }
  88. else {
  89. $result = db_query("SELECT module, captcha_type FROM {captcha_points} WHERE form_id = :form_id",
  90. array(':form_id' => $form_id));
  91. $captcha_point = $result->fetchObject();
  92. }
  93. // If no setting is available in database for the given form,
  94. // but 'captcha_default_challenge_on_nonlisted_forms' is enabled, pick the default type anyway.
  95. if (!$captcha_point && variable_get('captcha_default_challenge_on_nonlisted_forms', FALSE)) {
  96. $captcha_point = (object) array('captcha_type' => 'default');
  97. }
  98. // Handle (default) settings and symbolic mode.
  99. if (!$captcha_point) {
  100. $captcha_point = NULL;
  101. }
  102. elseif (!empty($captcha_point->captcha_type) && $captcha_point->captcha_type == 'default') {
  103. if (!$symbolic) {
  104. list($module, $type) = explode('/', variable_get('captcha_default_challenge', 'captcha/Math'));
  105. $captcha_point->module = $module;
  106. $captcha_point->captcha_type = $type;
  107. }
  108. else {
  109. $captcha_point = 'default';
  110. }
  111. }
  112. elseif (empty($captcha_point->module) && empty($captcha_point->captcha_type) && $symbolic) {
  113. $captcha_point = 'none';
  114. }
  115. elseif ($symbolic) {
  116. $captcha_point = $captcha_point->module . '/' . $captcha_point->captcha_type;
  117. }
  118. return $captcha_point;
  119. }
  120. /**
  121. * Helper function to load all captcha points.
  122. *
  123. * @return array of all captcha_points
  124. */
  125. function captcha_get_captcha_points() {
  126. if (module_exists('ctools')) {
  127. ctools_include('export');
  128. $captcha_points = ctools_export_load_object('captcha_points', 'all');
  129. }
  130. else {
  131. $captcha_points = array();
  132. $result = db_select('captcha_points', 'cp')->fields('cp')->orderBy('form_id')->execute();
  133. foreach ($result as $captcha_point) {
  134. $captcha_points[] = $captcha_point;
  135. }
  136. }
  137. return $captcha_points;
  138. }
  139. /**
  140. * Helper function for generating a new CAPTCHA session.
  141. *
  142. * @param string $form_id
  143. * the form_id of the form to add a CAPTCHA to.
  144. *
  145. * @param int $status
  146. * the initial status of the CAPTHCA session.
  147. *
  148. * @return int
  149. * the session ID of the new CAPTCHA session.
  150. */
  151. function _captcha_generate_captcha_session($form_id = NULL, $status = CAPTCHA_STATUS_UNSOLVED) {
  152. global $user;
  153. // Initialize solution with random data.
  154. $solution = md5(mt_rand());
  155. // Insert an entry and thankfully receive the value of the autoincrement field 'csid'.
  156. $captcha_sid = db_insert('captcha_sessions')
  157. ->fields(array(
  158. 'uid' => $user->uid,
  159. 'sid' => session_id(),
  160. 'ip_address' => ip_address(),
  161. 'timestamp' => REQUEST_TIME,
  162. 'form_id' => $form_id,
  163. 'solution' => $solution,
  164. 'status' => $status,
  165. 'attempts' => 0,
  166. ))
  167. ->execute();
  168. return $captcha_sid;
  169. }
  170. /**
  171. * Helper function for updating the solution in the CAPTCHA session table.
  172. *
  173. * @param int $captcha_sid
  174. * the CAPTCHA session ID to update.
  175. *
  176. * @param string $solution
  177. * the new solution to associate with the given CAPTCHA session.
  178. */
  179. function _captcha_update_captcha_session($captcha_sid, $solution) {
  180. db_update('captcha_sessions')
  181. ->condition('csid', $captcha_sid)
  182. ->fields(array(
  183. 'timestamp' => REQUEST_TIME,
  184. 'solution' => $solution,
  185. ))
  186. ->execute();
  187. }
  188. /**
  189. * Helper function for checking if CAPTCHA is required for user.
  190. *
  191. * Based on the CAPTCHA persistence setting, the CAPTCHA session ID and
  192. * user session info.
  193. */
  194. function _captcha_required_for_user($captcha_sid, $form_id) {
  195. // Get the CAPTCHA persistence setting.
  196. $captcha_persistence = variable_get('captcha_persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE);
  197. // First check: should we always add a CAPTCHA?
  198. if ($captcha_persistence == CAPTCHA_PERSISTENCE_SHOW_ALWAYS) {
  199. return TRUE;
  200. }
  201. // Get the status of the current CAPTCHA session.
  202. $captcha_session_status = db_query('SELECT status FROM {captcha_sessions} WHERE csid = :csid', array(':csid' => $captcha_sid))->fetchField();
  203. // Second check: if the current session is already solved: omit further CAPTCHAs.
  204. if ($captcha_session_status == CAPTCHA_STATUS_SOLVED) {
  205. return FALSE;
  206. }
  207. // Third check: look at the persistence level (per form instance, per form or per user).
  208. if ($captcha_persistence == CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE) {
  209. return TRUE;
  210. }
  211. else {
  212. $captcha_success_form_ids = isset($_SESSION['captcha_success_form_ids']) ? (array) ($_SESSION['captcha_success_form_ids']) : array();
  213. switch ($captcha_persistence) {
  214. case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL:
  215. return (count($captcha_success_form_ids) == 0);
  216. case CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_TYPE:
  217. return !isset($captcha_success_form_ids[$form_id]);
  218. }
  219. }
  220. // We should never get to this point, but to be sure, we return TRUE.
  221. return TRUE;
  222. }
  223. /**
  224. * Get the CAPTCHA description as configured on the general CAPTCHA settings page.
  225. *
  226. * If the locale module is enabled, the description will be returned
  227. * for the current language the page is rendered for. This language
  228. * can optionally been overridden with the $lang_code argument.
  229. *
  230. * @param string|null $lang_code
  231. * an optional language code to get the description for.
  232. *
  233. * @return string
  234. * String with (localized) CAPTCHA description.
  235. */
  236. function _captcha_get_description($lang_code = NULL) {
  237. // If no language code is given: use the language of the current page.
  238. global $language;
  239. $lang_code = isset($lang_code) ? $lang_code : $language->language;
  240. // The hardcoded but localizable default.
  241. $default = t('This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.', array(), array('langcode' => $lang_code));
  242. // Look up the configured CAPTCHA description or fall back on the (localized) default.
  243. if (module_exists('locale')) {
  244. $description = variable_get("captcha_description_$lang_code", $default);
  245. }
  246. else {
  247. $description = variable_get('captcha_description', $default);
  248. }
  249. return filter_xss_admin($description);
  250. }
  251. /**
  252. * Get the error message as configured on the general CAPTCHA settings page.
  253. *
  254. * If the locale module is enabled, the error message will be returned
  255. * for the current language the page is rendered for. This language
  256. * can optionally been overridden with the $lang_code argument.
  257. *
  258. * @param string|null $lang_code
  259. * an optional language code to get the description for.
  260. *
  261. * @return string
  262. * String with (localized) error message.
  263. */
  264. function _captcha_get_error_message($lang_code = NULL) {
  265. // If no language code is given: use the language of the current page.
  266. global $language;
  267. $lang_code = isset($lang_code) ? $lang_code : $language->language;
  268. // The hardcoded but localizable default.
  269. $default = t('The answer you entered for the CAPTCHA was not correct.', array(), array('langcode' => $lang_code));
  270. // Look up the configured error message or fall back on the (localized) default.
  271. if (module_exists('locale')) {
  272. $message = variable_get('captcha_error_message_' . $lang_code, $default);
  273. }
  274. else {
  275. $message = variable_get('captcha_error_message', $default);
  276. }
  277. return filter_xss_admin($message);
  278. }
  279. /**
  280. * Parse or interpret the given captcha_type.
  281. *
  282. * @param string $captcha_type
  283. * string representation of the CAPTCHA type,
  284. * e.g. 'default', 'none', 'captcha/Math', 'image_captcha/Image'
  285. *
  286. * @return array
  287. * list($captcha_module, $captcha_type)
  288. */
  289. function _captcha_parse_captcha_type($captcha_type) {
  290. if ($captcha_type == 'none') {
  291. return array(NULL, NULL);
  292. }
  293. if ($captcha_type == 'default') {
  294. $captcha_type = variable_get('captcha_default_challenge', 'captcha/Math');
  295. }
  296. return explode('/', $captcha_type);
  297. }
  298. /**
  299. * Helper function to get placement information for a given form_id.
  300. *
  301. * @param string $form_id
  302. * the form_id to get the placement information for.
  303. *
  304. * @param array $form
  305. * if a form corresponding to the given form_id, if there
  306. * is no placement info for the given form_id, this form is examined to
  307. * guess the placement.
  308. *
  309. * @return array
  310. * placement info array (@see _captcha_insert_captcha_element() for more
  311. * info about the fields 'path', 'key' and 'weight'.
  312. */
  313. function _captcha_get_captcha_placement($form_id, $form) {
  314. // Get CAPTCHA placement map from cache. Two levels of cache:
  315. // static variable in this function and storage in the variables table.
  316. static $placement_map = NULL;
  317. // Try first level cache.
  318. if ($placement_map === NULL) {
  319. // If first level cache missed: try second level cache.
  320. $placement_map = variable_get('captcha_placement_map_cache', NULL);
  321. if ($placement_map === NULL) {
  322. // If second level cache missed: initialize the placement map
  323. // and let other modules hook into this with the hook_captcha_placement_map hook.
  324. // By default however, probably all Drupal core forms are already correctly
  325. // handled with the best effort guess based on the 'actions' element (see below).
  326. $placement_map = module_invoke_all('captcha_placement_map');
  327. }
  328. }
  329. // Query the placement map.
  330. if (array_key_exists($form_id, $placement_map)) {
  331. $placement = $placement_map[$form_id];
  332. }
  333. // If no placement info is available in placement map: make a best effort guess.
  334. else {
  335. // If there is an "actions" button group, a good placement is just before that.
  336. if (isset($form['actions']) && isset($form['actions']['#type']) && $form['actions']['#type'] === 'actions') {
  337. $placement = array(
  338. 'path' => array(),
  339. 'key' => 'actions',
  340. // #type 'actions' defaults to 100.
  341. 'weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99),
  342. );
  343. }
  344. else {
  345. // Search the form for buttons and guess placement from it.
  346. $buttons = _captcha_search_buttons($form);
  347. if (count($buttons)) {
  348. // Pick first button.
  349. // TODO: make this more sofisticated? Use cases needed.
  350. $placement = (isset($buttons[count($buttons) - 1])) ? $buttons[count($buttons) - 1] : $buttons[0];
  351. }
  352. else {
  353. // Use NULL when no buttons were found.
  354. $placement = NULL;
  355. }
  356. }
  357. // Store calculated placement in cache.
  358. $placement_map[$form_id] = $placement;
  359. variable_set('captcha_placement_map_cache', $placement_map);
  360. }
  361. return $placement;
  362. }
  363. /**
  364. * Helper function for searching the buttons in a form.
  365. *
  366. * @param array $form
  367. * the form to search button elements in
  368. *
  369. * @return array
  370. * an array of paths to the buttons.
  371. * A path is an array of keys leading to the button, the last
  372. * item in the path is the weight of the button element
  373. * (or NULL if undefined).
  374. */
  375. function _captcha_search_buttons($form) {
  376. $buttons = array();
  377. foreach (element_children($form) as $key) {
  378. // Look for submit or button type elements.
  379. if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) {
  380. $weight = isset($form[$key]['#weight']) ? $form[$key]['#weight'] : NULL;
  381. $buttons[] = array(
  382. 'path' => array(),
  383. 'key' => $key,
  384. 'weight' => $weight,
  385. );
  386. }
  387. // Process children recurively.
  388. $children_buttons = _captcha_search_buttons($form[$key]);
  389. foreach ($children_buttons as $b) {
  390. $b['path'] = array_merge(array($key), $b['path']);
  391. $buttons[] = $b;
  392. }
  393. }
  394. return $buttons;
  395. }
  396. /**
  397. * Helper function to insert a CAPTCHA element in a form before a given form element.
  398. *
  399. * @param array $form
  400. * the form to add the CAPTCHA element to.
  401. *
  402. * @param array $placement
  403. * information where the CAPTCHA element should be inserted.
  404. * $placement should be an associative array with fields:
  405. * - 'path': path (array of path items) of the container in the form where the
  406. * CAPTCHA element should be inserted.
  407. * - 'key': the key of the element before which the CAPTCHA element
  408. * should be inserted. If the field 'key' is undefined or NULL, the CAPTCHA will
  409. * just be appended in the container.
  410. * - 'weight': if 'key' is not NULL: should be the weight of the element defined by 'key'.
  411. * If 'key' is NULL and weight is not NULL: set the weight property of the CAPTCHA element
  412. * to this value.
  413. *
  414. * @param array $captcha_element
  415. * the CAPTCHA element to insert.
  416. */
  417. function _captcha_insert_captcha_element(&$form, $placement, $captcha_element) {
  418. // Get path, target and target weight or use defaults if not available.
  419. $target_key = isset($placement['key']) ? $placement['key'] : NULL;
  420. $target_weight = isset($placement['weight']) ? $placement['weight'] : NULL;
  421. $path = isset($placement['path']) ? $placement['path'] : array();
  422. // Walk through the form along the path.
  423. $form_stepper = &$form;
  424. foreach ($path as $step) {
  425. if (isset($form_stepper[$step])) {
  426. $form_stepper = & $form_stepper[$step];
  427. }
  428. else {
  429. // Given path is invalid: stop stepping and
  430. // continue in best effort (append instead of insert).
  431. $target_key = NULL;
  432. break;
  433. }
  434. }
  435. // If no target is available: just append the CAPTCHA element to the container.
  436. if ($target_key == NULL || !array_key_exists($target_key, $form_stepper)) {
  437. // Optionally, set weight of CAPTCHA element.
  438. if ($target_weight != NULL) {
  439. $captcha_element['#weight'] = $target_weight;
  440. }
  441. $form_stepper['captcha'] = $captcha_element;
  442. }
  443. // If there is a target available: make sure the CAPTCHA element comes right before it.
  444. else {
  445. // If target has a weight: set weight of CAPTCHA element a bit smaller
  446. // and just append the CAPTCHA: sorting will fix the ordering anyway.
  447. if ($target_weight != NULL) {
  448. $captcha_element['#weight'] = $target_weight - .1;
  449. $form_stepper['captcha'] = $captcha_element;
  450. }
  451. else {
  452. // If we can't play with weights: insert the CAPTCHA element at the right position.
  453. // Because PHP lacks a function for this (array_splice() comes close,
  454. // but it does not preserve the key of the inserted element), we do it by hand:
  455. // chop of the end, append the CAPTCHA element and put the end back.
  456. $offset = array_search($target_key, array_keys($form_stepper));
  457. $end = array_splice($form_stepper, $offset);
  458. $form_stepper['captcha'] = $captcha_element;
  459. foreach ($end as $k => $v) {
  460. $form_stepper[$k] = $v;
  461. }
  462. }
  463. }
  464. }