captcha.php 5.9 KB

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