diff --git a/CHANGESET.md b/CHANGESET.md index 62560acc..5d205d35 100644 --- a/CHANGESET.md +++ b/CHANGESET.md @@ -1,8 +1,15 @@ -Version 0.9.0 - 2013-10-20 +Version 0.9.0 - tbd -------------------------- +- Updated Flask-WTF to 0.9.3 - Introduced `auth_ids` in the `User` model instead of separete auth ids per provider - To upgrade the existing user data deploy the `gae-init-migrate` to your GAE app and run the upgrade task +### Upgrading from older versions to 0.9.0: + +You need to import fields/validators from `wtforms` and `wtforms.validators` +directly as they are no longer part of the Flask-WTF package since v0.9.0: +https://flask-wtf.readthedocs.org/en/latest/upgrade.html#version-0-9-0 + Version 0.8.4 - 2013-10-19 -------------------------- - Added `rel='nofollow'` wherever applicable diff --git a/main/admin.py b/main/admin.py index 8b4bdbbb..c5129ca7 100644 --- a/main/admin.py +++ b/main/admin.py @@ -2,9 +2,11 @@ from google.appengine.api import app_identity import flask -from flaskext import wtf -from flaskext.babel import lazy_gettext as _ -from flaskext.babel import gettext as __ +from i18n import Form +import wtforms +import wtforms.validators +from flask.ext.babel import lazy_gettext as _ +from flask.ext.babel import gettext as __ import auth import util @@ -14,18 +16,18 @@ from main import app -class ConfigUpdateForm(wtf.Form): - analytics_id = wtf.TextField('Analytics ID', filters=[util.strip_filter]) - announcement_html = wtf.TextAreaField('Announcement HTML', filters=[util.strip_filter]) - announcement_type = wtf.SelectField('Announcement Type', choices=[(t, t.title()) for t in model.Config.announcement_type._choices]) - brand_name = wtf.TextField('Brand Name', [wtf.validators.required()], filters=[util.strip_filter]) - facebook_app_id = wtf.TextField('Facebook App ID', filters=[util.strip_filter]) - facebook_app_secret = wtf.TextField('Facebook App Secret', filters=[util.strip_filter]) - feedback_email = wtf.TextField('Feedback Email', [wtf.validators.optional(), wtf.validators.email()], filters=[util.email_filter]) - flask_secret_key = wtf.TextField('Flask Secret Key', [wtf.validators.required()], filters=[util.strip_filter]) - locale = wtf.SelectField('Default Locale', choices=config.LOCALE_SORTED) - twitter_consumer_key = wtf.TextField('Twitter Consumer Key', filters=[util.strip_filter]) - twitter_consumer_secret = wtf.TextField('Twitter Consumer Secret', filters=[util.strip_filter]) +class ConfigUpdateForm(Form): + analytics_id = wtforms.TextField('Analytics ID', filters=[util.strip_filter]) + announcement_html = wtforms.TextAreaField('Announcement HTML', filters=[util.strip_filter]) + announcement_type = wtforms.SelectField('Announcement Type', choices=[(t, t.title()) for t in model.Config.announcement_type._choices]) + brand_name = wtforms.TextField('Brand Name', [wtforms.validators.required()], filters=[util.strip_filter]) + facebook_app_id = wtforms.TextField('Facebook App ID', filters=[util.strip_filter]) + facebook_app_secret = wtforms.TextField('Facebook App Secret', filters=[util.strip_filter]) + feedback_email = wtforms.TextField('Feedback Email', [wtforms.validators.optional(), wtforms.validators.email()], filters=[util.email_filter]) + flask_secret_key = wtforms.TextField('Flask Secret Key', [wtforms.validators.required()], filters=[util.strip_filter]) + locale = wtforms.SelectField('Default Locale', choices=config.LOCALE_SORTED) + twitter_consumer_key = wtforms.TextField('Twitter Consumer Key', filters=[util.strip_filter]) + twitter_consumer_secret = wtforms.TextField('Twitter Consumer Secret', filters=[util.strip_filter]) @app.route('/_s/admin/config/', endpoint='admin_config_update_service') diff --git a/main/auth.py b/main/auth.py index b17cecc0..84f7cf56 100644 --- a/main/auth.py +++ b/main/auth.py @@ -7,10 +7,10 @@ import re import flask -from flaskext import login -from flaskext import oauth -from flaskext.babel import lazy_gettext as _ -from flaskext.babel import gettext as __ +from flask.ext import login +from flask.ext import oauth +from flask.ext.babel import lazy_gettext as _ +from flask.ext.babel import gettext as __ from babel import localedata import util diff --git a/main/i18n.py b/main/i18n.py new file mode 100644 index 00000000..8f535fc7 --- /dev/null +++ b/main/i18n.py @@ -0,0 +1,50 @@ +from flask.ext import wtf +from flask import _request_ctx_stack +from flask.ext.babel import get_locale +from babel import support +import os + + +def _get_translations(): + """Returns the correct gettext translations. + Copy from flask-babel with some modifications. + """ + ctx = _request_ctx_stack.top + if ctx is None: + return None + # babel should be in extensions for get_locale + if 'babel' not in ctx.app.extensions: + return None + translations = getattr(ctx, 'wtforms_translations', None) + if translations is None: + module_path = os.path.abspath(__file__) + dirname = os.path.join(os.path.dirname(module_path), 'translations') + translations = support.Translations.load( + dirname, [get_locale()], domain='messages' + ) + ctx.wtforms_translations = translations + return translations + + +class Translations(object): + def gettext(self, string): + t = _get_translations() + if t is None: + return string + return t.ugettext(string) + + def ngettext(self, singular, plural, n): + t = _get_translations() + if t is None: + if n == 1: + return singular + return plural + return t.ungettext(singular, plural, n) + + +translations = Translations() + + +class Form(wtf.Form): + def _get_translations(self): + return translations diff --git a/main/lib/flaskext/babel.py b/main/lib/flask_babel/__init__.py similarity index 99% rename from main/lib/flaskext/babel.py rename to main/lib/flask_babel/__init__.py index ddab9458..f0738c00 100644 --- a/main/lib/flaskext/babel.py +++ b/main/lib/flask_babel/__init__.py @@ -5,7 +5,7 @@ Implements i18n/l10n support for Flask applications based on Babel. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2013 by Armin Ronacher, Daniel Neuhäuser. :license: BSD, see LICENSE for more details. """ from __future__ import absolute_import @@ -28,6 +28,8 @@ timezone = pytz.timezone UTC = pytz.UTC +from flask_babel._compat import string_types + class Babel(object): """Central controller class that can be used to configure how @@ -235,7 +237,7 @@ def get_timezone(): if rv is None: tzinfo = babel.default_timezone else: - if isinstance(rv, basestring): + if isinstance(rv, string_types): tzinfo = timezone(rv) else: tzinfo = rv diff --git a/main/lib/flask_babel/_compat.py b/main/lib/flask_babel/_compat.py new file mode 100644 index 00000000..d263ef73 --- /dev/null +++ b/main/lib/flask_babel/_compat.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" + flask.ext.babel._compat + ~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: (c) 2013 by Armin Ronacher, Daniel Neuhäuser. + :license: BSD, see LICENSE for more details. +""" +import sys + + +PY2 = sys.version_info[0] == 2 + + +if PY2: + text_type = unicode + string_types = (str, unicode) +else: + text_type = str + string_types = (str, ) diff --git a/main/lib/flask_wtf/__init__.py b/main/lib/flask_wtf/__init__.py new file mode 100755 index 00000000..38a9b153 --- /dev/null +++ b/main/lib/flask_wtf/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" + flask_wtf + ~~~~~~~~~ + + Flask-WTF extension + + :copyright: (c) 2010 by Dan Jacob. + :license: BSD, see LICENSE for more details. +""" +# flake8: noqa +from __future__ import absolute_import + +from .form import Form +from .csrf import CsrfProtect +from .recaptcha import * + +__version__ = '0.9.3' diff --git a/main/lib/flask_wtf/_compat.py b/main/lib/flask_wtf/_compat.py new file mode 100755 index 00000000..7295b737 --- /dev/null +++ b/main/lib/flask_wtf/_compat.py @@ -0,0 +1,14 @@ +import sys +if sys.version_info[0] == 3: + text_type = str + string_types = (str,) +else: + text_type = unicode + string_types = (str, unicode) + + +def to_bytes(text): + """Transform string to bytes.""" + if isinstance(text, text_type): + text = text.encode('utf-8') + return text diff --git a/main/lib/flask_wtf/csrf.py b/main/lib/flask_wtf/csrf.py new file mode 100755 index 00000000..5d456362 --- /dev/null +++ b/main/lib/flask_wtf/csrf.py @@ -0,0 +1,230 @@ +# coding: utf-8 +""" + flask_wtf.csrf + ~~~~~~~~~~~~~~ + + CSRF protection for Flask. + + :copyright: (c) 2013 by Hsiaoming Yang. +""" + +import os +import hmac +import hashlib +import time +from flask import current_app, session, request, abort +from ._compat import to_bytes +try: + from urlparse import urlparse +except ImportError: + # python 3 + from urllib.parse import urlparse + + +__all__ = ('generate_csrf', 'validate_csrf', 'CsrfProtect') + + +def generate_csrf(secret_key=None, time_limit=None): + """Generate csrf token code. + + :param secret_key: A secret key for mixing in the token, + default is Flask.secret_key. + :param time_limit: Token valid in the time limit, + default is 3600s. + """ + if not secret_key: + secret_key = current_app.config.get( + 'WTF_CSRF_SECRET_KEY', current_app.secret_key + ) + + if not secret_key: + raise Exception('Must provide secret_key to use csrf.') + + if time_limit is None: + time_limit = current_app.config.get('WTF_CSRF_TIME_LIMIT', 3600) + + if 'csrf_token' not in session: + session['csrf_token'] = hashlib.sha1(os.urandom(64)).hexdigest() + + if time_limit: + expires = time.time() + time_limit + csrf_build = '%s%s' % (session['csrf_token'], expires) + else: + expires = '' + csrf_build = session['csrf_token'] + + hmac_csrf = hmac.new( + to_bytes(secret_key), + to_bytes(csrf_build), + digestmod=hashlib.sha1 + ).hexdigest() + return '%s##%s' % (expires, hmac_csrf) + + +def validate_csrf(data, secret_key=None, time_limit=None): + """Check if the given data is a valid csrf token. + + :param data: The csrf token value to be checked. + :param secret_key: A secret key for mixing in the token, + default is Flask.secret_key. + :param time_limit: Check if the csrf token is expired. + default is True. + """ + if not data or '##' not in data: + return False + + expires, hmac_csrf = data.split('##', 1) + try: + expires = float(expires) + except: + return False + + if time_limit is None: + time_limit = current_app.config.get('WTF_CSRF_TIME_LIMIT', 3600) + + if time_limit: + now = time.time() + if now > expires: + return False + + if not secret_key: + secret_key = current_app.config.get( + 'WTF_CSRF_SECRET_KEY', current_app.secret_key + ) + + if 'csrf_token' not in session: + return False + + csrf_build = '%s%s' % (session['csrf_token'], expires) + hmac_compare = hmac.new( + to_bytes(secret_key), + to_bytes(csrf_build), + digestmod=hashlib.sha1 + ).hexdigest() + return hmac_compare == hmac_csrf + + +class CsrfProtect(object): + """Enable csrf protect for Flask. + + Register it with:: + + app = Flask(__name__) + CsrfProtect(app) + + And in the templates, add the token input:: + + + + If you need to send the token via AJAX, and there is no form:: + + + + You can grab the csrf token with JavaScript, and send the token together. + """ + + def __init__(self, app=None): + self._exempt_views = set() + + if app: + self.init_app(app) + + def init_app(self, app): + app.jinja_env.globals['csrf_token'] = generate_csrf + strict = app.config.get('WTF_CSRF_SSL_STRICT', True) + csrf_enabled = app.config.get('WTF_CSRF_ENABLED', True) + + @app.before_request + def _csrf_protect(): + # many things come from django.middleware.csrf + if not csrf_enabled: + return + + if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): + return + + if self._exempt_views: + if not request.endpoint: + return + + view = app.view_functions.get(request.endpoint) + if not view: + return + + dest = '%s.%s' % (view.__module__, view.__name__) + if dest in self._exempt_views: + return + + csrf_token = None + if request.method in ('POST', 'PUT', 'PATCH'): + csrf_token = request.form.get('csrf_token') + if not csrf_token: + # You can get csrf token from header + # The header name is the same as Django + csrf_token = request.headers.get('X-CSRFToken') + if not csrf_token: + # The header name is the same as Rails + csrf_token = request.headers.get('X-CSRF-Token') + if not validate_csrf(csrf_token): + reason = 'CSRF token missing or incorrect.' + return self._error_response(reason) + + if request.is_secure and strict: + if not request.referrer: + reason = 'Referrer checking failed - no Referrer.' + return self._error_response(reason) + + good_referrer = 'https://%s/' % request.host + if not same_origin(request.referrer, good_referrer): + reason = 'Referrer checking failed - origin not match.' + return self._error_response(reason) + + request.csrf_valid = True # mark this request is csrf valid + + def exempt(self, view): + """A decorator that can exclude a view from csrf protection. + + Remember to put the decorator above the `route`:: + + csrf = CsrfProtect(app) + + @csrf.exempt + @app.route('/some-view', methods=['POST']) + def some_view(): + return + """ + view_location = '%s.%s' % (view.__module__, view.__name__) + self._exempt_views.add(view_location) + return view + + def _error_response(self, reason): + return abort(400, reason) + + def error_handler(self, view): + """A decorator that set the error response handler. + + It accepts one parameter `reason`:: + + @csrf.error_handler + def csrf_error(reason): + return render_template('error.html', reason=reason) + + By default, it will return a 400 response. + """ + self._error_response = view + return view + + +def same_origin(current_uri, compare_uri): + parsed_uri = urlparse(current_uri) + parsed_compare = urlparse(compare_uri) + + if parsed_uri.scheme != parsed_compare.scheme: + return False + + if parsed_uri.hostname != parsed_compare.hostname: + return False + + if parsed_uri.port != parsed_compare.port: + return False + return True diff --git a/main/lib/flaskext/wtf/file.py b/main/lib/flask_wtf/file.py similarity index 75% rename from main/lib/flaskext/wtf/file.py rename to main/lib/flask_wtf/file.py index 95a220b9..a61cacb6 100755 --- a/main/lib/flaskext/wtf/file.py +++ b/main/lib/flask_wtf/file.py @@ -2,6 +2,7 @@ from wtforms import FileField as _FileField from wtforms import ValidationError + class FileField(_FileField): """ Werkzeug-aware subclass of **wtforms.FileField** @@ -31,17 +32,17 @@ class FileRequired(object): """ Validates that field has a file. - `message` : error message + :param message: error message You can also use the synonym **file_required**. """ def __init__(self, message=None): - self.message=message + self.message = message def __call__(self, form, field): if not field.has_file(): - raise ValidationError, self.message + raise ValidationError(self.message) file_required = FileRequired @@ -51,9 +52,9 @@ class FileAllowed(object): Validates that the uploaded file is allowed by the given Flask-Uploads UploadSet. - `upload_set` : instance of **flask.ext.uploads.UploadSet** - - `message` : error message + :param upload_set: A list/tuple of extention names or an instance + of ``flask.ext.uploads.UploadSet`` + :param message: error message You can also use the synonym **file_allowed**. """ @@ -65,8 +66,14 @@ def __init__(self, upload_set, message=None): def __call__(self, form, field): if not field.has_file(): return + + if isinstance(self.upload_set, (tuple, list)): + ext = field.data.filename.rsplit('.', 1)[-1] + if ext.lower() in self.upload_set: + return + raise ValidationError(self.message) + if not self.upload_set.file_allowed(field.data, field.data.filename): - raise ValidationError, self.message + raise ValidationError(self.message) file_allowed = FileAllowed - diff --git a/main/lib/flask_wtf/form.py b/main/lib/flask_wtf/form.py new file mode 100755 index 00000000..9f063434 --- /dev/null +++ b/main/lib/flask_wtf/form.py @@ -0,0 +1,161 @@ +# coding: utf-8 + +import werkzeug.datastructures + +from jinja2 import Markup +from flask import request, session, current_app +from wtforms.fields import HiddenField +from wtforms.widgets import HiddenInput +from wtforms.validators import ValidationError +from wtforms.ext.csrf.form import SecureForm +from ._compat import text_type, string_types +from .csrf import generate_csrf, validate_csrf + +try: + from .i18n import translations +except: + translations = None + + +class _Auto(): + '''Placeholder for unspecified variables that should be set to defaults. + + Used when None is a valid option and should not be replaced by a default. + ''' + pass + + +def _is_hidden(field): + """Detect if the field is hidden.""" + if isinstance(field, HiddenField): + return True + if isinstance(field.widget, HiddenInput): + return True + return False + + +class Form(SecureForm): + """ + Flask-specific subclass of WTForms **SecureForm** class. + + If formdata is not specified, this will use flask.request.form. + Explicitly pass formdata = None to prevent this. + + :param csrf_context: a session or dict-like object to use when making + CSRF tokens. Default: flask.session. + + :param secret_key: a secret key for building CSRF tokens. If this isn't + specified, the form will take the first of these + that is defined: + + * SECRET_KEY attribute on this class + * WTF_CSRF_SECRET_KEY config of flask app + * SECRET_KEY config of flask app + * session secret key + + :param csrf_enabled: whether to use CSRF protection. If False, all + csrf behavior is suppressed. + Default: WTF_CSRF_ENABLED config value + """ + SECRET_KEY = None + TIME_LIMIT = 3600 + + def __init__(self, formdata=_Auto, obj=None, prefix='', csrf_context=None, + secret_key=None, csrf_enabled=None, *args, **kwargs): + + if csrf_enabled is None: + csrf_enabled = current_app.config.get('WTF_CSRF_ENABLED', True) + + self.csrf_enabled = csrf_enabled + + if formdata is _Auto: + if self.is_submitted(): + formdata = request.form + if request.files: + formdata = formdata.copy() + formdata.update(request.files) + elif request.json: + formdata = werkzeug.datastructures.MultiDict(request.json) + else: + formdata = None + + if self.csrf_enabled: + if csrf_context is None: + csrf_context = session + if secret_key is None: + # It wasn't passed in, check if the class has a SECRET_KEY + secret_key = getattr(self, "SECRET_KEY", None) + + self.SECRET_KEY = secret_key + else: + csrf_context = {} + self.SECRET_KEY = '' + super(Form, self).__init__(formdata, obj, prefix, + csrf_context=csrf_context, + *args, **kwargs) + + def generate_csrf_token(self, csrf_context=None): + if not self.csrf_enabled: + return None + return generate_csrf(self.SECRET_KEY, self.TIME_LIMIT) + + def validate_csrf_token(self, field): + if not self.csrf_enabled: + return True + if hasattr(request, 'csrf_valid') and request.csrf_valid: + # this is validated by CsrfProtect + return True + if not validate_csrf(field.data, self.SECRET_KEY, self.TIME_LIMIT): + raise ValidationError(field.gettext('CSRF token missing')) + + def validate_csrf_data(self, data): + """Check if the csrf data is valid. + + .. versionadded: 0.9.0 + + :param data: the csrf string to be validated. + """ + return validate_csrf(data, self.SECRET_KEY, self.TIME_LIMIT) + + def is_submitted(self): + """ + Checks if form has been submitted. The default case is if the HTTP + method is **PUT** or **POST**. + """ + + return request and request.method in ("PUT", "POST") + + def hidden_tag(self, *fields): + """ + Wraps hidden fields in a hidden DIV tag, in order to keep XHTML + compliance. + + .. versionadded:: 0.3 + + :param fields: list of hidden field names. If not provided will render + all hidden fields, including the CSRF field. + """ + + if not fields: + fields = [f for f in self if _is_hidden(f)] + + rv = [u'
'] + for field in fields: + if isinstance(field, string_types): + field = getattr(self, field) + rv.append(text_type(field)) + rv.append(u"
") + + return Markup(u"".join(rv)) + + def validate_on_submit(self): + """ + Checks if form has been submitted and if so runs validate. This is + a shortcut, equivalent to ``form.is_submitted() and form.validate()`` + """ + return self.is_submitted() and self.validate() + + def _get_translations(self): + if not current_app.config.get('WTF_I18N_ENABLED', True): + return None + return translations diff --git a/main/lib/flask_wtf/html5.py b/main/lib/flask_wtf/html5.py new file mode 100755 index 00000000..83de5a64 --- /dev/null +++ b/main/lib/flask_wtf/html5.py @@ -0,0 +1,4 @@ +# coding: utf-8 +# flake8: noqa +from wtforms.widgets.html5 import * +from wtforms.fields.html5 import * diff --git a/main/lib/flask_wtf/i18n.py b/main/lib/flask_wtf/i18n.py new file mode 100755 index 00000000..8c711f95 --- /dev/null +++ b/main/lib/flask_wtf/i18n.py @@ -0,0 +1,62 @@ +# coding: utf-8 +""" + flask_wtf.i18n + ~~~~~~~~~~~~~~ + + Internationalization support for Flask WTF. + + :copyright: (c) 2013 by Hsiaoming Yang. +""" + +from flask import _request_ctx_stack +from wtforms.ext.i18n.utils import messages_path +from flask.ext.babel import get_locale +from babel import support + +__all__ = ('Translations', 'translations') + + +def _get_translations(): + """Returns the correct gettext translations. + Copy from flask-babel with some modifications. + """ + ctx = _request_ctx_stack.top + if ctx is None: + return None + # babel should be in extensions for get_locale + if 'babel' not in ctx.app.extensions: + return None + translations = getattr(ctx, 'wtforms_translations', None) + if translations is None: + dirname = messages_path() + translations = support.Translations.load( + dirname, [get_locale()], domain='wtforms' + ) + ctx.wtforms_translations = translations + return translations + + +class Translations(object): + def gettext(self, string): + t = _get_translations() + if t is None: + return string + if hasattr(t, 'ugettext'): + return t.ugettext(string) + # Python 3 has no ugettext + return t.gettext(string) + + def ngettext(self, singular, plural, n): + t = _get_translations() + if t is None: + if n == 1: + return singular + return plural + + if hasattr(t, 'ungettext'): + return t.ungettext(singular, plural, n) + # Python 3 has no ungettext + return t.ngettext(singular, plural, n) + + +translations = Translations() diff --git a/main/lib/flask_wtf/recaptcha/__init__.py b/main/lib/flask_wtf/recaptcha/__init__.py new file mode 100755 index 00000000..47627d2d --- /dev/null +++ b/main/lib/flask_wtf/recaptcha/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa +from .fields import * +from .validators import * +from .widgets import * diff --git a/main/lib/flaskext/wtf/recaptcha/fields.py b/main/lib/flask_wtf/recaptcha/fields.py similarity index 100% rename from main/lib/flaskext/wtf/recaptcha/fields.py rename to main/lib/flask_wtf/recaptcha/fields.py diff --git a/main/lib/flaskext/wtf/recaptcha/validators.py b/main/lib/flask_wtf/recaptcha/validators.py similarity index 71% rename from main/lib/flaskext/wtf/recaptcha/validators.py rename to main/lib/flask_wtf/recaptcha/validators.py index 6294e451..332a3619 100755 --- a/main/lib/flaskext/wtf/recaptcha/validators.py +++ b/main/lib/flask_wtf/recaptcha/validators.py @@ -1,10 +1,13 @@ -import urllib2 +try: + import urllib2 as http +except ImportError: + # Python 3 + from urllib import request as http from flask import request, current_app - from wtforms import ValidationError - from werkzeug import url_encode +from .._compat import to_bytes RECAPTCHA_VERIFY_SERVER = 'http://api-verify.recaptcha.net/verify' @@ -13,25 +16,34 @@ class Recaptcha(object): """Validates a ReCaptcha.""" + _error_codes = { 'invalid-site-public-key': 'The public key for reCAPTCHA is invalid', 'invalid-site-private-key': 'The private key for reCAPTCHA is invalid', - 'invalid-referrer': 'The public key for reCAPTCHA is not valid for ' - 'this domainin', - 'verify-params-incorrect': 'The parameters passed to reCAPTCHA ' - 'verification are incorrect', + 'invalid-referrer': ( + 'The public key for reCAPTCHA is not valid for ' + 'this domainin' + ), + 'verify-params-incorrect': ( + 'The parameters passed to reCAPTCHA ' + 'verification are incorrect' + ) } def __init__(self, message=u'Invalid word. Please try again.'): self.message = message def __call__(self, form, field): + config = current_app.config + if current_app.testing and 'RECAPTCHA_PRIVATE_KEY' not in config: + return True + challenge = request.form.get('recaptcha_challenge_field', '') response = request.form.get('recaptcha_response_field', '') remote_ip = request.remote_addr if not challenge or not response: - raise ValidationError(field.gettext('This field is required.')) + raise ValidationError(field.gettext(self.message)) if not self._validate_recaptcha(challenge, response, remote_ip): field.recaptcha_error = 'incorrect-captcha-sol' @@ -46,7 +58,7 @@ def _validate_recaptcha(self, challenge, response, remote_addr): try: private_key = current_app.config['RECAPTCHA_PRIVATE_KEY'] except KeyError: - raise RuntimeError, "No RECAPTCHA_PRIVATE_KEY config set" + raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") data = url_encode({ 'privatekey': private_key, @@ -55,7 +67,7 @@ def _validate_recaptcha(self, challenge, response, remote_addr): 'response': response }) - response = urllib2.urlopen(RECAPTCHA_VERIFY_SERVER, data) + response = http.urlopen(RECAPTCHA_VERIFY_SERVER, to_bytes(data)) if response.code != 200: return False diff --git a/main/lib/flaskext/wtf/recaptcha/widgets.py b/main/lib/flask_wtf/recaptcha/widgets.py similarity index 52% rename from main/lib/flaskext/wtf/recaptcha/widgets.py rename to main/lib/flask_wtf/recaptcha/widgets.py index 9f731ad7..8cea2318 100755 --- a/main/lib/flaskext/wtf/recaptcha/widgets.py +++ b/main/lib/flask_wtf/recaptcha/widgets.py @@ -1,20 +1,22 @@ -""" -Custom widgets -""" -try: - import json -except ImportError: - import simplejson as json +# -*- coding: utf-8 -*- -from flask import current_app +from flask import current_app, Markup from werkzeug import url_encode - -# use flaskext.babel for translations, if available +from flask import json +from .._compat import text_type +JSONEncoder = json.JSONEncoder try: - from flaskext.babel import gettext as _ -except ImportError: - _ = lambda(s) : s + from speaklater import _LazyString + + class _JSONEncoder(JSONEncoder): + def default(self, o): + if isinstance(o, _LazyString): + return str(o) + return JSONEncoder.default(self, o) +except: + _JSONEncoder = JSONEncoder + RECAPTCHA_API_SERVER = 'http://api.recaptcha.net/' RECAPTCHA_SSL_API_SERVER = 'https://www.google.com/recaptcha/api/' @@ -23,8 +25,11 @@ ''' @@ -34,45 +39,45 @@ class RecaptchaWidget(object): def recaptcha_html(self, server, query, options): - return RECAPTCHA_HTML % dict( + html = current_app.config.get('RECAPTCHA_HTML', RECAPTCHA_HTML) + return Markup(html % dict( script_url='%schallenge?%s' % (server, query), frame_url='%snoscript?%s' % (server, query), - options=json.dumps(options) - ) + options=json.dumps(options, cls=_JSONEncoder) + )) def __call__(self, field, error=None, **kwargs): """Returns the recaptcha input HTML.""" if current_app.config.get('RECAPTCHA_USE_SSL', False): - server = RECAPTCHA_SSL_API_SERVER - else: - server = RECAPTCHA_API_SERVER try: public_key = current_app.config['RECAPTCHA_PUBLIC_KEY'] except KeyError: - raise RuntimeError, "RECAPTCHA_PUBLIC_KEY config not set" + raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set") query_options = dict(k=public_key) if field.recaptcha_error is not None: - query_options['error'] = unicode(field.recaptcha_error) + query_options['error'] = text_type(field.recaptcha_error) query = url_encode(query_options) + _ = field.gettext + options = { - 'theme': 'clean', + 'theme': 'clean', 'custom_translations': { - 'visual_challenge': _('Get a visual challenge'), - 'audio_challenge': _('Get an audio challenge'), - 'refresh_btn': _('Get a new challenge'), + 'visual_challenge': _('Get a visual challenge'), + 'audio_challenge': _('Get an audio challenge'), + 'refresh_btn': _('Get a new challenge'), 'instructions_visual': _('Type the two words:'), - 'instructions_audio': _('Type what you hear:'), - 'help_btn': _('Help'), - 'play_again': _('Play sound again'), - 'cant_hear_this': _('Download sound as MP3'), + 'instructions_audio': _('Type what you hear:'), + 'help_btn': _('Help'), + 'play_again': _('Play sound again'), + 'cant_hear_this': _('Download sound as MP3'), 'incorrect_try_again': _('Incorrect. Try again.'), } } diff --git a/main/lib/flaskext/wtf/__init__.py b/main/lib/flaskext/wtf/__init__.py deleted file mode 100755 index 479eca17..00000000 --- a/main/lib/flaskext/wtf/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.wtf - ~~~~~~~~~~~~ - - Flask-WTF extension - - :copyright: (c) 2010 by Dan Jacob. - :license: BSD, see LICENSE for more details. -""" - -try: - import sqlalchemy - _is_sqlalchemy = True -except ImportError: - _is_sqlalchemy = False - -from wtforms import fields, widgets, validators -from wtforms.fields import * -from wtforms.validators import * -from wtforms.widgets import * -from wtforms import ValidationError - -from . import html5 -from .form import Form -from . import recaptcha - -from .recaptcha.fields import RecaptchaField -from .recaptcha.widgets import RecaptchaWidget -from .recaptcha.validators import Recaptcha - -fields.RecaptchaField = RecaptchaField -widgets.RecaptchaWidget = RecaptchaWidget -validators.Recaptcha = Recaptcha - -from .file import FileField -from .file import FileAllowed, FileRequired, file_allowed, file_required - -fields.FileField = FileField - -validators.file_allowed = file_allowed -validators.file_required = file_required -validators.FileAllowed = FileAllowed -validators.FileRequired = FileRequired - - -__all__ = ['Form', 'ValidationError', 'fields', 'validators', 'widgets', 'html5'] - -__all__ += [str(v) for v in validators.__all__ ] -__all__ += [str(v) for v in (fields.__all__ if hasattr(fields, '__all__') else - fields.core.__all__) ] - -__all__ += [str(v) for v in (widgets.__all__ if hasattr(widgets, '__all__') else - widgets.core.__all__)] -__all__ += recaptcha.__all__ - -if _is_sqlalchemy: - from wtforms.ext.sqlalchemy.fields import QuerySelectField, \ - QuerySelectMultipleField - - __all__ += ['QuerySelectField', - 'QuerySelectMultipleField'] - - for field in (QuerySelectField, - QuerySelectMultipleField): - - setattr(fields, field.__name__, field) diff --git a/main/lib/flaskext/wtf/form.py b/main/lib/flaskext/wtf/form.py deleted file mode 100755 index 95327e35..00000000 --- a/main/lib/flaskext/wtf/form.py +++ /dev/null @@ -1,124 +0,0 @@ -import werkzeug.datastructures - -from jinja2 import Markup -from flask import request, session, current_app -from wtforms.fields import HiddenField -from wtforms.ext.csrf.session import SessionSecureForm - -class _Auto(): - '''Placeholder for unspecified variables that should be set to defaults. - - Used when None is a valid option and should not be replaced by a default. - ''' - pass - -class Form(SessionSecureForm): - - """ - Flask-specific subclass of WTForms **SessionSecureForm** class. - - Flask-specific behaviors: - If formdata is not specified, this will use flask.request.form. Explicitly - pass formdata = None to prevent this. - - csrf_context - a session or dict-like object to use when making CSRF tokens. - Default: flask.session. - - secret_key - a secret key for building CSRF tokens. If this isn't specified, - the form will take the first of these that is defined: - * the SECRET_KEY attribute on this class - * the value of flask.current_app.config["SECRET_KEY"] - * the session's secret_key - If none of these are set, raise an exception. - - csrf_enabled - whether to use CSRF protection. If False, all csrf behavior - is suppressed. Default: check app.config for CSRF_ENABLED, else True - - """ - def __init__(self, formdata=_Auto, obj=None, prefix='', csrf_context=None, - secret_key=None, csrf_enabled=None, *args, **kwargs): - - if csrf_enabled is None: - csrf_enabled = current_app.config.get('CSRF_ENABLED', True) - self.csrf_enabled = csrf_enabled - - if formdata is _Auto: - if self.is_submitted(): - formdata = request.form - if request.files: - formdata = formdata.copy() - formdata.update(request.files) - elif request.json: - formdata = werkzeug.datastructures.MultiDict(request.json); - else: - formdata = None - if self.csrf_enabled: - if csrf_context is None: - csrf_context = session - if secret_key is None: - # It wasn't passed in, check if the class has a SECRET_KEY set - secret_key = getattr(self, "SECRET_KEY", None) - if secret_key is None: - # It wasn't on the class, check the application config - secret_key = current_app.config.get("SECRET_KEY") - if secret_key is None and session: - # It's not there either! Is there a session secret key if we can - secret_key = session.secret_key - if secret_key is None: - # It wasn't anywhere. This is an error. - raise Exception('Must provide secret_key to use csrf.') - - self.SECRET_KEY = secret_key - else: - csrf_context = {} - self.SECRET_KEY = "" - super(Form, self).__init__(formdata, obj, prefix, csrf_context=csrf_context, *args, **kwargs) - - def generate_csrf_token(self, csrf_context=None): - if not self.csrf_enabled: - return None - return super(Form, self).generate_csrf_token(csrf_context) - - def validate_csrf_token(self, field): - if not self.csrf_enabled: - return - super(Form, self).validate_csrf_token(field) - - def is_submitted(self): - """ - Checks if form has been submitted. The default case is if the HTTP - method is **PUT** or **POST**. - """ - - return request and request.method in ("PUT", "POST") - - def hidden_tag(self, *fields): - """ - Wraps hidden fields in a hidden DIV tag, in order to keep XHTML - compliance. - - .. versionadded:: 0.3 - - :param fields: list of hidden field names. If not provided will render - all hidden fields, including the CSRF field. - """ - - if not fields: - fields = [f for f in self if isinstance(f, HiddenField)] - - rv = [u'
'] - for field in fields: - if isinstance(field, basestring): - field = getattr(self, field) - rv.append(unicode(field)) - rv.append(u"
") - - return Markup(u"".join(rv)) - - def validate_on_submit(self): - """ - Checks if form has been submitted and if so runs validate. This is - a shortcut, equivalent to ``form.is_submitted() and form.validate()`` - """ - return self.is_submitted() and self.validate() - diff --git a/main/lib/flaskext/wtf/html5.py b/main/lib/flaskext/wtf/html5.py deleted file mode 100755 index 4ffd8b77..00000000 --- a/main/lib/flaskext/wtf/html5.py +++ /dev/null @@ -1,125 +0,0 @@ -from wtforms import TextField -from wtforms import IntegerField as _IntegerField -from wtforms import DecimalField as _DecimalField -from wtforms import DateField as _DateField -from wtforms.widgets import Input - -class DateInput(Input): - """ - Creates `` widget - """ - input_type = "date" - - -class NumberInput(Input): - """ - Creates `` widget - """ - input_type="number" - - -class RangeInput(Input): - """ - Creates `` widget - """ - input_type="range" - - -class URLInput(Input): - """ - Creates `` widget - """ - input_type = "url" - - -class EmailInput(Input): - """ - Creates `` widget - """ - - input_type = "email" - - -class SearchInput(Input): - """ - Creates `` widget - """ - - input_type = "search" - -class TelInput(Input): - """ - Creates `` widget - """ - - input_type = "tel" - - -class SearchField(TextField): - """ - **TextField** using **SearchInput** by default - """ - widget = SearchInput() - - -class DateField(_DateField): - """ - **DateField** using **DateInput** by default - """ - - widget = DateInput() - - -class URLField(TextField): - """ - **TextField** using **URLInput** by default - """ - - widget = URLInput() - - -class EmailField(TextField): - """ - **TextField** using **EmailInput** by default - """ - - widget = EmailInput() - -class TelField(TextField): - """ - **TextField** using **TelInput** by default - """ - - widget = TelInput() - - -class IntegerField(_IntegerField): - """ - **IntegerField** using **NumberInput** by default - """ - - widget = NumberInput() - - -class DecimalField(_DecimalField): - """ - **DecimalField** using **NumberInput** by default - """ - - widget = NumberInput() - - -class IntegerRangeField(_IntegerField): - """ - **IntegerField** using **RangeInput** by default - """ - - widget = RangeInput() - - -class DecimalRangeField(_DecimalField): - """ - **DecimalField** using **RangeInput** by default - """ - - widget = RangeInput() diff --git a/main/lib/flaskext/wtf/recaptcha/__init__.py b/main/lib/flaskext/wtf/recaptcha/__init__.py deleted file mode 100755 index 322ff2da..00000000 --- a/main/lib/flaskext/wtf/recaptcha/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import fields -from . import validators -from . import widgets - -__all__ = fields.__all__ + validators.__all__ + widgets.__all__ diff --git a/main/main.py b/main/main.py index d56a0006..2e4b2f39 100644 --- a/main/main.py +++ b/main/main.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- from google.appengine.api import mail -from flaskext import wtf -from flaskext.babel import Babel -from flaskext.babel import gettext as __ -from flaskext.babel import lazy_gettext as _ +from i18n import Form +import wtforms +import wtforms.validators +from flask.ext.babel import Babel +from flask.ext.babel import gettext as __ +from flask.ext.babel import lazy_gettext as _ import flask import config @@ -49,15 +51,15 @@ def sitemap(): ################################################################################ # Profile stuff ################################################################################ -class ProfileUpdateForm(wtf.Form): - name = wtf.TextField(_('Name'), - [wtf.validators.required()], filters=[util.strip_filter], +class ProfileUpdateForm(Form): + name = wtforms.TextField(_('Name'), + [wtforms.validators.required()], filters=[util.strip_filter], ) - email = wtf.TextField(_('Email'), - [wtf.validators.optional(), wtf.validators.email()], + email = wtforms.TextField(_('Email'), + [wtforms.validators.optional(), wtforms.validators.email()], filters=[util.email_filter], ) - locale = wtf.SelectField(_('Language'), + locale = wtforms.SelectField(_('Language'), choices=config.LOCALE_SORTED, filters=[util.strip_filter], ) @@ -94,15 +96,15 @@ def profile(): ################################################################################ # Feedback ################################################################################ -class FeedbackForm(wtf.Form): - subject = wtf.TextField(_('Subject'), - [wtf.validators.required()], filters=[util.strip_filter], +class FeedbackForm(Form): + subject = wtforms.TextField(_('Subject'), + [wtforms.validators.required()], filters=[util.strip_filter], ) - message = wtf.TextAreaField(_('Message'), - [wtf.validators.required()], filters=[util.strip_filter], + message = wtforms.TextAreaField(_('Message'), + [wtforms.validators.required()], filters=[util.strip_filter], ) - email = wtf.TextField(_('Email (optional)'), - [wtf.validators.optional(), wtf.validators.email()], + email = wtforms.TextField(_('Email (optional)'), + [wtforms.validators.optional(), wtforms.validators.email()], filters=[util.strip_filter], )