This commit is contained in:
2025-05-22 20:25:38 +02:00
parent 09f6750c2b
commit ce03fbf12f
529 changed files with 3353 additions and 3312 deletions

View File

@@ -5,4 +5,4 @@ from .recaptcha import Recaptcha
from .recaptcha import RecaptchaField
from .recaptcha import RecaptchaWidget
__version__ = "1.1.1"
__version__ = "1.2.1"

View File

@@ -217,7 +217,7 @@ class CSRFProtect:
if not request.endpoint:
return
if request.blueprint in self._exempt_blueprints:
if app.blueprints.get(request.blueprint) in self._exempt_blueprints:
return
view = app.view_functions.get(request.endpoint)
@@ -292,7 +292,7 @@ class CSRFProtect:
"""
if isinstance(view, Blueprint):
self._exempt_blueprints.add(view.name)
self._exempt_blueprints.add(view)
return view
if isinstance(view, str):

View File

@@ -2,6 +2,7 @@ from collections import abc
from werkzeug.datastructures import FileStorage
from wtforms import FileField as _FileField
from wtforms import MultipleFileField as _MultipleFileField
from wtforms.validators import DataRequired
from wtforms.validators import StopValidation
from wtforms.validators import ValidationError
@@ -20,8 +21,24 @@ class FileField(_FileField):
self.raw_data = ()
class MultipleFileField(_MultipleFileField):
"""Werkzeug-aware subclass of :class:`wtforms.fields.MultipleFileField`.
.. versionadded:: 1.2.0
"""
def process_formdata(self, valuelist):
valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x)
data = list(valuelist) or None
if data is not None:
self.data = data
else:
self.raw_data = ()
class FileRequired(DataRequired):
"""Validates that the data is a Werkzeug
"""Validates that the uploaded files(s) is a Werkzeug
:class:`~werkzeug.datastructures.FileStorage` object.
:param message: error message
@@ -30,7 +47,10 @@ class FileRequired(DataRequired):
"""
def __call__(self, form, field):
if not (isinstance(field.data, FileStorage) and field.data):
field_data = [field.data] if not isinstance(field.data, list) else field.data
if not (
all(isinstance(x, FileStorage) and x for x in field_data) and field_data
):
raise StopValidation(
self.message or field.gettext("This field is required.")
)
@@ -40,7 +60,7 @@ file_required = FileRequired
class FileAllowed:
"""Validates that the uploaded file is allowed by a given list of
"""Validates that the uploaded file(s) is allowed by a given list of
extensions or a Flask-Uploads :class:`~flaskext.uploads.UploadSet`.
:param upload_set: A list of extensions or an
@@ -55,34 +75,38 @@ class FileAllowed:
self.message = message
def __call__(self, form, field):
if not (isinstance(field.data, FileStorage) and field.data):
field_data = [field.data] if not isinstance(field.data, list) else field.data
if not (
all(isinstance(x, FileStorage) and x for x in field_data) and field_data
):
return
filename = field.data.filename.lower()
filenames = [f.filename.lower() for f in field_data]
if isinstance(self.upload_set, abc.Iterable):
if any(filename.endswith("." + x) for x in self.upload_set):
return
for filename in filenames:
if isinstance(self.upload_set, abc.Iterable):
if any(filename.endswith("." + x) for x in self.upload_set):
continue
raise StopValidation(
self.message
or field.gettext(
"File does not have an approved extension: {extensions}"
).format(extensions=", ".join(self.upload_set))
)
raise StopValidation(
self.message
or field.gettext(
"File does not have an approved extension: {extensions}"
).format(extensions=", ".join(self.upload_set))
)
if not self.upload_set.file_allowed(field.data, filename):
raise StopValidation(
self.message
or field.gettext("File does not have an approved extension.")
)
if not self.upload_set.file_allowed(field_data, filename):
raise StopValidation(
self.message
or field.gettext("File does not have an approved extension.")
)
file_allowed = FileAllowed
class FileSize:
"""Validates that the uploaded file is within a minimum and maximum
"""Validates that the uploaded file(s) is within a minimum and maximum
file size (set in bytes).
:param min_size: minimum allowed file size (in bytes). Defaults to 0 bytes.
@@ -98,22 +122,26 @@ class FileSize:
self.message = message
def __call__(self, form, field):
if not (isinstance(field.data, FileStorage) and field.data):
field_data = [field.data] if not isinstance(field.data, list) else field.data
if not (
all(isinstance(x, FileStorage) and x for x in field_data) and field_data
):
return
file_size = len(field.data.read())
field.data.seek(0) # reset cursor position to beginning of file
for f in field_data:
file_size = len(f.read())
f.seek(0) # reset cursor position to beginning of file
if (file_size < self.min_size) or (file_size > self.max_size):
# the file is too small or too big => validation failure
raise ValidationError(
self.message
or field.gettext(
"File must be between {min_size} and {max_size} bytes.".format(
min_size=self.min_size, max_size=self.max_size
if (file_size < self.min_size) or (file_size > self.max_size):
# the file is too small or too big => validation failure
raise ValidationError(
self.message
or field.gettext(
"File must be between {min_size} and {max_size} bytes.".format(
min_size=self.min_size, max_size=self.max_size
)
)
)
)
file_size = FileSize

View File

@@ -1,9 +1,9 @@
import json
from urllib import request as http
from urllib.parse import urlencode
from flask import current_app
from flask import request
from werkzeug.urls import url_encode
from wtforms import ValidationError
RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify"
@@ -54,7 +54,7 @@ class Recaptcha:
if not verify_server:
verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT
data = url_encode(
data = urlencode(
{"secret": private_key, "remoteip": remote_addr, "response": response}
)

View File

@@ -1,6 +1,7 @@
from urllib.parse import urlencode
from flask import current_app
from flask import Markup
from werkzeug.urls import url_encode
from markupsafe import Markup
RECAPTCHA_SCRIPT_DEFAULT = "https://www.google.com/recaptcha/api.js"
RECAPTCHA_DIV_CLASS_DEFAULT = "g-recaptcha"
@@ -22,10 +23,10 @@ class RecaptchaWidget:
if not script:
script = RECAPTCHA_SCRIPT_DEFAULT
if params:
script += "?" + url_encode(params)
script += "?" + urlencode(params)
attrs = current_app.config.get("RECAPTCHA_DATA_ATTRS", {})
attrs["sitekey"] = public_key
snippet = " ".join(f'data-{k}="{attrs[k]}"' for k in attrs) # noqa: B028
snippet = " ".join(f'data-{k}="{attrs[k]}"' for k in attrs) # noqa: B028, B907
div_class = current_app.config.get("RECAPTCHA_DIV_CLASS")
if not div_class:
div_class = RECAPTCHA_DIV_CLASS_DEFAULT