Changeset 18083
- Timestamp:
- 7 Jun 2025, 02:06:59 (35 hours ago)
- Location:
- main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa
- Files:
-
- 9 edited
Legend:
- Unmodified
- Added
- Removed
-
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/applicants/browser.py
r18021 r18083 50 50 from waeup.kofa.applicants.applicant import search 51 51 from waeup.kofa.applicants.workflow import ( 52 INITIALIZED, STARTED, PAID, SUBMITTED, 52 INITIALIZED, STARTED, PAID, SUBMITTED, 53 53 ADMITTED, NOT_ADMITTED, CREATED, PROCESSED) 54 54 from waeup.kofa.browser import ( … … 843 843 def render(self): 844 844 return 845 845 846 846 class BalancePaymentAddFormPage(KofaAddFormPage): 847 847 """ Page to add an online payment which can balance s previous session … … 1078 1078 manage_applications = True 1079 1079 pnav = 3 1080 1080 1081 1081 @property 1082 1082 def display_actions(self): … … 1212 1212 if self.upload_success is False: # False is not None! 1213 1213 # Error during image upload. Ignore other values. 1214 return 1214 return 1215 1215 changed_fields = self.applyData(self.context, **data) 1216 1216 # Turn list of lists into single list … … 1578 1578 # Handle captcha 1579 1579 self.captcha = getUtility(ICaptchaManager).getCaptcha() 1580 self.captcha_result = self.captcha.verify(self.request) 1581 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 1580 self.captcha_result = self.captcha.verify( 1581 self.request, 'applicant_register') 1582 self.captcha_code = self.captcha.display( 1583 self.captcha_result.error_code, 'applicant_register') 1582 1584 return 1583 1585 … … 1707 1709 # Handle captcha 1708 1710 self.captcha = getUtility(ICaptchaManager).getCaptcha() 1709 self.captcha_result = self.captcha.verify(self.request) 1710 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 1711 self.captcha_result = self.captcha.verify( 1712 self.request, 'check_appl_status') 1713 self.captcha_code = self.captcha.display( 1714 self.captcha_result.error_code, 'check_appl_status') 1711 1715 if SUBMIT: 1712 1716 if not self.captcha_result.is_valid: … … 1792 1796 # Handle captcha 1793 1797 self.captcha = getUtility(ICaptchaManager).getCaptcha() 1794 self.captcha_result = self.captcha.verify(self.request) 1795 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 1798 self.captcha_result = self.captcha.verify( 1799 self.request, 'check_trans_state') 1800 self.captcha_code = self.captcha.display( 1801 self.captcha_result.error_code, 'check_trans_state') 1796 1802 if SUBMIT: 1797 1803 self.results = [] … … 2087 2093 args = {'mandate_id':mandate.mandate_id} 2088 2094 # Check if report exists. 2089 # (1) If mandate has been used to create a report, 2095 # (1) If mandate has been used to create a report, 2090 2096 # redirect to the pdf file. 2091 2097 if mandate.params.get('redirect_path2'): -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/browser/captcha.py
r15012 r18083 21 21 """ 22 22 import grok 23 import decimal 23 24 import json 25 import logging 24 26 import urllib 25 27 import urllib2 … … 33 35 ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig, 34 36 ICaptchaManager) 35 from waeup.kofa.interfaces import IUniversity 37 from waeup.kofa.interfaces import IUniversity, IReCaptchaConfig 36 38 37 39 # … … 103 105 grok.implements(ICaptcha) 104 106 105 def verify(self, request ):107 def verify(self, request, action="submit"): 106 108 return CaptchaResponse(True, None) 107 109 108 def display(self, error_code=None ):110 def display(self, error_code=None, action="submit"): 109 111 return u'' 110 112 … … 139 141 chal_field = 'challenge' 140 142 141 def verify(self, request ):143 def verify(self, request, action='submit'): 142 144 """Verify that a solution sent equals the challenge. 143 145 """ … … 149 151 return CaptchaResponse(is_valid=False) 150 152 151 def display(self, error_code=None ):153 def display(self, error_code=None, action='submit'): 152 154 """Display challenge and input field for solution as HTML. 153 155 """ … … 164 166 165 167 ## 166 ## ReCaptcha (v 2)168 ## ReCaptcha (v3) 167 169 ## 168 170 class ReCaptcha(StaticCaptcha): … … 170 172 171 173 This is the Kofa implementation to support captchas as provided by 172 http://www.google.com/recaptcha (v 2).174 http://www.google.com/recaptcha (v3). 173 175 174 176 ReCaptcha is widely used and adopted in web applications. See the … … 226 228 - Google will track all activity of our clients 227 229 """ 228 229 230 grok.implements(ICaptcha) 230 231 … … 240 241 VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify' 241 242 242 def verify(self, request): 243 @property 244 def sitekey(self): 245 return self.get_config()['public_key'] 246 247 @property 248 def logger(self): 249 # if there is no site, we are most probably in a testing env 250 return getattr(grok.getSite(), "logger", logging.getLogger("dummy")) 251 252 @property 253 def ob_class(self): 254 return self.__implemented__.__name__.replace('waeup.kofa.', '') 255 256 def get_config(self): 257 if not getattr(self, "config", None): 258 self.config = queryUtility(IReCaptchaConfig, default={ 259 'public_key': self.PUBLIC_KEY, 260 'private_key': self.PRIVATE_KEY, 261 'hostname': u'localhost', 262 'min_score': decimal.Decimal("0.5") 263 }) 264 self.logger.debug(u'%s : getting ReCaptchaConfig: %s' % ( 265 self.ob_class, self.config)) 266 return self.config 267 268 def verify(self, request, action='submit'): 243 269 """Grab challenge/solution from HTTP request and verify it. 244 270 … … 249 275 Returns a :class:`CaptchaResponse` indicating that the 250 276 verification failed or succeeded. 251 """ 277 278 Possible error codes (official Google codes): 279 missing-input-secret The secret parameter is missing. 280 invalid-input-secret The secret parameter is invalid or malformed. 281 missing-input-response The response parameter is missing. 282 invalid-input-response The response parameter is invalid or malformed. 283 bad-request The request is invalid or malformed. 284 timeout-or-duplicate The response is no longer valid: either is too old or has been used previously. 285 We also have our own ones: 286 invalid-action The passed action does not match the one we expected 287 insufficient-score The returned score is too low; talking to a bot 288 invalid-hostname The captcha was solved on a different domain. 289 """ 290 remote_ip = request.get('HTTP_X_FORWARDED_FOR', '127.0.0.1') 252 291 form = getattr(request, 'form', {}) 253 292 token = form.get(self.token_field, None) 293 oc = self.ob_class 294 self.logger.debug(u'%s: get token from form: %s' % (oc, token)) 254 295 if not token: 255 296 # on first-time display, we won't get a token 256 297 return CaptchaResponse(is_valid=False) 298 conf = self.get_config() 257 299 params = urllib.urlencode( 258 300 { 259 'secret': self.PRIVATE_KEY,301 'secret': conf['private_key'], 260 302 'response': token, 261 'remoteip': '127.0.0.1',303 'remoteip': remote_ip, 262 304 }) 263 request = urllib2.Request( 305 self.logger.debug(u'%s: send validation request to Google' % oc) 306 v_request = urllib2.Request( 264 307 url = self.VERIFY_URL, 265 308 data = params, … … 269 312 } 270 313 ) 271 conn = urllib2.urlopen(request) 272 ret_vals = json.loads(conn.read()) 273 conn.close() 314 try: 315 conn = urllib2.urlopen(v_request) 316 ret_vals = json.loads(conn.read()) 317 except: 318 conn.close() 319 return CaptchaResponse(is_valid=False, error_code="could-not-reach-verification-server") 320 self.logger.debug(u'%s: got answer from Google: %s' % (oc, ret_vals)) 321 err_codes = ret_vals.get('error-codes', []) 274 322 if ret_vals.get('success', False) is True: 275 return CaptchaResponse(is_valid=True) 323 r_action = ret_vals.get('action', None) 324 score = ret_vals.get('score', decimal.Decimal("0.0")) 325 hostname = ret_vals.get('hostname', '') 326 if r_action != action: 327 err_codes.append('invalid-action') 328 self.logger.warn( 329 u'%s: invalid action code: %s (expected: %s)' % 330 (oc, r_action, action)) 331 elif score < conf['min_score']: 332 err_codes.append('insufficent-score') 333 self.logger.debug(u'%s: insufficient score: %s' % (oc, score)) 334 elif not hostname.endswith(conf['hostname']): 335 err_codes.append('invalid-hostname') 336 self.logger.warn( 337 u'%s: cought captcha from illegal hostname: %s' % ( 338 oc, hostname)) 339 else: 340 self.logger.debug(u'%s: captcha accepted' % oc) 341 return CaptchaResponse(is_valid=True) 342 self.logger.info(u'%s: captcha invalid/refused, form-data: %s' % ( 343 oc, form)) 276 344 return CaptchaResponse( 277 is_valid=False, error_code="%s" % ret_vals['error-codes'])278 279 def display(self, error_code=None ):345 is_valid=False, error_code="%s" % err_codes) 346 347 def display(self, error_code=None, action='submit'): 280 348 """Display captcha widget snippet. 281 349 … … 290 358 form). 291 359 """ 292 error_param = '' 360 html = u'' 361 # TODO: display errors as warnings 293 362 if error_code: 294 error_param = '&error=%s' % error_code 295 296 html = ( 297 u'<script type="text/javascript" ' 298 u'src="%(ApiServer)s" async defer>' 363 html = u"<p class='warning'>Captcha-Error: Are you a bot?</p><!-- %s -->" % error_code 364 html += ( 365 # Main event handler, triggered when a submit button is clicked. 366 # Requests the captcha token for the form and also copies name and 367 # value of the clicked button to a hidden field `fakebtn` defined 368 # below. 369 u'<script class="kofa-script-recaptcha-v3" ' 370 u' src="https://www.google.com/recaptcha/api.js?render=%(sitekey)s">' 371 u'</script>\n' 372 u'<script class="kofa-script-recaptcha-v3"> ' 373 u' function onClickRecaptcha(e) {' 374 u' e.preventDefault();' 375 u' grecaptcha.ready(function() {' 376 u' grecaptcha.execute("%(sitekey)s", {action: "%(action)s"}).then(' 377 u' function(token) {' 378 u' document.getElementById("g-recaptcha-token").value = token;' 379 u' var fakebtn = document.getElementById("g-recaptcha-fakebtn");' 380 u' fakebtn.name = e.target.name;' 381 u' fakebtn.value = e.target.value;' 382 u' document.querySelector("form").submit();});});}' 383 u'</script>\n' 384 ) % { 385 'sitekey': self.sitekey, 386 'action': action, 387 } 388 html += ( 389 # Form fields we use to (1) embed the captcha token and (2) a field 390 # to embed the name and value of the submit button/input clicked. 391 u'<input id="g-recaptcha-token" type="hidden" name="%(token_field)s"/>\n' 392 u'<input id="g-recaptcha-fakebtn" type="hidden"/>\n' 393 ) % {'token_field': self.token_field} 394 html += ( 395 # install event handler `onClickRecaptcha` defined above, but only 396 # after document (HTML) loaded. 397 u'<script> window.addEventListener("load", function () {' 398 u' if (typeof onClickRecaptcha === "function") {' 399 u'document.querySelector("*[type=\'submit\']").addEventListener(' 400 u'"click", onClickRecaptcha);}})' 299 401 u'</script>' 300 u'<div class="g-recaptcha" data-sitekey="%(SiteKey)s"></div>' % { 301 'ApiServer': "https://www.google.com/recaptcha/api.js", 302 'SiteKey': self.PUBLIC_KEY, 303 } 304 ) 402 ) 305 403 return html 306 404 -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/browser/interfaces.py
r13089 r18083 90 90 class ICaptcha(Interface): 91 91 92 def verify(request ):92 def verify(request, action='submit'): 93 93 """Verify data entered in an HTTP request. 94 94 … … 97 97 not. 98 98 99 `action` is a simple string identifying the kind of operation 100 performed in the form. ``login`` or ``enquiry`` for instance. 101 Only used by recaptcha. 102 99 103 If the solution could not be verified (this might also happen 100 104 because of technical reasons), the response might contain an … … 102 106 """ 103 107 104 def display(error_code=None ):108 def display(error_code=None, action='submit'): 105 109 """Returns a piece of HTML code that displays the captcha. 106 110 … … 111 115 therefore should not contain a ``<form>`` nor any submit 112 116 buttons. 117 118 See `verify` for infos about the `action` parameter. 113 119 """ 114 120 -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/browser/pages.py
r18074 r18083 367 367 # Handle captcha 368 368 self.captcha = getUtility(ICaptchaManager).getCaptcha() 369 self.captcha_result = self.captcha.verify(self.request) 370 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 369 self.captcha_result = self.captcha.verify(self.request, 'login') 370 self.captcha_code = self.captcha.display( 371 self.captcha_result.error_code, 'login') 371 372 self.camefrom = camefrom 372 373 # Prefill form with URL params … … 456 457 return 457 458 # Display appropriate flash message if credentials are correct 458 # but student has been deactivated or a temporary or parents 459 # but student has been deactivated or a temporary or parents 459 460 # password has been set. 460 461 login = self.request.form['form.login'] … … 622 623 # Handle captcha 623 624 self.captcha = getUtility(ICaptchaManager).getCaptcha() 624 self.captcha_result = self.captcha.verify(self.request) 625 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 625 self.captcha_result = self.captcha.verify(self.request, 'enquiry') 626 self.captcha_code = self.captcha.display( 627 self.captcha_result.error_code, 'enquiry') 626 628 return 627 629 … … 2076 2078 if ena: 2077 2079 self.flash(ena, type='danger') 2078 return 2080 return 2079 2081 job_id = self.context.start_export_job( 2080 2082 exporter, self.request.principal.id) … … 2860 2862 # Handle captcha 2861 2863 self.captcha = getUtility(ICaptchaManager).getCaptcha() 2862 self.captcha_result = self.captcha.verify(self.request) 2863 self.captcha_code = self.captcha.display(self.captcha_result.error_code) 2864 self.captcha_result = self.captcha.verify(self.request, 'reset_pwd') 2865 self.captcha_code = self.captcha.display( 2866 self.captcha_result.error_code, 'reset_pwd') 2864 2867 # Unset default values maybe set by another person who used this form. 2865 2868 self.form_fields.get('identifier').field.default = None -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/browser/tests/test_captcha.py
r15012 r18083 171 171 result = captcha.display() 172 172 self.assertMatches( 173 '<script type="text/javascript" ' 174 'src="..." async defer></script>' 175 '<div class="g-recaptcha" data-sitekey="..."></div>', 173 '<script class="kofa-script-recaptcha-v3" ...</script>...' 174 '<input id="g-recaptcha-token" .../>...' 175 '<input id="g-recaptcha-fakebtn" .../>...' 176 '<script>...</script>', 176 177 result) 177 178 return -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/interfaces.py
r17787 r18083 16 16 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 17 ## 18 import decimal 18 19 import os 19 20 import re … … 1418 1419 ) 1419 1420 1421 1422 class IReCaptchaConfig(Interface): 1423 """The configuration values needed by Google recaptcha. 1424 """ 1425 public_key = schema.TextLine( 1426 title = u"Public site key", 1427 description = u"Public sitekey, provided by Google", 1428 required = True, 1429 ) 1430 private_key = schema.TextLine( 1431 title = u"Private site key", 1432 description = u"Private sitekey, provided by Google", 1433 required = True, 1434 ) 1435 hostname = schema.TextLine( 1436 title = u"Hostname", 1437 description = u"Hostname we use when we require captchas" 1438 u"i.e. our own domain", 1439 required = True, 1440 default = u"localhost", 1441 ) 1442 min_score = schema.Decimal( 1443 title = u"Minimum score", 1444 description = u"Minimum score required to qualify as non-bot " 1445 u"(range: 0.0 to 1.0)", 1446 required = True, 1447 default = decimal.Decimal("0.5") 1448 ) 1449 1450 1420 1451 # 1421 1452 # Asynchronous job handling and related -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/meta.zcml
r7811 r18083 17 17 /> 18 18 19 <meta:directive 20 namespace="http://namespaces.waeup.org/kofa" 21 name="recaptcha" 22 schema=".zcml.IReCaptchaConfig" 23 handler=".zcml.recaptcha_conf" 24 /> 19 25 20 26 </configure> -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/utils/logger.py
r13776 r18083 86 86 87 87 #: Default logging level (`logging.INFO`) 88 LEVEL = logging. INFO88 LEVEL = logging.DEBUG 89 89 90 90 class ILogger(Interface): -
main/waeup.kofa/branches/uli-recaptcha/src/waeup/kofa/zcml.py
r8057 r18083 17 17 ## 18 18 from zope.component.zcml import handler 19 from waeup.kofa.interfaces import IDataCenterConfig 19 from waeup.kofa.interfaces import IDataCenterConfig, IReCaptchaConfig 20 20 21 21 def data_center_conf(context, path): … … 49 49 {'path':path}, IDataCenterConfig, '') 50 50 ) 51 52 53 def recaptcha_conf(context, public_key, private_key, hostname, min_score): 54 """Handler for ZCML ``recaptcha`` directive. 55 56 Registers a global utility under IReCaptchaConfig and containing a 57 dictionary with entries: `enabled`, `public_key`, `private__key` and `url`. 58 59 The directive can be put into site.zcml like this: 60 61 - Add to the header: 62 ``xmlns:kofa="http://namespaces.waeup.org/kofa"`` 63 64 - Then, after including waeup.kofa: 65 ``<kofa:recaptcha 66 public_key="<YOUR_PUBKEY_HERE>" 67 private_key="<YOUR_PRIVKEY_HERE>" 68 hostname="mysite.com" 69 min_score="0.7" 70 />`` 71 72 In a running instance (where some directive like above was 73 processed during startup), one can then ask for the 74 IReCaptchaConfig utility: 75 76 >>> from waeup.kofa.interfaces import IReCaptchaConfig 77 >>> from zope.component import getUtility 78 >>> getUtility(IRecaptchaConfig) 79 {'enabled': False, 80 'public_key': '<GOOGLE_SITEKEY', 81 'private_key': 'GOOGLE_PRIVATE_SITE_KEY', 82 'hostname': 'mydomain.com' 83 'min_score': Decimal(0.7)} 84 85 """ 86 context.action( 87 discriminator = ('utility', IReCaptchaConfig, ''), 88 callable = handler, 89 args = ('registerUtility', 90 { 91 'public_key': public_key, 92 'private_key': private_key, 93 'hostname': hostname, 94 'min_score': min_score 95 }, IReCaptchaConfig, '') 96 )
Note: See TracChangeset for help on using the changeset viewer.