CaptchaBehavior.php 6.4 KB

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