fields.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 import url_encode
  20. from wtforms import ValidationError
  21. from wtforms.fields import DateField, Field
  22. from wtforms.widgets.core import Select, HTMLString, 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=range(1930, datetime.utcnow().year + 1)):
  122. """Initialzes the widget.
  123. :param years: The min year which should be chooseable.
  124. Defatuls to ``1930``.
  125. """
  126. super(SelectBirthdayWidget, self).__init__()
  127. self.FORMAT_CHOICES['%Y'] = [(x, str(x)) for x in years]
  128. def __call__(self, field, **kwargs):
  129. field_id = kwargs.pop('id', field.id)
  130. html = []
  131. allowed_format = ['%d', '%m', '%Y']
  132. surrounded_div = kwargs.pop('surrounded_div', None)
  133. css_class = kwargs.get('class', None)
  134. for date_format in field.format.split():
  135. if date_format in allowed_format:
  136. choices = self.FORMAT_CHOICES[date_format]
  137. id_suffix = date_format.replace('%', '-')
  138. id_current = field_id + id_suffix
  139. if css_class is not None: # pragma: no cover
  140. select_class = "{} {}".format(
  141. css_class, self.FORMAT_CLASSES[date_format]
  142. )
  143. else:
  144. select_class = self.FORMAT_CLASSES[date_format]
  145. kwargs['class'] = select_class
  146. try:
  147. del kwargs['placeholder']
  148. except KeyError:
  149. pass
  150. if surrounded_div is not None:
  151. html.append('<div class="%s">' % surrounded_div)
  152. html.append('<select %s>' % html_params(name=field.name,
  153. id=id_current,
  154. **kwargs))
  155. if field.data:
  156. current_value = int(field.data.strftime(date_format))
  157. else:
  158. current_value = None
  159. for value, label in choices:
  160. selected = (value == current_value)
  161. # Defaults to blank
  162. if value == 1 or value == 1930:
  163. html.append(
  164. Select.render_option("None", " ", selected)
  165. )
  166. html.append(Select.render_option(value, label, selected))
  167. html.append('</select>')
  168. if surrounded_div is not None:
  169. html.append("</div>")
  170. html.append(' ')
  171. return HTMLString(''.join(html))
  172. class BirthdayField(DateField):
  173. """Same as DateField, except it allows ``None`` values in case a user
  174. wants to delete his birthday.
  175. """
  176. widget = SelectBirthdayWidget()
  177. def __init__(self, label=None, validators=None, format='%Y-%m-%d',
  178. **kwargs):
  179. DateField.__init__(self, label, validators, format, **kwargs)
  180. def process_formdata(self, valuelist):
  181. if valuelist:
  182. date_str = ' '.join(valuelist)
  183. try:
  184. self.data = datetime.strptime(date_str, self.format).date()
  185. except ValueError:
  186. self.data = None
  187. # Only except the None value if all values are None.
  188. # A bit dirty though
  189. if valuelist != ["None", "None", "None"]:
  190. raise ValueError("Not a valid date value")