diff --git a/.gitignore b/.gitignore index 42d344d..8751269 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,7 @@ CTFd/uploads .vagrant # CTFd Exports -*.zip \ No newline at end of file +*.zip + +# JS +node_modules/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ba40c14 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +CTFd/themes/**/vendor/ +*.html +*.njk +*.png +*.svg +*.ico +*.ai +*.svg +*.mp3 +*.webm diff --git a/.travis.yml b/.travis.yml index ee29867..63bbcec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: python -cache: pip +dist: xenial +cache: + - pip + - yarn services: - mysql - postgresql @@ -7,26 +10,25 @@ services: addons: apt: sources: - - mysql-5.7-trusty + - deadsnakes packages: - - mysql-server - - mysql-client + - python3.6 + - python3-pip env: - - TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd' + - TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd' - TESTING_DATABASE_URL='sqlite://' - TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd' python: - 2.7 - 3.6 before_install: - - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" - - sudo mysql_upgrade -u root -ppassword - - sudo service mysql restart - sudo rm -f /etc/boto.cfg - export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE - export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + - python3.6 -m pip install black>=19.3b0 install: - pip install -r development.txt + - yarn global add prettier@1.17.0 before_script: - psql -c 'create database ctfd;' -U postgres script: diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 20fd234..bf4bacb 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -28,7 +28,7 @@ if sys.version_info[0] < 3: reload(sys) # noqa: F821 sys.setdefaultencoding("utf-8") -__version__ = '2.1.1' +__version__ = "2.1.1" class CTFdRequest(Request): @@ -48,7 +48,7 @@ class CTFdFlask(Flask): def __init__(self, *args, **kwargs): """Overriden Jinja constructor setting a custom jinja_environment""" self.jinja_environment = SandboxedBaseEnvironment - self.session_interface = CachingSessionInterface(key_prefix='session') + self.session_interface = CachingSessionInterface(key_prefix="session") self.request_class = CTFdRequest Flask.__init__(self, *args, **kwargs) @@ -59,9 +59,10 @@ class CTFdFlask(Flask): class SandboxedBaseEnvironment(SandboxedEnvironment): """SandboxEnvironment that mimics the Flask BaseEnvironment""" + def __init__(self, app, **options): - if 'loader' not in options: - options['loader'] = app.create_global_jinja_loader() + if "loader" not in options: + options["loader"] = app.create_global_jinja_loader() # Disable cache entirely so that themes can be switched (#662) # If the cache is enabled, switching themes will cause odd rendering errors SandboxedEnvironment.__init__(self, cache_size=0, **options) @@ -70,7 +71,8 @@ class SandboxedBaseEnvironment(SandboxedEnvironment): class ThemeLoader(FileSystemLoader): """Custom FileSystemLoader that switches themes based on the configuration value""" - def __init__(self, searchpath, encoding='utf-8', followlinks=False): + + def __init__(self, searchpath, encoding="utf-8", followlinks=False): super(ThemeLoader, self).__init__(searchpath, encoding, followlinks) self.overriden_templates = {} @@ -80,14 +82,14 @@ class ThemeLoader(FileSystemLoader): return self.overriden_templates[template], template, True # Check if the template requested is for the admin panel - if template.startswith('admin/'): + if template.startswith("admin/"): template = template[6:] # Strip out admin/ - template = "/".join(['admin', 'templates', template]) + template = "/".join(["admin", "templates", template]) return super(ThemeLoader, self).get_source(environment, template) # Load regular theme data - theme = utils.get_config('ctf_theme') - template = "/".join([theme, 'templates', template]) + theme = utils.get_config("ctf_theme") + template = "/".join([theme, "templates", template]) return super(ThemeLoader, self).get_source(environment, template) @@ -96,10 +98,10 @@ def confirm_upgrade(): print("/*\\ CTFd has updated and must update the database! /*\\") print("/*\\ Please backup your database before proceeding! /*\\") print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\") - if input('Run database migrations (Y/N)').lower().strip() == 'y': + if input("Run database migrations (Y/N)").lower().strip() == "y": return True else: - print('/*\\ Ignored database migrations... /*\\') + print("/*\\ Ignored database migrations... /*\\") return False else: return True @@ -107,24 +109,36 @@ def confirm_upgrade(): def run_upgrade(): upgrade() - utils.set_config('ctf_version', __version__) + utils.set_config("ctf_version", __version__) -def create_app(config='CTFd.config.Config'): +def create_app(config="CTFd.config.Config"): app = CTFdFlask(__name__) with app.app_context(): app.config.from_object(config) - theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True) + theme_loader = ThemeLoader( + os.path.join(app.root_path, "themes"), followlinks=True + ) app.jinja_loader = theme_loader - from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking # noqa: F401 + from CTFd.models import ( # noqa: F401 + db, + Teams, + Solves, + Challenges, + Fails, + Flags, + Tags, + Files, + Tracking, + ) url = create_database() # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This is mostly so we can force MySQL's charset - app.config['SQLALCHEMY_DATABASE_URI'] = str(url) + app.config["SQLALCHEMY_DATABASE_URI"] = str(url) # Register database db.init_app(app) @@ -133,7 +147,7 @@ def create_app(config='CTFd.config.Config'): migrations.init_app(app, db) # Alembic sqlite support is lacking so we should just create_all anyway - if url.drivername.startswith('sqlite'): + if url.drivername.startswith("sqlite"): db.create_all() stamp() else: @@ -153,15 +167,11 @@ def create_app(config='CTFd.config.Config'): cache.init_app(app) app.cache = cache - reverse_proxy = app.config.get('REVERSE_PROXY') + reverse_proxy = app.config.get("REVERSE_PROXY") if reverse_proxy: - if ',' in reverse_proxy: - proxyfix_args = [int(i) for i in reverse_proxy.split(',')] - app.wsgi_app = ProxyFix( - app.wsgi_app, - None, - *proxyfix_args - ) + if "," in reverse_proxy: + proxyfix_args = [int(i) for i in reverse_proxy.split(",")] + app.wsgi_app = ProxyFix(app.wsgi_app, None, *proxyfix_args) else: app.wsgi_app = ProxyFix( app.wsgi_app, @@ -170,10 +180,10 @@ def create_app(config='CTFd.config.Config'): x_proto=1, x_host=1, x_port=1, - x_prefix=1 + x_prefix=1, ) - version = utils.get_config('ctf_version') + version = utils.get_config("ctf_version") # Upgrading from an older version of CTFd if version and (StrictVersion(version) < StrictVersion(__version__)): @@ -183,10 +193,10 @@ def create_app(config='CTFd.config.Config'): exit() if not version: - utils.set_config('ctf_version', __version__) + utils.set_config("ctf_version", __version__) - if not utils.get_config('ctf_theme'): - utils.set_config('ctf_theme', 'core') + if not utils.get_config("ctf_theme"): + utils.set_config("ctf_theme", "core") update_check(force=True) diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index bf02680..f12465b 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -7,22 +7,18 @@ from flask import ( Blueprint, abort, render_template_string, - send_file + send_file, ) from CTFd.utils.decorators import admins_only from CTFd.utils.user import is_admin from CTFd.utils.security.auth import logout_user -from CTFd.utils import ( - config as ctf_config, - get_config, - set_config, -) +from CTFd.utils import config as ctf_config, get_config, set_config from CTFd.cache import cache, clear_config from CTFd.utils.helpers import get_errors from CTFd.utils.exports import ( export_ctf as export_ctf_util, - import_ctf as import_ctf_util + import_ctf as import_ctf_util, ) from CTFd.models import ( db, @@ -34,7 +30,7 @@ from CTFd.models import ( Solves, Awards, Unlocks, - Tracking + Tracking, ) import datetime import os @@ -42,7 +38,7 @@ import six import csv -admin = Blueprint('admin', __name__) +admin = Blueprint("admin", __name__) from CTFd.admin import challenges # noqa: F401 @@ -55,40 +51,45 @@ from CTFd.admin import submissions # noqa: F401 from CTFd.admin import notifications # noqa: F401 -@admin.route('/admin', methods=['GET']) +@admin.route("/admin", methods=["GET"]) def view(): if is_admin(): - return redirect(url_for('admin.statistics')) - return redirect(url_for('auth.login')) + return redirect(url_for("admin.statistics")) + return redirect(url_for("auth.login")) -@admin.route('/admin/plugins/', methods=['GET', 'POST']) +@admin.route("/admin/plugins/", methods=["GET", "POST"]) @admins_only def plugin(plugin): - if request.method == 'GET': - plugins_path = os.path.join(app.root_path, 'plugins') + if request.method == "GET": + plugins_path = os.path.join(app.root_path, "plugins") - config_html_plugins = [name for name in os.listdir(plugins_path) - if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))] + config_html_plugins = [ + name + for name in os.listdir(plugins_path) + if os.path.isfile(os.path.join(plugins_path, name, "config.html")) + ] if plugin in config_html_plugins: - config_html = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read() + config_html = open( + os.path.join(app.root_path, "plugins", plugin, "config.html") + ).read() return render_template_string(config_html) abort(404) - elif request.method == 'POST': + elif request.method == "POST": for k, v in request.form.items(): if k == "nonce": continue set_config(k, v) with app.app_context(): clear_config() - return '1' + return "1" -@admin.route('/admin/import', methods=['POST']) +@admin.route("/admin/import", methods=["POST"]) @admins_only def import_ctf(): - backup = request.files['backup'] + backup = request.files["backup"] errors = get_errors() try: import_ctf_util(backup) @@ -99,10 +100,10 @@ def import_ctf(): if errors: return errors[0], 500 else: - return redirect(url_for('admin.config')) + return redirect(url_for("admin.config")) -@admin.route('/admin/export', methods=['GET', 'POST']) +@admin.route("/admin/export", methods=["GET", "POST"]) @admins_only def export_ctf(): backup = export_ctf_util() @@ -112,10 +113,10 @@ def export_ctf(): return send_file(backup, as_attachment=True, attachment_filename=full_name) -@admin.route('/admin/export/csv') +@admin.route("/admin/export/csv") @admins_only def export_csv(): - table = request.args.get('table') + table = request.args.get("table") # TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information. model = get_class_by_tablename(table) @@ -131,18 +132,22 @@ def export_csv(): responses = model.query.all() for curr in responses: - writer.writerow([getattr(curr, column.name) for column in model.__mapper__.columns]) + writer.writerow( + [getattr(curr, column.name) for column in model.__mapper__.columns] + ) output.seek(0) return send_file( output, as_attachment=True, cache_timeout=-1, - attachment_filename="{name}-{table}.csv".format(name=ctf_config.ctf_name(), table=table) + attachment_filename="{name}-{table}.csv".format( + name=ctf_config.ctf_name(), table=table + ), ) -@admin.route('/admin/config', methods=['GET', 'POST']) +@admin.route("/admin/config", methods=["GET", "POST"]) @admins_only def config(): # Clear the config cache so that we don't get stale values @@ -154,20 +159,17 @@ def config(): configs = dict([(c.key, get_config(c.key)) for c in configs]) themes = ctf_config.get_themes() - themes.remove(get_config('ctf_theme')) + themes.remove(get_config("ctf_theme")) return render_template( - 'admin/config.html', - database_tables=database_tables, - themes=themes, - **configs + "admin/config.html", database_tables=database_tables, themes=themes, **configs ) -@admin.route('/admin/reset', methods=['GET', 'POST']) +@admin.route("/admin/reset", methods=["GET", "POST"]) @admins_only def reset(): - if request.method == 'POST': + if request.method == "POST": # Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking Tracking.query.delete() Solves.query.delete() @@ -176,11 +178,11 @@ def reset(): Unlocks.query.delete() Users.query.delete() Teams.query.delete() - set_config('setup', False) + set_config("setup", False) db.session.commit() cache.clear() logout_user() db.session.close() - return redirect(url_for('views.setup')) + return redirect(url_for("views.setup")) - return render_template('admin/reset.html') + return render_template("admin/reset.html") diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index 3751456..079ff61 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -8,44 +8,48 @@ import os import six -@admin.route('/admin/challenges') +@admin.route("/admin/challenges") @admins_only def challenges_listing(): challenges = Challenges.query.all() - return render_template('admin/challenges/challenges.html', challenges=challenges) + return render_template("admin/challenges/challenges.html", challenges=challenges) -@admin.route('/admin/challenges/') +@admin.route("/admin/challenges/") @admins_only def challenges_detail(challenge_id): - challenges = dict(Challenges.query.with_entities(Challenges.id, Challenges.name).all()) + challenges = dict( + Challenges.query.with_entities(Challenges.id, Challenges.name).all() + ) challenge = Challenges.query.filter_by(id=challenge_id).first_or_404() solves = Solves.query.filter_by(challenge_id=challenge.id).all() flags = Flags.query.filter_by(challenge_id=challenge.id).all() challenge_class = get_chal_class(challenge.type) - with open(os.path.join(app.root_path, challenge_class.templates['update'].lstrip('/')), 'rb') as update: + with open( + os.path.join(app.root_path, challenge_class.templates["update"].lstrip("/")), + "rb", + ) as update: tpl = update.read() if six.PY3 and isinstance(tpl, binary_type): - tpl = tpl.decode('utf-8') - update_j2 = render_template_string( - tpl, - challenge=challenge - ) + tpl = tpl.decode("utf-8") + update_j2 = render_template_string(tpl, challenge=challenge) - update_script = url_for('views.static_html', route=challenge_class.scripts['update'].lstrip('/')) + update_script = url_for( + "views.static_html", route=challenge_class.scripts["update"].lstrip("/") + ) return render_template( - 'admin/challenges/challenge.html', + "admin/challenges/challenge.html", update_template=update_j2, update_script=update_script, challenge=challenge, challenges=challenges, solves=solves, - flags=flags + flags=flags, ) -@admin.route('/admin/challenges/new') +@admin.route("/admin/challenges/new") @admins_only def challenges_new(): - return render_template('admin/challenges/new.html') + return render_template("admin/challenges/new.html") diff --git a/CTFd/admin/notifications.py b/CTFd/admin/notifications.py index cc5de89..bf2fa25 100644 --- a/CTFd/admin/notifications.py +++ b/CTFd/admin/notifications.py @@ -4,8 +4,8 @@ from CTFd.models import Notifications from CTFd.admin import admin -@admin.route('/admin/notifications') +@admin.route("/admin/notifications") @admins_only def notifications(): notifs = Notifications.query.order_by(Notifications.id.desc()).all() - return render_template('admin/notifications.html', notifications=notifs) + return render_template("admin/notifications.html", notifications=notifs) diff --git a/CTFd/admin/pages.py b/CTFd/admin/pages.py index 1fc31d4..7f7bb03 100644 --- a/CTFd/admin/pages.py +++ b/CTFd/admin/pages.py @@ -6,38 +6,38 @@ from CTFd.utils import markdown from CTFd.admin import admin -@admin.route('/admin/pages') +@admin.route("/admin/pages") @admins_only def pages_listing(): pages = Pages.query.all() - return render_template('admin/pages.html', pages=pages) + return render_template("admin/pages.html", pages=pages) -@admin.route('/admin/pages/new') +@admin.route("/admin/pages/new") @admins_only def pages_new(): - return render_template('admin/editor.html') + return render_template("admin/editor.html") -@admin.route('/admin/pages/preview', methods=['POST']) +@admin.route("/admin/pages/preview", methods=["POST"]) @admins_only def pages_preview(): data = request.form.to_dict() schema = PageSchema() page = schema.load(data) - return render_template('page.html', content=markdown(page.data.content)) + return render_template("page.html", content=markdown(page.data.content)) -@admin.route('/admin/pages/') +@admin.route("/admin/pages/") @admins_only def pages_detail(page_id): page = Pages.query.filter_by(id=page_id).first_or_404() - page_op = request.args.get('operation') + page_op = request.args.get("operation") - if request.method == 'GET' and page_op == 'preview': - return render_template('page.html', content=markdown(page.content)) + if request.method == "GET" and page_op == "preview": + return render_template("page.html", content=markdown(page.content)) - if request.method == 'GET' and page_op == 'create': - return render_template('admin/editor.html') + if request.method == "GET" and page_op == "create": + return render_template("admin/editor.html") - return render_template('admin/editor.html', page=page) + return render_template("admin/editor.html", page=page) diff --git a/CTFd/admin/scoreboard.py b/CTFd/admin/scoreboard.py index 539bf08..71e2cb8 100644 --- a/CTFd/admin/scoreboard.py +++ b/CTFd/admin/scoreboard.py @@ -4,8 +4,8 @@ from CTFd.admin import admin from CTFd.scoreboard import get_standings -@admin.route('/admin/scoreboard') +@admin.route("/admin/scoreboard") @admins_only def scoreboard_listing(): standings = get_standings(admin=True) - return render_template('admin/scoreboard.html', standings=standings) + return render_template("admin/scoreboard.html", standings=standings) diff --git a/CTFd/admin/statistics.py b/CTFd/admin/statistics.py index e7edbf7..79c867b 100644 --- a/CTFd/admin/statistics.py +++ b/CTFd/admin/statistics.py @@ -6,7 +6,7 @@ from CTFd.models import db, Solves, Challenges, Fails, Tracking from CTFd.admin import admin -@admin.route('/admin/statistics', methods=['GET']) +@admin.route("/admin/statistics", methods=["GET"]) @admins_only def statistics(): update_check() @@ -15,47 +15,41 @@ def statistics(): teams_registered = Model.query.count() - wrong_count = Fails.query.join( - Model, - Fails.account_id == Model.id - ).filter( - Model.banned == False, - Model.hidden == False - ).count() + wrong_count = ( + Fails.query.join(Model, Fails.account_id == Model.id) + .filter(Model.banned == False, Model.hidden == False) + .count() + ) - solve_count = Solves.query.join( - Model, - Solves.account_id == Model.id - ).filter( - Model.banned == False, - Model.hidden == False - ).count() + solve_count = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter(Model.banned == False, Model.hidden == False) + .count() + ) challenge_count = Challenges.query.count() ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count() - solves_sub = db.session.query( - Solves.challenge_id, - db.func.count(Solves.challenge_id).label('solves_cnt') - ).join( - Model, - Solves.account_id == Model.id - ).filter( - Model.banned == False, - Model.hidden == False - ).group_by( - Solves.challenge_id - ).subquery() + solves_sub = ( + db.session.query( + Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves_cnt") + ) + .join(Model, Solves.account_id == Model.id) + .filter(Model.banned == False, Model.hidden == False) + .group_by(Solves.challenge_id) + .subquery() + ) - solves = db.session.query( - solves_sub.columns.challenge_id, - solves_sub.columns.solves_cnt, - Challenges.name - ).join( - Challenges, - solves_sub.columns.challenge_id == Challenges.id - ).all() + solves = ( + db.session.query( + solves_sub.columns.challenge_id, + solves_sub.columns.solves_cnt, + Challenges.name, + ) + .join(Challenges, solves_sub.columns.challenge_id == Challenges.id) + .all() + ) solve_data = {} for chal, count, name in solves: @@ -70,7 +64,7 @@ def statistics(): db.session.close() return render_template( - 'admin/statistics.html', + "admin/statistics.html", team_count=teams_registered, ip_count=ip_count, wrong_count=wrong_count, @@ -78,5 +72,5 @@ def statistics(): challenge_count=challenge_count, solve_data=solve_data, most_solved=most_solved, - least_solved=least_solved + least_solved=least_solved, ) diff --git a/CTFd/admin/submissions.py b/CTFd/admin/submissions.py index 35f0ce6..6b9e862 100644 --- a/CTFd/admin/submissions.py +++ b/CTFd/admin/submissions.py @@ -5,15 +5,15 @@ from CTFd.utils.modes import get_model from CTFd.admin import admin -@admin.route('/admin/submissions', defaults={'submission_type': None}) -@admin.route('/admin/submissions/') +@admin.route("/admin/submissions", defaults={"submission_type": None}) +@admin.route("/admin/submissions/") @admins_only def submissions_listing(submission_type): filters = {} if submission_type: - filters['type'] = submission_type + filters["type"] = submission_type - curr_page = abs(int(request.args.get('page', 1, type=int))) + curr_page = abs(int(request.args.get("page", 1, type=int))) results_per_page = 50 page_start = results_per_page * (curr_page - 1) page_end = results_per_page * (curr_page - 1) + results_per_page @@ -22,27 +22,29 @@ def submissions_listing(submission_type): Model = get_model() - submissions = Submissions.query.add_columns( - Submissions.id, - Submissions.type, - Submissions.challenge_id, - Submissions.provided, - Submissions.account_id, - Submissions.date, - Challenges.name.label('challenge_name'), - Model.name.label('team_name') - )\ - .filter_by(**filters) \ - .join(Challenges)\ - .join(Model)\ - .order_by(Submissions.date.desc())\ - .slice(page_start, page_end)\ + submissions = ( + Submissions.query.add_columns( + Submissions.id, + Submissions.type, + Submissions.challenge_id, + Submissions.provided, + Submissions.account_id, + Submissions.date, + Challenges.name.label("challenge_name"), + Model.name.label("team_name"), + ) + .filter_by(**filters) + .join(Challenges) + .join(Model) + .order_by(Submissions.date.desc()) + .slice(page_start, page_end) .all() + ) return render_template( - 'admin/submissions.html', + "admin/submissions.html", submissions=submissions, page_count=page_count, curr_page=curr_page, - type=submission_type + type=submission_type, ) diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index 4d6c21d..c367cde 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -7,28 +7,47 @@ from CTFd.utils.helpers import get_errors from sqlalchemy.sql import not_ -@admin.route('/admin/teams') +@admin.route("/admin/teams") @admins_only def teams_listing(): - page = abs(request.args.get('page', 1, type=int)) - q = request.args.get('q') + page = abs(request.args.get("page", 1, type=int)) + q = request.args.get("q") if q: - field = request.args.get('field') + field = request.args.get("field") teams = [] errors = get_errors() - if field == 'id': + if field == "id": if q.isnumeric(): teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all() else: teams = [] - errors.append('Your ID search term is not numeric') - elif field == 'name': - teams = Teams.query.filter(Teams.name.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() - elif field == 'email': - teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() - elif field == 'affiliation': - teams = Teams.query.filter(Teams.affiliation.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() - return render_template('admin/teams/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field) + errors.append("Your ID search term is not numeric") + elif field == "name": + teams = ( + Teams.query.filter(Teams.name.like("%{}%".format(q))) + .order_by(Teams.id.asc()) + .all() + ) + elif field == "email": + teams = ( + Teams.query.filter(Teams.email.like("%{}%".format(q))) + .order_by(Teams.id.asc()) + .all() + ) + elif field == "affiliation": + teams = ( + Teams.query.filter(Teams.affiliation.like("%{}%".format(q))) + .order_by(Teams.id.asc()) + .all() + ) + return render_template( + "admin/teams/teams.html", + teams=teams, + pages=None, + curr_page=None, + q=q, + field=field, + ) page = abs(int(page)) results_per_page = 50 @@ -38,16 +57,18 @@ def teams_listing(): teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all() count = db.session.query(db.func.count(Teams.id)).first()[0] pages = int(count / results_per_page) + (count % results_per_page > 0) - return render_template('admin/teams/teams.html', teams=teams, pages=pages, curr_page=page) + return render_template( + "admin/teams/teams.html", teams=teams, pages=pages, curr_page=page + ) -@admin.route('/admin/teams/new') +@admin.route("/admin/teams/new") @admins_only def teams_new(): - return render_template('admin/teams/new.html') + return render_template("admin/teams/new.html") -@admin.route('/admin/teams/') +@admin.route("/admin/teams/") @admins_only def teams_detail(team_id): team = Teams.query.filter_by(id=team_id).first_or_404() @@ -69,14 +90,17 @@ def teams_detail(team_id): missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all() # Get addresses for all members - last_seen = db.func.max(Tracking.date).label('last_seen') - addrs = db.session.query(Tracking.ip, last_seen) \ - .filter(Tracking.user_id.in_(member_ids)) \ - .group_by(Tracking.ip) \ - .order_by(last_seen.desc()).all() + last_seen = db.func.max(Tracking.date).label("last_seen") + addrs = ( + db.session.query(Tracking.ip, last_seen) + .filter(Tracking.user_id.in_(member_ids)) + .group_by(Tracking.ip) + .order_by(last_seen.desc()) + .all() + ) return render_template( - 'admin/teams/team.html', + "admin/teams/team.html", team=team, members=members, score=score, diff --git a/CTFd/admin/users.py b/CTFd/admin/users.py index 1b9ebf8..a8fff1c 100644 --- a/CTFd/admin/users.py +++ b/CTFd/admin/users.py @@ -9,28 +9,47 @@ from CTFd.utils.helpers import get_errors from sqlalchemy.sql import not_ -@admin.route('/admin/users') +@admin.route("/admin/users") @admins_only def users_listing(): - page = abs(request.args.get('page', 1, type=int)) - q = request.args.get('q') + page = abs(request.args.get("page", 1, type=int)) + q = request.args.get("q") if q: - field = request.args.get('field') + field = request.args.get("field") users = [] errors = get_errors() - if field == 'id': + if field == "id": if q.isnumeric(): users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all() else: users = [] - errors.append('Your ID search term is not numeric') - elif field == 'name': - users = Users.query.filter(Users.name.like('%{}%'.format(q))).order_by(Users.id.asc()).all() - elif field == 'email': - users = Users.query.filter(Users.email.like('%{}%'.format(q))).order_by(Users.id.asc()).all() - elif field == 'affiliation': - users = Users.query.filter(Users.affiliation.like('%{}%'.format(q))).order_by(Users.id.asc()).all() - return render_template('admin/users/users.html', users=users, pages=None, curr_page=None, q=q, field=field) + errors.append("Your ID search term is not numeric") + elif field == "name": + users = ( + Users.query.filter(Users.name.like("%{}%".format(q))) + .order_by(Users.id.asc()) + .all() + ) + elif field == "email": + users = ( + Users.query.filter(Users.email.like("%{}%".format(q))) + .order_by(Users.id.asc()) + .all() + ) + elif field == "affiliation": + users = ( + Users.query.filter(Users.affiliation.like("%{}%".format(q))) + .order_by(Users.id.asc()) + .all() + ) + return render_template( + "admin/users/users.html", + users=users, + pages=None, + curr_page=None, + q=q, + field=field, + ) page = abs(int(page)) results_per_page = 50 @@ -41,16 +60,18 @@ def users_listing(): count = db.session.query(db.func.count(Users.id)).first()[0] pages = int(count / results_per_page) + (count % results_per_page > 0) - return render_template('admin/users/users.html', users=users, pages=pages, curr_page=page) + return render_template( + "admin/users/users.html", users=users, pages=pages, curr_page=page + ) -@admin.route('/admin/users/new') +@admin.route("/admin/users/new") @admins_only def users_new(): - return render_template('admin/users/new.html') + return render_template("admin/users/new.html") -@admin.route('/admin/users/') +@admin.route("/admin/users/") @admins_only def users_detail(user_id): # Get user object @@ -60,7 +81,7 @@ def users_detail(user_id): solves = user.get_solves(admin=True) # Get challenges that the user is missing - if get_config('user_mode') == TEAMS_MODE: + if get_config("user_mode") == TEAMS_MODE: if user.team: all_solves = user.team.get_solves(admin=True) else: @@ -72,11 +93,14 @@ def users_detail(user_id): missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all() # Get IP addresses that the User has used - last_seen = db.func.max(Tracking.date).label('last_seen') - addrs = db.session.query(Tracking.ip, last_seen) \ - .filter_by(user_id=user_id) \ - .group_by(Tracking.ip) \ - .order_by(last_seen.desc()).all() + last_seen = db.func.max(Tracking.date).label("last_seen") + addrs = ( + db.session.query(Tracking.ip, last_seen) + .filter_by(user_id=user_id) + .group_by(Tracking.ip) + .order_by(last_seen.desc()) + .all() + ) # Get Fails fails = user.get_fails(admin=True) @@ -89,7 +113,7 @@ def users_detail(user_id): place = user.get_place(admin=True) return render_template( - 'admin/users/user.html', + "admin/users/user.html", solves=solves, user=user, addrs=addrs, @@ -97,5 +121,5 @@ def users_detail(user_id): missing=missing, place=place, fails=fails, - awards=awards + awards=awards, ) diff --git a/CTFd/api/__init__.py b/CTFd/api/__init__.py index b952726..9eec5e8 100644 --- a/CTFd/api/__init__.py +++ b/CTFd/api/__init__.py @@ -17,21 +17,21 @@ from CTFd.api.v1.notifications import notifications_namespace from CTFd.api.v1.pages import pages_namespace from CTFd.api.v1.unlocks import unlocks_namespace -api = Blueprint('api', __name__, url_prefix='/api/v1') -CTFd_API_v1 = Api(api, version='v1', doc=current_app.config.get('SWAGGER_UI')) +api = Blueprint("api", __name__, url_prefix="/api/v1") +CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI")) -CTFd_API_v1.add_namespace(challenges_namespace, '/challenges') -CTFd_API_v1.add_namespace(tags_namespace, '/tags') -CTFd_API_v1.add_namespace(awards_namespace, '/awards') -CTFd_API_v1.add_namespace(hints_namespace, '/hints') -CTFd_API_v1.add_namespace(flags_namespace, '/flags') -CTFd_API_v1.add_namespace(submissions_namespace, '/submissions') -CTFd_API_v1.add_namespace(scoreboard_namespace, '/scoreboard') -CTFd_API_v1.add_namespace(teams_namespace, '/teams') -CTFd_API_v1.add_namespace(users_namespace, '/users') -CTFd_API_v1.add_namespace(statistics_namespace, '/statistics') -CTFd_API_v1.add_namespace(files_namespace, '/files') -CTFd_API_v1.add_namespace(notifications_namespace, '/notifications') -CTFd_API_v1.add_namespace(configs_namespace, '/configs') -CTFd_API_v1.add_namespace(pages_namespace, '/pages') -CTFd_API_v1.add_namespace(unlocks_namespace, '/unlocks') +CTFd_API_v1.add_namespace(challenges_namespace, "/challenges") +CTFd_API_v1.add_namespace(tags_namespace, "/tags") +CTFd_API_v1.add_namespace(awards_namespace, "/awards") +CTFd_API_v1.add_namespace(hints_namespace, "/hints") +CTFd_API_v1.add_namespace(flags_namespace, "/flags") +CTFd_API_v1.add_namespace(submissions_namespace, "/submissions") +CTFd_API_v1.add_namespace(scoreboard_namespace, "/scoreboard") +CTFd_API_v1.add_namespace(teams_namespace, "/teams") +CTFd_API_v1.add_namespace(users_namespace, "/users") +CTFd_API_v1.add_namespace(statistics_namespace, "/statistics") +CTFd_API_v1.add_namespace(files_namespace, "/files") +CTFd_API_v1.add_namespace(notifications_namespace, "/notifications") +CTFd_API_v1.add_namespace(configs_namespace, "/configs") +CTFd_API_v1.add_namespace(pages_namespace, "/pages") +CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks") diff --git a/CTFd/api/v1/awards.py b/CTFd/api/v1/awards.py index a3cdaae..0262111 100644 --- a/CTFd/api/v1/awards.py +++ b/CTFd/api/v1/awards.py @@ -2,16 +2,13 @@ from flask import request from flask_restplus import Namespace, Resource from CTFd.models import db, Awards from CTFd.schemas.awards import AwardSchema -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only -awards_namespace = Namespace('awards', description="Endpoint to retrieve Awards") +awards_namespace = Namespace("awards", description="Endpoint to retrieve Awards") -@awards_namespace.route('') +@awards_namespace.route("") class AwardList(Resource): - @admins_only def post(self): req = request.get_json() @@ -19,10 +16,7 @@ class AwardList(Resource): response = schema.load(req, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -30,29 +24,20 @@ class AwardList(Resource): response = schema.dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@awards_namespace.route('/') -@awards_namespace.param('award_id', 'An Award ID') +@awards_namespace.route("/") +@awards_namespace.param("award_id", "An Award ID") class Award(Resource): @admins_only def get(self, award_id): award = Awards.query.filter_by(id=award_id).first_or_404() response = AwardSchema().dump(award) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, award_id): @@ -61,6 +46,4 @@ class Award(Resource): db.session.commit() db.session.close() - return { - 'success': True, - } + return {"success": True} diff --git a/CTFd/api/v1/challenges.py b/CTFd/api/v1/challenges.py index c064c4d..71b4115 100644 --- a/CTFd/api/v1/challenges.py +++ b/CTFd/api/v1/challenges.py @@ -16,14 +16,18 @@ from CTFd.utils.dates import isoformat from CTFd.utils.decorators import ( during_ctf_time_only, require_verified_emails, - admins_only + admins_only, ) from CTFd.utils.decorators.visibility import ( check_challenge_visibility, - check_score_visibility + check_score_visibility, ) from CTFd.cache import clear_standings -from CTFd.utils.config.visibility import scores_visible, accounts_visible, challenges_visible +from CTFd.utils.config.visibility import ( + scores_visible, + accounts_visible, + challenges_visible, +) from CTFd.utils.user import is_admin, authed from CTFd.utils.modes import get_model, USERS_MODE, TEAMS_MODE from CTFd.schemas.tags import TagSchema @@ -40,11 +44,12 @@ from CTFd.utils.security.signing import serialize from sqlalchemy.sql import and_ import datetime -challenges_namespace = Namespace('challenges', - description="Endpoint to retrieve Challenges") +challenges_namespace = Namespace( + "challenges", description="Endpoint to retrieve Challenges" +) -@challenges_namespace.route('') +@challenges_namespace.route("") class ChallengeList(Resource): @check_challenge_visibility @during_ctf_time_only @@ -53,16 +58,21 @@ class ChallengeList(Resource): # This can return None (unauth) if visibility is set to public user = get_current_user() - challenges = Challenges.query.filter( - and_(Challenges.state != 'hidden', Challenges.state != 'locked') - ).order_by(Challenges.value).all() + challenges = ( + Challenges.query.filter( + and_(Challenges.state != "hidden", Challenges.state != "locked") + ) + .order_by(Challenges.value) + .all() + ) if user: - solve_ids = Solves.query\ - .with_entities(Solves.challenge_id)\ - .filter_by(account_id=user.account_id)\ - .order_by(Solves.challenge_id.asc())\ + solve_ids = ( + Solves.query.with_entities(Solves.challenge_id) + .filter_by(account_id=user.account_id) + .order_by(Solves.challenge_id.asc()) .all() + ) solve_ids = set([value for value, in solve_ids]) # TODO: Convert this into a re-useable decorator @@ -75,61 +85,59 @@ class ChallengeList(Resource): solve_ids = set() response = [] - tag_schema = TagSchema(view='user', many=True) + tag_schema = TagSchema(view="user", many=True) for challenge in challenges: if challenge.requirements: - requirements = challenge.requirements.get('prerequisites', []) - anonymize = challenge.requirements.get('anonymize') + requirements = challenge.requirements.get("prerequisites", []) + anonymize = challenge.requirements.get("anonymize") prereqs = set(requirements) if solve_ids >= prereqs: pass else: if anonymize: - response.append({ - 'id': challenge.id, - 'type': 'hidden', - 'name': '???', - 'value': 0, - 'category': '???', - 'tags': [], - 'template': '', - 'script': '' - }) + response.append( + { + "id": challenge.id, + "type": "hidden", + "name": "???", + "value": 0, + "category": "???", + "tags": [], + "template": "", + "script": "", + } + ) # Fallthrough to continue continue challenge_type = get_chal_class(challenge.type) - response.append({ - 'id': challenge.id, - 'type': challenge_type.name, - 'name': challenge.name, - 'value': challenge.value, - 'category': challenge.category, - 'tags': tag_schema.dump(challenge.tags).data, - 'template': challenge_type.templates['view'], - 'script': challenge_type.scripts['view'], - }) + response.append( + { + "id": challenge.id, + "type": challenge_type.name, + "name": challenge.name, + "value": challenge.value, + "category": challenge.category, + "tags": tag_schema.dump(challenge.tags).data, + "template": challenge_type.templates["view"], + "script": challenge_type.scripts["view"], + } + ) db.session.close() - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} @admins_only def post(self): data = request.form or request.get_json() - challenge_type = data['type'] + challenge_type = data["type"] challenge_class = get_chal_class(challenge_type) challenge = challenge_class.create(request) response = challenge_class.read(challenge) - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} -@challenges_namespace.route('/types') +@challenges_namespace.route("/types") class ChallengeTypes(Resource): @admins_only def get(self): @@ -138,19 +146,16 @@ class ChallengeTypes(Resource): for class_id in CHALLENGE_CLASSES: challenge_class = CHALLENGE_CLASSES.get(class_id) response[challenge_class.id] = { - 'id': challenge_class.id, - 'name': challenge_class.name, - 'templates': challenge_class.templates, - 'scripts': challenge_class.scripts, + "id": challenge_class.id, + "name": challenge_class.name, + "templates": challenge_class.templates, + "scripts": challenge_class.scripts, } - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} -@challenges_namespace.route('/') -@challenges_namespace.param('challenge_id', 'A Challenge ID') +@challenges_namespace.route("/") +@challenges_namespace.param("challenge_id", "A Challenge ID") class Challenge(Resource): @check_challenge_visibility @during_ctf_time_only @@ -160,22 +165,24 @@ class Challenge(Resource): chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404() else: chal = Challenges.query.filter( - Challenges.id == challenge_id, and_(Challenges.state != 'hidden', Challenges.state != 'locked') + Challenges.id == challenge_id, + and_(Challenges.state != "hidden", Challenges.state != "locked"), ).first_or_404() chal_class = get_chal_class(chal.type) if chal.requirements: - requirements = chal.requirements.get('prerequisites', []) - anonymize = chal.requirements.get('anonymize') + requirements = chal.requirements.get("prerequisites", []) + anonymize = chal.requirements.get("anonymize") if challenges_visible(): user = get_current_user() if user: - solve_ids = Solves.query \ - .with_entities(Solves.challenge_id) \ - .filter_by(account_id=user.account_id) \ - .order_by(Solves.challenge_id.asc()) \ + solve_ids = ( + Solves.query.with_entities(Solves.challenge_id) + .filter_by(account_id=user.account_id) + .order_by(Solves.challenge_id.asc()) .all() + ) else: # We need to handle the case where a user is viewing challenges anonymously solve_ids = [] @@ -186,26 +193,24 @@ class Challenge(Resource): else: if anonymize: return { - 'success': True, - 'data': { - 'id': chal.id, - 'type': 'hidden', - 'name': '???', - 'value': 0, - 'category': '???', - 'tags': [], - 'template': '', - 'script': '' - } + "success": True, + "data": { + "id": chal.id, + "type": "hidden", + "name": "???", + "value": 0, + "category": "???", + "tags": [], + "template": "", + "script": "", + }, } abort(403) else: abort(403) tags = [ - tag['value'] for tag in TagSchema( - "user", many=True).dump( - chal.tags).data + tag["value"] for tag in TagSchema("user", many=True).dump(chal.tags).data ] unlocked_hints = set() @@ -221,56 +226,59 @@ class Challenge(Resource): if config.is_teams_mode() and team is None: abort(403) - unlocked_hints = set([ - u.target for u in HintUnlocks.query.filter_by(type='hints', account_id=user.account_id) - ]) + unlocked_hints = set( + [ + u.target + for u in HintUnlocks.query.filter_by( + type="hints", account_id=user.account_id + ) + ] + ) files = [] for f in chal.files: token = { - 'user_id': user.id, - 'team_id': team.id if team else None, - 'file_id': f.id, + "user_id": user.id, + "team_id": team.id if team else None, + "file_id": f.id, } files.append( - url_for('views.files', path=f.location, token=serialize(token)) + url_for("views.files", path=f.location, token=serialize(token)) ) else: - files = [ - url_for('views.files', path=f.location) for f in chal.files - ] + files = [url_for("views.files", path=f.location) for f in chal.files] for hint in Hints.query.filter_by(challenge_id=chal.id).all(): if hint.id in unlocked_hints or ctf_ended(): - hints.append({ - 'id': hint.id, - 'cost': hint.cost, - 'content': hint.content - }) + hints.append( + {"id": hint.id, "cost": hint.cost, "content": hint.content} + ) else: - hints.append({'id': hint.id, 'cost': hint.cost}) + hints.append({"id": hint.id, "cost": hint.cost}) response = chal_class.read(challenge=chal) Model = get_model() if scores_visible() is True and accounts_visible() is True: - solves = Solves.query\ - .join(Model, Solves.account_id == Model.id)\ - .filter(Solves.challenge_id == chal.id, Model.banned == False, Model.hidden == False)\ + solves = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter( + Solves.challenge_id == chal.id, + Model.banned == False, + Model.hidden == False, + ) .count() - response['solves'] = solves + ) + response["solves"] = solves else: - response['solves'] = None + response["solves"] = None - response['files'] = files - response['tags'] = tags - response['hints'] = hints + response["files"] = files + response["tags"] = tags + response["hints"] = hints db.session.close() - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} @admins_only def patch(self, challenge_id): @@ -278,10 +286,7 @@ class Challenge(Resource): challenge_class = get_chal_class(challenge.type) challenge = challenge_class.update(challenge, request) response = challenge_class.read(challenge) - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} @admins_only def delete(self, challenge_id): @@ -289,55 +294,51 @@ class Challenge(Resource): chal_class = get_chal_class(challenge.type) chal_class.delete(challenge) - return { - 'success': True, - } + return {"success": True} -@challenges_namespace.route('/attempt') +@challenges_namespace.route("/attempt") class ChallengeAttempt(Resource): @check_challenge_visibility @during_ctf_time_only @require_verified_emails def post(self): if authed() is False: - return { - 'success': True, - 'data': { - 'status': "authentication_required", - } - }, 403 + return {"success": True, "data": {"status": "authentication_required"}}, 403 - if request.content_type != 'application/json': + if request.content_type != "application/json": request_data = request.form else: request_data = request.get_json() - challenge_id = request_data.get('challenge_id') + challenge_id = request_data.get("challenge_id") if current_user.is_admin(): - preview = request.args.get('preview', False) + preview = request.args.get("preview", False) if preview: challenge = Challenges.query.filter_by(id=challenge_id).first_or_404() chal_class = get_chal_class(challenge.type) status, message = chal_class.attempt(challenge, request) return { - 'success': True, - 'data': { - 'status': "correct" if status else "incorrect", - 'message': message - } + "success": True, + "data": { + "status": "correct" if status else "incorrect", + "message": message, + }, } if ctf_paused(): - return { - 'success': True, - 'data': { - 'status': "paused", - 'message': '{} is paused'.format(config.ctf_name()) - } - }, 403 + return ( + { + "success": True, + "data": { + "status": "paused", + "message": "{} is paused".format(config.ctf_name()), + }, + }, + 403, + ) user = get_current_user() team = get_current_team() @@ -347,26 +348,25 @@ class ChallengeAttempt(Resource): abort(403) fails = Fails.query.filter_by( - account_id=user.account_id, - challenge_id=challenge_id + account_id=user.account_id, challenge_id=challenge_id ).count() - challenge = Challenges.query.filter_by( - id=challenge_id).first_or_404() + challenge = Challenges.query.filter_by(id=challenge_id).first_or_404() - if challenge.state == 'hidden': + if challenge.state == "hidden": abort(404) - if challenge.state == 'locked': + if challenge.state == "locked": abort(403) if challenge.requirements: - requirements = challenge.requirements.get('prerequisites', []) - solve_ids = Solves.query \ - .with_entities(Solves.challenge_id) \ - .filter_by(account_id=user.account_id) \ - .order_by(Solves.challenge_id.asc()) \ + requirements = challenge.requirements.get("prerequisites", []) + solve_ids = ( + Solves.query.with_entities(Solves.challenge_id) + .filter_by(account_id=user.account_id) + .order_by(Solves.challenge_id.asc()) .all() + ) solve_ids = set([solve_id for solve_id, in solve_ids]) prereqs = set(requirements) if solve_ids >= prereqs: @@ -381,29 +381,28 @@ class ChallengeAttempt(Resource): if kpm > 10: if ctftime(): chal_class.fail( - user=user, - team=team, - challenge=challenge, - request=request + user=user, team=team, challenge=challenge, request=request ) log( - 'submissions', + "submissions", "[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]", - submission=request_data['submission'].encode('utf-8'), - kpm=kpm + submission=request_data["submission"].encode("utf-8"), + kpm=kpm, ) # Submitting too fast - return { - 'success': True, - 'data': { - 'status': "ratelimited", - 'message': "You're submitting flags too fast. Slow down." - } - }, 429 + return ( + { + "success": True, + "data": { + "status": "ratelimited", + "message": "You're submitting flags too fast. Slow down.", + }, + }, + 429, + ) solves = Solves.query.filter_by( - account_id=user.account_id, - challenge_id=challenge_id + account_id=user.account_id, challenge_id=challenge_id ).first() # Challenge not solved yet @@ -411,99 +410,92 @@ class ChallengeAttempt(Resource): # Hit max attempts max_tries = challenge.max_attempts if max_tries and fails >= max_tries > 0: - return { - 'success': True, - 'data': { - 'status': "incorrect", - 'message': "You have 0 tries remaining" - } - }, 403 + return ( + { + "success": True, + "data": { + "status": "incorrect", + "message": "You have 0 tries remaining", + }, + }, + 403, + ) status, message = chal_class.attempt(challenge, request) if status: # The challenge plugin says the input is right if ctftime() or current_user.is_admin(): chal_class.solve( - user=user, - team=team, - challenge=challenge, - request=request + user=user, team=team, challenge=challenge, request=request ) clear_standings() log( - 'submissions', + "submissions", "[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]", - submission=request_data['submission'].encode('utf-8'), - kpm=kpm + submission=request_data["submission"].encode("utf-8"), + kpm=kpm, ) return { - 'success': True, - 'data': { - 'status': "correct", - 'message': message - } + "success": True, + "data": {"status": "correct", "message": message}, } else: # The challenge plugin says the input is wrong if ctftime() or current_user.is_admin(): chal_class.fail( - user=user, - team=team, - challenge=challenge, - request=request + user=user, team=team, challenge=challenge, request=request ) clear_standings() log( - 'submissions', + "submissions", "[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]", - submission=request_data['submission'].encode('utf-8'), - kpm=kpm + submission=request_data["submission"].encode("utf-8"), + kpm=kpm, ) if max_tries: # Off by one since fails has changed since it was gotten attempts_left = max_tries - fails - 1 - tries_str = 'tries' + tries_str = "tries" if attempts_left == 1: - tries_str = 'try' + tries_str = "try" # Add a punctuation mark if there isn't one - if message[-1] not in '!().;?[]{}': - message = message + '.' + if message[-1] not in "!().;?[]{}": + message = message + "." return { - 'success': True, - 'data': { - 'status': "incorrect", - 'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str) - } + "success": True, + "data": { + "status": "incorrect", + "message": "{} You have {} {} remaining.".format( + message, attempts_left, tries_str + ), + }, } else: return { - 'success': True, - 'data': { - 'status': "incorrect", - 'message': message - } + "success": True, + "data": {"status": "incorrect", "message": message}, } # Challenge already solved else: log( - 'submissions', + "submissions", "[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]", - submission=request_data['submission'].encode('utf-8'), - kpm=kpm + submission=request_data["submission"].encode("utf-8"), + kpm=kpm, ) return { - 'success': True, - 'data': { - 'status': "already_solved", - 'message': 'You already solved this' - } + "success": True, + "data": { + "status": "already_solved", + "message": "You already solved this", + }, } -@challenges_namespace.route('//solves') -@challenges_namespace.param('id', 'A Challenge ID') +@challenges_namespace.route("//solves") +@challenges_namespace.param("id", "A Challenge ID") class ChallengeSolves(Resource): @check_challenge_visibility @check_score_visibility @@ -515,68 +507,67 @@ class ChallengeSolves(Resource): # TODO: Need a generic challenge visibility call. # However, it should be stated that a solve on a gated challenge is not considered private. - if challenge.state == 'hidden' and is_admin() is False: + if challenge.state == "hidden" and is_admin() is False: abort(404) Model = get_model() - solves = Solves.query.join(Model, Solves.account_id == Model.id)\ - .filter(Solves.challenge_id == challenge_id, Model.banned == False, Model.hidden == False)\ + solves = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter( + Solves.challenge_id == challenge_id, + Model.banned == False, + Model.hidden == False, + ) .order_by(Solves.date.asc()) + ) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze: - preview = request.args.get('preview') + preview = request.args.get("preview") if (is_admin() is False) or (is_admin() is True and preview): dt = datetime.datetime.utcfromtimestamp(freeze) solves = solves.filter(Solves.date < dt) endpoint = None - if get_config('user_mode') == TEAMS_MODE: - endpoint = 'teams.public' - arg = 'team_id' - elif get_config('user_mode') == USERS_MODE: - endpoint = 'users.public' - arg = 'user_id' + if get_config("user_mode") == TEAMS_MODE: + endpoint = "teams.public" + arg = "team_id" + elif get_config("user_mode") == USERS_MODE: + endpoint = "users.public" + arg = "user_id" for solve in solves: - response.append({ - 'account_id': solve.account_id, - 'name': solve.account.name, - 'date': isoformat(solve.date), - 'account_url': url_for(endpoint, **{arg: solve.account_id}) - }) + response.append( + { + "account_id": solve.account_id, + "name": solve.account.name, + "date": isoformat(solve.date), + "account_url": url_for(endpoint, **{arg: solve.account_id}), + } + ) - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} -@challenges_namespace.route('//files') -@challenges_namespace.param('id', 'A Challenge ID') +@challenges_namespace.route("//files") +@challenges_namespace.param("id", "A Challenge ID") class ChallengeFiles(Resource): @admins_only def get(self, challenge_id): response = [] challenge_files = ChallengeFilesModel.query.filter_by( - challenge_id=challenge_id).all() + challenge_id=challenge_id + ).all() for f in challenge_files: - response.append({ - 'id': f.id, - 'type': f.type, - 'location': f.location - }) - return { - 'success': True, - 'data': response - } + response.append({"id": f.id, "type": f.type, "location": f.location}) + return {"success": True, "data": response} -@challenges_namespace.route('//tags') -@challenges_namespace.param('id', 'A Challenge ID') +@challenges_namespace.route("//tags") +@challenges_namespace.param("id", "A Challenge ID") class ChallengeTags(Resource): @admins_only def get(self, challenge_id): @@ -585,19 +576,14 @@ class ChallengeTags(Resource): tags = Tags.query.filter_by(challenge_id=challenge_id).all() for t in tags: - response.append({ - 'id': t.id, - 'challenge_id': t.challenge_id, - 'value': t.value - }) - return { - 'success': True, - 'data': response - } + response.append( + {"id": t.id, "challenge_id": t.challenge_id, "value": t.value} + ) + return {"success": True, "data": response} -@challenges_namespace.route('//hints') -@challenges_namespace.param('id', 'A Challenge ID') +@challenges_namespace.route("//hints") +@challenges_namespace.param("id", "A Challenge ID") class ChallengeHints(Resource): @admins_only def get(self, challenge_id): @@ -606,19 +592,13 @@ class ChallengeHints(Resource): response = schema.dump(hints) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@challenges_namespace.route('//flags') -@challenges_namespace.param('id', 'A Challenge ID') +@challenges_namespace.route("//flags") +@challenges_namespace.param("id", "A Challenge ID") class ChallengeFlags(Resource): @admins_only def get(self, challenge_id): @@ -627,12 +607,6 @@ class ChallengeFlags(Resource): response = schema.dump(flags) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} diff --git a/CTFd/api/v1/config.py b/CTFd/api/v1/config.py index b136b51..51a1f57 100644 --- a/CTFd/api/v1/config.py +++ b/CTFd/api/v1/config.py @@ -2,16 +2,14 @@ from flask import request from flask_restplus import Namespace, Resource from CTFd.models import db, Configs from CTFd.schemas.config import ConfigSchema -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only from CTFd.utils import get_config, set_config from CTFd.cache import clear_config, clear_standings -configs_namespace = Namespace('configs', description="Endpoint to retrieve Configs") +configs_namespace = Namespace("configs", description="Endpoint to retrieve Configs") -@configs_namespace.route('') +@configs_namespace.route("") class ConfigList(Resource): @admins_only def get(self): @@ -19,15 +17,9 @@ class ConfigList(Resource): schema = ConfigSchema(many=True) response = schema.dump(configs) if response.errors: - return { - 'success': False, - 'errors': response.errors, - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): @@ -36,10 +28,7 @@ class ConfigList(Resource): response = schema.load(req) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -50,10 +39,7 @@ class ConfigList(Resource): clear_config() clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self): @@ -65,20 +51,15 @@ class ConfigList(Resource): clear_config() clear_standings() - return { - 'success': True - } + return {"success": True} -@configs_namespace.route('/') +@configs_namespace.route("/") class Config(Resource): @admins_only def get(self, config_key): - return { - 'success': True, - 'data': get_config(config_key) - } + return {"success": True, "data": get_config(config_key)} @admins_only def patch(self, config_key): @@ -89,7 +70,7 @@ class Config(Resource): response = schema.load(data) else: schema = ConfigSchema() - data['key'] = config_key + data["key"] = config_key response = schema.load(data) db.session.add(response.data) @@ -104,10 +85,7 @@ class Config(Resource): clear_config() clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, config_key): @@ -120,6 +98,4 @@ class Config(Resource): clear_config() clear_standings() - return { - 'success': True, - } + return {"success": True} diff --git a/CTFd/api/v1/files.py b/CTFd/api/v1/files.py index 03e9940..ef50024 100644 --- a/CTFd/api/v1/files.py +++ b/CTFd/api/v1/files.py @@ -3,36 +3,28 @@ from flask_restplus import Namespace, Resource from CTFd.models import db, Files from CTFd.schemas.files import FileSchema from CTFd.utils import uploads -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only -files_namespace = Namespace('files', description="Endpoint to retrieve Files") +files_namespace = Namespace("files", description="Endpoint to retrieve Files") -@files_namespace.route('') +@files_namespace.route("") class FilesList(Resource): @admins_only def get(self): - file_type = request.args.get('type') + file_type = request.args.get("type") files = Files.query.filter_by(type=file_type).all() schema = FileSchema(many=True) response = schema.dump(files) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): - files = request.files.getlist('file') + files = request.files.getlist("file") # challenge_id # page_id @@ -46,18 +38,12 @@ class FilesList(Resource): response = schema.dump(objs) if response.errors: - return { - 'success': False, - 'errors': response.errorss - }, 400 + return {"success": False, "errors": response.errorss}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@files_namespace.route('/') +@files_namespace.route("/") class FilesDetail(Resource): @admins_only def get(self, file_id): @@ -66,15 +52,9 @@ class FilesDetail(Resource): response = schema.dump(f) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, file_id): @@ -84,6 +64,4 @@ class FilesDetail(Resource): db.session.commit() db.session.close() - return { - 'success': True, - } + return {"success": True} diff --git a/CTFd/api/v1/flags.py b/CTFd/api/v1/flags.py index 1f8d3c0..c7977af 100644 --- a/CTFd/api/v1/flags.py +++ b/CTFd/api/v1/flags.py @@ -3,14 +3,12 @@ from flask_restplus import Namespace, Resource from CTFd.models import db, Flags from CTFd.schemas.flags import FlagSchema from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only -flags_namespace = Namespace('flags', description="Endpoint to retrieve Flags") +flags_namespace = Namespace("flags", description="Endpoint to retrieve Flags") -@flags_namespace.route('') +@flags_namespace.route("") class FlagList(Resource): @admins_only def get(self): @@ -18,15 +16,9 @@ class FlagList(Resource): schema = FlagSchema(many=True) response = schema.dump(flags) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): @@ -35,10 +27,7 @@ class FlagList(Resource): response = schema.load(req, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -46,42 +35,30 @@ class FlagList(Resource): response = schema.dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@flags_namespace.route('/types', defaults={'type_name': None}) -@flags_namespace.route('/types/') +@flags_namespace.route("/types", defaults={"type_name": None}) +@flags_namespace.route("/types/") class FlagTypes(Resource): @admins_only def get(self, type_name): if type_name: flag_class = get_flag_class(type_name) - response = { - 'name': flag_class.name, - 'templates': flag_class.templates - } - return { - 'success': True, - 'data': response - } + response = {"name": flag_class.name, "templates": flag_class.templates} + return {"success": True, "data": response} else: response = {} for class_id in FLAG_CLASSES: flag_class = FLAG_CLASSES.get(class_id) response[class_id] = { - 'name': flag_class.name, - 'templates': flag_class.templates, + "name": flag_class.name, + "templates": flag_class.templates, } - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} -@flags_namespace.route('/') +@flags_namespace.route("/") class Flag(Resource): @admins_only def get(self, flag_id): @@ -90,17 +67,11 @@ class Flag(Resource): response = schema.dump(flag) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - response.data['templates'] = get_flag_class(flag.type).templates + response.data["templates"] = get_flag_class(flag.type).templates - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, flag_id): @@ -110,9 +81,7 @@ class Flag(Resource): db.session.commit() db.session.close() - return { - 'success': True - } + return {"success": True} @admins_only def patch(self, flag_id): @@ -123,17 +92,11 @@ class Flag(Resource): response = schema.load(req, session=db.session, instance=flag, partial=True) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() response = schema.dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} diff --git a/CTFd/api/v1/hints.py b/CTFd/api/v1/hints.py index 0c2188f..d7abf2c 100644 --- a/CTFd/api/v1/hints.py +++ b/CTFd/api/v1/hints.py @@ -3,16 +3,12 @@ from flask_restplus import Namespace, Resource from CTFd.models import db, Hints, HintUnlocks from CTFd.utils.user import get_current_user, is_admin from CTFd.schemas.hints import HintSchema -from CTFd.utils.decorators import ( - during_ctf_time_only, - admins_only, - authed_only -) +from CTFd.utils.decorators import during_ctf_time_only, admins_only, authed_only -hints_namespace = Namespace('hints', description="Endpoint to retrieve Hints") +hints_namespace = Namespace("hints", description="Endpoint to retrieve Hints") -@hints_namespace.route('') +@hints_namespace.route("") class HintList(Resource): @admins_only def get(self): @@ -20,40 +16,28 @@ class HintList(Resource): response = HintSchema(many=True).dump(hints) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): req = request.get_json() - schema = HintSchema('admin') + schema = HintSchema("admin") response = schema.load(req, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() response = schema.dump(response.data) - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@hints_namespace.route('/') +@hints_namespace.route("/") class Hint(Resource): @during_ctf_time_only @authed_only @@ -61,32 +45,25 @@ class Hint(Resource): user = get_current_user() hint = Hints.query.filter_by(id=hint_id).first_or_404() - view = 'unlocked' + view = "unlocked" if hint.cost: - view = 'locked' + view = "locked" unlocked = HintUnlocks.query.filter_by( - account_id=user.account_id, - target=hint.id + account_id=user.account_id, target=hint.id ).first() if unlocked: - view = 'unlocked' + view = "unlocked" if is_admin(): - if request.args.get('preview', False): - view = 'admin' + if request.args.get("preview", False): + view = "admin" response = HintSchema(view=view).dump(hint) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self, hint_id): @@ -97,20 +74,14 @@ class Hint(Resource): response = schema.load(req, instance=hint, partial=True, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() response = schema.dump(response.data) - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, hint_id): @@ -119,6 +90,4 @@ class Hint(Resource): db.session.commit() db.session.close() - return { - 'success': True - } + return {"success": True} diff --git a/CTFd/api/v1/notifications.py b/CTFd/api/v1/notifications.py index c0db7b8..5f8419e 100644 --- a/CTFd/api/v1/notifications.py +++ b/CTFd/api/v1/notifications.py @@ -3,28 +3,22 @@ from flask_restplus import Namespace, Resource from CTFd.models import db, Notifications from CTFd.schemas.notifications import NotificationSchema -from CTFd.utils.decorators import ( - admins_only +from CTFd.utils.decorators import admins_only + +notifications_namespace = Namespace( + "notifications", description="Endpoint to retrieve Notifications" ) -notifications_namespace = Namespace('notifications', description="Endpoint to retrieve Notifications") - -@notifications_namespace.route('') +@notifications_namespace.route("") class NotificantionList(Resource): def get(self): notifications = Notifications.query.all() schema = NotificationSchema(many=True) result = schema.dump(notifications) if result.errors: - return { - 'success': False, - 'errors': result.errors - }, 400 - return { - 'success': True, - 'data': result.data - } + return {"success": False, "errors": result.errors}, 400 + return {"success": True, "data": result.data} @admins_only def post(self): @@ -34,42 +28,28 @@ class NotificantionList(Resource): result = schema.load(req) if result.errors: - return { - 'success': False, - 'errors': result.errors - }, 400 + return {"success": False, "errors": result.errors}, 400 db.session.add(result.data) db.session.commit() response = schema.dump(result.data) - current_app.events_manager.publish( - data=response.data, type='notification' - ) + current_app.events_manager.publish(data=response.data, type="notification") - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@notifications_namespace.route('/') -@notifications_namespace.param('notification_id', 'A Notification ID') +@notifications_namespace.route("/") +@notifications_namespace.param("notification_id", "A Notification ID") class Notification(Resource): def get(self, notification_id): notif = Notifications.query.filter_by(id=notification_id).first_or_404() schema = NotificationSchema() response = schema.dump(notif) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, notification_id): @@ -78,6 +58,4 @@ class Notification(Resource): db.session.commit() db.session.close() - return { - 'success': True, - } + return {"success": True} diff --git a/CTFd/api/v1/pages.py b/CTFd/api/v1/pages.py index 5158e6e..5d6f53d 100644 --- a/CTFd/api/v1/pages.py +++ b/CTFd/api/v1/pages.py @@ -4,30 +4,22 @@ from CTFd.models import db, Pages from CTFd.schemas.pages import PageSchema from CTFd.cache import clear_pages -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only -pages_namespace = Namespace('pages', description="Endpoint to retrieve Pages") +pages_namespace = Namespace("pages", description="Endpoint to retrieve Pages") -@pages_namespace.route('') +@pages_namespace.route("") class PageList(Resource): @admins_only def get(self): pages = Pages.query.all() - schema = PageSchema(exclude=['content'], many=True) + schema = PageSchema(exclude=["content"], many=True) response = schema.dump(pages) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): @@ -36,10 +28,7 @@ class PageList(Resource): response = schema.load(req) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -49,13 +38,10 @@ class PageList(Resource): clear_pages() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@pages_namespace.route('/') +@pages_namespace.route("/") class PageDetail(Resource): @admins_only def get(self, page_id): @@ -64,15 +50,9 @@ class PageDetail(Resource): response = schema.dump(page) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self, page_id): @@ -83,10 +63,7 @@ class PageDetail(Resource): response = schema.load(req, instance=page, partial=True) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() @@ -95,10 +72,7 @@ class PageDetail(Resource): clear_pages() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, page_id): @@ -109,6 +83,4 @@ class PageDetail(Resource): clear_pages() - return { - 'success': True - } + return {"success": True} diff --git a/CTFd/api/v1/scoreboard.py b/CTFd/api/v1/scoreboard.py index 61bfc98..66f6fae 100644 --- a/CTFd/api/v1/scoreboard.py +++ b/CTFd/api/v1/scoreboard.py @@ -6,12 +6,17 @@ from CTFd.utils.scores import get_standings from CTFd.utils import get_config from CTFd.utils.modes import TEAMS_MODE from CTFd.utils.dates import unix_time_to_utc, isoformat -from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility +from CTFd.utils.decorators.visibility import ( + check_account_visibility, + check_score_visibility, +) -scoreboard_namespace = Namespace('scoreboard', description="Endpoint to retrieve scores") +scoreboard_namespace = Namespace( + "scoreboard", description="Endpoint to retrieve scores" +) -@scoreboard_namespace.route('') +@scoreboard_namespace.route("") class ScoreboardList(Resource): @check_account_visibility @check_score_visibility @@ -19,7 +24,7 @@ class ScoreboardList(Resource): def get(self): standings = get_standings() response = [] - mode = get_config('user_mode') + mode = get_config("user_mode") if mode == TEAMS_MODE: team_ids = [] @@ -30,36 +35,33 @@ class ScoreboardList(Resource): for i, x in enumerate(standings): entry = { - 'pos': i + 1, - 'account_id': x.account_id, - 'oauth_id': x.oauth_id, - 'name': x.name, - 'score': int(x.score) + "pos": i + 1, + "account_id": x.account_id, + "oauth_id": x.oauth_id, + "name": x.name, + "score": int(x.score), } if mode == TEAMS_MODE: members = [] for member in teams[i].members: - members.append({ - 'id': member.id, - 'oauth_id': member.oauth_id, - 'name': member.name, - 'score': int(member.score) - }) + members.append( + { + "id": member.id, + "oauth_id": member.oauth_id, + "name": member.name, + "score": int(member.score), + } + ) - entry['members'] = members + entry["members"] = members - response.append( - entry - ) - return { - 'success': True, - 'data': response - } + response.append(entry) + return {"success": True, "data": response} -@scoreboard_namespace.route('/top/') -@scoreboard_namespace.param('count', 'How many top teams to return') +@scoreboard_namespace.route("/top/") +@scoreboard_namespace.param("count", "How many top teams to return") class ScoreboardDetail(Resource): @check_account_visibility @check_score_visibility @@ -74,7 +76,7 @@ class ScoreboardDetail(Resource): solves = Solves.query.filter(Solves.account_id.in_(team_ids)) awards = Awards.query.filter(Awards.account_id.in_(team_ids)) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze: solves = solves.filter(Solves.date < unix_time_to_utc(freeze)) @@ -85,33 +87,36 @@ class ScoreboardDetail(Resource): for i, team in enumerate(team_ids): response[i + 1] = { - 'id': standings[i].account_id, - 'name': standings[i].name, - 'solves': [] + "id": standings[i].account_id, + "name": standings[i].name, + "solves": [], } for solve in solves: if solve.account_id == team: - response[i + 1]['solves'].append({ - 'challenge_id': solve.challenge_id, - 'account_id': solve.account_id, - 'team_id': solve.team_id, - 'user_id': solve.user_id, - 'value': solve.challenge.value, - 'date': isoformat(solve.date) - }) + response[i + 1]["solves"].append( + { + "challenge_id": solve.challenge_id, + "account_id": solve.account_id, + "team_id": solve.team_id, + "user_id": solve.user_id, + "value": solve.challenge.value, + "date": isoformat(solve.date), + } + ) for award in awards: if award.account_id == team: - response[i + 1]['solves'].append({ - 'challenge_id': None, - 'account_id': award.account_id, - 'team_id': award.team_id, - 'user_id': award.user_id, - 'value': award.value, - 'date': isoformat(award.date) - }) - response[i + 1]['solves'] = sorted(response[i + 1]['solves'], key=lambda k: k['date']) + response[i + 1]["solves"].append( + { + "challenge_id": None, + "account_id": award.account_id, + "team_id": award.team_id, + "user_id": award.user_id, + "value": award.value, + "date": isoformat(award.date), + } + ) + response[i + 1]["solves"] = sorted( + response[i + 1]["solves"], key=lambda k: k["date"] + ) - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} diff --git a/CTFd/api/v1/statistics/__init__.py b/CTFd/api/v1/statistics/__init__.py index 6efbc06..2c0dc59 100644 --- a/CTFd/api/v1/statistics/__init__.py +++ b/CTFd/api/v1/statistics/__init__.py @@ -1,6 +1,8 @@ from flask_restplus import Namespace -statistics_namespace = Namespace('statistics', description="Endpoint to retrieve Statistics") +statistics_namespace = Namespace( + "statistics", description="Endpoint to retrieve Statistics" +) from CTFd.api.v1.statistics import challenges # noqa: F401 from CTFd.api.v1.statistics import teams # noqa: F401 diff --git a/CTFd/api/v1/statistics/challenges.py b/CTFd/api/v1/statistics/challenges.py index c947f40..b121992 100644 --- a/CTFd/api/v1/statistics/challenges.py +++ b/CTFd/api/v1/statistics/challenges.py @@ -1,123 +1,119 @@ from flask_restplus import Resource from CTFd.models import db, Challenges, Solves from CTFd.utils.modes import get_model -from CTFd.utils.decorators import ( - admins_only, -) +from CTFd.utils.decorators import admins_only from CTFd.api.v1.statistics import statistics_namespace from sqlalchemy import func from sqlalchemy.sql import or_ -@statistics_namespace.route('/challenges/') +@statistics_namespace.route("/challenges/") class ChallengePropertyCounts(Resource): @admins_only def get(self, column): if column in Challenges.__table__.columns.keys(): prop = getattr(Challenges, column) - data = Challenges.query\ - .with_entities(prop, func.count(prop))\ - .group_by(prop)\ + data = ( + Challenges.query.with_entities(prop, func.count(prop)) + .group_by(prop) .all() - return { - 'success': True, - 'data': dict(data) - } + ) + return {"success": True, "data": dict(data)} else: - response = { - 'message': 'That could not be found' - }, 404 + response = {"message": "That could not be found"}, 404 return response -@statistics_namespace.route('/challenges/solves') +@statistics_namespace.route("/challenges/solves") class ChallengeSolveStatistics(Resource): @admins_only def get(self): - chals = Challenges.query \ - .filter(or_(Challenges.state != 'hidden', Challenges.state != 'locked')) \ - .order_by(Challenges.value) \ + chals = ( + Challenges.query.filter( + or_(Challenges.state != "hidden", Challenges.state != "locked") + ) + .order_by(Challenges.value) .all() + ) Model = get_model() - solves_sub = db.session.query( - Solves.challenge_id, - db.func.count(Solves.challenge_id).label('solves') - ) \ - .join(Model, Solves.account_id == Model.id) \ - .filter(Model.banned == False, Model.hidden == False) \ - .group_by(Solves.challenge_id).subquery() + solves_sub = ( + db.session.query( + Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves") + ) + .join(Model, Solves.account_id == Model.id) + .filter(Model.banned == False, Model.hidden == False) + .group_by(Solves.challenge_id) + .subquery() + ) - solves = db.session.query( - solves_sub.columns.challenge_id, - solves_sub.columns.solves, - Challenges.name - ) \ - .join(Challenges, solves_sub.columns.challenge_id == Challenges.id).all() + solves = ( + db.session.query( + solves_sub.columns.challenge_id, + solves_sub.columns.solves, + Challenges.name, + ) + .join(Challenges, solves_sub.columns.challenge_id == Challenges.id) + .all() + ) response = [] has_solves = [] for challenge_id, count, name in solves: - challenge = { - 'id': challenge_id, - 'name': name, - 'solves': count, - } + challenge = {"id": challenge_id, "name": name, "solves": count} response.append(challenge) has_solves.append(challenge_id) for c in chals: if c.id not in has_solves: - challenge = { - 'id': c.id, - 'name': c.name, - 'solves': 0, - } + challenge = {"id": c.id, "name": c.name, "solves": 0} response.append(challenge) db.session.close() - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} -@statistics_namespace.route('/challenges/solves/percentages') +@statistics_namespace.route("/challenges/solves/percentages") class ChallengeSolvePercentages(Resource): @admins_only def get(self): - challenges = Challenges.query\ - .add_columns('id', 'name', 'state', 'max_attempts')\ - .order_by(Challenges.value).all() + challenges = ( + Challenges.query.add_columns("id", "name", "state", "max_attempts") + .order_by(Challenges.value) + .all() + ) Model = get_model() - teams_with_points = db.session.query(Solves.account_id) \ - .join(Model) \ - .filter(Model.banned == False, Model.hidden == False) \ - .group_by(Solves.account_id) \ + teams_with_points = ( + db.session.query(Solves.account_id) + .join(Model) + .filter(Model.banned == False, Model.hidden == False) + .group_by(Solves.account_id) .count() + ) percentage_data = [] for challenge in challenges: - solve_count = Solves.query.join(Model, Solves.account_id == Model.id) \ - .filter(Solves.challenge_id == challenge.id, Model.banned == False, Model.hidden == False) \ + solve_count = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter( + Solves.challenge_id == challenge.id, + Model.banned == False, + Model.hidden == False, + ) .count() + ) if teams_with_points > 0: - percentage = (float(solve_count) / float(teams_with_points)) + percentage = float(solve_count) / float(teams_with_points) else: percentage = 0.0 - percentage_data.append({ - 'id': challenge.id, - 'name': challenge.name, - 'percentage': percentage, - }) + percentage_data.append( + {"id": challenge.id, "name": challenge.name, "percentage": percentage} + ) - response = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True) - return { - 'success': True, - 'data': response - } + response = sorted(percentage_data, key=lambda x: x["percentage"], reverse=True) + return {"success": True, "data": response} diff --git a/CTFd/api/v1/statistics/submissions.py b/CTFd/api/v1/statistics/submissions.py index 6f631f3..c088e05 100644 --- a/CTFd/api/v1/statistics/submissions.py +++ b/CTFd/api/v1/statistics/submissions.py @@ -1,29 +1,22 @@ from flask_restplus import Resource from CTFd.models import Submissions -from CTFd.utils.decorators import ( - admins_only, -) +from CTFd.utils.decorators import admins_only from CTFd.api.v1.statistics import statistics_namespace from sqlalchemy import func -@statistics_namespace.route('/submissions/') +@statistics_namespace.route("/submissions/") class SubmissionPropertyCounts(Resource): @admins_only def get(self, column): if column in Submissions.__table__.columns.keys(): prop = getattr(Submissions, column) - data = Submissions.query \ - .with_entities(prop, func.count(prop)) \ - .group_by(prop) \ + data = ( + Submissions.query.with_entities(prop, func.count(prop)) + .group_by(prop) .all() - return { - 'success': True, - 'data': dict(data) - } + ) + return {"success": True, "data": dict(data)} else: - response = { - 'success': False, - 'errors': 'That could not be found' - }, 404 + response = {"success": False, "errors": "That could not be found"}, 404 return response diff --git a/CTFd/api/v1/statistics/teams.py b/CTFd/api/v1/statistics/teams.py index d95664a..3611102 100644 --- a/CTFd/api/v1/statistics/teams.py +++ b/CTFd/api/v1/statistics/teams.py @@ -1,20 +1,13 @@ from flask_restplus import Resource from CTFd.models import Teams -from CTFd.utils.decorators import ( - admins_only, -) +from CTFd.utils.decorators import admins_only from CTFd.api.v1.statistics import statistics_namespace -@statistics_namespace.route('/teams') +@statistics_namespace.route("/teams") class TeamStatistics(Resource): @admins_only def get(self): registered = Teams.query.count() - data = { - 'registered': registered, - } - return { - 'success': True, - 'data': data - } + data = {"registered": registered} + return {"success": True, "data": data} diff --git a/CTFd/api/v1/statistics/users.py b/CTFd/api/v1/statistics/users.py index 7502841..fcc3567 100644 --- a/CTFd/api/v1/statistics/users.py +++ b/CTFd/api/v1/statistics/users.py @@ -5,37 +5,24 @@ from CTFd.utils.decorators import admins_only from sqlalchemy import func -@statistics_namespace.route('/users') +@statistics_namespace.route("/users") class UserStatistics(Resource): def get(self): registered = Users.query.count() confirmed = Users.query.filter_by(verified=True).count() - data = { - 'registered': registered, - 'confirmed': confirmed - } - return { - 'success': True, - 'data': data - } + data = {"registered": registered, "confirmed": confirmed} + return {"success": True, "data": data} -@statistics_namespace.route('/users/') +@statistics_namespace.route("/users/") class UserPropertyCounts(Resource): @admins_only def get(self, column): if column in Users.__table__.columns.keys(): prop = getattr(Users, column) - data = Users.query \ - .with_entities(prop, func.count(prop)) \ - .group_by(prop) \ - .all() - return { - 'success': True, - 'data': dict(data) - } + data = ( + Users.query.with_entities(prop, func.count(prop)).group_by(prop).all() + ) + return {"success": True, "data": dict(data)} else: - return { - 'success': False, - 'message': 'That could not be found' - }, 404 + return {"success": False, "message": "That could not be found"}, 404 diff --git a/CTFd/api/v1/submissions.py b/CTFd/api/v1/submissions.py index 9fc36d3..55ddbdb 100644 --- a/CTFd/api/v1/submissions.py +++ b/CTFd/api/v1/submissions.py @@ -4,16 +4,15 @@ from flask_restplus import Namespace, Resource from CTFd.cache import clear_standings from CTFd.models import db, Submissions from CTFd.schemas.submissions import SubmissionSchema -from CTFd.utils.decorators import ( - admins_only, +from CTFd.utils.decorators import admins_only + +submissions_namespace = Namespace( + "submissions", description="Endpoint to retrieve Submission" ) -submissions_namespace = Namespace('submissions', description="Endpoint to retrieve Submission") - -@submissions_namespace.route('') +@submissions_namespace.route("") class SubmissionsList(Resource): - @admins_only def get(self): args = request.args.to_dict() @@ -26,27 +25,18 @@ class SubmissionsList(Resource): response = schema.dump(submissions) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): req = request.get_json() - Model = Submissions.get_child(type=req.get('type')) + Model = Submissions.get_child(type=req.get("type")) schema = SubmissionSchema(instance=Model()) response = schema.load(req) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -57,14 +47,11 @@ class SubmissionsList(Resource): # Delete standings cache clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@submissions_namespace.route('/') -@submissions_namespace.param('submission_id', 'A Submission ID') +@submissions_namespace.route("/") +@submissions_namespace.param("submission_id", "A Submission ID") class Submission(Resource): @admins_only def get(self, submission_id): @@ -73,15 +60,9 @@ class Submission(Resource): response = schema.dump(submission) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, submission_id): @@ -93,6 +74,4 @@ class Submission(Resource): # Delete standings cache clear_standings() - return { - 'success': True - } + return {"success": True} diff --git a/CTFd/api/v1/tags.py b/CTFd/api/v1/tags.py index 5732734..e5ef8cf 100644 --- a/CTFd/api/v1/tags.py +++ b/CTFd/api/v1/tags.py @@ -2,14 +2,12 @@ from flask import request from flask_restplus import Namespace, Resource from CTFd.models import db, Tags from CTFd.schemas.tags import TagSchema -from CTFd.utils.decorators import ( - admins_only -) +from CTFd.utils.decorators import admins_only -tags_namespace = Namespace('tags', description="Endpoint to retrieve Tags") +tags_namespace = Namespace("tags", description="Endpoint to retrieve Tags") -@tags_namespace.route('') +@tags_namespace.route("") class TagList(Resource): @admins_only def get(self): @@ -19,15 +17,9 @@ class TagList(Resource): response = schema.dump(tags) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): @@ -36,10 +28,7 @@ class TagList(Resource): response = schema.load(req, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -47,14 +36,11 @@ class TagList(Resource): response = schema.dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@tags_namespace.route('/') -@tags_namespace.param('tag_id', 'A Tag ID') +@tags_namespace.route("/") +@tags_namespace.param("tag_id", "A Tag ID") class Tag(Resource): @admins_only def get(self, tag_id): @@ -63,15 +49,9 @@ class Tag(Resource): response = TagSchema().dump(tag) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self, tag_id): @@ -81,20 +61,14 @@ class Tag(Resource): response = schema.load(req, session=db.session, instance=tag) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() response = schema.dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, tag_id): @@ -103,6 +77,4 @@ class Tag(Resource): db.session.commit() db.session.close() - return { - 'success': True - } + return {"success": True} diff --git a/CTFd/api/v1/teams.py b/CTFd/api/v1/teams.py index a9410aa..125879a 100644 --- a/CTFd/api/v1/teams.py +++ b/CTFd/api/v1/teams.py @@ -6,58 +6,37 @@ from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.awards import AwardSchema from CTFd.cache import clear_standings from CTFd.utils.decorators.visibility import check_account_visibility -from CTFd.utils.config.visibility import ( - accounts_visible, - scores_visible -) -from CTFd.utils.user import ( - get_current_team, - is_admin, - authed -) -from CTFd.utils.decorators import ( - authed_only, - admins_only, -) +from CTFd.utils.config.visibility import accounts_visible, scores_visible +from CTFd.utils.user import get_current_team, is_admin, authed +from CTFd.utils.decorators import authed_only, admins_only import copy -teams_namespace = Namespace('teams', description="Endpoint to retrieve Teams") +teams_namespace = Namespace("teams", description="Endpoint to retrieve Teams") -@teams_namespace.route('') +@teams_namespace.route("") class TeamList(Resource): @check_account_visibility def get(self): teams = Teams.query.filter_by(hidden=False, banned=False) - view = copy.deepcopy(TeamSchema.views.get( - session.get('type', 'user') - )) - view.remove('members') + view = copy.deepcopy(TeamSchema.views.get(session.get("type", "user"))) + view.remove("members") response = TeamSchema(view=view, many=True).dump(teams) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def post(self): req = request.get_json() - view = TeamSchema.views.get(session.get('type', 'self')) + view = TeamSchema.views.get(session.get("type", "self")) schema = TeamSchema(view=view) response = schema.load(req) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() @@ -67,14 +46,11 @@ class TeamList(Resource): clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@teams_namespace.route('/') -@teams_namespace.param('team_id', "Team ID") +@teams_namespace.route("/") +@teams_namespace.param("team_id", "Team ID") class TeamPublic(Resource): @check_account_visibility def get(self, team_id): @@ -83,35 +59,26 @@ class TeamPublic(Resource): if (team.banned or team.hidden) and is_admin() is False: abort(404) - view = TeamSchema.views.get(session.get('type', 'user')) + view = TeamSchema.views.get(session.get("type", "user")) schema = TeamSchema(view=view) response = schema.dump(team) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self, team_id): team = Teams.query.filter_by(id=team_id).first_or_404() data = request.get_json() - data['id'] = team_id + data["id"] = team_id - schema = TeamSchema(view='admin', instance=team, partial=True) + schema = TeamSchema(view="admin", instance=team, partial=True) response = schema.load(data) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 response = schema.dump(response.data) db.session.commit() @@ -119,10 +86,7 @@ class TeamPublic(Resource): clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def delete(self, team_id): @@ -137,170 +101,131 @@ class TeamPublic(Resource): clear_standings() - return { - 'success': True, - } + return {"success": True} -@teams_namespace.route('/me') -@teams_namespace.param('team_id', "Current Team") +@teams_namespace.route("/me") +@teams_namespace.param("team_id", "Current Team") class TeamPrivate(Resource): @authed_only def get(self): team = get_current_team() - response = TeamSchema(view='self').dump(team) + response = TeamSchema(view="self").dump(team) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @authed_only def patch(self): team = get_current_team() - if team.captain_id != session['id']: - return { - 'success': False, - 'errors': { - '': [ - 'Only team captains can edit team information' - ] - } - }, 400 + if team.captain_id != session["id"]: + return ( + { + "success": False, + "errors": {"": ["Only team captains can edit team information"]}, + }, + 400, + ) data = request.get_json() - response = TeamSchema(view='self', instance=team, partial=True).load(data) + response = TeamSchema(view="self", instance=team, partial=True).load(data) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() - response = TeamSchema('self').dump(response.data) + response = TeamSchema("self").dump(response.data) db.session.close() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@teams_namespace.route('//members') -@teams_namespace.param('team_id', "Team ID") +@teams_namespace.route("//members") +@teams_namespace.param("team_id", "Team ID") class TeamMembers(Resource): @admins_only def get(self, team_id): team = Teams.query.filter_by(id=team_id).first_or_404() - view = 'admin' if is_admin() else 'user' + view = "admin" if is_admin() else "user" schema = TeamSchema(view=view) response = schema.dump(team) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - members = response.data.get('members') + members = response.data.get("members") - return { - 'success': True, - 'data': members - } + return {"success": True, "data": members} @admins_only def post(self, team_id): team = Teams.query.filter_by(id=team_id).first_or_404() data = request.get_json() - user_id = data['id'] + user_id = data["id"] user = Users.query.filter_by(id=user_id).first_or_404() if user.team_id is None: team.members.append(user) db.session.commit() else: - return { - 'success': False, - 'errors': { - 'id': [ - 'User has already joined a team' - ] - } - }, 400 + return ( + { + "success": False, + "errors": {"id": ["User has already joined a team"]}, + }, + 400, + ) - view = 'admin' if is_admin() else 'user' + view = "admin" if is_admin() else "user" schema = TeamSchema(view=view) response = schema.dump(team) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - members = response.data.get('members') + members = response.data.get("members") - return { - 'success': True, - 'data': members - } + return {"success": True, "data": members} @admins_only def delete(self, team_id): team = Teams.query.filter_by(id=team_id).first_or_404() data = request.get_json() - user_id = data['id'] + user_id = data["id"] user = Users.query.filter_by(id=user_id).first_or_404() if user.team_id == team.id: team.members.remove(user) db.session.commit() else: - return { - 'success': False, - 'errors': { - 'id': [ - 'User is not part of this team' - ] - } - }, 400 + return ( + {"success": False, "errors": {"id": ["User is not part of this team"]}}, + 400, + ) - view = 'admin' if is_admin() else 'user' + view = "admin" if is_admin() else "user" schema = TeamSchema(view=view) response = schema.dump(team) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - members = response.data.get('members') + members = response.data.get("members") - return { - 'success': True, - 'data': members - } + return {"success": True, "data": members} -@teams_namespace.route('//solves') -@teams_namespace.param('team_id', "Team ID or 'me'") +@teams_namespace.route("//solves") +@teams_namespace.param("team_id", "Team ID or 'me'") class TeamSolves(Resource): - def get(self, team_id): - if team_id == 'me': + if team_id == "me": if not authed(): abort(403) team = get_current_team() @@ -314,28 +239,21 @@ class TeamSolves(Resource): abort(404) solves = team.get_solves(admin=is_admin()) - view = 'admin' if is_admin() else 'user' + view = "admin" if is_admin() else "user" schema = SubmissionSchema(view=view, many=True) response = schema.dump(solves) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@teams_namespace.route('//fails') -@teams_namespace.param('team_id', "Team ID or 'me'") +@teams_namespace.route("//fails") +@teams_namespace.param("team_id", "Team ID or 'me'") class TeamFails(Resource): - def get(self, team_id): - if team_id == 'me': + if team_id == "me": if not authed(): abort(403) team = get_current_team() @@ -349,16 +267,13 @@ class TeamFails(Resource): abort(404) fails = team.get_fails(admin=is_admin()) - view = 'admin' if is_admin() else 'user' + view = "admin" if is_admin() else "user" schema = SubmissionSchema(view=view, many=True) response = schema.dump(fails) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 if is_admin(): data = response.data @@ -366,21 +281,14 @@ class TeamFails(Resource): data = [] count = len(response.data) - return { - 'success': True, - 'data': data, - 'meta': { - 'count': count - } - } + return {"success": True, "data": data, "meta": {"count": count}} -@teams_namespace.route('//awards') -@teams_namespace.param('team_id', "Team ID or 'me'") +@teams_namespace.route("//awards") +@teams_namespace.param("team_id", "Team ID or 'me'") class TeamAwards(Resource): - def get(self, team_id): - if team_id == 'me': + if team_id == "me": if not authed(): abort(403) team = get_current_team() @@ -398,12 +306,6 @@ class TeamAwards(Resource): response = schema.dump(awards) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} diff --git a/CTFd/api/v1/unlocks.py b/CTFd/api/v1/unlocks.py index 9e0aaa6..910b77c 100644 --- a/CTFd/api/v1/unlocks.py +++ b/CTFd/api/v1/unlocks.py @@ -8,13 +8,13 @@ from CTFd.utils.decorators import ( during_ctf_time_only, require_verified_emails, admins_only, - authed_only + authed_only, ) -unlocks_namespace = Namespace('unlocks', description="Endpoint to retrieve Unlocks") +unlocks_namespace = Namespace("unlocks", description="Endpoint to retrieve Unlocks") -@unlocks_namespace.route('') +@unlocks_namespace.route("") class UnlockList(Resource): @admins_only def get(self): @@ -23,15 +23,9 @@ class UnlockList(Resource): response = schema.dump(hints) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @during_ctf_time_only @require_verified_emails @@ -40,39 +34,39 @@ class UnlockList(Resource): req = request.get_json() user = get_current_user() - req['user_id'] = user.id - req['team_id'] = user.team_id + req["user_id"] = user.id + req["team_id"] = user.team_id - Model = get_class_by_tablename(req['type']) - target = Model.query.filter_by(id=req['target']).first_or_404() + Model = get_class_by_tablename(req["type"]) + target = Model.query.filter_by(id=req["target"]).first_or_404() if target.cost > user.score: - return { - 'success': False, - 'errors': { - 'score': 'You do not have enough points to unlock this hint' - } - }, 400 + return ( + { + "success": False, + "errors": { + "score": "You do not have enough points to unlock this hint" + }, + }, + 400, + ) schema = UnlockSchema() response = schema.load(req, session=db.session) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) award_schema = AwardSchema() award = { - 'user_id': user.id, - 'team_id': user.team_id, - 'name': target.name, - 'description': target.description, - 'value': (-target.cost), - 'category': target.category + "user_id": user.id, + "team_id": user.team_id, + "name": target.name, + "description": target.description, + "value": (-target.cost), + "category": target.category, } award = award_schema.load(award) @@ -81,7 +75,4 @@ class UnlockList(Resource): response = schema.dump(response.data) - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py index a3a10a4..ebf4b6a 100644 --- a/CTFd/api/v1/users.py +++ b/CTFd/api/v1/users.py @@ -1,88 +1,77 @@ from flask import session, request, abort from flask_restplus import Namespace, Resource -from CTFd.models import db, Users, Solves, Awards, Tracking, Unlocks, Submissions, Notifications -from CTFd.utils.decorators import ( - authed_only, - admins_only, - authed, - ratelimit +from CTFd.models import ( + db, + Users, + Solves, + Awards, + Tracking, + Unlocks, + Submissions, + Notifications, ) +from CTFd.utils.decorators import authed_only, admins_only, authed, ratelimit from CTFd.cache import clear_standings from CTFd.utils.config import get_mail_provider from CTFd.utils.email import sendmail, user_created_notification from CTFd.utils.user import get_current_user, is_admin from CTFd.utils.decorators.visibility import check_account_visibility -from CTFd.utils.config.visibility import ( - accounts_visible, - scores_visible -) +from CTFd.utils.config.visibility import accounts_visible, scores_visible from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.awards import AwardSchema from CTFd.schemas.users import UserSchema -users_namespace = Namespace('users', description="Endpoint to retrieve Users") +users_namespace = Namespace("users", description="Endpoint to retrieve Users") -@users_namespace.route('') +@users_namespace.route("") class UserList(Resource): @check_account_visibility def get(self): users = Users.query.filter_by(banned=False, hidden=False) - response = UserSchema(view='user', many=True).dump(users) + response = UserSchema(view="user", many=True).dump(users) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data + return {"success": True, "data": response.data} + + @users_namespace.doc( + params={ + "notify": "Whether to send the created user an email with their credentials" } - - @users_namespace.doc(params={'notify': 'Whether to send the created user an email with their credentials'}) + ) @admins_only def post(self): req = request.get_json() - schema = UserSchema('admin') + schema = UserSchema("admin") response = schema.load(req) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.add(response.data) db.session.commit() - if request.args.get('notify'): + if request.args.get("notify"): name = response.data.name email = response.data.email - password = req.get('password') + password = req.get("password") - user_created_notification( - addr=email, - name=name, - password=password - ) + user_created_notification(addr=email, name=name, password=password) clear_standings() response = schema.dump(response.data) - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@users_namespace.route('/') -@users_namespace.param('user_id', "User ID") +@users_namespace.route("/") +@users_namespace.param("user_id", "User ID") class UserPublic(Resource): @check_account_visibility def get(self, user_id): @@ -91,36 +80,25 @@ class UserPublic(Resource): if (user.banned or user.hidden) and is_admin() is False: abort(404) - response = UserSchema( - view=session.get('type', 'user') - ).dump(user) + response = UserSchema(view=session.get("type", "user")).dump(user) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - response.data['place'] = user.place - response.data['score'] = user.score + response.data["place"] = user.place + response.data["score"] = user.score - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} @admins_only def patch(self, user_id): user = Users.query.filter_by(id=user_id).first_or_404() data = request.get_json() - data['id'] = user_id - schema = UserSchema(view='admin', instance=user, partial=True) + data["id"] = user_id + schema = UserSchema(view="admin", instance=user, partial=True) response = schema.load(data) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() @@ -130,10 +108,7 @@ class UserPublic(Resource): clear_standings() - return { - 'success': True, - 'data': response - } + return {"success": True, "data": response} @admins_only def delete(self, user_id): @@ -149,35 +124,27 @@ class UserPublic(Resource): clear_standings() - return { - 'success': True - } + return {"success": True} -@users_namespace.route('/me') +@users_namespace.route("/me") class UserPrivate(Resource): @authed_only def get(self): user = get_current_user() - response = UserSchema('self').dump(user).data - response['place'] = user.place - response['score'] = user.score - return { - 'success': True, - 'data': response - } + response = UserSchema("self").dump(user).data + response["place"] = user.place + response["score"] = user.score + return {"success": True, "data": response} @authed_only def patch(self): user = get_current_user() data = request.get_json() - schema = UserSchema(view='self', instance=user, partial=True) + schema = UserSchema(view="self", instance=user, partial=True) response = schema.load(data) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 db.session.commit() @@ -186,17 +153,14 @@ class UserPrivate(Resource): clear_standings() - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@users_namespace.route('//solves') -@users_namespace.param('user_id', "User ID or 'me'") +@users_namespace.route("//solves") +@users_namespace.param("user_id", "User ID or 'me'") class UserSolves(Resource): def get(self, user_id): - if user_id == 'me': + if user_id == "me": if not authed(): abort(403) user = get_current_user() @@ -210,26 +174,20 @@ class UserSolves(Resource): abort(404) solves = user.get_solves(admin=is_admin()) - view = 'user' if not is_admin() else 'admin' + view = "user" if not is_admin() else "admin" response = SubmissionSchema(view=view, many=True).dump(solves) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@users_namespace.route('//fails') -@users_namespace.param('user_id', "User ID or 'me'") +@users_namespace.route("//fails") +@users_namespace.param("user_id", "User ID or 'me'") class UserFails(Resource): def get(self, user_id): - if user_id == 'me': + if user_id == "me": if not authed(): abort(403) user = get_current_user() @@ -243,13 +201,10 @@ class UserFails(Resource): abort(404) fails = user.get_fails(admin=is_admin()) - view = 'user' if not is_admin() else 'admin' + view = "user" if not is_admin() else "admin" response = SubmissionSchema(view=view, many=True).dump(fails) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 if is_admin(): data = response.data @@ -257,20 +212,14 @@ class UserFails(Resource): data = [] count = len(response.data) - return { - 'success': True, - 'data': data, - 'meta': { - 'count': count - } - } + return {"success": True, "data": data, "meta": {"count": count}} -@users_namespace.route('//awards') -@users_namespace.param('user_id', "User ID or 'me'") +@users_namespace.route("//awards") +@users_namespace.param("user_id", "User ID or 'me'") class UserAwards(Resource): def get(self, user_id): - if user_id == 'me': + if user_id == "me": if not authed(): abort(403) user = get_current_user() @@ -284,57 +233,37 @@ class UserAwards(Resource): abort(404) awards = user.get_awards(admin=is_admin()) - view = 'user' if not is_admin() else 'admin' + view = "user" if not is_admin() else "admin" response = AwardSchema(view=view, many=True).dump(awards) if response.errors: - return { - 'success': False, - 'errors': response.errors - }, 400 + return {"success": False, "errors": response.errors}, 400 - return { - 'success': True, - 'data': response.data - } + return {"success": True, "data": response.data} -@users_namespace.route('//email') -@users_namespace.param('user_id', "User ID") +@users_namespace.route("//email") +@users_namespace.param("user_id", "User ID") class UserEmails(Resource): @admins_only @ratelimit(method="POST", limit=10, interval=60) def post(self, user_id): req = request.get_json() - text = req.get('text', '').strip() + text = req.get("text", "").strip() user = Users.query.filter_by(id=user_id).first_or_404() if get_mail_provider() is None: - return { - 'success': False, - 'errors': { - "": [ - "Email settings not configured" - ] - } - }, 400 + return ( + {"success": False, "errors": {"": ["Email settings not configured"]}}, + 400, + ) if not text: - return { - 'success': False, - 'errors': { - "text": [ - "Email text cannot be empty" - ] - } - }, 400 + return ( + {"success": False, "errors": {"text": ["Email text cannot be empty"]}}, + 400, + ) - result, response = sendmail( - addr=user.email, - text=text - ) + result, response = sendmail(addr=user.email, text=text) - return { - 'success': result, - 'data': {} - } + return {"success": result, "data": {}} diff --git a/CTFd/auth.py b/CTFd/auth.py index c32e039..bc99e2c 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -28,119 +28,150 @@ from CTFd.utils.helpers import error_for, get_errors import base64 import requests -auth = Blueprint('auth', __name__) +auth = Blueprint("auth", __name__) -@auth.route('/confirm', methods=['POST', 'GET']) -@auth.route('/confirm/', methods=['GET']) +@auth.route("/confirm", methods=["POST", "GET"]) +@auth.route("/confirm/", methods=["GET"]) @ratelimit(method="POST", limit=10, interval=60) def confirm(data=None): - if not get_config('verify_emails'): + if not get_config("verify_emails"): # If the CTF doesn't care about confirming email addresses then redierct to challenges - return redirect(url_for('challenges.listing')) + return redirect(url_for("challenges.listing")) # User is confirming email account if data and request.method == "GET": try: user_email = unserialize(data, max_age=1800) except (BadTimeSignature, SignatureExpired): - return render_template('confirm.html', errors=['Your confirmation link has expired']) + return render_template( + "confirm.html", errors=["Your confirmation link has expired"] + ) except (BadSignature, TypeError, base64.binascii.Error): - return render_template('confirm.html', errors=['Your confirmation token is invalid']) + return render_template( + "confirm.html", errors=["Your confirmation token is invalid"] + ) user = Users.query.filter_by(email=user_email).first_or_404() user.verified = True - log('registrations', format="[{date}] {ip} - successful confirmation for {name}", name=user.name) + log( + "registrations", + format="[{date}] {ip} - successful confirmation for {name}", + name=user.name, + ) db.session.commit() db.session.close() if current_user.authed(): - return redirect(url_for('challenges.listing')) - return redirect(url_for('auth.login')) + return redirect(url_for("challenges.listing")) + return redirect(url_for("auth.login")) # User is trying to start or restart the confirmation flow if not current_user.authed(): - return redirect(url_for('auth.login')) + return redirect(url_for("auth.login")) - user = Users.query.filter_by(id=session['id']).first_or_404() + user = Users.query.filter_by(id=session["id"]).first_or_404() if user.verified: - return redirect(url_for('views.settings')) + return redirect(url_for("views.settings")) if data is None: if request.method == "POST": # User wants to resend their confirmation email email.verify_email_address(user.email) - log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend") - return render_template('confirm.html', user=user, infos=['Your confirmation email has been resent!']) + log( + "registrations", + format="[{date}] {ip} - {name} initiated a confirmation email resend", + ) + return render_template( + "confirm.html", + user=user, + infos=["Your confirmation email has been resent!"], + ) elif request.method == "GET": # User has been directed to the confirm page - return render_template('confirm.html', user=user) + return render_template("confirm.html", user=user) -@auth.route('/reset_password', methods=['POST', 'GET']) -@auth.route('/reset_password/', methods=['POST', 'GET']) +@auth.route("/reset_password", methods=["POST", "GET"]) +@auth.route("/reset_password/", methods=["POST", "GET"]) @ratelimit(method="POST", limit=10, interval=60) def reset_password(data=None): if data is not None: try: name = unserialize(data, max_age=1800) except (BadTimeSignature, SignatureExpired): - return render_template('reset_password.html', errors=['Your link has expired']) + return render_template( + "reset_password.html", errors=["Your link has expired"] + ) except (BadSignature, TypeError, base64.binascii.Error): - return render_template('reset_password.html', errors=['Your reset token is invalid']) + return render_template( + "reset_password.html", errors=["Your reset token is invalid"] + ) if request.method == "GET": - return render_template('reset_password.html', mode='set') + return render_template("reset_password.html", mode="set") if request.method == "POST": user = Users.query.filter_by(name=name).first_or_404() - user.password = request.form['password'].strip() + user.password = request.form["password"].strip() db.session.commit() - log('logins', format="[{date}] {ip} - successful password reset for {name}", name=name) + log( + "logins", + format="[{date}] {ip} - successful password reset for {name}", + name=name, + ) db.session.close() - return redirect(url_for('auth.login')) + return redirect(url_for("auth.login")) - if request.method == 'POST': - email_address = request.form['email'].strip() + if request.method == "POST": + email_address = request.form["email"].strip() team = Users.query.filter_by(email=email_address).first() get_errors() if config.can_send_mail() is False: return render_template( - 'reset_password.html', - errors=['Email could not be sent due to server misconfiguration'] + "reset_password.html", + errors=["Email could not be sent due to server misconfiguration"], ) if not team: return render_template( - 'reset_password.html', - errors=['If that account exists you will receive an email, please check your inbox'] + "reset_password.html", + errors=[ + "If that account exists you will receive an email, please check your inbox" + ], ) email.forgot_password(email_address, team.name) return render_template( - 'reset_password.html', - errors=['If that account exists you will receive an email, please check your inbox'] + "reset_password.html", + errors=[ + "If that account exists you will receive an email, please check your inbox" + ], ) - return render_template('reset_password.html') + return render_template("reset_password.html") -@auth.route('/register', methods=['POST', 'GET']) +@auth.route("/register", methods=["POST", "GET"]) @check_registration_visibility @ratelimit(method="POST", limit=10, interval=5) def register(): errors = get_errors() - if request.method == 'POST': - name = request.form['name'] - email_address = request.form['email'] - password = request.form['password'] + if request.method == "POST": + name = request.form["name"] + email_address = request.form["email"] + password = request.form["password"] name_len = len(name) == 0 - names = Users.query.add_columns('name', 'id').filter_by(name=name).first() - emails = Users.query.add_columns('email', 'id').filter_by(email=email_address).first() + names = Users.query.add_columns("name", "id").filter_by(name=name).first() + emails = ( + Users.query.add_columns("email", "id") + .filter_by(email=email_address) + .first() + ) pass_short = len(password) == 0 pass_long = len(password) > 128 - valid_email = validators.validate_email(request.form['email']) + valid_email = validators.validate_email(request.form["email"]) team_name_email_check = validators.validate_email(name) if not valid_email: @@ -148,36 +179,36 @@ def register(): if email.check_email_is_whitelisted(email_address) is False: errors.append( "Only email addresses under {domains} may register".format( - domains=get_config('domain_whitelist') + domains=get_config("domain_whitelist") ) ) if names: - errors.append('That user name is already taken') + errors.append("That user name is already taken") if team_name_email_check is True: - errors.append('Your user name cannot be an email address') + errors.append("Your user name cannot be an email address") if emails: - errors.append('That email has already been used') + errors.append("That email has already been used") if pass_short: - errors.append('Pick a longer password') + errors.append("Pick a longer password") if pass_long: - errors.append('Pick a shorter password') + errors.append("Pick a shorter password") if name_len: - errors.append('Pick a longer user name') + errors.append("Pick a longer user name") if len(errors) > 0: return render_template( - 'register.html', + "register.html", errors=errors, - name=request.form['name'], - email=request.form['email'], - password=request.form['password'] + name=request.form["name"], + email=request.form["email"], + password=request.form["password"], ) else: with app.app_context(): user = Users( name=name.strip(), email=email_address.lower(), - password=password.strip() + password=password.strip(), ) db.session.add(user) db.session.commit() @@ -185,31 +216,40 @@ def register(): login_user(user) - if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email. - log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}") + if config.can_send_mail() and get_config( + "verify_emails" + ): # Confirming users is enabled and we can send email. + log( + "registrations", + format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}", + ) email.verify_email_address(user.email) db.session.close() - return redirect(url_for('auth.confirm')) + return redirect(url_for("auth.confirm")) else: # Don't care about confirming users - if config.can_send_mail(): # We want to notify the user that they have registered. + if ( + config.can_send_mail() + ): # We want to notify the user that they have registered. email.sendmail( - request.form['email'], - "You've successfully registered for {}".format(get_config('ctf_name')) + request.form["email"], + "You've successfully registered for {}".format( + get_config("ctf_name") + ), ) - log('registrations', "[{date}] {ip} - {name} registered with {email}") + log("registrations", "[{date}] {ip} - {name} registered with {email}") db.session.close() - return redirect(url_for('challenges.listing')) + return redirect(url_for("challenges.listing")) else: - return render_template('register.html', errors=errors) + return render_template("register.html", errors=errors) -@auth.route('/login', methods=['POST', 'GET']) +@auth.route("/login", methods=["POST", "GET"]) @ratelimit(method="POST", limit=10, interval=5) def login(): errors = get_errors() - if request.method == 'POST': - name = request.form['name'] + if request.method == "POST": + name = request.form["name"] # Check if the user submitted an email address or a team name if validators.validate_email(name) is True: @@ -218,107 +258,112 @@ def login(): user = Users.query.filter_by(name=name).first() if user: - if user and verify_password(request.form['password'], user.password): + if user and verify_password(request.form["password"], user.password): session.regenerate() login_user(user) - log('logins', "[{date}] {ip} - {name} logged in") + log("logins", "[{date}] {ip} - {name} logged in") db.session.close() - if request.args.get('next') and validators.is_safe_url(request.args.get('next')): - return redirect(request.args.get('next')) - return redirect(url_for('challenges.listing')) + if request.args.get("next") and validators.is_safe_url( + request.args.get("next") + ): + return redirect(request.args.get("next")) + return redirect(url_for("challenges.listing")) else: # This user exists but the password is wrong - log('logins', "[{date}] {ip} - submitted invalid password for {name}") + log("logins", "[{date}] {ip} - submitted invalid password for {name}") errors.append("Your username or password is incorrect") db.session.close() - return render_template('login.html', errors=errors) + return render_template("login.html", errors=errors) else: # This user just doesn't exist - log('logins', "[{date}] {ip} - submitted invalid account information") + log("logins", "[{date}] {ip} - submitted invalid account information") errors.append("Your username or password is incorrect") db.session.close() - return render_template('login.html', errors=errors) + return render_template("login.html", errors=errors) else: db.session.close() - return render_template('login.html', errors=errors) + return render_template("login.html", errors=errors) -@auth.route('/oauth') +@auth.route("/oauth") def oauth_login(): - endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \ - or get_config('oauth_authorization_endpoint') \ - or 'https://auth.majorleaguecyber.org/oauth/authorize' + endpoint = ( + get_app_config("OAUTH_AUTHORIZATION_ENDPOINT") + or get_config("oauth_authorization_endpoint") + or "https://auth.majorleaguecyber.org/oauth/authorize" + ) - if get_config('user_mode') == 'teams': - scope = 'profile team' + if get_config("user_mode") == "teams": + scope = "profile team" else: - scope = 'profile' + scope = "profile" - client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id') + client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id") if client_id is None: error_for( - endpoint='auth.login', - message='OAuth Settings not configured. ' - 'Ask your CTF administrator to configure MajorLeagueCyber integration.' + endpoint="auth.login", + message="OAuth Settings not configured. " + "Ask your CTF administrator to configure MajorLeagueCyber integration.", ) - return redirect(url_for('auth.login')) + return redirect(url_for("auth.login")) redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format( - endpoint=endpoint, - client_id=client_id, - scope=scope, - state=session['nonce'] + endpoint=endpoint, client_id=client_id, scope=scope, state=session["nonce"] ) return redirect(redirect_url) -@auth.route('/redirect', methods=['GET']) +@auth.route("/redirect", methods=["GET"]) @ratelimit(method="GET", limit=10, interval=60) def oauth_redirect(): - oauth_code = request.args.get('code') - state = request.args.get('state') - if session['nonce'] != state: - log('logins', "[{date}] {ip} - OAuth State validation mismatch") - error_for(endpoint='auth.login', message='OAuth State validation mismatch.') - return redirect(url_for('auth.login')) + oauth_code = request.args.get("code") + state = request.args.get("state") + if session["nonce"] != state: + log("logins", "[{date}] {ip} - OAuth State validation mismatch") + error_for(endpoint="auth.login", message="OAuth State validation mismatch.") + return redirect(url_for("auth.login")) if oauth_code: - url = get_app_config('OAUTH_TOKEN_ENDPOINT') \ - or get_config('oauth_token_endpoint') \ - or 'https://auth.majorleaguecyber.org/oauth/token' + url = ( + get_app_config("OAUTH_TOKEN_ENDPOINT") + or get_config("oauth_token_endpoint") + or "https://auth.majorleaguecyber.org/oauth/token" + ) - client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id') - client_secret = get_app_config('OAUTH_CLIENT_SECRET') or get_config('oauth_client_secret') - headers = { - 'content-type': 'application/x-www-form-urlencoded' - } + client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id") + client_secret = get_app_config("OAUTH_CLIENT_SECRET") or get_config( + "oauth_client_secret" + ) + headers = {"content-type": "application/x-www-form-urlencoded"} data = { - 'code': oauth_code, - 'client_id': client_id, - 'client_secret': client_secret, - 'grant_type': 'authorization_code' + "code": oauth_code, + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "authorization_code", } token_request = requests.post(url, data=data, headers=headers) if token_request.status_code == requests.codes.ok: - token = token_request.json()['access_token'] - user_url = get_app_config('OAUTH_API_ENDPOINT') \ - or get_config('oauth_api_endpoint') \ - or 'https://api.majorleaguecyber.org/user' + token = token_request.json()["access_token"] + user_url = ( + get_app_config("OAUTH_API_ENDPOINT") + or get_config("oauth_api_endpoint") + or "https://api.majorleaguecyber.org/user" + ) headers = { - 'Authorization': 'Bearer ' + str(token), - 'Content-type': 'application/json' + "Authorization": "Bearer " + str(token), + "Content-type": "application/json", } api_data = requests.get(url=user_url, headers=headers).json() - user_id = api_data['id'] - user_name = api_data['name'] - user_email = api_data['email'] + user_id = api_data["id"] + user_name = api_data["name"] + user_email = api_data["email"] user = Users.query.filter_by(email=user_email).first() if user is None: @@ -328,29 +373,25 @@ def oauth_redirect(): name=user_name, email=user_email, oauth_id=user_id, - verified=True + verified=True, ) db.session.add(user) db.session.commit() else: - log('logins', "[{date}] {ip} - Public registration via MLC blocked") + log("logins", "[{date}] {ip} - Public registration via MLC blocked") error_for( - endpoint='auth.login', - message='Public registration is disabled. Please try again later.' + endpoint="auth.login", + message="Public registration is disabled. Please try again later.", ) - return redirect(url_for('auth.login')) + return redirect(url_for("auth.login")) - if get_config('user_mode') == TEAMS_MODE: - team_id = api_data['team']['id'] - team_name = api_data['team']['name'] + if get_config("user_mode") == TEAMS_MODE: + team_id = api_data["team"]["id"] + team_name = api_data["team"]["name"] team = Teams.query.filter_by(oauth_id=team_id).first() if team is None: - team = Teams( - name=team_name, - oauth_id=team_id, - captain_id=user.id - ) + team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id) db.session.add(team) db.session.commit() @@ -364,25 +405,21 @@ def oauth_redirect(): login_user(user) - return redirect(url_for('challenges.listing')) + return redirect(url_for("challenges.listing")) else: - log('logins', "[{date}] {ip} - OAuth token retrieval failure") - error_for( - endpoint='auth.login', - message='OAuth token retrieval failure.' - ) - return redirect(url_for('auth.login')) + log("logins", "[{date}] {ip} - OAuth token retrieval failure") + error_for(endpoint="auth.login", message="OAuth token retrieval failure.") + return redirect(url_for("auth.login")) else: - log('logins', "[{date}] {ip} - Received redirect without OAuth code") + log("logins", "[{date}] {ip} - Received redirect without OAuth code") error_for( - endpoint='auth.login', - message='Received redirect without OAuth code.' + endpoint="auth.login", message="Received redirect without OAuth code." ) - return redirect(url_for('auth.login')) + return redirect(url_for("auth.login")) -@auth.route('/logout') +@auth.route("/logout") def logout(): if current_user.authed(): logout_user() - return redirect(url_for('views.static_html')) + return redirect(url_for("views.static_html")) diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py index 86c6096..d079369 100644 --- a/CTFd/cache/__init__.py +++ b/CTFd/cache/__init__.py @@ -4,7 +4,7 @@ from flask_caching import Cache cache = Cache() -def make_cache_key(path=None, key_prefix='view/%s'): +def make_cache_key(path=None, key_prefix="view/%s"): """ This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses. Over time this function may be replaced with a cleaner custom cache implementation. @@ -20,6 +20,7 @@ def make_cache_key(path=None, key_prefix='view/%s'): def clear_config(): from CTFd.utils import _get_config, get_app_config + cache.delete_memoized(_get_config) cache.delete_memoized(get_app_config) @@ -28,17 +29,15 @@ def clear_standings(): from CTFd.utils.scores import get_standings from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList from CTFd.api import api + cache.delete_memoized(get_standings) - cache.delete( - make_cache_key(path=api.name + '.' + ScoreboardList.endpoint) - ) - cache.delete( - make_cache_key(path=api.name + '.' + ScoreboardDetail.endpoint) - ) + cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint)) + cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint)) cache.delete_memoized(ScoreboardList.get) def clear_pages(): from CTFd.utils.config.pages import get_page, get_pages + cache.delete_memoized(get_pages) cache.delete_memoized(get_page) diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 927c7a1..2525240 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -1,21 +1,18 @@ -from flask import ( - render_template, - Blueprint, -) +from flask import render_template, Blueprint from CTFd.utils.decorators import ( during_ctf_time_only, require_verified_emails, - require_team + require_team, ) from CTFd.utils.decorators.visibility import check_challenge_visibility from CTFd.utils import config, get_config from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf from CTFd.utils.helpers import get_errors, get_infos -challenges = Blueprint('challenges', __name__) +challenges = Blueprint("challenges", __name__) -@challenges.route('/challenges', methods=['GET']) +@challenges.route("/challenges", methods=["GET"]) @during_ctf_time_only @require_verified_emails @check_challenge_visibility @@ -23,14 +20,16 @@ challenges = Blueprint('challenges', __name__) def listing(): infos = get_infos() errors = get_errors() - start = get_config('start') or 0 - end = get_config('end') or 0 + start = get_config("start") or 0 + end = get_config("end") or 0 if ctf_paused(): - infos.append('{} is paused'.format(config.ctf_name())) + infos.append("{} is paused".format(config.ctf_name())) # CTF has ended but we want to allow view_after_ctf. Show error but let JS load challenges. if ctf_ended() and view_after_ctf(): - infos.append('{} has ended'.format(config.ctf_name())) + infos.append("{} has ended".format(config.ctf_name())) - return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) + return render_template( + "challenges.html", infos=infos, errors=errors, start=int(start), end=int(end) + ) diff --git a/CTFd/config.py b/CTFd/config.py index 1f3557a..5370cda 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -1,12 +1,12 @@ import os -''' GENERATE SECRET KEY ''' +""" GENERATE SECRET KEY """ -if not os.getenv('SECRET_KEY'): +if not os.getenv("SECRET_KEY"): # Attempt to read the secret from the secret file # This will fail if the secret has not been written try: - with open('.ctfd_secret_key', 'rb') as secret: + with open(".ctfd_secret_key", "rb") as secret: key = secret.read() except (OSError, IOError): key = None @@ -16,14 +16,14 @@ if not os.getenv('SECRET_KEY'): # Attempt to write the secret file # This will fail if the filesystem is read-only try: - with open('.ctfd_secret_key', 'wb') as secret: + with open(".ctfd_secret_key", "wb") as secret: secret.write(key) secret.flush() except (OSError, IOError): pass -''' SERVER SETTINGS ''' +""" SERVER SETTINGS """ class Config(object): @@ -31,7 +31,7 @@ class Config(object): CTFd Configuration Object """ - ''' + """ === REQUIRED SETTINGS === SECRET_KEY: @@ -61,21 +61,27 @@ class Config(object): REDIS_URL is the URL to connect to a Redis server. e.g. redis://user:password@localhost:6379 http://pythonhosted.org/Flask-Caching/#configuring-flask-caching - ''' - SECRET_KEY = os.getenv('SECRET_KEY') or key - DATABASE_URL = os.getenv('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__))) - REDIS_URL = os.getenv('REDIS_URL') + """ + SECRET_KEY = os.getenv("SECRET_KEY") or key + DATABASE_URL = os.getenv("DATABASE_URL") or "sqlite:///{}/ctfd.db".format( + os.path.dirname(os.path.abspath(__file__)) + ) + REDIS_URL = os.getenv("REDIS_URL") SQLALCHEMY_DATABASE_URI = DATABASE_URL CACHE_REDIS_URL = REDIS_URL if CACHE_REDIS_URL: - CACHE_TYPE = 'redis' + CACHE_TYPE = "redis" else: - CACHE_TYPE = 'filesystem' - CACHE_DIR = os.path.join(os.path.dirname(__file__), os.pardir, '.data', 'filesystem_cache') - CACHE_THRESHOLD = 0 # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing. + CACHE_TYPE = "filesystem" + CACHE_DIR = os.path.join( + os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache" + ) + CACHE_THRESHOLD = ( + 0 + ) # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing. - ''' + """ === SECURITY === SESSION_COOKIE_HTTPONLY: @@ -91,23 +97,25 @@ class Config(object): CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based solely on IP addresses unless you know what you are doing. - ''' - SESSION_COOKIE_HTTPONLY = (not os.getenv("SESSION_COOKIE_HTTPONLY")) # Defaults True - SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or 'Lax' - PERMANENT_SESSION_LIFETIME = int(os.getenv("PERMANENT_SESSION_LIFETIME") or 604800) # 7 days in seconds + """ + SESSION_COOKIE_HTTPONLY = not os.getenv("SESSION_COOKIE_HTTPONLY") # Defaults True + SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax" + PERMANENT_SESSION_LIFETIME = int( + os.getenv("PERMANENT_SESSION_LIFETIME") or 604800 + ) # 7 days in seconds TRUSTED_PROXIES = [ - r'^127\.0\.0\.1$', + r"^127\.0\.0\.1$", # Remove the following proxies if you do not trust the local network # For example if you are running a CTF on your laptop and the teams are # all on the same network - r'^::1$', - r'^fc00:', - r'^10\.', - r'^172\.(1[6-9]|2[0-9]|3[0-1])\.', - r'^192\.168\.' + r"^::1$", + r"^fc00:", + r"^10\.", + r"^172\.(1[6-9]|2[0-9]|3[0-1])\.", + r"^192\.168\.", ] - ''' + """ === EMAIL === MAILFROM_ADDR: @@ -139,7 +147,7 @@ class Config(object): MAILGUN_BASE_URL Mailgun base url to send email over Mailgun - ''' + """ MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io" MAIL_SERVER = os.getenv("MAIL_SERVER") or None MAIL_PORT = os.getenv("MAIL_PORT") @@ -151,15 +159,17 @@ class Config(object): MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY") MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL") - ''' + """ === LOGS === LOG_FOLDER: The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins. The default location is the CTFd/logs folder. - ''' - LOG_FOLDER = os.getenv('LOG_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') + """ + LOG_FOLDER = os.getenv("LOG_FOLDER") or os.path.join( + os.path.dirname(os.path.abspath(__file__)), "logs" + ) - ''' + """ === UPLOADS === UPLOAD_PROVIDER: @@ -180,16 +190,18 @@ class Config(object): AWS_S3_ENDPOINT_URL: A URL pointing to a custom S3 implementation. - ''' - UPLOAD_PROVIDER = os.getenv('UPLOAD_PROVIDER') or 'filesystem' - UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads') - if UPLOAD_PROVIDER == 's3': - AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') - AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') - AWS_S3_BUCKET = os.getenv('AWS_S3_BUCKET') - AWS_S3_ENDPOINT_URL = os.getenv('AWS_S3_ENDPOINT_URL') + """ + UPLOAD_PROVIDER = os.getenv("UPLOAD_PROVIDER") or "filesystem" + UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER") or os.path.join( + os.path.dirname(os.path.abspath(__file__)), "uploads" + ) + if UPLOAD_PROVIDER == "s3": + AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") + AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET") + AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL") - ''' + """ === OPTIONAL === REVERSE_PROXY: @@ -216,33 +228,35 @@ class Config(object): APPLICATION_ROOT: Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory. Example: /ctfd - ''' + """ REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False - TEMPLATES_AUTO_RELOAD = (not os.getenv("TEMPLATES_AUTO_RELOAD")) # Defaults True - SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None # Defaults False - SWAGGER_UI = '/' if os.getenv("SWAGGER_UI") is not None else False # Defaults False - UPDATE_CHECK = (not os.getenv("UPDATE_CHECK")) # Defaults True - APPLICATION_ROOT = os.getenv('APPLICATION_ROOT') or '/' + TEMPLATES_AUTO_RELOAD = not os.getenv("TEMPLATES_AUTO_RELOAD") # Defaults True + SQLALCHEMY_TRACK_MODIFICATIONS = ( + os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None + ) # Defaults False + SWAGGER_UI = "/" if os.getenv("SWAGGER_UI") is not None else False # Defaults False + UPDATE_CHECK = not os.getenv("UPDATE_CHECK") # Defaults True + APPLICATION_ROOT = os.getenv("APPLICATION_ROOT") or "/" - ''' + """ === OAUTH === MajorLeagueCyber Integration Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here - ''' + """ OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID") OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET") class TestingConfig(Config): - SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA' + SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA" PRESERVE_CONTEXT_ON_EXCEPTION = False TESTING = True DEBUG = True - SQLALCHEMY_DATABASE_URI = os.getenv('TESTING_DATABASE_URL') or 'sqlite://' - SERVER_NAME = 'localhost' + SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://" + SERVER_NAME = "localhost" UPDATE_CHECK = False REDIS_URL = None - CACHE_TYPE = 'simple' + CACHE_TYPE = "simple" CACHE_THRESHOLD = 500 SAFE_MODE = True diff --git a/CTFd/errors.py b/CTFd/errors.py index 02d2a97..d6fe386 100644 --- a/CTFd/errors.py +++ b/CTFd/errors.py @@ -3,19 +3,19 @@ from flask import render_template # 404 def page_not_found(error): - return render_template('errors/404.html', error=error.description), 404 + return render_template("errors/404.html", error=error.description), 404 # 403 def forbidden(error): - return render_template('errors/403.html', error=error.description), 403 + return render_template("errors/403.html", error=error.description), 403 # 500 def general_error(error): - return render_template('errors/500.html'), 500 + return render_template("errors/500.html"), 500 # 502 def gateway_error(error): - return render_template('errors/502.html', error=error.description), 502 + return render_template("errors/502.html", error=error.description), 502 diff --git a/CTFd/events/__init__.py b/CTFd/events/__init__.py index 8e8e023..7fad919 100644 --- a/CTFd/events/__init__.py +++ b/CTFd/events/__init__.py @@ -1,7 +1,7 @@ from flask import current_app, Blueprint, Response, stream_with_context from CTFd.utils.decorators import authed_only, ratelimit -events = Blueprint('events', __name__) +events = Blueprint("events", __name__) @events.route("/events") diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index d6819ab..6720226 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -20,29 +20,29 @@ def get_class_by_tablename(tablename): :return: Class reference or None. """ for c in db.Model._decl_class_registry.values(): - if hasattr(c, '__tablename__') and c.__tablename__ == tablename: + if hasattr(c, "__tablename__") and c.__tablename__ == tablename: return c return None class Notifications(db.Model): - __tablename__ = 'notifications' + __tablename__ = "notifications" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text) content = db.Column(db.Text) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) - user_id = db.Column(db.Integer, db.ForeignKey('users.id')) - team_id = db.Column(db.Integer, db.ForeignKey('teams.id')) + user_id = db.Column(db.Integer, db.ForeignKey("users.id")) + team_id = db.Column(db.Integer, db.ForeignKey("teams.id")) - user = db.relationship('Users', foreign_keys="Notifications.user_id", lazy='select') - team = db.relationship('Teams', foreign_keys="Notifications.team_id", lazy='select') + user = db.relationship("Users", foreign_keys="Notifications.user_id", lazy="select") + team = db.relationship("Teams", foreign_keys="Notifications.team_id", lazy="select") def __init__(self, *args, **kwargs): super(Notifications, self).__init__(**kwargs) class Pages(db.Model): - __tablename__ = 'pages' + __tablename__ = "pages" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80)) route = db.Column(db.String(128), unique=True) @@ -62,7 +62,7 @@ class Pages(db.Model): class Challenges(db.Model): - __tablename__ = 'challenges' + __tablename__ = "challenges" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80)) description = db.Column(db.Text) @@ -70,7 +70,7 @@ class Challenges(db.Model): value = db.Column(db.Integer) category = db.Column(db.String(80)) type = db.Column(db.String(80)) - state = db.Column(db.String(80), nullable=False, default='visible') + state = db.Column(db.String(80), nullable=False, default="visible") requirements = db.Column(db.JSON) files = db.relationship("ChallengeFiles", backref="challenge") @@ -78,31 +78,27 @@ class Challenges(db.Model): hints = db.relationship("Hints", backref="challenge") flags = db.relationship("Flags", backref="challenge") - __mapper_args__ = { - 'polymorphic_identity': 'standard', - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} def __init__(self, *args, **kwargs): super(Challenges, self).__init__(**kwargs) def __repr__(self): - return '' % self.name + return "" % self.name class Hints(db.Model): - __tablename__ = 'hints' + __tablename__ = "hints" id = db.Column(db.Integer, primary_key=True) - type = db.Column(db.String(80), default='standard') - challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) + type = db.Column(db.String(80), default="standard") + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) content = db.Column(db.Text) cost = db.Column(db.Integer, default=0) requirements = db.Column(db.JSON) - __mapper_args__ = { - 'polymorphic_identity': 'standard', - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} @property def name(self): @@ -120,15 +116,15 @@ class Hints(db.Model): super(Hints, self).__init__(**kwargs) def __repr__(self): - return '' % self.content + return "" % self.content class Awards(db.Model): - __tablename__ = 'awards' + __tablename__ = "awards" id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) - team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')) - type = db.Column(db.String(80), default='standard') + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")) + type = db.Column(db.String(80), default="standard") name = db.Column(db.String(80)) description = db.Column(db.Text) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) @@ -137,33 +133,32 @@ class Awards(db.Model): icon = db.Column(db.Text) requirements = db.Column(db.JSON) - user = db.relationship('Users', foreign_keys="Awards.user_id", lazy='select') - team = db.relationship('Teams', foreign_keys="Awards.team_id", lazy='select') + user = db.relationship("Users", foreign_keys="Awards.user_id", lazy="select") + team = db.relationship("Teams", foreign_keys="Awards.team_id", lazy="select") - __mapper_args__ = { - 'polymorphic_identity': 'standard', - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} @hybrid_property def account_id(self): - user_mode = get_config('user_mode') - if user_mode == 'teams': + user_mode = get_config("user_mode") + if user_mode == "teams": return self.team_id - elif user_mode == 'users': + elif user_mode == "users": return self.user_id def __init__(self, *args, **kwargs): super(Awards, self).__init__(**kwargs) def __repr__(self): - return '' % self.name + return "" % self.name class Tags(db.Model): - __tablename__ = 'tags' + __tablename__ = "tags" id = db.Column(db.Integer, primary_key=True) - challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) value = db.Column(db.String(80)) def __init__(self, *args, **kwargs): @@ -171,54 +166,51 @@ class Tags(db.Model): class Files(db.Model): - __tablename__ = 'files' + __tablename__ = "files" id = db.Column(db.Integer, primary_key=True) - type = db.Column(db.String(80), default='standard') + type = db.Column(db.String(80), default="standard") location = db.Column(db.Text) - __mapper_args__ = { - 'polymorphic_identity': 'standard', - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} def __init__(self, *args, **kwargs): super(Files, self).__init__(**kwargs) def __repr__(self): - return "".format(type=self.type, location=self.location) + return "".format( + type=self.type, location=self.location + ) class ChallengeFiles(Files): - __mapper_args__ = { - 'polymorphic_identity': 'challenge' - } - challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) + __mapper_args__ = {"polymorphic_identity": "challenge"} + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) def __init__(self, *args, **kwargs): super(ChallengeFiles, self).__init__(**kwargs) class PageFiles(Files): - __mapper_args__ = { - 'polymorphic_identity': 'page' - } - page_id = db.Column(db.Integer, db.ForeignKey('pages.id')) + __mapper_args__ = {"polymorphic_identity": "page"} + page_id = db.Column(db.Integer, db.ForeignKey("pages.id")) def __init__(self, *args, **kwargs): super(PageFiles, self).__init__(**kwargs) class Flags(db.Model): - __tablename__ = 'flags' + __tablename__ = "flags" id = db.Column(db.Integer, primary_key=True) - challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) type = db.Column(db.String(80)) content = db.Column(db.Text) data = db.Column(db.Text) - __mapper_args__ = { - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_on": type} def __init__(self, *args, **kwargs): super(Flags, self).__init__(**kwargs) @@ -228,11 +220,8 @@ class Flags(db.Model): class Users(db.Model): - __tablename__ = 'users' - __table_args__ = ( - db.UniqueConstraint('id', 'oauth_id'), - {} - ) + __tablename__ = "users" + __table_args__ = (db.UniqueConstraint("id", "oauth_id"), {}) # Core attributes id = db.Column(db.Integer, primary_key=True) oauth_id = db.Column(db.Integer, unique=True) @@ -253,28 +242,25 @@ class Users(db.Model): verified = db.Column(db.Boolean, default=False) # Relationship for Teams - team_id = db.Column(db.Integer, db.ForeignKey('teams.id')) + team_id = db.Column(db.Integer, db.ForeignKey("teams.id")) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) - __mapper_args__ = { - 'polymorphic_identity': 'user', - 'polymorphic_on': type - } + __mapper_args__ = {"polymorphic_identity": "user", "polymorphic_on": type} def __init__(self, **kwargs): super(Users, self).__init__(**kwargs) - @validates('password') + @validates("password") def validate_password(self, key, plaintext): return hash_password(str(plaintext)) @hybrid_property def account_id(self): - user_mode = get_config('user_mode') - if user_mode == 'teams': + user_mode = get_config("user_mode") + if user_mode == "teams": return self.team_id - elif user_mode == 'users': + elif user_mode == "users": return self.id @property @@ -299,7 +285,7 @@ class Users(db.Model): def get_solves(self, admin=False): solves = Solves.query.filter_by(user_id=self.id) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) solves = solves.filter(Solves.date < dt) @@ -307,7 +293,7 @@ class Users(db.Model): def get_fails(self, admin=False): fails = Fails.query.filter_by(user_id=self.id) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) fails = fails.filter(Fails.date < dt) @@ -315,27 +301,26 @@ class Users(db.Model): def get_awards(self, admin=False): awards = Awards.query.filter_by(user_id=self.id) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) awards = awards.filter(Awards.date < dt) return awards.all() def get_score(self, admin=False): - score = db.func.sum(Challenges.value).label('score') - user = db.session.query( - Solves.user_id, - score - ) \ - .join(Users, Solves.user_id == Users.id) \ - .join(Challenges, Solves.challenge_id == Challenges.id) \ + score = db.func.sum(Challenges.value).label("score") + user = ( + db.session.query(Solves.user_id, score) + .join(Users, Solves.user_id == Users.id) + .join(Challenges, Solves.challenge_id == Challenges.id) .filter(Users.id == self.id) + ) - award_score = db.func.sum(Awards.value).label('award_score') + award_score = db.func.sum(Awards.value).label("award_score") award = db.session.query(award_score).filter_by(user_id=self.id) if not admin: - freeze = Configs.query.filter_by(key='freeze').first() + freeze = Configs.query.filter_by(key="freeze").first() if freeze and freeze.value: freeze = int(freeze.value) freeze = datetime.datetime.utcfromtimestamp(freeze) @@ -361,50 +346,63 @@ class Users(db.Model): to no imports within the CTFd application as importing from the application itself will result in a circular import. """ - scores = db.session.query( - Solves.user_id.label('user_id'), - db.func.sum(Challenges.value).label('score'), - db.func.max(Solves.id).label('id'), - db.func.max(Solves.date).label('date') - ).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id) + scores = ( + db.session.query( + Solves.user_id.label("user_id"), + db.func.sum(Challenges.value).label("score"), + db.func.max(Solves.id).label("id"), + db.func.max(Solves.date).label("date"), + ) + .join(Challenges) + .filter(Challenges.value != 0) + .group_by(Solves.user_id) + ) - awards = db.session.query( - Awards.user_id.label('user_id'), - db.func.sum(Awards.value).label('score'), - db.func.max(Awards.id).label('id'), - db.func.max(Awards.date).label('date') - ).filter(Awards.value != 0).group_by(Awards.user_id) + awards = ( + db.session.query( + Awards.user_id.label("user_id"), + db.func.sum(Awards.value).label("score"), + db.func.max(Awards.id).label("id"), + db.func.max(Awards.date).label("date"), + ) + .filter(Awards.value != 0) + .group_by(Awards.user_id) + ) if not admin: - freeze = Configs.query.filter_by(key='freeze').first() + freeze = Configs.query.filter_by(key="freeze").first() if freeze and freeze.value: freeze = int(freeze.value) freeze = datetime.datetime.utcfromtimestamp(freeze) scores = scores.filter(Solves.date < freeze) awards = awards.filter(Awards.date < freeze) - results = union_all(scores, awards).alias('results') + results = union_all(scores, awards).alias("results") - sumscores = db.session.query( - results.columns.user_id, - db.func.sum(results.columns.score).label('score'), - db.func.max(results.columns.id).label('id'), - db.func.max(results.columns.date).label('date') - ).group_by(results.columns.user_id).subquery() + sumscores = ( + db.session.query( + results.columns.user_id, + db.func.sum(results.columns.score).label("score"), + db.func.max(results.columns.id).label("id"), + db.func.max(results.columns.date).label("date"), + ) + .group_by(results.columns.user_id) + .subquery() + ) if admin: - standings_query = db.session.query( - Users.id.label('user_id'), - ) \ - .join(sumscores, Users.id == sumscores.columns.user_id) \ + standings_query = ( + db.session.query(Users.id.label("user_id")) + .join(sumscores, Users.id == sumscores.columns.user_id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id) + ) else: - standings_query = db.session.query( - Users.id.label('user_id'), - ) \ - .join(sumscores, Users.id == sumscores.columns.user_id) \ - .filter(Users.banned == False, Users.hidden == False) \ + standings_query = ( + db.session.query(Users.id.label("user_id")) + .join(sumscores, Users.id == sumscores.columns.user_id) + .filter(Users.banned == False, Users.hidden == False) .order_by(sumscores.columns.score.desc(), sumscores.columns.id) + ) standings = standings_query.all() @@ -415,24 +413,19 @@ class Users(db.Model): return i else: k = i % 10 - return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4]) + return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k :: 4]) except ValueError: return 0 class Admins(Users): - __tablename__ = 'admins' - __mapper_args__ = { - 'polymorphic_identity': 'admin' - } + __tablename__ = "admins" + __mapper_args__ = {"polymorphic_identity": "admin"} class Teams(db.Model): - __tablename__ = 'teams' - __table_args__ = ( - db.UniqueConstraint('id', 'oauth_id'), - {} - ) + __tablename__ = "teams" + __table_args__ = (db.UniqueConstraint("id", "oauth_id"), {}) # Core attributes id = db.Column(db.Integer, primary_key=True) oauth_id = db.Column(db.Integer, unique=True) @@ -442,7 +435,7 @@ class Teams(db.Model): password = db.Column(db.String(128)) secret = db.Column(db.String(128)) - members = db.relationship("Users", backref="team", foreign_keys='Users.team_id') + members = db.relationship("Users", backref="team", foreign_keys="Users.team_id") # Supplementary attributes website = db.Column(db.String(128)) @@ -453,7 +446,7 @@ class Teams(db.Model): banned = db.Column(db.Boolean, default=False) # Relationship for Users - captain_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL')) + captain_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) captain = db.relationship("Users", foreign_keys=[captain_id]) created = db.Column(db.DateTime, default=datetime.datetime.utcnow) @@ -461,7 +454,7 @@ class Teams(db.Model): def __init__(self, **kwargs): super(Teams, self).__init__(**kwargs) - @validates('password') + @validates("password") def validate_password(self, key, plaintext): return hash_password(str(plaintext)) @@ -488,13 +481,11 @@ class Teams(db.Model): def get_solves(self, admin=False): member_ids = [member.id for member in self.members] - solves = Solves.query.filter( - Solves.user_id.in_(member_ids) - ).order_by( + solves = Solves.query.filter(Solves.user_id.in_(member_ids)).order_by( Solves.date.asc() ) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) solves = solves.filter(Solves.date < dt) @@ -504,13 +495,11 @@ class Teams(db.Model): def get_fails(self, admin=False): member_ids = [member.id for member in self.members] - fails = Fails.query.filter( - Fails.user_id.in_(member_ids) - ).order_by( + fails = Fails.query.filter(Fails.user_id.in_(member_ids)).order_by( Fails.date.asc() ) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) fails = fails.filter(Fails.date < dt) @@ -520,13 +509,11 @@ class Teams(db.Model): def get_awards(self, admin=False): member_ids = [member.id for member in self.members] - awards = Awards.query.filter( - Awards.user_id.in_(member_ids) - ).order_by( + awards = Awards.query.filter(Awards.user_id.in_(member_ids)).order_by( Awards.date.asc() ) - freeze = get_config('freeze') + freeze = get_config("freeze") if freeze and admin is False: dt = datetime.datetime.utcfromtimestamp(freeze) awards = awards.filter(Awards.date < dt) @@ -546,50 +533,63 @@ class Teams(db.Model): to no imports within the CTFd application as importing from the application itself will result in a circular import. """ - scores = db.session.query( - Solves.team_id.label('team_id'), - db.func.sum(Challenges.value).label('score'), - db.func.max(Solves.id).label('id'), - db.func.max(Solves.date).label('date') - ).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id) + scores = ( + db.session.query( + Solves.team_id.label("team_id"), + db.func.sum(Challenges.value).label("score"), + db.func.max(Solves.id).label("id"), + db.func.max(Solves.date).label("date"), + ) + .join(Challenges) + .filter(Challenges.value != 0) + .group_by(Solves.team_id) + ) - awards = db.session.query( - Awards.team_id.label('team_id'), - db.func.sum(Awards.value).label('score'), - db.func.max(Awards.id).label('id'), - db.func.max(Awards.date).label('date') - ).filter(Awards.value != 0).group_by(Awards.team_id) + awards = ( + db.session.query( + Awards.team_id.label("team_id"), + db.func.sum(Awards.value).label("score"), + db.func.max(Awards.id).label("id"), + db.func.max(Awards.date).label("date"), + ) + .filter(Awards.value != 0) + .group_by(Awards.team_id) + ) if not admin: - freeze = Configs.query.filter_by(key='freeze').first() + freeze = Configs.query.filter_by(key="freeze").first() if freeze and freeze.value: freeze = int(freeze.value) freeze = datetime.datetime.utcfromtimestamp(freeze) scores = scores.filter(Solves.date < freeze) awards = awards.filter(Awards.date < freeze) - results = union_all(scores, awards).alias('results') + results = union_all(scores, awards).alias("results") - sumscores = db.session.query( - results.columns.team_id, - db.func.sum(results.columns.score).label('score'), - db.func.max(results.columns.id).label('id'), - db.func.max(results.columns.date).label('date') - ).group_by(results.columns.team_id).subquery() + sumscores = ( + db.session.query( + results.columns.team_id, + db.func.sum(results.columns.score).label("score"), + db.func.max(results.columns.id).label("id"), + db.func.max(results.columns.date).label("date"), + ) + .group_by(results.columns.team_id) + .subquery() + ) if admin: - standings_query = db.session.query( - Teams.id.label('team_id'), - ) \ - .join(sumscores, Teams.id == sumscores.columns.team_id) \ + standings_query = ( + db.session.query(Teams.id.label("team_id")) + .join(sumscores, Teams.id == sumscores.columns.team_id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id) + ) else: - standings_query = db.session.query( - Teams.id.label('team_id'), - ) \ - .join(sumscores, Teams.id == sumscores.columns.team_id) \ - .filter(Teams.banned == False) \ + standings_query = ( + db.session.query(Teams.id.label("team_id")) + .join(sumscores, Teams.id == sumscores.columns.team_id) + .filter(Teams.banned == False) .order_by(sumscores.columns.score.desc(), sumscores.columns.id) + ) standings = standings_query.all() @@ -597,45 +597,47 @@ class Teams(db.Model): try: i = standings.index((self.id,)) + 1 k = i % 10 - return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4]) + return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k :: 4]) except ValueError: return 0 class Submissions(db.Model): - __tablename__ = 'submissions' + __tablename__ = "submissions" id = db.Column(db.Integer, primary_key=True) - challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) - user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) - team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')) + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")) ip = db.Column(db.String(46)) provided = db.Column(db.Text) type = db.Column(db.String(32)) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) # Relationships - user = db.relationship('Users', foreign_keys="Submissions.user_id", lazy='select') - team = db.relationship('Teams', foreign_keys="Submissions.team_id", lazy='select') - challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select') + user = db.relationship("Users", foreign_keys="Submissions.user_id", lazy="select") + team = db.relationship("Teams", foreign_keys="Submissions.team_id", lazy="select") + challenge = db.relationship( + "Challenges", foreign_keys="Submissions.challenge_id", lazy="select" + ) - __mapper_args__ = { - 'polymorphic_on': type, - } + __mapper_args__ = {"polymorphic_on": type} @hybrid_property def account_id(self): - user_mode = get_config('user_mode') - if user_mode == 'teams': + user_mode = get_config("user_mode") + if user_mode == "teams": return self.team_id - elif user_mode == 'users': + elif user_mode == "users": return self.user_id @hybrid_property def account(self): - user_mode = get_config('user_mode') - if user_mode == 'teams': + user_mode = get_config("user_mode") + if user_mode == "teams": return self.team - elif user_mode == 'users': + elif user_mode == "users": return self.user @staticmethod @@ -647,91 +649,95 @@ class Submissions(db.Model): return child_classes[type] def __repr__(self): - return ''.format(self.team_id, self.challenge_id, self.ip, self.provided) + return "".format( + self.team_id, self.challenge_id, self.ip, self.provided + ) class Solves(Submissions): - __tablename__ = 'solves' + __tablename__ = "solves" __table_args__ = ( - db.UniqueConstraint('challenge_id', 'user_id'), - db.UniqueConstraint('challenge_id', 'team_id'), - {} + db.UniqueConstraint("challenge_id", "user_id"), + db.UniqueConstraint("challenge_id", "team_id"), + {}, + ) + id = db.Column( + None, db.ForeignKey("submissions.id", ondelete="CASCADE"), primary_key=True + ) + challenge_id = column_property( + db.Column(db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")), + Submissions.challenge_id, + ) + user_id = column_property( + db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")), + Submissions.user_id, + ) + team_id = column_property( + db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")), + Submissions.team_id, ) - id = db.Column(None, db.ForeignKey('submissions.id', ondelete='CASCADE'), primary_key=True) - challenge_id = column_property(db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')), - Submissions.challenge_id) - user_id = column_property(db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')), Submissions.user_id) - team_id = column_property(db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')), Submissions.team_id) - user = db.relationship('Users', foreign_keys="Solves.user_id", lazy='select') - team = db.relationship('Teams', foreign_keys="Solves.team_id", lazy='select') - challenge = db.relationship('Challenges', foreign_keys="Solves.challenge_id", lazy='select') + user = db.relationship("Users", foreign_keys="Solves.user_id", lazy="select") + team = db.relationship("Teams", foreign_keys="Solves.team_id", lazy="select") + challenge = db.relationship( + "Challenges", foreign_keys="Solves.challenge_id", lazy="select" + ) - __mapper_args__ = { - 'polymorphic_identity': 'correct' - } + __mapper_args__ = {"polymorphic_identity": "correct"} class Fails(Submissions): - __mapper_args__ = { - 'polymorphic_identity': 'incorrect' - } + __mapper_args__ = {"polymorphic_identity": "incorrect"} class Unlocks(db.Model): - __tablename__ = 'unlocks' + __tablename__ = "unlocks" id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) - team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")) target = db.Column(db.Integer) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) type = db.Column(db.String(32)) - __mapper_args__ = { - 'polymorphic_on': type, - } + __mapper_args__ = {"polymorphic_on": type} @hybrid_property def account_id(self): - user_mode = get_config('user_mode') - if user_mode == 'teams': + user_mode = get_config("user_mode") + if user_mode == "teams": return self.team_id - elif user_mode == 'users': + elif user_mode == "users": return self.user_id def __repr__(self): - return '' % self.id + return "" % self.id class HintUnlocks(Unlocks): - __mapper_args__ = { - 'polymorphic_identity': 'hints' - } + __mapper_args__ = {"polymorphic_identity": "hints"} class Tracking(db.Model): - __tablename__ = 'tracking' + __tablename__ = "tracking" id = db.Column(db.Integer, primary_key=True) type = db.Column(db.String(32)) ip = db.Column(db.String(46)) - user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) - user = db.relationship('Users', foreign_keys="Tracking.user_id", lazy='select') + user = db.relationship("Users", foreign_keys="Tracking.user_id", lazy="select") - __mapper_args__ = { - 'polymorphic_on': type, - } + __mapper_args__ = {"polymorphic_on": type} def __init__(self, *args, **kwargs): super(Tracking, self).__init__(**kwargs) def __repr__(self): - return '' % self.ip + return "" % self.ip class Configs(db.Model): - __tablename__ = 'config' + __tablename__ = "config" id = db.Column(db.Integer, primary_key=True) key = db.Column(db.Text) value = db.Column(db.Text) @@ -751,9 +757,9 @@ def get_config(key): if value and value.isdigit(): return int(value) elif value and isinstance(value, six.string_types): - if value.lower() == 'true': + if value.lower() == "true": return True - elif value.lower() == 'false': + elif value.lower() == "false": return False else: return value diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py index 90dd842..2ebb278 100644 --- a/CTFd/plugins/__init__.py +++ b/CTFd/plugins/__init__.py @@ -10,12 +10,12 @@ from CTFd.utils.plugins import ( register_script as utils_register_plugin_script, register_stylesheet as utils_register_plugin_stylesheet, register_admin_script as utils_register_admin_plugin_script, - register_admin_stylesheet as utils_register_admin_plugin_stylesheet + register_admin_stylesheet as utils_register_admin_plugin_stylesheet, ) from CTFd.utils.config.pages import get_pages -Menu = namedtuple('Menu', ['title', 'route']) +Menu = namedtuple("Menu", ["title", "route"]) def register_plugin_assets_directory(app, base_path, admins_only=False): @@ -27,12 +27,12 @@ def register_plugin_assets_directory(app, base_path, admins_only=False): :param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public :return: """ - base_path = base_path.strip('/') + base_path = base_path.strip("/") def assets_handler(path): return send_from_directory(base_path, path) - rule = '/' + base_path + '/' + rule = "/" + base_path + "/" app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler) @@ -45,14 +45,14 @@ def register_plugin_asset(app, asset_path, admins_only=False): :param boolean admins_only: Whether or not this file should be accessible to the public :return: """ - asset_path = asset_path.strip('/') + asset_path = asset_path.strip("/") def asset_handler(): return send_file(asset_path) if admins_only: asset_handler = admins_only_wrapper(asset_handler) - rule = '/' + asset_path + rule = "/" + asset_path app.add_url_rule(rule=rule, endpoint=asset_path, view_func=asset_handler) @@ -170,14 +170,14 @@ def init_plugins(app): app.admin_plugin_menu_bar = [] app.plugin_menu_bar = [] - if app.config.get('SAFE_MODE', False) is False: + if app.config.get("SAFE_MODE", False) is False: modules = sorted(glob.glob(os.path.dirname(__file__) + "/*")) - blacklist = {'__pycache__'} + blacklist = {"__pycache__"} for module in modules: module_name = os.path.basename(module) if os.path.isdir(module) and module_name not in blacklist: - module = '.' + module_name - module = importlib.import_module(module, package='CTFd.plugins') + module = "." + module_name + module = importlib.import_module(module, package="CTFd.plugins") module.load(app) print(" * Loaded module, %s" % module) diff --git a/CTFd/plugins/challenges/__init__.py b/CTFd/plugins/challenges/__init__.py index be019cf..7dea80e 100644 --- a/CTFd/plugins/challenges/__init__.py +++ b/CTFd/plugins/challenges/__init__.py @@ -1,6 +1,15 @@ from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins.flags import get_flag_class -from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints +from CTFd.models import ( + db, + Solves, + Fails, + Flags, + Challenges, + ChallengeFiles, + Tags, + Hints, +) from CTFd.utils.user import get_ip from CTFd.utils.uploads import delete_file from flask import Blueprint @@ -17,19 +26,21 @@ class CTFdStandardChallenge(BaseChallenge): id = "standard" # Unique identifier used to register challenges name = "standard" # Name of a challenge type templates = { # Templates used for each aspect of challenge editing & viewing - 'create': '/plugins/challenges/assets/create.html', - 'update': '/plugins/challenges/assets/update.html', - 'view': '/plugins/challenges/assets/view.html', + "create": "/plugins/challenges/assets/create.html", + "update": "/plugins/challenges/assets/update.html", + "view": "/plugins/challenges/assets/view.html", } scripts = { # Scripts that are loaded when a template is loaded - 'create': '/plugins/challenges/assets/create.js', - 'update': '/plugins/challenges/assets/update.js', - 'view': '/plugins/challenges/assets/view.js', + "create": "/plugins/challenges/assets/create.js", + "update": "/plugins/challenges/assets/update.js", + "view": "/plugins/challenges/assets/view.js", } # Route at which files are accessible. This must be registered using register_plugin_assets_directory() - route = '/plugins/challenges/assets/' + route = "/plugins/challenges/assets/" # Blueprint used to access the static_folder directory. - blueprint = Blueprint('standard', __name__, template_folder='templates', static_folder='assets') + blueprint = Blueprint( + "standard", __name__, template_folder="templates", static_folder="assets" + ) @staticmethod def create(request): @@ -57,20 +68,20 @@ class CTFdStandardChallenge(BaseChallenge): :return: Challenge object, data dictionary to be returned to the user """ data = { - 'id': challenge.id, - 'name': challenge.name, - 'value': challenge.value, - 'description': challenge.description, - 'category': challenge.category, - 'state': challenge.state, - 'max_attempts': challenge.max_attempts, - 'type': challenge.type, - 'type_data': { - 'id': CTFdStandardChallenge.id, - 'name': CTFdStandardChallenge.name, - 'templates': CTFdStandardChallenge.templates, - 'scripts': CTFdStandardChallenge.scripts, - } + "id": challenge.id, + "name": challenge.name, + "value": challenge.value, + "description": challenge.description, + "category": challenge.category, + "state": challenge.state, + "max_attempts": challenge.max_attempts, + "type": challenge.type, + "type_data": { + "id": CTFdStandardChallenge.id, + "name": CTFdStandardChallenge.name, + "templates": CTFdStandardChallenge.templates, + "scripts": CTFdStandardChallenge.scripts, + }, } return data @@ -123,12 +134,12 @@ class CTFdStandardChallenge(BaseChallenge): :return: (boolean, string) """ data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() flags = Flags.query.filter_by(challenge_id=challenge.id).all() for flag in flags: if get_flag_class(flag.type).compare(flag, submission): - return True, 'Correct' - return False, 'Incorrect' + return True, "Correct" + return False, "Incorrect" @staticmethod def solve(user, team, challenge, request): @@ -141,13 +152,13 @@ class CTFdStandardChallenge(BaseChallenge): :return: """ data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() solve = Solves( user_id=user.id, team_id=team.id if team else None, challenge_id=challenge.id, ip=get_ip(req=request), - provided=submission + provided=submission, ) db.session.add(solve) db.session.commit() @@ -164,13 +175,13 @@ class CTFdStandardChallenge(BaseChallenge): :return: """ data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() wrong = Fails( user_id=user.id, team_id=team.id if team else None, challenge_id=challenge.id, ip=get_ip(request), - provided=submission + provided=submission, ) db.session.add(wrong) db.session.commit() @@ -194,10 +205,8 @@ def get_chal_class(class_id): Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register your Challenge Type. """ -CHALLENGE_CLASSES = { - "standard": CTFdStandardChallenge -} +CHALLENGE_CLASSES = {"standard": CTFdStandardChallenge} def load(app): - register_plugin_assets_directory(app, base_path='/plugins/challenges/assets/') + register_plugin_assets_directory(app, base_path="/plugins/challenges/assets/") diff --git a/CTFd/plugins/dynamic_challenges/__init__.py b/CTFd/plugins/dynamic_challenges/__init__.py index fc66969..7f594db 100644 --- a/CTFd/plugins/dynamic_challenges/__init__.py +++ b/CTFd/plugins/dynamic_challenges/__init__.py @@ -2,7 +2,16 @@ from __future__ import division # Use floating point for math calculations from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins.flags import get_flag_class -from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints +from CTFd.models import ( + db, + Solves, + Fails, + Flags, + Challenges, + ChallengeFiles, + Tags, + Hints, +) from CTFd.utils.user import get_ip from CTFd.utils.uploads import delete_file from CTFd.utils.modes import get_model @@ -14,19 +23,24 @@ class DynamicValueChallenge(BaseChallenge): id = "dynamic" # Unique identifier used to register challenges name = "dynamic" # Name of a challenge type templates = { # Handlebars templates used for each aspect of challenge editing & viewing - 'create': '/plugins/dynamic_challenges/assets/create.html', - 'update': '/plugins/dynamic_challenges/assets/update.html', - 'view': '/plugins/dynamic_challenges/assets/view.html', + "create": "/plugins/dynamic_challenges/assets/create.html", + "update": "/plugins/dynamic_challenges/assets/update.html", + "view": "/plugins/dynamic_challenges/assets/view.html", } scripts = { # Scripts that are loaded when a template is loaded - 'create': '/plugins/dynamic_challenges/assets/create.js', - 'update': '/plugins/dynamic_challenges/assets/update.js', - 'view': '/plugins/dynamic_challenges/assets/view.js', + "create": "/plugins/dynamic_challenges/assets/create.js", + "update": "/plugins/dynamic_challenges/assets/update.js", + "view": "/plugins/dynamic_challenges/assets/view.js", } # Route at which files are accessible. This must be registered using register_plugin_assets_directory() - route = '/plugins/dynamic_challenges/assets/' + route = "/plugins/dynamic_challenges/assets/" # Blueprint used to access the static_folder directory. - blueprint = Blueprint('dynamic_challenges', __name__, template_folder='templates', static_folder='assets') + blueprint = Blueprint( + "dynamic_challenges", + __name__, + template_folder="templates", + static_folder="assets", + ) @staticmethod def create(request): @@ -54,23 +68,23 @@ class DynamicValueChallenge(BaseChallenge): """ challenge = DynamicChallenge.query.filter_by(id=challenge.id).first() data = { - 'id': challenge.id, - 'name': challenge.name, - 'value': challenge.value, - 'initial': challenge.initial, - 'decay': challenge.decay, - 'minimum': challenge.minimum, - 'description': challenge.description, - 'category': challenge.category, - 'state': challenge.state, - 'max_attempts': challenge.max_attempts, - 'type': challenge.type, - 'type_data': { - 'id': DynamicValueChallenge.id, - 'name': DynamicValueChallenge.name, - 'templates': DynamicValueChallenge.templates, - 'scripts': DynamicValueChallenge.scripts, - } + "id": challenge.id, + "name": challenge.name, + "value": challenge.value, + "initial": challenge.initial, + "decay": challenge.decay, + "minimum": challenge.minimum, + "description": challenge.description, + "category": challenge.category, + "state": challenge.state, + "max_attempts": challenge.max_attempts, + "type": challenge.type, + "type_data": { + "id": DynamicValueChallenge.id, + "name": DynamicValueChallenge.name, + "templates": DynamicValueChallenge.templates, + "scripts": DynamicValueChallenge.scripts, + }, } return data @@ -88,20 +102,28 @@ class DynamicValueChallenge(BaseChallenge): for attr, value in data.items(): # We need to set these to floats so that the next operations don't operate on strings - if attr in ('initial', 'minimum', 'decay'): + if attr in ("initial", "minimum", "decay"): value = float(value) setattr(challenge, attr, value) Model = get_model() - solve_count = Solves.query \ - .join(Model, Solves.account_id == Model.id) \ - .filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \ + solve_count = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter( + Solves.challenge_id == challenge.id, + Model.hidden == False, + Model.banned == False, + ) .count() + ) # It is important that this calculation takes into account floats. # Hence this file uses from __future__ import division - value = (((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) * (solve_count ** 2)) + challenge.initial + value = ( + ((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) + * (solve_count ** 2) + ) + challenge.initial value = math.ceil(value) @@ -146,12 +168,12 @@ class DynamicValueChallenge(BaseChallenge): :return: (boolean, string) """ data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() flags = Flags.query.filter_by(challenge_id=challenge.id).all() for flag in flags: if get_flag_class(flag.type).compare(flag, submission): - return True, 'Correct' - return False, 'Incorrect' + return True, "Correct" + return False, "Incorrect" @staticmethod def solve(user, team, challenge, request): @@ -165,7 +187,7 @@ class DynamicValueChallenge(BaseChallenge): """ chal = DynamicChallenge.query.filter_by(id=challenge.id).first() data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() Model = get_model() @@ -174,14 +196,19 @@ class DynamicValueChallenge(BaseChallenge): team_id=team.id if team else None, challenge_id=challenge.id, ip=get_ip(req=request), - provided=submission + provided=submission, ) db.session.add(solve) - solve_count = Solves.query \ - .join(Model, Solves.account_id == Model.id) \ - .filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \ + solve_count = ( + Solves.query.join(Model, Solves.account_id == Model.id) + .filter( + Solves.challenge_id == challenge.id, + Model.hidden == False, + Model.banned == False, + ) .count() + ) # We subtract -1 to allow the first solver to get max point value solve_count -= 1 @@ -189,9 +216,7 @@ class DynamicValueChallenge(BaseChallenge): # It is important that this calculation takes into account floats. # Hence this file uses from __future__ import division value = ( - ( - (chal.minimum - chal.initial) / (chal.decay**2) - ) * (solve_count**2) + ((chal.minimum - chal.initial) / (chal.decay ** 2)) * (solve_count ** 2) ) + chal.initial value = math.ceil(value) @@ -215,13 +240,13 @@ class DynamicValueChallenge(BaseChallenge): :return: """ data = request.form or request.get_json() - submission = data['submission'].strip() + submission = data["submission"].strip() wrong = Fails( user_id=user.id, team_id=team.id if team else None, challenge_id=challenge.id, ip=get_ip(request), - provided=submission + provided=submission, ) db.session.add(wrong) db.session.commit() @@ -229,19 +254,21 @@ class DynamicValueChallenge(BaseChallenge): class DynamicChallenge(Challenges): - __mapper_args__ = {'polymorphic_identity': 'dynamic'} - id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True) + __mapper_args__ = {"polymorphic_identity": "dynamic"} + id = db.Column(None, db.ForeignKey("challenges.id"), primary_key=True) initial = db.Column(db.Integer, default=0) minimum = db.Column(db.Integer, default=0) decay = db.Column(db.Integer, default=0) def __init__(self, *args, **kwargs): super(DynamicChallenge, self).__init__(**kwargs) - self.initial = kwargs['value'] + self.initial = kwargs["value"] def load(app): # upgrade() app.db.create_all() - CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge - register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/') + CHALLENGE_CLASSES["dynamic"] = DynamicValueChallenge + register_plugin_assets_directory( + app, base_path="/plugins/dynamic_challenges/assets/" + ) diff --git a/CTFd/plugins/flags/__init__.py b/CTFd/plugins/flags/__init__.py index 18bbe46..5847693 100644 --- a/CTFd/plugins/flags/__init__.py +++ b/CTFd/plugins/flags/__init__.py @@ -15,8 +15,8 @@ class BaseFlag(object): class CTFdStaticFlag(BaseFlag): name = "static" templates = { # Nunjucks templates used for key editing & viewing - 'create': '/plugins/flags/assets/static/create.html', - 'update': '/plugins/flags/assets/static/edit.html', + "create": "/plugins/flags/assets/static/create.html", + "update": "/plugins/flags/assets/static/edit.html", } @staticmethod @@ -40,8 +40,8 @@ class CTFdStaticFlag(BaseFlag): class CTFdRegexFlag(BaseFlag): name = "regex" templates = { # Nunjucks templates used for key editing & viewing - 'create': '/plugins/flags/assets/regex/create.html', - 'update': '/plugins/flags/assets/regex/edit.html', + "create": "/plugins/flags/assets/regex/create.html", + "update": "/plugins/flags/assets/regex/edit.html", } @staticmethod @@ -57,10 +57,7 @@ class CTFdRegexFlag(BaseFlag): return res and res.group() == provided -FLAG_CLASSES = { - 'static': CTFdStaticFlag, - 'regex': CTFdRegexFlag -} +FLAG_CLASSES = {"static": CTFdStaticFlag, "regex": CTFdRegexFlag} def get_flag_class(class_id): @@ -71,4 +68,4 @@ def get_flag_class(class_id): def load(app): - register_plugin_assets_directory(app, base_path='/plugins/flags/assets/') + register_plugin_assets_directory(app, base_path="/plugins/flags/assets/") diff --git a/CTFd/schemas/awards.py b/CTFd/schemas/awards.py index a73d958..4cfccc4 100644 --- a/CTFd/schemas/awards.py +++ b/CTFd/schemas/awards.py @@ -6,43 +6,43 @@ class AwardSchema(ma.ModelSchema): class Meta: model = Awards include_fk = True - dump_only = ('id', 'date') + dump_only = ("id", "date") views = { - 'admin': [ - 'category', - 'user_id', - 'name', - 'description', - 'value', - 'team_id', - 'user', - 'team', - 'date', - 'requirements', - 'id', - 'icon' + "admin": [ + "category", + "user_id", + "name", + "description", + "value", + "team_id", + "user", + "team", + "date", + "requirements", + "id", + "icon", + ], + "user": [ + "category", + "user_id", + "name", + "description", + "value", + "team_id", + "user", + "team", + "date", + "id", + "icon", ], - 'user': [ - 'category', - 'user_id', - 'name', - 'description', - 'value', - 'team_id', - 'user', - 'team', - 'date', - 'id', - 'icon' - ] } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(AwardSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/challenges.py b/CTFd/schemas/challenges.py index 42a4c46..24d8bb4 100644 --- a/CTFd/schemas/challenges.py +++ b/CTFd/schemas/challenges.py @@ -5,4 +5,4 @@ class ChallengeSchema(ma.ModelSchema): class Meta: model = Challenges include_fk = True - dump_only = ('id',) + dump_only = ("id",) diff --git a/CTFd/schemas/config.py b/CTFd/schemas/config.py index 2950117..138303f 100644 --- a/CTFd/schemas/config.py +++ b/CTFd/schemas/config.py @@ -6,21 +6,15 @@ class ConfigSchema(ma.ModelSchema): class Meta: model = Configs include_fk = True - dump_only = ('id',) + dump_only = ("id",) - views = { - 'admin': [ - 'id', - 'key', - 'value' - ], - } + views = {"admin": ["id", "key", "value"]} def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(ConfigSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/files.py b/CTFd/schemas/files.py index b675453..b11736e 100644 --- a/CTFd/schemas/files.py +++ b/CTFd/schemas/files.py @@ -6,13 +6,13 @@ class FileSchema(ma.ModelSchema): class Meta: model = Files include_fk = True - dump_only = ('id', 'type', 'location') + dump_only = ("id", "type", "location") def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(FileSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/flags.py b/CTFd/schemas/flags.py index 13ea1ea..0c10f9d 100644 --- a/CTFd/schemas/flags.py +++ b/CTFd/schemas/flags.py @@ -6,13 +6,13 @@ class FlagSchema(ma.ModelSchema): class Meta: model = Flags include_fk = True - dump_only = ('id',) + dump_only = ("id",) def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(FlagSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/hints.py b/CTFd/schemas/hints.py index 09c869b..b28970f 100644 --- a/CTFd/schemas/hints.py +++ b/CTFd/schemas/hints.py @@ -6,37 +6,19 @@ class HintSchema(ma.ModelSchema): class Meta: model = Hints include_fk = True - dump_only = ('id', 'type') + dump_only = ("id", "type") views = { - 'locked': [ - 'id', - 'type', - 'challenge', - 'cost' - ], - 'unlocked': [ - 'id', - 'type', - 'challenge', - 'content', - 'cost' - ], - 'admin': [ - 'id', - 'type', - 'challenge', - 'content', - 'cost', - 'requirements' - ] + "locked": ["id", "type", "challenge", "cost"], + "unlocked": ["id", "type", "challenge", "content", "cost"], + "admin": ["id", "type", "challenge", "content", "cost", "requirements"], } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(HintSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/notifications.py b/CTFd/schemas/notifications.py index 370ca9f..b0d1a63 100644 --- a/CTFd/schemas/notifications.py +++ b/CTFd/schemas/notifications.py @@ -6,13 +6,13 @@ class NotificationSchema(ma.ModelSchema): class Meta: model = Notifications include_fk = True - dump_only = ('id', 'date') + dump_only = ("id", "date") def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(NotificationSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/pages.py b/CTFd/schemas/pages.py index f5128b1..f534b08 100644 --- a/CTFd/schemas/pages.py +++ b/CTFd/schemas/pages.py @@ -7,19 +7,19 @@ class PageSchema(ma.ModelSchema): class Meta: model = Pages include_fk = True - dump_only = ('id', ) + dump_only = ("id",) @pre_load def validate_route(self, data): - route = data.get('route') - if route and route.startswith('/'): - data['route'] = route.strip('/') + route = data.get("route") + if route and route.startswith("/"): + data["route"] = route.strip("/") def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(PageSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/submissions.py b/CTFd/schemas/submissions.py index 21e3b08..2e546a2 100644 --- a/CTFd/schemas/submissions.py +++ b/CTFd/schemas/submissions.py @@ -5,41 +5,33 @@ from CTFd.utils import string_types class SubmissionSchema(ma.ModelSchema): - challenge = fields.Nested(ChallengeSchema, only=['name', 'category', 'value']) + challenge = fields.Nested(ChallengeSchema, only=["name", "category", "value"]) class Meta: model = Submissions include_fk = True - dump_only = ('id', ) + dump_only = ("id",) views = { - 'admin': [ - 'provided', - 'ip', - 'challenge_id', - 'challenge', - 'user', - 'team', - 'date', - 'type', - 'id' + "admin": [ + "provided", + "ip", + "challenge_id", + "challenge", + "user", + "team", + "date", + "type", + "id", ], - 'user': [ - 'challenge_id', - 'challenge', - 'user', - 'team', - 'date', - 'type', - 'id' - ] + "user": ["challenge_id", "challenge", "user", "team", "date", "type", "id"], } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(SubmissionSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/tags.py b/CTFd/schemas/tags.py index 76ab338..cf75ad1 100644 --- a/CTFd/schemas/tags.py +++ b/CTFd/schemas/tags.py @@ -6,24 +6,15 @@ class TagSchema(ma.ModelSchema): class Meta: model = Tags include_fk = True - dump_only = ('id',) + dump_only = ("id",) - views = { - 'admin': [ - 'id', - 'challenge', - 'value' - ], - 'user': [ - 'value' - ] - } + views = {"admin": ["id", "challenge", "value"], "user": ["value"]} def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(TagSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/teams.py b/CTFd/schemas/teams.py index 0339000..8a6d94d 100644 --- a/CTFd/schemas/teams.py +++ b/CTFd/schemas/teams.py @@ -12,44 +12,40 @@ class TeamSchema(ma.ModelSchema): class Meta: model = Teams include_fk = True - dump_only = ('id', 'oauth_id', 'created', 'members') - load_only = ('password',) + dump_only = ("id", "oauth_id", "created", "members") + load_only = ("password",) name = field_for( Teams, - 'name', + "name", required=True, validate=[ - validate.Length(min=1, max=128, error='Team names must not be empty') - ] + validate.Length(min=1, max=128, error="Team names must not be empty") + ], ) email = field_for( Teams, - 'email', - validate=validate.Email('Emails must be a properly formatted email address') + "email", + validate=validate.Email("Emails must be a properly formatted email address"), ) website = field_for( Teams, - 'website', + "website", validate=[ # This is a dirty hack to let website accept empty strings so you can remove your website lambda website: validate.URL( - error='Websites must be a proper URL starting with http or https', - schemes={'http', 'https'} - )(website) if website else True - ] - ) - country = field_for( - Teams, - 'country', - validate=[ - validate_country_code - ] + error="Websites must be a proper URL starting with http or https", + schemes={"http", "https"}, + )(website) + if website + else True + ], ) + country = field_for(Teams, "country", validate=[validate_country_code]) @pre_load def validate_name(self, data): - name = data.get('name') + name = data.get("name") if name is None: return @@ -57,57 +53,73 @@ class TeamSchema(ma.ModelSchema): current_team = get_current_team() # Admins should be able to patch anyone but they cannot cause a collision. if is_admin(): - team_id = int(data.get('id', 0)) + team_id = int(data.get("id", 0)) if team_id: if existing_team and existing_team.id != team_id: - raise ValidationError('Team name has already been taken', field_names=['name']) + raise ValidationError( + "Team name has already been taken", field_names=["name"] + ) else: # If there's no Team ID it means that the admin is creating a team with no ID. if existing_team: if current_team: if current_team.id != existing_team.id: - raise ValidationError('Team name has already been taken', field_names=['name']) + raise ValidationError( + "Team name has already been taken", field_names=["name"] + ) else: - raise ValidationError('Team name has already been taken', field_names=['name']) + raise ValidationError( + "Team name has already been taken", field_names=["name"] + ) else: # We need to allow teams to edit themselves and allow the "conflict" - if data['name'] == current_team.name: + if data["name"] == current_team.name: return data else: - name_changes = get_config('name_changes', default=True) + name_changes = get_config("name_changes", default=True) if bool(name_changes) is False: - raise ValidationError('Name changes are disabled', field_names=['name']) + raise ValidationError( + "Name changes are disabled", field_names=["name"] + ) if existing_team: - raise ValidationError('Team name has already been taken', field_names=['name']) + raise ValidationError( + "Team name has already been taken", field_names=["name"] + ) @pre_load def validate_email(self, data): - email = data.get('email') + email = data.get("email") if email is None: return existing_team = Teams.query.filter_by(email=email).first() if is_admin(): - team_id = data.get('id') + team_id = data.get("id") if team_id: if existing_team and existing_team.id != team_id: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) else: if existing_team: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) else: current_team = get_current_team() if email == current_team.email: return data else: if existing_team: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) @pre_load def validate_password_confirmation(self, data): - password = data.get('password') - confirm = data.get('confirm') + password = data.get("password") + confirm = data.get("confirm") if is_admin(): pass @@ -116,29 +128,38 @@ class TeamSchema(ma.ModelSchema): current_user = get_current_user() if current_team.captain_id != current_user.id: - raise ValidationError('Only the captain can change the team password', field_names=['captain_id']) + raise ValidationError( + "Only the captain can change the team password", + field_names=["captain_id"], + ) if password and (bool(confirm) is False): - raise ValidationError('Please confirm your current password', field_names=['confirm']) + raise ValidationError( + "Please confirm your current password", field_names=["confirm"] + ) if password and confirm: - test = verify_password(plaintext=confirm, ciphertext=current_team.password) + test = verify_password( + plaintext=confirm, ciphertext=current_team.password + ) if test is True: return data else: - raise ValidationError('Your previous password is incorrect', field_names=['confirm']) + raise ValidationError( + "Your previous password is incorrect", field_names=["confirm"] + ) else: - data.pop('password', None) - data.pop('confirm', None) + data.pop("password", None) + data.pop("confirm", None) @pre_load def validate_captain_id(self, data): - captain_id = data.get('captain_id') + captain_id = data.get("captain_id") if captain_id is None: return if is_admin(): - team_id = data.get('id') + team_id = data.get("id") if team_id: target_team = Teams.query.filter_by(id=team_id).first() else: @@ -147,64 +168,67 @@ class TeamSchema(ma.ModelSchema): if captain in target_team.members: return else: - raise ValidationError('Invalid Captain ID', field_names=['captain_id']) + raise ValidationError("Invalid Captain ID", field_names=["captain_id"]) else: current_team = get_current_team() current_user = get_current_user() if current_team.captain_id == current_user.id: return else: - raise ValidationError('Only the captain can change team captain', field_names=['captain_id']) + raise ValidationError( + "Only the captain can change team captain", + field_names=["captain_id"], + ) views = { - 'user': [ - 'website', - 'name', - 'country', - 'affiliation', - 'bracket', - 'members', - 'id', - 'oauth_id', - 'captain_id', + "user": [ + "website", + "name", + "country", + "affiliation", + "bracket", + "members", + "id", + "oauth_id", + "captain_id", ], - 'self': [ - 'website', - 'name', - 'email', - 'country', - 'affiliation', - 'bracket', - 'members', - 'id', - 'oauth_id', - 'password', - 'captain_id', + "self": [ + "website", + "name", + "email", + "country", + "affiliation", + "bracket", + "members", + "id", + "oauth_id", + "password", + "captain_id", + ], + "admin": [ + "website", + "name", + "created", + "country", + "banned", + "email", + "affiliation", + "secret", + "bracket", + "members", + "hidden", + "id", + "oauth_id", + "password", + "captain_id", ], - 'admin': [ - 'website', - 'name', - 'created', - 'country', - 'banned', - 'email', - 'affiliation', - 'secret', - 'bracket', - 'members', - 'hidden', - 'id', - 'oauth_id', - 'password', - 'captain_id', - ] } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(TeamSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/unlocks.py b/CTFd/schemas/unlocks.py index 6c4f5ac..84ae37b 100644 --- a/CTFd/schemas/unlocks.py +++ b/CTFd/schemas/unlocks.py @@ -6,30 +6,18 @@ class UnlockSchema(ma.ModelSchema): class Meta: model = Unlocks include_fk = True - dump_only = ('id', 'date') + dump_only = ("id", "date") views = { - 'admin': [ - 'user_id', - 'target', - 'team_id', - 'date', - 'type', - 'id' - ], - 'user': [ - 'target', - 'date', - 'type', - 'id' - ] + "admin": ["user_id", "target", "team_id", "date", "type", "id"], + "user": ["target", "date", "type", "id"], } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(UnlockSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/users.py b/CTFd/schemas/users.py index 18e2a02..77ad24b 100644 --- a/CTFd/schemas/users.py +++ b/CTFd/schemas/users.py @@ -13,181 +13,199 @@ class UserSchema(ma.ModelSchema): class Meta: model = Users include_fk = True - dump_only = ('id', 'oauth_id', 'created') - load_only = ('password',) + dump_only = ("id", "oauth_id", "created") + load_only = ("password",) name = field_for( Users, - 'name', + "name", required=True, validate=[ - validate.Length(min=1, max=128, error='User names must not be empty') - ] + validate.Length(min=1, max=128, error="User names must not be empty") + ], ) email = field_for( Users, - 'email', + "email", validate=[ - validate.Email('Emails must be a properly formatted email address'), - validate.Length(min=1, max=128, error='Emails must not be empty'), - ] + validate.Email("Emails must be a properly formatted email address"), + validate.Length(min=1, max=128, error="Emails must not be empty"), + ], ) website = field_for( Users, - 'website', + "website", validate=[ # This is a dirty hack to let website accept empty strings so you can remove your website lambda website: validate.URL( - error='Websites must be a proper URL starting with http or https', - schemes={'http', 'https'} - )(website) if website else True - ] - ) - country = field_for( - Users, - 'country', - validate=[ - validate_country_code - ] - ) - password = field_for( - Users, - 'password', + error="Websites must be a proper URL starting with http or https", + schemes={"http", "https"}, + )(website) + if website + else True + ], ) + country = field_for(Users, "country", validate=[validate_country_code]) + password = field_for(Users, "password") @pre_load def validate_name(self, data): - name = data.get('name') + name = data.get("name") if name is None: return existing_user = Users.query.filter_by(name=name).first() current_user = get_current_user() if is_admin(): - user_id = data.get('id') + user_id = data.get("id") if user_id: if existing_user and existing_user.id != user_id: - raise ValidationError('User name has already been taken', field_names=['name']) + raise ValidationError( + "User name has already been taken", field_names=["name"] + ) else: if existing_user: if current_user: if current_user.id != existing_user.id: - raise ValidationError('User name has already been taken', field_names=['name']) + raise ValidationError( + "User name has already been taken", field_names=["name"] + ) else: - raise ValidationError('User name has already been taken', field_names=['name']) + raise ValidationError( + "User name has already been taken", field_names=["name"] + ) else: if name == current_user.name: return data else: - name_changes = get_config('name_changes', default=True) + name_changes = get_config("name_changes", default=True) if bool(name_changes) is False: - raise ValidationError('Name changes are disabled', field_names=['name']) + raise ValidationError( + "Name changes are disabled", field_names=["name"] + ) if existing_user: - raise ValidationError('User name has already been taken', field_names=['name']) + raise ValidationError( + "User name has already been taken", field_names=["name"] + ) @pre_load def validate_email(self, data): - email = data.get('email') + email = data.get("email") if email is None: return existing_user = Users.query.filter_by(email=email).first() current_user = get_current_user() if is_admin(): - user_id = data.get('id') + user_id = data.get("id") if user_id: if existing_user and existing_user.id != user_id: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) else: if existing_user: if current_user: if current_user.id != existing_user.id: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", + field_names=["email"], + ) else: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) else: if email == current_user.email: return data else: if existing_user: - raise ValidationError('Email address has already been used', field_names=['email']) + raise ValidationError( + "Email address has already been used", field_names=["email"] + ) if check_email_is_whitelisted(email) is False: raise ValidationError( "Only email addresses under {domains} may register".format( - domains=get_config('domain_whitelist') + domains=get_config("domain_whitelist") ), - field_names=['email'] + field_names=["email"], ) - if get_config('verify_emails'): + if get_config("verify_emails"): current_user.verified = False @pre_load def validate_password_confirmation(self, data): - password = data.get('password') - confirm = data.get('confirm') + password = data.get("password") + confirm = data.get("confirm") target_user = get_current_user() if is_admin(): pass else: if password and (bool(confirm) is False): - raise ValidationError('Please confirm your current password', field_names=['confirm']) + raise ValidationError( + "Please confirm your current password", field_names=["confirm"] + ) if password and confirm: - test = verify_password(plaintext=confirm, ciphertext=target_user.password) + test = verify_password( + plaintext=confirm, ciphertext=target_user.password + ) if test is True: return data else: - raise ValidationError('Your previous password is incorrect', field_names=['confirm']) + raise ValidationError( + "Your previous password is incorrect", field_names=["confirm"] + ) else: - data.pop('password', None) - data.pop('confirm', None) + data.pop("password", None) + data.pop("confirm", None) views = { - 'user': [ - 'website', - 'name', - 'country', - 'affiliation', - 'bracket', - 'id', - 'oauth_id', + "user": [ + "website", + "name", + "country", + "affiliation", + "bracket", + "id", + "oauth_id", ], - 'self': [ - 'website', - 'name', - 'email', - 'country', - 'affiliation', - 'bracket', - 'id', - 'oauth_id', - 'password' + "self": [ + "website", + "name", + "email", + "country", + "affiliation", + "bracket", + "id", + "oauth_id", + "password", + ], + "admin": [ + "website", + "name", + "created", + "country", + "banned", + "email", + "affiliation", + "secret", + "bracket", + "hidden", + "id", + "oauth_id", + "password", + "type", + "verified", ], - 'admin': [ - 'website', - 'name', - 'created', - 'country', - 'banned', - 'email', - 'affiliation', - 'secret', - 'bracket', - 'hidden', - 'id', - 'oauth_id', - 'password', - 'type', - 'verified' - ] } def __init__(self, view=None, *args, **kwargs): if view: if isinstance(view, string_types): - kwargs['only'] = self.views[view] + kwargs["only"] = self.views[view] elif isinstance(view, list): - kwargs['only'] = view + kwargs["only"] = view super(UserSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index b925eef..028e0b9 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -5,15 +5,15 @@ from CTFd.utils.decorators.visibility import check_score_visibility from CTFd.utils.scores import get_standings -scoreboard = Blueprint('scoreboard', __name__) +scoreboard = Blueprint("scoreboard", __name__) -@scoreboard.route('/scoreboard') +@scoreboard.route("/scoreboard") @check_score_visibility def listing(): standings = get_standings() return render_template( - 'scoreboard.html', + "scoreboard.html", standings=standings, - score_frozen=config.is_scoreboard_frozen() + score_frozen=config.is_scoreboard_frozen(), ) diff --git a/CTFd/teams.py b/CTFd/teams.py index 3648d8b..f32be59 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -5,17 +5,20 @@ from CTFd.utils.decorators.modes import require_team_mode from CTFd.utils import config from CTFd.utils.user import get_current_user from CTFd.utils.crypto import verify_password -from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility +from CTFd.utils.decorators.visibility import ( + check_account_visibility, + check_score_visibility, +) from CTFd.utils.helpers import get_errors -teams = Blueprint('teams', __name__) +teams = Blueprint("teams", __name__) -@teams.route('/teams') +@teams.route("/teams") @check_account_visibility @require_team_mode def listing(): - page = abs(request.args.get('page', 1, type=int)) + page = abs(request.args.get("page", 1, type=int)) results_per_page = 50 page_start = results_per_page * (page - 1) page_end = results_per_page * (page - 1) + results_per_page @@ -26,21 +29,25 @@ def listing(): # teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all() # else: count = Teams.query.filter_by(hidden=False, banned=False).count() - teams = Teams.query.filter_by(hidden=False, banned=False).slice(page_start, page_end).all() + teams = ( + Teams.query.filter_by(hidden=False, banned=False) + .slice(page_start, page_end) + .all() + ) pages = int(count / results_per_page) + (count % results_per_page > 0) - return render_template('teams/teams.html', teams=teams, pages=pages, curr_page=page) + return render_template("teams/teams.html", teams=teams, pages=pages, curr_page=page) -@teams.route('/teams/join', methods=['GET', 'POST']) +@teams.route("/teams/join", methods=["GET", "POST"]) @authed_only @require_team_mode def join(): - if request.method == 'GET': - return render_template('teams/join_team.html') - if request.method == 'POST': - teamname = request.form.get('name') - passphrase = request.form.get('password', '').strip() + if request.method == "GET": + return render_template("teams/join_team.html") + if request.method == "POST": + teamname = request.form.get("name") + passphrase = request.form.get("password", "").strip() team = Teams.query.filter_by(name=teamname).first() user = get_current_user() @@ -52,57 +59,51 @@ def join(): team.captain_id = user.id db.session.commit() - return redirect(url_for('challenges.listing')) + return redirect(url_for("challenges.listing")) else: - errors = ['That information is incorrect'] - return render_template('teams/join_team.html', errors=errors) + errors = ["That information is incorrect"] + return render_template("teams/join_team.html", errors=errors) -@teams.route('/teams/new', methods=['GET', 'POST']) +@teams.route("/teams/new", methods=["GET", "POST"]) @authed_only @require_team_mode def new(): - if request.method == 'GET': + if request.method == "GET": return render_template("teams/new_team.html") - elif request.method == 'POST': - teamname = request.form.get('name') - passphrase = request.form.get('password', '').strip() + elif request.method == "POST": + teamname = request.form.get("name") + passphrase = request.form.get("password", "").strip() errors = get_errors() user = get_current_user() existing_team = Teams.query.filter_by(name=teamname).first() if existing_team: - errors.append('That team name is already taken') + errors.append("That team name is already taken") if not teamname: - errors.append('That team name is invalid') + errors.append("That team name is invalid") if errors: return render_template("teams/new_team.html", errors=errors) - team = Teams( - name=teamname, - password=passphrase, - captain_id=user.id - ) + team = Teams(name=teamname, password=passphrase, captain_id=user.id) db.session.add(team) db.session.commit() user.team_id = team.id db.session.commit() - return redirect(url_for('challenges.listing')) + return redirect(url_for("challenges.listing")) -@teams.route('/team') +@teams.route("/team") @authed_only @require_team_mode def private(): user = get_current_user() if not user.team_id: - return render_template( - 'teams/team_enrollment.html', - ) + return render_template("teams/team_enrollment.html") team_id = user.team_id @@ -114,18 +115,18 @@ def private(): score = team.score return render_template( - 'teams/private.html', + "teams/private.html", solves=solves, awards=awards, user=user, team=team, score=score, place=place, - score_frozen=config.is_scoreboard_frozen() + score_frozen=config.is_scoreboard_frozen(), ) -@teams.route('/teams/') +@teams.route("/teams/") @check_account_visibility @check_score_visibility @require_team_mode @@ -139,14 +140,14 @@ def public(team_id): score = team.score if errors: - return render_template('teams/public.html', team=team, errors=errors) + return render_template("teams/public.html", team=team, errors=errors) return render_template( - 'teams/public.html', + "teams/public.html", solves=solves, awards=awards, team=team, score=score, place=place, - score_frozen=config.is_scoreboard_frozen() + score_frozen=config.is_scoreboard_frozen(), ) diff --git a/CTFd/themes/admin/static/css/base.css b/CTFd/themes/admin/static/css/base.css index f5e7478..b4b170d 100644 --- a/CTFd/themes/admin/static/css/base.css +++ b/CTFd/themes/admin/static/css/base.css @@ -1,196 +1,200 @@ -html, body, .container { - font-family: 'Lato', 'LatoOffline', sans-serif; +html, +body, +.container { + font-family: "Lato", "LatoOffline", sans-serif; } -h1, h2 { - font-family: 'Raleway', 'RalewayOffline', sans-serif; - font-weight: 500; - letter-spacing: 2px; +h1, +h2 { + font-family: "Raleway", "RalewayOffline", sans-serif; + font-weight: 500; + letter-spacing: 2px; } a { - color: #337ab7; - text-decoration: none; + color: #337ab7; + text-decoration: none; } table > thead > tr > td { - /* Remove border line from thead of all tables */ - /* It can overlap with other element styles */ - border-top: none !important; + /* Remove border line from thead of all tables */ + /* It can overlap with other element styles */ + border-top: none !important; } -.table td, .table th { - vertical-align: inherit; +.table td, +.table th { + vertical-align: inherit; } pre { - white-space: pre-wrap; - margin: 0; - padding: 0; + white-space: pre-wrap; + margin: 0; + padding: 0; } .fa-spin.spinner { - margin-top: 225px; - text-align: center; - opacity: 0.5; + margin-top: 225px; + text-align: center; + opacity: 0.5; } .spinner-error { - padding-top: 20vh; - text-align: center; - opacity: 0.5; + padding-top: 20vh; + text-align: center; + opacity: 0.5; } .jumbotron { - /*background-color: #343a40;*/ - /*color: #FFF;*/ - border-radius: 0; - text-align: center; + /*background-color: #343a40;*/ + /*color: #FFF;*/ + border-radius: 0; + text-align: center; } .form-control { - position: relative; - display: block; - /*padding: 0.8em;*/ - border-radius: 0; - /*background: #f0f0f0;*/ - /*color: #aaa;*/ - font-weight: 400; - font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; - -webkit-appearance: none; + position: relative; + display: block; + /*padding: 0.8em;*/ + border-radius: 0; + /*background: #f0f0f0;*/ + /*color: #aaa;*/ + font-weight: 400; + font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-appearance: none; } .form-control:focus { - background-color: transparent; - border-color: #a3d39c; - box-shadow: 0 0 0 0.2rem #a3d39c; - transition: background-color 0.3s, border-color 0.3s; + background-color: transparent; + border-color: #a3d39c; + box-shadow: 0 0 0 0.2rem #a3d39c; + transition: background-color 0.3s, border-color 0.3s; } .input-filled-valid { - background-color: transparent; - border-color: #a3d39c; - box-shadow: 0 0 0 0.2rem #a3d39c; - transition: background-color 0.3s, border-color 0.3s; + background-color: transparent; + border-color: #a3d39c; + box-shadow: 0 0 0 0.2rem #a3d39c; + transition: background-color 0.3s, border-color 0.3s; } .input-filled-invalid { - background-color: transparent; - border-color: #d46767; - box-shadow: 0 0 0 0.2rem #d46767; - transition: background-color 0.3s, border-color 0.3s; + background-color: transparent; + border-color: #d46767; + box-shadow: 0 0 0 0.2rem #d46767; + transition: background-color 0.3s, border-color 0.3s; } .btn-outlined.btn-theme { - background: none; - color: #545454; - border-color: #545454; - border: 3px solid; + background: none; + color: #545454; + border-color: #545454; + border: 3px solid; } .btn-outlined { - border-radius: 0; - -webkit-transition: all 0.3s; - -moz-transition: all 0.3s; - transition: all 0.3s; + border-radius: 0; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + transition: all 0.3s; } .btn { - letter-spacing: 1px; - text-decoration: none; - -moz-user-select: none; - border-radius: 0; - cursor: pointer; - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - white-space: nowrap; - font-size: 14px; - line-height: 20px; - font-weight: 700; - padding: 8px 20px; + letter-spacing: 1px; + text-decoration: none; + -moz-user-select: none; + border-radius: 0; + cursor: pointer; + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + white-space: nowrap; + font-size: 14px; + line-height: 20px; + font-weight: 700; + padding: 8px 20px; } .btn-info { - background-color: #5B7290 !important; - border-color: #5B7290 !important; + background-color: #5b7290 !important; + border-color: #5b7290 !important; } .badge-info { - background-color: #5B7290 !important; + background-color: #5b7290 !important; } .alert { - border-radius: 0 !important; - padding: 0.8em; + border-radius: 0 !important; + padding: 0.8em; } #score-graph { - height: 450px; - display: block; - clear: both; + height: 450px; + display: block; + clear: both; } #solves-graph { - display: block; - height: 350px; + display: block; + height: 350px; } #keys-pie-graph { - height: 400px; - display: block; + height: 400px; + display: block; } #categories-pie-graph { - height: 400px; - display: block; + height: 400px; + display: block; } #solve-percentages-graph { - height: 400px; - display: block; + height: 400px; + display: block; } .no-decoration { - color: inherit !important; - text-decoration: none !important; + color: inherit !important; + text-decoration: none !important; } .no-decoration:hover { - color: inherit !important; - text-decoration: none !important; + color: inherit !important; + text-decoration: none !important; } .btn-fa { - cursor: pointer; + cursor: pointer; } .close { - cursor: pointer; + cursor: pointer; } .cursor-pointer { - cursor: pointer; + cursor: pointer; } .cursor-help { - cursor: help; + cursor: help; } .modal-content { - -webkit-border-radius: 0 !important; - -moz-border-radius: 0 !important; - border-radius: 0 !important; + -webkit-border-radius: 0 !important; + -moz-border-radius: 0 !important; + border-radius: 0 !important; } .text-break { - /* TODO: This is .text-break cloned from Bootstrap 4.3 with a fix for browsers not supporting break-word. Remove later. */ - word-break: break-all !important; - word-break: break-word !important; - overflow-wrap: break-word !important; + /* TODO: This is .text-break cloned from Bootstrap 4.3 with a fix for browsers not supporting break-word. Remove later. */ + word-break: break-all !important; + word-break: break-word !important; + overflow-wrap: break-word !important; } .fa-disabled { - opacity: 0.5; - cursor: not-allowed; -} \ No newline at end of file + opacity: 0.5; + cursor: not-allowed; +} diff --git a/CTFd/themes/admin/static/css/challenge-board.css b/CTFd/themes/admin/static/css/challenge-board.css index 6a7e6cd..392f058 100644 --- a/CTFd/themes/admin/static/css/challenge-board.css +++ b/CTFd/themes/admin/static/css/challenge-board.css @@ -1,58 +1,58 @@ .chal-desc { - padding-left: 30px; - padding-right: 30px; - font-size: 14px; + padding-left: 30px; + padding-right: 30px; + font-size: 14px; } .chal-desc img { - max-width: 100%; - height: auto; + max-width: 100%; + height: auto; } .modal-content { - border-radius: 0px; - max-width: 1000px; - padding: 1em; - margin: 0 auto; + border-radius: 0px; + max-width: 1000px; + padding: 1em; + margin: 0 auto; } .btn-info { - background-color: #5B7290 !important; + background-color: #5b7290 !important; } .badge-info { - background-color: #5B7290 !important; + background-color: #5b7290 !important; } .challenge-button { - box-shadow: 3px 3px 3px grey; + box-shadow: 3px 3px 3px grey; } .solved-challenge { - background-color: #37d63e !important; - opacity: 0.4; - border: none; + background-color: #37d63e !important; + opacity: 0.4; + border: none; } .corner-button-check { - margin-top: -10px; - margin-right: 25px; - position: absolute; - right: 0; + margin-top: -10px; + margin-right: 25px; + position: absolute; + right: 0; } .key-submit .btn { - height: 51px; + height: 51px; } #challenge-window .form-control { - position: relative; - display: block; - padding: 0.8em; - border-radius: 0; - background: #f0f0f0; - color: #aaa; - font-weight: 400; - font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; - -webkit-appearance: none; + position: relative; + display: block; + padding: 0.8em; + border-radius: 0; + background: #f0f0f0; + color: #aaa; + font-weight: 400; + font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-appearance: none; } diff --git a/CTFd/themes/admin/static/css/jumbotron.css b/CTFd/themes/admin/static/css/jumbotron.css index ca51624..d751264 100644 --- a/CTFd/themes/admin/static/css/jumbotron.css +++ b/CTFd/themes/admin/static/css/jumbotron.css @@ -1,4 +1,4 @@ /* Move down content because we have a fixed navbar that is 3.5rem tall */ body { - padding-top: 3.5rem; -} \ No newline at end of file + padding-top: 3.5rem; +} diff --git a/CTFd/themes/admin/static/css/sticky-footer.css b/CTFd/themes/admin/static/css/sticky-footer.css index 4a894bb..283e4bf 100644 --- a/CTFd/themes/admin/static/css/sticky-footer.css +++ b/CTFd/themes/admin/static/css/sticky-footer.css @@ -1,19 +1,19 @@ /* Sticky footer styles -------------------------------------------------- */ html { - position: relative; - min-height: 100%; + position: relative; + min-height: 100%; } body { - margin-bottom: 60px; /* Margin bottom by footer height */ + margin-bottom: 60px; /* Margin bottom by footer height */ } .footer { - position: absolute; - bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */ - width: 100%; - height: 60px; /* Set the fixed height of the footer here */ - /*line-height: 60px; !* Vertically center the text there *!*/ - /*background-color: #f5f5f5;*/ -} \ No newline at end of file + position: absolute; + bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */ + width: 100%; + height: 60px; /* Set the fixed height of the footer here */ + /*line-height: 60px; !* Vertically center the text there *!*/ + /*background-color: #f5f5f5;*/ +} diff --git a/CTFd/themes/admin/static/css/style.css b/CTFd/themes/admin/static/css/style.css index 689cd71..1f4cdb3 100644 --- a/CTFd/themes/admin/static/css/style.css +++ b/CTFd/themes/admin/static/css/style.css @@ -1,7 +1,7 @@ tbody tr:hover { - background-color: rgba(0, 0, 0, .1) !important; + background-color: rgba(0, 0, 0, 0.1) !important; } tr[data-href] { - cursor: pointer; -} \ No newline at end of file + cursor: pointer; +} diff --git a/CTFd/themes/admin/static/js/challenges/challenge.js b/CTFd/themes/admin/static/js/challenges/challenge.js index 309829f..e4dd3c5 100644 --- a/CTFd/themes/admin/static/js/challenges/challenge.js +++ b/CTFd/themes/admin/static/js/challenges/challenge.js @@ -1,195 +1,237 @@ function renderSubmissionResponse(response, cb) { - var result = response.data; + var result = response.data; - var result_message = $('#result-message'); - var result_notification = $('#result-notification'); - var answer_input = $("#submission-input"); - result_notification.removeClass(); - result_message.text(result.message); + var result_message = $("#result-message"); + var result_notification = $("#result-notification"); + var answer_input = $("#submission-input"); + result_notification.removeClass(); + result_message.text(result.message); - if (result.status === "authentication_required") { - window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash; - return - } - else if (result.status === "incorrect") { // Incorrect key - result_notification.addClass('alert alert-danger alert-dismissable text-center'); - result_notification.slideDown(); + if (result.status === "authentication_required") { + window.location = + script_root + + "/login?next=" + + script_root + + window.location.pathname + + window.location.hash; + return; + } else if (result.status === "incorrect") { + // Incorrect key + result_notification.addClass( + "alert alert-danger alert-dismissable text-center" + ); + result_notification.slideDown(); - answer_input.removeClass("correct"); - answer_input.addClass("wrong"); - setTimeout(function () { - answer_input.removeClass("wrong"); - }, 3000); - } - else if (result.status === "correct") { // Challenge Solved - result_notification.addClass('alert alert-success alert-dismissable text-center'); - result_notification.slideDown(); - - $('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves")); - - answer_input.val(""); - answer_input.removeClass("wrong"); - answer_input.addClass("correct"); - } - else if (result.status === "already_solved") { // Challenge already solved - result_notification.addClass('alert alert-info alert-dismissable text-center'); - result_notification.slideDown(); - - answer_input.addClass("correct"); - } - else if (result.status === "paused") { // CTF is paused - result_notification.addClass('alert alert-warning alert-dismissable text-center'); - result_notification.slideDown(); - } - else if (result.status === "ratelimited") { // Keys per minute too high - result_notification.addClass('alert alert-warning alert-dismissable text-center'); - result_notification.slideDown(); - - answer_input.addClass("too-fast"); - setTimeout(function () { - answer_input.removeClass("too-fast"); - }, 3000); - } - setTimeout(function () { - $('.alert').slideUp(); - $('#submit-key').removeClass("disabled-button"); - $('#submit-key').prop('disabled', false); + answer_input.removeClass("correct"); + answer_input.addClass("wrong"); + setTimeout(function() { + answer_input.removeClass("wrong"); }, 3000); + } else if (result.status === "correct") { + // Challenge Solved + result_notification.addClass( + "alert alert-success alert-dismissable text-center" + ); + result_notification.slideDown(); - if (cb) { - cb(result); - } + $(".challenge-solves").text( + parseInt( + $(".challenge-solves") + .text() + .split(" ")[0] + ) + + 1 + + " Solves" + ); + + answer_input.val(""); + answer_input.removeClass("wrong"); + answer_input.addClass("correct"); + } else if (result.status === "already_solved") { + // Challenge already solved + result_notification.addClass( + "alert alert-info alert-dismissable text-center" + ); + result_notification.slideDown(); + + answer_input.addClass("correct"); + } else if (result.status === "paused") { + // CTF is paused + result_notification.addClass( + "alert alert-warning alert-dismissable text-center" + ); + result_notification.slideDown(); + } else if (result.status === "ratelimited") { + // Keys per minute too high + result_notification.addClass( + "alert alert-warning alert-dismissable text-center" + ); + result_notification.slideDown(); + + answer_input.addClass("too-fast"); + setTimeout(function() { + answer_input.removeClass("too-fast"); + }, 3000); + } + setTimeout(function() { + $(".alert").slideUp(); + $("#submit-key").removeClass("disabled-button"); + $("#submit-key").prop("disabled", false); + }, 3000); + + if (cb) { + cb(result); + } } -$(document).ready(function () { - $('.preview-challenge').click(function (e) { - window.challenge = new Object(); - $.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function (response) { - var challenge_data = response.data; - challenge_data['solves'] = null; +$(document).ready(function() { + $(".preview-challenge").click(function(e) { + window.challenge = new Object(); + $.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function( + response + ) { + var challenge_data = response.data; + challenge_data["solves"] = null; - $.getScript(script_root + challenge_data.type_data.scripts.view, function () { - $.get(script_root + challenge_data.type_data.templates.view, function (template_data) { + $.getScript( + script_root + challenge_data.type_data.scripts.view, + function() { + $.get(script_root + challenge_data.type_data.templates.view, function( + template_data + ) { + $("#challenge-window").empty(); + var template = nunjucks.compile(template_data); + window.challenge.data = challenge_data; + window.challenge.preRender(); - $('#challenge-window').empty(); - var template = nunjucks.compile(template_data); - window.challenge.data = challenge_data; - window.challenge.preRender(); + challenge_data["description"] = window.challenge.render( + challenge_data["description"] + ); + challenge_data["script_root"] = script_root; - challenge_data['description'] = window.challenge.render(challenge_data['description']); - challenge_data['script_root'] = script_root; + $("#challenge-window").append(template.render(challenge_data)); - $('#challenge-window').append(template.render(challenge_data)); - - $('.challenge-solves').click(function (e) { - getsolves($('#challenge-id').val()) - }); - $('.nav-tabs a').click(function (e) { - e.preventDefault(); - $(this).tab('show') - }); - - // Handle modal toggling - $('#challenge-window').on('hide.bs.modal', function (event) { - $("#submission-input").removeClass("wrong"); - $("#submission-input").removeClass("correct"); - $("#incorrect-key").slideUp(); - $("#correct-key").slideUp(); - $("#already-solved").slideUp(); - $("#too-fast").slideUp(); - }); - - $('#submit-key').click(function (e) { - e.preventDefault(); - $('#submit-key').addClass("disabled-button"); - $('#submit-key').prop('disabled', true); - window.challenge.submit(function (data) { - renderSubmissionResponse(data) - }, true); - // Preview passed as true - }); - - $("#submission-input").keyup(function (event) { - if (event.keyCode == 13) { - $("#submit-key").click(); - } - }); - - $(".input-field").bind({ - focus: function () { - $(this).parent().addClass('input--filled'); - $label = $(this).siblings(".input-label"); - }, - blur: function () { - if ($(this).val() === '') { - $(this).parent().removeClass('input--filled'); - $label = $(this).siblings(".input-label"); - $label.removeClass('input--hide'); - } - } - }); - - window.challenge.postRender(); - window.location.replace(window.location.href.split('#')[0] + '#preview'); - - $('#challenge-window').modal(); - }); + $(".challenge-solves").click(function(e) { + getsolves($("#challenge-id").val()); }); - }); + $(".nav-tabs a").click(function(e) { + e.preventDefault(); + $(this).tab("show"); + }); + + // Handle modal toggling + $("#challenge-window").on("hide.bs.modal", function(event) { + $("#submission-input").removeClass("wrong"); + $("#submission-input").removeClass("correct"); + $("#incorrect-key").slideUp(); + $("#correct-key").slideUp(); + $("#already-solved").slideUp(); + $("#too-fast").slideUp(); + }); + + $("#submit-key").click(function(e) { + e.preventDefault(); + $("#submit-key").addClass("disabled-button"); + $("#submit-key").prop("disabled", true); + window.challenge.submit(function(data) { + renderSubmissionResponse(data); + }, true); + // Preview passed as true + }); + + $("#submission-input").keyup(function(event) { + if (event.keyCode == 13) { + $("#submit-key").click(); + } + }); + + $(".input-field").bind({ + focus: function() { + $(this) + .parent() + .addClass("input--filled"); + $label = $(this).siblings(".input-label"); + }, + blur: function() { + if ($(this).val() === "") { + $(this) + .parent() + .removeClass("input--filled"); + $label = $(this).siblings(".input-label"); + $label.removeClass("input--hide"); + } + } + }); + + window.challenge.postRender(); + window.location.replace( + window.location.href.split("#")[0] + "#preview" + ); + + $("#challenge-window").modal(); + }); + } + ); }); + }); - $('.delete-challenge').click(function(e){ - ezq({ - title: "Delete Challenge", - body: "Are you sure you want to delete {0}".format("" + htmlentities(CHALLENGE_NAME) + ""), - success: function () { - CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { - method: 'DELETE', - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - window.location = script_root + '/admin/challenges'; - } - }); - } - }); - }); - - $('#challenge-update-container > form').submit(function(e){ - e.preventDefault(); - var params = $(e.target).serializeJSON(true); - console.log(params); - - - CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { - method: 'PATCH', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }).then(function (response) { + $(".delete-challenge").click(function(e) { + ezq({ + title: "Delete Challenge", + body: "Are you sure you want to delete {0}".format( + "" + htmlentities(CHALLENGE_NAME) + "" + ), + success: function() { + CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, { + method: "DELETE" + }) + .then(function(response) { return response.json(); - }).then(function (data) { - if (data.success) { - ezal({ - title: "Success", - body: "Your challenge has been updated!", - button: "OK" - }); + }) + .then(function(response) { + if (response.success) { + window.location = script_root + "/admin/challenges"; } - }); + }); + } }); + }); - if (window.location.hash) { - let hash = window.location.hash.replace("<>[]'\"", ""); - $('nav a[href="' + hash + '"]').tab('show'); - } + $("#challenge-update-container > form").submit(function(e) { + e.preventDefault(); + var params = $(e.target).serializeJSON(true); + console.log(params); - $('.nav-tabs a').click(function (e) { - $(this).tab('show'); - window.location.hash = this.hash; - }); -}); \ No newline at end of file + CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, { + method: "PATCH", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(params) + }) + .then(function(response) { + return response.json(); + }) + .then(function(data) { + if (data.success) { + ezal({ + title: "Success", + body: "Your challenge has been updated!", + button: "OK" + }); + } + }); + }); + + if (window.location.hash) { + let hash = window.location.hash.replace("<>[]'\"", ""); + $('nav a[href="' + hash + '"]').tab("show"); + } + + $(".nav-tabs a").click(function(e) { + $(this).tab("show"); + window.location.hash = this.hash; + }); +}); diff --git a/CTFd/themes/admin/static/js/challenges/challenges.js b/CTFd/themes/admin/static/js/challenges/challenges.js index d04e352..5325860 100644 --- a/CTFd/themes/admin/static/js/challenges/challenges.js +++ b/CTFd/themes/admin/static/js/challenges/challenges.js @@ -1,2 +1 @@ -$(document).ready(function () { -}); \ No newline at end of file +$(document).ready(function() {}); diff --git a/CTFd/themes/admin/static/js/challenges/files.js b/CTFd/themes/admin/static/js/challenges/files.js index 69b4fe0..97716d0 100644 --- a/CTFd/themes/admin/static/js/challenges/files.js +++ b/CTFd/themes/admin/static/js/challenges/files.js @@ -1,75 +1,75 @@ -$(document).ready(function () { - $('#file-add-form').submit(function (e) { - e.preventDefault(); - var formData = new FormData(e.target); - formData.append('nonce', csrf_nonce); - formData.append('challenge', CHALLENGE_ID); - formData.append('type', 'challenge'); - var pg = ezpg({ - width: 0, - title: "Upload Progress", - }); - $.ajax({ - url: script_root + '/api/v1/files', - data: formData, - type: 'POST', - cache: false, - contentType: false, - processData: false, - xhr: function () { - var xhr = $.ajaxSettings.xhr(); - xhr.upload.onprogress = function (e) { - if (e.lengthComputable) { - var width = (e.loaded / e.total) * 100; - pg = ezpg({ - target: pg, - width: width - }); - } - }; - return xhr; - }, - success: function (data) { - // TODO: Refresh files on submit - e.target.reset(); - - // Refresh modal - pg = ezpg({ - target: pg, - width: 100, - }); - setTimeout( - function () { - pg.modal('hide'); - }, 500 - ); - - setTimeout( - function () { - window.location.reload(); - }, 700 - ); - } - }); +$(document).ready(function() { + $("#file-add-form").submit(function(e) { + e.preventDefault(); + var formData = new FormData(e.target); + formData.append("nonce", csrf_nonce); + formData.append("challenge", CHALLENGE_ID); + formData.append("type", "challenge"); + var pg = ezpg({ + width: 0, + title: "Upload Progress" }); + $.ajax({ + url: script_root + "/api/v1/files", + data: formData, + type: "POST", + cache: false, + contentType: false, + processData: false, + xhr: function() { + var xhr = $.ajaxSettings.xhr(); + xhr.upload.onprogress = function(e) { + if (e.lengthComputable) { + var width = (e.loaded / e.total) * 100; + pg = ezpg({ + target: pg, + width: width + }); + } + }; + return xhr; + }, + success: function(data) { + // TODO: Refresh files on submit + e.target.reset(); - $('.delete-file').click(function(e){ - var file_id = $(this).attr('file-id'); - var row = $(this).parent().parent(); - ezq({ - title: "Delete Files", - body: "Are you sure you want to delete this file?", - success: function () { - CTFd.fetch('/api/v1/files/' + file_id, { - method: 'DELETE', - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - row.remove(); - } - }); - } + // Refresh modal + pg = ezpg({ + target: pg, + width: 100 }); + setTimeout(function() { + pg.modal("hide"); + }, 500); + + setTimeout(function() { + window.location.reload(); + }, 700); + } }); -}); \ No newline at end of file + }); + + $(".delete-file").click(function(e) { + var file_id = $(this).attr("file-id"); + var row = $(this) + .parent() + .parent(); + ezq({ + title: "Delete Files", + body: "Are you sure you want to delete this file?", + success: function() { + CTFd.fetch("/api/v1/files/" + file_id, { + method: "DELETE" + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + row.remove(); + } + }); + } + }); + }); +}); diff --git a/CTFd/themes/admin/static/js/challenges/flags.js b/CTFd/themes/admin/static/js/challenges/flags.js index 2a3abd8..2abaf3b 100644 --- a/CTFd/themes/admin/static/js/challenges/flags.js +++ b/CTFd/themes/admin/static/js/challenges/flags.js @@ -1,116 +1,134 @@ -$(document).ready(function () { - $('#flag-add-button').click(function (e) { - $.get(script_root + '/api/v1/flags/types', function (response) { - var data = response.data; - var flag_type_select = $("#flags-create-select"); - flag_type_select.empty(); +$(document).ready(function() { + $("#flag-add-button").click(function(e) { + $.get(script_root + "/api/v1/flags/types", function(response) { + var data = response.data; + var flag_type_select = $("#flags-create-select"); + flag_type_select.empty(); - var option = ""; - flag_type_select.append(option); + var option = ""; + flag_type_select.append(option); - for (var key in data) { - if (data.hasOwnProperty(key)) { - option = "".format(key, data[key].name); - flag_type_select.append(option); - } - } - $("#flag-edit-modal").modal(); - }); - - $('#flag-edit-modal form').submit(function(e){ - e.preventDefault(); - var params = $(this).serializeJSON(true); - params['challenge'] = CHALLENGE_ID; - CTFd.fetch('/api/v1/flags', { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }).then(function (response) { - return response.json() - }).then(function (response) { - window.location.reload(); - }); - }); - $('#flag-edit-modal').modal(); + for (var key in data) { + if (data.hasOwnProperty(key)) { + option = "".format( + key, + data[key].name + ); + flag_type_select.append(option); + } + } + $("#flag-edit-modal").modal(); }); - - $("#flags-create-select").change(function (e) { - e.preventDefault(); - var flag_type_name = $(this).find("option:selected").text(); - - $.get(script_root + '/api/v1/flags/types/' + flag_type_name, function (response) { - var data = response.data; - $.get(script_root + data.templates.create, function (template_data) { - var template = nunjucks.compile(template_data); - $("#create-keys-entry-div").html(template.render()); - $("#create-keys-button-div").show(); - }); + $("#flag-edit-modal form").submit(function(e) { + e.preventDefault(); + var params = $(this).serializeJSON(true); + params["challenge"] = CHALLENGE_ID; + CTFd.fetch("/api/v1/flags", { + method: "POST", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(params) + }) + .then(function(response) { + return response.json(); }) + .then(function(response) { + window.location.reload(); + }); }); + $("#flag-edit-modal").modal(); + }); - $('.edit-flag').click(function (e) { - e.preventDefault(); - var flag_id = $(this).attr('flag-id'); - var row = $(this).parent().parent(); + $("#flags-create-select").change(function(e) { + e.preventDefault(); + var flag_type_name = $(this) + .find("option:selected") + .text(); - $.get(script_root + '/api/v1/flags/' + flag_id, function (response) { - var data = response.data; - $.get(script_root + data.templates.update, function (template_data) { - $('#edit-flags form').empty(); + $.get(script_root + "/api/v1/flags/types/" + flag_type_name, function( + response + ) { + var data = response.data; + $.get(script_root + data.templates.create, function(template_data) { + var template = nunjucks.compile(template_data); + $("#create-keys-entry-div").html(template.render()); + $("#create-keys-button-div").show(); + }); + }); + }); - var template = nunjucks.compile(template_data); - $('#edit-flags form').append(template.render(data)); + $(".edit-flag").click(function(e) { + e.preventDefault(); + var flag_id = $(this).attr("flag-id"); + var row = $(this) + .parent() + .parent(); - $('#edit-flags form').submit(function (e) { - e.preventDefault(); - var params = $('#edit-flags form').serializeJSON(); + $.get(script_root + "/api/v1/flags/" + flag_id, function(response) { + var data = response.data; + $.get(script_root + data.templates.update, function(template_data) { + $("#edit-flags form").empty(); - CTFd.fetch('/api/v1/flags/' + flag_id, { - method: 'PATCH', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - $(row).find('.flag-content').text(response.data.content); - $('#edit-flags').modal('toggle'); - } - }); - }); - $('#edit-flags').modal(); + var template = nunjucks.compile(template_data); + $("#edit-flags form").append(template.render(data)); + + $("#edit-flags form").submit(function(e) { + e.preventDefault(); + var params = $("#edit-flags form").serializeJSON(); + + CTFd.fetch("/api/v1/flags/" + flag_id, { + method: "PATCH", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(params) + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + $(row) + .find(".flag-content") + .text(response.data.content); + $("#edit-flags").modal("toggle"); + } }); }); + $("#edit-flags").modal(); + }); }); + }); - $('.delete-flag').click(function (e) { - e.preventDefault(); - var flag_id = $(this).attr('flag-id'); - var row = $(this).parent().parent(); + $(".delete-flag").click(function(e) { + e.preventDefault(); + var flag_id = $(this).attr("flag-id"); + var row = $(this) + .parent() + .parent(); - ezq({ - title: "Delete Flag", - body: "Are you sure you want to delete this flag?", - success: function () { - CTFd.fetch('/api/v1/flags/' + flag_id, { - method: 'DELETE', - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - row.remove(); - } - }); + ezq({ + title: "Delete Flag", + body: "Are you sure you want to delete this flag?", + success: function() { + CTFd.fetch("/api/v1/flags/" + flag_id, { + method: "DELETE" + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + row.remove(); } - }); + }); + } }); -}); \ No newline at end of file + }); +}); diff --git a/CTFd/themes/admin/static/js/challenges/hints.js b/CTFd/themes/admin/static/js/challenges/hints.js index 2120f05..681f5bb 100644 --- a/CTFd/themes/admin/static/js/challenges/hints.js +++ b/CTFd/themes/admin/static/js/challenges/hints.js @@ -1,145 +1,157 @@ function hint(id) { - return CTFd.fetch('/api/v1/hints/' + id + '?preview=true', { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }).then(function (response) { - return response.json(); - }); + return CTFd.fetch("/api/v1/hints/" + id + "?preview=true", { + method: "GET", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + } + }).then(function(response) { + return response.json(); + }); } function loadhint(hintid) { - var md = window.markdownit({ - html: true, - linkify: true, - }); + var md = window.markdownit({ + html: true, + linkify: true + }); - hint(hintid).then(function (response) { - if (response.data.content) { - ezal({ - title: "Hint", - body: md.render(response.data.content), - button: "Got it!" - }); - } else { - ezal({ - title: "Error", - body: "Error loading hint!", - button: "OK" - }); - } - }); + hint(hintid).then(function(response) { + if (response.data.content) { + ezal({ + title: "Hint", + body: md.render(response.data.content), + button: "Got it!" + }); + } else { + ezal({ + title: "Error", + body: "Error loading hint!", + button: "OK" + }); + } + }); } -$(document).ready(function () { - $('#hint-add-button').click(function (e) { - $('#hint-edit-modal form').find("input, textarea").val(""); +$(document).ready(function() { + $("#hint-add-button").click(function(e) { + $("#hint-edit-modal form") + .find("input, textarea") + .val(""); - // Markdown Preview - $('#new-hint-edit').on('shown.bs.tab', function (event) { - console.log(event.target.hash); - if (event.target.hash == '#hint-preview') { - console.log(event.target.hash); - var renderer = window.markdownit({ - html: true, - linkify: true, - }); - var editor_value = $('#hint-write textarea').val(); - $(event.target.hash).html(renderer.render(editor_value)); - } + // Markdown Preview + $("#new-hint-edit").on("shown.bs.tab", function(event) { + console.log(event.target.hash); + if (event.target.hash == "#hint-preview") { + console.log(event.target.hash); + var renderer = window.markdownit({ + html: true, + linkify: true }); - - $('#hint-edit-modal').modal(); + var editor_value = $("#hint-write textarea").val(); + $(event.target.hash).html(renderer.render(editor_value)); + } }); - $('.delete-hint').click(function(e){ - e.preventDefault(); - var hint_id = $(this).attr('hint-id'); - var row = $(this).parent().parent(); - ezq({ - title: "Delete Hint", - body: "Are you sure you want to delete this hint?", - success: function () { - CTFd.fetch('/api/v1/hints/' + hint_id, { - method: 'DELETE', - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - row.remove(); - } - }); - } - }); - }); + $("#hint-edit-modal").modal(); + }); - $('.edit-hint').click(function (e) { - e.preventDefault(); - var hint_id = $(this).attr('hint-id'); - - CTFd.fetch('/api/v1/hints/' + hint_id + '?preview=true', { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }).then(function (response) { - return response.json() - }).then(function (response) { + $(".delete-hint").click(function(e) { + e.preventDefault(); + var hint_id = $(this).attr("hint-id"); + var row = $(this) + .parent() + .parent(); + ezq({ + title: "Delete Hint", + body: "Are you sure you want to delete this hint?", + success: function() { + CTFd.fetch("/api/v1/hints/" + hint_id, { + method: "DELETE" + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { if (response.success) { - $('#hint-edit-form input[name=content],textarea[name=content]').val(response.data.content); - $('#hint-edit-form input[name=cost]').val(response.data.cost); - $('#hint-edit-form input[name=id]').val(response.data.id); - - // Markdown Preview - $('#new-hint-edit').on('shown.bs.tab', function (event) { - console.log(event.target.hash); - if (event.target.hash == '#hint-preview') { - console.log(event.target.hash); - var renderer = new markdownit({ - html: true, - linkify: true, - }); - var editor_value = $('#hint-write textarea').val(); - $(event.target.hash).html(renderer.render(editor_value)); - } - }); - - $('#hint-edit-modal').modal(); + row.remove(); } - }); + }); + } }); + }); - $('#hint-edit-form').submit(function (e) { - e.preventDefault(); - var params = $(this).serializeJSON(true); - params['challenge'] = CHALLENGE_ID; + $(".edit-hint").click(function(e) { + e.preventDefault(); + var hint_id = $(this).attr("hint-id"); - var method = 'POST'; - var url = '/api/v1/hints'; - if (params.id){ - method = 'PATCH'; - url = '/api/v1/hints/' + params.id; + CTFd.fetch("/api/v1/hints/" + hint_id + "?preview=true", { + method: "GET", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + } + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + $("#hint-edit-form input[name=content],textarea[name=content]").val( + response.data.content + ); + $("#hint-edit-form input[name=cost]").val(response.data.cost); + $("#hint-edit-form input[name=id]").val(response.data.id); + + // Markdown Preview + $("#new-hint-edit").on("shown.bs.tab", function(event) { + console.log(event.target.hash); + if (event.target.hash == "#hint-preview") { + console.log(event.target.hash); + var renderer = new markdownit({ + html: true, + linkify: true + }); + var editor_value = $("#hint-write textarea").val(); + $(event.target.hash).html(renderer.render(editor_value)); + } + }); + + $("#hint-edit-modal").modal(); } - CTFd.fetch(url, { - method: method, - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }).then(function (response) { - return response.json() - }).then(function(response) { - if (response.success){ - // TODO: Refresh hints on submit. - window.location.reload(); - } - }); - }); -}); \ No newline at end of file + }); + }); + + $("#hint-edit-form").submit(function(e) { + e.preventDefault(); + var params = $(this).serializeJSON(true); + params["challenge"] = CHALLENGE_ID; + + var method = "POST"; + var url = "/api/v1/hints"; + if (params.id) { + method = "PATCH"; + url = "/api/v1/hints/" + params.id; + } + CTFd.fetch(url, { + method: method, + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(params) + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + // TODO: Refresh hints on submit. + window.location.reload(); + } + }); + }); +}); diff --git a/CTFd/themes/admin/static/js/challenges/new.js b/CTFd/themes/admin/static/js/challenges/new.js index 976b54e..93f3aad 100644 --- a/CTFd/themes/admin/static/js/challenges/new.js +++ b/CTFd/themes/admin/static/js/challenges/new.js @@ -2,61 +2,68 @@ $.ajaxSetup({ cache: false }); window.challenge = new Object(); -function load_chal_template(challenge){ - $.getScript(script_root + challenge.scripts.view, function () { - console.log('loaded renderer'); - $.get(script_root + challenge.templates.create, function (template_data) { - var template = nunjucks.compile(template_data); - $("#create-chal-entry-div").html(template.render({'nonce': nonce, 'script_root': script_root})); - $.getScript(script_root + challenge.scripts.create, function () { - console.log('loaded'); - $("#create-chal-entry-div form").submit(function (e) { - e.preventDefault(); - var params = $("#create-chal-entry-div form").serializeJSON(); - CTFd.fetch('/api/v1/challenges', { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(params) - }).then(function (response) { - return response.json(); - }).then(function (response) { - if (response.success) { - window.location = script_root + '/admin/challenges/' + response.data.id; - } - }); - }); +function load_chal_template(challenge) { + $.getScript(script_root + challenge.scripts.view, function() { + console.log("loaded renderer"); + $.get(script_root + challenge.templates.create, function(template_data) { + var template = nunjucks.compile(template_data); + $("#create-chal-entry-div").html( + template.render({ nonce: nonce, script_root: script_root }) + ); + $.getScript(script_root + challenge.scripts.create, function() { + console.log("loaded"); + $("#create-chal-entry-div form").submit(function(e) { + e.preventDefault(); + var params = $("#create-chal-entry-div form").serializeJSON(); + CTFd.fetch("/api/v1/challenges", { + method: "POST", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify(params) + }) + .then(function(response) { + return response.json(); + }) + .then(function(response) { + if (response.success) { + window.location = + script_root + "/admin/challenges/" + response.data.id; + } }); }); + }); }); + }); } -$.get(script_root + '/api/v1/challenges/types', function(response){ - $("#create-chals-select").empty(); - var data = response.data; - var chal_type_amt = Object.keys(data).length; - if (chal_type_amt > 1){ - var option = ""; - $("#create-chals-select").append(option); - for (var key in data){ - var challenge = data[key]; - var option = $(""; + $("#create-chals-select").append(option); + for (var key in data) { + var challenge = data[key]; + var option = $("