captcha.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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. * Parse or interpret the given captcha_type.
  253. *
  254. * @param string $captcha_type
  255. * string representation of the CAPTCHA type,
  256. * e.g. 'default', 'none', 'captcha/Math', 'image_captcha/Image'
  257. *
  258. * @return array
  259. * list($captcha_module, $captcha_type)
  260. */
  261. function _captcha_parse_captcha_type($captcha_type) {
  262. if ($captcha_type == 'none') {
  263. return array(NULL, NULL);
  264. }
  265. if ($captcha_type == 'default') {
  266. $captcha_type = variable_get('captcha_default_challenge', 'captcha/Math');
  267. }
  268. return explode('/', $captcha_type);
  269. }
  270. /**
  271. * Helper function to get placement information for a given form_id.
  272. *
  273. * @param string $form_id
  274. * the form_id to get the placement information for.
  275. *
  276. * @param array $form
  277. * if a form corresponding to the given form_id, if there
  278. * is no placement info for the given form_id, this form is examined to
  279. * guess the placement.
  280. *
  281. * @return array
  282. * placement info array (@see _captcha_insert_captcha_element() for more
  283. * info about the fields 'path', 'key' and 'weight'.
  284. */
  285. function _captcha_get_captcha_placement($form_id, $form) {
  286. // Get CAPTCHA placement map from cache. Two levels of cache:
  287. // static variable in this function and storage in the variables table.
  288. static $placement_map = NULL;
  289. // Try first level cache.
  290. if ($placement_map === NULL) {
  291. // If first level cache missed: try second level cache.
  292. $placement_map = variable_get('captcha_placement_map_cache', NULL);
  293. if ($placement_map === NULL) {
  294. // If second level cache missed: initialize the placement map
  295. // and let other modules hook into this with the hook_captcha_placement_map hook.
  296. // By default however, probably all Drupal core forms are already correctly
  297. // handled with the best effort guess based on the 'actions' element (see below).
  298. $placement_map = module_invoke_all('captcha_placement_map');
  299. }
  300. }
  301. // Query the placement map.
  302. if (array_key_exists($form_id, $placement_map)) {
  303. $placement = $placement_map[$form_id];
  304. }
  305. // If no placement info is available in placement map: make a best effort guess.
  306. else {
  307. // If there is an "actions" button group, a good placement is just before that.
  308. if (isset($form['actions']) && isset($form['actions']['#type']) && $form['actions']['#type'] === 'actions') {
  309. $placement = array(
  310. 'path' => array(),
  311. 'key' => 'actions',
  312. // #type 'actions' defaults to 100.
  313. 'weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99),
  314. );
  315. }
  316. else {
  317. // Search the form for buttons and guess placement from it.
  318. $buttons = _captcha_search_buttons($form);
  319. if (count($buttons)) {
  320. // Pick first button.
  321. // TODO: make this more sofisticated? Use cases needed.
  322. $placement = $buttons[0];
  323. }
  324. else {
  325. // Use NULL when no buttons were found.
  326. $placement = NULL;
  327. }
  328. }
  329. // Store calculated placement in cache.
  330. $placement_map[$form_id] = $placement;
  331. variable_set('captcha_placement_map_cache', $placement_map);
  332. }
  333. return $placement;
  334. }
  335. /**
  336. * Helper function for searching the buttons in a form.
  337. *
  338. * @param array $form
  339. * the form to search button elements in
  340. *
  341. * @return array
  342. * an array of paths to the buttons.
  343. * A path is an array of keys leading to the button, the last
  344. * item in the path is the weight of the button element
  345. * (or NULL if undefined).
  346. */
  347. function _captcha_search_buttons($form) {
  348. $buttons = array();
  349. foreach (element_children($form) as $key) {
  350. // Look for submit or button type elements.
  351. if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) {
  352. $weight = isset($form[$key]['#weight']) ? $form[$key]['#weight'] : NULL;
  353. $buttons[] = array(
  354. 'path' => array(),
  355. 'key' => $key,
  356. 'weight' => $weight,
  357. );
  358. }
  359. // Process children recurively.
  360. $children_buttons = _captcha_search_buttons($form[$key]);
  361. foreach ($children_buttons as $b) {
  362. $b['path'] = array_merge(array($key), $b['path']);
  363. $buttons[] = $b;
  364. }
  365. }
  366. return $buttons;
  367. }
  368. /**
  369. * Helper function to insert a CAPTCHA element in a form before a given form element.
  370. *
  371. * @param array $form
  372. * the form to add the CAPTCHA element to.
  373. *
  374. * @param array $placement
  375. * information where the CAPTCHA element should be inserted.
  376. * $placement should be an associative array with fields:
  377. * - 'path': path (array of path items) of the container in the form where the
  378. * CAPTCHA element should be inserted.
  379. * - 'key': the key of the element before which the CAPTCHA element
  380. * should be inserted. If the field 'key' is undefined or NULL, the CAPTCHA will
  381. * just be appended in the container.
  382. * - 'weight': if 'key' is not NULL: should be the weight of the element defined by 'key'.
  383. * If 'key' is NULL and weight is not NULL: set the weight property of the CAPTCHA element
  384. * to this value.
  385. *
  386. * @param array $captcha_element
  387. * the CAPTCHA element to insert.
  388. */
  389. function _captcha_insert_captcha_element(&$form, $placement, $captcha_element) {
  390. // Get path, target and target weight or use defaults if not available.
  391. $target_key = isset($placement['key']) ? $placement['key'] : NULL;
  392. $target_weight = isset($placement['weight']) ? $placement['weight'] : NULL;
  393. $path = isset($placement['path']) ? $placement['path'] : array();
  394. // Walk through the form along the path.
  395. $form_stepper = &$form;
  396. foreach ($path as $step) {
  397. if (isset($form_stepper[$step])) {
  398. $form_stepper = & $form_stepper[$step];
  399. }
  400. else {
  401. // Given path is invalid: stop stepping and
  402. // continue in best effort (append instead of insert).
  403. $target_key = NULL;
  404. break;
  405. }
  406. }
  407. // If no target is available: just append the CAPTCHA element to the container.
  408. if ($target_key == NULL || !array_key_exists($target_key, $form_stepper)) {
  409. // Optionally, set weight of CAPTCHA element.
  410. if ($target_weight != NULL) {
  411. $captcha_element['#weight'] = $target_weight;
  412. }
  413. $form_stepper['captcha'] = $captcha_element;
  414. }
  415. // If there is a target available: make sure the CAPTCHA element comes right before it.
  416. else {
  417. // If target has a weight: set weight of CAPTCHA element a bit smaller
  418. // and just append the CAPTCHA: sorting will fix the ordering anyway.
  419. if ($target_weight != NULL) {
  420. $captcha_element['#weight'] = $target_weight - .1;
  421. $form_stepper['captcha'] = $captcha_element;
  422. }
  423. else {
  424. // If we can't play with weights: insert the CAPTCHA element at the right position.
  425. // Because PHP lacks a function for this (array_splice() comes close,
  426. // but it does not preserve the key of the inserted element), we do it by hand:
  427. // chop of the end, append the CAPTCHA element and put the end back.
  428. $offset = array_search($target_key, array_keys($form_stepper));
  429. $end = array_splice($form_stepper, $offset);
  430. $form_stepper['captcha'] = $captcha_element;
  431. foreach ($end as $k => $v) {
  432. $form_stepper[$k] = $v;
  433. }
  434. }
  435. }
  436. }