CaptchaBehavior.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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. App::uses('CaptchaLib', 'Tools.Lib');
  5. /**
  6. * CaptchaBehavior
  7. * NOTES: needs captcha helper
  8. *
  9. * validate passive or active captchas
  10. * active: session-based, db-based or hash-based
  11. * 2009-12-12 ms
  12. */
  13. class CaptchaBehavior extends ModelBehavior {
  14. protected $defaults = array(
  15. 'minTime' => CAPTCHA_MIN_TIME,
  16. 'maxTime' => CAPTCHA_MAX_TIME,
  17. 'log' => false, # log errors
  18. 'hashType' => null,
  19. );
  20. protected $error = '';
  21. protected $internalError = '';
  22. //
  23. //protected $useSession = false;
  24. public function setup(Model $Model, $settings = array()) {
  25. $defaults = array_merge(CaptchaLib::$defaults, $this->defaults);
  26. $this->Model = $Model;
  27. # bootstrap configs
  28. $this->settings[$Model->alias] = $defaults;
  29. $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], (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. if ($this->settings[$this->Model->alias]['dummyField']) {
  73. $list[] = $this->settings[$this->Model->alias]['dummyField'];
  74. }
  75. return $list;
  76. }
  77. protected function _validateDummyField($data) {
  78. $dummyField = $this->settings[$this->Model->alias]['dummyField'];
  79. if (!isset($data[$dummyField])) {
  80. return $this->_setError(__('Illegal call'));
  81. }
  82. if (!empty($data[$dummyField])) {
  83. # dummy field not empty - SPAM!
  84. return $this->_setError(__('Illegal content'), 'DummyField = \''.$data[$dummyField].'\'');
  85. }
  86. return true;
  87. }
  88. /**
  89. * flood protection by time
  90. * TODO: SESSION based one as alternative
  91. */
  92. protected function _validateCaptchaMinTime($data) {
  93. if ($this->settings[$this->Model->alias]['minTime'] <= 0) {
  94. return true;
  95. }
  96. if (isset($data['captcha_hash']) && isset($data['captcha_time'])) {
  97. if ($data['captcha_time'] < time() - $this->settings[$this->Model->alias]['minTime']) {
  98. return true;
  99. }
  100. }
  101. return false;
  102. }
  103. /**
  104. * validates maximum time
  105. *
  106. * @param array $data
  107. * @return bool
  108. */
  109. protected function _validateCaptchaMaxTime($data) {
  110. if ($this->settings[$this->Model->alias]['maxTime'] <= 0) {
  111. return true;
  112. }
  113. if (isset($data['captcha_hash']) && isset($data['captcha_time'])) {
  114. if ($data['captcha_time'] + $this->settings[$this->Model->alias]['maxTime'] > time()) {
  115. return true;
  116. }
  117. }
  118. return false;
  119. }
  120. /**
  121. * flood protection by false fields and math code
  122. * TODO: build in floodProtection (max Trials etc)
  123. * TODO: SESSION based one as alternative
  124. */
  125. protected function _validateCaptcha($data) {
  126. if (!isset($data['captcha'])) {
  127. # form inputs missing? SPAM!
  128. return $this->_setError(__('captchaContentMissing'));
  129. }
  130. $hash = $this->_buildHash($data);
  131. if ($data['captcha_hash'] == $hash) {
  132. return true;
  133. }
  134. # wrong captcha content or session expired
  135. return $this->_setError(__('Captcha incorrect'), 'SubmittedResult = \''.$data['captcha'].'\'');
  136. }
  137. /**
  138. * return error message (or empty string if none)
  139. * @return string
  140. */
  141. public function errors() {
  142. return $this->error;
  143. }
  144. /**
  145. * only neccessary if there is more than one request per model
  146. * 2009-12-18 ms
  147. */
  148. public function reset() {
  149. $this->error = '';
  150. }
  151. /**
  152. * build and log error message
  153. * 2009-12-18 ms
  154. */
  155. protected function _setError($msg = null, $internalMsg = null) {
  156. if (!empty($msg)) {
  157. $this->error = $msg;
  158. }
  159. if (!empty($internalMsg)) {
  160. $this->internalError = $internalMsg;
  161. }
  162. $this->_logAttempt();
  163. return false;
  164. }
  165. protected function _buildHash($data) {
  166. return CaptchaLib::buildHash($data, $this->settings[$this->Model->alias]);
  167. }
  168. /**
  169. * logs attempts
  170. * @param bool errorsOnly (only if error occured, otherwise always)
  171. * @returns null if not logged, true otherwise
  172. * 2009-12-18 ms
  173. */
  174. protected function _logAttempt($errorsOnly = true) {
  175. if ($errorsOnly === true && empty($this->error) && empty($this->internalError)) {
  176. return null;
  177. }
  178. if (!$this->settings[$this->Model->alias]['log']) {
  179. return null;
  180. }
  181. //App::import('Component', 'RequestHandler');
  182. $msg = 'IP \''.CakeRequest::clientIP().'\', Agent \''.env('HTTP_USER_AGENT').'\', Referer \''.env('HTTP_REFERER').'\', Host-Referer \''.CommonComponent::getReferer().'\'';
  183. if (!empty($this->error)) {
  184. $msg .= ', '.$this->error;
  185. }
  186. if (!empty($this->internalError)) {
  187. $msg .= ' ('.$this->internalError.')';
  188. }
  189. $this->log($msg, 'captcha');
  190. return true;
  191. }
  192. }