diff --git a/my_flask_app.py b/my_flask_app.py index efe9112fe2ae894ece2f7e39669ebdafff58a435..25ce3523f03fe88bd42ebde543cca6fb95b0c15d 100644 --- a/my_flask_app.py +++ b/my_flask_app.py @@ -2,7 +2,9 @@ import os import secrets from flask import Flask, render_template, request, redirect, url_for, send_from_directory, abort from flask_sqlalchemy import SQLAlchemy -from models import db, Project +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SubmitField +from wtforms.validators import DataRequired app = Flask(__name__, static_folder='static') app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 @@ -10,11 +12,17 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.abspath(os.path.j app.config['SECRET_KEY'] = secrets.token_hex(16) db = SQLAlchemy(app) + class Project(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) description = db.Column(db.Text, nullable=False) +class ProjectForm(FlaskForm): + title = StringField('Title', validators=[DataRequired()]) + description = TextAreaField('Description', validators=[DataRequired()]) + submit = SubmitField('Submit') + @app.route('/') def home(): try: @@ -46,23 +54,20 @@ def portfolio(): def contact(): return render_template('contact.html') -# Updated route for adding a project without Flask-WTF form +# Updated route for adding a project with Flask-WTF form @app.route('/add_project', methods=['GET', 'POST']) def add_project(): - if request.method == 'POST': - # Retrieve form data directly from request - title = request.form.get('title') - description = request.form.get('description') - - # Print or log the form data to check if it's received - print(f"Received form data - Title: {title}, Description: {description}") + form = ProjectForm() + if form.validate_on_submit(): + title = form.title.data + description = form.description.data new_project = Project(title=title, description=description) db.session.add(new_project) db.session.commit() return redirect(url_for('home')) - return render_template('add_project.html') + return render_template('add_project.html', form=form) # Updated route for serving the 'my-cv.docx' file @app.route('/download_cv') @@ -75,20 +80,17 @@ def download_cv(): @app.route('/download_assessment/<filename>') def download_assessment(filename): try: - file_path = f'static/{filename}' - print(f"Attempting to serve file: {file_path}") + file_path = os.path.join('static', filename) + app.logger.debug(f"Attempting to serve file: {file_path}") return send_from_directory('static', filename, as_attachment=True) except FileNotFoundError: - print(f"File not found: {file_path}") + app.logger.error(f"File not found: {file_path}") abort(404) # Return a 404 Not Found error except Exception as e: - print(f"Error serving assessment file: {str(e)}") - app.logger.exception(f"Error serving assessment file: {str(e)}") - abort(500) + app.logger.error(f"Error serving assessment file: {str(e)}") + abort(500) if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True, port=int(os.environ.get('PORT', 8080))) - - diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..92f1ff25658838ae9639052003770899dc3851cb --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA @@ -0,0 +1,72 @@ +Metadata-Version: 2.1 +Name: Flask-WTF +Version: 1.2.1 +Summary: Form rendering, validation, and CSRF protection for Flask with WTForms. +Project-URL: Documentation, https://flask-wtf.readthedocs.io/ +Project-URL: Changes, https://flask-wtf.readthedocs.io/changes/ +Project-URL: Source Code, https://github.com/wtforms/flask-wtf/ +Project-URL: Issue Tracker, https://github.com/wtforms/flask-wtf/issues/ +Project-URL: Chat, https://discord.gg/pallets +Maintainer: WTForms +License: Copyright 2010 WTForms + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License-File: LICENSE.rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.8 +Requires-Dist: flask +Requires-Dist: itsdangerous +Requires-Dist: wtforms +Provides-Extra: email +Requires-Dist: email-validator; extra == 'email' +Description-Content-Type: text/x-rst + +Flask-WTF +========= + +Simple integration of Flask and WTForms, including CSRF, file upload, +and reCAPTCHA. + +Links +----- + +- Documentation: https://flask-wtf.readthedocs.io/ +- Changes: https://flask-wtf.readthedocs.io/changes/ +- PyPI Releases: https://pypi.org/project/Flask-WTF/ +- Source Code: https://github.com/wtforms/flask-wtf/ +- Issue Tracker: https://github.com/wtforms/flask-wtf/issues/ +- Chat: https://discord.gg/pallets diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..a7814665f525f142935e652b0eafe2213194eccf --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD @@ -0,0 +1,26 @@ +flask_wtf-1.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask_wtf-1.2.1.dist-info/METADATA,sha256=9Y5upDJ7WU2m2l4erWImF3HcVSWIZKH3TdX6klYpq4M,3373 +flask_wtf-1.2.1.dist-info/RECORD,, +flask_wtf-1.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_wtf-1.2.1.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87 +flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst,sha256=1fGQNkUVeMs27u8EyZ6_fXyi5w3PBDY2UZvEIOFafGI,1475 +flask_wtf/__init__.py,sha256=x6ydw5SJzsXZgz-Y6IM_95Sy8VufRepvZH1DUIlFoTo,214 +flask_wtf/__pycache__/__init__.cpython-311.pyc,, +flask_wtf/__pycache__/_compat.cpython-311.pyc,, +flask_wtf/__pycache__/csrf.cpython-311.pyc,, +flask_wtf/__pycache__/file.cpython-311.pyc,, +flask_wtf/__pycache__/form.cpython-311.pyc,, +flask_wtf/__pycache__/i18n.cpython-311.pyc,, +flask_wtf/_compat.py,sha256=N3sqC9yzFWY-3MZ7QazX1sidvkO3d5yy4NR6lkp0s94,248 +flask_wtf/csrf.py,sha256=O-fjnWygxxi_FsIU2koua97ZpIhiOJVDHA57dXLpvTA,10171 +flask_wtf/file.py,sha256=AsfkYTCgtqGWySimc_NjeAxg-DtpdcthhqMLrXIDAhU,4706 +flask_wtf/form.py,sha256=TmR7xCrxin2LHp6thn7fq1OeU8aLB7xsZzvv52nH7Ss,4049 +flask_wtf/i18n.py,sha256=TyO8gqt9DocHMSaNhj0KKgxoUrPYs-G1nVW-jns0SOw,1166 +flask_wtf/recaptcha/__init__.py,sha256=m4eNGoU3Q0Wnt_wP8VvOlA0mwWuoMtAcK9pYT7sPFp8,106 +flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc,, +flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc,, +flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc,, +flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc,, +flask_wtf/recaptcha/fields.py,sha256=M1-RFuUKOsJAzsLm3xaaxuhX2bB9oRqS-HVSN-NpkmI,433 +flask_wtf/recaptcha/validators.py,sha256=3sd1mUQT3Y3D_WJeKwecxUGstnhh_QD-A_dEBJfkf6s,2434 +flask_wtf/recaptcha/widgets.py,sha256=J_XyxAZt3uB15diIMnkXXGII2dmsWCsVsKV3KQYn4Ns,1512 diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..ba1a8af28bcccdacebb8c22dfda1537447a1a58a --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.18.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst new file mode 100644 index 0000000000000000000000000000000000000000..63c3617a2d7164d30cae358c23eb3f75b5a758a1 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 WTForms + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/Lib/site-packages/flask_wtf/__init__.py b/venv/Lib/site-packages/flask_wtf/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..be2649e26d8dfa2cde5457f13b72715135d12b5a --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/__init__.py @@ -0,0 +1,8 @@ +from .csrf import CSRFProtect +from .form import FlaskForm +from .form import Form +from .recaptcha import Recaptcha +from .recaptcha import RecaptchaField +from .recaptcha import RecaptchaWidget + +__version__ = "1.2.1" diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c0573b1438c137de88c7976d43ddff37c7ebb80 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf6047bf2e16901a5247250b9ec7f46df2d116ef Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a7b10bdeeef961ea8006709ee5d3d18fdcf34cd Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff1baf217281277c65ce5ebaa756d3a5b1c90463 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68c6e91088694c15d2928b11f2eebd165dd1d435 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b7bd93f3fe31e810f7d0208a568d028cfbf0c82 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/_compat.py b/venv/Lib/site-packages/flask_wtf/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..50973e063bbdbd6982fc9501221603efbc2e88f9 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/_compat.py @@ -0,0 +1,11 @@ +import warnings + + +class FlaskWTFDeprecationWarning(DeprecationWarning): + pass + + +warnings.simplefilter("always", FlaskWTFDeprecationWarning) +warnings.filterwarnings( + "ignore", category=FlaskWTFDeprecationWarning, module="wtforms|flask_wtf" +) diff --git a/venv/Lib/site-packages/flask_wtf/csrf.py b/venv/Lib/site-packages/flask_wtf/csrf.py new file mode 100644 index 0000000000000000000000000000000000000000..06afa0cd4ef3670ca3357d47bbecc2baa7e18fe0 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/csrf.py @@ -0,0 +1,329 @@ +import hashlib +import hmac +import logging +import os +from urllib.parse import urlparse + +from flask import Blueprint +from flask import current_app +from flask import g +from flask import request +from flask import session +from itsdangerous import BadData +from itsdangerous import SignatureExpired +from itsdangerous import URLSafeTimedSerializer +from werkzeug.exceptions import BadRequest +from wtforms import ValidationError +from wtforms.csrf.core import CSRF + +__all__ = ("generate_csrf", "validate_csrf", "CSRFProtect") +logger = logging.getLogger(__name__) + + +def generate_csrf(secret_key=None, token_key=None): + """Generate a CSRF token. The token is cached for a request, so multiple + calls to this function will generate the same token. + + During testing, it might be useful to access the signed token in + ``g.csrf_token`` and the raw token in ``session['csrf_token']``. + + :param secret_key: Used to securely sign the token. Default is + ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``. + :param token_key: Key where token is stored in session for comparison. + Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``. + """ + + secret_key = _get_config( + secret_key, + "WTF_CSRF_SECRET_KEY", + current_app.secret_key, + message="A secret key is required to use CSRF.", + ) + field_name = _get_config( + token_key, + "WTF_CSRF_FIELD_NAME", + "csrf_token", + message="A field name is required to use CSRF.", + ) + + if field_name not in g: + s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token") + + if field_name not in session: + session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest() + + try: + token = s.dumps(session[field_name]) + except TypeError: + session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest() + token = s.dumps(session[field_name]) + + setattr(g, field_name, token) + + return g.get(field_name) + + +def validate_csrf(data, secret_key=None, time_limit=None, token_key=None): + """Check if the given data is a valid CSRF token. This compares the given + signed token to the one stored in the session. + + :param data: The signed CSRF token to be checked. + :param secret_key: Used to securely sign the token. Default is + ``WTF_CSRF_SECRET_KEY`` or ``SECRET_KEY``. + :param time_limit: Number of seconds that the token is valid. Default is + ``WTF_CSRF_TIME_LIMIT`` or 3600 seconds (60 minutes). + :param token_key: Key where token is stored in session for comparison. + Default is ``WTF_CSRF_FIELD_NAME`` or ``'csrf_token'``. + + :raises ValidationError: Contains the reason that validation failed. + + .. versionchanged:: 0.14 + Raises ``ValidationError`` with a specific error message rather than + returning ``True`` or ``False``. + """ + + secret_key = _get_config( + secret_key, + "WTF_CSRF_SECRET_KEY", + current_app.secret_key, + message="A secret key is required to use CSRF.", + ) + field_name = _get_config( + token_key, + "WTF_CSRF_FIELD_NAME", + "csrf_token", + message="A field name is required to use CSRF.", + ) + time_limit = _get_config(time_limit, "WTF_CSRF_TIME_LIMIT", 3600, required=False) + + if not data: + raise ValidationError("The CSRF token is missing.") + + if field_name not in session: + raise ValidationError("The CSRF session token is missing.") + + s = URLSafeTimedSerializer(secret_key, salt="wtf-csrf-token") + + try: + token = s.loads(data, max_age=time_limit) + except SignatureExpired as e: + raise ValidationError("The CSRF token has expired.") from e + except BadData as e: + raise ValidationError("The CSRF token is invalid.") from e + + if not hmac.compare_digest(session[field_name], token): + raise ValidationError("The CSRF tokens do not match.") + + +def _get_config( + value, config_name, default=None, required=True, message="CSRF is not configured." +): + """Find config value based on provided value, Flask config, and default + value. + + :param value: already provided config value + :param config_name: Flask ``config`` key + :param default: default value if not provided or configured + :param required: whether the value must not be ``None`` + :param message: error message if required config is not found + :raises KeyError: if required config is not found + """ + + if value is None: + value = current_app.config.get(config_name, default) + + if required and value is None: + raise RuntimeError(message) + + return value + + +class _FlaskFormCSRF(CSRF): + def setup_form(self, form): + self.meta = form.meta + return super().setup_form(form) + + def generate_csrf_token(self, csrf_token_field): + return generate_csrf( + secret_key=self.meta.csrf_secret, token_key=self.meta.csrf_field_name + ) + + def validate_csrf_token(self, form, field): + if g.get("csrf_valid", False): + # already validated by CSRFProtect + return + + try: + validate_csrf( + field.data, + self.meta.csrf_secret, + self.meta.csrf_time_limit, + self.meta.csrf_field_name, + ) + except ValidationError as e: + logger.info(e.args[0]) + raise + + +class CSRFProtect: + """Enable CSRF protection globally for a Flask app. + + :: + + app = Flask(__name__) + csrf = CSRFProtect(app) + + Checks the ``csrf_token`` field sent with forms, or the ``X-CSRFToken`` + header sent with JavaScript requests. Render the token in templates using + ``{{ csrf_token() }}``. + + See the :ref:`csrf` documentation. + """ + + def __init__(self, app=None): + self._exempt_views = set() + self._exempt_blueprints = set() + + if app: + self.init_app(app) + + def init_app(self, app): + app.extensions["csrf"] = self + + app.config.setdefault("WTF_CSRF_ENABLED", True) + app.config.setdefault("WTF_CSRF_CHECK_DEFAULT", True) + app.config["WTF_CSRF_METHODS"] = set( + app.config.get("WTF_CSRF_METHODS", ["POST", "PUT", "PATCH", "DELETE"]) + ) + app.config.setdefault("WTF_CSRF_FIELD_NAME", "csrf_token") + app.config.setdefault("WTF_CSRF_HEADERS", ["X-CSRFToken", "X-CSRF-Token"]) + app.config.setdefault("WTF_CSRF_TIME_LIMIT", 3600) + app.config.setdefault("WTF_CSRF_SSL_STRICT", True) + + app.jinja_env.globals["csrf_token"] = generate_csrf + app.context_processor(lambda: {"csrf_token": generate_csrf}) + + @app.before_request + def csrf_protect(): + if not app.config["WTF_CSRF_ENABLED"]: + return + + if not app.config["WTF_CSRF_CHECK_DEFAULT"]: + return + + if request.method not in app.config["WTF_CSRF_METHODS"]: + return + + if not request.endpoint: + return + + if app.blueprints.get(request.blueprint) in self._exempt_blueprints: + return + + view = app.view_functions.get(request.endpoint) + dest = f"{view.__module__}.{view.__name__}" + + if dest in self._exempt_views: + return + + self.protect() + + def _get_csrf_token(self): + # find the token in the form data + field_name = current_app.config["WTF_CSRF_FIELD_NAME"] + base_token = request.form.get(field_name) + + if base_token: + return base_token + + # if the form has a prefix, the name will be {prefix}-csrf_token + for key in request.form: + if key.endswith(field_name): + csrf_token = request.form[key] + + if csrf_token: + return csrf_token + + # find the token in the headers + for header_name in current_app.config["WTF_CSRF_HEADERS"]: + csrf_token = request.headers.get(header_name) + + if csrf_token: + return csrf_token + + return None + + def protect(self): + if request.method not in current_app.config["WTF_CSRF_METHODS"]: + return + + try: + validate_csrf(self._get_csrf_token()) + except ValidationError as e: + logger.info(e.args[0]) + self._error_response(e.args[0]) + + if request.is_secure and current_app.config["WTF_CSRF_SSL_STRICT"]: + if not request.referrer: + self._error_response("The referrer header is missing.") + + good_referrer = f"https://{request.host}/" + + if not same_origin(request.referrer, good_referrer): + self._error_response("The referrer does not match the host.") + + g.csrf_valid = True # mark this request as CSRF valid + + def exempt(self, view): + """Mark a view or blueprint to be excluded from CSRF protection. + + :: + + @app.route('/some-view', methods=['POST']) + @csrf.exempt + def some_view(): + ... + + :: + + bp = Blueprint(...) + csrf.exempt(bp) + + """ + + if isinstance(view, Blueprint): + self._exempt_blueprints.add(view) + return view + + if isinstance(view, str): + view_location = view + else: + view_location = ".".join((view.__module__, view.__name__)) + + self._exempt_views.add(view_location) + return view + + def _error_response(self, reason): + raise CSRFError(reason) + + +class CSRFError(BadRequest): + """Raise if the client sends invalid CSRF data with the request. + + Generates a 400 Bad Request response with the failure reason by default. + Customize the response by registering a handler with + :meth:`flask.Flask.errorhandler`. + """ + + description = "CSRF validation failed." + + +def same_origin(current_uri, compare_uri): + current = urlparse(current_uri) + compare = urlparse(compare_uri) + + return ( + current.scheme == compare.scheme + and current.hostname == compare.hostname + and current.port == compare.port + ) diff --git a/venv/Lib/site-packages/flask_wtf/file.py b/venv/Lib/site-packages/flask_wtf/file.py new file mode 100644 index 0000000000000000000000000000000000000000..a720dff8d81911df179e80512caa0056a47be410 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/file.py @@ -0,0 +1,147 @@ +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 + + +class FileField(_FileField): + """Werkzeug-aware subclass of :class:`wtforms.fields.FileField`.""" + + def process_formdata(self, valuelist): + valuelist = (x for x in valuelist if isinstance(x, FileStorage) and x) + data = next(valuelist, None) + + if data is not None: + self.data = data + else: + 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 uploaded files(s) is a Werkzeug + :class:`~werkzeug.datastructures.FileStorage` object. + + :param message: error message + + You can also use the synonym ``file_required``. + """ + + def __call__(self, form, field): + 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.") + ) + + +file_required = FileRequired + + +class FileAllowed: + """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 + :class:`~flaskext.uploads.UploadSet` + :param message: error message + + You can also use the synonym ``file_allowed``. + """ + + def __init__(self, upload_set, message=None): + self.upload_set = upload_set + self.message = message + + def __call__(self, form, field): + 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 + + filenames = [f.filename.lower() for f in field_data] + + 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)) + ) + + 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(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. + :param max_size: maximum allowed file size (in bytes). + :param message: error message + + You can also use the synonym ``file_size``. + """ + + def __init__(self, max_size, min_size=0, message=None): + self.min_size = min_size + self.max_size = max_size + self.message = message + + def __call__(self, form, field): + 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 + + 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 + ) + ) + ) + + +file_size = FileSize diff --git a/venv/Lib/site-packages/flask_wtf/form.py b/venv/Lib/site-packages/flask_wtf/form.py new file mode 100644 index 0000000000000000000000000000000000000000..c7f52e022c82fe43d6674377e5df040c82d10d79 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/form.py @@ -0,0 +1,127 @@ +from flask import current_app +from flask import request +from flask import session +from markupsafe import Markup +from werkzeug.datastructures import CombinedMultiDict +from werkzeug.datastructures import ImmutableMultiDict +from werkzeug.utils import cached_property +from wtforms import Form +from wtforms.meta import DefaultMeta +from wtforms.widgets import HiddenInput + +from .csrf import _FlaskFormCSRF + +try: + from .i18n import translations +except ImportError: + translations = None # babel not installed + + +SUBMIT_METHODS = {"POST", "PUT", "PATCH", "DELETE"} +_Auto = object() + + +class FlaskForm(Form): + """Flask-specific subclass of WTForms :class:`~wtforms.form.Form`. + + If ``formdata`` is not specified, this will use :attr:`flask.request.form` + and :attr:`flask.request.files`. Explicitly pass ``formdata=None`` to + prevent this. + """ + + class Meta(DefaultMeta): + csrf_class = _FlaskFormCSRF + csrf_context = session # not used, provided for custom csrf_class + + @cached_property + def csrf(self): + return current_app.config.get("WTF_CSRF_ENABLED", True) + + @cached_property + def csrf_secret(self): + return current_app.config.get("WTF_CSRF_SECRET_KEY", current_app.secret_key) + + @cached_property + def csrf_field_name(self): + return current_app.config.get("WTF_CSRF_FIELD_NAME", "csrf_token") + + @cached_property + def csrf_time_limit(self): + return current_app.config.get("WTF_CSRF_TIME_LIMIT", 3600) + + def wrap_formdata(self, form, formdata): + if formdata is _Auto: + if _is_submitted(): + if request.files: + return CombinedMultiDict((request.files, request.form)) + elif request.form: + return request.form + elif request.is_json: + return ImmutableMultiDict(request.get_json()) + + return None + + return formdata + + def get_translations(self, form): + if not current_app.config.get("WTF_I18N_ENABLED", True): + return super().get_translations(form) + + return translations + + def __init__(self, formdata=_Auto, **kwargs): + super().__init__(formdata=formdata, **kwargs) + + def is_submitted(self): + """Consider the form submitted if there is an active request and + the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``. + """ + + return _is_submitted() + + def validate_on_submit(self, extra_validators=None): + """Call :meth:`validate` only if the form is submitted. + This is a shortcut for ``form.is_submitted() and form.validate()``. + """ + return self.is_submitted() and self.validate(extra_validators=extra_validators) + + def hidden_tag(self, *fields): + """Render the form's hidden fields in one call. + + A field is considered hidden if it uses the + :class:`~wtforms.widgets.HiddenInput` widget. + + If ``fields`` are given, only render the given fields that + are hidden. If a string is passed, render the field with that + name if it exists. + + .. versionchanged:: 0.13 + + No longer wraps inputs in hidden div. + This is valid HTML 5. + + .. versionchanged:: 0.13 + + Skip passed fields that aren't hidden. + Skip passed names that don't exist. + """ + + def hidden_fields(fields): + for f in fields: + if isinstance(f, str): + f = getattr(self, f, None) + + if f is None or not isinstance(f.widget, HiddenInput): + continue + + yield f + + return Markup("\n".join(str(f) for f in hidden_fields(fields or self))) + + +def _is_submitted(): + """Consider the form submitted if there is an active request and + the method is ``POST``, ``PUT``, ``PATCH``, or ``DELETE``. + """ + + return bool(request) and request.method in SUBMIT_METHODS diff --git a/venv/Lib/site-packages/flask_wtf/i18n.py b/venv/Lib/site-packages/flask_wtf/i18n.py new file mode 100644 index 0000000000000000000000000000000000000000..1cc0e9c5a6d2fee8d18c4f46a79ed82f93d132a7 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/i18n.py @@ -0,0 +1,47 @@ +from babel import support +from flask import current_app +from flask import request +from flask_babel import get_locale +from wtforms.i18n import messages_path + +__all__ = ("Translations", "translations") + + +def _get_translations(): + """Returns the correct gettext translations. + Copy from flask-babel with some modifications. + """ + + if not request: + return None + + # babel should be in extensions for get_locale + if "babel" not in current_app.extensions: + return None + + translations = getattr(request, "wtforms_translations", None) + + if translations is None: + translations = support.Translations.load( + messages_path(), [get_locale()], domain="wtforms" + ) + request.wtforms_translations = translations + + return translations + + +class Translations: + def gettext(self, string): + t = _get_translations() + return string if t is None else t.ugettext(string) + + def ngettext(self, singular, plural, n): + t = _get_translations() + + if t is None: + return singular if n == 1 else plural + + return t.ungettext(singular, plural, n) + + +translations = Translations() diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3100d37e3389219d98787b585357edbe0d9bcc37 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py @@ -0,0 +1,3 @@ +from .fields import RecaptchaField +from .validators import Recaptcha +from .widgets import RecaptchaWidget diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f546fdcc15a0e8960fc72d690940a98e3e7e797 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2448d610b0c6db964cd16355a47e1ca99fa9e123 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daaff04283480f89350a9442f17337effb27f9bc Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2e05915fccf962c1146aeba49c1a004f3638e79 Binary files /dev/null and b/venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc differ diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py new file mode 100644 index 0000000000000000000000000000000000000000..e91fd092f98c01932a90ffafe68bbf98390ff2ed --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/recaptcha/fields.py @@ -0,0 +1,17 @@ +from wtforms.fields import Field + +from . import widgets +from .validators import Recaptcha + +__all__ = ["RecaptchaField"] + + +class RecaptchaField(Field): + widget = widgets.RecaptchaWidget() + + # error message if recaptcha validation fails + recaptcha_error = None + + def __init__(self, label="", validators=None, **kwargs): + validators = validators or [Recaptcha()] + super().__init__(label, validators, **kwargs) diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..c5cafb3478cd644a199fb731cf5d2c0c440f986c --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/recaptcha/validators.py @@ -0,0 +1,75 @@ +import json +from urllib import request as http +from urllib.parse import urlencode + +from flask import current_app +from flask import request +from wtforms import ValidationError + +RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify" +RECAPTCHA_ERROR_CODES = { + "missing-input-secret": "The secret parameter is missing.", + "invalid-input-secret": "The secret parameter is invalid or malformed.", + "missing-input-response": "The response parameter is missing.", + "invalid-input-response": "The response parameter is invalid or malformed.", +} + + +__all__ = ["Recaptcha"] + + +class Recaptcha: + """Validates a ReCaptcha.""" + + def __init__(self, message=None): + if message is None: + message = RECAPTCHA_ERROR_CODES["missing-input-response"] + self.message = message + + def __call__(self, form, field): + if current_app.testing: + return True + + if request.is_json: + response = request.json.get("g-recaptcha-response", "") + else: + response = request.form.get("g-recaptcha-response", "") + remote_ip = request.remote_addr + + if not response: + raise ValidationError(field.gettext(self.message)) + + if not self._validate_recaptcha(response, remote_ip): + field.recaptcha_error = "incorrect-captcha-sol" + raise ValidationError(field.gettext(self.message)) + + def _validate_recaptcha(self, response, remote_addr): + """Performs the actual validation.""" + try: + private_key = current_app.config["RECAPTCHA_PRIVATE_KEY"] + except KeyError: + raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") from None + + verify_server = current_app.config.get("RECAPTCHA_VERIFY_SERVER") + if not verify_server: + verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT + + data = urlencode( + {"secret": private_key, "remoteip": remote_addr, "response": response} + ) + + http_response = http.urlopen(verify_server, data.encode("utf-8")) + + if http_response.code != 200: + return False + + json_resp = json.loads(http_response.read()) + + if json_resp["success"]: + return True + + for error in json_resp.get("error-codes", []): + if error in RECAPTCHA_ERROR_CODES: + raise ValidationError(RECAPTCHA_ERROR_CODES[error]) + + return False diff --git a/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py new file mode 100644 index 0000000000000000000000000000000000000000..bfae830bb17f188f2123931847f958590c77e690 --- /dev/null +++ b/venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py @@ -0,0 +1,43 @@ +from urllib.parse import urlencode + +from flask import current_app +from markupsafe import Markup + +RECAPTCHA_SCRIPT_DEFAULT = "https://www.google.com/recaptcha/api.js" +RECAPTCHA_DIV_CLASS_DEFAULT = "g-recaptcha" +RECAPTCHA_TEMPLATE = """ +<script src='%s' async defer></script> +<div class="%s" %s></div> +""" + +__all__ = ["RecaptchaWidget"] + + +class RecaptchaWidget: + def recaptcha_html(self, public_key): + html = current_app.config.get("RECAPTCHA_HTML") + if html: + return Markup(html) + params = current_app.config.get("RECAPTCHA_PARAMETERS") + script = current_app.config.get("RECAPTCHA_SCRIPT") + if not script: + script = RECAPTCHA_SCRIPT_DEFAULT + if 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, B907 + div_class = current_app.config.get("RECAPTCHA_DIV_CLASS") + if not div_class: + div_class = RECAPTCHA_DIV_CLASS_DEFAULT + return Markup(RECAPTCHA_TEMPLATE % (script, div_class, snippet)) + + def __call__(self, field, error=None, **kwargs): + """Returns the recaptcha input HTML.""" + + try: + public_key = current_app.config["RECAPTCHA_PUBLIC_KEY"] + except KeyError: + raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set") from None + + return self.recaptcha_html(public_key)