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