From 940312f977e705bb436be614e63074072b54ad77 Mon Sep 17 00:00:00 2001 From: Felix Chadwick-Smith <chadwick-smithff@cardiff.ac.uk> Date: Wed, 10 Jan 2024 19:39:57 +0000 Subject: [PATCH] changed location to site.db and added Flask_WTF --- my_flask_app.py | 38 +- .../flask_wtf-1.2.1.dist-info/INSTALLER | 1 + .../flask_wtf-1.2.1.dist-info/METADATA | 72 ++++ .../flask_wtf-1.2.1.dist-info/RECORD | 26 ++ .../flask_wtf-1.2.1.dist-info/REQUESTED | 0 .../flask_wtf-1.2.1.dist-info/WHEEL | 4 + .../licenses/LICENSE.rst | 28 ++ venv/Lib/site-packages/flask_wtf/__init__.py | 8 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 532 bytes .../__pycache__/_compat.cpython-311.pyc | Bin 0 -> 698 bytes .../__pycache__/csrf.cpython-311.pyc | Bin 0 -> 15040 bytes .../__pycache__/file.cpython-311.pyc | Bin 0 -> 8377 bytes .../__pycache__/form.cpython-311.pyc | Bin 0 -> 7261 bytes .../__pycache__/i18n.cpython-311.pyc | Bin 0 -> 2060 bytes venv/Lib/site-packages/flask_wtf/_compat.py | 11 + venv/Lib/site-packages/flask_wtf/csrf.py | 329 ++++++++++++++++++ venv/Lib/site-packages/flask_wtf/file.py | 147 ++++++++ venv/Lib/site-packages/flask_wtf/form.py | 127 +++++++ venv/Lib/site-packages/flask_wtf/i18n.py | 47 +++ .../flask_wtf/recaptcha/__init__.py | 3 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 380 bytes .../__pycache__/fields.cpython-311.pyc | Bin 0 -> 1134 bytes .../__pycache__/validators.cpython-311.pyc | Bin 0 -> 3739 bytes .../__pycache__/widgets.cpython-311.pyc | Bin 0 -> 2875 bytes .../flask_wtf/recaptcha/fields.py | 17 + .../flask_wtf/recaptcha/validators.py | 75 ++++ .../flask_wtf/recaptcha/widgets.py | 43 +++ 27 files changed, 958 insertions(+), 18 deletions(-) create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/INSTALLER create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/METADATA create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/RECORD create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/REQUESTED create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/WHEEL create mode 100644 venv/Lib/site-packages/flask_wtf-1.2.1.dist-info/licenses/LICENSE.rst create mode 100644 venv/Lib/site-packages/flask_wtf/__init__.py create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/__init__.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/_compat.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/csrf.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/file.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/form.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/__pycache__/i18n.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/_compat.py create mode 100644 venv/Lib/site-packages/flask_wtf/csrf.py create mode 100644 venv/Lib/site-packages/flask_wtf/file.py create mode 100644 venv/Lib/site-packages/flask_wtf/form.py create mode 100644 venv/Lib/site-packages/flask_wtf/i18n.py create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__init__.py create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/__init__.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/fields.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/validators.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/__pycache__/widgets.cpython-311.pyc create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/fields.py create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/validators.py create mode 100644 venv/Lib/site-packages/flask_wtf/recaptcha/widgets.py diff --git a/my_flask_app.py b/my_flask_app.py index efe9112..25ce352 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 0000000..a1b589e --- /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 0000000..92f1ff2 --- /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 0000000..a781466 --- /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 0000000..e69de29 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 0000000..ba1a8af --- /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 0000000..63c3617 --- /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 0000000..be2649e --- /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 GIT binary patch literal 532 zcmY+AKTE?v7{>3Kq)A#@U33w_MWKV$R&)??sRRU3X%Qg=j+k6(Fn`MBTCtPg!p&9O z{UDhnbP}9gMCjJZ_d;99dk^>ip6B2_?%ry(3WECheWjn!zlB*;X%5pazzcGbivtv5 z6Jx=ppcG1`l(PgZn{v)_poC?!oU;O~nrhBvV9nHkRa1Ah9%|@67zQ`+q}u6qjcXQj z>Ts~S5!lIt5wj4CJnwp!I`)7&_cny}r7?UOxR9(YlUv{IQ$AK&&33bO*{DopCt)6w z;Hru2!B8dxGDUcuErznP2vxCZ;-pH*h_b|wBSPNM6e2Xc>Kt1)38a?O-al*|w2!Q_ z*cpa2;)!)0+I^Z>BN~mY3;)had`@=<w)2q3o_G;?<lY`3e&lmPnuDjAb`r+!FrcSw z73Pc2NXD@JNf_e{ZKg$KXd^8uL+fc#8QMyV%FuRNREBoaqQ2^D&sDKl9j^ZeCXJKS literal 0 HcmV?d00001 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 GIT binary patch literal 698 zcmZWnJ#P~+7=G<b4!25M0fI<OFlOMRtzbwH3sDggWC2tuI>|D3PB?mB*q25@3^(ut z*t#(mAsG1~htwhR%EW@MN(a;l`>q#M^|k!GAAbCPSpKrMwhCH){Q1oPYW>SNwKUI+ zh0w+Uc;KM`hJfJ-c=#iD7DtB`z&E`s8Db0^-Y$Kw!SNy1481Es`nF~?YTs*e?AhNh zozH`Gr5Dg4zYT5R)?}m6nX_2e#(~Z?L>>%r$;ax(PQc{e?#rD%PXu>a>c`P86OkW{ z2OUchN+T9>O4TZ*VeCx;W49=MH(^0_@)Z`Q!s*~~*LfwmkdC``@BZfY)&r*>yOWSd zsdS!(Y|N#z&!c_kng806e#&nr%-t))Bhv$&rlY&mjl+bccaoVf6PI8NxuGFju6cuG zeX&wAl+A)En@MH+<0uyV{biji7NPvG%-q3>=;`=47Bgj6lXRMjOsiR?x^nRW%Ic~m z{V)mm$PZF3)Ow}sYl4^_%0|quI3*9O)Q`kf?V6{TnTEnb2+d(5D_3D7v{JxD$Zu^Q zoAAm1GXm&(Mvmaxr#Jb{?oa3)K`)2i9NH$@o;O-~do%AoFD#7kF=#k3P@IRd!+&#} BvWNfx literal 0 HcmV?d00001 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 GIT binary patch literal 15040 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 literal 0 HcmV?d00001 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 GIT binary patch literal 8377 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 literal 0 HcmV?d00001 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 GIT binary patch literal 7261 zcmcf`TWl29_0GP%UVDw#VDt2ZK+KY4dBrGkdDPhJ;DFZ@n*b})*<oka9@yP+?#yCv zOyWqc3KFfHiimzBmD?r+L@MPYk@8ciks7t1v(ZXfjf8|$MM{1uQxXaB)pPFb?Cy9? z+9*}8XAgJIx%bXJ&wI}0gO-*cf%fq~e<v5ig!}_v?B=a>9{dYBH;6<eE=zJ;lH;)N zg1#&1VtqIC-AOm=d$OLKH|b@4Z`POdC;hDNgMJ_xV10kKDHluzSwE0%&V`a8)^EzT z<XV%htRKvVb8X2s=r>ED?5bRQvYjJM_|}q*<T{ca9LcvlN#xaA2*t0pCOb}(yl0wR zbe|^E+$530f8>&DTtx1?3H!feb48L}F7hIg+UAM0N)Fw00}XywORkmL=Sgy%)H+6@ zk#7(g<;>=Efl@iI3*yuiy!xs9YC+a?>}s;6DQX_Nz7dh0FCcx@fSNn2<Yj53kkyqz zC5<E7hjY1tE}qTG_1JJ)Oi#*^Fh$iVnd+Bdpl3*>Irz7EP|k?3z=*7i_<C58Bso8v zpDO6zaMr$Bg`updoyQ3SW5<VJR7j^{UdxI)>`6-;@W43JeeA@z={Yt!Hg0;4^^Xr6 zHhqKfqw(>08rid3RoO2C=9i#unIt)hBwZ4hbTfguKwxg!vrJ%K*_Yw2kPIhz=9`j! z$qV8ONKKM|o=Y}K0eA~uAs4x1GYB+jHdQw~bDwpe)u!aMl2OvURydmmZZuxa@F&NS zCyno8n7-8O(|QIGF@!OsrDDM#4{bQZr&5TOL|sg!ctzv$s?Jwdm8EBReG+ifN;b<E zG@0)cb)EL5GAOrLMUQON6kATrOOF|%WMwTC<N5f-sjQM#^z0>m3U^!k`u>EPmthOK z%9fg<@&&LN_Wc;cIOOfB2>!wRGI`7Hq(}B9eX?KT=E*YC4FDGOungM-Sa$<92v|=8 zwwY-t#5Crc4<uWdcF^4Yk6NqRiJ{e{(ah|Iri5<t;1GZtM7Qzq@44!@MVqTSDuJ*e zxo!gGPL&E@b0^F;P_uxBE5sB1hmOVv$8WnSe#7*o)qF;oFx?Zfej8juld~BOzrsH{ zGSGMCgeFt%OnUdu-d%fk?>#f9rVBX`wRUDWCr-%P8Ib#hGe?!PXEa5ZpPdrZ=NXzQ zT>w+s!OUptlIclnG&2)%vK>R@7WkO90l*yjDzd2*+4^N9`b8vaY~S~(RE!LiA_GQf z-~rId%2Glv`k_*C$Q0$kT7%?V2{Tfa+*o|zczj$q5<eaFFxk=dFpJ{O&7dZysjLg< z<x6xUAS?;eZlpCMlY%v1X59(`sL;0IJe;vy4hKJ9U+O<ujGQV(P8p$7^a&WVvMBy{ z22*2uXgGd!P)PKT#LXb%L|4ztdAbfZ(ZFh$4ag{RRub}JPM+Dc61z3}GsyBvsz3Ob z4aG>J6iFDN#Q&M!@!^rUaCCTNcwA{~1s=bLQC-Q&LRQHs`pS&f=sO?JXh$(JREi84 zp&^UWsx`utY9!bU9(jYzy6Ww=K<F>@y3x)^MGHF4uC#O2adfNRWMc_gd5i;VpH?K} zx(U0ugRz0NG3V^+I2u}c{<85tdmYN_kPg{RudV~pE?{Gwye;2{M8-fiW8^lMh&GvC zp}(N3W{aR`0-SW99bJ|v3d8hf@NA)e?0YaWnSK~_PE+#~KV=3W2(s6xn{EOG-OaG} zp{Jn~;Uu*Dx@DS*Q-V#K*|5yi9n|MwqV^hqIZ_UHzT0brd3f$d__q#TKe$AEtA@C{ zotv&-ULxMr(Cyy&Nzcd6e%|@l4aTlSv3s=CJ!*7~-d)STvtfz2*@&m!nKineMpX9_ z;TUSyCq1Rzhl;xgOS=b)-SJX)+~|tm>s-H}-;1ojGHRJu^<aR_RL$!;yu8i*&>*_} zsL`I#QL>fhRM}#kG1?;tndO$NQfE1`HKOk58s7DMqIz^ieapkIS1UkHXSSjG>eDic zhUL&@lOV7}M-VE?oMXzQeK2sQdzlKCFNfh(+YjL39J#ylQ3bka*?YHh!&}qWr@!pn z@kQs3k37ZB-co0;(boG_q}vE}Gd)DZW<X#uQV`6bAmmi3kd>hy5`<R^Viu#PAW#TE zaJaCj%C=3>JIs)UQ1iNcQK#t6lt-`?K@<T7R=O7fG6ue2ah;a4SwXnX(VqZTLr?yx zWy>B+LTD!-c%=Ld0Ptx4l~+G`C0Op<bR~4JscXr_H4SpWC&2q|5?p(&yMpq;3wBR4 z>gjqapls?WqmI^70cBHPWYibysetl@n!xa@V1uWLUn2h<VA7@oz)-s0^iht@@Oy9o z0Ghsw=+($ZmSugltV@C%eU?+WS=W2quQ@E9n32+6Smic}y66kgLCq?8MHd7}6j&HR zvgtcNEz$|il0CWuM3Rd24Z+(?d(Han`G*0oJq-YqQ4Vzd%6rv+&0h+18G&AS7PkSo zm9bu`&~o!SUVybqK8bq)C$FP9ofLTCX08rEJ)=l6gyl(@$MA;rf)z!1C4*QhWAZQN zc`>ak7i7MY+yh2p**7r8Cg)^*Qk8IQDuqQ+D)kKZPS_o+kPxR*KwE`U3aSm9jxEYj zJU1J%j8J`GpIK8ATRr{=%+%0*=E!$J(zfyH8`s`20-Kn05)M<qppL%>Rw6k|7U89_ zIYp(ehH-U*tc{=LK5U3;Q5T3+gu18?MbekLAZ8V)qU02>=ApW=q-x0;gjE%HO%&r; zS)t5%ZBnIrx}f9eN<|c_=X+Ze)xg$@*`L`Kb&Az&!?+;|6}n0_u-su_!3@|_0KqJm zi4hIRKK0t|5rUep9Iy39UnBK6OxCsom?KL8($?{1nExWo7sH!N;mt<)<f8t;<v(0D zhF)I0T<ken>N#lyPBxk_p3Zo-JeUQ5B{M6kxcR_kSG_q)u9bCyC7TOUgjsHiyGopA z&fqd<cT$~i;2<9HAc*6*oUiwg9*v)5cLqGmZ9!Fe(5%d-LFbj~84bGMBBLUq>S9hN zOZH4K0!7EdSwr8cIzdBSKCMV_E@`p4ONtZ}-H5FFlvS)_BCbK5EP}^QfT!lc*U;eL zhQ;1KL)+WdCq=#bWt?Pj1+_S=2ezkEB|m}N$9fzq{<?)2=dcld682xW44!0Y;lP}H zQPE)M)^!33Bo^Z@Kz#`}DCtQtKOswfef-YYu06F8Fest&Srv$s$CL_X3->Zi#1)xK z%7vPzzy#Y_m@V>$$48Fxy+8D$W9OBr3YYc5K}qI&bXzV}S;1H9XCTk0Mp5S_6~{Bq zYnyabZ$2>TJ!sqS0eG7`LO4wI$sl=U_%c^--LssIo>*zlVwJ9{Pn@rUrAx$leRLTP zt~b{?*Q=EUdNtLC!w<evyFk#mYw=m`Wpab#$hDX7z@stPLey&p6-~)&x|mPPrW<U- z^n(rJrH|>e?2(3Wpxg9WUSV<>7E~OWm)T-zQLx6$JY5Ut6@o5K#P(%X@Fnd)%rQ6z z%WD|`55Fc~LwqT(UcW^AEnN&OaCajce=EKpDMq%!r2-@@t9IP&>R$9Linlt7U9nPE zY@z>Nq+{WQ_g}j8>W3HqyuP?4UfL30c%j^};oh3g1;6#T6t-7nVd)VUbZE~5pJ&>` z6yMg}E0N`zbH<U1G&^e2<mk^}e$-{UVms*x=xV6yO3;1Ov~NPr%NM8UftjA~mrr%z zG_J7?fF&BEI|0wFUGKXW`+x6y&u6{YL&hD1eKbeYb52$AcH%q^7>b7@Q(VeE34%5K z%yg`@t5Y}yd4cR@X?p-tN74ws>mY4w7Ni@>Zxm}cAGLhc@@eMN{NFvrog<~4BY-V- zCrSX<BuZ-%#qek;JZc22a>g>n5Z#Ac#LS#xK4<y`K~mF@KVyo=l0UiwdqW8Dpra!Q z&LHSVa0q~g$GMdc)76MX(1nDL0RRv7-;&Gg4M3#^p!F#)WTK7W%4_!#`)!SMj3ZAx z1xD!I`*`5<dZC!Bf>w|B6r{rqFx2fm2KfIBnMxTOY2cvRFxkQ1gJS@k`Jf}ffIRM0 zQyjIuo_4Mph;+1<O1psBFgNq&kIx`6_52apW7K1M&Z=tGs``D_N@nZWi9>L$BaFny z504Ix(Ku||5)t0IS-B=AoHOvN+)6>}7;1ale|7)0{YLZtcTc|a^FO_AY}{{n_FL|< z9<CJde%mTYF&0=AS}@1{he8uGvgJ^n(=t6jBNrxOczX`PvXIsbv~m|=T|Tr{hZJ-r ztFgMy^jEIqDb~ei$iAEhjznhMX4pnm@&Xoft#XhRj~->$^LbVWo6W<yDM%jUl&X}9 zxSmfvdrnU4mUA$hX9>4cFpmQJtm^at&B7$K2(18c$<1+GnQSziPnoPWoX<*_##y}v zaXe+R*Kj^%vfXe#WzuChpEB8EIG-}<F`Umm!W+(K$>Zj_%AU<e!}GB0ZwH6vAa>cH z9O;}pihn48u7?jFLR#DYoUv*TJaet(K<gFX)xfpDoOj75a2$H-<NsOk+_$LTItLB9 fz1#r^1r0!J2l0RA@%1;8&zk-HtKFY<Z~*@egp|Ub literal 0 HcmV?d00001 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 GIT binary patch literal 2060 zcmZ`(&2Jk;6rWkI?ag|lwAkPzZHcL>5>uKlAcTOBqE@OZsuW1!U|Fm--c90WckRxM zU7BtyA9`?6fnHDoq9~^jNF{pUz@OnDOIRyKii9}yf?TC==!rMutYa(m?abTv-h93J z&3o_1-d+^}{rdM0)>kq@e{v-t&;{+Sz;6|q$iy~sa0X*8OSa^MG9e*{K$bJIkYzjU zL^2T}hwZ4NWE7AinI1EG2W3<VT|lOC2bnz^5<*+hTa<}O$ck-%vK5$6Ml;npl#V^- zmNfRGv{Wn>2m>*eD-mM3OgD-}K0>T(C5!SAb<twFUC0@>1zN9TQEDt&R4*EADJ}Wh z6=Jy5HdwykQeR^oTCROC2w~_U=zISHTSW|bKvXjE900)D&LecSEtfG7<18A?M_sGU zkcl^7=Pkawpj*g8l?zM>bl8obE@(XxNS%Mycc67c#*RL3WD%S4!|>sgJ8!1<1gkv+ zz(-b$k1bXrE={o|E0rq%<T;iCfSL6<OLby5qNae(6^b`f3#8zr7HoqqpPe`6Ejx8R z&z4fO;8-cAVCEO{xfb}XC9{55%<>c0*+PLhRPWr@kM3LHDI;dVG4gI&B^({XqtjQx z56h+EEu-L<!5ASNSi;ftWxHUQgg*q~I1&w58WVm?ih-q`eQ<6%`w6uO&E_V?-Wq>n zVlsQakSjS5S~Ppn;gORqTW&dfDL<d3d1jq08o6a*7FX24ztQ~olsi(q;UCp`>~;E2 z;&2MjFzNxTpnp`P#hVC+6OF!jl{F(s>HB86{?hBUC~v~}SA6in+qF~Ezn$NX&(z~H zl}o$PcrAKtC;4(cdFGGg*zd`)9c|$5<$ISOT>aMm-d_9c(Zyy6^^ZS6Qos6?v#JJL z55M_kw62`mjU{Vxau3c&InDy&0C3tH1h$GSl))y-K<q-AhD-^#Y=$5TWPxR_9pEsV zBh35WhhixUwgRm?9B)aURFORFCOm1qV-`%@Cl7CceXBhN2jbbZB!Kgk(tfO^L%vKc zd%=%TM)K~Wj~V3-K+KUB;P3Ot=RvuWxYCi)Bm35qFonJftb%s56WiKQT^p+6#=zj( zK($;gZ{lCnpVdc`+r!iK;pv((O%gCS7t}wH56XcF3Y3GkzlUxtdvZnfAjy{nVuarw zd(s^8<mVyaNnWTsF#!c>_Ax&sfTE}523UkiKMeQ^1t3bcLHtP3E)m1_u}k<{Ql1}x zOn{9NtXz4q3!K1wny;A#)`iY)Osl@r7)q_rd{<s8-=DfawK-YS#&`RUe0}qan`=|+ zpKgA%-8WkA8?7m$0<hV1*jIGjH5^OVeO1>X-Agu?HC?|}GVIm}IR-}&{v<yku}-|8 z2r|K5;qFWbcPii#|C*8%ez%F*Oo@cYn&>cVZO2o8Hr-2N@E66v4~twCB|{VsKh{cK zQF%mZ@U{IyGs2Tulng(r>xOOXx`@S=f4p)Kj@N<X&y@FC_k5q6fdT$z^dYcj2xHtp z1GVnfKqqS5?f>SStf8*fK&NZnZC8D*5^Y8jxW9?oY$S#Om&5D{w>>==IF61SuX=ZH M-@9GW_$O}jAF@~B5dZ)H literal 0 HcmV?d00001 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 0000000..50973e0 --- /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 0000000..06afa0c --- /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 0000000..a720dff --- /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 0000000..c7f52e0 --- /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 0000000..1cc0e9c --- /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 0000000..3100d37 --- /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 GIT binary patch literal 380 zcmZ3^%ge<81Y6$COU(k(k3k$5V1hC}YXKS48B!Qh7;_kM8KW2(L2RZRrd;MIW-yyM zhb5OaiWSIa31-k_eaQ&apvicPFDNxRv7jV5Bhf80H75lq!U-1va`@q#@XVC-)Dk~U zwp(mzAeF^M%s>-vag`<JWTqsR<QEkev48~F%fSkZi&%lYl?<POB*U*rXRDad;?$zz zm}DaZQ$u4Tvly5B<kH;KyprM=&)mfH)Z&=3)V#77pUk9~;>?m%-GapAY_M=zPGWI( ze0fQlei6*2`tk9Zd6^~g@p=W7zc_4i^HWN5QtgU(fHr}ARqO~PJ}@&fGTvZNx`2vq TFeqL?MK>5!E?`4Npg;!zO^j@W literal 0 HcmV?d00001 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 GIT binary patch literal 1134 zcmb7CL2DC16rS15W;bcmq7quDXoXV9!M0Y~Ls1cxN@<CpmckMkx4V<L?QRlhHjPSz z912EI@gnsk6>3#_68s?%dI)<ec<MzW@gP0<X15y*dhzY-x9{z|dGoz*=0hr#L||XO zzonlsLf=FXnR4uOmcZFU7P7E|T&!b^EXk4`$(40E@MBKQRdfY>rGWLggl-}$z6lY< zyD~z1@P?+VmU;(ev_lbQu&>P9)Ugg>XA!AY+bU3xgPXWbO{46Y_YHrb>jueYSSiG) zaOWDtPk|&wI<}B5Nr=j8u_$#HoV_SOx?%xIE7m}HY{{DmU2UL;Sl57<!c<7h`pAqb zi&a91K6pF#0AAxQRFldBo*;c}z4Z(D3GDCvQK5H!O^Vjla81ffHmb=x_@$5)0SBDT z$Se*izQQYI%6yFwyJ&laWE4Nfsgw5=$5^F~pWHAU+cLZo<9>X-YOn(L6NH$K!8svZ z&>jV=AFW)U%Pn)tc+Q-jnz?XsdNy~XWL8{S^muN;H42pHHfV7pw`8y8xb4yNWy4$# ze&?asI;nd3Nfw>RWL_K^pC~_K{jkO3q22?Ui0BT&B%cHLRY$G#*=Bm`{ldG&oyG0T zjbuw3exW>5pR4=Y@SF5@vZ>9$_?QpBUF^VWl#y5hKG{q{NKrIgN{F8%#4TABNBI4O zJg68>IKxES{$Q8WN^qzAczCz`!EPIbGFD<i3x4WYHIq6H=#1G3D8j`FUu7o&gKH6h zV8ZVLKvU7vN{uyE_ETeDQ{(%o@%nr_j)q3+x7smDnQf#00CoxrXET~V0D_m8%O^sq znYc@V2tjvDC>vf8l@LQv4!C_nL4knrk#;H6=L$Om*`gI*1!&6{;}#li_KpLTZuX8A hI@jzS2Yn;W<mtNFj%yhD`Ws#?{}6wF{$na2_8aiZ8OZ<u literal 0 HcmV?d00001 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 GIT binary patch literal 3739 zcmb_fO>7&-6`tkpa!HXi6^VaRB$1Y7t4u;Fw&kFRZ5awlN2**|pcDgUWh^$_mAo?l zGP{&wf&v7=2OfMdiWKU>D1;u876u9-2#_Aro_r*Q4NNRRKtSO`E(HT8fZ<c$EPtep zq@srohmUXGyqS43^WHa)U%6a82+BAA{*$zAN9bRq(J5@3c~u5x2Z>0eGAKoUEg33n zNzoMH=?s%)Q>?)=8EckHaRzSz-jcEyJe#p)?I}C()>Mzki8j$L_K1$BOv)iTSE-cK zf<8u~>k$&2Us@1)2H#dmxkPFah0pvzn)6{w=dkoyQBqW3?M0lCa_PJ%LBo+QVl3rU zUMLix>AoXmWKmG%d@g};9+xKWt7<{{aAIO(V<Wnn&#z{rXgZ&rz*1T$sOkH{giw$t z6j_zlB`mKzEQOZtOCd!{V@VAa1T17FRl*@z31wwPk#nojQsguZIk!$$59M(vD`Zyk zI4g<K(kM~Kl2XX$6zP;HCr;NnandkT^0e)V$hksMjkGK-dD`kG=^LL}w_{AnXV&h> zb$haNI_dU(7(bocd36DrJ4iw)N<=A(NTq1eD$<Xr6eBW^P>K~<pjOcWH^^ZdD3^Tg zdy$lo5K2n1*4anr)15iV0)(gG`|1*q9aN^aTq<y#B2oh#7Hv{xs*HH5BM=d+Wtqnu z2*MWKn@q%}7nWi((|jVCyq)A@x8sRLoy$s!BCJZF!6;H@1v^1qA<KqBZ+;j1aQPEO z!pd^`()(8~UcU6fay*|dX2H3X<=HF@Q<m4I-1_n@`O{_MFp+|geqe}KGJ^7e-%wYM zxRv1wf||!lwD3^3@w}XqRh}>Pb<Z~1kp+kq!$7uB-O*RM(eS-neJ`&05|xkZ{*emv z*ja(kD^Ngbaz33x;?y{=zri`|prdEC);$>v_p^vprZWhkweB4FtV!P0+3g^};UM5d zi%6TJbd%17Ofy(J1OE3JOykM*8D*0x)5^H|R;P_n*|G~bb-a|(nt^1b=S$f2v+mB# zt!?m^UB~h9Ff(0d%==QJB2%{T#5;`g|Fy&LRd#-<<XMe$VF<VuLg=XEl>yxV)zWID z4N?&$p9#A#xfY#M0R{nxFu6vZlNEkV$>(&&p!BMw>I{J%-GQZSUX^%36fq_jqjS)y zN}sES+w1*&+eIb5YlZIavb;p_Gt6Qa48?vjo?VfpjHuhXrrO&}$OVO%79s_oA*$f- zX(5yOrN6gi#MPC50NFwfhfi~iG!bPF)cgHUCSN-G9^Ysp+8(DG{*f=rFZ|>C{_*Os zroOY({4vcRtISkpUY^p^@Vu>g#w+#%x1Y>-qtSPE_xx9pKS%bMZ=AK!>)Pn`THln` zH&t~`9k}~nxX1V1<A1U3g=_9>n)_N4S?o9JV^=E)&3X2}jcu}^XlUbJ*o7%3$w3=2 zISR0{gY2h7r3uUE+{igogpMKn7KPMqG?3Ucd$UMyTQ<<=^j)+;ogDYO-Dn1jx<zEn z^FW+23a54>2%$|zB{$wK5F3^<C9+THUyNf_cSi>hg<!l<%1oI(IZj2Zc?NUDZCcCL z!#EbX2L?s~cG&>Ci1W3}S~n)k*4-m^cKQJWfPL0&n%CUq%A9CFd_`dXo<&sVPB|;b zDl%IcNB7Qe+R8R{sJj4KM|e;T9=4D6m2HP>nJw9-9$z4P3p-1}1qqXYRYEEx3PM^f z3YkzlG$D<N9(~LflCyWFmlFJ3;%@2Od_Hve%pK_orSrKJc@>fvbzb)$s@zE=XK&u+ z7ZXX~!nAH}<qRaA2~fxdOaimS7S)x=uYY(B{7s!xis>{wnoA{zfd>(ib}8ZB78F@q zE9tg5>7kKs>CR*^r^;E$Xkj1Bp$AW~fR86`PJeQ12@^-v8Iq^z9He&nf|S#(tprbJ z$a>gIG}ugD5EY%lk|1IN<OZmP?Ja;JrepGu>W%`I*98E+2hu~`)ynF4c-Mi`8AuHU z-9^>{&}##(ogk;t)LCPb3VCP@FzW&u9t2ToIN8aIq9=f76jYQ+cz(7}lcDVkRAcbG zHuzpO64wS3mAT5?>qZNh7AOKw9opy{sRrY<zJ%77cs^Nm-fq&Q^Y!c3KU>kjm=<{F zMIgE#h*l?NYJpiTFk5le-9u!CYmK3+mEXQ}2kQROJ;(Dv)tQ9)BW>yJ`3V83FhASS z!0=P&?!CSEw{u_5eaF=%XSB)gJO_?|<`~}X-*=2v9b@(Wz>_O4J%KNN^T*8>!HfIB zi?!foEqJ-+nbbU!0ILqqle?NDc!<^q-jSVqzrVM8eQ&hpy`p)q5KxT6?g*_U>Tb{X zfk@R&YQuN_`wLh9p3p9QRP)6&U#!|2YtpDU+O(oxZ>6XZ2-)uQqCdR-J{_j!!yI`- za*%%Wyxzm}*}PcH5Z=l2pFxDQdpMpK^J$*P1dT8WL*u6YiVS2n-XbFbH3)Gd<ra{w z?x)U#s!Ry3i-m2ONspTaknfVwVb<G3$BD5}_)N=3t>C{I`BUR*#_z&Py0ayT!e0sS zLBrKqBVBAi=8!a!*f)F`zeDUNuC2qqjOVju{8b_+PmQsmH@0i7)0o23AS3yX@=qY( zHxyMzjxFO;N2679JuW#w!&P&wqk*cq*3r4DxgLxzR#EU^XsSLmS0DMPK61NhX9p}z g1f+_*&0gg6S0Y<n)A|7gM|Pb2NMt{q&|&QHKe>*J<NyEw literal 0 HcmV?d00001 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 GIT binary patch literal 2875 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 OejvR6{vUxIhW`Ny>aUvs literal 0 HcmV?d00001 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 0000000..e91fd09 --- /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 0000000..c5cafb3 --- /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 0000000..bfae830 --- /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) -- GitLab