captcha.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. define('CAPTCHA_MIN_TIME', 2); # seconds the form will need to be filled in by a human
  3. //define('CAPTCHA_MAX_TIME', HOUR); # seconds the form will need to be submitted in
  4. /**
  5. * CaptchaBehavior
  6. * NOTES: needs captcha helper
  7. *
  8. * validate passive or active captchas
  9. * active: session-based, db-based or hash-based
  10. * 2009-12-12 ms
  11. */
  12. class CaptchaBehavior extends ModelBehavior {
  13. private $options = array(
  14. 'dummyField' => 'homepage',
  15. 'method' => 'hash',
  16. 'checkSession' => false,
  17. 'checkIp' => false,
  18. 'salt' => '',
  19. 'type' => 'active',
  20. # behaviour only:
  21. 'minTime' => CAPTCHA_MIN_TIME,
  22. 'maxTime' => 0,
  23. 'log' => false,
  24. );
  25. private $dummyField = 'homepage';
  26. private $methods = array('hash', 'db', 'session');
  27. private $method = 'hash';
  28. private $log = false;
  29. private $error = '';
  30. private $internalError = '';
  31. //private $types = array('passive','active','both');
  32. //private $useSession = false;
  33. function setup(&$Model, $settings) {
  34. # bootstrap configs
  35. $configs = (array )Configure::read('Captcha');
  36. if (!empty($configs)) {
  37. $this->options = array_merge($this->options, $configs);
  38. }
  39. # local configs in specific action
  40. if (!empty($settings['minTime'])) {
  41. $this->options['minTime'] = (int)$settings['minTime'];
  42. }
  43. if (!empty($settings['maxTime'])) {
  44. $this->options['maxTime'] = (int)$settings['maxTime'];
  45. }
  46. if (isset($settings['log'])) {
  47. $this->options['log'] = (bool)$settings['log'];
  48. }
  49. /*
  50. better:
  51. if (!isset($this->settings[$Model->alias])) {
  52. $this->settings[$Model->alias] = array(
  53. 'option1_key' => 'option1_default_value'
  54. );
  55. }
  56. $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], (array)$settings);
  57. */
  58. }
  59. public function beforeValidate(&$Model, &$queryData) {
  60. $this->Model = &$Model;
  61. if (!$this->validateCaptchaTime($this->Model->data[$this->Model->name])) {
  62. $this->Model->invalidate('captcha', 'captchaResultTooFast', true);
  63. } elseif (!$this->validateDummyField($this->Model->data[$this->Model->name])) {
  64. $this->Model->invalidate('captcha', 'captchaIllegalContent', true);
  65. } elseif ($this->options['type'] == 'active' && !$this->validateCaptcha($this->Model->data[$this->Model->name])) {
  66. $this->Model->invalidate('captcha', 'captchaResultIncorrect', true);
  67. }
  68. unset($this->Model->data[$this->Model->name]['captcha']);
  69. unset($this->Model->data[$this->Model->name]['captcha_hash']);
  70. unset($this->Model->data[$this->Model->name]['captcha_time']);
  71. return true;
  72. }
  73. /**
  74. * return the current used field names to be passed in whitelist etc
  75. * 2010-01-22 ms
  76. */
  77. public function fields() {
  78. $list = array('captcha', 'captcha_hash', 'captcha_time');
  79. $list[] = $this->options['dummyField'];
  80. return $list;
  81. }
  82. private function validateDummyField($data) {
  83. $dummyField = $this->options['dummyField'];
  84. if (!empty($data[$dummyField])) {
  85. # dummy field not empty - SPAM!
  86. return $this->error('Illegal content', 'DummyField = \''.$data[$dummyField].'\'');
  87. }
  88. return true;
  89. }
  90. /**
  91. * flood protection by time
  92. * TODO: SESSION based one as alternative
  93. */
  94. private function validateCaptchaTime($data) {
  95. if ($this->options['minTime'] <= 0) {
  96. return true;
  97. }
  98. if (empty($data['captcha_hash']) || empty($data['captcha_time']) || $data['captcha_time'] > time() - $this->options['minTime']) {
  99. // trigger error - SPAM!!!
  100. return false;
  101. }
  102. # //TODO: max?
  103. if (false) {
  104. return false;
  105. }
  106. return true;
  107. }
  108. /**
  109. * flood protection by false fields and math code
  110. * TODO: build in floodProtection (max Trials etc)
  111. * TODO: SESSION based one as alternative
  112. */
  113. private function validateCaptcha($data) {
  114. if (!isset($data['captcha'])) {
  115. # form inputs missing? SPAM!
  116. return $this->error('Captcha content missing');
  117. }
  118. $hash = $this->buildHash($data);
  119. if ($data['captcha_hash'] == $hash) {
  120. return true;
  121. }
  122. # wrong captcha content or session expired
  123. return $this->error('Captcha incorrect', 'SubmittedResult = \''.$data['captcha'].'\'');
  124. }
  125. /**
  126. * return error message (or empty string if none)
  127. * @return string
  128. */
  129. public function errors() {
  130. return $this->error;
  131. }
  132. /**
  133. * only neccessary if there is more than one request per model
  134. * 2009-12-18 ms
  135. */
  136. public function reset() {
  137. $this->error = '';
  138. }
  139. /**
  140. * build and log error message
  141. * 2009-12-18 ms
  142. */
  143. private function error($msg = null, $internalMsg = null) {
  144. if (!empty($msg)) {
  145. $this->error = $msg;
  146. }
  147. if (!empty($internalMsg)) {
  148. $this->internalError = $internalMsg;
  149. }
  150. if ($this->log) {
  151. $this->logAttempt();
  152. }
  153. return false;
  154. }
  155. function buildHash($data) {
  156. $hashValue = date(FORMAT_DB_DATE, (int)$data['captcha_time']).'_';
  157. $hashValue .= ($this->options['checkSession'])?session_id().'_' : '';
  158. $hashValue .= ($this->options['checkIp'])?env('REMOTE_ADDR').'_' : '';
  159. $hashValue .= $data['captcha'].'_'.$this->options['salt'];
  160. return Security::hash($hashValue);
  161. }
  162. /**
  163. * logs attempts
  164. * @param bool errorsOnly (only if error occured, otherwise always)
  165. * @returns null if not logged, true otherwise
  166. * 2009-12-18 ms
  167. */
  168. private function logAttempt($errorsOnly = true) {
  169. if ($errorsOnly === true && empty($this->error) && empty($this->internalError)) {
  170. return null;
  171. }
  172. App::import('Component', 'RequestHandler');
  173. $msg = 'Ip \''.RequestHandlerComponent::getClientIP().'\', Agent \''.env('HTTP_USER_AGENT').'\', Referer \''.env('HTTP_REFERER').'\', Host-Referer \''.RequestHandlerComponent::getReferer().'\'';
  174. if (!empty($this->error)) {
  175. $msg .= ', '.$this->error;
  176. }
  177. if (!empty($this->internalError)) {
  178. $msg .= ' ('.$this->internalError.')';
  179. }
  180. $this->log($msg, 'captcha');
  181. return true;
  182. }
  183. }
  184. ?>