fields.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.fields
  4. ~~~~~~~~~~~~~~~~~~~~
  5. Additional fields and widgets for wtforms.
  6. The reCAPTCHA Field was taken from Flask-WTF and modified
  7. to use our own settings system.
  8. :copyright: (c) 2014 by the FlaskBB Team.
  9. :license: BSD, see LICENSE for more details.
  10. """
  11. from datetime import datetime
  12. import logging
  13. try:
  14. import urllib2 as http
  15. except ImportError:
  16. # Python 3
  17. from urllib import request as http
  18. from flask import request, current_app, Markup, json
  19. from werkzeug.urls import url_encode
  20. from wtforms import ValidationError
  21. from wtforms.fields import DateField, Field
  22. from wtforms.widgets.core import Select, html_params
  23. from flaskbb._compat import to_bytes, to_unicode
  24. from flaskbb.utils.settings import flaskbb_config
  25. logger = logging.getLogger(__name__)
  26. JSONEncoder = json.JSONEncoder
  27. RECAPTCHA_SCRIPT = u'https://www.google.com/recaptcha/api.js'
  28. RECAPTCHA_TEMPLATE = u'''
  29. <script src='%s' async defer></script>
  30. <div class="g-recaptcha" %s></div>
  31. '''
  32. RECAPTCHA_VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify'
  33. RECAPTCHA_ERROR_CODES = {
  34. 'missing-input-secret': 'The secret parameter is missing.',
  35. 'invalid-input-secret': 'The secret parameter is invalid or malformed.',
  36. 'missing-input-response': 'The response parameter is missing.',
  37. 'invalid-input-response': 'The response parameter is invalid or malformed.'
  38. }
  39. class RecaptchaValidator(object):
  40. """Validates a ReCaptcha."""
  41. def __init__(self, message=None):
  42. if message is None:
  43. message = RECAPTCHA_ERROR_CODES['missing-input-response']
  44. self.message = message
  45. def __call__(self, form, field):
  46. if current_app.testing or not flaskbb_config["RECAPTCHA_ENABLED"]:
  47. return True
  48. if request.json:
  49. response = request.json.get('g-recaptcha-response', '')
  50. else:
  51. response = request.form.get('g-recaptcha-response', '')
  52. remote_ip = request.remote_addr
  53. if not response:
  54. raise ValidationError(field.gettext(self.message))
  55. if not self._validate_recaptcha(response, remote_ip):
  56. field.recaptcha_error = 'incorrect-captcha-sol'
  57. raise ValidationError(field.gettext(self.message))
  58. def _validate_recaptcha(self, response, remote_addr):
  59. """Performs the actual validation."""
  60. try:
  61. private_key = flaskbb_config['RECAPTCHA_PRIVATE_KEY']
  62. except KeyError:
  63. raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set")
  64. data = url_encode({
  65. 'secret': private_key,
  66. 'remoteip': remote_addr,
  67. 'response': response
  68. })
  69. http_response = http.urlopen(RECAPTCHA_VERIFY_SERVER, to_bytes(data))
  70. if http_response.code != 200:
  71. return False
  72. json_resp = json.loads(to_unicode(http_response.read()))
  73. if json_resp["success"]:
  74. return True
  75. for error in json_resp.get("error-codes", []):
  76. if error in RECAPTCHA_ERROR_CODES:
  77. raise ValidationError(RECAPTCHA_ERROR_CODES[error])
  78. return False
  79. class RecaptchaWidget(object):
  80. def recaptcha_html(self, public_key):
  81. html = current_app.config.get('RECAPTCHA_HTML')
  82. if html:
  83. return Markup(html)
  84. params = current_app.config.get('RECAPTCHA_PARAMETERS')
  85. script = RECAPTCHA_SCRIPT
  86. if params:
  87. script += u'?' + url_encode(params)
  88. attrs = current_app.config.get('RECAPTCHA_DATA_ATTRS', {})
  89. attrs['sitekey'] = public_key
  90. snippet = u' '.join([u'data-%s="%s"' % (k, attrs[k]) for k in attrs])
  91. return Markup(RECAPTCHA_TEMPLATE % (script, snippet))
  92. def __call__(self, field, error=None, **kwargs):
  93. """Returns the recaptcha input HTML."""
  94. if not flaskbb_config["RECAPTCHA_ENABLED"]:
  95. return
  96. try:
  97. public_key = flaskbb_config['RECAPTCHA_PUBLIC_KEY']
  98. except KeyError:
  99. raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set")
  100. return self.recaptcha_html(public_key)
  101. class RecaptchaField(Field):
  102. widget = RecaptchaWidget()
  103. # error message if recaptcha validation fails
  104. recaptcha_error = None
  105. def __init__(self, label='', validators=None, **kwargs):
  106. validators = validators or [RecaptchaValidator()]
  107. super(RecaptchaField, self).__init__(label, validators, **kwargs)
  108. class SelectBirthdayWidget(object):
  109. """Renders a DateTime field with 3 selects.
  110. For more information see: http://stackoverflow.com/a/14664504
  111. """
  112. FORMAT_CHOICES = {
  113. '%d': [(x, str(x)) for x in range(1, 32)],
  114. '%m': [(x, str(x)) for x in range(1, 13)]
  115. }
  116. FORMAT_CLASSES = {
  117. '%d': 'select_date_day',
  118. '%m': 'select_date_month',
  119. '%Y': 'select_date_year'
  120. }
  121. def __init__(self, years=None):
  122. """Initialzes the widget.
  123. :param years: The min year which should be chooseable.
  124. Defatuls to ``1930``.
  125. """
  126. if years is None:
  127. years = range(1930, datetime.utcnow().year + 1)
  128. super(SelectBirthdayWidget, self).__init__()
  129. self.FORMAT_CHOICES['%Y'] = [(x, str(x)) for x in years]
  130. # TODO(anr): clean up
  131. def __call__(self, field, **kwargs): # noqa: C901
  132. field_id = kwargs.pop('id', field.id)
  133. html = []
  134. allowed_format = ['%d', '%m', '%Y']
  135. surrounded_div = kwargs.pop('surrounded_div', None)
  136. css_class = kwargs.get('class', None)
  137. for date_format in field.format.split():
  138. if date_format in allowed_format:
  139. choices = self.FORMAT_CHOICES[date_format]
  140. id_suffix = date_format.replace('%', '-')
  141. id_current = field_id + id_suffix
  142. if css_class is not None: # pragma: no cover
  143. select_class = "{} {}".format(
  144. css_class, self.FORMAT_CLASSES[date_format]
  145. )
  146. else:
  147. select_class = self.FORMAT_CLASSES[date_format]
  148. kwargs['class'] = select_class
  149. try:
  150. del kwargs['placeholder']
  151. except KeyError:
  152. pass
  153. if surrounded_div is not None:
  154. html.append('<div class="%s">' % surrounded_div)
  155. html.append('<select %s>' % html_params(name=field.name,
  156. id=id_current,
  157. **kwargs))
  158. if field.data:
  159. current_value = int(field.data.strftime(date_format))
  160. else:
  161. current_value = None
  162. for value, label in choices:
  163. selected = (value == current_value)
  164. # Defaults to blank
  165. if value == 1 or value == 1930:
  166. html.append(
  167. Select.render_option("None", " ", selected)
  168. )
  169. html.append(Select.render_option(value, label, selected))
  170. html.append('</select>')
  171. if surrounded_div is not None:
  172. html.append("</div>")
  173. html.append(' ')
  174. return Markup(''.join(html))
  175. class BirthdayField(DateField):
  176. """Same as DateField, except it allows ``None`` values in case a user
  177. wants to delete his birthday.
  178. """
  179. widget = SelectBirthdayWidget()
  180. def __init__(self, label=None, validators=None, format='%Y-%m-%d',
  181. **kwargs):
  182. DateField.__init__(self, label, validators, format, **kwargs)
  183. def process_formdata(self, valuelist):
  184. if valuelist:
  185. date_str = ' '.join(valuelist)
  186. try:
  187. self.data = datetime.strptime(date_str, self.format).date()
  188. except ValueError:
  189. self.data = None
  190. # Only except the None value if all values are None.
  191. # A bit dirty though
  192. if valuelist != ["None", "None", "None"]:
  193. raise ValueError("Not a valid date value")