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