CaptchaBehavior.php 6.1 KB

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