Format all the things (#991)

* Format Javascript and CSS files with `prettier`: `prettier --write 'CTFd/themes/**/*'`
* Format Python with `black`: `black CTFd` & `black tests`
* Travis now uses xenial instead of trusty.
selenium-screenshot-testing
Kevin Chung 2019-05-11 21:09:37 -04:00 committed by GitHub
parent 3d23ece370
commit 6833378c36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
201 changed files with 9561 additions and 9107 deletions

3
.gitignore vendored
View File

@ -70,3 +70,6 @@ CTFd/uploads
# CTFd Exports # CTFd Exports
*.zip *.zip
# JS
node_modules/

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
CTFd/themes/**/vendor/
*.html
*.njk
*.png
*.svg
*.ico
*.ai
*.svg
*.mp3
*.webm

View File

@ -1,5 +1,8 @@
language: python language: python
cache: pip dist: xenial
cache:
- pip
- yarn
services: services:
- mysql - mysql
- postgresql - postgresql
@ -7,26 +10,25 @@ services:
addons: addons:
apt: apt:
sources: sources:
- mysql-5.7-trusty - deadsnakes
packages: packages:
- mysql-server - python3.6
- mysql-client - python3-pip
env: 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='sqlite://'
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd' - TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
python: python:
- 2.7 - 2.7
- 3.6 - 3.6
before_install: 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 - sudo rm -f /etc/boto.cfg
- export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE - export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
- export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY - export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- python3.6 -m pip install black>=19.3b0
install: install:
- pip install -r development.txt - pip install -r development.txt
- yarn global add prettier@1.17.0
before_script: before_script:
- psql -c 'create database ctfd;' -U postgres - psql -c 'create database ctfd;' -U postgres
script: script:

View File

@ -28,7 +28,7 @@ if sys.version_info[0] < 3:
reload(sys) # noqa: F821 reload(sys) # noqa: F821
sys.setdefaultencoding("utf-8") sys.setdefaultencoding("utf-8")
__version__ = '2.1.1' __version__ = "2.1.1"
class CTFdRequest(Request): class CTFdRequest(Request):
@ -48,7 +48,7 @@ class CTFdFlask(Flask):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Overriden Jinja constructor setting a custom jinja_environment""" """Overriden Jinja constructor setting a custom jinja_environment"""
self.jinja_environment = SandboxedBaseEnvironment self.jinja_environment = SandboxedBaseEnvironment
self.session_interface = CachingSessionInterface(key_prefix='session') self.session_interface = CachingSessionInterface(key_prefix="session")
self.request_class = CTFdRequest self.request_class = CTFdRequest
Flask.__init__(self, *args, **kwargs) Flask.__init__(self, *args, **kwargs)
@ -59,9 +59,10 @@ class CTFdFlask(Flask):
class SandboxedBaseEnvironment(SandboxedEnvironment): class SandboxedBaseEnvironment(SandboxedEnvironment):
"""SandboxEnvironment that mimics the Flask BaseEnvironment""" """SandboxEnvironment that mimics the Flask BaseEnvironment"""
def __init__(self, app, **options): def __init__(self, app, **options):
if 'loader' not in options: if "loader" not in options:
options['loader'] = app.create_global_jinja_loader() options["loader"] = app.create_global_jinja_loader()
# Disable cache entirely so that themes can be switched (#662) # Disable cache entirely so that themes can be switched (#662)
# If the cache is enabled, switching themes will cause odd rendering errors # If the cache is enabled, switching themes will cause odd rendering errors
SandboxedEnvironment.__init__(self, cache_size=0, **options) SandboxedEnvironment.__init__(self, cache_size=0, **options)
@ -70,7 +71,8 @@ class SandboxedBaseEnvironment(SandboxedEnvironment):
class ThemeLoader(FileSystemLoader): class ThemeLoader(FileSystemLoader):
"""Custom FileSystemLoader that switches themes based on the configuration value""" """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) super(ThemeLoader, self).__init__(searchpath, encoding, followlinks)
self.overriden_templates = {} self.overriden_templates = {}
@ -80,14 +82,14 @@ class ThemeLoader(FileSystemLoader):
return self.overriden_templates[template], template, True return self.overriden_templates[template], template, True
# Check if the template requested is for the admin panel # 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 = template[6:] # Strip out admin/
template = "/".join(['admin', 'templates', template]) template = "/".join(["admin", "templates", template])
return super(ThemeLoader, self).get_source(environment, template) return super(ThemeLoader, self).get_source(environment, template)
# Load regular theme data # Load regular theme data
theme = utils.get_config('ctf_theme') theme = utils.get_config("ctf_theme")
template = "/".join([theme, 'templates', template]) template = "/".join([theme, "templates", template])
return super(ThemeLoader, self).get_source(environment, 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("/*\\ CTFd has updated and must update the database! /*\\")
print("/*\\ Please backup your database before proceeding! /*\\") print("/*\\ Please backup your database before proceeding! /*\\")
print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\") 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 return True
else: else:
print('/*\\ Ignored database migrations... /*\\') print("/*\\ Ignored database migrations... /*\\")
return False return False
else: else:
return True return True
@ -107,24 +109,36 @@ def confirm_upgrade():
def run_upgrade(): def run_upgrade():
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__) app = CTFdFlask(__name__)
with app.app_context(): with app.app_context():
app.config.from_object(config) 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 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() url = create_database()
# This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in # This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in
# This is mostly so we can force MySQL's charset # 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 # Register database
db.init_app(app) db.init_app(app)
@ -133,7 +147,7 @@ def create_app(config='CTFd.config.Config'):
migrations.init_app(app, db) migrations.init_app(app, db)
# Alembic sqlite support is lacking so we should just create_all anyway # 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() db.create_all()
stamp() stamp()
else: else:
@ -153,15 +167,11 @@ def create_app(config='CTFd.config.Config'):
cache.init_app(app) cache.init_app(app)
app.cache = cache app.cache = cache
reverse_proxy = app.config.get('REVERSE_PROXY') reverse_proxy = app.config.get("REVERSE_PROXY")
if reverse_proxy: if reverse_proxy:
if ',' in reverse_proxy: if "," in reverse_proxy:
proxyfix_args = [int(i) for i in reverse_proxy.split(',')] proxyfix_args = [int(i) for i in reverse_proxy.split(",")]
app.wsgi_app = ProxyFix( app.wsgi_app = ProxyFix(app.wsgi_app, None, *proxyfix_args)
app.wsgi_app,
None,
*proxyfix_args
)
else: else:
app.wsgi_app = ProxyFix( app.wsgi_app = ProxyFix(
app.wsgi_app, app.wsgi_app,
@ -170,10 +180,10 @@ def create_app(config='CTFd.config.Config'):
x_proto=1, x_proto=1,
x_host=1, x_host=1,
x_port=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 # Upgrading from an older version of CTFd
if version and (StrictVersion(version) < StrictVersion(__version__)): if version and (StrictVersion(version) < StrictVersion(__version__)):
@ -183,10 +193,10 @@ def create_app(config='CTFd.config.Config'):
exit() exit()
if not version: if not version:
utils.set_config('ctf_version', __version__) utils.set_config("ctf_version", __version__)
if not utils.get_config('ctf_theme'): if not utils.get_config("ctf_theme"):
utils.set_config('ctf_theme', 'core') utils.set_config("ctf_theme", "core")
update_check(force=True) update_check(force=True)

View File

@ -7,22 +7,18 @@ from flask import (
Blueprint, Blueprint,
abort, abort,
render_template_string, render_template_string,
send_file send_file,
) )
from CTFd.utils.decorators import admins_only from CTFd.utils.decorators import admins_only
from CTFd.utils.user import is_admin from CTFd.utils.user import is_admin
from CTFd.utils.security.auth import logout_user from CTFd.utils.security.auth import logout_user
from CTFd.utils import ( from CTFd.utils import config as ctf_config, get_config, set_config
config as ctf_config,
get_config,
set_config,
)
from CTFd.cache import cache, clear_config from CTFd.cache import cache, clear_config
from CTFd.utils.helpers import get_errors from CTFd.utils.helpers import get_errors
from CTFd.utils.exports import ( from CTFd.utils.exports import (
export_ctf as export_ctf_util, export_ctf as export_ctf_util,
import_ctf as import_ctf_util import_ctf as import_ctf_util,
) )
from CTFd.models import ( from CTFd.models import (
db, db,
@ -34,7 +30,7 @@ from CTFd.models import (
Solves, Solves,
Awards, Awards,
Unlocks, Unlocks,
Tracking Tracking,
) )
import datetime import datetime
import os import os
@ -42,7 +38,7 @@ import six
import csv import csv
admin = Blueprint('admin', __name__) admin = Blueprint("admin", __name__)
from CTFd.admin import challenges # noqa: F401 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 from CTFd.admin import notifications # noqa: F401
@admin.route('/admin', methods=['GET']) @admin.route("/admin", methods=["GET"])
def view(): def view():
if is_admin(): if is_admin():
return redirect(url_for('admin.statistics')) return redirect(url_for("admin.statistics"))
return redirect(url_for('auth.login')) return redirect(url_for("auth.login"))
@admin.route('/admin/plugins/<plugin>', methods=['GET', 'POST']) @admin.route("/admin/plugins/<plugin>", methods=["GET", "POST"])
@admins_only @admins_only
def plugin(plugin): def plugin(plugin):
if request.method == 'GET': if request.method == "GET":
plugins_path = os.path.join(app.root_path, 'plugins') plugins_path = os.path.join(app.root_path, "plugins")
config_html_plugins = [name for name in os.listdir(plugins_path) config_html_plugins = [
if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))] 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: 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) return render_template_string(config_html)
abort(404) abort(404)
elif request.method == 'POST': elif request.method == "POST":
for k, v in request.form.items(): for k, v in request.form.items():
if k == "nonce": if k == "nonce":
continue continue
set_config(k, v) set_config(k, v)
with app.app_context(): with app.app_context():
clear_config() clear_config()
return '1' return "1"
@admin.route('/admin/import', methods=['POST']) @admin.route("/admin/import", methods=["POST"])
@admins_only @admins_only
def import_ctf(): def import_ctf():
backup = request.files['backup'] backup = request.files["backup"]
errors = get_errors() errors = get_errors()
try: try:
import_ctf_util(backup) import_ctf_util(backup)
@ -99,10 +100,10 @@ def import_ctf():
if errors: if errors:
return errors[0], 500 return errors[0], 500
else: 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 @admins_only
def export_ctf(): def export_ctf():
backup = export_ctf_util() backup = export_ctf_util()
@ -112,10 +113,10 @@ def export_ctf():
return send_file(backup, as_attachment=True, attachment_filename=full_name) return send_file(backup, as_attachment=True, attachment_filename=full_name)
@admin.route('/admin/export/csv') @admin.route("/admin/export/csv")
@admins_only @admins_only
def export_csv(): 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. # TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
model = get_class_by_tablename(table) model = get_class_by_tablename(table)
@ -131,18 +132,22 @@ def export_csv():
responses = model.query.all() responses = model.query.all()
for curr in responses: 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) output.seek(0)
return send_file( return send_file(
output, output,
as_attachment=True, as_attachment=True,
cache_timeout=-1, 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 @admins_only
def config(): def config():
# Clear the config cache so that we don't get stale values # 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]) configs = dict([(c.key, get_config(c.key)) for c in configs])
themes = ctf_config.get_themes() themes = ctf_config.get_themes()
themes.remove(get_config('ctf_theme')) themes.remove(get_config("ctf_theme"))
return render_template( return render_template(
'admin/config.html', "admin/config.html", database_tables=database_tables, themes=themes, **configs
database_tables=database_tables,
themes=themes,
**configs
) )
@admin.route('/admin/reset', methods=['GET', 'POST']) @admin.route("/admin/reset", methods=["GET", "POST"])
@admins_only @admins_only
def reset(): def reset():
if request.method == 'POST': if request.method == "POST":
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking # Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
Tracking.query.delete() Tracking.query.delete()
Solves.query.delete() Solves.query.delete()
@ -176,11 +178,11 @@ def reset():
Unlocks.query.delete() Unlocks.query.delete()
Users.query.delete() Users.query.delete()
Teams.query.delete() Teams.query.delete()
set_config('setup', False) set_config("setup", False)
db.session.commit() db.session.commit()
cache.clear() cache.clear()
logout_user() logout_user()
db.session.close() 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")

View File

@ -8,44 +8,48 @@ import os
import six import six
@admin.route('/admin/challenges') @admin.route("/admin/challenges")
@admins_only @admins_only
def challenges_listing(): def challenges_listing():
challenges = Challenges.query.all() 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/<int:challenge_id>') @admin.route("/admin/challenges/<int:challenge_id>")
@admins_only @admins_only
def challenges_detail(challenge_id): 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() challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
solves = Solves.query.filter_by(challenge_id=challenge.id).all() solves = Solves.query.filter_by(challenge_id=challenge.id).all()
flags = Flags.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) 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() tpl = update.read()
if six.PY3 and isinstance(tpl, binary_type): if six.PY3 and isinstance(tpl, binary_type):
tpl = tpl.decode('utf-8') tpl = tpl.decode("utf-8")
update_j2 = render_template_string( update_j2 = render_template_string(tpl, challenge=challenge)
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( return render_template(
'admin/challenges/challenge.html', "admin/challenges/challenge.html",
update_template=update_j2, update_template=update_j2,
update_script=update_script, update_script=update_script,
challenge=challenge, challenge=challenge,
challenges=challenges, challenges=challenges,
solves=solves, solves=solves,
flags=flags flags=flags,
) )
@admin.route('/admin/challenges/new') @admin.route("/admin/challenges/new")
@admins_only @admins_only
def challenges_new(): def challenges_new():
return render_template('admin/challenges/new.html') return render_template("admin/challenges/new.html")

View File

@ -4,8 +4,8 @@ from CTFd.models import Notifications
from CTFd.admin import admin from CTFd.admin import admin
@admin.route('/admin/notifications') @admin.route("/admin/notifications")
@admins_only @admins_only
def notifications(): def notifications():
notifs = Notifications.query.order_by(Notifications.id.desc()).all() 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)

View File

@ -6,38 +6,38 @@ from CTFd.utils import markdown
from CTFd.admin import admin from CTFd.admin import admin
@admin.route('/admin/pages') @admin.route("/admin/pages")
@admins_only @admins_only
def pages_listing(): def pages_listing():
pages = Pages.query.all() 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 @admins_only
def pages_new(): 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 @admins_only
def pages_preview(): def pages_preview():
data = request.form.to_dict() data = request.form.to_dict()
schema = PageSchema() schema = PageSchema()
page = schema.load(data) 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/<int:page_id>') @admin.route("/admin/pages/<int:page_id>")
@admins_only @admins_only
def pages_detail(page_id): def pages_detail(page_id):
page = Pages.query.filter_by(id=page_id).first_or_404() 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': if request.method == "GET" and page_op == "preview":
return render_template('page.html', content=markdown(page.content)) return render_template("page.html", content=markdown(page.content))
if request.method == 'GET' and page_op == 'create': if request.method == "GET" and page_op == "create":
return render_template('admin/editor.html') return render_template("admin/editor.html")
return render_template('admin/editor.html', page=page) return render_template("admin/editor.html", page=page)

View File

@ -4,8 +4,8 @@ from CTFd.admin import admin
from CTFd.scoreboard import get_standings from CTFd.scoreboard import get_standings
@admin.route('/admin/scoreboard') @admin.route("/admin/scoreboard")
@admins_only @admins_only
def scoreboard_listing(): def scoreboard_listing():
standings = get_standings(admin=True) standings = get_standings(admin=True)
return render_template('admin/scoreboard.html', standings=standings) return render_template("admin/scoreboard.html", standings=standings)

View File

@ -6,7 +6,7 @@ from CTFd.models import db, Solves, Challenges, Fails, Tracking
from CTFd.admin import admin from CTFd.admin import admin
@admin.route('/admin/statistics', methods=['GET']) @admin.route("/admin/statistics", methods=["GET"])
@admins_only @admins_only
def statistics(): def statistics():
update_check() update_check()
@ -15,47 +15,41 @@ def statistics():
teams_registered = Model.query.count() teams_registered = Model.query.count()
wrong_count = Fails.query.join( wrong_count = (
Model, Fails.query.join(Model, Fails.account_id == Model.id)
Fails.account_id == Model.id .filter(Model.banned == False, Model.hidden == False)
).filter( .count()
Model.banned == False, )
Model.hidden == False
).count()
solve_count = Solves.query.join( solve_count = (
Model, Solves.query.join(Model, Solves.account_id == Model.id)
Solves.account_id == Model.id .filter(Model.banned == False, Model.hidden == False)
).filter( .count()
Model.banned == False, )
Model.hidden == False
).count()
challenge_count = Challenges.query.count() challenge_count = Challenges.query.count()
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count() ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
solves_sub = db.session.query( solves_sub = (
Solves.challenge_id, db.session.query(
db.func.count(Solves.challenge_id).label('solves_cnt') Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves_cnt")
).join( )
Model, .join(Model, Solves.account_id == Model.id)
Solves.account_id == Model.id .filter(Model.banned == False, Model.hidden == False)
).filter( .group_by(Solves.challenge_id)
Model.banned == False, .subquery()
Model.hidden == False )
).group_by(
Solves.challenge_id
).subquery()
solves = db.session.query( solves = (
db.session.query(
solves_sub.columns.challenge_id, solves_sub.columns.challenge_id,
solves_sub.columns.solves_cnt, solves_sub.columns.solves_cnt,
Challenges.name Challenges.name,
).join( )
Challenges, .join(Challenges, solves_sub.columns.challenge_id == Challenges.id)
solves_sub.columns.challenge_id == Challenges.id .all()
).all() )
solve_data = {} solve_data = {}
for chal, count, name in solves: for chal, count, name in solves:
@ -70,7 +64,7 @@ def statistics():
db.session.close() db.session.close()
return render_template( return render_template(
'admin/statistics.html', "admin/statistics.html",
team_count=teams_registered, team_count=teams_registered,
ip_count=ip_count, ip_count=ip_count,
wrong_count=wrong_count, wrong_count=wrong_count,
@ -78,5 +72,5 @@ def statistics():
challenge_count=challenge_count, challenge_count=challenge_count,
solve_data=solve_data, solve_data=solve_data,
most_solved=most_solved, most_solved=most_solved,
least_solved=least_solved least_solved=least_solved,
) )

View File

@ -5,15 +5,15 @@ from CTFd.utils.modes import get_model
from CTFd.admin import admin from CTFd.admin import admin
@admin.route('/admin/submissions', defaults={'submission_type': None}) @admin.route("/admin/submissions", defaults={"submission_type": None})
@admin.route('/admin/submissions/<submission_type>') @admin.route("/admin/submissions/<submission_type>")
@admins_only @admins_only
def submissions_listing(submission_type): def submissions_listing(submission_type):
filters = {} filters = {}
if submission_type: 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 results_per_page = 50
page_start = results_per_page * (curr_page - 1) page_start = results_per_page * (curr_page - 1)
page_end = results_per_page * (curr_page - 1) + results_per_page page_end = results_per_page * (curr_page - 1) + results_per_page
@ -22,27 +22,29 @@ def submissions_listing(submission_type):
Model = get_model() Model = get_model()
submissions = Submissions.query.add_columns( submissions = (
Submissions.query.add_columns(
Submissions.id, Submissions.id,
Submissions.type, Submissions.type,
Submissions.challenge_id, Submissions.challenge_id,
Submissions.provided, Submissions.provided,
Submissions.account_id, Submissions.account_id,
Submissions.date, Submissions.date,
Challenges.name.label('challenge_name'), Challenges.name.label("challenge_name"),
Model.name.label('team_name') Model.name.label("team_name"),
)\ )
.filter_by(**filters) \ .filter_by(**filters)
.join(Challenges)\ .join(Challenges)
.join(Model)\ .join(Model)
.order_by(Submissions.date.desc())\ .order_by(Submissions.date.desc())
.slice(page_start, page_end)\ .slice(page_start, page_end)
.all() .all()
)
return render_template( return render_template(
'admin/submissions.html', "admin/submissions.html",
submissions=submissions, submissions=submissions,
page_count=page_count, page_count=page_count,
curr_page=curr_page, curr_page=curr_page,
type=submission_type type=submission_type,
) )

View File

@ -7,28 +7,47 @@ from CTFd.utils.helpers import get_errors
from sqlalchemy.sql import not_ from sqlalchemy.sql import not_
@admin.route('/admin/teams') @admin.route("/admin/teams")
@admins_only @admins_only
def teams_listing(): def teams_listing():
page = abs(request.args.get('page', 1, type=int)) page = abs(request.args.get("page", 1, type=int))
q = request.args.get('q') q = request.args.get("q")
if q: if q:
field = request.args.get('field') field = request.args.get("field")
teams = [] teams = []
errors = get_errors() errors = get_errors()
if field == 'id': if field == "id":
if q.isnumeric(): if q.isnumeric():
teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all() teams = Teams.query.filter(Teams.id == q).order_by(Teams.id.asc()).all()
else: else:
teams = [] teams = []
errors.append('Your ID search term is not numeric') errors.append("Your ID search term is not numeric")
elif field == 'name': elif field == "name":
teams = Teams.query.filter(Teams.name.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() teams = (
elif field == 'email': Teams.query.filter(Teams.name.like("%{}%".format(q)))
teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all() .order_by(Teams.id.asc())
elif field == 'affiliation': .all()
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) 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)) page = abs(int(page))
results_per_page = 50 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() 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] count = db.session.query(db.func.count(Teams.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 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 @admins_only
def teams_new(): def teams_new():
return render_template('admin/teams/new.html') return render_template("admin/teams/new.html")
@admin.route('/admin/teams/<int:team_id>') @admin.route("/admin/teams/<int:team_id>")
@admins_only @admins_only
def teams_detail(team_id): def teams_detail(team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() 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() missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
# Get addresses for all members # Get addresses for all members
last_seen = db.func.max(Tracking.date).label('last_seen') last_seen = db.func.max(Tracking.date).label("last_seen")
addrs = db.session.query(Tracking.ip, last_seen) \ addrs = (
.filter(Tracking.user_id.in_(member_ids)) \ db.session.query(Tracking.ip, last_seen)
.group_by(Tracking.ip) \ .filter(Tracking.user_id.in_(member_ids))
.order_by(last_seen.desc()).all() .group_by(Tracking.ip)
.order_by(last_seen.desc())
.all()
)
return render_template( return render_template(
'admin/teams/team.html', "admin/teams/team.html",
team=team, team=team,
members=members, members=members,
score=score, score=score,

View File

@ -9,28 +9,47 @@ from CTFd.utils.helpers import get_errors
from sqlalchemy.sql import not_ from sqlalchemy.sql import not_
@admin.route('/admin/users') @admin.route("/admin/users")
@admins_only @admins_only
def users_listing(): def users_listing():
page = abs(request.args.get('page', 1, type=int)) page = abs(request.args.get("page", 1, type=int))
q = request.args.get('q') q = request.args.get("q")
if q: if q:
field = request.args.get('field') field = request.args.get("field")
users = [] users = []
errors = get_errors() errors = get_errors()
if field == 'id': if field == "id":
if q.isnumeric(): if q.isnumeric():
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all() users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
else: else:
users = [] users = []
errors.append('Your ID search term is not numeric') errors.append("Your ID search term is not numeric")
elif field == 'name': elif field == "name":
users = Users.query.filter(Users.name.like('%{}%'.format(q))).order_by(Users.id.asc()).all() users = (
elif field == 'email': Users.query.filter(Users.name.like("%{}%".format(q)))
users = Users.query.filter(Users.email.like('%{}%'.format(q))).order_by(Users.id.asc()).all() .order_by(Users.id.asc())
elif field == 'affiliation': .all()
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) 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)) page = abs(int(page))
results_per_page = 50 results_per_page = 50
@ -41,16 +60,18 @@ def users_listing():
count = db.session.query(db.func.count(Users.id)).first()[0] count = db.session.query(db.func.count(Users.id)).first()[0]
pages = int(count / results_per_page) + (count % results_per_page > 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 @admins_only
def users_new(): def users_new():
return render_template('admin/users/new.html') return render_template("admin/users/new.html")
@admin.route('/admin/users/<int:user_id>') @admin.route("/admin/users/<int:user_id>")
@admins_only @admins_only
def users_detail(user_id): def users_detail(user_id):
# Get user object # Get user object
@ -60,7 +81,7 @@ def users_detail(user_id):
solves = user.get_solves(admin=True) solves = user.get_solves(admin=True)
# Get challenges that the user is missing # Get challenges that the user is missing
if get_config('user_mode') == TEAMS_MODE: if get_config("user_mode") == TEAMS_MODE:
if user.team: if user.team:
all_solves = user.team.get_solves(admin=True) all_solves = user.team.get_solves(admin=True)
else: else:
@ -72,11 +93,14 @@ def users_detail(user_id):
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all() missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
# Get IP addresses that the User has used # Get IP addresses that the User has used
last_seen = db.func.max(Tracking.date).label('last_seen') last_seen = db.func.max(Tracking.date).label("last_seen")
addrs = db.session.query(Tracking.ip, last_seen) \ addrs = (
.filter_by(user_id=user_id) \ db.session.query(Tracking.ip, last_seen)
.group_by(Tracking.ip) \ .filter_by(user_id=user_id)
.order_by(last_seen.desc()).all() .group_by(Tracking.ip)
.order_by(last_seen.desc())
.all()
)
# Get Fails # Get Fails
fails = user.get_fails(admin=True) fails = user.get_fails(admin=True)
@ -89,7 +113,7 @@ def users_detail(user_id):
place = user.get_place(admin=True) place = user.get_place(admin=True)
return render_template( return render_template(
'admin/users/user.html', "admin/users/user.html",
solves=solves, solves=solves,
user=user, user=user,
addrs=addrs, addrs=addrs,
@ -97,5 +121,5 @@ def users_detail(user_id):
missing=missing, missing=missing,
place=place, place=place,
fails=fails, fails=fails,
awards=awards awards=awards,
) )

View File

@ -17,21 +17,21 @@ from CTFd.api.v1.notifications import notifications_namespace
from CTFd.api.v1.pages import pages_namespace from CTFd.api.v1.pages import pages_namespace
from CTFd.api.v1.unlocks import unlocks_namespace from CTFd.api.v1.unlocks import unlocks_namespace
api = Blueprint('api', __name__, url_prefix='/api/v1') 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 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI"))
CTFd_API_v1.add_namespace(challenges_namespace, '/challenges') CTFd_API_v1.add_namespace(challenges_namespace, "/challenges")
CTFd_API_v1.add_namespace(tags_namespace, '/tags') CTFd_API_v1.add_namespace(tags_namespace, "/tags")
CTFd_API_v1.add_namespace(awards_namespace, '/awards') CTFd_API_v1.add_namespace(awards_namespace, "/awards")
CTFd_API_v1.add_namespace(hints_namespace, '/hints') CTFd_API_v1.add_namespace(hints_namespace, "/hints")
CTFd_API_v1.add_namespace(flags_namespace, '/flags') CTFd_API_v1.add_namespace(flags_namespace, "/flags")
CTFd_API_v1.add_namespace(submissions_namespace, '/submissions') CTFd_API_v1.add_namespace(submissions_namespace, "/submissions")
CTFd_API_v1.add_namespace(scoreboard_namespace, '/scoreboard') CTFd_API_v1.add_namespace(scoreboard_namespace, "/scoreboard")
CTFd_API_v1.add_namespace(teams_namespace, '/teams') CTFd_API_v1.add_namespace(teams_namespace, "/teams")
CTFd_API_v1.add_namespace(users_namespace, '/users') CTFd_API_v1.add_namespace(users_namespace, "/users")
CTFd_API_v1.add_namespace(statistics_namespace, '/statistics') CTFd_API_v1.add_namespace(statistics_namespace, "/statistics")
CTFd_API_v1.add_namespace(files_namespace, '/files') CTFd_API_v1.add_namespace(files_namespace, "/files")
CTFd_API_v1.add_namespace(notifications_namespace, '/notifications') CTFd_API_v1.add_namespace(notifications_namespace, "/notifications")
CTFd_API_v1.add_namespace(configs_namespace, '/configs') CTFd_API_v1.add_namespace(configs_namespace, "/configs")
CTFd_API_v1.add_namespace(pages_namespace, '/pages') CTFd_API_v1.add_namespace(pages_namespace, "/pages")
CTFd_API_v1.add_namespace(unlocks_namespace, '/unlocks') CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks")

View File

@ -2,16 +2,13 @@ from flask import request
from flask_restplus import Namespace, Resource from flask_restplus import Namespace, Resource
from CTFd.models import db, Awards from CTFd.models import db, Awards
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class AwardList(Resource):
@admins_only @admins_only
def post(self): def post(self):
req = request.get_json() req = request.get_json()
@ -19,10 +16,7 @@ class AwardList(Resource):
response = schema.load(req, session=db.session) response = schema.load(req, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -30,29 +24,20 @@ class AwardList(Resource):
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@awards_namespace.route('/<award_id>') @awards_namespace.route("/<award_id>")
@awards_namespace.param('award_id', 'An Award ID') @awards_namespace.param("award_id", "An Award ID")
class Award(Resource): class Award(Resource):
@admins_only @admins_only
def get(self, award_id): def get(self, award_id):
award = Awards.query.filter_by(id=award_id).first_or_404() award = Awards.query.filter_by(id=award_id).first_or_404()
response = AwardSchema().dump(award) response = AwardSchema().dump(award)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, award_id): def delete(self, award_id):
@ -61,6 +46,4 @@ class Award(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True,
}

View File

@ -16,14 +16,18 @@ from CTFd.utils.dates import isoformat
from CTFd.utils.decorators import ( from CTFd.utils.decorators import (
during_ctf_time_only, during_ctf_time_only,
require_verified_emails, require_verified_emails,
admins_only admins_only,
) )
from CTFd.utils.decorators.visibility import ( from CTFd.utils.decorators.visibility import (
check_challenge_visibility, check_challenge_visibility,
check_score_visibility check_score_visibility,
) )
from CTFd.cache import clear_standings 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.user import is_admin, authed
from CTFd.utils.modes import get_model, USERS_MODE, TEAMS_MODE from CTFd.utils.modes import get_model, USERS_MODE, TEAMS_MODE
from CTFd.schemas.tags import TagSchema from CTFd.schemas.tags import TagSchema
@ -40,11 +44,12 @@ from CTFd.utils.security.signing import serialize
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
import datetime import datetime
challenges_namespace = Namespace('challenges', challenges_namespace = Namespace(
description="Endpoint to retrieve Challenges") "challenges", description="Endpoint to retrieve Challenges"
)
@challenges_namespace.route('') @challenges_namespace.route("")
class ChallengeList(Resource): class ChallengeList(Resource):
@check_challenge_visibility @check_challenge_visibility
@during_ctf_time_only @during_ctf_time_only
@ -53,16 +58,21 @@ class ChallengeList(Resource):
# This can return None (unauth) if visibility is set to public # This can return None (unauth) if visibility is set to public
user = get_current_user() user = get_current_user()
challenges = Challenges.query.filter( challenges = (
and_(Challenges.state != 'hidden', Challenges.state != 'locked') Challenges.query.filter(
).order_by(Challenges.value).all() and_(Challenges.state != "hidden", Challenges.state != "locked")
)
.order_by(Challenges.value)
.all()
)
if user: if user:
solve_ids = Solves.query\ solve_ids = (
.with_entities(Solves.challenge_id)\ Solves.query.with_entities(Solves.challenge_id)
.filter_by(account_id=user.account_id)\ .filter_by(account_id=user.account_id)
.order_by(Solves.challenge_id.asc())\ .order_by(Solves.challenge_id.asc())
.all() .all()
)
solve_ids = set([value for value, in solve_ids]) solve_ids = set([value for value, in solve_ids])
# TODO: Convert this into a re-useable decorator # TODO: Convert this into a re-useable decorator
@ -75,61 +85,59 @@ class ChallengeList(Resource):
solve_ids = set() solve_ids = set()
response = [] response = []
tag_schema = TagSchema(view='user', many=True) tag_schema = TagSchema(view="user", many=True)
for challenge in challenges: for challenge in challenges:
if challenge.requirements: if challenge.requirements:
requirements = challenge.requirements.get('prerequisites', []) requirements = challenge.requirements.get("prerequisites", [])
anonymize = challenge.requirements.get('anonymize') anonymize = challenge.requirements.get("anonymize")
prereqs = set(requirements) prereqs = set(requirements)
if solve_ids >= prereqs: if solve_ids >= prereqs:
pass pass
else: else:
if anonymize: if anonymize:
response.append({ response.append(
'id': challenge.id, {
'type': 'hidden', "id": challenge.id,
'name': '???', "type": "hidden",
'value': 0, "name": "???",
'category': '???', "value": 0,
'tags': [], "category": "???",
'template': '', "tags": [],
'script': '' "template": "",
}) "script": "",
}
)
# Fallthrough to continue # Fallthrough to continue
continue continue
challenge_type = get_chal_class(challenge.type) challenge_type = get_chal_class(challenge.type)
response.append({ response.append(
'id': challenge.id, {
'type': challenge_type.name, "id": challenge.id,
'name': challenge.name, "type": challenge_type.name,
'value': challenge.value, "name": challenge.name,
'category': challenge.category, "value": challenge.value,
'tags': tag_schema.dump(challenge.tags).data, "category": challenge.category,
'template': challenge_type.templates['view'], "tags": tag_schema.dump(challenge.tags).data,
'script': challenge_type.scripts['view'], "template": challenge_type.templates["view"],
}) "script": challenge_type.scripts["view"],
}
)
db.session.close() db.session.close()
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@admins_only @admins_only
def post(self): def post(self):
data = request.form or request.get_json() data = request.form or request.get_json()
challenge_type = data['type'] challenge_type = data["type"]
challenge_class = get_chal_class(challenge_type) challenge_class = get_chal_class(challenge_type)
challenge = challenge_class.create(request) challenge = challenge_class.create(request)
response = challenge_class.read(challenge) response = challenge_class.read(challenge)
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@challenges_namespace.route('/types') @challenges_namespace.route("/types")
class ChallengeTypes(Resource): class ChallengeTypes(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -138,19 +146,16 @@ class ChallengeTypes(Resource):
for class_id in CHALLENGE_CLASSES: for class_id in CHALLENGE_CLASSES:
challenge_class = CHALLENGE_CLASSES.get(class_id) challenge_class = CHALLENGE_CLASSES.get(class_id)
response[challenge_class.id] = { response[challenge_class.id] = {
'id': challenge_class.id, "id": challenge_class.id,
'name': challenge_class.name, "name": challenge_class.name,
'templates': challenge_class.templates, "templates": challenge_class.templates,
'scripts': challenge_class.scripts, "scripts": challenge_class.scripts,
}
return {
'success': True,
'data': response
} }
return {"success": True, "data": response}
@challenges_namespace.route('/<challenge_id>') @challenges_namespace.route("/<challenge_id>")
@challenges_namespace.param('challenge_id', 'A Challenge ID') @challenges_namespace.param("challenge_id", "A Challenge ID")
class Challenge(Resource): class Challenge(Resource):
@check_challenge_visibility @check_challenge_visibility
@during_ctf_time_only @during_ctf_time_only
@ -160,22 +165,24 @@ class Challenge(Resource):
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404() chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
else: else:
chal = Challenges.query.filter( 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() ).first_or_404()
chal_class = get_chal_class(chal.type) chal_class = get_chal_class(chal.type)
if chal.requirements: if chal.requirements:
requirements = chal.requirements.get('prerequisites', []) requirements = chal.requirements.get("prerequisites", [])
anonymize = chal.requirements.get('anonymize') anonymize = chal.requirements.get("anonymize")
if challenges_visible(): if challenges_visible():
user = get_current_user() user = get_current_user()
if user: if user:
solve_ids = Solves.query \ solve_ids = (
.with_entities(Solves.challenge_id) \ Solves.query.with_entities(Solves.challenge_id)
.filter_by(account_id=user.account_id) \ .filter_by(account_id=user.account_id)
.order_by(Solves.challenge_id.asc()) \ .order_by(Solves.challenge_id.asc())
.all() .all()
)
else: else:
# We need to handle the case where a user is viewing challenges anonymously # We need to handle the case where a user is viewing challenges anonymously
solve_ids = [] solve_ids = []
@ -186,26 +193,24 @@ class Challenge(Resource):
else: else:
if anonymize: if anonymize:
return { return {
'success': True, "success": True,
'data': { "data": {
'id': chal.id, "id": chal.id,
'type': 'hidden', "type": "hidden",
'name': '???', "name": "???",
'value': 0, "value": 0,
'category': '???', "category": "???",
'tags': [], "tags": [],
'template': '', "template": "",
'script': '' "script": "",
} },
} }
abort(403) abort(403)
else: else:
abort(403) abort(403)
tags = [ tags = [
tag['value'] for tag in TagSchema( tag["value"] for tag in TagSchema("user", many=True).dump(chal.tags).data
"user", many=True).dump(
chal.tags).data
] ]
unlocked_hints = set() unlocked_hints = set()
@ -221,56 +226,59 @@ class Challenge(Resource):
if config.is_teams_mode() and team is None: if config.is_teams_mode() and team is None:
abort(403) abort(403)
unlocked_hints = set([ unlocked_hints = set(
u.target for u in HintUnlocks.query.filter_by(type='hints', account_id=user.account_id) [
]) u.target
for u in HintUnlocks.query.filter_by(
type="hints", account_id=user.account_id
)
]
)
files = [] files = []
for f in chal.files: for f in chal.files:
token = { token = {
'user_id': user.id, "user_id": user.id,
'team_id': team.id if team else None, "team_id": team.id if team else None,
'file_id': f.id, "file_id": f.id,
} }
files.append( files.append(
url_for('views.files', path=f.location, token=serialize(token)) url_for("views.files", path=f.location, token=serialize(token))
) )
else: else:
files = [ files = [url_for("views.files", path=f.location) for f in chal.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(): for hint in Hints.query.filter_by(challenge_id=chal.id).all():
if hint.id in unlocked_hints or ctf_ended(): if hint.id in unlocked_hints or ctf_ended():
hints.append({ hints.append(
'id': hint.id, {"id": hint.id, "cost": hint.cost, "content": hint.content}
'cost': hint.cost, )
'content': hint.content
})
else: else:
hints.append({'id': hint.id, 'cost': hint.cost}) hints.append({"id": hint.id, "cost": hint.cost})
response = chal_class.read(challenge=chal) response = chal_class.read(challenge=chal)
Model = get_model() Model = get_model()
if scores_visible() is True and accounts_visible() is True: if scores_visible() is True and accounts_visible() is True:
solves = Solves.query\ solves = (
.join(Model, Solves.account_id == Model.id)\ Solves.query.join(Model, Solves.account_id == Model.id)
.filter(Solves.challenge_id == chal.id, Model.banned == False, Model.hidden == False)\ .filter(
Solves.challenge_id == chal.id,
Model.banned == False,
Model.hidden == False,
)
.count() .count()
response['solves'] = solves )
response["solves"] = solves
else: else:
response['solves'] = None response["solves"] = None
response['files'] = files response["files"] = files
response['tags'] = tags response["tags"] = tags
response['hints'] = hints response["hints"] = hints
db.session.close() db.session.close()
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@admins_only @admins_only
def patch(self, challenge_id): def patch(self, challenge_id):
@ -278,10 +286,7 @@ class Challenge(Resource):
challenge_class = get_chal_class(challenge.type) challenge_class = get_chal_class(challenge.type)
challenge = challenge_class.update(challenge, request) challenge = challenge_class.update(challenge, request)
response = challenge_class.read(challenge) response = challenge_class.read(challenge)
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@admins_only @admins_only
def delete(self, challenge_id): def delete(self, challenge_id):
@ -289,55 +294,51 @@ class Challenge(Resource):
chal_class = get_chal_class(challenge.type) chal_class = get_chal_class(challenge.type)
chal_class.delete(challenge) chal_class.delete(challenge)
return { return {"success": True}
'success': True,
}
@challenges_namespace.route('/attempt') @challenges_namespace.route("/attempt")
class ChallengeAttempt(Resource): class ChallengeAttempt(Resource):
@check_challenge_visibility @check_challenge_visibility
@during_ctf_time_only @during_ctf_time_only
@require_verified_emails @require_verified_emails
def post(self): def post(self):
if authed() is False: if authed() is False:
return { return {"success": True, "data": {"status": "authentication_required"}}, 403
'success': True,
'data': {
'status': "authentication_required",
}
}, 403
if request.content_type != 'application/json': if request.content_type != "application/json":
request_data = request.form request_data = request.form
else: else:
request_data = request.get_json() request_data = request.get_json()
challenge_id = request_data.get('challenge_id') challenge_id = request_data.get("challenge_id")
if current_user.is_admin(): if current_user.is_admin():
preview = request.args.get('preview', False) preview = request.args.get("preview", False)
if preview: if preview:
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404() challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
chal_class = get_chal_class(challenge.type) chal_class = get_chal_class(challenge.type)
status, message = chal_class.attempt(challenge, request) status, message = chal_class.attempt(challenge, request)
return { return {
'success': True, "success": True,
'data': { "data": {
'status': "correct" if status else "incorrect", "status": "correct" if status else "incorrect",
'message': message "message": message,
} },
} }
if ctf_paused(): if ctf_paused():
return { return (
'success': True, {
'data': { "success": True,
'status': "paused", "data": {
'message': '{} is paused'.format(config.ctf_name()) "status": "paused",
} "message": "{} is paused".format(config.ctf_name()),
}, 403 },
},
403,
)
user = get_current_user() user = get_current_user()
team = get_current_team() team = get_current_team()
@ -347,26 +348,25 @@ class ChallengeAttempt(Resource):
abort(403) abort(403)
fails = Fails.query.filter_by( fails = Fails.query.filter_by(
account_id=user.account_id, account_id=user.account_id, challenge_id=challenge_id
challenge_id=challenge_id
).count() ).count()
challenge = Challenges.query.filter_by( challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
id=challenge_id).first_or_404()
if challenge.state == 'hidden': if challenge.state == "hidden":
abort(404) abort(404)
if challenge.state == 'locked': if challenge.state == "locked":
abort(403) abort(403)
if challenge.requirements: if challenge.requirements:
requirements = challenge.requirements.get('prerequisites', []) requirements = challenge.requirements.get("prerequisites", [])
solve_ids = Solves.query \ solve_ids = (
.with_entities(Solves.challenge_id) \ Solves.query.with_entities(Solves.challenge_id)
.filter_by(account_id=user.account_id) \ .filter_by(account_id=user.account_id)
.order_by(Solves.challenge_id.asc()) \ .order_by(Solves.challenge_id.asc())
.all() .all()
)
solve_ids = set([solve_id for solve_id, in solve_ids]) solve_ids = set([solve_id for solve_id, in solve_ids])
prereqs = set(requirements) prereqs = set(requirements)
if solve_ids >= prereqs: if solve_ids >= prereqs:
@ -381,29 +381,28 @@ class ChallengeAttempt(Resource):
if kpm > 10: if kpm > 10:
if ctftime(): if ctftime():
chal_class.fail( chal_class.fail(
user=user, user=user, team=team, challenge=challenge, request=request
team=team,
challenge=challenge,
request=request
) )
log( log(
'submissions', "submissions",
"[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]", "[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]",
submission=request_data['submission'].encode('utf-8'), submission=request_data["submission"].encode("utf-8"),
kpm=kpm kpm=kpm,
) )
# Submitting too fast # Submitting too fast
return { return (
'success': True, {
'data': { "success": True,
'status': "ratelimited", "data": {
'message': "You're submitting flags too fast. Slow down." "status": "ratelimited",
} "message": "You're submitting flags too fast. Slow down.",
}, 429 },
},
429,
)
solves = Solves.query.filter_by( solves = Solves.query.filter_by(
account_id=user.account_id, account_id=user.account_id, challenge_id=challenge_id
challenge_id=challenge_id
).first() ).first()
# Challenge not solved yet # Challenge not solved yet
@ -411,99 +410,92 @@ class ChallengeAttempt(Resource):
# Hit max attempts # Hit max attempts
max_tries = challenge.max_attempts max_tries = challenge.max_attempts
if max_tries and fails >= max_tries > 0: if max_tries and fails >= max_tries > 0:
return { return (
'success': True, {
'data': { "success": True,
'status': "incorrect", "data": {
'message': "You have 0 tries remaining" "status": "incorrect",
} "message": "You have 0 tries remaining",
}, 403 },
},
403,
)
status, message = chal_class.attempt(challenge, request) status, message = chal_class.attempt(challenge, request)
if status: # The challenge plugin says the input is right if status: # The challenge plugin says the input is right
if ctftime() or current_user.is_admin(): if ctftime() or current_user.is_admin():
chal_class.solve( chal_class.solve(
user=user, user=user, team=team, challenge=challenge, request=request
team=team,
challenge=challenge,
request=request
) )
clear_standings() clear_standings()
log( log(
'submissions', "submissions",
"[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]", "[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]",
submission=request_data['submission'].encode('utf-8'), submission=request_data["submission"].encode("utf-8"),
kpm=kpm kpm=kpm,
) )
return { return {
'success': True, "success": True,
'data': { "data": {"status": "correct", "message": message},
'status': "correct",
'message': message
}
} }
else: # The challenge plugin says the input is wrong else: # The challenge plugin says the input is wrong
if ctftime() or current_user.is_admin(): if ctftime() or current_user.is_admin():
chal_class.fail( chal_class.fail(
user=user, user=user, team=team, challenge=challenge, request=request
team=team,
challenge=challenge,
request=request
) )
clear_standings() clear_standings()
log( log(
'submissions', "submissions",
"[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]", "[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]",
submission=request_data['submission'].encode('utf-8'), submission=request_data["submission"].encode("utf-8"),
kpm=kpm kpm=kpm,
) )
if max_tries: if max_tries:
# Off by one since fails has changed since it was gotten # Off by one since fails has changed since it was gotten
attempts_left = max_tries - fails - 1 attempts_left = max_tries - fails - 1
tries_str = 'tries' tries_str = "tries"
if attempts_left == 1: if attempts_left == 1:
tries_str = 'try' tries_str = "try"
# Add a punctuation mark if there isn't one # Add a punctuation mark if there isn't one
if message[-1] not in '!().;?[]{}': if message[-1] not in "!().;?[]{}":
message = message + '.' message = message + "."
return { return {
'success': True, "success": True,
'data': { "data": {
'status': "incorrect", "status": "incorrect",
'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str) "message": "{} You have {} {} remaining.".format(
} message, attempts_left, tries_str
),
},
} }
else: else:
return { return {
'success': True, "success": True,
'data': { "data": {"status": "incorrect", "message": message},
'status': "incorrect",
'message': message
}
} }
# Challenge already solved # Challenge already solved
else: else:
log( log(
'submissions', "submissions",
"[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]", "[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]",
submission=request_data['submission'].encode('utf-8'), submission=request_data["submission"].encode("utf-8"),
kpm=kpm kpm=kpm,
) )
return { return {
'success': True, "success": True,
'data': { "data": {
'status': "already_solved", "status": "already_solved",
'message': 'You already solved this' "message": "You already solved this",
} },
} }
@challenges_namespace.route('/<challenge_id>/solves') @challenges_namespace.route("/<challenge_id>/solves")
@challenges_namespace.param('id', 'A Challenge ID') @challenges_namespace.param("id", "A Challenge ID")
class ChallengeSolves(Resource): class ChallengeSolves(Resource):
@check_challenge_visibility @check_challenge_visibility
@check_score_visibility @check_score_visibility
@ -515,68 +507,67 @@ class ChallengeSolves(Resource):
# TODO: Need a generic challenge visibility call. # TODO: Need a generic challenge visibility call.
# However, it should be stated that a solve on a gated challenge is not considered private. # 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) abort(404)
Model = get_model() Model = get_model()
solves = Solves.query.join(Model, Solves.account_id == Model.id)\ solves = (
.filter(Solves.challenge_id == challenge_id, Model.banned == False, Model.hidden == False)\ 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()) .order_by(Solves.date.asc())
)
freeze = get_config('freeze') freeze = get_config("freeze")
if 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): if (is_admin() is False) or (is_admin() is True and preview):
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt) solves = solves.filter(Solves.date < dt)
endpoint = None endpoint = None
if get_config('user_mode') == TEAMS_MODE: if get_config("user_mode") == TEAMS_MODE:
endpoint = 'teams.public' endpoint = "teams.public"
arg = 'team_id' arg = "team_id"
elif get_config('user_mode') == USERS_MODE: elif get_config("user_mode") == USERS_MODE:
endpoint = 'users.public' endpoint = "users.public"
arg = 'user_id' arg = "user_id"
for solve in solves: for solve in solves:
response.append({ response.append(
'account_id': solve.account_id, {
'name': solve.account.name, "account_id": solve.account_id,
'date': isoformat(solve.date), "name": solve.account.name,
'account_url': url_for(endpoint, **{arg: solve.account_id}) "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('/<challenge_id>/files') @challenges_namespace.route("/<challenge_id>/files")
@challenges_namespace.param('id', 'A Challenge ID') @challenges_namespace.param("id", "A Challenge ID")
class ChallengeFiles(Resource): class ChallengeFiles(Resource):
@admins_only @admins_only
def get(self, challenge_id): def get(self, challenge_id):
response = [] response = []
challenge_files = ChallengeFilesModel.query.filter_by( challenge_files = ChallengeFilesModel.query.filter_by(
challenge_id=challenge_id).all() challenge_id=challenge_id
).all()
for f in challenge_files: for f in challenge_files:
response.append({ response.append({"id": f.id, "type": f.type, "location": f.location})
'id': f.id, return {"success": True, "data": response}
'type': f.type,
'location': f.location
})
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>/tags') @challenges_namespace.route("/<challenge_id>/tags")
@challenges_namespace.param('id', 'A Challenge ID') @challenges_namespace.param("id", "A Challenge ID")
class ChallengeTags(Resource): class ChallengeTags(Resource):
@admins_only @admins_only
def get(self, challenge_id): def get(self, challenge_id):
@ -585,19 +576,14 @@ class ChallengeTags(Resource):
tags = Tags.query.filter_by(challenge_id=challenge_id).all() tags = Tags.query.filter_by(challenge_id=challenge_id).all()
for t in tags: for t in tags:
response.append({ response.append(
'id': t.id, {"id": t.id, "challenge_id": t.challenge_id, "value": t.value}
'challenge_id': t.challenge_id, )
'value': t.value return {"success": True, "data": response}
})
return {
'success': True,
'data': response
}
@challenges_namespace.route('/<challenge_id>/hints') @challenges_namespace.route("/<challenge_id>/hints")
@challenges_namespace.param('id', 'A Challenge ID') @challenges_namespace.param("id", "A Challenge ID")
class ChallengeHints(Resource): class ChallengeHints(Resource):
@admins_only @admins_only
def get(self, challenge_id): def get(self, challenge_id):
@ -606,19 +592,13 @@ class ChallengeHints(Resource):
response = schema.dump(hints) response = schema.dump(hints)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@challenges_namespace.route('/<challenge_id>/flags') @challenges_namespace.route("/<challenge_id>/flags")
@challenges_namespace.param('id', 'A Challenge ID') @challenges_namespace.param("id", "A Challenge ID")
class ChallengeFlags(Resource): class ChallengeFlags(Resource):
@admins_only @admins_only
def get(self, challenge_id): def get(self, challenge_id):
@ -627,12 +607,6 @@ class ChallengeFlags(Resource):
response = schema.dump(flags) response = schema.dump(flags)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}

View File

@ -2,16 +2,14 @@ from flask import request
from flask_restplus import Namespace, Resource from flask_restplus import Namespace, Resource
from CTFd.models import db, Configs from CTFd.models import db, Configs
from CTFd.schemas.config import ConfigSchema from CTFd.schemas.config import ConfigSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
admins_only
)
from CTFd.utils import get_config, set_config from CTFd.utils import get_config, set_config
from CTFd.cache import clear_config, clear_standings 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): class ConfigList(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -19,15 +17,9 @@ class ConfigList(Resource):
schema = ConfigSchema(many=True) schema = ConfigSchema(many=True)
response = schema.dump(configs) response = schema.dump(configs)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors,
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
@ -36,10 +28,7 @@ class ConfigList(Resource):
response = schema.load(req) response = schema.load(req)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -50,10 +39,7 @@ class ConfigList(Resource):
clear_config() clear_config()
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self): def patch(self):
@ -65,20 +51,15 @@ class ConfigList(Resource):
clear_config() clear_config()
clear_standings() clear_standings()
return { return {"success": True}
'success': True
}
@configs_namespace.route('/<config_key>') @configs_namespace.route("/<config_key>")
class Config(Resource): class Config(Resource):
@admins_only @admins_only
def get(self, config_key): def get(self, config_key):
return { return {"success": True, "data": get_config(config_key)}
'success': True,
'data': get_config(config_key)
}
@admins_only @admins_only
def patch(self, config_key): def patch(self, config_key):
@ -89,7 +70,7 @@ class Config(Resource):
response = schema.load(data) response = schema.load(data)
else: else:
schema = ConfigSchema() schema = ConfigSchema()
data['key'] = config_key data["key"] = config_key
response = schema.load(data) response = schema.load(data)
db.session.add(response.data) db.session.add(response.data)
@ -104,10 +85,7 @@ class Config(Resource):
clear_config() clear_config()
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, config_key): def delete(self, config_key):
@ -120,6 +98,4 @@ class Config(Resource):
clear_config() clear_config()
clear_standings() clear_standings()
return { return {"success": True}
'success': True,
}

View File

@ -3,36 +3,28 @@ from flask_restplus import Namespace, Resource
from CTFd.models import db, Files from CTFd.models import db, Files
from CTFd.schemas.files import FileSchema from CTFd.schemas.files import FileSchema
from CTFd.utils import uploads from CTFd.utils import uploads
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class FilesList(Resource):
@admins_only @admins_only
def get(self): def get(self):
file_type = request.args.get('type') file_type = request.args.get("type")
files = Files.query.filter_by(type=file_type).all() files = Files.query.filter_by(type=file_type).all()
schema = FileSchema(many=True) schema = FileSchema(many=True)
response = schema.dump(files) response = schema.dump(files)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
files = request.files.getlist('file') files = request.files.getlist("file")
# challenge_id # challenge_id
# page_id # page_id
@ -46,18 +38,12 @@ class FilesList(Resource):
response = schema.dump(objs) response = schema.dump(objs)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errorss}, 400
'success': False,
'errors': response.errorss
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@files_namespace.route('/<file_id>') @files_namespace.route("/<file_id>")
class FilesDetail(Resource): class FilesDetail(Resource):
@admins_only @admins_only
def get(self, file_id): def get(self, file_id):
@ -66,15 +52,9 @@ class FilesDetail(Resource):
response = schema.dump(f) response = schema.dump(f)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, file_id): def delete(self, file_id):
@ -84,6 +64,4 @@ class FilesDetail(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True,
}

View File

@ -3,14 +3,12 @@ from flask_restplus import Namespace, Resource
from CTFd.models import db, Flags from CTFd.models import db, Flags
from CTFd.schemas.flags import FlagSchema from CTFd.schemas.flags import FlagSchema
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class FlagList(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -18,15 +16,9 @@ class FlagList(Resource):
schema = FlagSchema(many=True) schema = FlagSchema(many=True)
response = schema.dump(flags) response = schema.dump(flags)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
@ -35,10 +27,7 @@ class FlagList(Resource):
response = schema.load(req, session=db.session) response = schema.load(req, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -46,42 +35,30 @@ class FlagList(Resource):
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@flags_namespace.route('/types', defaults={'type_name': None}) @flags_namespace.route("/types", defaults={"type_name": None})
@flags_namespace.route('/types/<type_name>') @flags_namespace.route("/types/<type_name>")
class FlagTypes(Resource): class FlagTypes(Resource):
@admins_only @admins_only
def get(self, type_name): def get(self, type_name):
if type_name: if type_name:
flag_class = get_flag_class(type_name) flag_class = get_flag_class(type_name)
response = { response = {"name": flag_class.name, "templates": flag_class.templates}
'name': flag_class.name, return {"success": True, "data": response}
'templates': flag_class.templates
}
return {
'success': True,
'data': response
}
else: else:
response = {} response = {}
for class_id in FLAG_CLASSES: for class_id in FLAG_CLASSES:
flag_class = FLAG_CLASSES.get(class_id) flag_class = FLAG_CLASSES.get(class_id)
response[class_id] = { response[class_id] = {
'name': flag_class.name, "name": flag_class.name,
'templates': flag_class.templates, "templates": flag_class.templates,
}
return {
'success': True,
'data': response
} }
return {"success": True, "data": response}
@flags_namespace.route('/<flag_id>') @flags_namespace.route("/<flag_id>")
class Flag(Resource): class Flag(Resource):
@admins_only @admins_only
def get(self, flag_id): def get(self, flag_id):
@ -90,17 +67,11 @@ class Flag(Resource):
response = schema.dump(flag) response = schema.dump(flag)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'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 { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, flag_id): def delete(self, flag_id):
@ -110,9 +81,7 @@ class Flag(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True
}
@admins_only @admins_only
def patch(self, flag_id): def patch(self, flag_id):
@ -123,17 +92,11 @@ class Flag(Resource):
response = schema.load(req, session=db.session, instance=flag, partial=True) response = schema.load(req, session=db.session, instance=flag, partial=True)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}

View File

@ -3,16 +3,12 @@ from flask_restplus import Namespace, Resource
from CTFd.models import db, Hints, HintUnlocks from CTFd.models import db, Hints, HintUnlocks
from CTFd.utils.user import get_current_user, is_admin from CTFd.utils.user import get_current_user, is_admin
from CTFd.schemas.hints import HintSchema from CTFd.schemas.hints import HintSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import during_ctf_time_only, admins_only, authed_only
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): class HintList(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -20,40 +16,28 @@ class HintList(Resource):
response = HintSchema(many=True).dump(hints) response = HintSchema(many=True).dump(hints)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
req = request.get_json() req = request.get_json()
schema = HintSchema('admin') schema = HintSchema("admin")
response = schema.load(req, session=db.session) response = schema.load(req, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
response = schema.dump(response.data) response = schema.dump(response.data)
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@hints_namespace.route('/<hint_id>') @hints_namespace.route("/<hint_id>")
class Hint(Resource): class Hint(Resource):
@during_ctf_time_only @during_ctf_time_only
@authed_only @authed_only
@ -61,32 +45,25 @@ class Hint(Resource):
user = get_current_user() user = get_current_user()
hint = Hints.query.filter_by(id=hint_id).first_or_404() hint = Hints.query.filter_by(id=hint_id).first_or_404()
view = 'unlocked' view = "unlocked"
if hint.cost: if hint.cost:
view = 'locked' view = "locked"
unlocked = HintUnlocks.query.filter_by( unlocked = HintUnlocks.query.filter_by(
account_id=user.account_id, account_id=user.account_id, target=hint.id
target=hint.id
).first() ).first()
if unlocked: if unlocked:
view = 'unlocked' view = "unlocked"
if is_admin(): if is_admin():
if request.args.get('preview', False): if request.args.get("preview", False):
view = 'admin' view = "admin"
response = HintSchema(view=view).dump(hint) response = HintSchema(view=view).dump(hint)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self, hint_id): def patch(self, hint_id):
@ -97,20 +74,14 @@ class Hint(Resource):
response = schema.load(req, instance=hint, partial=True, session=db.session) response = schema.load(req, instance=hint, partial=True, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
response = schema.dump(response.data) response = schema.dump(response.data)
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, hint_id): def delete(self, hint_id):
@ -119,6 +90,4 @@ class Hint(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True
}

View File

@ -3,28 +3,22 @@ from flask_restplus import Namespace, Resource
from CTFd.models import db, Notifications from CTFd.models import db, Notifications
from CTFd.schemas.notifications import NotificationSchema from CTFd.schemas.notifications import NotificationSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class NotificantionList(Resource):
def get(self): def get(self):
notifications = Notifications.query.all() notifications = Notifications.query.all()
schema = NotificationSchema(many=True) schema = NotificationSchema(many=True)
result = schema.dump(notifications) result = schema.dump(notifications)
if result.errors: if result.errors:
return { return {"success": False, "errors": result.errors}, 400
'success': False, return {"success": True, "data": result.data}
'errors': result.errors
}, 400
return {
'success': True,
'data': result.data
}
@admins_only @admins_only
def post(self): def post(self):
@ -34,42 +28,28 @@ class NotificantionList(Resource):
result = schema.load(req) result = schema.load(req)
if result.errors: if result.errors:
return { return {"success": False, "errors": result.errors}, 400
'success': False,
'errors': result.errors
}, 400
db.session.add(result.data) db.session.add(result.data)
db.session.commit() db.session.commit()
response = schema.dump(result.data) response = schema.dump(result.data)
current_app.events_manager.publish( current_app.events_manager.publish(data=response.data, type="notification")
data=response.data, type='notification'
)
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@notifications_namespace.route('/<notification_id>') @notifications_namespace.route("/<notification_id>")
@notifications_namespace.param('notification_id', 'A Notification ID') @notifications_namespace.param("notification_id", "A Notification ID")
class Notification(Resource): class Notification(Resource):
def get(self, notification_id): def get(self, notification_id):
notif = Notifications.query.filter_by(id=notification_id).first_or_404() notif = Notifications.query.filter_by(id=notification_id).first_or_404()
schema = NotificationSchema() schema = NotificationSchema()
response = schema.dump(notif) response = schema.dump(notif)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, notification_id): def delete(self, notification_id):
@ -78,6 +58,4 @@ class Notification(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True,
}

View File

@ -4,30 +4,22 @@ from CTFd.models import db, Pages
from CTFd.schemas.pages import PageSchema from CTFd.schemas.pages import PageSchema
from CTFd.cache import clear_pages from CTFd.cache import clear_pages
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class PageList(Resource):
@admins_only @admins_only
def get(self): def get(self):
pages = Pages.query.all() pages = Pages.query.all()
schema = PageSchema(exclude=['content'], many=True) schema = PageSchema(exclude=["content"], many=True)
response = schema.dump(pages) response = schema.dump(pages)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
@ -36,10 +28,7 @@ class PageList(Resource):
response = schema.load(req) response = schema.load(req)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -49,13 +38,10 @@ class PageList(Resource):
clear_pages() clear_pages()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@pages_namespace.route('/<page_id>') @pages_namespace.route("/<page_id>")
class PageDetail(Resource): class PageDetail(Resource):
@admins_only @admins_only
def get(self, page_id): def get(self, page_id):
@ -64,15 +50,9 @@ class PageDetail(Resource):
response = schema.dump(page) response = schema.dump(page)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self, page_id): def patch(self, page_id):
@ -83,10 +63,7 @@ class PageDetail(Resource):
response = schema.load(req, instance=page, partial=True) response = schema.load(req, instance=page, partial=True)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
@ -95,10 +72,7 @@ class PageDetail(Resource):
clear_pages() clear_pages()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, page_id): def delete(self, page_id):
@ -109,6 +83,4 @@ class PageDetail(Resource):
clear_pages() clear_pages()
return { return {"success": True}
'success': True
}

View File

@ -6,12 +6,17 @@ from CTFd.utils.scores import get_standings
from CTFd.utils import get_config from CTFd.utils import get_config
from CTFd.utils.modes import TEAMS_MODE from CTFd.utils.modes import TEAMS_MODE
from CTFd.utils.dates import unix_time_to_utc, isoformat 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): class ScoreboardList(Resource):
@check_account_visibility @check_account_visibility
@check_score_visibility @check_score_visibility
@ -19,7 +24,7 @@ class ScoreboardList(Resource):
def get(self): def get(self):
standings = get_standings() standings = get_standings()
response = [] response = []
mode = get_config('user_mode') mode = get_config("user_mode")
if mode == TEAMS_MODE: if mode == TEAMS_MODE:
team_ids = [] team_ids = []
@ -30,36 +35,33 @@ class ScoreboardList(Resource):
for i, x in enumerate(standings): for i, x in enumerate(standings):
entry = { entry = {
'pos': i + 1, "pos": i + 1,
'account_id': x.account_id, "account_id": x.account_id,
'oauth_id': x.oauth_id, "oauth_id": x.oauth_id,
'name': x.name, "name": x.name,
'score': int(x.score) "score": int(x.score),
} }
if mode == TEAMS_MODE: if mode == TEAMS_MODE:
members = [] members = []
for member in teams[i].members: for member in teams[i].members:
members.append({ members.append(
'id': member.id, {
'oauth_id': member.oauth_id, "id": member.id,
'name': member.name, "oauth_id": member.oauth_id,
'score': int(member.score) "name": member.name,
}) "score": int(member.score),
entry['members'] = members
response.append(
entry
)
return {
'success': True,
'data': response
} }
)
entry["members"] = members
response.append(entry)
return {"success": True, "data": response}
@scoreboard_namespace.route('/top/<count>') @scoreboard_namespace.route("/top/<count>")
@scoreboard_namespace.param('count', 'How many top teams to return') @scoreboard_namespace.param("count", "How many top teams to return")
class ScoreboardDetail(Resource): class ScoreboardDetail(Resource):
@check_account_visibility @check_account_visibility
@check_score_visibility @check_score_visibility
@ -74,7 +76,7 @@ class ScoreboardDetail(Resource):
solves = Solves.query.filter(Solves.account_id.in_(team_ids)) solves = Solves.query.filter(Solves.account_id.in_(team_ids))
awards = Awards.query.filter(Awards.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: if freeze:
solves = solves.filter(Solves.date < unix_time_to_utc(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): for i, team in enumerate(team_ids):
response[i + 1] = { response[i + 1] = {
'id': standings[i].account_id, "id": standings[i].account_id,
'name': standings[i].name, "name": standings[i].name,
'solves': [] "solves": [],
} }
for solve in solves: for solve in solves:
if solve.account_id == team: if solve.account_id == team:
response[i + 1]['solves'].append({ response[i + 1]["solves"].append(
'challenge_id': solve.challenge_id, {
'account_id': solve.account_id, "challenge_id": solve.challenge_id,
'team_id': solve.team_id, "account_id": solve.account_id,
'user_id': solve.user_id, "team_id": solve.team_id,
'value': solve.challenge.value, "user_id": solve.user_id,
'date': isoformat(solve.date) "value": solve.challenge.value,
}) "date": isoformat(solve.date),
}
)
for award in awards: for award in awards:
if award.account_id == team: if award.account_id == team:
response[i + 1]['solves'].append({ response[i + 1]["solves"].append(
'challenge_id': None, {
'account_id': award.account_id, "challenge_id": None,
'team_id': award.team_id, "account_id": award.account_id,
'user_id': award.user_id, "team_id": award.team_id,
'value': award.value, "user_id": award.user_id,
'date': isoformat(award.date) "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
} }
)
response[i + 1]["solves"] = sorted(
response[i + 1]["solves"], key=lambda k: k["date"]
)
return {"success": True, "data": response}

View File

@ -1,6 +1,8 @@
from flask_restplus import Namespace 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 challenges # noqa: F401
from CTFd.api.v1.statistics import teams # noqa: F401 from CTFd.api.v1.statistics import teams # noqa: F401

View File

@ -1,123 +1,119 @@
from flask_restplus import Resource from flask_restplus import Resource
from CTFd.models import db, Challenges, Solves from CTFd.models import db, Challenges, Solves
from CTFd.utils.modes import get_model from CTFd.utils.modes import get_model
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
admins_only,
)
from CTFd.api.v1.statistics import statistics_namespace from CTFd.api.v1.statistics import statistics_namespace
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql import or_ from sqlalchemy.sql import or_
@statistics_namespace.route('/challenges/<column>') @statistics_namespace.route("/challenges/<column>")
class ChallengePropertyCounts(Resource): class ChallengePropertyCounts(Resource):
@admins_only @admins_only
def get(self, column): def get(self, column):
if column in Challenges.__table__.columns.keys(): if column in Challenges.__table__.columns.keys():
prop = getattr(Challenges, column) prop = getattr(Challenges, column)
data = Challenges.query\ data = (
.with_entities(prop, func.count(prop))\ Challenges.query.with_entities(prop, func.count(prop))
.group_by(prop)\ .group_by(prop)
.all() .all()
return { )
'success': True, return {"success": True, "data": dict(data)}
'data': dict(data)
}
else: else:
response = { response = {"message": "That could not be found"}, 404
'message': 'That could not be found'
}, 404
return response return response
@statistics_namespace.route('/challenges/solves') @statistics_namespace.route("/challenges/solves")
class ChallengeSolveStatistics(Resource): class ChallengeSolveStatistics(Resource):
@admins_only @admins_only
def get(self): def get(self):
chals = Challenges.query \ chals = (
.filter(or_(Challenges.state != 'hidden', Challenges.state != 'locked')) \ Challenges.query.filter(
.order_by(Challenges.value) \ or_(Challenges.state != "hidden", Challenges.state != "locked")
)
.order_by(Challenges.value)
.all() .all()
)
Model = get_model() Model = get_model()
solves_sub = db.session.query( solves_sub = (
Solves.challenge_id, db.session.query(
db.func.count(Solves.challenge_id).label('solves') Solves.challenge_id, db.func.count(Solves.challenge_id).label("solves")
) \ )
.join(Model, Solves.account_id == Model.id) \ .join(Model, Solves.account_id == Model.id)
.filter(Model.banned == False, Model.hidden == False) \ .filter(Model.banned == False, Model.hidden == False)
.group_by(Solves.challenge_id).subquery() .group_by(Solves.challenge_id)
.subquery()
)
solves = db.session.query( solves = (
db.session.query(
solves_sub.columns.challenge_id, solves_sub.columns.challenge_id,
solves_sub.columns.solves, solves_sub.columns.solves,
Challenges.name Challenges.name,
) \ )
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id).all() .join(Challenges, solves_sub.columns.challenge_id == Challenges.id)
.all()
)
response = [] response = []
has_solves = [] has_solves = []
for challenge_id, count, name in solves: for challenge_id, count, name in solves:
challenge = { challenge = {"id": challenge_id, "name": name, "solves": count}
'id': challenge_id,
'name': name,
'solves': count,
}
response.append(challenge) response.append(challenge)
has_solves.append(challenge_id) has_solves.append(challenge_id)
for c in chals: for c in chals:
if c.id not in has_solves: if c.id not in has_solves:
challenge = { challenge = {"id": c.id, "name": c.name, "solves": 0}
'id': c.id,
'name': c.name,
'solves': 0,
}
response.append(challenge) response.append(challenge)
db.session.close() db.session.close()
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@statistics_namespace.route('/challenges/solves/percentages') @statistics_namespace.route("/challenges/solves/percentages")
class ChallengeSolvePercentages(Resource): class ChallengeSolvePercentages(Resource):
@admins_only @admins_only
def get(self): def get(self):
challenges = Challenges.query\ challenges = (
.add_columns('id', 'name', 'state', 'max_attempts')\ Challenges.query.add_columns("id", "name", "state", "max_attempts")
.order_by(Challenges.value).all() .order_by(Challenges.value)
.all()
)
Model = get_model() Model = get_model()
teams_with_points = db.session.query(Solves.account_id) \ teams_with_points = (
.join(Model) \ db.session.query(Solves.account_id)
.filter(Model.banned == False, Model.hidden == False) \ .join(Model)
.group_by(Solves.account_id) \ .filter(Model.banned == False, Model.hidden == False)
.group_by(Solves.account_id)
.count() .count()
)
percentage_data = [] percentage_data = []
for challenge in challenges: for challenge in challenges:
solve_count = Solves.query.join(Model, Solves.account_id == Model.id) \ solve_count = (
.filter(Solves.challenge_id == challenge.id, Model.banned == False, Model.hidden == False) \ Solves.query.join(Model, Solves.account_id == Model.id)
.filter(
Solves.challenge_id == challenge.id,
Model.banned == False,
Model.hidden == False,
)
.count() .count()
)
if teams_with_points > 0: if teams_with_points > 0:
percentage = (float(solve_count) / float(teams_with_points)) percentage = float(solve_count) / float(teams_with_points)
else: else:
percentage = 0.0 percentage = 0.0
percentage_data.append({ percentage_data.append(
'id': challenge.id, {"id": challenge.id, "name": challenge.name, "percentage": percentage}
'name': challenge.name, )
'percentage': percentage,
})
response = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True) response = sorted(percentage_data, key=lambda x: x["percentage"], reverse=True)
return { return {"success": True, "data": response}
'success': True,
'data': response
}

View File

@ -1,29 +1,22 @@
from flask_restplus import Resource from flask_restplus import Resource
from CTFd.models import Submissions from CTFd.models import Submissions
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
admins_only,
)
from CTFd.api.v1.statistics import statistics_namespace from CTFd.api.v1.statistics import statistics_namespace
from sqlalchemy import func from sqlalchemy import func
@statistics_namespace.route('/submissions/<column>') @statistics_namespace.route("/submissions/<column>")
class SubmissionPropertyCounts(Resource): class SubmissionPropertyCounts(Resource):
@admins_only @admins_only
def get(self, column): def get(self, column):
if column in Submissions.__table__.columns.keys(): if column in Submissions.__table__.columns.keys():
prop = getattr(Submissions, column) prop = getattr(Submissions, column)
data = Submissions.query \ data = (
.with_entities(prop, func.count(prop)) \ Submissions.query.with_entities(prop, func.count(prop))
.group_by(prop) \ .group_by(prop)
.all() .all()
return { )
'success': True, return {"success": True, "data": dict(data)}
'data': dict(data)
}
else: else:
response = { response = {"success": False, "errors": "That could not be found"}, 404
'success': False,
'errors': 'That could not be found'
}, 404
return response return response

View File

@ -1,20 +1,13 @@
from flask_restplus import Resource from flask_restplus import Resource
from CTFd.models import Teams from CTFd.models import Teams
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
admins_only,
)
from CTFd.api.v1.statistics import statistics_namespace from CTFd.api.v1.statistics import statistics_namespace
@statistics_namespace.route('/teams') @statistics_namespace.route("/teams")
class TeamStatistics(Resource): class TeamStatistics(Resource):
@admins_only @admins_only
def get(self): def get(self):
registered = Teams.query.count() registered = Teams.query.count()
data = { data = {"registered": registered}
'registered': registered, return {"success": True, "data": data}
}
return {
'success': True,
'data': data
}

View File

@ -5,37 +5,24 @@ from CTFd.utils.decorators import admins_only
from sqlalchemy import func from sqlalchemy import func
@statistics_namespace.route('/users') @statistics_namespace.route("/users")
class UserStatistics(Resource): class UserStatistics(Resource):
def get(self): def get(self):
registered = Users.query.count() registered = Users.query.count()
confirmed = Users.query.filter_by(verified=True).count() confirmed = Users.query.filter_by(verified=True).count()
data = { data = {"registered": registered, "confirmed": confirmed}
'registered': registered, return {"success": True, "data": data}
'confirmed': confirmed
}
return {
'success': True,
'data': data
}
@statistics_namespace.route('/users/<column>') @statistics_namespace.route("/users/<column>")
class UserPropertyCounts(Resource): class UserPropertyCounts(Resource):
@admins_only @admins_only
def get(self, column): def get(self, column):
if column in Users.__table__.columns.keys(): if column in Users.__table__.columns.keys():
prop = getattr(Users, column) prop = getattr(Users, column)
data = Users.query \ data = (
.with_entities(prop, func.count(prop)) \ Users.query.with_entities(prop, func.count(prop)).group_by(prop).all()
.group_by(prop) \ )
.all() return {"success": True, "data": dict(data)}
return {
'success': True,
'data': dict(data)
}
else: else:
return { return {"success": False, "message": "That could not be found"}, 404
'success': False,
'message': 'That could not be found'
}, 404

View File

@ -4,16 +4,15 @@ from flask_restplus import Namespace, Resource
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.models import db, Submissions from CTFd.models import db, Submissions
from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.submissions import SubmissionSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class SubmissionsList(Resource):
@admins_only @admins_only
def get(self): def get(self):
args = request.args.to_dict() args = request.args.to_dict()
@ -26,27 +25,18 @@ class SubmissionsList(Resource):
response = schema.dump(submissions) response = schema.dump(submissions)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
req = request.get_json() req = request.get_json()
Model = Submissions.get_child(type=req.get('type')) Model = Submissions.get_child(type=req.get("type"))
schema = SubmissionSchema(instance=Model()) schema = SubmissionSchema(instance=Model())
response = schema.load(req) response = schema.load(req)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -57,14 +47,11 @@ class SubmissionsList(Resource):
# Delete standings cache # Delete standings cache
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@submissions_namespace.route('/<submission_id>') @submissions_namespace.route("/<submission_id>")
@submissions_namespace.param('submission_id', 'A Submission ID') @submissions_namespace.param("submission_id", "A Submission ID")
class Submission(Resource): class Submission(Resource):
@admins_only @admins_only
def get(self, submission_id): def get(self, submission_id):
@ -73,15 +60,9 @@ class Submission(Resource):
response = schema.dump(submission) response = schema.dump(submission)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, submission_id): def delete(self, submission_id):
@ -93,6 +74,4 @@ class Submission(Resource):
# Delete standings cache # Delete standings cache
clear_standings() clear_standings()
return { return {"success": True}
'success': True
}

View File

@ -2,14 +2,12 @@ from flask import request
from flask_restplus import Namespace, Resource from flask_restplus import Namespace, Resource
from CTFd.models import db, Tags from CTFd.models import db, Tags
from CTFd.schemas.tags import TagSchema from CTFd.schemas.tags import TagSchema
from CTFd.utils.decorators import ( from CTFd.utils.decorators import admins_only
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): class TagList(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -19,15 +17,9 @@ class TagList(Resource):
response = schema.dump(tags) response = schema.dump(tags)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
@ -36,10 +28,7 @@ class TagList(Resource):
response = schema.load(req, session=db.session) response = schema.load(req, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -47,14 +36,11 @@ class TagList(Resource):
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@tags_namespace.route('/<tag_id>') @tags_namespace.route("/<tag_id>")
@tags_namespace.param('tag_id', 'A Tag ID') @tags_namespace.param("tag_id", "A Tag ID")
class Tag(Resource): class Tag(Resource):
@admins_only @admins_only
def get(self, tag_id): def get(self, tag_id):
@ -63,15 +49,9 @@ class Tag(Resource):
response = TagSchema().dump(tag) response = TagSchema().dump(tag)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self, tag_id): def patch(self, tag_id):
@ -81,20 +61,14 @@ class Tag(Resource):
response = schema.load(req, session=db.session, instance=tag) response = schema.load(req, session=db.session, instance=tag)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, tag_id): def delete(self, tag_id):
@ -103,6 +77,4 @@ class Tag(Resource):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return { return {"success": True}
'success': True
}

View File

@ -6,58 +6,37 @@ from CTFd.schemas.submissions import SubmissionSchema
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.utils.decorators.visibility import check_account_visibility from CTFd.utils.decorators.visibility import check_account_visibility
from CTFd.utils.config.visibility import ( from CTFd.utils.config.visibility import accounts_visible, scores_visible
accounts_visible, from CTFd.utils.user import get_current_team, is_admin, authed
scores_visible from CTFd.utils.decorators import authed_only, admins_only
)
from CTFd.utils.user import (
get_current_team,
is_admin,
authed
)
from CTFd.utils.decorators import (
authed_only,
admins_only,
)
import copy 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): class TeamList(Resource):
@check_account_visibility @check_account_visibility
def get(self): def get(self):
teams = Teams.query.filter_by(hidden=False, banned=False) teams = Teams.query.filter_by(hidden=False, banned=False)
view = copy.deepcopy(TeamSchema.views.get( view = copy.deepcopy(TeamSchema.views.get(session.get("type", "user")))
session.get('type', 'user') view.remove("members")
))
view.remove('members')
response = TeamSchema(view=view, many=True).dump(teams) response = TeamSchema(view=view, many=True).dump(teams)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def post(self): def post(self):
req = request.get_json() req = request.get_json()
view = TeamSchema.views.get(session.get('type', 'self')) view = TeamSchema.views.get(session.get("type", "self"))
schema = TeamSchema(view=view) schema = TeamSchema(view=view)
response = schema.load(req) response = schema.load(req)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
@ -67,14 +46,11 @@ class TeamList(Resource):
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@teams_namespace.route('/<int:team_id>') @teams_namespace.route("/<int:team_id>")
@teams_namespace.param('team_id', "Team ID") @teams_namespace.param("team_id", "Team ID")
class TeamPublic(Resource): class TeamPublic(Resource):
@check_account_visibility @check_account_visibility
def get(self, team_id): def get(self, team_id):
@ -83,35 +59,26 @@ class TeamPublic(Resource):
if (team.banned or team.hidden) and is_admin() is False: if (team.banned or team.hidden) and is_admin() is False:
abort(404) abort(404)
view = TeamSchema.views.get(session.get('type', 'user')) view = TeamSchema.views.get(session.get("type", "user"))
schema = TeamSchema(view=view) schema = TeamSchema(view=view)
response = schema.dump(team) response = schema.dump(team)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self, team_id): def patch(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
data = request.get_json() 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) response = schema.load(data)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
response = schema.dump(response.data) response = schema.dump(response.data)
db.session.commit() db.session.commit()
@ -119,10 +86,7 @@ class TeamPublic(Resource):
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def delete(self, team_id): def delete(self, team_id):
@ -137,170 +101,131 @@ class TeamPublic(Resource):
clear_standings() clear_standings()
return { return {"success": True}
'success': True,
}
@teams_namespace.route('/me') @teams_namespace.route("/me")
@teams_namespace.param('team_id', "Current Team") @teams_namespace.param("team_id", "Current Team")
class TeamPrivate(Resource): class TeamPrivate(Resource):
@authed_only @authed_only
def get(self): def get(self):
team = get_current_team() team = get_current_team()
response = TeamSchema(view='self').dump(team) response = TeamSchema(view="self").dump(team)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@authed_only @authed_only
def patch(self): def patch(self):
team = get_current_team() team = get_current_team()
if team.captain_id != session['id']: if team.captain_id != session["id"]:
return { return (
'success': False, {
'errors': { "success": False,
'': [ "errors": {"": ["Only team captains can edit team information"]},
'Only team captains can edit team information' },
] 400,
} )
}, 400
data = request.get_json() 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: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
response = TeamSchema('self').dump(response.data) response = TeamSchema("self").dump(response.data)
db.session.close() db.session.close()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@teams_namespace.route('/<team_id>/members') @teams_namespace.route("/<team_id>/members")
@teams_namespace.param('team_id', "Team ID") @teams_namespace.param("team_id", "Team ID")
class TeamMembers(Resource): class TeamMembers(Resource):
@admins_only @admins_only
def get(self, team_id): def get(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() 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) schema = TeamSchema(view=view)
response = schema.dump(team) response = schema.dump(team)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
members = response.data.get('members') members = response.data.get("members")
return { return {"success": True, "data": members}
'success': True,
'data': members
}
@admins_only @admins_only
def post(self, team_id): def post(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
data = request.get_json() data = request.get_json()
user_id = data['id'] user_id = data["id"]
user = Users.query.filter_by(id=user_id).first_or_404() user = Users.query.filter_by(id=user_id).first_or_404()
if user.team_id is None: if user.team_id is None:
team.members.append(user) team.members.append(user)
db.session.commit() db.session.commit()
else: else:
return { return (
'success': False, {
'errors': { "success": False,
'id': [ "errors": {"id": ["User has already joined a team"]},
'User has already joined a team' },
] 400,
} )
}, 400
view = 'admin' if is_admin() else 'user' view = "admin" if is_admin() else "user"
schema = TeamSchema(view=view) schema = TeamSchema(view=view)
response = schema.dump(team) response = schema.dump(team)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
members = response.data.get('members') members = response.data.get("members")
return { return {"success": True, "data": members}
'success': True,
'data': members
}
@admins_only @admins_only
def delete(self, team_id): def delete(self, team_id):
team = Teams.query.filter_by(id=team_id).first_or_404() team = Teams.query.filter_by(id=team_id).first_or_404()
data = request.get_json() data = request.get_json()
user_id = data['id'] user_id = data["id"]
user = Users.query.filter_by(id=user_id).first_or_404() user = Users.query.filter_by(id=user_id).first_or_404()
if user.team_id == team.id: if user.team_id == team.id:
team.members.remove(user) team.members.remove(user)
db.session.commit() db.session.commit()
else: else:
return { return (
'success': False, {"success": False, "errors": {"id": ["User is not part of this team"]}},
'errors': { 400,
'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) schema = TeamSchema(view=view)
response = schema.dump(team) response = schema.dump(team)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
members = response.data.get('members') members = response.data.get("members")
return { return {"success": True, "data": members}
'success': True,
'data': members
}
@teams_namespace.route('/<team_id>/solves') @teams_namespace.route("/<team_id>/solves")
@teams_namespace.param('team_id', "Team ID or 'me'") @teams_namespace.param("team_id", "Team ID or 'me'")
class TeamSolves(Resource): class TeamSolves(Resource):
def get(self, team_id): def get(self, team_id):
if team_id == 'me': if team_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
team = get_current_team() team = get_current_team()
@ -314,28 +239,21 @@ class TeamSolves(Resource):
abort(404) abort(404)
solves = team.get_solves(admin=is_admin()) 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) schema = SubmissionSchema(view=view, many=True)
response = schema.dump(solves) response = schema.dump(solves)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@teams_namespace.route('/<team_id>/fails') @teams_namespace.route("/<team_id>/fails")
@teams_namespace.param('team_id', "Team ID or 'me'") @teams_namespace.param("team_id", "Team ID or 'me'")
class TeamFails(Resource): class TeamFails(Resource):
def get(self, team_id): def get(self, team_id):
if team_id == 'me': if team_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
team = get_current_team() team = get_current_team()
@ -349,16 +267,13 @@ class TeamFails(Resource):
abort(404) abort(404)
fails = team.get_fails(admin=is_admin()) 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) schema = SubmissionSchema(view=view, many=True)
response = schema.dump(fails) response = schema.dump(fails)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
if is_admin(): if is_admin():
data = response.data data = response.data
@ -366,21 +281,14 @@ class TeamFails(Resource):
data = [] data = []
count = len(response.data) count = len(response.data)
return { return {"success": True, "data": data, "meta": {"count": count}}
'success': True,
'data': data,
'meta': {
'count': count
}
}
@teams_namespace.route('/<team_id>/awards') @teams_namespace.route("/<team_id>/awards")
@teams_namespace.param('team_id', "Team ID or 'me'") @teams_namespace.param("team_id", "Team ID or 'me'")
class TeamAwards(Resource): class TeamAwards(Resource):
def get(self, team_id): def get(self, team_id):
if team_id == 'me': if team_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
team = get_current_team() team = get_current_team()
@ -398,12 +306,6 @@ class TeamAwards(Resource):
response = schema.dump(awards) response = schema.dump(awards)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}

View File

@ -8,13 +8,13 @@ from CTFd.utils.decorators import (
during_ctf_time_only, during_ctf_time_only,
require_verified_emails, require_verified_emails,
admins_only, 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): class UnlockList(Resource):
@admins_only @admins_only
def get(self): def get(self):
@ -23,15 +23,9 @@ class UnlockList(Resource):
response = schema.dump(hints) response = schema.dump(hints)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@during_ctf_time_only @during_ctf_time_only
@require_verified_emails @require_verified_emails
@ -40,39 +34,39 @@ class UnlockList(Resource):
req = request.get_json() req = request.get_json()
user = get_current_user() user = get_current_user()
req['user_id'] = user.id req["user_id"] = user.id
req['team_id'] = user.team_id req["team_id"] = user.team_id
Model = get_class_by_tablename(req['type']) Model = get_class_by_tablename(req["type"])
target = Model.query.filter_by(id=req['target']).first_or_404() target = Model.query.filter_by(id=req["target"]).first_or_404()
if target.cost > user.score: if target.cost > user.score:
return { return (
'success': False, {
'errors': { "success": False,
'score': 'You do not have enough points to unlock this hint' "errors": {
} "score": "You do not have enough points to unlock this hint"
}, 400 },
},
400,
)
schema = UnlockSchema() schema = UnlockSchema()
response = schema.load(req, session=db.session) response = schema.load(req, session=db.session)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
award_schema = AwardSchema() award_schema = AwardSchema()
award = { award = {
'user_id': user.id, "user_id": user.id,
'team_id': user.team_id, "team_id": user.team_id,
'name': target.name, "name": target.name,
'description': target.description, "description": target.description,
'value': (-target.cost), "value": (-target.cost),
'category': target.category "category": target.category,
} }
award = award_schema.load(award) award = award_schema.load(award)
@ -81,7 +75,4 @@ class UnlockList(Resource):
response = schema.dump(response.data) response = schema.dump(response.data)
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}

View File

@ -1,88 +1,77 @@
from flask import session, request, abort from flask import session, request, abort
from flask_restplus import Namespace, Resource from flask_restplus import Namespace, Resource
from CTFd.models import db, Users, Solves, Awards, Tracking, Unlocks, Submissions, Notifications from CTFd.models import (
from CTFd.utils.decorators import ( db,
authed_only, Users,
admins_only, Solves,
authed, Awards,
ratelimit Tracking,
Unlocks,
Submissions,
Notifications,
) )
from CTFd.utils.decorators import authed_only, admins_only, authed, ratelimit
from CTFd.cache import clear_standings from CTFd.cache import clear_standings
from CTFd.utils.config import get_mail_provider from CTFd.utils.config import get_mail_provider
from CTFd.utils.email import sendmail, user_created_notification from CTFd.utils.email import sendmail, user_created_notification
from CTFd.utils.user import get_current_user, is_admin from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.decorators.visibility import check_account_visibility from CTFd.utils.decorators.visibility import check_account_visibility
from CTFd.utils.config.visibility import ( from CTFd.utils.config.visibility import accounts_visible, scores_visible
accounts_visible,
scores_visible
)
from CTFd.schemas.submissions import SubmissionSchema from CTFd.schemas.submissions import SubmissionSchema
from CTFd.schemas.awards import AwardSchema from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.users import UserSchema 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): class UserList(Resource):
@check_account_visibility @check_account_visibility
def get(self): def get(self):
users = Users.query.filter_by(banned=False, hidden=False) 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: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'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 @admins_only
def post(self): def post(self):
req = request.get_json() req = request.get_json()
schema = UserSchema('admin') schema = UserSchema("admin")
response = schema.load(req) response = schema.load(req)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.add(response.data) db.session.add(response.data)
db.session.commit() db.session.commit()
if request.args.get('notify'): if request.args.get("notify"):
name = response.data.name name = response.data.name
email = response.data.email email = response.data.email
password = req.get('password') password = req.get("password")
user_created_notification( user_created_notification(addr=email, name=name, password=password)
addr=email,
name=name,
password=password
)
clear_standings() clear_standings()
response = schema.dump(response.data) response = schema.dump(response.data)
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@users_namespace.route('/<int:user_id>') @users_namespace.route("/<int:user_id>")
@users_namespace.param('user_id', "User ID") @users_namespace.param("user_id", "User ID")
class UserPublic(Resource): class UserPublic(Resource):
@check_account_visibility @check_account_visibility
def get(self, user_id): def get(self, user_id):
@ -91,36 +80,25 @@ class UserPublic(Resource):
if (user.banned or user.hidden) and is_admin() is False: if (user.banned or user.hidden) and is_admin() is False:
abort(404) abort(404)
response = UserSchema( response = UserSchema(view=session.get("type", "user")).dump(user)
view=session.get('type', 'user')
).dump(user)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
response.data['place'] = user.place response.data["place"] = user.place
response.data['score'] = user.score response.data["score"] = user.score
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@admins_only @admins_only
def patch(self, user_id): def patch(self, user_id):
user = Users.query.filter_by(id=user_id).first_or_404() user = Users.query.filter_by(id=user_id).first_or_404()
data = request.get_json() data = request.get_json()
data['id'] = user_id data["id"] = user_id
schema = UserSchema(view='admin', instance=user, partial=True) schema = UserSchema(view="admin", instance=user, partial=True)
response = schema.load(data) response = schema.load(data)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
@ -130,10 +108,7 @@ class UserPublic(Resource):
clear_standings() clear_standings()
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@admins_only @admins_only
def delete(self, user_id): def delete(self, user_id):
@ -149,35 +124,27 @@ class UserPublic(Resource):
clear_standings() clear_standings()
return { return {"success": True}
'success': True
}
@users_namespace.route('/me') @users_namespace.route("/me")
class UserPrivate(Resource): class UserPrivate(Resource):
@authed_only @authed_only
def get(self): def get(self):
user = get_current_user() user = get_current_user()
response = UserSchema('self').dump(user).data response = UserSchema("self").dump(user).data
response['place'] = user.place response["place"] = user.place
response['score'] = user.score response["score"] = user.score
return { return {"success": True, "data": response}
'success': True,
'data': response
}
@authed_only @authed_only
def patch(self): def patch(self):
user = get_current_user() user = get_current_user()
data = request.get_json() data = request.get_json()
schema = UserSchema(view='self', instance=user, partial=True) schema = UserSchema(view="self", instance=user, partial=True)
response = schema.load(data) response = schema.load(data)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
db.session.commit() db.session.commit()
@ -186,17 +153,14 @@ class UserPrivate(Resource):
clear_standings() clear_standings()
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@users_namespace.route('/<user_id>/solves') @users_namespace.route("/<user_id>/solves")
@users_namespace.param('user_id', "User ID or 'me'") @users_namespace.param("user_id", "User ID or 'me'")
class UserSolves(Resource): class UserSolves(Resource):
def get(self, user_id): def get(self, user_id):
if user_id == 'me': if user_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
user = get_current_user() user = get_current_user()
@ -210,26 +174,20 @@ class UserSolves(Resource):
abort(404) abort(404)
solves = user.get_solves(admin=is_admin()) 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) response = SubmissionSchema(view=view, many=True).dump(solves)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@users_namespace.route('/<user_id>/fails') @users_namespace.route("/<user_id>/fails")
@users_namespace.param('user_id', "User ID or 'me'") @users_namespace.param("user_id", "User ID or 'me'")
class UserFails(Resource): class UserFails(Resource):
def get(self, user_id): def get(self, user_id):
if user_id == 'me': if user_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
user = get_current_user() user = get_current_user()
@ -243,13 +201,10 @@ class UserFails(Resource):
abort(404) abort(404)
fails = user.get_fails(admin=is_admin()) 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) response = SubmissionSchema(view=view, many=True).dump(fails)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
if is_admin(): if is_admin():
data = response.data data = response.data
@ -257,20 +212,14 @@ class UserFails(Resource):
data = [] data = []
count = len(response.data) count = len(response.data)
return { return {"success": True, "data": data, "meta": {"count": count}}
'success': True,
'data': data,
'meta': {
'count': count
}
}
@users_namespace.route('/<user_id>/awards') @users_namespace.route("/<user_id>/awards")
@users_namespace.param('user_id', "User ID or 'me'") @users_namespace.param("user_id", "User ID or 'me'")
class UserAwards(Resource): class UserAwards(Resource):
def get(self, user_id): def get(self, user_id):
if user_id == 'me': if user_id == "me":
if not authed(): if not authed():
abort(403) abort(403)
user = get_current_user() user = get_current_user()
@ -284,57 +233,37 @@ class UserAwards(Resource):
abort(404) abort(404)
awards = user.get_awards(admin=is_admin()) 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) response = AwardSchema(view=view, many=True).dump(awards)
if response.errors: if response.errors:
return { return {"success": False, "errors": response.errors}, 400
'success': False,
'errors': response.errors
}, 400
return { return {"success": True, "data": response.data}
'success': True,
'data': response.data
}
@users_namespace.route('/<int:user_id>/email') @users_namespace.route("/<int:user_id>/email")
@users_namespace.param('user_id', "User ID") @users_namespace.param("user_id", "User ID")
class UserEmails(Resource): class UserEmails(Resource):
@admins_only @admins_only
@ratelimit(method="POST", limit=10, interval=60) @ratelimit(method="POST", limit=10, interval=60)
def post(self, user_id): def post(self, user_id):
req = request.get_json() 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() user = Users.query.filter_by(id=user_id).first_or_404()
if get_mail_provider() is None: if get_mail_provider() is None:
return { return (
'success': False, {"success": False, "errors": {"": ["Email settings not configured"]}},
'errors': { 400,
"": [
"Email settings not configured"
]
}
}, 400
if not text:
return {
'success': False,
'errors': {
"text": [
"Email text cannot be empty"
]
}
}, 400
result, response = sendmail(
addr=user.email,
text=text
) )
return { if not text:
'success': result, return (
'data': {} {"success": False, "errors": {"text": ["Email text cannot be empty"]}},
} 400,
)
result, response = sendmail(addr=user.email, text=text)
return {"success": result, "data": {}}

View File

@ -28,119 +28,150 @@ from CTFd.utils.helpers import error_for, get_errors
import base64 import base64
import requests import requests
auth = Blueprint('auth', __name__) auth = Blueprint("auth", __name__)
@auth.route('/confirm', methods=['POST', 'GET']) @auth.route("/confirm", methods=["POST", "GET"])
@auth.route('/confirm/<data>', methods=['GET']) @auth.route("/confirm/<data>", methods=["GET"])
@ratelimit(method="POST", limit=10, interval=60) @ratelimit(method="POST", limit=10, interval=60)
def confirm(data=None): 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 # 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 # User is confirming email account
if data and request.method == "GET": if data and request.method == "GET":
try: try:
user_email = unserialize(data, max_age=1800) user_email = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired): 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): 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 = Users.query.filter_by(email=user_email).first_or_404()
user.verified = True 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.commit()
db.session.close() db.session.close()
if current_user.authed(): if current_user.authed():
return redirect(url_for('challenges.listing')) return redirect(url_for("challenges.listing"))
return redirect(url_for('auth.login')) return redirect(url_for("auth.login"))
# User is trying to start or restart the confirmation flow # User is trying to start or restart the confirmation flow
if not current_user.authed(): 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: if user.verified:
return redirect(url_for('views.settings')) return redirect(url_for("views.settings"))
if data is None: if data is None:
if request.method == "POST": if request.method == "POST":
# User wants to resend their confirmation email # User wants to resend their confirmation email
email.verify_email_address(user.email) email.verify_email_address(user.email)
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend") log(
return render_template('confirm.html', user=user, infos=['Your confirmation email has been resent!']) "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": elif request.method == "GET":
# User has been directed to the confirm page # 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/<data>', methods=['POST', 'GET']) @auth.route("/reset_password/<data>", methods=["POST", "GET"])
@ratelimit(method="POST", limit=10, interval=60) @ratelimit(method="POST", limit=10, interval=60)
def reset_password(data=None): def reset_password(data=None):
if data is not None: if data is not None:
try: try:
name = unserialize(data, max_age=1800) name = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired): 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): 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": if request.method == "GET":
return render_template('reset_password.html', mode='set') return render_template("reset_password.html", mode="set")
if request.method == "POST": if request.method == "POST":
user = Users.query.filter_by(name=name).first_or_404() 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() 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() db.session.close()
return redirect(url_for('auth.login')) return redirect(url_for("auth.login"))
if request.method == 'POST': if request.method == "POST":
email_address = request.form['email'].strip() email_address = request.form["email"].strip()
team = Users.query.filter_by(email=email_address).first() team = Users.query.filter_by(email=email_address).first()
get_errors() get_errors()
if config.can_send_mail() is False: if config.can_send_mail() is False:
return render_template( return render_template(
'reset_password.html', "reset_password.html",
errors=['Email could not be sent due to server misconfiguration'] errors=["Email could not be sent due to server misconfiguration"],
) )
if not team: if not team:
return render_template( return render_template(
'reset_password.html', "reset_password.html",
errors=['If that account exists you will receive an email, please check your inbox'] errors=[
"If that account exists you will receive an email, please check your inbox"
],
) )
email.forgot_password(email_address, team.name) email.forgot_password(email_address, team.name)
return render_template( return render_template(
'reset_password.html', "reset_password.html",
errors=['If that account exists you will receive an email, please check your inbox'] 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 @check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5) @ratelimit(method="POST", limit=10, interval=5)
def register(): def register():
errors = get_errors() errors = get_errors()
if request.method == 'POST': if request.method == "POST":
name = request.form['name'] name = request.form["name"]
email_address = request.form['email'] email_address = request.form["email"]
password = request.form['password'] password = request.form["password"]
name_len = len(name) == 0 name_len = len(name) == 0
names = Users.query.add_columns('name', 'id').filter_by(name=name).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() emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)
pass_short = len(password) == 0 pass_short = len(password) == 0
pass_long = len(password) > 128 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) team_name_email_check = validators.validate_email(name)
if not valid_email: if not valid_email:
@ -148,36 +179,36 @@ def register():
if email.check_email_is_whitelisted(email_address) is False: if email.check_email_is_whitelisted(email_address) is False:
errors.append( errors.append(
"Only email addresses under {domains} may register".format( "Only email addresses under {domains} may register".format(
domains=get_config('domain_whitelist') domains=get_config("domain_whitelist")
) )
) )
if names: 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: 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: if emails:
errors.append('That email has already been used') errors.append("That email has already been used")
if pass_short: if pass_short:
errors.append('Pick a longer password') errors.append("Pick a longer password")
if pass_long: if pass_long:
errors.append('Pick a shorter password') errors.append("Pick a shorter password")
if name_len: if name_len:
errors.append('Pick a longer user name') errors.append("Pick a longer user name")
if len(errors) > 0: if len(errors) > 0:
return render_template( return render_template(
'register.html', "register.html",
errors=errors, errors=errors,
name=request.form['name'], name=request.form["name"],
email=request.form['email'], email=request.form["email"],
password=request.form['password'] password=request.form["password"],
) )
else: else:
with app.app_context(): with app.app_context():
user = Users( user = Users(
name=name.strip(), name=name.strip(),
email=email_address.lower(), email=email_address.lower(),
password=password.strip() password=password.strip(),
) )
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -185,31 +216,40 @@ def register():
login_user(user) login_user(user)
if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email. if config.can_send_mail() and get_config(
log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}") "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) email.verify_email_address(user.email)
db.session.close() db.session.close()
return redirect(url_for('auth.confirm')) return redirect(url_for("auth.confirm"))
else: # Don't care about confirming users 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( email.sendmail(
request.form['email'], request.form["email"],
"You've successfully registered for {}".format(get_config('ctf_name')) "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() db.session.close()
return redirect(url_for('challenges.listing')) return redirect(url_for("challenges.listing"))
else: 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) @ratelimit(method="POST", limit=10, interval=5)
def login(): def login():
errors = get_errors() errors = get_errors()
if request.method == 'POST': if request.method == "POST":
name = request.form['name'] name = request.form["name"]
# Check if the user submitted an email address or a team name # Check if the user submitted an email address or a team name
if validators.validate_email(name) is True: if validators.validate_email(name) is True:
@ -218,107 +258,112 @@ def login():
user = Users.query.filter_by(name=name).first() user = Users.query.filter_by(name=name).first()
if user: if user:
if user and verify_password(request.form['password'], user.password): if user and verify_password(request.form["password"], user.password):
session.regenerate() session.regenerate()
login_user(user) login_user(user)
log('logins', "[{date}] {ip} - {name} logged in") log("logins", "[{date}] {ip} - {name} logged in")
db.session.close() db.session.close()
if request.args.get('next') and validators.is_safe_url(request.args.get('next')): if request.args.get("next") and validators.is_safe_url(
return redirect(request.args.get('next')) request.args.get("next")
return redirect(url_for('challenges.listing')) ):
return redirect(request.args.get("next"))
return redirect(url_for("challenges.listing"))
else: else:
# This user exists but the password is wrong # 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") errors.append("Your username or password is incorrect")
db.session.close() db.session.close()
return render_template('login.html', errors=errors) return render_template("login.html", errors=errors)
else: else:
# This user just doesn't exist # 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") errors.append("Your username or password is incorrect")
db.session.close() db.session.close()
return render_template('login.html', errors=errors) return render_template("login.html", errors=errors)
else: else:
db.session.close() 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(): def oauth_login():
endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \ endpoint = (
or get_config('oauth_authorization_endpoint') \ get_app_config("OAUTH_AUTHORIZATION_ENDPOINT")
or 'https://auth.majorleaguecyber.org/oauth/authorize' or get_config("oauth_authorization_endpoint")
or "https://auth.majorleaguecyber.org/oauth/authorize"
)
if get_config('user_mode') == 'teams': if get_config("user_mode") == "teams":
scope = 'profile team' scope = "profile team"
else: 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: if client_id is None:
error_for( error_for(
endpoint='auth.login', endpoint="auth.login",
message='OAuth Settings not configured. ' message="OAuth Settings not configured. "
'Ask your CTF administrator to configure MajorLeagueCyber integration.' "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( redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
endpoint=endpoint, endpoint=endpoint, client_id=client_id, scope=scope, state=session["nonce"]
client_id=client_id,
scope=scope,
state=session['nonce']
) )
return redirect(redirect_url) return redirect(redirect_url)
@auth.route('/redirect', methods=['GET']) @auth.route("/redirect", methods=["GET"])
@ratelimit(method="GET", limit=10, interval=60) @ratelimit(method="GET", limit=10, interval=60)
def oauth_redirect(): def oauth_redirect():
oauth_code = request.args.get('code') oauth_code = request.args.get("code")
state = request.args.get('state') state = request.args.get("state")
if session['nonce'] != state: if session["nonce"] != state:
log('logins', "[{date}] {ip} - OAuth State validation mismatch") log("logins", "[{date}] {ip} - OAuth State validation mismatch")
error_for(endpoint='auth.login', message='OAuth State validation mismatch.') error_for(endpoint="auth.login", message="OAuth State validation mismatch.")
return redirect(url_for('auth.login')) return redirect(url_for("auth.login"))
if oauth_code: if oauth_code:
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \ url = (
or get_config('oauth_token_endpoint') \ get_app_config("OAUTH_TOKEN_ENDPOINT")
or 'https://auth.majorleaguecyber.org/oauth/token' 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_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') client_secret = get_app_config("OAUTH_CLIENT_SECRET") or get_config(
headers = { "oauth_client_secret"
'content-type': 'application/x-www-form-urlencoded' )
} headers = {"content-type": "application/x-www-form-urlencoded"}
data = { data = {
'code': oauth_code, "code": oauth_code,
'client_id': client_id, "client_id": client_id,
'client_secret': client_secret, "client_secret": client_secret,
'grant_type': 'authorization_code' "grant_type": "authorization_code",
} }
token_request = requests.post(url, data=data, headers=headers) token_request = requests.post(url, data=data, headers=headers)
if token_request.status_code == requests.codes.ok: if token_request.status_code == requests.codes.ok:
token = token_request.json()['access_token'] token = token_request.json()["access_token"]
user_url = get_app_config('OAUTH_API_ENDPOINT') \ user_url = (
or get_config('oauth_api_endpoint') \ get_app_config("OAUTH_API_ENDPOINT")
or 'https://api.majorleaguecyber.org/user' or get_config("oauth_api_endpoint")
or "https://api.majorleaguecyber.org/user"
)
headers = { headers = {
'Authorization': 'Bearer ' + str(token), "Authorization": "Bearer " + str(token),
'Content-type': 'application/json' "Content-type": "application/json",
} }
api_data = requests.get(url=user_url, headers=headers).json() api_data = requests.get(url=user_url, headers=headers).json()
user_id = api_data['id'] user_id = api_data["id"]
user_name = api_data['name'] user_name = api_data["name"]
user_email = api_data['email'] user_email = api_data["email"]
user = Users.query.filter_by(email=user_email).first() user = Users.query.filter_by(email=user_email).first()
if user is None: if user is None:
@ -328,29 +373,25 @@ def oauth_redirect():
name=user_name, name=user_name,
email=user_email, email=user_email,
oauth_id=user_id, oauth_id=user_id,
verified=True verified=True,
) )
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
else: else:
log('logins', "[{date}] {ip} - Public registration via MLC blocked") log("logins", "[{date}] {ip} - Public registration via MLC blocked")
error_for( error_for(
endpoint='auth.login', endpoint="auth.login",
message='Public registration is disabled. Please try again later.' 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: if get_config("user_mode") == TEAMS_MODE:
team_id = api_data['team']['id'] team_id = api_data["team"]["id"]
team_name = api_data['team']['name'] team_name = api_data["team"]["name"]
team = Teams.query.filter_by(oauth_id=team_id).first() team = Teams.query.filter_by(oauth_id=team_id).first()
if team is None: if team is None:
team = Teams( team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id)
name=team_name,
oauth_id=team_id,
captain_id=user.id
)
db.session.add(team) db.session.add(team)
db.session.commit() db.session.commit()
@ -364,25 +405,21 @@ def oauth_redirect():
login_user(user) login_user(user)
return redirect(url_for('challenges.listing')) return redirect(url_for("challenges.listing"))
else: else:
log('logins', "[{date}] {ip} - OAuth token retrieval failure") log("logins", "[{date}] {ip} - OAuth token retrieval failure")
error_for( error_for(endpoint="auth.login", message="OAuth token retrieval failure.")
endpoint='auth.login', return redirect(url_for("auth.login"))
message='OAuth token retrieval failure.'
)
return redirect(url_for('auth.login'))
else: else:
log('logins', "[{date}] {ip} - Received redirect without OAuth code") log("logins", "[{date}] {ip} - Received redirect without OAuth code")
error_for( error_for(
endpoint='auth.login', endpoint="auth.login", message="Received redirect without OAuth code."
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(): def logout():
if current_user.authed(): if current_user.authed():
logout_user() logout_user()
return redirect(url_for('views.static_html')) return redirect(url_for("views.static_html"))

View File

@ -4,7 +4,7 @@ from flask_caching import Cache
cache = 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. 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. 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(): def clear_config():
from CTFd.utils import _get_config, get_app_config from CTFd.utils import _get_config, get_app_config
cache.delete_memoized(_get_config) cache.delete_memoized(_get_config)
cache.delete_memoized(get_app_config) cache.delete_memoized(get_app_config)
@ -28,17 +29,15 @@ def clear_standings():
from CTFd.utils.scores import get_standings from CTFd.utils.scores import get_standings
from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList
from CTFd.api import api from CTFd.api import api
cache.delete_memoized(get_standings) cache.delete_memoized(get_standings)
cache.delete( cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint))
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 + '.' + ScoreboardDetail.endpoint)
)
cache.delete_memoized(ScoreboardList.get) cache.delete_memoized(ScoreboardList.get)
def clear_pages(): def clear_pages():
from CTFd.utils.config.pages import get_page, get_pages from CTFd.utils.config.pages import get_page, get_pages
cache.delete_memoized(get_pages) cache.delete_memoized(get_pages)
cache.delete_memoized(get_page) cache.delete_memoized(get_page)

View File

@ -1,21 +1,18 @@
from flask import ( from flask import render_template, Blueprint
render_template,
Blueprint,
)
from CTFd.utils.decorators import ( from CTFd.utils.decorators import (
during_ctf_time_only, during_ctf_time_only,
require_verified_emails, require_verified_emails,
require_team require_team,
) )
from CTFd.utils.decorators.visibility import check_challenge_visibility from CTFd.utils.decorators.visibility import check_challenge_visibility
from CTFd.utils import config, get_config from CTFd.utils import config, get_config
from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf
from CTFd.utils.helpers import get_errors, get_infos 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 @during_ctf_time_only
@require_verified_emails @require_verified_emails
@check_challenge_visibility @check_challenge_visibility
@ -23,14 +20,16 @@ challenges = Blueprint('challenges', __name__)
def listing(): def listing():
infos = get_infos() infos = get_infos()
errors = get_errors() errors = get_errors()
start = get_config('start') or 0 start = get_config("start") or 0
end = get_config('end') or 0 end = get_config("end") or 0
if ctf_paused(): 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. # 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(): 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)
)

View File

@ -1,12 +1,12 @@
import os 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 # Attempt to read the secret from the secret file
# This will fail if the secret has not been written # This will fail if the secret has not been written
try: try:
with open('.ctfd_secret_key', 'rb') as secret: with open(".ctfd_secret_key", "rb") as secret:
key = secret.read() key = secret.read()
except (OSError, IOError): except (OSError, IOError):
key = None key = None
@ -16,14 +16,14 @@ if not os.getenv('SECRET_KEY'):
# Attempt to write the secret file # Attempt to write the secret file
# This will fail if the filesystem is read-only # This will fail if the filesystem is read-only
try: try:
with open('.ctfd_secret_key', 'wb') as secret: with open(".ctfd_secret_key", "wb") as secret:
secret.write(key) secret.write(key)
secret.flush() secret.flush()
except (OSError, IOError): except (OSError, IOError):
pass pass
''' SERVER SETTINGS ''' """ SERVER SETTINGS """
class Config(object): class Config(object):
@ -31,7 +31,7 @@ class Config(object):
CTFd Configuration Object CTFd Configuration Object
""" """
''' """
=== REQUIRED SETTINGS === === REQUIRED SETTINGS ===
SECRET_KEY: SECRET_KEY:
@ -61,21 +61,27 @@ class Config(object):
REDIS_URL is the URL to connect to a Redis server. REDIS_URL is the URL to connect to a Redis server.
e.g. redis://user:password@localhost:6379 e.g. redis://user:password@localhost:6379
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
''' """
SECRET_KEY = os.getenv('SECRET_KEY') or key 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__))) DATABASE_URL = os.getenv("DATABASE_URL") or "sqlite:///{}/ctfd.db".format(
REDIS_URL = os.getenv('REDIS_URL') os.path.dirname(os.path.abspath(__file__))
)
REDIS_URL = os.getenv("REDIS_URL")
SQLALCHEMY_DATABASE_URI = DATABASE_URL SQLALCHEMY_DATABASE_URI = DATABASE_URL
CACHE_REDIS_URL = REDIS_URL CACHE_REDIS_URL = REDIS_URL
if CACHE_REDIS_URL: if CACHE_REDIS_URL:
CACHE_TYPE = 'redis' CACHE_TYPE = "redis"
else: else:
CACHE_TYPE = 'filesystem' CACHE_TYPE = "filesystem"
CACHE_DIR = os.path.join(os.path.dirname(__file__), os.pardir, '.data', 'filesystem_cache') CACHE_DIR = os.path.join(
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. 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 === === SECURITY ===
SESSION_COOKIE_HTTPONLY: 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 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. solely on IP addresses unless you know what you are doing.
''' """
SESSION_COOKIE_HTTPONLY = (not os.getenv("SESSION_COOKIE_HTTPONLY")) # Defaults True SESSION_COOKIE_HTTPONLY = not os.getenv("SESSION_COOKIE_HTTPONLY") # Defaults True
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or 'Lax' 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 PERMANENT_SESSION_LIFETIME = int(
os.getenv("PERMANENT_SESSION_LIFETIME") or 604800
) # 7 days in seconds
TRUSTED_PROXIES = [ TRUSTED_PROXIES = [
r'^127\.0\.0\.1$', r"^127\.0\.0\.1$",
# Remove the following proxies if you do not trust the local network # 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 # For example if you are running a CTF on your laptop and the teams are
# all on the same network # all on the same network
r'^::1$', r"^::1$",
r'^fc00:', r"^fc00:",
r'^10\.', r"^10\.",
r'^172\.(1[6-9]|2[0-9]|3[0-1])\.', r"^172\.(1[6-9]|2[0-9]|3[0-1])\.",
r'^192\.168\.' r"^192\.168\.",
] ]
''' """
=== EMAIL === === EMAIL ===
MAILFROM_ADDR: MAILFROM_ADDR:
@ -139,7 +147,7 @@ class Config(object):
MAILGUN_BASE_URL MAILGUN_BASE_URL
Mailgun base url to send email over Mailgun Mailgun base url to send email over Mailgun
''' """
MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io" MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io"
MAIL_SERVER = os.getenv("MAIL_SERVER") or None MAIL_SERVER = os.getenv("MAIL_SERVER") or None
MAIL_PORT = os.getenv("MAIL_PORT") MAIL_PORT = os.getenv("MAIL_PORT")
@ -151,15 +159,17 @@ class Config(object):
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY") MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY")
MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL") MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL")
''' """
=== LOGS === === LOGS ===
LOG_FOLDER: LOG_FOLDER:
The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins. 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. 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 === === UPLOADS ===
UPLOAD_PROVIDER: UPLOAD_PROVIDER:
@ -180,16 +190,18 @@ class Config(object):
AWS_S3_ENDPOINT_URL: AWS_S3_ENDPOINT_URL:
A URL pointing to a custom S3 implementation. A URL pointing to a custom S3 implementation.
''' """
UPLOAD_PROVIDER = os.getenv('UPLOAD_PROVIDER') or 'filesystem' 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') UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER") or os.path.join(
if UPLOAD_PROVIDER == 's3': os.path.dirname(os.path.abspath(__file__)), "uploads"
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') )
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') if UPLOAD_PROVIDER == "s3":
AWS_S3_BUCKET = os.getenv('AWS_S3_BUCKET') AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_S3_ENDPOINT_URL = os.getenv('AWS_S3_ENDPOINT_URL') 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 === === OPTIONAL ===
REVERSE_PROXY: REVERSE_PROXY:
@ -216,33 +228,35 @@ class Config(object):
APPLICATION_ROOT: APPLICATION_ROOT:
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory. Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
Example: /ctfd Example: /ctfd
''' """
REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False
TEMPLATES_AUTO_RELOAD = (not os.getenv("TEMPLATES_AUTO_RELOAD")) # Defaults True TEMPLATES_AUTO_RELOAD = not os.getenv("TEMPLATES_AUTO_RELOAD") # Defaults True
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None # Defaults False SQLALCHEMY_TRACK_MODIFICATIONS = (
SWAGGER_UI = '/' if os.getenv("SWAGGER_UI") is not None else False # Defaults False os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None
UPDATE_CHECK = (not os.getenv("UPDATE_CHECK")) # Defaults True ) # Defaults False
APPLICATION_ROOT = os.getenv('APPLICATION_ROOT') or '/' 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 === === OAUTH ===
MajorLeagueCyber Integration MajorLeagueCyber Integration
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here 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_ID = os.getenv("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET") OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET")
class TestingConfig(Config): class TestingConfig(Config):
SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA' SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA"
PRESERVE_CONTEXT_ON_EXCEPTION = False PRESERVE_CONTEXT_ON_EXCEPTION = False
TESTING = True TESTING = True
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = os.getenv('TESTING_DATABASE_URL') or 'sqlite://' SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://"
SERVER_NAME = 'localhost' SERVER_NAME = "localhost"
UPDATE_CHECK = False UPDATE_CHECK = False
REDIS_URL = None REDIS_URL = None
CACHE_TYPE = 'simple' CACHE_TYPE = "simple"
CACHE_THRESHOLD = 500 CACHE_THRESHOLD = 500
SAFE_MODE = True SAFE_MODE = True

View File

@ -3,19 +3,19 @@ from flask import render_template
# 404 # 404
def page_not_found(error): 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 # 403
def forbidden(error): def forbidden(error):
return render_template('errors/403.html', error=error.description), 403 return render_template("errors/403.html", error=error.description), 403
# 500 # 500
def general_error(error): def general_error(error):
return render_template('errors/500.html'), 500 return render_template("errors/500.html"), 500
# 502 # 502
def gateway_error(error): def gateway_error(error):
return render_template('errors/502.html', error=error.description), 502 return render_template("errors/502.html", error=error.description), 502

View File

@ -1,7 +1,7 @@
from flask import current_app, Blueprint, Response, stream_with_context from flask import current_app, Blueprint, Response, stream_with_context
from CTFd.utils.decorators import authed_only, ratelimit from CTFd.utils.decorators import authed_only, ratelimit
events = Blueprint('events', __name__) events = Blueprint("events", __name__)
@events.route("/events") @events.route("/events")

View File

@ -20,29 +20,29 @@ def get_class_by_tablename(tablename):
:return: Class reference or None. :return: Class reference or None.
""" """
for c in db.Model._decl_class_registry.values(): 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 c
return None return None
class Notifications(db.Model): class Notifications(db.Model):
__tablename__ = 'notifications' __tablename__ = "notifications"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text) title = db.Column(db.Text)
content = db.Column(db.Text) content = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow) date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
team_id = db.Column(db.Integer, db.ForeignKey('teams.id')) team_id = db.Column(db.Integer, db.ForeignKey("teams.id"))
user = db.relationship('Users', foreign_keys="Notifications.user_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') team = db.relationship("Teams", foreign_keys="Notifications.team_id", lazy="select")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Notifications, self).__init__(**kwargs) super(Notifications, self).__init__(**kwargs)
class Pages(db.Model): class Pages(db.Model):
__tablename__ = 'pages' __tablename__ = "pages"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80)) title = db.Column(db.String(80))
route = db.Column(db.String(128), unique=True) route = db.Column(db.String(128), unique=True)
@ -62,7 +62,7 @@ class Pages(db.Model):
class Challenges(db.Model): class Challenges(db.Model):
__tablename__ = 'challenges' __tablename__ = "challenges"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80)) name = db.Column(db.String(80))
description = db.Column(db.Text) description = db.Column(db.Text)
@ -70,7 +70,7 @@ class Challenges(db.Model):
value = db.Column(db.Integer) value = db.Column(db.Integer)
category = db.Column(db.String(80)) category = db.Column(db.String(80))
type = 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) requirements = db.Column(db.JSON)
files = db.relationship("ChallengeFiles", backref="challenge") files = db.relationship("ChallengeFiles", backref="challenge")
@ -78,31 +78,27 @@ class Challenges(db.Model):
hints = db.relationship("Hints", backref="challenge") hints = db.relationship("Hints", backref="challenge")
flags = db.relationship("Flags", backref="challenge") flags = db.relationship("Flags", backref="challenge")
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Challenges, self).__init__(**kwargs) super(Challenges, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return '<Challenge %r>' % self.name return "<Challenge %r>" % self.name
class Hints(db.Model): class Hints(db.Model):
__tablename__ = 'hints' __tablename__ = "hints"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(80), default='standard') type = db.Column(db.String(80), default="standard")
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) challenge_id = db.Column(
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
)
content = db.Column(db.Text) content = db.Column(db.Text)
cost = db.Column(db.Integer, default=0) cost = db.Column(db.Integer, default=0)
requirements = db.Column(db.JSON) requirements = db.Column(db.JSON)
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
@property @property
def name(self): def name(self):
@ -120,15 +116,15 @@ class Hints(db.Model):
super(Hints, self).__init__(**kwargs) super(Hints, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return '<Hint %r>' % self.content return "<Hint %r>" % self.content
class Awards(db.Model): class Awards(db.Model):
__tablename__ = 'awards' __tablename__ = "awards"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.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')) team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
type = db.Column(db.String(80), default='standard') type = db.Column(db.String(80), default="standard")
name = db.Column(db.String(80)) name = db.Column(db.String(80))
description = db.Column(db.Text) description = db.Column(db.Text)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow) date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
@ -137,33 +133,32 @@ class Awards(db.Model):
icon = db.Column(db.Text) icon = db.Column(db.Text)
requirements = db.Column(db.JSON) requirements = db.Column(db.JSON)
user = db.relationship('Users', foreign_keys="Awards.user_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') team = db.relationship("Teams", foreign_keys="Awards.team_id", lazy="select")
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
@hybrid_property @hybrid_property
def account_id(self): def account_id(self):
user_mode = get_config('user_mode') user_mode = get_config("user_mode")
if user_mode == 'teams': if user_mode == "teams":
return self.team_id return self.team_id
elif user_mode == 'users': elif user_mode == "users":
return self.user_id return self.user_id
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Awards, self).__init__(**kwargs) super(Awards, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return '<Award %r>' % self.name return "<Award %r>" % self.name
class Tags(db.Model): class Tags(db.Model):
__tablename__ = 'tags' __tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True) 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)) value = db.Column(db.String(80))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -171,54 +166,51 @@ class Tags(db.Model):
class Files(db.Model): class Files(db.Model):
__tablename__ = 'files' __tablename__ = "files"
id = db.Column(db.Integer, primary_key=True) 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) location = db.Column(db.Text)
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
'polymorphic_identity': 'standard',
'polymorphic_on': type
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Files, self).__init__(**kwargs) super(Files, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return "<File type={type} location={location}>".format(type=self.type, location=self.location) return "<File type={type} location={location}>".format(
type=self.type, location=self.location
)
class ChallengeFiles(Files): class ChallengeFiles(Files):
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "challenge"}
'polymorphic_identity': 'challenge' challenge_id = db.Column(
} db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ChallengeFiles, self).__init__(**kwargs) super(ChallengeFiles, self).__init__(**kwargs)
class PageFiles(Files): class PageFiles(Files):
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "page"}
'polymorphic_identity': 'page' page_id = db.Column(db.Integer, db.ForeignKey("pages.id"))
}
page_id = db.Column(db.Integer, db.ForeignKey('pages.id'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PageFiles, self).__init__(**kwargs) super(PageFiles, self).__init__(**kwargs)
class Flags(db.Model): class Flags(db.Model):
__tablename__ = 'flags' __tablename__ = "flags"
id = db.Column(db.Integer, primary_key=True) 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)) type = db.Column(db.String(80))
content = db.Column(db.Text) content = db.Column(db.Text)
data = db.Column(db.Text) data = db.Column(db.Text)
__mapper_args__ = { __mapper_args__ = {"polymorphic_on": type}
'polymorphic_on': type
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Flags, self).__init__(**kwargs) super(Flags, self).__init__(**kwargs)
@ -228,11 +220,8 @@ class Flags(db.Model):
class Users(db.Model): class Users(db.Model):
__tablename__ = 'users' __tablename__ = "users"
__table_args__ = ( __table_args__ = (db.UniqueConstraint("id", "oauth_id"), {})
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes # Core attributes
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True) oauth_id = db.Column(db.Integer, unique=True)
@ -253,28 +242,25 @@ class Users(db.Model):
verified = db.Column(db.Boolean, default=False) verified = db.Column(db.Boolean, default=False)
# Relationship for Teams # 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) created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "user", "polymorphic_on": type}
'polymorphic_identity': 'user',
'polymorphic_on': type
}
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Users, self).__init__(**kwargs) super(Users, self).__init__(**kwargs)
@validates('password') @validates("password")
def validate_password(self, key, plaintext): def validate_password(self, key, plaintext):
return hash_password(str(plaintext)) return hash_password(str(plaintext))
@hybrid_property @hybrid_property
def account_id(self): def account_id(self):
user_mode = get_config('user_mode') user_mode = get_config("user_mode")
if user_mode == 'teams': if user_mode == "teams":
return self.team_id return self.team_id
elif user_mode == 'users': elif user_mode == "users":
return self.id return self.id
@property @property
@ -299,7 +285,7 @@ class Users(db.Model):
def get_solves(self, admin=False): def get_solves(self, admin=False):
solves = Solves.query.filter_by(user_id=self.id) solves = Solves.query.filter_by(user_id=self.id)
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt) solves = solves.filter(Solves.date < dt)
@ -307,7 +293,7 @@ class Users(db.Model):
def get_fails(self, admin=False): def get_fails(self, admin=False):
fails = Fails.query.filter_by(user_id=self.id) fails = Fails.query.filter_by(user_id=self.id)
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Fails.date < dt) fails = fails.filter(Fails.date < dt)
@ -315,27 +301,26 @@ class Users(db.Model):
def get_awards(self, admin=False): def get_awards(self, admin=False):
awards = Awards.query.filter_by(user_id=self.id) awards = Awards.query.filter_by(user_id=self.id)
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Awards.date < dt) awards = awards.filter(Awards.date < dt)
return awards.all() return awards.all()
def get_score(self, admin=False): def get_score(self, admin=False):
score = db.func.sum(Challenges.value).label('score') score = db.func.sum(Challenges.value).label("score")
user = db.session.query( user = (
Solves.user_id, db.session.query(Solves.user_id, score)
score .join(Users, Solves.user_id == Users.id)
) \ .join(Challenges, Solves.challenge_id == Challenges.id)
.join(Users, Solves.user_id == Users.id) \
.join(Challenges, Solves.challenge_id == Challenges.id) \
.filter(Users.id == self.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) award = db.session.query(award_score).filter_by(user_id=self.id)
if not admin: if not admin:
freeze = Configs.query.filter_by(key='freeze').first() freeze = Configs.query.filter_by(key="freeze").first()
if freeze and freeze.value: if freeze and freeze.value:
freeze = int(freeze.value) freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze) freeze = datetime.datetime.utcfromtimestamp(freeze)
@ -361,50 +346,63 @@ class Users(db.Model):
to no imports within the CTFd application as importing from the to no imports within the CTFd application as importing from the
application itself will result in a circular import. application itself will result in a circular import.
""" """
scores = db.session.query( scores = (
Solves.user_id.label('user_id'), db.session.query(
db.func.sum(Challenges.value).label('score'), Solves.user_id.label("user_id"),
db.func.max(Solves.id).label('id'), db.func.sum(Challenges.value).label("score"),
db.func.max(Solves.date).label('date') db.func.max(Solves.id).label("id"),
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id) db.func.max(Solves.date).label("date"),
)
.join(Challenges)
.filter(Challenges.value != 0)
.group_by(Solves.user_id)
)
awards = db.session.query( awards = (
Awards.user_id.label('user_id'), db.session.query(
db.func.sum(Awards.value).label('score'), Awards.user_id.label("user_id"),
db.func.max(Awards.id).label('id'), db.func.sum(Awards.value).label("score"),
db.func.max(Awards.date).label('date') db.func.max(Awards.id).label("id"),
).filter(Awards.value != 0).group_by(Awards.user_id) db.func.max(Awards.date).label("date"),
)
.filter(Awards.value != 0)
.group_by(Awards.user_id)
)
if not admin: if not admin:
freeze = Configs.query.filter_by(key='freeze').first() freeze = Configs.query.filter_by(key="freeze").first()
if freeze and freeze.value: if freeze and freeze.value:
freeze = int(freeze.value) freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze) freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze) scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.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( sumscores = (
db.session.query(
results.columns.user_id, results.columns.user_id,
db.func.sum(results.columns.score).label('score'), db.func.sum(results.columns.score).label("score"),
db.func.max(results.columns.id).label('id'), db.func.max(results.columns.id).label("id"),
db.func.max(results.columns.date).label('date') db.func.max(results.columns.date).label("date"),
).group_by(results.columns.user_id).subquery() )
.group_by(results.columns.user_id)
.subquery()
)
if admin: if admin:
standings_query = db.session.query( standings_query = (
Users.id.label('user_id'), db.session.query(Users.id.label("user_id"))
) \ .join(sumscores, Users.id == sumscores.columns.user_id)
.join(sumscores, Users.id == sumscores.columns.user_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
else: else:
standings_query = db.session.query( standings_query = (
Users.id.label('user_id'), db.session.query(Users.id.label("user_id"))
) \ .join(sumscores, Users.id == sumscores.columns.user_id)
.join(sumscores, Users.id == sumscores.columns.user_id) \ .filter(Users.banned == False, Users.hidden == False)
.filter(Users.banned == False, Users.hidden == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
standings = standings_query.all() standings = standings_query.all()
@ -421,18 +419,13 @@ class Users(db.Model):
class Admins(Users): class Admins(Users):
__tablename__ = 'admins' __tablename__ = "admins"
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "admin"}
'polymorphic_identity': 'admin'
}
class Teams(db.Model): class Teams(db.Model):
__tablename__ = 'teams' __tablename__ = "teams"
__table_args__ = ( __table_args__ = (db.UniqueConstraint("id", "oauth_id"), {})
db.UniqueConstraint('id', 'oauth_id'),
{}
)
# Core attributes # Core attributes
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
oauth_id = db.Column(db.Integer, unique=True) oauth_id = db.Column(db.Integer, unique=True)
@ -442,7 +435,7 @@ class Teams(db.Model):
password = db.Column(db.String(128)) password = db.Column(db.String(128))
secret = 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 # Supplementary attributes
website = db.Column(db.String(128)) website = db.Column(db.String(128))
@ -453,7 +446,7 @@ class Teams(db.Model):
banned = db.Column(db.Boolean, default=False) banned = db.Column(db.Boolean, default=False)
# Relationship for Users # 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]) captain = db.relationship("Users", foreign_keys=[captain_id])
created = db.Column(db.DateTime, default=datetime.datetime.utcnow) created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
@ -461,7 +454,7 @@ class Teams(db.Model):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Teams, self).__init__(**kwargs) super(Teams, self).__init__(**kwargs)
@validates('password') @validates("password")
def validate_password(self, key, plaintext): def validate_password(self, key, plaintext):
return hash_password(str(plaintext)) return hash_password(str(plaintext))
@ -488,13 +481,11 @@ class Teams(db.Model):
def get_solves(self, admin=False): def get_solves(self, admin=False):
member_ids = [member.id for member in self.members] member_ids = [member.id for member in self.members]
solves = Solves.query.filter( solves = Solves.query.filter(Solves.user_id.in_(member_ids)).order_by(
Solves.user_id.in_(member_ids)
).order_by(
Solves.date.asc() Solves.date.asc()
) )
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
solves = solves.filter(Solves.date < dt) solves = solves.filter(Solves.date < dt)
@ -504,13 +495,11 @@ class Teams(db.Model):
def get_fails(self, admin=False): def get_fails(self, admin=False):
member_ids = [member.id for member in self.members] member_ids = [member.id for member in self.members]
fails = Fails.query.filter( fails = Fails.query.filter(Fails.user_id.in_(member_ids)).order_by(
Fails.user_id.in_(member_ids)
).order_by(
Fails.date.asc() Fails.date.asc()
) )
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
fails = fails.filter(Fails.date < dt) fails = fails.filter(Fails.date < dt)
@ -520,13 +509,11 @@ class Teams(db.Model):
def get_awards(self, admin=False): def get_awards(self, admin=False):
member_ids = [member.id for member in self.members] member_ids = [member.id for member in self.members]
awards = Awards.query.filter( awards = Awards.query.filter(Awards.user_id.in_(member_ids)).order_by(
Awards.user_id.in_(member_ids)
).order_by(
Awards.date.asc() Awards.date.asc()
) )
freeze = get_config('freeze') freeze = get_config("freeze")
if freeze and admin is False: if freeze and admin is False:
dt = datetime.datetime.utcfromtimestamp(freeze) dt = datetime.datetime.utcfromtimestamp(freeze)
awards = awards.filter(Awards.date < dt) 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 to no imports within the CTFd application as importing from the
application itself will result in a circular import. application itself will result in a circular import.
""" """
scores = db.session.query( scores = (
Solves.team_id.label('team_id'), db.session.query(
db.func.sum(Challenges.value).label('score'), Solves.team_id.label("team_id"),
db.func.max(Solves.id).label('id'), db.func.sum(Challenges.value).label("score"),
db.func.max(Solves.date).label('date') db.func.max(Solves.id).label("id"),
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id) db.func.max(Solves.date).label("date"),
)
.join(Challenges)
.filter(Challenges.value != 0)
.group_by(Solves.team_id)
)
awards = db.session.query( awards = (
Awards.team_id.label('team_id'), db.session.query(
db.func.sum(Awards.value).label('score'), Awards.team_id.label("team_id"),
db.func.max(Awards.id).label('id'), db.func.sum(Awards.value).label("score"),
db.func.max(Awards.date).label('date') db.func.max(Awards.id).label("id"),
).filter(Awards.value != 0).group_by(Awards.team_id) db.func.max(Awards.date).label("date"),
)
.filter(Awards.value != 0)
.group_by(Awards.team_id)
)
if not admin: if not admin:
freeze = Configs.query.filter_by(key='freeze').first() freeze = Configs.query.filter_by(key="freeze").first()
if freeze and freeze.value: if freeze and freeze.value:
freeze = int(freeze.value) freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze) freeze = datetime.datetime.utcfromtimestamp(freeze)
scores = scores.filter(Solves.date < freeze) scores = scores.filter(Solves.date < freeze)
awards = awards.filter(Awards.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( sumscores = (
db.session.query(
results.columns.team_id, results.columns.team_id,
db.func.sum(results.columns.score).label('score'), db.func.sum(results.columns.score).label("score"),
db.func.max(results.columns.id).label('id'), db.func.max(results.columns.id).label("id"),
db.func.max(results.columns.date).label('date') db.func.max(results.columns.date).label("date"),
).group_by(results.columns.team_id).subquery() )
.group_by(results.columns.team_id)
.subquery()
)
if admin: if admin:
standings_query = db.session.query( standings_query = (
Teams.id.label('team_id'), db.session.query(Teams.id.label("team_id"))
) \ .join(sumscores, Teams.id == sumscores.columns.team_id)
.join(sumscores, Teams.id == sumscores.columns.team_id) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
else: else:
standings_query = db.session.query( standings_query = (
Teams.id.label('team_id'), db.session.query(Teams.id.label("team_id"))
) \ .join(sumscores, Teams.id == sumscores.columns.team_id)
.join(sumscores, Teams.id == sumscores.columns.team_id) \ .filter(Teams.banned == False)
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.id) .order_by(sumscores.columns.score.desc(), sumscores.columns.id)
)
standings = standings_query.all() standings = standings_query.all()
@ -603,39 +603,41 @@ class Teams(db.Model):
class Submissions(db.Model): class Submissions(db.Model):
__tablename__ = 'submissions' __tablename__ = "submissions"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')) challenge_id = db.Column(
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) db.Integer, db.ForeignKey("challenges.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"))
ip = db.Column(db.String(46)) ip = db.Column(db.String(46))
provided = db.Column(db.Text) provided = db.Column(db.Text)
type = db.Column(db.String(32)) type = db.Column(db.String(32))
date = db.Column(db.DateTime, default=datetime.datetime.utcnow) date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
# Relationships # Relationships
user = db.relationship('Users', foreign_keys="Submissions.user_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') team = db.relationship("Teams", foreign_keys="Submissions.team_id", lazy="select")
challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select') challenge = db.relationship(
"Challenges", foreign_keys="Submissions.challenge_id", lazy="select"
)
__mapper_args__ = { __mapper_args__ = {"polymorphic_on": type}
'polymorphic_on': type,
}
@hybrid_property @hybrid_property
def account_id(self): def account_id(self):
user_mode = get_config('user_mode') user_mode = get_config("user_mode")
if user_mode == 'teams': if user_mode == "teams":
return self.team_id return self.team_id
elif user_mode == 'users': elif user_mode == "users":
return self.user_id return self.user_id
@hybrid_property @hybrid_property
def account(self): def account(self):
user_mode = get_config('user_mode') user_mode = get_config("user_mode")
if user_mode == 'teams': if user_mode == "teams":
return self.team return self.team
elif user_mode == 'users': elif user_mode == "users":
return self.user return self.user
@staticmethod @staticmethod
@ -647,91 +649,95 @@ class Submissions(db.Model):
return child_classes[type] return child_classes[type]
def __repr__(self): def __repr__(self):
return '<Submission {}, {}, {}, {}>'.format(self.team_id, self.challenge_id, self.ip, self.provided) return "<Submission {}, {}, {}, {}>".format(
self.team_id, self.challenge_id, self.ip, self.provided
)
class Solves(Submissions): class Solves(Submissions):
__tablename__ = 'solves' __tablename__ = "solves"
__table_args__ = ( __table_args__ = (
db.UniqueConstraint('challenge_id', 'user_id'), db.UniqueConstraint("challenge_id", "user_id"),
db.UniqueConstraint('challenge_id', 'team_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') user = db.relationship("Users", foreign_keys="Solves.user_id", lazy="select")
team = db.relationship('Teams', foreign_keys="Solves.team_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') challenge = db.relationship(
"Challenges", foreign_keys="Solves.challenge_id", lazy="select"
)
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "correct"}
'polymorphic_identity': 'correct'
}
class Fails(Submissions): class Fails(Submissions):
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "incorrect"}
'polymorphic_identity': 'incorrect'
}
class Unlocks(db.Model): class Unlocks(db.Model):
__tablename__ = 'unlocks' __tablename__ = "unlocks"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.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')) team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
target = db.Column(db.Integer) target = db.Column(db.Integer)
date = db.Column(db.DateTime, default=datetime.datetime.utcnow) date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
type = db.Column(db.String(32)) type = db.Column(db.String(32))
__mapper_args__ = { __mapper_args__ = {"polymorphic_on": type}
'polymorphic_on': type,
}
@hybrid_property @hybrid_property
def account_id(self): def account_id(self):
user_mode = get_config('user_mode') user_mode = get_config("user_mode")
if user_mode == 'teams': if user_mode == "teams":
return self.team_id return self.team_id
elif user_mode == 'users': elif user_mode == "users":
return self.user_id return self.user_id
def __repr__(self): def __repr__(self):
return '<Unlock %r>' % self.id return "<Unlock %r>" % self.id
class HintUnlocks(Unlocks): class HintUnlocks(Unlocks):
__mapper_args__ = { __mapper_args__ = {"polymorphic_identity": "hints"}
'polymorphic_identity': 'hints'
}
class Tracking(db.Model): class Tracking(db.Model):
__tablename__ = 'tracking' __tablename__ = "tracking"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(32)) type = db.Column(db.String(32))
ip = db.Column(db.String(46)) 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) 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__ = { __mapper_args__ = {"polymorphic_on": type}
'polymorphic_on': type,
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Tracking, self).__init__(**kwargs) super(Tracking, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return '<Tracking %r>' % self.ip return "<Tracking %r>" % self.ip
class Configs(db.Model): class Configs(db.Model):
__tablename__ = 'config' __tablename__ = "config"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.Text) key = db.Column(db.Text)
value = db.Column(db.Text) value = db.Column(db.Text)
@ -751,9 +757,9 @@ def get_config(key):
if value and value.isdigit(): if value and value.isdigit():
return int(value) return int(value)
elif value and isinstance(value, six.string_types): elif value and isinstance(value, six.string_types):
if value.lower() == 'true': if value.lower() == "true":
return True return True
elif value.lower() == 'false': elif value.lower() == "false":
return False return False
else: else:
return value return value

View File

@ -10,12 +10,12 @@ from CTFd.utils.plugins import (
register_script as utils_register_plugin_script, register_script as utils_register_plugin_script,
register_stylesheet as utils_register_plugin_stylesheet, register_stylesheet as utils_register_plugin_stylesheet,
register_admin_script as utils_register_admin_plugin_script, 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 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): 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 :param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public
:return: :return:
""" """
base_path = base_path.strip('/') base_path = base_path.strip("/")
def assets_handler(path): def assets_handler(path):
return send_from_directory(base_path, path) return send_from_directory(base_path, path)
rule = '/' + base_path + '/<path:path>' rule = "/" + base_path + "/<path:path>"
app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler) 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 :param boolean admins_only: Whether or not this file should be accessible to the public
:return: :return:
""" """
asset_path = asset_path.strip('/') asset_path = asset_path.strip("/")
def asset_handler(): def asset_handler():
return send_file(asset_path) return send_file(asset_path)
if admins_only: if admins_only:
asset_handler = admins_only_wrapper(asset_handler) 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) 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.admin_plugin_menu_bar = []
app.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__) + "/*")) modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
blacklist = {'__pycache__'} blacklist = {"__pycache__"}
for module in modules: for module in modules:
module_name = os.path.basename(module) module_name = os.path.basename(module)
if os.path.isdir(module) and module_name not in blacklist: if os.path.isdir(module) and module_name not in blacklist:
module = '.' + module_name module = "." + module_name
module = importlib.import_module(module, package='CTFd.plugins') module = importlib.import_module(module, package="CTFd.plugins")
module.load(app) module.load(app)
print(" * Loaded module, %s" % module) print(" * Loaded module, %s" % module)

View File

@ -1,6 +1,15 @@
from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins import register_plugin_assets_directory
from CTFd.plugins.flags import get_flag_class 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.user import get_ip
from CTFd.utils.uploads import delete_file from CTFd.utils.uploads import delete_file
from flask import Blueprint from flask import Blueprint
@ -17,19 +26,21 @@ class CTFdStandardChallenge(BaseChallenge):
id = "standard" # Unique identifier used to register challenges id = "standard" # Unique identifier used to register challenges
name = "standard" # Name of a challenge type name = "standard" # Name of a challenge type
templates = { # Templates used for each aspect of challenge editing & viewing templates = { # Templates used for each aspect of challenge editing & viewing
'create': '/plugins/challenges/assets/create.html', "create": "/plugins/challenges/assets/create.html",
'update': '/plugins/challenges/assets/update.html', "update": "/plugins/challenges/assets/update.html",
'view': '/plugins/challenges/assets/view.html', "view": "/plugins/challenges/assets/view.html",
} }
scripts = { # Scripts that are loaded when a template is loaded scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/challenges/assets/create.js', "create": "/plugins/challenges/assets/create.js",
'update': '/plugins/challenges/assets/update.js', "update": "/plugins/challenges/assets/update.js",
'view': '/plugins/challenges/assets/view.js', "view": "/plugins/challenges/assets/view.js",
} }
# Route at which files are accessible. This must be registered using register_plugin_assets_directory() # 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 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 @staticmethod
def create(request): def create(request):
@ -57,20 +68,20 @@ class CTFdStandardChallenge(BaseChallenge):
:return: Challenge object, data dictionary to be returned to the user :return: Challenge object, data dictionary to be returned to the user
""" """
data = { data = {
'id': challenge.id, "id": challenge.id,
'name': challenge.name, "name": challenge.name,
'value': challenge.value, "value": challenge.value,
'description': challenge.description, "description": challenge.description,
'category': challenge.category, "category": challenge.category,
'state': challenge.state, "state": challenge.state,
'max_attempts': challenge.max_attempts, "max_attempts": challenge.max_attempts,
'type': challenge.type, "type": challenge.type,
'type_data': { "type_data": {
'id': CTFdStandardChallenge.id, "id": CTFdStandardChallenge.id,
'name': CTFdStandardChallenge.name, "name": CTFdStandardChallenge.name,
'templates': CTFdStandardChallenge.templates, "templates": CTFdStandardChallenge.templates,
'scripts': CTFdStandardChallenge.scripts, "scripts": CTFdStandardChallenge.scripts,
} },
} }
return data return data
@ -123,12 +134,12 @@ class CTFdStandardChallenge(BaseChallenge):
:return: (boolean, string) :return: (boolean, string)
""" """
data = request.form or request.get_json() 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() flags = Flags.query.filter_by(challenge_id=challenge.id).all()
for flag in flags: for flag in flags:
if get_flag_class(flag.type).compare(flag, submission): if get_flag_class(flag.type).compare(flag, submission):
return True, 'Correct' return True, "Correct"
return False, 'Incorrect' return False, "Incorrect"
@staticmethod @staticmethod
def solve(user, team, challenge, request): def solve(user, team, challenge, request):
@ -141,13 +152,13 @@ class CTFdStandardChallenge(BaseChallenge):
:return: :return:
""" """
data = request.form or request.get_json() data = request.form or request.get_json()
submission = data['submission'].strip() submission = data["submission"].strip()
solve = Solves( solve = Solves(
user_id=user.id, user_id=user.id,
team_id=team.id if team else None, team_id=team.id if team else None,
challenge_id=challenge.id, challenge_id=challenge.id,
ip=get_ip(req=request), ip=get_ip(req=request),
provided=submission provided=submission,
) )
db.session.add(solve) db.session.add(solve)
db.session.commit() db.session.commit()
@ -164,13 +175,13 @@ class CTFdStandardChallenge(BaseChallenge):
:return: :return:
""" """
data = request.form or request.get_json() data = request.form or request.get_json()
submission = data['submission'].strip() submission = data["submission"].strip()
wrong = Fails( wrong = Fails(
user_id=user.id, user_id=user.id,
team_id=team.id if team else None, team_id=team.id if team else None,
challenge_id=challenge.id, challenge_id=challenge.id,
ip=get_ip(request), ip=get_ip(request),
provided=submission provided=submission,
) )
db.session.add(wrong) db.session.add(wrong)
db.session.commit() 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 Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register
your Challenge Type. your Challenge Type.
""" """
CHALLENGE_CLASSES = { CHALLENGE_CLASSES = {"standard": CTFdStandardChallenge}
"standard": CTFdStandardChallenge
}
def load(app): def load(app):
register_plugin_assets_directory(app, base_path='/plugins/challenges/assets/') register_plugin_assets_directory(app, base_path="/plugins/challenges/assets/")

View File

@ -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.challenges import BaseChallenge, CHALLENGE_CLASSES
from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins import register_plugin_assets_directory
from CTFd.plugins.flags import get_flag_class 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.user import get_ip
from CTFd.utils.uploads import delete_file from CTFd.utils.uploads import delete_file
from CTFd.utils.modes import get_model from CTFd.utils.modes import get_model
@ -14,19 +23,24 @@ class DynamicValueChallenge(BaseChallenge):
id = "dynamic" # Unique identifier used to register challenges id = "dynamic" # Unique identifier used to register challenges
name = "dynamic" # Name of a challenge type name = "dynamic" # Name of a challenge type
templates = { # Handlebars templates used for each aspect of challenge editing & viewing templates = { # Handlebars templates used for each aspect of challenge editing & viewing
'create': '/plugins/dynamic_challenges/assets/create.html', "create": "/plugins/dynamic_challenges/assets/create.html",
'update': '/plugins/dynamic_challenges/assets/update.html', "update": "/plugins/dynamic_challenges/assets/update.html",
'view': '/plugins/dynamic_challenges/assets/view.html', "view": "/plugins/dynamic_challenges/assets/view.html",
} }
scripts = { # Scripts that are loaded when a template is loaded scripts = { # Scripts that are loaded when a template is loaded
'create': '/plugins/dynamic_challenges/assets/create.js', "create": "/plugins/dynamic_challenges/assets/create.js",
'update': '/plugins/dynamic_challenges/assets/update.js', "update": "/plugins/dynamic_challenges/assets/update.js",
'view': '/plugins/dynamic_challenges/assets/view.js', "view": "/plugins/dynamic_challenges/assets/view.js",
} }
# Route at which files are accessible. This must be registered using register_plugin_assets_directory() # 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 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 @staticmethod
def create(request): def create(request):
@ -54,23 +68,23 @@ class DynamicValueChallenge(BaseChallenge):
""" """
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first() challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = { data = {
'id': challenge.id, "id": challenge.id,
'name': challenge.name, "name": challenge.name,
'value': challenge.value, "value": challenge.value,
'initial': challenge.initial, "initial": challenge.initial,
'decay': challenge.decay, "decay": challenge.decay,
'minimum': challenge.minimum, "minimum": challenge.minimum,
'description': challenge.description, "description": challenge.description,
'category': challenge.category, "category": challenge.category,
'state': challenge.state, "state": challenge.state,
'max_attempts': challenge.max_attempts, "max_attempts": challenge.max_attempts,
'type': challenge.type, "type": challenge.type,
'type_data': { "type_data": {
'id': DynamicValueChallenge.id, "id": DynamicValueChallenge.id,
'name': DynamicValueChallenge.name, "name": DynamicValueChallenge.name,
'templates': DynamicValueChallenge.templates, "templates": DynamicValueChallenge.templates,
'scripts': DynamicValueChallenge.scripts, "scripts": DynamicValueChallenge.scripts,
} },
} }
return data return data
@ -88,20 +102,28 @@ class DynamicValueChallenge(BaseChallenge):
for attr, value in data.items(): for attr, value in data.items():
# We need to set these to floats so that the next operations don't operate on strings # 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) value = float(value)
setattr(challenge, attr, value) setattr(challenge, attr, value)
Model = get_model() Model = get_model()
solve_count = Solves.query \ solve_count = (
.join(Model, Solves.account_id == Model.id) \ Solves.query.join(Model, Solves.account_id == Model.id)
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \ .filter(
Solves.challenge_id == challenge.id,
Model.hidden == False,
Model.banned == False,
)
.count() .count()
)
# It is important that this calculation takes into account floats. # It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division # 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) value = math.ceil(value)
@ -146,12 +168,12 @@ class DynamicValueChallenge(BaseChallenge):
:return: (boolean, string) :return: (boolean, string)
""" """
data = request.form or request.get_json() 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() flags = Flags.query.filter_by(challenge_id=challenge.id).all()
for flag in flags: for flag in flags:
if get_flag_class(flag.type).compare(flag, submission): if get_flag_class(flag.type).compare(flag, submission):
return True, 'Correct' return True, "Correct"
return False, 'Incorrect' return False, "Incorrect"
@staticmethod @staticmethod
def solve(user, team, challenge, request): def solve(user, team, challenge, request):
@ -165,7 +187,7 @@ class DynamicValueChallenge(BaseChallenge):
""" """
chal = DynamicChallenge.query.filter_by(id=challenge.id).first() chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = request.form or request.get_json() data = request.form or request.get_json()
submission = data['submission'].strip() submission = data["submission"].strip()
Model = get_model() Model = get_model()
@ -174,14 +196,19 @@ class DynamicValueChallenge(BaseChallenge):
team_id=team.id if team else None, team_id=team.id if team else None,
challenge_id=challenge.id, challenge_id=challenge.id,
ip=get_ip(req=request), ip=get_ip(req=request),
provided=submission provided=submission,
) )
db.session.add(solve) db.session.add(solve)
solve_count = Solves.query \ solve_count = (
.join(Model, Solves.account_id == Model.id) \ Solves.query.join(Model, Solves.account_id == Model.id)
.filter(Solves.challenge_id == challenge.id, Model.hidden == False, Model.banned == False) \ .filter(
Solves.challenge_id == challenge.id,
Model.hidden == False,
Model.banned == False,
)
.count() .count()
)
# We subtract -1 to allow the first solver to get max point value # We subtract -1 to allow the first solver to get max point value
solve_count -= 1 solve_count -= 1
@ -189,9 +216,7 @@ class DynamicValueChallenge(BaseChallenge):
# It is important that this calculation takes into account floats. # It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division # Hence this file uses from __future__ import division
value = ( value = (
( ((chal.minimum - chal.initial) / (chal.decay ** 2)) * (solve_count ** 2)
(chal.minimum - chal.initial) / (chal.decay**2)
) * (solve_count**2)
) + chal.initial ) + chal.initial
value = math.ceil(value) value = math.ceil(value)
@ -215,13 +240,13 @@ class DynamicValueChallenge(BaseChallenge):
:return: :return:
""" """
data = request.form or request.get_json() data = request.form or request.get_json()
submission = data['submission'].strip() submission = data["submission"].strip()
wrong = Fails( wrong = Fails(
user_id=user.id, user_id=user.id,
team_id=team.id if team else None, team_id=team.id if team else None,
challenge_id=challenge.id, challenge_id=challenge.id,
ip=get_ip(request), ip=get_ip(request),
provided=submission provided=submission,
) )
db.session.add(wrong) db.session.add(wrong)
db.session.commit() db.session.commit()
@ -229,19 +254,21 @@ class DynamicValueChallenge(BaseChallenge):
class DynamicChallenge(Challenges): class DynamicChallenge(Challenges):
__mapper_args__ = {'polymorphic_identity': 'dynamic'} __mapper_args__ = {"polymorphic_identity": "dynamic"}
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True) id = db.Column(None, db.ForeignKey("challenges.id"), primary_key=True)
initial = db.Column(db.Integer, default=0) initial = db.Column(db.Integer, default=0)
minimum = db.Column(db.Integer, default=0) minimum = db.Column(db.Integer, default=0)
decay = db.Column(db.Integer, default=0) decay = db.Column(db.Integer, default=0)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DynamicChallenge, self).__init__(**kwargs) super(DynamicChallenge, self).__init__(**kwargs)
self.initial = kwargs['value'] self.initial = kwargs["value"]
def load(app): def load(app):
# upgrade() # upgrade()
app.db.create_all() app.db.create_all()
CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge CHALLENGE_CLASSES["dynamic"] = DynamicValueChallenge
register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/') register_plugin_assets_directory(
app, base_path="/plugins/dynamic_challenges/assets/"
)

View File

@ -15,8 +15,8 @@ class BaseFlag(object):
class CTFdStaticFlag(BaseFlag): class CTFdStaticFlag(BaseFlag):
name = "static" name = "static"
templates = { # Nunjucks templates used for key editing & viewing templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/flags/assets/static/create.html', "create": "/plugins/flags/assets/static/create.html",
'update': '/plugins/flags/assets/static/edit.html', "update": "/plugins/flags/assets/static/edit.html",
} }
@staticmethod @staticmethod
@ -40,8 +40,8 @@ class CTFdStaticFlag(BaseFlag):
class CTFdRegexFlag(BaseFlag): class CTFdRegexFlag(BaseFlag):
name = "regex" name = "regex"
templates = { # Nunjucks templates used for key editing & viewing templates = { # Nunjucks templates used for key editing & viewing
'create': '/plugins/flags/assets/regex/create.html', "create": "/plugins/flags/assets/regex/create.html",
'update': '/plugins/flags/assets/regex/edit.html', "update": "/plugins/flags/assets/regex/edit.html",
} }
@staticmethod @staticmethod
@ -57,10 +57,7 @@ class CTFdRegexFlag(BaseFlag):
return res and res.group() == provided return res and res.group() == provided
FLAG_CLASSES = { FLAG_CLASSES = {"static": CTFdStaticFlag, "regex": CTFdRegexFlag}
'static': CTFdStaticFlag,
'regex': CTFdRegexFlag
}
def get_flag_class(class_id): def get_flag_class(class_id):
@ -71,4 +68,4 @@ def get_flag_class(class_id):
def load(app): def load(app):
register_plugin_assets_directory(app, base_path='/plugins/flags/assets/') register_plugin_assets_directory(app, base_path="/plugins/flags/assets/")

View File

@ -6,43 +6,43 @@ class AwardSchema(ma.ModelSchema):
class Meta: class Meta:
model = Awards model = Awards
include_fk = True include_fk = True
dump_only = ('id', 'date') dump_only = ("id", "date")
views = { views = {
'admin': [ "admin": [
'category', "category",
'user_id', "user_id",
'name', "name",
'description', "description",
'value', "value",
'team_id', "team_id",
'user', "user",
'team', "team",
'date', "date",
'requirements', "requirements",
'id', "id",
'icon' "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): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(AwardSchema, self).__init__(*args, **kwargs) super(AwardSchema, self).__init__(*args, **kwargs)

View File

@ -5,4 +5,4 @@ class ChallengeSchema(ma.ModelSchema):
class Meta: class Meta:
model = Challenges model = Challenges
include_fk = True include_fk = True
dump_only = ('id',) dump_only = ("id",)

View File

@ -6,21 +6,15 @@ class ConfigSchema(ma.ModelSchema):
class Meta: class Meta:
model = Configs model = Configs
include_fk = True include_fk = True
dump_only = ('id',) dump_only = ("id",)
views = { views = {"admin": ["id", "key", "value"]}
'admin': [
'id',
'key',
'value'
],
}
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(ConfigSchema, self).__init__(*args, **kwargs) super(ConfigSchema, self).__init__(*args, **kwargs)

View File

@ -6,13 +6,13 @@ class FileSchema(ma.ModelSchema):
class Meta: class Meta:
model = Files model = Files
include_fk = True include_fk = True
dump_only = ('id', 'type', 'location') dump_only = ("id", "type", "location")
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(FileSchema, self).__init__(*args, **kwargs) super(FileSchema, self).__init__(*args, **kwargs)

View File

@ -6,13 +6,13 @@ class FlagSchema(ma.ModelSchema):
class Meta: class Meta:
model = Flags model = Flags
include_fk = True include_fk = True
dump_only = ('id',) dump_only = ("id",)
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(FlagSchema, self).__init__(*args, **kwargs) super(FlagSchema, self).__init__(*args, **kwargs)

View File

@ -6,37 +6,19 @@ class HintSchema(ma.ModelSchema):
class Meta: class Meta:
model = Hints model = Hints
include_fk = True include_fk = True
dump_only = ('id', 'type') dump_only = ("id", "type")
views = { views = {
'locked': [ "locked": ["id", "type", "challenge", "cost"],
'id', "unlocked": ["id", "type", "challenge", "content", "cost"],
'type', "admin": ["id", "type", "challenge", "content", "cost", "requirements"],
'challenge',
'cost'
],
'unlocked': [
'id',
'type',
'challenge',
'content',
'cost'
],
'admin': [
'id',
'type',
'challenge',
'content',
'cost',
'requirements'
]
} }
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(HintSchema, self).__init__(*args, **kwargs) super(HintSchema, self).__init__(*args, **kwargs)

View File

@ -6,13 +6,13 @@ class NotificationSchema(ma.ModelSchema):
class Meta: class Meta:
model = Notifications model = Notifications
include_fk = True include_fk = True
dump_only = ('id', 'date') dump_only = ("id", "date")
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(NotificationSchema, self).__init__(*args, **kwargs) super(NotificationSchema, self).__init__(*args, **kwargs)

View File

@ -7,19 +7,19 @@ class PageSchema(ma.ModelSchema):
class Meta: class Meta:
model = Pages model = Pages
include_fk = True include_fk = True
dump_only = ('id', ) dump_only = ("id",)
@pre_load @pre_load
def validate_route(self, data): def validate_route(self, data):
route = data.get('route') route = data.get("route")
if route and route.startswith('/'): if route and route.startswith("/"):
data['route'] = route.strip('/') data["route"] = route.strip("/")
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(PageSchema, self).__init__(*args, **kwargs) super(PageSchema, self).__init__(*args, **kwargs)

View File

@ -5,41 +5,33 @@ from CTFd.utils import string_types
class SubmissionSchema(ma.ModelSchema): class SubmissionSchema(ma.ModelSchema):
challenge = fields.Nested(ChallengeSchema, only=['name', 'category', 'value']) challenge = fields.Nested(ChallengeSchema, only=["name", "category", "value"])
class Meta: class Meta:
model = Submissions model = Submissions
include_fk = True include_fk = True
dump_only = ('id', ) dump_only = ("id",)
views = { views = {
'admin': [ "admin": [
'provided', "provided",
'ip', "ip",
'challenge_id', "challenge_id",
'challenge', "challenge",
'user', "user",
'team', "team",
'date', "date",
'type', "type",
'id' "id",
], ],
'user': [ "user": ["challenge_id", "challenge", "user", "team", "date", "type", "id"],
'challenge_id',
'challenge',
'user',
'team',
'date',
'type',
'id'
]
} }
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(SubmissionSchema, self).__init__(*args, **kwargs) super(SubmissionSchema, self).__init__(*args, **kwargs)

View File

@ -6,24 +6,15 @@ class TagSchema(ma.ModelSchema):
class Meta: class Meta:
model = Tags model = Tags
include_fk = True include_fk = True
dump_only = ('id',) dump_only = ("id",)
views = { views = {"admin": ["id", "challenge", "value"], "user": ["value"]}
'admin': [
'id',
'challenge',
'value'
],
'user': [
'value'
]
}
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(TagSchema, self).__init__(*args, **kwargs) super(TagSchema, self).__init__(*args, **kwargs)

View File

@ -12,44 +12,40 @@ class TeamSchema(ma.ModelSchema):
class Meta: class Meta:
model = Teams model = Teams
include_fk = True include_fk = True
dump_only = ('id', 'oauth_id', 'created', 'members') dump_only = ("id", "oauth_id", "created", "members")
load_only = ('password',) load_only = ("password",)
name = field_for( name = field_for(
Teams, Teams,
'name', "name",
required=True, required=True,
validate=[ 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( email = field_for(
Teams, Teams,
'email', "email",
validate=validate.Email('Emails must be a properly formatted email address') validate=validate.Email("Emails must be a properly formatted email address"),
) )
website = field_for( website = field_for(
Teams, Teams,
'website', "website",
validate=[ validate=[
# This is a dirty hack to let website accept empty strings so you can remove your website # This is a dirty hack to let website accept empty strings so you can remove your website
lambda website: validate.URL( lambda website: validate.URL(
error='Websites must be a proper URL starting with http or https', error="Websites must be a proper URL starting with http or https",
schemes={'http', 'https'} schemes={"http", "https"},
)(website) if website else True )(website)
] if website
) else True
country = field_for( ],
Teams,
'country',
validate=[
validate_country_code
]
) )
country = field_for(Teams, "country", validate=[validate_country_code])
@pre_load @pre_load
def validate_name(self, data): def validate_name(self, data):
name = data.get('name') name = data.get("name")
if name is None: if name is None:
return return
@ -57,57 +53,73 @@ class TeamSchema(ma.ModelSchema):
current_team = get_current_team() current_team = get_current_team()
# Admins should be able to patch anyone but they cannot cause a collision. # Admins should be able to patch anyone but they cannot cause a collision.
if is_admin(): if is_admin():
team_id = int(data.get('id', 0)) team_id = int(data.get("id", 0))
if team_id: if team_id:
if existing_team and existing_team.id != 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: else:
# If there's no Team ID it means that the admin is creating a team with no ID. # If there's no Team ID it means that the admin is creating a team with no ID.
if existing_team: if existing_team:
if current_team: if current_team:
if current_team.id != existing_team.id: 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: else:
raise ValidationError('Team name has already been taken', field_names=['name']) raise ValidationError(
"Team name has already been taken", field_names=["name"]
)
else: else:
# We need to allow teams to edit themselves and allow the "conflict" # 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 return data
else: else:
name_changes = get_config('name_changes', default=True) name_changes = get_config("name_changes", default=True)
if bool(name_changes) is False: 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: 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 @pre_load
def validate_email(self, data): def validate_email(self, data):
email = data.get('email') email = data.get("email")
if email is None: if email is None:
return return
existing_team = Teams.query.filter_by(email=email).first() existing_team = Teams.query.filter_by(email=email).first()
if is_admin(): if is_admin():
team_id = data.get('id') team_id = data.get("id")
if team_id: if team_id:
if existing_team and existing_team.id != 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: else:
if existing_team: 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: else:
current_team = get_current_team() current_team = get_current_team()
if email == current_team.email: if email == current_team.email:
return data return data
else: else:
if existing_team: 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 @pre_load
def validate_password_confirmation(self, data): def validate_password_confirmation(self, data):
password = data.get('password') password = data.get("password")
confirm = data.get('confirm') confirm = data.get("confirm")
if is_admin(): if is_admin():
pass pass
@ -116,29 +128,38 @@ class TeamSchema(ma.ModelSchema):
current_user = get_current_user() current_user = get_current_user()
if current_team.captain_id != current_user.id: 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): 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: 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: if test is True:
return data return data
else: else:
raise ValidationError('Your previous password is incorrect', field_names=['confirm']) raise ValidationError(
"Your previous password is incorrect", field_names=["confirm"]
)
else: else:
data.pop('password', None) data.pop("password", None)
data.pop('confirm', None) data.pop("confirm", None)
@pre_load @pre_load
def validate_captain_id(self, data): def validate_captain_id(self, data):
captain_id = data.get('captain_id') captain_id = data.get("captain_id")
if captain_id is None: if captain_id is None:
return return
if is_admin(): if is_admin():
team_id = data.get('id') team_id = data.get("id")
if team_id: if team_id:
target_team = Teams.query.filter_by(id=team_id).first() target_team = Teams.query.filter_by(id=team_id).first()
else: else:
@ -147,64 +168,67 @@ class TeamSchema(ma.ModelSchema):
if captain in target_team.members: if captain in target_team.members:
return return
else: else:
raise ValidationError('Invalid Captain ID', field_names=['captain_id']) raise ValidationError("Invalid Captain ID", field_names=["captain_id"])
else: else:
current_team = get_current_team() current_team = get_current_team()
current_user = get_current_user() current_user = get_current_user()
if current_team.captain_id == current_user.id: if current_team.captain_id == current_user.id:
return return
else: 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 = { views = {
'user': [ "user": [
'website', "website",
'name', "name",
'country', "country",
'affiliation', "affiliation",
'bracket', "bracket",
'members', "members",
'id', "id",
'oauth_id', "oauth_id",
'captain_id', "captain_id",
], ],
'self': [ "self": [
'website', "website",
'name', "name",
'email', "email",
'country', "country",
'affiliation', "affiliation",
'bracket', "bracket",
'members', "members",
'id', "id",
'oauth_id', "oauth_id",
'password', "password",
'captain_id', "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): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(TeamSchema, self).__init__(*args, **kwargs) super(TeamSchema, self).__init__(*args, **kwargs)

View File

@ -6,30 +6,18 @@ class UnlockSchema(ma.ModelSchema):
class Meta: class Meta:
model = Unlocks model = Unlocks
include_fk = True include_fk = True
dump_only = ('id', 'date') dump_only = ("id", "date")
views = { views = {
'admin': [ "admin": ["user_id", "target", "team_id", "date", "type", "id"],
'user_id', "user": ["target", "date", "type", "id"],
'target',
'team_id',
'date',
'type',
'id'
],
'user': [
'target',
'date',
'type',
'id'
]
} }
def __init__(self, view=None, *args, **kwargs): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(UnlockSchema, self).__init__(*args, **kwargs) super(UnlockSchema, self).__init__(*args, **kwargs)

View File

@ -13,181 +13,199 @@ class UserSchema(ma.ModelSchema):
class Meta: class Meta:
model = Users model = Users
include_fk = True include_fk = True
dump_only = ('id', 'oauth_id', 'created') dump_only = ("id", "oauth_id", "created")
load_only = ('password',) load_only = ("password",)
name = field_for( name = field_for(
Users, Users,
'name', "name",
required=True, required=True,
validate=[ 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( email = field_for(
Users, Users,
'email', "email",
validate=[ validate=[
validate.Email('Emails must be a properly formatted email address'), validate.Email("Emails must be a properly formatted email address"),
validate.Length(min=1, max=128, error='Emails must not be empty'), validate.Length(min=1, max=128, error="Emails must not be empty"),
] ],
) )
website = field_for( website = field_for(
Users, Users,
'website', "website",
validate=[ validate=[
# This is a dirty hack to let website accept empty strings so you can remove your website # This is a dirty hack to let website accept empty strings so you can remove your website
lambda website: validate.URL( lambda website: validate.URL(
error='Websites must be a proper URL starting with http or https', error="Websites must be a proper URL starting with http or https",
schemes={'http', 'https'} schemes={"http", "https"},
)(website) if website else True )(website)
] if website
) else True
country = field_for( ],
Users,
'country',
validate=[
validate_country_code
]
)
password = field_for(
Users,
'password',
) )
country = field_for(Users, "country", validate=[validate_country_code])
password = field_for(Users, "password")
@pre_load @pre_load
def validate_name(self, data): def validate_name(self, data):
name = data.get('name') name = data.get("name")
if name is None: if name is None:
return return
existing_user = Users.query.filter_by(name=name).first() existing_user = Users.query.filter_by(name=name).first()
current_user = get_current_user() current_user = get_current_user()
if is_admin(): if is_admin():
user_id = data.get('id') user_id = data.get("id")
if user_id: if user_id:
if existing_user and existing_user.id != 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: else:
if existing_user: if existing_user:
if current_user: if current_user:
if current_user.id != existing_user.id: 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: else:
raise ValidationError('User name has already been taken', field_names=['name']) raise ValidationError(
"User name has already been taken", field_names=["name"]
)
else: else:
if name == current_user.name: if name == current_user.name:
return data return data
else: else:
name_changes = get_config('name_changes', default=True) name_changes = get_config("name_changes", default=True)
if bool(name_changes) is False: 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: 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 @pre_load
def validate_email(self, data): def validate_email(self, data):
email = data.get('email') email = data.get("email")
if email is None: if email is None:
return return
existing_user = Users.query.filter_by(email=email).first() existing_user = Users.query.filter_by(email=email).first()
current_user = get_current_user() current_user = get_current_user()
if is_admin(): if is_admin():
user_id = data.get('id') user_id = data.get("id")
if user_id: if user_id:
if existing_user and existing_user.id != 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: else:
if existing_user: if existing_user:
if current_user: if current_user:
if current_user.id != existing_user.id: 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: else:
raise ValidationError('Email address has already been used', field_names=['email']) raise ValidationError(
"Email address has already been used", field_names=["email"]
)
else: else:
if email == current_user.email: if email == current_user.email:
return data return data
else: else:
if existing_user: 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: if check_email_is_whitelisted(email) is False:
raise ValidationError( raise ValidationError(
"Only email addresses under {domains} may register".format( "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 current_user.verified = False
@pre_load @pre_load
def validate_password_confirmation(self, data): def validate_password_confirmation(self, data):
password = data.get('password') password = data.get("password")
confirm = data.get('confirm') confirm = data.get("confirm")
target_user = get_current_user() target_user = get_current_user()
if is_admin(): if is_admin():
pass pass
else: else:
if password and (bool(confirm) is False): 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: 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: if test is True:
return data return data
else: else:
raise ValidationError('Your previous password is incorrect', field_names=['confirm']) raise ValidationError(
"Your previous password is incorrect", field_names=["confirm"]
)
else: else:
data.pop('password', None) data.pop("password", None)
data.pop('confirm', None) data.pop("confirm", None)
views = { views = {
'user': [ "user": [
'website', "website",
'name', "name",
'country', "country",
'affiliation', "affiliation",
'bracket', "bracket",
'id', "id",
'oauth_id', "oauth_id",
], ],
'self': [ "self": [
'website', "website",
'name', "name",
'email', "email",
'country', "country",
'affiliation', "affiliation",
'bracket', "bracket",
'id', "id",
'oauth_id', "oauth_id",
'password' "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): def __init__(self, view=None, *args, **kwargs):
if view: if view:
if isinstance(view, string_types): if isinstance(view, string_types):
kwargs['only'] = self.views[view] kwargs["only"] = self.views[view]
elif isinstance(view, list): elif isinstance(view, list):
kwargs['only'] = view kwargs["only"] = view
super(UserSchema, self).__init__(*args, **kwargs) super(UserSchema, self).__init__(*args, **kwargs)

View File

@ -5,15 +5,15 @@ from CTFd.utils.decorators.visibility import check_score_visibility
from CTFd.utils.scores import get_standings from CTFd.utils.scores import get_standings
scoreboard = Blueprint('scoreboard', __name__) scoreboard = Blueprint("scoreboard", __name__)
@scoreboard.route('/scoreboard') @scoreboard.route("/scoreboard")
@check_score_visibility @check_score_visibility
def listing(): def listing():
standings = get_standings() standings = get_standings()
return render_template( return render_template(
'scoreboard.html', "scoreboard.html",
standings=standings, standings=standings,
score_frozen=config.is_scoreboard_frozen() score_frozen=config.is_scoreboard_frozen(),
) )

View File

@ -5,17 +5,20 @@ from CTFd.utils.decorators.modes import require_team_mode
from CTFd.utils import config from CTFd.utils import config
from CTFd.utils.user import get_current_user from CTFd.utils.user import get_current_user
from CTFd.utils.crypto import verify_password 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 from CTFd.utils.helpers import get_errors
teams = Blueprint('teams', __name__) teams = Blueprint("teams", __name__)
@teams.route('/teams') @teams.route("/teams")
@check_account_visibility @check_account_visibility
@require_team_mode @require_team_mode
def listing(): def listing():
page = abs(request.args.get('page', 1, type=int)) page = abs(request.args.get("page", 1, type=int))
results_per_page = 50 results_per_page = 50
page_start = results_per_page * (page - 1) page_start = results_per_page * (page - 1)
page_end = results_per_page * (page - 1) + results_per_page 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() # teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all()
# else: # else:
count = Teams.query.filter_by(hidden=False, banned=False).count() 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) 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 @authed_only
@require_team_mode @require_team_mode
def join(): def join():
if request.method == 'GET': if request.method == "GET":
return render_template('teams/join_team.html') return render_template("teams/join_team.html")
if request.method == 'POST': if request.method == "POST":
teamname = request.form.get('name') teamname = request.form.get("name")
passphrase = request.form.get('password', '').strip() passphrase = request.form.get("password", "").strip()
team = Teams.query.filter_by(name=teamname).first() team = Teams.query.filter_by(name=teamname).first()
user = get_current_user() user = get_current_user()
@ -52,57 +59,51 @@ def join():
team.captain_id = user.id team.captain_id = user.id
db.session.commit() db.session.commit()
return redirect(url_for('challenges.listing')) return redirect(url_for("challenges.listing"))
else: else:
errors = ['That information is incorrect'] errors = ["That information is incorrect"]
return render_template('teams/join_team.html', errors=errors) 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 @authed_only
@require_team_mode @require_team_mode
def new(): def new():
if request.method == 'GET': if request.method == "GET":
return render_template("teams/new_team.html") return render_template("teams/new_team.html")
elif request.method == 'POST': elif request.method == "POST":
teamname = request.form.get('name') teamname = request.form.get("name")
passphrase = request.form.get('password', '').strip() passphrase = request.form.get("password", "").strip()
errors = get_errors() errors = get_errors()
user = get_current_user() user = get_current_user()
existing_team = Teams.query.filter_by(name=teamname).first() existing_team = Teams.query.filter_by(name=teamname).first()
if existing_team: if existing_team:
errors.append('That team name is already taken') errors.append("That team name is already taken")
if not teamname: if not teamname:
errors.append('That team name is invalid') errors.append("That team name is invalid")
if errors: if errors:
return render_template("teams/new_team.html", errors=errors) return render_template("teams/new_team.html", errors=errors)
team = Teams( team = Teams(name=teamname, password=passphrase, captain_id=user.id)
name=teamname,
password=passphrase,
captain_id=user.id
)
db.session.add(team) db.session.add(team)
db.session.commit() db.session.commit()
user.team_id = team.id user.team_id = team.id
db.session.commit() db.session.commit()
return redirect(url_for('challenges.listing')) return redirect(url_for("challenges.listing"))
@teams.route('/team') @teams.route("/team")
@authed_only @authed_only
@require_team_mode @require_team_mode
def private(): def private():
user = get_current_user() user = get_current_user()
if not user.team_id: if not user.team_id:
return render_template( return render_template("teams/team_enrollment.html")
'teams/team_enrollment.html',
)
team_id = user.team_id team_id = user.team_id
@ -114,18 +115,18 @@ def private():
score = team.score score = team.score
return render_template( return render_template(
'teams/private.html', "teams/private.html",
solves=solves, solves=solves,
awards=awards, awards=awards,
user=user, user=user,
team=team, team=team,
score=score, score=score,
place=place, place=place,
score_frozen=config.is_scoreboard_frozen() score_frozen=config.is_scoreboard_frozen(),
) )
@teams.route('/teams/<int:team_id>') @teams.route("/teams/<int:team_id>")
@check_account_visibility @check_account_visibility
@check_score_visibility @check_score_visibility
@require_team_mode @require_team_mode
@ -139,14 +140,14 @@ def public(team_id):
score = team.score score = team.score
if errors: 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( return render_template(
'teams/public.html', "teams/public.html",
solves=solves, solves=solves,
awards=awards, awards=awards,
team=team, team=team,
score=score, score=score,
place=place, place=place,
score_frozen=config.is_scoreboard_frozen() score_frozen=config.is_scoreboard_frozen(),
) )

View File

@ -1,9 +1,12 @@
html, body, .container { html,
font-family: 'Lato', 'LatoOffline', sans-serif; body,
.container {
font-family: "Lato", "LatoOffline", sans-serif;
} }
h1, h2 { h1,
font-family: 'Raleway', 'RalewayOffline', sans-serif; h2 {
font-family: "Raleway", "RalewayOffline", sans-serif;
font-weight: 500; font-weight: 500;
letter-spacing: 2px; letter-spacing: 2px;
} }
@ -19,7 +22,8 @@ table > thead > tr > td {
border-top: none !important; border-top: none !important;
} }
.table td, .table th { .table td,
.table th {
vertical-align: inherit; vertical-align: inherit;
} }
@ -112,12 +116,12 @@ pre {
} }
.btn-info { .btn-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
border-color: #5B7290 !important; border-color: #5b7290 !important;
} }
.badge-info { .badge-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.alert { .alert {

View File

@ -17,11 +17,11 @@
} }
.btn-info { .btn-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.badge-info { .badge-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.challenge-button { .challenge-button {

View File

@ -1,5 +1,5 @@
tbody tr:hover { tbody tr:hover {
background-color: rgba(0, 0, 0, .1) !important; background-color: rgba(0, 0, 0, 0.1) !important;
} }
tr[data-href] { tr[data-href] {

View File

@ -1,18 +1,25 @@
function renderSubmissionResponse(response, cb) { function renderSubmissionResponse(response, cb) {
var result = response.data; var result = response.data;
var result_message = $('#result-message'); var result_message = $("#result-message");
var result_notification = $('#result-notification'); var result_notification = $("#result-notification");
var answer_input = $("#submission-input"); var answer_input = $("#submission-input");
result_notification.removeClass(); result_notification.removeClass();
result_message.text(result.message); result_message.text(result.message);
if (result.status === "authentication_required") { if (result.status === "authentication_required") {
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash; window.location =
return script_root +
} "/login?next=" +
else if (result.status === "incorrect") { // Incorrect key script_root +
result_notification.addClass('alert alert-danger alert-dismissable text-center'); 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(); result_notification.slideDown();
answer_input.removeClass("correct"); answer_input.removeClass("correct");
@ -20,29 +27,45 @@ function renderSubmissionResponse(response, cb) {
setTimeout(function() { setTimeout(function() {
answer_input.removeClass("wrong"); answer_input.removeClass("wrong");
}, 3000); }, 3000);
} } else if (result.status === "correct") {
else if (result.status === "correct") { // Challenge Solved // Challenge Solved
result_notification.addClass('alert alert-success alert-dismissable text-center'); result_notification.addClass(
"alert alert-success alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
$('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves")); $(".challenge-solves").text(
parseInt(
$(".challenge-solves")
.text()
.split(" ")[0]
) +
1 +
" Solves"
);
answer_input.val(""); answer_input.val("");
answer_input.removeClass("wrong"); answer_input.removeClass("wrong");
answer_input.addClass("correct"); answer_input.addClass("correct");
} } else if (result.status === "already_solved") {
else if (result.status === "already_solved") { // Challenge already solved // Challenge already solved
result_notification.addClass('alert alert-info alert-dismissable text-center'); result_notification.addClass(
"alert alert-info alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
answer_input.addClass("correct"); answer_input.addClass("correct");
} } else if (result.status === "paused") {
else if (result.status === "paused") { // CTF is paused // CTF is paused
result_notification.addClass('alert alert-warning alert-dismissable text-center'); result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
} } else if (result.status === "ratelimited") {
else if (result.status === "ratelimited") { // Keys per minute too high // Keys per minute too high
result_notification.addClass('alert alert-warning alert-dismissable text-center'); result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
answer_input.addClass("too-fast"); answer_input.addClass("too-fast");
@ -51,9 +74,9 @@ function renderSubmissionResponse(response, cb) {
}, 3000); }, 3000);
} }
setTimeout(function() { setTimeout(function() {
$('.alert').slideUp(); $(".alert").slideUp();
$('#submit-key').removeClass("disabled-button"); $("#submit-key").removeClass("disabled-button");
$('#submit-key').prop('disabled', false); $("#submit-key").prop("disabled", false);
}, 3000); }, 3000);
if (cb) { if (cb) {
@ -62,35 +85,42 @@ function renderSubmissionResponse(response, cb) {
} }
$(document).ready(function() { $(document).ready(function() {
$('.preview-challenge').click(function (e) { $(".preview-challenge").click(function(e) {
window.challenge = new Object(); window.challenge = new Object();
$.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function (response) { $.get(script_root + "/api/v1/challenges/" + CHALLENGE_ID, function(
response
) {
var challenge_data = response.data; var challenge_data = response.data;
challenge_data['solves'] = null; challenge_data["solves"] = null;
$.getScript(script_root + challenge_data.type_data.scripts.view, function () { $.getScript(
$.get(script_root + challenge_data.type_data.templates.view, function (template_data) { script_root + challenge_data.type_data.scripts.view,
function() {
$('#challenge-window').empty(); $.get(script_root + challenge_data.type_data.templates.view, function(
template_data
) {
$("#challenge-window").empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
window.challenge.data = challenge_data; window.challenge.data = challenge_data;
window.challenge.preRender(); window.challenge.preRender();
challenge_data['description'] = window.challenge.render(challenge_data['description']); challenge_data["description"] = window.challenge.render(
challenge_data['script_root'] = script_root; 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) { $(".challenge-solves").click(function(e) {
getsolves($('#challenge-id').val()) getsolves($("#challenge-id").val());
}); });
$('.nav-tabs a').click(function (e) { $(".nav-tabs a").click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show') $(this).tab("show");
}); });
// Handle modal toggling // Handle modal toggling
$('#challenge-window').on('hide.bs.modal', function (event) { $("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong"); $("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct"); $("#submission-input").removeClass("correct");
$("#incorrect-key").slideUp(); $("#incorrect-key").slideUp();
@ -99,12 +129,12 @@ $(document).ready(function () {
$("#too-fast").slideUp(); $("#too-fast").slideUp();
}); });
$('#submit-key').click(function (e) { $("#submit-key").click(function(e) {
e.preventDefault(); e.preventDefault();
$('#submit-key').addClass("disabled-button"); $("#submit-key").addClass("disabled-button");
$('#submit-key').prop('disabled', true); $("#submit-key").prop("disabled", true);
window.challenge.submit(function(data) { window.challenge.submit(function(data) {
renderSubmissionResponse(data) renderSubmissionResponse(data);
}, true); }, true);
// Preview passed as true // Preview passed as true
}); });
@ -117,62 +147,74 @@ $(document).ready(function () {
$(".input-field").bind({ $(".input-field").bind({
focus: function() { focus: function() {
$(this).parent().addClass('input--filled'); $(this)
.parent()
.addClass("input--filled");
$label = $(this).siblings(".input-label"); $label = $(this).siblings(".input-label");
}, },
blur: function() { blur: function() {
if ($(this).val() === '') { if ($(this).val() === "") {
$(this).parent().removeClass('input--filled'); $(this)
.parent()
.removeClass("input--filled");
$label = $(this).siblings(".input-label"); $label = $(this).siblings(".input-label");
$label.removeClass('input--hide'); $label.removeClass("input--hide");
} }
} }
}); });
window.challenge.postRender(); window.challenge.postRender();
window.location.replace(window.location.href.split('#')[0] + '#preview'); window.location.replace(
window.location.href.split("#")[0] + "#preview"
);
$('#challenge-window').modal(); $("#challenge-window").modal();
});
}); });
}
);
}); });
}); });
$('.delete-challenge').click(function(e){ $(".delete-challenge").click(function(e) {
ezq({ ezq({
title: "Delete Challenge", title: "Delete Challenge",
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(CHALLENGE_NAME) + "</strong>"), body: "Are you sure you want to delete {0}".format(
"<strong>" + htmlentities(CHALLENGE_NAME) + "</strong>"
),
success: function() { success: function() {
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location = script_root + '/admin/challenges'; window.location = script_root + "/admin/challenges";
} }
}); });
} }
}); });
}); });
$('#challenge-update-container > form').submit(function(e){ $("#challenge-update-container > form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $(e.target).serializeJSON(true); var params = $(e.target).serializeJSON(true);
console.log(params); console.log(params);
CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { method: "PATCH",
method: 'PATCH', credentials: "same-origin",
credentials: 'same-origin',
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (data) { })
.then(function(data) {
if (data.success) { if (data.success) {
ezal({ ezal({
title: "Success", title: "Success",
@ -185,11 +227,11 @@ $(document).ready(function () {
if (window.location.hash) { if (window.location.hash) {
let hash = window.location.hash.replace("<>[]'\"", ""); let hash = window.location.hash.replace("<>[]'\"", "");
$('nav a[href="' + hash + '"]').tab('show'); $('nav a[href="' + hash + '"]').tab("show");
} }
$('.nav-tabs a').click(function (e) { $(".nav-tabs a").click(function(e) {
$(this).tab('show'); $(this).tab("show");
window.location.hash = this.hash; window.location.hash = this.hash;
}); });
}); });

View File

@ -1,2 +1 @@
$(document).ready(function () { $(document).ready(function() {});
});

View File

@ -1,18 +1,18 @@
$(document).ready(function() { $(document).ready(function() {
$('#file-add-form').submit(function (e) { $("#file-add-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var formData = new FormData(e.target); var formData = new FormData(e.target);
formData.append('nonce', csrf_nonce); formData.append("nonce", csrf_nonce);
formData.append('challenge', CHALLENGE_ID); formData.append("challenge", CHALLENGE_ID);
formData.append('type', 'challenge'); formData.append("type", "challenge");
var pg = ezpg({ var pg = ezpg({
width: 0, width: 0,
title: "Upload Progress", title: "Upload Progress"
}); });
$.ajax({ $.ajax({
url: script_root + '/api/v1/files', url: script_root + "/api/v1/files",
data: formData, data: formData,
type: 'POST', type: "POST",
cache: false, cache: false,
contentType: false, contentType: false,
processData: false, processData: false,
@ -36,35 +36,35 @@ $(document).ready(function () {
// Refresh modal // Refresh modal
pg = ezpg({ pg = ezpg({
target: pg, target: pg,
width: 100, width: 100
}); });
setTimeout( setTimeout(function() {
function () { pg.modal("hide");
pg.modal('hide'); }, 500);
}, 500
);
setTimeout( setTimeout(function() {
function () {
window.location.reload(); window.location.reload();
}, 700 }, 700);
);
} }
}); });
}); });
$('.delete-file').click(function(e){ $(".delete-file").click(function(e) {
var file_id = $(this).attr('file-id'); var file_id = $(this).attr("file-id");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Files", title: "Delete Files",
body: "Are you sure you want to delete this file?", body: "Are you sure you want to delete this file?",
success: function() { success: function() {
CTFd.fetch('/api/v1/files/' + file_id, { CTFd.fetch("/api/v1/files/" + file_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }

View File

@ -1,6 +1,6 @@
$(document).ready(function() { $(document).ready(function() {
$('#flag-add-button').click(function (e) { $("#flag-add-button").click(function(e) {
$.get(script_root + '/api/v1/flags/types', function (response) { $.get(script_root + "/api/v1/flags/types", function(response) {
var data = response.data; var data = response.data;
var flag_type_select = $("#flags-create-select"); var flag_type_select = $("#flags-create-select");
flag_type_select.empty(); flag_type_select.empty();
@ -10,102 +10,120 @@ $(document).ready(function () {
for (var key in data) { for (var key in data) {
if (data.hasOwnProperty(key)) { if (data.hasOwnProperty(key)) {
option = "<option value='{0}'>{1}</option>".format(key, data[key].name); option = "<option value='{0}'>{1}</option>".format(
key,
data[key].name
);
flag_type_select.append(option); flag_type_select.append(option);
} }
} }
$("#flag-edit-modal").modal(); $("#flag-edit-modal").modal();
}); });
$('#flag-edit-modal form').submit(function(e){ $("#flag-edit-modal form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $(this).serializeJSON(true); var params = $(this).serializeJSON(true);
params['challenge'] = CHALLENGE_ID; params["challenge"] = CHALLENGE_ID;
CTFd.fetch('/api/v1/flags', { CTFd.fetch("/api/v1/flags", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
return response.json() .then(function(response) {
}).then(function (response) { return response.json();
})
.then(function(response) {
window.location.reload(); window.location.reload();
}); });
}); });
$('#flag-edit-modal').modal(); $("#flag-edit-modal").modal();
}); });
$("#flags-create-select").change(function(e) { $("#flags-create-select").change(function(e) {
e.preventDefault(); e.preventDefault();
var flag_type_name = $(this).find("option:selected").text(); var flag_type_name = $(this)
.find("option:selected")
.text();
$.get(script_root + '/api/v1/flags/types/' + flag_type_name, function (response) { $.get(script_root + "/api/v1/flags/types/" + flag_type_name, function(
response
) {
var data = response.data; var data = response.data;
$.get(script_root + data.templates.create, function(template_data) { $.get(script_root + data.templates.create, function(template_data) {
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
$("#create-keys-entry-div").html(template.render()); $("#create-keys-entry-div").html(template.render());
$("#create-keys-button-div").show(); $("#create-keys-button-div").show();
}); });
}) });
}); });
$('.edit-flag').click(function (e) { $(".edit-flag").click(function(e) {
e.preventDefault(); e.preventDefault();
var flag_id = $(this).attr('flag-id'); var flag_id = $(this).attr("flag-id");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
$.get(script_root + '/api/v1/flags/' + flag_id, function (response) { $.get(script_root + "/api/v1/flags/" + flag_id, function(response) {
var data = response.data; var data = response.data;
$.get(script_root + data.templates.update, function(template_data) { $.get(script_root + data.templates.update, function(template_data) {
$('#edit-flags form').empty(); $("#edit-flags form").empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
$('#edit-flags form').append(template.render(data)); $("#edit-flags form").append(template.render(data));
$('#edit-flags form').submit(function (e) { $("#edit-flags form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#edit-flags form').serializeJSON(); var params = $("#edit-flags form").serializeJSON();
CTFd.fetch('/api/v1/flags/' + flag_id, { CTFd.fetch("/api/v1/flags/" + flag_id, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
$(row).find('.flag-content').text(response.data.content); $(row)
$('#edit-flags').modal('toggle'); .find(".flag-content")
.text(response.data.content);
$("#edit-flags").modal("toggle");
} }
}); });
}); });
$('#edit-flags').modal(); $("#edit-flags").modal();
}); });
}); });
}); });
$('.delete-flag').click(function (e) { $(".delete-flag").click(function(e) {
e.preventDefault(); e.preventDefault();
var flag_id = $(this).attr('flag-id'); var flag_id = $(this).attr("flag-id");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Flag", title: "Delete Flag",
body: "Are you sure you want to delete this flag?", body: "Are you sure you want to delete this flag?",
success: function() { success: function() {
CTFd.fetch('/api/v1/flags/' + flag_id, { CTFd.fetch("/api/v1/flags/" + flag_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }

View File

@ -1,10 +1,10 @@
function hint(id) { function hint(id) {
return CTFd.fetch('/api/v1/hints/' + id + '?preview=true', { return CTFd.fetch("/api/v1/hints/" + id + "?preview=true", {
method: 'GET', method: "GET",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function(response) { }).then(function(response) {
return response.json(); return response.json();
@ -14,7 +14,7 @@ function hint(id) {
function loadhint(hintid) { function loadhint(hintid) {
var md = window.markdownit({ var md = window.markdownit({
html: true, html: true,
linkify: true, linkify: true
}); });
hint(hintid).then(function(response) { hint(hintid).then(function(response) {
@ -35,39 +35,45 @@ function loadhint(hintid) {
} }
$(document).ready(function() { $(document).ready(function() {
$('#hint-add-button').click(function (e) { $("#hint-add-button").click(function(e) {
$('#hint-edit-modal form').find("input, textarea").val(""); $("#hint-edit-modal form")
.find("input, textarea")
.val("");
// Markdown Preview // Markdown Preview
$('#new-hint-edit').on('shown.bs.tab', function (event) { $("#new-hint-edit").on("shown.bs.tab", function(event) {
console.log(event.target.hash); console.log(event.target.hash);
if (event.target.hash == '#hint-preview') { if (event.target.hash == "#hint-preview") {
console.log(event.target.hash); console.log(event.target.hash);
var renderer = window.markdownit({ var renderer = window.markdownit({
html: true, html: true,
linkify: true, linkify: true
}); });
var editor_value = $('#hint-write textarea').val(); var editor_value = $("#hint-write textarea").val();
$(event.target.hash).html(renderer.render(editor_value)); $(event.target.hash).html(renderer.render(editor_value));
} }
}); });
$('#hint-edit-modal').modal(); $("#hint-edit-modal").modal();
}); });
$('.delete-hint').click(function(e){ $(".delete-hint").click(function(e) {
e.preventDefault(); e.preventDefault();
var hint_id = $(this).attr('hint-id'); var hint_id = $(this).attr("hint-id");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Hint", title: "Delete Hint",
body: "Are you sure you want to delete this hint?", body: "Are you sure you want to delete this hint?",
success: function() { success: function() {
CTFd.fetch('/api/v1/hints/' + hint_id, { CTFd.fetch("/api/v1/hints/" + hint_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }
@ -76,66 +82,72 @@ $(document).ready(function () {
}); });
}); });
$('.edit-hint').click(function (e) { $(".edit-hint").click(function(e) {
e.preventDefault(); e.preventDefault();
var hint_id = $(this).attr('hint-id'); var hint_id = $(this).attr("hint-id");
CTFd.fetch('/api/v1/hints/' + hint_id + '?preview=true', { CTFd.fetch("/api/v1/hints/" + hint_id + "?preview=true", {
method: 'GET', method: "GET",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function (response) { })
return response.json() .then(function(response) {
}).then(function (response) { return response.json();
})
.then(function(response) {
if (response.success) { if (response.success) {
$('#hint-edit-form input[name=content],textarea[name=content]').val(response.data.content); $("#hint-edit-form input[name=content],textarea[name=content]").val(
$('#hint-edit-form input[name=cost]').val(response.data.cost); response.data.content
$('#hint-edit-form input[name=id]').val(response.data.id); );
$("#hint-edit-form input[name=cost]").val(response.data.cost);
$("#hint-edit-form input[name=id]").val(response.data.id);
// Markdown Preview // Markdown Preview
$('#new-hint-edit').on('shown.bs.tab', function (event) { $("#new-hint-edit").on("shown.bs.tab", function(event) {
console.log(event.target.hash); console.log(event.target.hash);
if (event.target.hash == '#hint-preview') { if (event.target.hash == "#hint-preview") {
console.log(event.target.hash); console.log(event.target.hash);
var renderer = new markdownit({ var renderer = new markdownit({
html: true, html: true,
linkify: true, linkify: true
}); });
var editor_value = $('#hint-write textarea').val(); var editor_value = $("#hint-write textarea").val();
$(event.target.hash).html(renderer.render(editor_value)); $(event.target.hash).html(renderer.render(editor_value));
} }
}); });
$('#hint-edit-modal').modal(); $("#hint-edit-modal").modal();
} }
}); });
}); });
$('#hint-edit-form').submit(function (e) { $("#hint-edit-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $(this).serializeJSON(true); var params = $(this).serializeJSON(true);
params['challenge'] = CHALLENGE_ID; params["challenge"] = CHALLENGE_ID;
var method = 'POST'; var method = "POST";
var url = '/api/v1/hints'; var url = "/api/v1/hints";
if (params.id) { if (params.id) {
method = 'PATCH'; method = "PATCH";
url = '/api/v1/hints/' + params.id; url = "/api/v1/hints/" + params.id;
} }
CTFd.fetch(url, { CTFd.fetch(url, {
method: method, method: method,
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
return response.json() .then(function(response) {
}).then(function(response) { return response.json();
})
.then(function(response) {
if (response.success) { if (response.success) {
// TODO: Refresh hints on submit. // TODO: Refresh hints on submit.
window.location.reload(); window.location.reload();

View File

@ -4,28 +4,33 @@ window.challenge = new Object();
function load_chal_template(challenge) { function load_chal_template(challenge) {
$.getScript(script_root + challenge.scripts.view, function() { $.getScript(script_root + challenge.scripts.view, function() {
console.log('loaded renderer'); console.log("loaded renderer");
$.get(script_root + challenge.templates.create, function(template_data) { $.get(script_root + challenge.templates.create, function(template_data) {
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
$("#create-chal-entry-div").html(template.render({'nonce': nonce, 'script_root': script_root})); $("#create-chal-entry-div").html(
template.render({ nonce: nonce, script_root: script_root })
);
$.getScript(script_root + challenge.scripts.create, function() { $.getScript(script_root + challenge.scripts.create, function() {
console.log('loaded'); console.log("loaded");
$("#create-chal-entry-div form").submit(function(e) { $("#create-chal-entry-div form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $("#create-chal-entry-div form").serializeJSON(); var params = $("#create-chal-entry-div form").serializeJSON();
CTFd.fetch('/api/v1/challenges', { CTFd.fetch("/api/v1/challenges", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location = script_root + '/admin/challenges/' + response.data.id; window.location =
script_root + "/admin/challenges/" + response.data.id;
} }
}); });
}); });
@ -34,7 +39,7 @@ function load_chal_template(challenge){
}); });
} }
$.get(script_root + '/api/v1/challenges/types', function(response){ $.get(script_root + "/api/v1/challenges/types", function(response) {
$("#create-chals-select").empty(); $("#create-chals-select").empty();
var data = response.data; var data = response.data;
var chal_type_amt = Object.keys(data).length; var chal_type_amt = Object.keys(data).length;
@ -44,9 +49,9 @@ $.get(script_root + '/api/v1/challenges/types', function(response){
for (var key in data) { for (var key in data) {
var challenge = data[key]; var challenge = data[key];
var option = $("<option/>"); var option = $("<option/>");
option.attr('value', challenge.type); option.attr("value", challenge.type);
option.text(challenge.name); option.text(challenge.name);
option.data('meta', challenge); option.data("meta", challenge);
$("#create-chals-select").append(option); $("#create-chals-select").append(option);
} }
$("#create-chals-select-div").show(); $("#create-chals-select-div").show();
@ -56,7 +61,9 @@ $.get(script_root + '/api/v1/challenges/types', function(response){
load_chal_template(data[key]); load_chal_template(data[key]);
} }
}); });
$('#create-chals-select').change(function(){ $("#create-chals-select").change(function() {
var challenge = $(this).find("option:selected").data('meta'); var challenge = $(this)
.find("option:selected")
.data("meta");
load_chal_template(challenge); load_chal_template(challenge);
}); });

View File

@ -1,26 +1,28 @@
$(document).ready(function() { $(document).ready(function() {
$('#prerequisite-add-form').submit(function (e) { $("#prerequisite-add-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var requirements = $('#prerequisite-add-form').serializeJSON(); var requirements = $("#prerequisite-add-form").serializeJSON();
CHALLENGE_REQUIREMENTS.prerequisites.push( CHALLENGE_REQUIREMENTS.prerequisites.push(
parseInt(requirements['prerequisite']) parseInt(requirements["prerequisite"])
); );
var params = { var params = {
'requirements': CHALLENGE_REQUIREMENTS requirements: CHALLENGE_REQUIREMENTS
}; };
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (data) { })
.then(function(data) {
if (data.success) { if (data.success) {
// TODO: Make this refresh requirements // TODO: Make this refresh requirements
window.location.reload(); window.location.reload();
@ -28,27 +30,31 @@ $(document).ready(function () {
}); });
}); });
$('.delete-requirement').click(function (e) { $(".delete-requirement").click(function(e) {
var challenge_id = $(this).attr('challenge-id'); var challenge_id = $(this).attr("challenge-id");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
CHALLENGE_REQUIREMENTS.prerequisites.pop(challenge_id); CHALLENGE_REQUIREMENTS.prerequisites.pop(challenge_id);
var params = { var params = {
'requirements': CHALLENGE_REQUIREMENTS requirements: CHALLENGE_REQUIREMENTS
}; };
CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, { CTFd.fetch("/api/v1/challenges/" + CHALLENGE_ID, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (data) { })
.then(function(data) {
if (data.success) { if (data.success) {
row.remove(); row.remove();
} }

View File

@ -1,51 +1,55 @@
function delete_tag(elem) { function delete_tag(elem) {
var elem = $(elem); var elem = $(elem);
var tag_id = elem.attr('tag-id'); var tag_id = elem.attr("tag-id");
CTFd.fetch('/api/v1/tags/' + tag_id, { CTFd.fetch("/api/v1/tags/" + tag_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
$(elem).parent().remove() $(elem)
.parent()
.remove();
} }
}); });
} }
$(document).ready(function() { $(document).ready(function() {
$('#tags-add-input').keyup(function (e) { $("#tags-add-input").keyup(function(e) {
if (e.keyCode == 13) { if (e.keyCode == 13) {
var tag = $('#tags-add-input').val(); var tag = $("#tags-add-input").val();
var params = { var params = {
value: tag, value: tag,
challenge: CHALLENGE_ID challenge: CHALLENGE_ID
}; };
CTFd.fetch('/api/v1/tags', { CTFd.fetch("/api/v1/tags", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
var tpl = "<span class='badge badge-primary mx-1 challenge-tag'>" + var tpl =
"<span class='badge badge-primary mx-1 challenge-tag'>" +
"<span>{0}</span>" + "<span>{0}</span>" +
"<a class='btn-fa delete-tag' tag-id='{1}' onclick='delete_tag(this)'>&times;</a></span>"; "<a class='btn-fa delete-tag' tag-id='{1}' onclick='delete_tag(this)'>&times;</a></span>";
tag = tpl.format( tag = tpl.format(response.data.value, response.data.id);
response.data.value, $("#challenge-tags").append(tag);
response.data.id
);
$('#challenge-tags').append(tag);
} }
}); });
$('#tags-add-input').val(""); $("#tags-add-input").val("");
} }
}); });
}); });

View File

@ -1,16 +1,16 @@
var months = { var months = {
'January': 1, January: 1,
'February': 2, February: 2,
'March': 3, March: 3,
'April': 4, April: 4,
'May': 5, May: 5,
'June': 6, June: 6,
'July': 7, July: 7,
'August': 8, August: 8,
'September': 9, September: 9,
'October': 10, October: 10,
'November': 11, November: 11,
'December': 12, December: 12
}; };
function load_timestamp(place, timestamp) { function load_timestamp(place, timestamp) {
@ -18,35 +18,39 @@ function load_timestamp(place, timestamp) {
var timestamp = parseInt(timestamp); var timestamp = parseInt(timestamp);
} }
var m = moment(timestamp * 1000); var m = moment(timestamp * 1000);
console.log('Loading ' + place); console.log("Loading " + place);
console.log(timestamp); console.log(timestamp);
console.log(m.toISOString()); console.log(m.toISOString());
console.log(m.unix()); console.log(m.unix());
var month = $('#' + place + '-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/) var month = $("#" + place + "-month").val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/)
var day = $('#' + place + '-day').val(m.date()); var day = $("#" + place + "-day").val(m.date());
var year = $('#' + place + '-year').val(m.year()); var year = $("#" + place + "-year").val(m.year());
var hour = $('#' + place + '-hour').val(m.hour()); var hour = $("#" + place + "-hour").val(m.hour());
var minute = $('#' + place + '-minute').val(m.minute()); var minute = $("#" + place + "-minute").val(m.minute());
load_date_values(place); load_date_values(place);
} }
function load_date_values(place) { function load_date_values(place) {
var month = $('#' + place + '-month').val(); var month = $("#" + place + "-month").val();
var day = $('#' + place + '-day').val(); var day = $("#" + place + "-day").val();
var year = $('#' + place + '-year').val(); var year = $("#" + place + "-year").val();
var hour = $('#' + place + '-hour').val(); var hour = $("#" + place + "-hour").val();
var minute = $('#' + place + '-minute').val(); var minute = $("#" + place + "-minute").val();
var timezone = $('#' + place + '-timezone').val(); var timezone = $("#" + place + "-timezone").val();
var utc = convert_date_to_moment(month, day, year, hour, minute, timezone); var utc = convert_date_to_moment(month, day, year, hour, minute, timezone);
if (isNaN(utc.unix())) { if (isNaN(utc.unix())) {
$('#' + place).val(''); $("#" + place).val("");
$('#' + place + '-local').val(''); $("#" + place + "-local").val("");
$('#' + place + '-zonetime').val(''); $("#" + place + "-zonetime").val("");
} else { } else {
$('#' + place).val(utc.unix()); $("#" + place).val(utc.unix());
$('#' + place + '-local').val(utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz")); $("#" + place + "-local").val(
$('#' + place + '-zonetime').val(utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz")); utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz")
);
$("#" + place + "-zonetime").val(
utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz")
);
} }
} }
@ -72,14 +76,24 @@ function convert_date_to_moment(month, day, year, hour, minute, timezone) {
} }
// 2013-02-08 24:00 // 2013-02-08 24:00
var date_string = year.toString() + '-' + month_num + '-' + day_str + ' ' + hour_str + ':' + min_str + ':00'; var date_string =
year.toString() +
"-" +
month_num +
"-" +
day_str +
" " +
hour_str +
":" +
min_str +
":00";
var m = moment(date_string, moment.ISO_8601); var m = moment(date_string, moment.ISO_8601);
return m; return m;
} }
function update_configs(obj) { function update_configs(obj) {
var target = '/api/v1/configs'; var target = "/api/v1/configs";
var method = 'PATCH'; var method = "PATCH";
var params = {}; var params = {};
@ -107,15 +121,17 @@ function update_configs(obj){
CTFd.fetch(target, { CTFd.fetch(target, {
method: method, method: method,
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function(response) { })
return response.json() .then(function(response) {
}).then(function(data) { return response.json();
})
.then(function(data) {
window.location.reload(); window.location.reload();
}); });
} }
@ -125,21 +141,23 @@ function upload_logo(form) {
var upload = response.data[0]; var upload = response.data[0];
if (upload.location) { if (upload.location) {
var params = { var params = {
'value': upload.location value: upload.location
}; };
CTFd.fetch('/api/v1/configs/ctf_logo', { CTFd.fetch("/api/v1/configs/ctf_logo", {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location.reload() window.location.reload();
} else { } else {
ezal({ ezal({
title: "Error!", title: "Error!",
@ -158,19 +176,21 @@ function remove_logo() {
body: "Are you sure you'd like to remove the CTF logo?", body: "Are you sure you'd like to remove the CTF logo?",
success: function() { success: function() {
var params = { var params = {
'value': null value: null
}; };
CTFd.fetch('/api/v1/configs/ctf_logo', { CTFd.fetch("/api/v1/configs/ctf_logo", {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (data) { })
.then(function(data) {
window.location.reload(); window.location.reload();
}); });
} }
@ -178,53 +198,52 @@ function remove_logo() {
} }
$(function() { $(function() {
$('.config-section > form:not(.form-upload)').submit(function(e){ $(".config-section > form:not(.form-upload)").submit(function(e) {
e.preventDefault(); e.preventDefault();
var obj = $(this).serializeJSON(); var obj = $(this).serializeJSON();
update_configs(obj); update_configs(obj);
}); });
$('#logo-upload').submit(function(e){ $("#logo-upload").submit(function(e) {
e.preventDefault(); e.preventDefault();
var form = e.target; var form = e.target;
upload_logo(form); upload_logo(form);
}); });
$(".start-date").change(function() {
$('.start-date').change(function () { load_date_values("start");
load_date_values('start');
}); });
$('.end-date').change(function () { $(".end-date").change(function() {
load_date_values('end'); load_date_values("end");
}); });
$('.freeze-date').change(function () { $(".freeze-date").change(function() {
load_date_values('freeze'); load_date_values("freeze");
}); });
$('#export-button').click(function (e) { $("#export-button").click(function(e) {
e.preventDefault(); e.preventDefault();
var href = script_root + '/admin/export'; var href = script_root + "/admin/export";
window.location.href = $('#export-button').attr('href'); window.location.href = $("#export-button").attr("href");
}); });
$('#import-button').click(function (e) { $("#import-button").click(function(e) {
e.preventDefault(); e.preventDefault();
var import_file = document.getElementById('import-file').files[0]; var import_file = document.getElementById("import-file").files[0];
var form_data = new FormData(); var form_data = new FormData();
form_data.append('backup', import_file); form_data.append("backup", import_file);
form_data.append('nonce', csrf_nonce); form_data.append("nonce", csrf_nonce);
var pg = ezpg({ var pg = ezpg({
width: 0, width: 0,
title: "Upload Progress", title: "Upload Progress"
}); });
$.ajax({ $.ajax({
url: script_root + '/admin/import', url: script_root + "/admin/import",
type: 'POST', type: "POST",
data: form_data, data: form_data,
processData: false, processData: false,
contentType: false, contentType: false,
@ -251,19 +270,15 @@ $(function () {
// Refresh modal // Refresh modal
pg = ezpg({ pg = ezpg({
target: pg, target: pg,
width: 100, width: 100
}); });
setTimeout( setTimeout(function() {
function () { pg.modal("hide");
pg.modal('hide'); }, 500);
}, 500
);
setTimeout( setTimeout(function() {
function () {
window.location.reload(); window.location.reload();
}, 700 }, 700);
);
} }
}); });
}); });
@ -271,30 +286,32 @@ $(function () {
var hash = window.location.hash; var hash = window.location.hash;
if (hash) { if (hash) {
hash = hash.replace("<>[]'\"", ""); hash = hash.replace("<>[]'\"", "");
$('ul.nav a[href="' + hash + '"]').tab('show'); $('ul.nav a[href="' + hash + '"]').tab("show");
} }
$('.nav-pills a').click(function (e) { $(".nav-pills a").click(function(e) {
$(this).tab('show'); $(this).tab("show");
window.location.hash = this.hash; window.location.hash = this.hash;
}); });
var start = $('#start').val(); var start = $("#start").val();
var end = $('#end').val(); var end = $("#end").val();
var freeze = $('#freeze').val(); var freeze = $("#freeze").val();
if (start) { if (start) {
load_timestamp('start', start); load_timestamp("start", start);
} }
if (end) { if (end) {
load_timestamp('end', end); load_timestamp("end", end);
} }
if (freeze) { if (freeze) {
load_timestamp('freeze', freeze); load_timestamp("freeze", freeze);
} }
// Toggle username and password based on stored value // Toggle username and password based on stored value
$('#mail_useauth').change(function () { $("#mail_useauth")
$('#mail_username_password').toggle(this.checked); .change(function() {
}).change(); $("#mail_username_password").toggle(this.checked);
})
.change();
}); });

View File

@ -3,15 +3,15 @@ function upload_files(form, cb) {
form = form[0]; form = form[0];
} }
var formData = new FormData(form); var formData = new FormData(form);
formData.append('nonce', csrf_nonce); formData.append("nonce", csrf_nonce);
var pg = ezpg({ var pg = ezpg({
width: 0, width: 0,
title: "Upload Progress", title: "Upload Progress"
}); });
$.ajax({ $.ajax({
url: script_root + '/api/v1/files', url: script_root + "/api/v1/files",
data: formData, data: formData,
type: 'POST', type: "POST",
cache: false, cache: false,
contentType: false, contentType: false,
processData: false, processData: false,
@ -32,13 +32,11 @@ function upload_files(form, cb) {
// Refresh modal // Refresh modal
pg = ezpg({ pg = ezpg({
target: pg, target: pg,
width: 100, width: 100
}); });
setTimeout( setTimeout(function() {
function () { pg.modal("hide");
pg.modal('hide'); }, 500);
}, 500
);
if (cb) { if (cb) {
cb(data); cb(data);

View File

@ -1,20 +1,22 @@
$(document).ready(function() { $(document).ready(function() {
$('#notifications_form').submit(function(e){ $("#notifications_form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var form = $('#notifications_form'); var form = $("#notifications_form");
var params = form.serializeJSON(); var params = form.serializeJSON();
CTFd.fetch('/api/v1/notifications', { CTFd.fetch("/api/v1/notifications", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
setTimeout(function() { setTimeout(function() {
window.location.reload(); window.location.reload();
@ -23,20 +25,22 @@ $(document).ready(function () {
}); });
}); });
$('.delete-notification').click(function (e) { $(".delete-notification").click(function(e) {
e.preventDefault(); e.preventDefault();
var elem = $(this); var elem = $(this);
var notif_id = elem.attr("notif-id"); var notif_id = elem.attr("notif-id");
ezq({ ezq({
title: 'Delete Notification', title: "Delete Notification",
body: "Are you sure you want to delete this notification?", body: "Are you sure you want to delete this notification?",
success: function() { success: function() {
CTFd.fetch('/api/v1/notifications/' + notif_id, { CTFd.fetch("/api/v1/notifications/" + notif_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
elem.parent().remove(); elem.parent().remove();
} }

View File

@ -1,70 +1,83 @@
var editor = CodeMirror.fromTextArea( var editor = CodeMirror.fromTextArea(
document.getElementById("admin-pages-editor"), { document.getElementById("admin-pages-editor"),
{
lineNumbers: true, lineNumbers: true,
lineWrapping: true, lineWrapping: true,
mode: "xml", mode: "xml",
htmlMode: true, htmlMode: true
} }
); );
function show_files(data) { function show_files(data) {
var list = $('#media-library-list'); var list = $("#media-library-list");
list.empty(); list.empty();
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var f = data[i]; var f = data[i];
var fname = f.location.split('/').pop(); var fname = f.location.split("/").pop();
var ext = get_filetype_icon_class(f.location); var ext = get_filetype_icon_class(f.location);
var wrapper = $('<div>').attr('class', 'media-item-wrapper'); var wrapper = $("<div>").attr("class", "media-item-wrapper");
var link = $('<a>'); var link = $("<a>");
link.attr('href', '##'); link.attr("href", "##");
if (ext === undefined) { if (ext === undefined) {
link.append('<i class="far fa-file" aria-hidden="true"></i> '.format(ext)); link.append(
'<i class="far fa-file" aria-hidden="true"></i> '.format(ext)
);
} else { } else {
link.append('<i class="far {0}" aria-hidden="true"></i> '.format(ext)); link.append('<i class="far {0}" aria-hidden="true"></i> '.format(ext));
} }
link.append($('<small>').attr('class', 'media-item-title').text(fname)); link.append(
$("<small>")
.attr("class", "media-item-title")
.text(fname)
);
link.click(function(e) { link.click(function(e) {
var media_div = $(this).parent(); var media_div = $(this).parent();
var icon = $(this).find('i')[0]; var icon = $(this).find("i")[0];
var f_loc = media_div.attr('data-location'); var f_loc = media_div.attr("data-location");
var fname = media_div.attr('data-filename'); var fname = media_div.attr("data-filename");
var f_id = media_div.attr('data-id'); var f_id = media_div.attr("data-id");
$('#media-delete').attr('data-id', f_id); $("#media-delete").attr("data-id", f_id);
$('#media-link').val(f_loc); $("#media-link").val(f_loc);
$('#media-filename').html( $("#media-filename").html(
$('<a>').attr('href', f_loc).attr('target', '_blank').text(fname) $("<a>")
.attr("href", f_loc)
.attr("target", "_blank")
.text(fname)
); );
$('#media-icon').empty(); $("#media-icon").empty();
if ($(icon).hasClass('fa-file-image')) { if ($(icon).hasClass("fa-file-image")) {
$('#media-icon').append($('<img>').attr('src', f_loc).css({ $("#media-icon").append(
'max-width': '100%', $("<img>")
'max-height': '100%', .attr("src", f_loc)
'object-fit': 'contain' .css({
})); "max-width": "100%",
"max-height": "100%",
"object-fit": "contain"
})
);
} else { } else {
// icon is empty so we need to pull outerHTML // icon is empty so we need to pull outerHTML
var copy_icon = $(icon).clone(); var copy_icon = $(icon).clone();
$(copy_icon).addClass('fa-4x'); $(copy_icon).addClass("fa-4x");
$('#media-icon').append(copy_icon); $("#media-icon").append(copy_icon);
} }
$('#media-item').show(); $("#media-item").show();
}); });
wrapper.append(link); wrapper.append(link);
wrapper.attr('data-location', script_root + '/files/' + f.location); wrapper.attr("data-location", script_root + "/files/" + f.location);
wrapper.attr('data-id', f.id); wrapper.attr("data-id", f.id);
wrapper.attr('data-filename', fname); wrapper.attr("data-filename", fname);
list.append(wrapper); list.append(wrapper);
} }
} }
function refresh_files(cb) { function refresh_files(cb) {
get_page_files().then(function(response) { get_page_files().then(function(response) {
var data = response.data; var data = response.data;
@ -84,88 +97,91 @@ function insert_at_cursor(editor, text) {
function submit_form() { function submit_form() {
editor.save(); // Save the CodeMirror data to the Textarea editor.save(); // Save the CodeMirror data to the Textarea
var params = $("#page-edit").serializeJSON(); var params = $("#page-edit").serializeJSON();
var target = '/api/v1/pages'; var target = "/api/v1/pages";
var method = 'POST'; var method = "POST";
if (params.id) { if (params.id) {
target += '/' + params.id; target += "/" + params.id;
method = 'PATCH'; method = "PATCH";
} }
CTFd.fetch(target, { CTFd.fetch(target, {
method: method, method: method,
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function(response){ })
if (method === 'PATCH' && response.success) { .then(function(response) {
if (method === "PATCH" && response.success) {
ezal({ ezal({
title: 'Saved', title: "Saved",
body: 'Your changes have been saved', body: "Your changes have been saved",
button: 'Okay' button: "Okay"
}); });
} else { } else {
window.location = script_root + '/admin/pages/' + response.data.id; window.location = script_root + "/admin/pages/" + response.data.id;
} }
}); });
} }
function preview_page() { function preview_page() {
editor.save(); // Save the CodeMirror data to the Textarea editor.save(); // Save the CodeMirror data to the Textarea
$('#page-edit').attr('action', script_root + '/admin/pages/preview'); $("#page-edit").attr("action", script_root + "/admin/pages/preview");
$('#page-edit').attr('target', '_blank'); $("#page-edit").attr("target", "_blank");
$('#page-edit').submit(); $("#page-edit").submit();
} }
function upload_media() { function upload_media() {
upload_files($('#media-library-upload'), function (data) { upload_files($("#media-library-upload"), function(data) {
refresh_files(); refresh_files();
}); });
} }
$(document).ready(function() { $(document).ready(function() {
$('#media-insert').click(function (e) { $("#media-insert").click(function(e) {
var tag = ''; var tag = "";
try { try {
tag = $('#media-icon').children()[0].nodeName.toLowerCase(); tag = $("#media-icon")
.children()[0]
.nodeName.toLowerCase();
} catch (err) { } catch (err) {
tag = ''; tag = "";
} }
var link = $('#media-link').val(); var link = $("#media-link").val();
var fname = $('#media-filename').text(); var fname = $("#media-filename").text();
var entry = null; var entry = null;
if (tag === 'img') { if (tag === "img") {
entry = '![{0}]({1})'.format(fname, link); entry = "![{0}]({1})".format(fname, link);
} else { } else {
entry = '[{0}]({1})'.format(fname, link); entry = "[{0}]({1})".format(fname, link);
} }
insert_at_cursor(editor, entry); insert_at_cursor(editor, entry);
}); });
$('#media-download').click(function (e) { $("#media-download").click(function(e) {
var link = $('#media-link').val(); var link = $("#media-link").val();
window.open(link, "_blank"); window.open(link, "_blank");
}); });
$('#media-delete').click(function (e) { $("#media-delete").click(function(e) {
var file_id = $(this).attr('data-id'); var file_id = $(this).attr("data-id");
ezq({ ezq({
title: "Delete File?", title: "Delete File?",
body: "Are you sure you want to delete this file?", body: "Are you sure you want to delete this file?",
success: function() { success: function() {
CTFd.fetch('/api/v1/files/' + file_id, { CTFd.fetch("/api/v1/files/" + file_id, {
method: 'DELETE', method: "DELETE",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, }
}).then(function(response) { }).then(function(response) {
if (response.status === 200) { if (response.status === 200) {
response.json().then(function(object) { response.json().then(function(object) {
@ -179,15 +195,15 @@ $(document).ready(function () {
}); });
}); });
$('#save-page').click(function (e) { $("#save-page").click(function(e) {
e.preventDefault(); e.preventDefault();
submit_form(); submit_form();
}); });
$('#media-button').click(function () { $("#media-button").click(function() {
$('#media-library-list').empty(); $("#media-library-list").empty();
refresh_files(function() { refresh_files(function() {
$('#media-modal').modal('show'); $("#media-modal").modal("show");
}); });
// get_page_files().then(function (data) { // get_page_files().then(function (data) {
// var files = data; // var files = data;

View File

@ -1,59 +1,57 @@
function get_filetype_icon_class(filename) { function get_filetype_icon_class(filename) {
var mapping = { var mapping = {
// Image Files // Image Files
'png': 'fa-file-image', png: "fa-file-image",
'jpg': 'fa-file-image', jpg: "fa-file-image",
'jpeg': 'fa-file-image', jpeg: "fa-file-image",
'gif': 'fa-file-image', gif: "fa-file-image",
'bmp': 'fa-file-image', bmp: "fa-file-image",
'svg': 'fa-file-image', svg: "fa-file-image",
// Text Files // Text Files
'txt': 'fa-file-alt', txt: "fa-file-alt",
// Video Files // Video Files
'mov': 'fa-file-video', mov: "fa-file-video",
'mp4': 'fa-file-video', mp4: "fa-file-video",
'wmv': 'fa-file-video', wmv: "fa-file-video",
'flv': 'fa-file-video', flv: "fa-file-video",
'mkv': 'fa-file-video', mkv: "fa-file-video",
'avi': 'fa-file-video', avi: "fa-file-video",
// PDF Files // PDF Files
'pdf': 'fa-file-pdf', pdf: "fa-file-pdf",
// Audio Files // Audio Files
'mp3': 'fa-file-sound', mp3: "fa-file-sound",
'wav': 'fa-file-sound', wav: "fa-file-sound",
'aac': 'fa-file-sound', aac: "fa-file-sound",
// Archive Files // Archive Files
'zip': 'fa-file-archive', zip: "fa-file-archive",
'gz': 'fa-file-archive', gz: "fa-file-archive",
'tar': 'fa-file-archive', tar: "fa-file-archive",
'7z': 'fa-file-archive', "7z": "fa-file-archive",
'rar': 'fa-file-archive', rar: "fa-file-archive",
// Code Files // Code Files
'py': 'fa-file-code', py: "fa-file-code",
'c': 'fa-file-code', c: "fa-file-code",
'cpp': 'fa-file-code', cpp: "fa-file-code",
'html': 'fa-file-code', html: "fa-file-code",
'js': 'fa-file-code', js: "fa-file-code",
'rb': 'fa-file-code', rb: "fa-file-code",
'go': 'fa-file-code' go: "fa-file-code"
}; };
var ext = filename.split('.').pop(); var ext = filename.split(".").pop();
return mapping[ext]; return mapping[ext];
} }
function get_page_files() { function get_page_files() {
return CTFd.fetch( return CTFd.fetch("/api/v1/files?type=page", {
'/api/v1/files?type=page', { credentials: "same-origin"
credentials: 'same-origin', }).then(function(response) {
}
).then(function (response) {
return response.json(); return response.json();
}); });
} }

View File

@ -1,21 +1,26 @@
$(document).ready(function() { $(document).ready(function() {
$('.delete-page').click(function () { $(".delete-page").click(function() {
var elem = $(this); var elem = $(this);
var name = elem.attr("page-route"); var name = elem.attr("page-route");
var page_id = elem.attr("page-id"); var page_id = elem.attr("page-id");
ezq({ ezq({
title: 'Delete ' + name, title: "Delete " + name,
body: "Are you sure you want to delete {0}?".format( body: "Are you sure you want to delete {0}?".format(
"<strong>" + htmlentities(name) + "</strong>" "<strong>" + htmlentities(name) + "</strong>"
), ),
success: function() { success: function() {
CTFd.fetch('/api/v1/pages/' + page_id, { CTFd.fetch("/api/v1/pages/" + page_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
elem.parent().parent().remove(); elem
.parent()
.parent()
.remove();
} }
}); });
} }

View File

@ -1,7 +1,7 @@
function toggle_account(elem) { function toggle_account(elem) {
var btn = $(elem); var btn = $(elem);
var teamId = btn.attr('team-id'); var teamId = btn.attr("team-id");
var state = btn.attr('state'); var state = btn.attr("state");
var hidden = undefined; var hidden = undefined;
if (state == "visible") { if (state == "visible") {
hidden = true; hidden = true;
@ -10,29 +10,31 @@ function toggle_account(elem) {
} }
var params = { var params = {
'hidden': hidden hidden: hidden
}; };
CTFd.fetch('/api/v1/'+ user_mode +'/' + teamId, { CTFd.fetch("/api/v1/" + user_mode + "/" + teamId, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
if (hidden) { if (hidden) {
btn.attr('state', 'hidden'); btn.attr("state", "hidden");
btn.addClass('btn-danger').removeClass('btn-success'); btn.addClass("btn-danger").removeClass("btn-success");
btn.text('Hidden'); btn.text("Hidden");
} else { } else {
btn.attr('state', 'visible'); btn.attr("state", "visible");
btn.addClass('btn-success').removeClass('btn-danger'); btn.addClass("btn-success").removeClass("btn-danger");
btn.text('Visible'); btn.text("Visible");
} }
} }
}); });

View File

@ -1,5 +1,7 @@
function solves_graph() { function solves_graph() {
$.get(script_root + '/api/v1/statistics/challenges/solves', function (response) { $.get(script_root + "/api/v1/statistics/challenges/solves", function(
response
) {
var data = response.data; var data = response.data;
var res = $.parseJSON(JSON.stringify(data)); var res = $.parseJSON(JSON.stringify(data));
var chals = []; var chals = [];
@ -9,14 +11,14 @@ function solves_graph() {
var i = 1; var i = 1;
var solves = {}; var solves = {};
for (var c = 0; c < res.length; c++) { for (var c = 0; c < res.length; c++) {
solves[res[c]['id']] = { solves[res[c]["id"]] = {
name: res[c]['name'], name: res[c]["name"],
solves: res[c]['solves'], solves: res[c]["solves"]
}; };
} }
var solves_order = Object.keys(solves).sort(function(a, b) { var solves_order = Object.keys(solves).sort(function(a, b) {
return solves[b].solves - solves[a].solves return solves[b].solves - solves[a].solves;
}); });
$.each(solves_order, function(key, value) { $.each(solves_order, function(key, value) {
@ -27,75 +29,82 @@ function solves_graph() {
x: solves[value].name, x: solves[value].name,
y: solves[value].solves, y: solves[value].solves,
text: solves[value].solves, text: solves[value].solves,
xanchor: 'center', xanchor: "center",
yanchor: 'bottom', yanchor: "bottom",
showarrow: false, showarrow: false
}; };
annotations.push(result); annotations.push(result);
}); });
var data = [{ var data = [
type: 'bar', {
type: "bar",
x: chals, x: chals,
y: counts, y: counts,
text: counts, text: counts,
orientation: 'v', orientation: "v"
/*marker: { /*marker: {
color: colors color: colors
},*/ },*/
}]; }
];
var layout = { var layout = {
title: 'Solve Counts', title: "Solve Counts",
annotations: annotations, annotations: annotations,
xaxis: { xaxis: {
title: 'Challenge Name' title: "Challenge Name"
}, },
yaxis: { yaxis: {
title: 'Amount of Solves' title: "Amount of Solves"
} }
}; };
$('#solves-graph').empty(); $("#solves-graph").empty();
document.getElementById('solves-graph').fn = 'CTFd_solves_' + (new Date).toISOString().slice(0, 19); document.getElementById("solves-graph").fn =
Plotly.newPlot('solves-graph', data, layout); "CTFd_solves_" + new Date().toISOString().slice(0, 19);
Plotly.newPlot("solves-graph", data, layout);
}); });
} }
function keys_percentage_graph() { function keys_percentage_graph() {
// Solves and Fails pie chart // Solves and Fails pie chart
$.get(script_root + '/api/v1/statistics/submissions/type', function (response) { $.get(script_root + "/api/v1/statistics/submissions/type", function(
response
) {
var data = response.data; var data = response.data;
var res = $.parseJSON(JSON.stringify(data)); var res = $.parseJSON(JSON.stringify(data));
var solves = res['correct']; var solves = res["correct"];
var fails = res['incorrect']; var fails = res["incorrect"];
var data = [{ var data = [
{
values: [solves, fails], values: [solves, fails],
labels: ['Correct', 'Incorrect'], labels: ["Correct", "Incorrect"],
marker: { marker: {
colors: [ colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
}, },
text: ['Solves', 'Fails'], text: ["Solves", "Fails"],
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Submission Percentages' title: "Submission Percentages"
}; };
$('#keys-pie-graph').empty(); $("#keys-pie-graph").empty();
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_' + (new Date).toISOString().slice(0, 19); document.getElementById("keys-pie-graph").fn =
Plotly.newPlot('keys-pie-graph', data, layout); "CTFd_submissions_" + new Date().toISOString().slice(0, 19);
Plotly.newPlot("keys-pie-graph", data, layout);
}); });
} }
function category_breakdown_graph() { function category_breakdown_graph() {
$.get(script_root + '/api/v1/statistics/challenges/category', function (response) { $.get(script_root + "/api/v1/statistics/challenges/category", function(
response
) {
var data = response.data; var data = response.data;
var res = $.parseJSON(JSON.stringify(data)); var res = $.parseJSON(JSON.stringify(data));
@ -114,25 +123,30 @@ function category_breakdown_graph() {
count.push(res[i].count); count.push(res[i].count);
} }
var data = [{ var data = [
{
values: count, values: count,
labels: categories, labels: categories,
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Category Breakdown' title: "Category Breakdown"
}; };
$('#categories-pie-graph').empty(); $("#categories-pie-graph").empty();
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_' + (new Date).toISOString().slice(0, 19); document.getElementById("categories-pie-graph").fn =
Plotly.newPlot('categories-pie-graph', data, layout); "CTFd_categories_" + new Date().toISOString().slice(0, 19);
Plotly.newPlot("categories-pie-graph", data, layout);
}); });
} }
function solve_percentages_graph() { function solve_percentages_graph() {
$.get(script_root + '/api/v1/statistics/challenges/solves/percentages', function (response) { $.get(
script_root + "/api/v1/statistics/challenges/solves/percentages",
function(response) {
var res = response.data; var res = response.data;
var names = []; var names = [];
@ -143,42 +157,48 @@ function solve_percentages_graph() {
for (var key in res) { for (var key in res) {
names.push(res[key].name); names.push(res[key].name);
percents.push((res[key].percentage * 100)); percents.push(res[key].percentage * 100);
var result = { var result = {
x: res[key].name, x: res[key].name,
y: (res[key].percentage * 100), y: res[key].percentage * 100,
text: Math.round(res[key].percentage * 100) + '%', text: Math.round(res[key].percentage * 100) + "%",
xanchor: 'center', xanchor: "center",
yanchor: 'bottom', yanchor: "bottom",
showarrow: false, showarrow: false
}; };
annotations.push(result); annotations.push(result);
} }
var data = [{ var data = [
type: 'bar', {
type: "bar",
x: names, x: names,
y: percents, y: percents,
orientation: 'v' orientation: "v"
}]; }
];
var layout = { var layout = {
title: 'Solve Percentages per Challenge', title: "Solve Percentages per Challenge",
xaxis: { xaxis: {
title: 'Challenge Name' title: "Challenge Name"
}, },
yaxis: { yaxis: {
title: 'Percentage of {0} (%)'.format(user_mode.charAt(0).toUpperCase() + user_mode.slice(1)), title: "Percentage of {0} (%)".format(
user_mode.charAt(0).toUpperCase() + user_mode.slice(1)
),
range: [0, 100] range: [0, 100]
}, },
annotations: annotations annotations: annotations
}; };
$('#solve-percentages-graph').empty(); $("#solve-percentages-graph").empty();
document.getElementById('solve-percentages-graph').fn = 'CTFd_challenge_percentages_' + (new Date).toISOString().slice(0, 19); document.getElementById("solve-percentages-graph").fn =
Plotly.newPlot('solve-percentages-graph', data, layout); "CTFd_challenge_percentages_" + new Date().toISOString().slice(0, 19);
}); Plotly.newPlot("solve-percentages-graph", data, layout);
}
);
} }
function update() { function update() {
@ -191,11 +211,11 @@ function update() {
$(function() { $(function() {
update(); update();
window.onresize = function() { window.onresize = function() {
console.log('resizing'); console.log("resizing");
Plotly.Plots.resize(document.getElementById('keys-pie-graph')); Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
Plotly.Plots.resize(document.getElementById('categories-pie-graph')); Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
Plotly.Plots.resize(document.getElementById('solves-graph')); Plotly.Plots.resize(document.getElementById("solves-graph"));
Plotly.Plots.resize(document.getElementById('solve-percentages-graph')); Plotly.Plots.resize(document.getElementById("solve-percentages-graph"));
}; };
}); });

View File

@ -1,25 +1,24 @@
$(".form-control").bind({ $(".form-control").bind({
focus: function() { focus: function() {
$(this).addClass('input-filled-valid' ); $(this).addClass("input-filled-valid");
}, },
blur: function() { blur: function() {
if ($(this).val() === '') { if ($(this).val() === "") {
$(this).removeClass('input-filled-valid' ); $(this).removeClass("input-filled-valid");
} }
} }
}); });
$('.modal').on('show.bs.modal', function (e) { $(".modal").on("show.bs.modal", function(e) {
$('.form-control').each(function () { $(".form-control").each(function() {
if ($(this).val()) { if ($(this).val()) {
$(this).addClass("input-filled-valid"); $(this).addClass("input-filled-valid");
} }
}); });
}); });
$(function() { $(function() {
$('.form-control').each(function () { $(".form-control").each(function() {
if ($(this).val()) { if ($(this).val()) {
$(this).addClass("input-filled-valid"); $(this).addClass("input-filled-valid");
} }
@ -28,7 +27,7 @@ $(function () {
$("tr").click(function() { $("tr").click(function() {
var sel = getSelection().toString(); var sel = getSelection().toString();
if (!sel) { if (!sel) {
var href = $(this).attr('data-href'); var href = $(this).attr("data-href");
if (href) { if (href) {
window.location = href; window.location = href;
} }
@ -38,10 +37,10 @@ $(function () {
$("tr a, button").click(function(e) { $("tr a, button").click(function(e) {
// TODO: This is a hack to allow modal close buttons to work // TODO: This is a hack to allow modal close buttons to work
if (!$(this).attr('data-dismiss')) { if (!$(this).attr("data-dismiss")) {
e.stopPropagation(); e.stopPropagation();
} }
}); });
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip();
}); });

View File

@ -1,27 +1,39 @@
// TODO: Replace this with CTFd JS library // TODO: Replace this with CTFd JS library
$(document).ready(function() { $(document).ready(function() {
$('.delete-correct-submission').click(function () { $(".delete-correct-submission").click(function() {
var elem = $(this).parent().parent(); var elem = $(this)
var chal = elem.find('.chal').attr('id'); .parent()
var chal_name = elem.find('.chal').text().trim(); .parent();
var team = elem.find('.team').attr('id'); var chal = elem.find(".chal").attr("id");
var team_name = elem.find('.team').text().trim(); var chal_name = elem
var key_id = elem.find('.flag').attr('id'); .find(".chal")
.text()
.trim();
var team = elem.find(".team").attr("id");
var team_name = elem
.find(".team")
.text()
.trim();
var key_id = elem.find(".flag").attr("id");
var td_row = $(this).parent().parent(); var td_row = $(this)
.parent()
.parent();
ezq({ ezq({
title: 'Delete Submission', title: "Delete Submission",
body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format( body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format(
"<strong>" + htmlentities(team_name) + "</strong>", "<strong>" + htmlentities(team_name) + "</strong>",
"<strong>" + htmlentities(chal_name) + "</strong>" "<strong>" + htmlentities(chal_name) + "</strong>"
), ),
success: function() { success: function() {
CTFd.fetch('/api/v1/submissions/' + key_id, { CTFd.fetch("/api/v1/submissions/" + key_id, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
td_row.remove(); td_row.remove();
} }

View File

@ -1,35 +1,39 @@
$(document).ready(function() { $(document).ready(function() {
$('.edit-team').click(function (e) { $(".edit-team").click(function(e) {
$('#team-info-modal').modal('toggle'); $("#team-info-modal").modal("toggle");
}); });
$('.edit-captain').click(function (e) { $(".edit-captain").click(function(e) {
$('#team-captain-modal').modal('toggle'); $("#team-captain-modal").modal("toggle");
}); });
$('.delete-team').click(function (e) { $(".delete-team").click(function(e) {
ezq({ ezq({
title: "Delete Team", title: "Delete Team",
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(TEAM_NAME) + "</strong>"), body: "Are you sure you want to delete {0}".format(
"<strong>" + htmlentities(TEAM_NAME) + "</strong>"
),
success: function() { success: function() {
CTFd.fetch('/api/v1/teams/' + TEAM_ID, { CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location = script_root + '/admin/teams'; window.location = script_root + "/admin/teams";
} }
}); });
} }
}); });
}); });
$('.delete-submission').click(function (e) { $(".delete-submission").click(function(e) {
e.preventDefault(); e.preventDefault();
var submission_id = $(this).attr('submission-id'); var submission_id = $(this).attr("submission-id");
var submission_type = $(this).attr('submission-type'); var submission_type = $(this).attr("submission-type");
var submission_challenge = $(this).attr('submission-challenge'); var submission_challenge = $(this).attr("submission-challenge");
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format( var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
htmlentities(submission_type), htmlentities(submission_type),
@ -37,22 +41,26 @@ $(document).ready(function () {
htmlentities(submission_challenge) htmlentities(submission_challenge)
); );
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Submission", title: "Delete Submission",
body: body, body: body,
success: function() { success: function() {
CTFd.fetch('/api/v1/submissions/' + submission_id, { CTFd.fetch("/api/v1/submissions/" + submission_id, {
method: 'DELETE', method: "DELETE",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }
@ -61,32 +69,36 @@ $(document).ready(function () {
}); });
}); });
$('.delete-award').click(function (e) { $(".delete-award").click(function(e) {
e.preventDefault(); e.preventDefault();
var award_id = $(this).attr('award-id'); var award_id = $(this).attr("award-id");
var award_name = $(this).attr('award-name'); var award_name = $(this).attr("award-name");
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format( var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
htmlentities(award_name), htmlentities(award_name),
htmlentities(TEAM_NAME) htmlentities(TEAM_NAME)
); );
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Award", title: "Delete Award",
body: body, body: body,
success: function() { success: function() {
CTFd.fetch('/api/v1/awards/' + award_id, { CTFd.fetch("/api/v1/awards/" + award_id, {
method: 'DELETE', method: "DELETE",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }

View File

@ -2,8 +2,12 @@ $(document).ready(function () {
function scoregraph() { function scoregraph() {
var times = []; var times = [];
var scores = []; var scores = [];
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/solves', function (solve_data) { $.get(script_root + "/api/v1/teams/" + TEAM_ID + "/solves", function(
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/awards', function (award_data) { solve_data
) {
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/awards", function(
award_data
) {
var solves = solve_data.data; var solves = solve_data.data;
var awards = award_data.data; var awards = award_data.data;
@ -28,85 +32,94 @@ $(document).ready(function () {
{ {
x: times, x: times,
y: scores, y: scores,
type: 'scatter', type: "scatter",
marker: { marker: {
color: colorhash(TEAM_NAME + TEAM_ID), color: colorhash(TEAM_NAME + TEAM_ID)
}, },
line: { line: {
color: colorhash(TEAM_NAME + TEAM_ID), color: colorhash(TEAM_NAME + TEAM_ID)
}, },
fill: 'tozeroy' fill: "tozeroy"
} }
]; ];
var layout = { var layout = {
title: 'Score over Time', title: "Score over Time",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
hovermode: 'closest', hovermode: "closest",
xaxis: { xaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
yaxis: { yaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
legend: { legend: {
"orientation": "h" orientation: "h"
} }
}; };
$('#score-graph').empty(); $("#score-graph").empty();
document.getElementById('score-graph').fn = 'CTFd_score_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("score-graph").fn =
Plotly.newPlot('score-graph', data, layout); "CTFd_score_team_" +
TEAM_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("score-graph", data, layout);
}); });
}); });
} }
function keys_percentage_graph() { function keys_percentage_graph() {
var base_url = script_root + '/api/v1/teams/' + TEAM_ID; var base_url = script_root + "/api/v1/teams/" + TEAM_ID;
$.get(base_url + '/fails', function (fails) { $.get(base_url + "/fails", function(fails) {
$.get(base_url + '/solves', function (solves) { $.get(base_url + "/solves", function(solves) {
var solves_count = solves.data.length; var solves_count = solves.data.length;
var fails_count = fails.data.length; var fails_count = fails.data.length;
var graph_data = [{ var graph_data = [
{
values: [solves_count, fails_count], values: [solves_count, fails_count],
labels: ['Solves', 'Fails'], labels: ["Solves", "Fails"],
marker: { marker: {
colors: [ colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
}, },
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Solve Percentages', title: "Solve Percentages",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
legend: { legend: {
"orientation": "h" orientation: "h"
} }
}; };
$('#keys-pie-graph').empty(); $("#keys-pie-graph").empty();
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("keys-pie-graph").fn =
Plotly.newPlot('keys-pie-graph', graph_data, layout); "CTFd_submissions_team_" +
TEAM_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("keys-pie-graph", graph_data, layout);
}); });
}); });
} }
function category_breakdown_graph() { function category_breakdown_graph() {
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/solves', function (response) { $.get(script_root + "/api/v1/teams/" + TEAM_ID + "/solves", function(
response
) {
var solves = response.data; var solves = response.data;
var categories = []; var categories = [];
for (var i = 0; i < solves.length; i++) { for (var i = 0; i < solves.length; i++) {
categories.push(solves[i].challenge.category) categories.push(solves[i].challenge.category);
} }
var keys = categories.filter(function(elem, pos) { var keys = categories.filter(function(elem, pos) {
@ -121,28 +134,34 @@ $(document).ready(function () {
count++; count++;
} }
} }
counts.push(count) counts.push(count);
} }
var data = [{ var data = [
{
values: counts, values: counts,
labels: keys, labels: keys,
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Category Breakdown', title: "Category Breakdown",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
legend: { legend: {
"orientation": "v" orientation: "v"
} }
}; };
$('#categories-pie-graph').empty(); $("#categories-pie-graph").empty();
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("categories-pie-graph").fn =
Plotly.newPlot('categories-pie-graph', data, layout); "CTFd_categories_team_" +
TEAM_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("categories-pie-graph", data, layout);
}); });
} }
@ -150,10 +169,9 @@ $(document).ready(function () {
keys_percentage_graph(); keys_percentage_graph();
scoregraph(); scoregraph();
window.onresize = function() { window.onresize = function() {
Plotly.Plots.resize(document.getElementById('keys-pie-graph')); Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
Plotly.Plots.resize(document.getElementById('categories-pie-graph')); Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
Plotly.Plots.resize(document.getElementById('score-graph')); Plotly.Plots.resize(document.getElementById("score-graph"));
}; };
}); });

View File

@ -1,72 +1,77 @@
$(document).ready(function() { $(document).ready(function() {
$('#team-info-form').submit(function (e) { $("#team-info-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#team-info-form').serializeJSON(true); var params = $("#team-info-form").serializeJSON(true);
CTFd.fetch('/api/v1/teams/' + TEAM_ID, { CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location.reload(); window.location.reload();
} else { } else {
$('#team-info-form > #results').empty(); $("#team-info-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#team-info-form > #results').append( $("#team-info-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#team-info-form').find('input[name={0}]'.format(key)); var i = $("#team-info-form").find("input[name={0}]".format(key));
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}) });
}); });
$("#team-captain-form").submit(function(e) {
$('#team-captain-form').submit(function (e) {
e.preventDefault(); e.preventDefault();
var params = $('#team-captain-form').serializeJSON(true); var params = $("#team-captain-form").serializeJSON(true);
CTFd.fetch('/api/v1/teams/' + TEAM_ID, { CTFd.fetch("/api/v1/teams/" + TEAM_ID, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location.reload(); window.location.reload();
} else { } else {
$('#team-captain-form > #results').empty(); $("#team-captain-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#team-captain-form > #results').append( $("#team-captain-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#team-captain-form').find('select[name={0}]'.format(key)); var i = $("#team-captain-form").find(
"select[name={0}]".format(key)
);
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}) });
}); });
}); });

View File

@ -1,37 +1,39 @@
$(document).ready(function() { $(document).ready(function() {
$('#team-info-form').submit(function (e) { $("#team-info-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#team-info-form').serializeJSON(true); var params = $("#team-info-form").serializeJSON(true);
CTFd.fetch('/api/v1/teams', { CTFd.fetch("/api/v1/teams", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
var team_id = response.data.id; var team_id = response.data.id;
window.location = script_root + '/admin/teams/' + team_id; window.location = script_root + "/admin/teams/" + team_id;
} else { } else {
$('#team-info-form > #results').empty(); $("#team-info-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#team-info-form > #results').append( $("#team-info-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#team-info-form').find('input[name={0}]'.format(key)); var i = $("#team-info-form").find("input[name={0}]".format(key));
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}) });
}); });
}); });

View File

@ -1,3 +1 @@
$(document).ready(function () { $(document).ready(function() {});
});

View File

@ -1,114 +1,126 @@
$(document).ready(function() { $(document).ready(function() {
$('.delete-user').click(function(e){ $(".delete-user").click(function(e) {
ezq({ ezq({
title: "Delete User", title: "Delete User",
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(USER_NAME) + "</strong>"), body: "Are you sure you want to delete {0}".format(
"<strong>" + htmlentities(USER_NAME) + "</strong>"
),
success: function() { success: function() {
CTFd.fetch('/api/v1/users/' + USER_ID, { CTFd.fetch("/api/v1/users/" + USER_ID, {
method: 'DELETE', method: "DELETE"
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location = script_root + '/admin/users'; window.location = script_root + "/admin/users";
} }
}); });
} }
}); });
}); });
$('.edit-user').click(function (e) { $(".edit-user").click(function(e) {
$('#user-info-modal').modal('toggle'); $("#user-info-modal").modal("toggle");
}); });
$('.award-user').click(function (e) { $(".award-user").click(function(e) {
$('#user-award-modal').modal('toggle'); $("#user-award-modal").modal("toggle");
}); });
$('.email-user').click(function (e) { $(".email-user").click(function(e) {
$('#user-email-modal').modal('toggle'); $("#user-email-modal").modal("toggle");
}); });
$('#user-award-form').submit(function(e){ $("#user-award-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#user-award-form').serializeJSON(true); var params = $("#user-award-form").serializeJSON(true);
params['user_id'] = USER_ID; params["user_id"] = USER_ID;
CTFd.fetch('/api/v1/awards', { CTFd.fetch("/api/v1/awards", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location.reload() window.location.reload();
} else { } else {
$('#user-award-form > #results').empty(); $("#user-award-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#user-award-form > #results').append( $("#user-award-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#user-award-form').find('input[name={0}]'.format(key)); var i = $("#user-award-form").find("input[name={0}]".format(key));
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}); });
}); });
$('#user-mail-form').submit(function(e){ $("#user-mail-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#user-mail-form').serializeJSON(true); var params = $("#user-mail-form").serializeJSON(true);
CTFd.fetch('/api/v1/users/'+USER_ID+'/email', { CTFd.fetch("/api/v1/users/" + USER_ID + "/email", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
$('#user-mail-form > #results').append( $("#user-mail-form > #results").append(
ezbadge({ ezbadge({
type: 'success', type: "success",
body: 'E-Mail sent successfully!' body: "E-Mail sent successfully!"
}) })
); );
$('#user-mail-form').find("input[type=text], textarea").val("") $("#user-mail-form")
.find("input[type=text], textarea")
.val("");
} else { } else {
$('#user-mail-form > #results').empty(); $("#user-mail-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#user-mail-form > #results').append( $("#user-mail-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#user-mail-form').find('input[name={0}], textarea[name={0}]'.format(key)); var i = $("#user-mail-form").find(
"input[name={0}], textarea[name={0}]".format(key)
);
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}); });
}); });
$('.delete-submission').click(function(e){ $(".delete-submission").click(function(e) {
e.preventDefault(); e.preventDefault();
var submission_id = $(this).attr('submission-id'); var submission_id = $(this).attr("submission-id");
var submission_type = $(this).attr('submission-type'); var submission_type = $(this).attr("submission-type");
var submission_challenge = $(this).attr('submission-challenge'); var submission_challenge = $(this).attr("submission-challenge");
var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format( var body = "<span>Are you sure you want to delete <strong>{0}</strong> submission from <strong>{1}</strong> for <strong>{2}</strong>?</span>".format(
htmlentities(submission_type), htmlentities(submission_type),
@ -116,22 +128,26 @@ $(document).ready(function () {
htmlentities(submission_challenge) htmlentities(submission_challenge)
); );
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Submission", title: "Delete Submission",
body: body, body: body,
success: function() { success: function() {
CTFd.fetch('/api/v1/submissions/' + submission_id, { CTFd.fetch("/api/v1/submissions/" + submission_id, {
method: 'DELETE', method: "DELETE",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }
@ -140,32 +156,36 @@ $(document).ready(function () {
}); });
}); });
$('.delete-award').click(function(e){ $(".delete-award").click(function(e) {
e.preventDefault(); e.preventDefault();
var award_id = $(this).attr('award-id'); var award_id = $(this).attr("award-id");
var award_name = $(this).attr('award-name'); var award_name = $(this).attr("award-name");
var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format( var body = "<span>Are you sure you want to delete the <strong>{0}</strong> award from <strong>{1}</strong>?".format(
htmlentities(award_name), htmlentities(award_name),
htmlentities(USER_NAME) htmlentities(USER_NAME)
); );
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
ezq({ ezq({
title: "Delete Award", title: "Delete Award",
body: body, body: body,
success: function() { success: function() {
CTFd.fetch('/api/v1/awards/' + award_id, { CTFd.fetch("/api/v1/awards/" + award_id, {
method: 'DELETE', method: "DELETE",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
row.remove(); row.remove();
} }
@ -174,10 +194,12 @@ $(document).ready(function () {
}); });
}); });
$('.correct-submission').click(function(e) { $(".correct-submission").click(function(e) {
var challenge_id = $(this).attr('challenge-id'); var challenge_id = $(this).attr("challenge-id");
var challenge_name = $(this).attr('challenge-name'); var challenge_name = $(this).attr("challenge-name");
var row = $(this).parent().parent(); var row = $(this)
.parent()
.parent();
var body = "<span>Are you sure you want to mark <strong>{0}</strong> solved for from <strong>{1}</strong>?".format( var body = "<span>Are you sure you want to mark <strong>{0}</strong> solved for from <strong>{1}</strong>?".format(
htmlentities(challenge_name), htmlentities(challenge_name),
@ -196,17 +218,19 @@ $(document).ready(function () {
title: "Mark Correct", title: "Mark Correct",
body: body, body: body,
success: function() { success: function() {
CTFd.fetch('/api/v1/submissions', { CTFd.fetch("/api/v1/submissions", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
// TODO: Refresh missing and solves instead of reloading // TODO: Refresh missing and solves instead of reloading
row.remove(); row.remove();
@ -214,6 +238,6 @@ $(document).ready(function () {
} }
}); });
} }
}) });
}); });
}); });

View File

@ -1,8 +1,12 @@
function scoregraph() { function scoregraph() {
var times = []; var times = [];
var scores = []; var scores = [];
$.get(script_root + '/api/v1/users/' + USER_ID + '/solves', function (solve_data) { $.get(script_root + "/api/v1/users/" + USER_ID + "/solves", function(
$.get(script_root + '/api/v1/users/' + USER_ID + '/awards', function (award_data) { solve_data
) {
$.get(script_root + "/api/v1/users/" + USER_ID + "/awards", function(
award_data
) {
var solves = solve_data.data; var solves = solve_data.data;
var awards = award_data.data; var awards = award_data.data;
@ -28,86 +32,95 @@ function scoregraph() {
{ {
x: times, x: times,
y: scores, y: scores,
type: 'scatter', type: "scatter",
marker: { marker: {
color: colorhash(USER_NAME + USER_ID), color: colorhash(USER_NAME + USER_ID)
}, },
line: { line: {
color: colorhash(USER_NAME + USER_ID), color: colorhash(USER_NAME + USER_ID)
}, },
fill: 'tozeroy' fill: "tozeroy"
} }
]; ];
var layout = { var layout = {
title: 'Score over Time', title: "Score over Time",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
hovermode: 'closest', hovermode: "closest",
xaxis: { xaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
yaxis: { yaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
legend: { legend: {
"orientation": "h" orientation: "h"
} }
}; };
$('#score-graph').empty(); $("#score-graph").empty();
document.getElementById('score-graph').fn = 'CTFd_score_user_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("score-graph").fn =
Plotly.newPlot('score-graph', data, layout); "CTFd_score_user_" +
USER_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("score-graph", data, layout);
}); });
}); });
} }
function keys_percentage_graph() { function keys_percentage_graph() {
// Solves and Fails pie chart // Solves and Fails pie chart
var base_url = script_root + '/api/v1/users/' + USER_ID; var base_url = script_root + "/api/v1/users/" + USER_ID;
$.get(base_url + '/fails', function (fails) { $.get(base_url + "/fails", function(fails) {
$.get(base_url + '/solves', function (solves) { $.get(base_url + "/solves", function(solves) {
var solves_count = solves.data.length; var solves_count = solves.data.length;
var fails_count = fails.data.length; var fails_count = fails.data.length;
var graph_data = [{ var graph_data = [
{
values: [solves_count, fails_count], values: [solves_count, fails_count],
labels: ['Solves', 'Fails'], labels: ["Solves", "Fails"],
marker: { marker: {
colors: [ colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
}, },
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Solve Percentages', title: "Solve Percentages",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
legend: { legend: {
"orientation": "h" orientation: "h"
} }
}; };
$('#keys-pie-graph').empty(); $("#keys-pie-graph").empty();
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_user_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("keys-pie-graph").fn =
Plotly.newPlot('keys-pie-graph', graph_data, layout); "CTFd_submissions_user_" +
USER_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("keys-pie-graph", graph_data, layout);
}); });
}); });
} }
function category_breakdown_graph() { function category_breakdown_graph() {
$.get(script_root + '/api/v1/users/' + USER_ID + '/solves', function (response) { $.get(script_root + "/api/v1/users/" + USER_ID + "/solves", function(
response
) {
var solves = response.data; var solves = response.data;
var categories = []; var categories = [];
for (var i = 0; i < solves.length; i++) { for (var i = 0; i < solves.length; i++) {
categories.push(solves[i].challenge.category) categories.push(solves[i].challenge.category);
} }
var keys = categories.filter(function(elem, pos) { var keys = categories.filter(function(elem, pos) {
@ -122,39 +135,44 @@ function category_breakdown_graph() {
count++; count++;
} }
} }
counts.push(count) counts.push(count);
} }
var data = [{ var data = [
{
values: counts, values: counts,
labels: keys, labels: keys,
hole: .4, hole: 0.4,
type: 'pie' type: "pie"
}]; }
];
var layout = { var layout = {
title: 'Category Breakdown', title: "Category Breakdown",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
legend: { legend: {
"orientation": "v" orientation: "v"
} }
}; };
$('#categories-pie-graph').empty(); $("#categories-pie-graph").empty();
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_team_' + USER_ID + '_' + (new Date).toISOString().slice(0, 19); document.getElementById("categories-pie-graph").fn =
Plotly.newPlot('categories-pie-graph', data, layout); "CTFd_categories_team_" +
USER_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("categories-pie-graph", data, layout);
}); });
} }
$(document).ready(function() { $(document).ready(function() {
category_breakdown_graph(); category_breakdown_graph();
keys_percentage_graph(); keys_percentage_graph();
scoregraph(); scoregraph();
window.onresize = function() { window.onresize = function() {
Plotly.Plots.resize(document.getElementById('keys-pie-graph')); Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
Plotly.Plots.resize(document.getElementById('categories-pie-graph')); Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
Plotly.Plots.resize(document.getElementById('score-graph')); Plotly.Plots.resize(document.getElementById("score-graph"));
}; };
}); });

View File

@ -1,36 +1,38 @@
$(document).ready(function() { $(document).ready(function() {
$('#user-info-form').submit(function(e){ $("#user-info-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#user-info-form').serializeJSON(true); var params = $("#user-info-form").serializeJSON(true);
CTFd.fetch('/api/v1/users/' + USER_ID, { CTFd.fetch("/api/v1/users/" + USER_ID, {
method: 'PATCH', method: "PATCH",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
window.location.reload(); window.location.reload();
} else { } else {
$('#user-info-form > #results').empty(); $("#user-info-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#user-info-form > #results').append( $("#user-info-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#user-info-form').find('input[name={0}]'.format(key)); var i = $("#user-info-form").find("input[name={0}]".format(key));
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}); });
}) });
}); });

View File

@ -1,40 +1,42 @@
$(document).ready(function() { $(document).ready(function() {
$('#user-info-form').submit(function (e) { $("#user-info-form").submit(function(e) {
e.preventDefault(); e.preventDefault();
var params = $('#user-info-form').serializeJSON(true); var params = $("#user-info-form").serializeJSON(true);
var url = '/api/v1/users'; var url = "/api/v1/users";
if (params.notify) { if (params.notify) {
url += '?notify=true' url += "?notify=true";
} }
delete params.notify; delete params.notify;
CTFd.fetch(url, { CTFd.fetch(url, {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function (response) { })
.then(function(response) {
return response.json(); return response.json();
}).then(function (response) { })
.then(function(response) {
if (response.success) { if (response.success) {
var user_id = response.data.id; var user_id = response.data.id;
window.location = script_root + '/admin/users/' + user_id; window.location = script_root + "/admin/users/" + user_id;
} else { } else {
$('#user-info-form > #results').empty(); $("#user-info-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) { Object.keys(response.errors).forEach(function(key, index) {
$('#user-info-form > #results').append( $("#user-info-form > #results").append(
ezbadge({ ezbadge({
type: 'error', type: "error",
body: response.errors[key] body: response.errors[key]
}) })
); );
var i = $('#user-info-form').find('input[name={0}]'.format(key)); var i = $("#user-info-form").find("input[name={0}]".format(key));
var input = $(i); var input = $(i);
input.addClass('input-filled-invalid'); input.addClass("input-filled-invalid");
input.removeClass('input-filled-valid'); input.removeClass("input-filled-valid");
}); });
} }
}); });

View File

@ -1,3 +1 @@
$(document).ready(function () { $(document).ready(function() {});
});

View File

@ -1,9 +1,12 @@
html, body, .container { html,
font-family: 'Lato', 'LatoOffline', sans-serif; body,
.container {
font-family: "Lato", "LatoOffline", sans-serif;
} }
h1, h2 { h1,
font-family: 'Raleway', 'RalewayOffline', sans-serif; h2 {
font-family: "Raleway", "RalewayOffline", sans-serif;
font-weight: 500; font-weight: 500;
letter-spacing: 2px; letter-spacing: 2px;
} }
@ -39,7 +42,7 @@ table > thead > tr > td {
.jumbotron { .jumbotron {
background-color: #343a40; background-color: #343a40;
color: #FFF; color: #fff;
border-radius: 0; border-radius: 0;
text-align: center; text-align: center;
} }
@ -108,12 +111,12 @@ table > thead > tr > td {
} }
.btn-info { .btn-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
border-color: #5B7290 !important; border-color: #5b7290 !important;
} }
.badge-info { .badge-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.alert { .alert {

View File

@ -17,11 +17,11 @@
} }
.btn-info { .btn-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.badge-info { .badge-info {
background-color: #5B7290 !important; background-color: #5b7290 !important;
} }
.challenge-button { .challenge-button {

View File

@ -1,11 +1,13 @@
html,
html, body, .container { body,
.container {
height: 100% !important; height: 100% !important;
font-family: 'Lato', 'LatoOffline', sans-serif; font-family: "Lato", "LatoOffline", sans-serif;
} }
h1, h2 { h1,
font-family: 'Raleway', 'RalewayOffline', sans-serif; h2 {
font-family: "Raleway", "RalewayOffline", sans-serif;
font-weight: 500; font-weight: 500;
letter-spacing: 2px; letter-spacing: 2px;
} }
@ -43,11 +45,11 @@ img {
font-size: 24px; font-size: 24px;
letter-spacing: -0.04rem; letter-spacing: -0.04rem;
line-height: 15px; line-height: 15px;
color: #FFF; color: #fff;
} }
.navbar-inverse .navbar-nav > li > a { .navbar-inverse .navbar-nav > li > a {
color: #FFF !important; color: #fff !important;
} }
.navbar li > a { .navbar li > a {
@ -179,7 +181,7 @@ table{
} }
.file-wrapper { .file-wrapper {
background-color: #5B7290; background-color: #5b7290;
} }
.file-wrapper:hover { .file-wrapper:hover {
@ -188,11 +190,10 @@ table{
.theme-background { .theme-background {
background-color: #545454 !important; background-color: #545454 !important;
} }
.solved-challenge { .solved-challenge {
background-color: #8EDC9D !important; background-color: #8edc9d !important;
} }
.panel-theme { .panel-theme {
@ -203,7 +204,7 @@ table{
border-color: #545454; border-color: #545454;
background-color: #545454; background-color: #545454;
opacity: 1; opacity: 1;
color: #FFF; color: #fff;
text-align: center; text-align: center;
} }
@ -217,7 +218,7 @@ table{
.btn-outlined.btn-theme:hover, .btn-outlined.btn-theme:hover,
.btn-outlined.btn-theme:active { .btn-outlined.btn-theme:active {
color: #FFF; color: #fff;
background: #545454; background: #545454;
border-color: #545454; border-color: #545454;
} }
@ -230,7 +231,7 @@ table{
.jumbotron { .jumbotron {
background-color: #545454; background-color: #545454;
color: #FFF; color: #fff;
padding: 0px 0px 25px; padding: 0px 0px 25px;
} }
@ -351,7 +352,7 @@ table{
} }
.label-content { .label-content {
color: #8B8C8B; color: #8b8c8b;
padding: 0.25em 0; padding: 0.25em 0;
-webkit-transition: -webkit-transform 0.3s; -webkit-transition: -webkit-transform 0.3s;
transition: transform 0.3s; transition: transform 0.3s;

View File

@ -1,10 +1,9 @@
var CTFd = (function() { var CTFd = (function() {
var options = { var options = {
urlRoot: '', urlRoot: "",
csrfNonce: '', csrfNonce: "",
start: null, start: null,
end: null, end: null
}; };
var challenges = {}; var challenges = {};
@ -20,19 +19,18 @@ var CTFd = (function () {
options = { options = {
method: "GET", method: "GET",
credentials: "same-origin", credentials: "same-origin",
headers: {}, headers: {}
}; };
} }
url = this.options.urlRoot + url; url = this.options.urlRoot + url;
if (options.headers === undefined) { if (options.headers === undefined) {
options.headers = {}; options.headers = {};
} }
options.credentials = 'same-origin'; options.credentials = "same-origin";
options.headers['Accept'] = 'application/json'; options.headers["Accept"] = "application/json";
options.headers['Content-Type'] = 'application/json'; options.headers["Content-Type"] = "application/json";
options.headers['CSRF-Token'] = this.options.csrfNonce; options.headers["CSRF-Token"] = this.options.csrfNonce;
return window.fetch(url, options); return window.fetch(url, options);
}; };

View File

@ -9,7 +9,7 @@ function loadchal(id) {
return e.id == id; return e.id == id;
})[0]; })[0];
if (obj.type === 'hidden') { if (obj.type === "hidden") {
ezal({ ezal({
title: "Challenge Hidden!", title: "Challenge Hidden!",
body: "You haven't unlocked this challenge yet!", body: "You haven't unlocked this challenge yet!",
@ -35,26 +35,28 @@ function updateChalWindow(obj) {
$.getScript(script_root + obj.script, function() { $.getScript(script_root + obj.script, function() {
$.get(script_root + obj.template, function(template_data) { $.get(script_root + obj.template, function(template_data) {
$('#challenge-window').empty(); $("#challenge-window").empty();
var template = nunjucks.compile(template_data); var template = nunjucks.compile(template_data);
window.challenge.data = challenge_data; window.challenge.data = challenge_data;
window.challenge.preRender(); window.challenge.preRender();
challenge_data['description'] = window.challenge.render(challenge_data['description']); challenge_data["description"] = window.challenge.render(
challenge_data['script_root'] = script_root; 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) { $(".challenge-solves").click(function(e) {
getsolves($('#challenge-id').val()) getsolves($("#challenge-id").val());
}); });
$('.nav-tabs a').click(function (e) { $(".nav-tabs a").click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show') $(this).tab("show");
}); });
// Handle modal toggling // Handle modal toggling
$('#challenge-window').on('hide.bs.modal', function (event) { $("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong"); $("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct"); $("#submission-input").removeClass("correct");
$("#incorrect-key").slideUp(); $("#incorrect-key").slideUp();
@ -63,10 +65,10 @@ function updateChalWindow(obj) {
$("#too-fast").slideUp(); $("#too-fast").slideUp();
}); });
$('#submit-key').click(function (e) { $("#submit-key").click(function(e) {
e.preventDefault(); e.preventDefault();
$('#submit-key').addClass("disabled-button"); $("#submit-key").addClass("disabled-button");
$('#submit-key').prop('disabled', true); $("#submit-key").prop("disabled", true);
window.challenge.submit(function(data) { window.challenge.submit(function(data) {
renderSubmissionResponse(data); renderSubmissionResponse(data);
loadchals(function() { loadchals(function() {
@ -83,22 +85,28 @@ function updateChalWindow(obj) {
$(".input-field").bind({ $(".input-field").bind({
focus: function() { focus: function() {
$(this).parent().addClass('input--filled'); $(this)
.parent()
.addClass("input--filled");
$label = $(this).siblings(".input-label"); $label = $(this).siblings(".input-label");
}, },
blur: function() { blur: function() {
if ($(this).val() === '') { if ($(this).val() === "") {
$(this).parent().removeClass('input--filled'); $(this)
.parent()
.removeClass("input--filled");
$label = $(this).siblings(".input-label"); $label = $(this).siblings(".input-label");
$label.removeClass('input--hide'); $label.removeClass("input--hide");
} }
} }
}); });
window.challenge.postRender(); window.challenge.postRender();
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name); window.location.replace(
$('#challenge-window').modal(); window.location.href.split("#")[0] + "#" + obj.name
);
$("#challenge-window").modal();
}); });
}); });
}); });
@ -110,22 +118,28 @@ $("#submission-input").keyup(function (event) {
} }
}); });
function renderSubmissionResponse(response, cb) { function renderSubmissionResponse(response, cb) {
var result = response.data; var result = response.data;
var result_message = $('#result-message'); var result_message = $("#result-message");
var result_notification = $('#result-notification'); var result_notification = $("#result-notification");
var answer_input = $("#submission-input"); var answer_input = $("#submission-input");
result_notification.removeClass(); result_notification.removeClass();
result_message.text(result.message); result_message.text(result.message);
if (result.status === "authentication_required") { if (result.status === "authentication_required") {
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash; window.location =
return script_root +
} "/login?next=" +
else if (result.status === "incorrect") { // Incorrect key script_root +
result_notification.addClass('alert alert-danger alert-dismissable text-center'); 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(); result_notification.slideDown();
answer_input.removeClass("correct"); answer_input.removeClass("correct");
@ -133,29 +147,45 @@ function renderSubmissionResponse(response, cb) {
setTimeout(function() { setTimeout(function() {
answer_input.removeClass("wrong"); answer_input.removeClass("wrong");
}, 3000); }, 3000);
} } else if (result.status === "correct") {
else if (result.status === "correct") { // Challenge Solved // Challenge Solved
result_notification.addClass('alert alert-success alert-dismissable text-center'); result_notification.addClass(
"alert alert-success alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
$('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves")); $(".challenge-solves").text(
parseInt(
$(".challenge-solves")
.text()
.split(" ")[0]
) +
1 +
" Solves"
);
answer_input.val(""); answer_input.val("");
answer_input.removeClass("wrong"); answer_input.removeClass("wrong");
answer_input.addClass("correct"); answer_input.addClass("correct");
} } else if (result.status === "already_solved") {
else if (result.status === "already_solved") { // Challenge already solved // Challenge already solved
result_notification.addClass('alert alert-info alert-dismissable text-center'); result_notification.addClass(
"alert alert-info alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
answer_input.addClass("correct"); answer_input.addClass("correct");
} } else if (result.status === "paused") {
else if (result.status === "paused") { // CTF is paused // CTF is paused
result_notification.addClass('alert alert-warning alert-dismissable text-center'); result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
} } else if (result.status === "ratelimited") {
else if (result.status === "ratelimited") { // Keys per minute too high // Keys per minute too high
result_notification.addClass('alert alert-warning alert-dismissable text-center'); result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown(); result_notification.slideDown();
answer_input.addClass("too-fast"); answer_input.addClass("too-fast");
@ -164,9 +194,9 @@ function renderSubmissionResponse(response, cb) {
}, 3000); }, 3000);
} }
setTimeout(function() { setTimeout(function() {
$('.alert').slideUp(); $(".alert").slideUp();
$('#submit-key').removeClass("disabled-button"); $("#submit-key").removeClass("disabled-button");
$('#submit-key').prop('disabled', false); $("#submit-key").prop("disabled", false);
}, 3000); }, 3000);
if (cb) { if (cb) {
@ -175,13 +205,15 @@ function renderSubmissionResponse(response, cb) {
} }
function marksolves(cb) { function marksolves(cb) {
$.get(script_root + '/api/v1/' + user_mode + '/me/solves', function (response) { $.get(script_root + "/api/v1/" + user_mode + "/me/solves", function(
response
) {
var solves = response.data; var solves = response.data;
for (var i = solves.length - 1; i >= 0; i--) { for (var i = solves.length - 1; i >= 0; i--) {
var id = solves[i].challenge_id; var id = solves[i].challenge_id;
var btn = $('button[value="' + id + '"]'); var btn = $('button[value="' + id + '"]');
btn.addClass('solved-challenge'); btn.addClass("solved-challenge");
btn.prepend("<i class='fas fa-check corner-button-check'></i>") btn.prepend("<i class='fas fa-check corner-button-check'></i>");
} }
if (cb) { if (cb) {
cb(); cb();
@ -191,13 +223,14 @@ function marksolves(cb) {
function load_user_solves(cb) { function load_user_solves(cb) {
if (authed) { if (authed) {
$.get(script_root + '/api/v1/' + user_mode + '/me/solves', function (response) { $.get(script_root + "/api/v1/" + user_mode + "/me/solves", function(
response
) {
var solves = response.data; var solves = response.data;
for (var i = solves.length - 1; i >= 0; i--) { for (var i = solves.length - 1; i >= 0; i--) {
var chal_id = solves[i].challenge_id; var chal_id = solves[i].challenge_id;
user_solves.push(chal_id); user_solves.push(chal_id);
} }
if (cb) { if (cb) {
cb(); cb();
@ -209,19 +242,28 @@ function load_user_solves(cb) {
} }
function getsolves(id) { function getsolves(id) {
$.get(script_root + '/api/v1/challenges/' + id + '/solves', function (response) { $.get(script_root + "/api/v1/challenges/" + id + "/solves", function(
response
) {
var data = response.data; var data = response.data;
$('.challenge-solves').text( $(".challenge-solves").text(parseInt(data.length) + " Solves");
(parseInt(data.length) + " Solves") var box = $("#challenge-solves-names");
);
var box = $('#challenge-solves-names');
box.empty(); box.empty();
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var id = data[i].account_id; var id = data[i].account_id;
var name = data[i].name; var name = data[i].name;
var date = moment(data[i].date).local().fromNow(); var date = moment(data[i].date)
var account_url = data[i].account_url .local()
box.append('<tr><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(account_url, id, htmlentities(name), date)); .fromNow();
var account_url = data[i].account_url;
box.append(
'<tr><td><a href="{0}">{2}</td><td>{3}</td></tr>'.format(
account_url,
id,
htmlentities(name),
date
)
);
} }
}); });
} }
@ -231,7 +273,7 @@ function loadchals(cb) {
var categories = []; var categories = [];
challenges = response.data; challenges = response.data;
$('#challenges-board').empty(); $("#challenges-board").empty();
for (var i = challenges.length - 1; i >= 0; i--) { for (var i = challenges.length - 1; i >= 0; i--) {
challenges[i].solves = 0; challenges[i].solves = 0;
@ -240,17 +282,21 @@ function loadchals(cb) {
categories.push(category); categories.push(category);
var categoryid = category.replace(/ /g, "-").hashCode(); var categoryid = category.replace(/ /g, "-").hashCode();
var categoryrow = $('' + var categoryrow = $(
"" +
'<div id="{0}-row" class="pt-5">'.format(categoryid) + '<div id="{0}-row" class="pt-5">'.format(categoryid) +
'<div class="category-header col-md-12 mb-3">' + '<div class="category-header col-md-12 mb-3">' +
'</div>' + "</div>" +
'<div class="category-challenges col-md-12">' + '<div class="category-challenges col-md-12">' +
'<div class="challenges-row col-md-12"></div>' + '<div class="challenges-row col-md-12"></div>' +
'</div>' + "</div>" +
'</div>'); "</div>"
categoryrow.find(".category-header").append($("<h3>" + category + "</h3>")); );
categoryrow
.find(".category-header")
.append($("<h3>" + category + "</h3>"));
$('#challenges-board').append(categoryrow); $("#challenges-board").append(categoryrow);
} }
} }
@ -259,18 +305,28 @@ function loadchals(cb) {
var challenge = chalinfo.category.replace(/ /g, "-").hashCode(); var challenge = chalinfo.category.replace(/ /g, "-").hashCode();
var chalid = chalinfo.name.replace(/ /g, "-").hashCode(); var chalid = chalinfo.name.replace(/ /g, "-").hashCode();
var catid = chalinfo.category.replace(/ /g, "-").hashCode(); var catid = chalinfo.category.replace(/ /g, "-").hashCode();
var chalwrap = $("<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid)); var chalwrap = $(
"<div id='{0}' class='col-md-3 d-inline-block'></div>".format(chalid)
);
if (user_solves.indexOf(chalinfo.id) == -1) { if (user_solves.indexOf(chalinfo.id) == -1) {
var chalbutton = $("<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(chalinfo.id)); var chalbutton = $(
"<button class='btn btn-dark challenge-button w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'></button>".format(
chalinfo.id
)
);
} else { } else {
var chalbutton = $("<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(chalinfo.id)); var chalbutton = $(
"<button class='btn btn-dark challenge-button solved-challenge w-100 text-truncate pt-3 pb-3 mb-2' value='{0}'><i class='fas fa-check corner-button-check'></i></button>".format(
chalinfo.id
)
);
} }
var chalheader = $("<p>{0}</p>".format(chalinfo.name)); var chalheader = $("<p>{0}</p>".format(chalinfo.name));
var chalscore = $("<span>{0}</span>".format(chalinfo.value)); var chalscore = $("<span>{0}</span>".format(chalinfo.value));
for (var j = 0; j < chalinfo.tags.length; j++) { for (var j = 0; j < chalinfo.tags.length; j++) {
var tag = 'tag-' + chalinfo.tags[j].value.replace(/ /g, '-'); var tag = "tag-" + chalinfo.tags[j].value.replace(/ /g, "-");
chalwrap.addClass(tag); chalwrap.addClass(tag);
} }
@ -278,10 +334,12 @@ function loadchals(cb) {
chalbutton.append(chalscore); chalbutton.append(chalscore);
chalwrap.append(chalbutton); chalwrap.append(chalbutton);
$("#" + catid + "-row").find(".category-challenges > .challenges-row").append(chalwrap); $("#" + catid + "-row")
.find(".category-challenges > .challenges-row")
.append(chalwrap);
} }
$('.challenge-button').click(function (e) { $(".challenge-button").click(function(e) {
loadchal(this.value); loadchal(this.value);
getsolves(this.value); getsolves(this.value);
}); });
@ -292,17 +350,19 @@ function loadchals(cb) {
}); });
} }
$("#submit-key").click(function(e) {
submitkey(
$('#submit-key').click(function (e) { $("#challenge-id").val(),
submitkey($('#challenge-id').val(), $('#submission-input').val(), $('#nonce').val()) $("#submission-input").val(),
$("#nonce").val()
);
}); });
$('.challenge-solves').click(function (e) { $(".challenge-solves").click(function(e) {
getsolves($('#challenge-id').val()) getsolves($("#challenge-id").val());
}); });
$('#challenge-window').on('hide.bs.modal', function (event) { $("#challenge-window").on("hide.bs.modal", function(event) {
$("#submission-input").removeClass("wrong"); $("#submission-input").removeClass("wrong");
$("#submission-input").removeClass("correct"); $("#submission-input").removeClass("correct");
$("#incorrect-key").slideUp(); $("#incorrect-key").slideUp();
@ -318,8 +378,10 @@ var load_location_hash = function () {
}; };
function update(cb) { function update(cb) {
load_user_solves(function () { // Load the user's solved challenge ids load_user_solves(function() {
loadchals(function () { // Load the full list of challenges // Load the user's solved challenge ids
loadchals(function() {
// Load the full list of challenges
if (cb) { if (cb) {
cb(); cb();
} }
@ -333,14 +395,14 @@ $(function () {
}); });
}); });
$('.nav-tabs a').click(function (e) { $(".nav-tabs a").click(function(e) {
e.preventDefault(); e.preventDefault();
$(this).tab('show') $(this).tab("show");
}); });
$('#challenge-window').on('hidden.bs.modal', function () { $("#challenge-window").on("hidden.bs.modal", function() {
$('.nav-tabs a:first').tab('show'); $(".nav-tabs a:first").tab("show");
history.replaceState('', document.title, window.location.pathname); history.replaceState("", document.title, window.location.pathname);
}); });
setInterval(update, 300000); setInterval(update, 300000);

View File

@ -3,18 +3,22 @@ var wc = new WindowController();
var sound = new Howl({ var sound = new Howl({
src: [ src: [
script_root + "/themes/core/static/sounds/notification.webm", script_root + "/themes/core/static/sounds/notification.webm",
script_root + "/themes/core/static/sounds/notification.mp3", script_root + "/themes/core/static/sounds/notification.mp3"
] ]
}); });
function connect() { function connect() {
window.ctfEventSource = new EventSource(script_root + "/events"); window.ctfEventSource = new EventSource(script_root + "/events");
window.ctfEventSource.addEventListener('notification', function (event) { window.ctfEventSource.addEventListener(
"notification",
function(event) {
var data = JSON.parse(event.data); var data = JSON.parse(event.data);
wc.broadcast('notification', data); wc.broadcast("notification", data);
render(data); render(data);
}, false); },
false
);
} }
function disconnect() { function disconnect() {

View File

@ -1,53 +1,56 @@
var modal = '<div class="modal fade" tabindex="-1" role="dialog">' + var modal =
'<div class="modal fade" tabindex="-1" role="dialog">' +
' <div class="modal-dialog" role="document">' + ' <div class="modal-dialog" role="document">' +
' <div class="modal-content">' + ' <div class="modal-content">' +
' <div class="modal-header">' + ' <div class="modal-header">' +
' <h5 class="modal-title">\{0\}</h5>' + ' <h5 class="modal-title">{0}</h5>' +
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' + ' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' + ' <span aria-hidden="true">&times;</span>' +
' </button>' + " </button>" +
' </div>' + " </div>" +
' <div class="modal-body">' + ' <div class="modal-body">' +
' <p>\{1\}</p>' + " <p>{1}</p>" +
' </div>' + " </div>" +
' <div class="modal-footer">' + ' <div class="modal-footer">' +
' </div>' + " </div>" +
' </div>' + " </div>" +
' </div>' + " </div>" +
'</div>';
var progress = '<div class="progress">' +
' <div class="progress-bar progress-bar-success progress-bar-striped progress-bar-animated" role="progressbar" style="width: \{0\}%">' +
' </div>' +
'</div>';
var error_template = "<div class=\"alert alert-danger alert-dismissable\" role=\"alert\">\n" +
" <span class=\"sr-only\">Error:</span>\n" +
" \{0\}\n" +
" <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\n" +
"</div>"; "</div>";
var progress =
'<div class="progress">' +
' <div class="progress-bar progress-bar-success progress-bar-striped progress-bar-animated" role="progressbar" style="width: {0}%">' +
" </div>" +
"</div>";
var success_template = "<div class=\"alert alert-success alert-dismissable submit-row\" role=\"alert\">\n" + var error_template =
'<div class="alert alert-danger alert-dismissable" role="alert">\n' +
' <span class="sr-only">Error:</span>\n' +
" {0}\n" +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>\n' +
"</div>";
var success_template =
'<div class="alert alert-success alert-dismissable submit-row" role="alert">\n' +
" <strong>Success!</strong>\n" + " <strong>Success!</strong>\n" +
" \{0\}\n" + " {0}\n" +
" <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\n" + ' <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>\n' +
"</div>"; "</div>";
function ezal(args) { function ezal(args) {
var res = modal.format(args.title, args.body); var res = modal.format(args.title, args.body);
var obj = $(res); var obj = $(res);
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(args.button); var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(
args.button
);
obj.find('.modal-footer').append(button); obj.find(".modal-footer").append(button);
$('main').append(obj); $("main").append(obj);
obj.modal('show'); obj.modal("show");
$(obj).on('hidden.bs.modal', function (e) { $(obj).on("hidden.bs.modal", function(e) {
$(this).modal('dispose'); $(this).modal("dispose");
}); });
return obj; return obj;
@ -56,23 +59,26 @@ function ezal(args){
function ezq(args) { function ezq(args) {
var res = modal.format(args.title, args.body); var res = modal.format(args.title, args.body);
var obj = $(res); var obj = $(res);
var deny = '<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>'; var deny =
var confirm = $('<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>'); '<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>';
var confirm = $(
'<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>'
);
obj.find('.modal-footer').append(deny); obj.find(".modal-footer").append(deny);
obj.find('.modal-footer').append(confirm); obj.find(".modal-footer").append(confirm);
$('main').append(obj); $("main").append(obj);
$(obj).on('hidden.bs.modal', function (e) { $(obj).on("hidden.bs.modal", function(e) {
$(this).modal('dispose'); $(this).modal("dispose");
}); });
$(confirm).click(function() { $(confirm).click(function() {
args.success(); args.success();
}); });
obj.modal('show'); obj.modal("show");
return obj; return obj;
} }
@ -80,16 +86,16 @@ function ezq(args){
function ezpg(args) { function ezpg(args) {
if (args.target) { if (args.target) {
var obj = $(args.target); var obj = $(args.target);
var pbar = obj.find('.progress-bar'); var pbar = obj.find(".progress-bar");
pbar.css('width', args.width + '%'); pbar.css("width", args.width + "%");
return obj; return obj;
} }
var bar = progress.format(args.width); var bar = progress.format(args.width);
var res = modal.format(args.title, bar); var res = modal.format(args.title, bar);
var obj = $(res); var obj = $(res);
$('main').append(obj); $("main").append(obj);
obj.modal('show'); obj.modal("show");
return obj; return obj;
} }
@ -98,9 +104,9 @@ function ezbadge(args) {
var type = args.type; var type = args.type;
var body = args.body; var body = args.body;
var tpl = undefined; var tpl = undefined;
if (type === 'success') { if (type === "success") {
tpl = success_template; tpl = success_template;
} else if (type === 'error') { } else if (type === "error") {
tpl = error_template; tpl = error_template;
} }

View File

@ -1,24 +1,23 @@
function hint(id) { function hint(id) {
return CTFd.fetch('/api/v1/hints/' + id, { return CTFd.fetch("/api/v1/hints/" + id, {
method: 'GET', method: "GET",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
}).then(function(response) { }).then(function(response) {
return response.json(); return response.json();
}); });
} }
function unlock(params) { function unlock(params) {
return CTFd.fetch('/api/v1/unlocks', { return CTFd.fetch("/api/v1/unlocks", {
method: 'POST', method: "POST",
credentials: 'same-origin', credentials: "same-origin",
headers: { headers: {
'Accept': 'application/json', Accept: "application/json",
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
body: JSON.stringify(params) body: JSON.stringify(params)
}).then(function(response) { }).then(function(response) {
@ -29,7 +28,7 @@ function unlock(params){
function loadhint(hintid) { function loadhint(hintid) {
var md = window.markdownit({ var md = window.markdownit({
html: true, html: true,
linkify: true, linkify: true
}); });
hint(hintid).then(function(response) { hint(hintid).then(function(response) {

View File

@ -1,5 +1,5 @@
(function($, window) { (function($, window) {
'use strict'; "use strict";
var MultiModal = function(element) { var MultiModal = function(element) {
this.$element = $(element); this.$element = $(element);
@ -10,55 +10,59 @@
MultiModal.prototype.show = function(target) { MultiModal.prototype.show = function(target) {
var that = this; var that = this;
var $target = $(target); var $target = $(target);
var modalCount = $('.modal:visible').length; var modalCount = $(".modal:visible").length;
$target.css('z-index', MultiModal.BASE_ZINDEX + (modalCount * 20) + 10); $target.css("z-index", MultiModal.BASE_ZINDEX + modalCount * 20 + 10);
window.setTimeout(function() { window.setTimeout(function() {
var modalCount = $('.modal:visible').length; var modalCount = $(".modal:visible").length;
if (modalCount > 0) if (modalCount > 0)
$('.modal-backdrop').not(':first').addClass('hidden'); $(".modal-backdrop")
.not(":first")
.addClass("hidden");
that.adjustBackdrop(modalCount); that.adjustBackdrop(modalCount);
}); });
}; };
MultiModal.prototype.hidden = function(target) { MultiModal.prototype.hidden = function(target) {
var modalCount = $('.modal:visible').length; var modalCount = $(".modal:visible").length;
var $target = $(target); var $target = $(target);
if (modalCount) { if (modalCount) {
this.adjustBackdrop(modalCount - 1); this.adjustBackdrop(modalCount - 1);
$('body').addClass('modal-open'); $("body").addClass("modal-open");
} }
}; };
MultiModal.prototype.adjustBackdrop = function(modalCount) { MultiModal.prototype.adjustBackdrop = function(modalCount) {
$('.modal-backdrop:first').css('z-index', MultiModal.BASE_ZINDEX + ((modalCount)* 20)); $(".modal-backdrop:first").css(
"z-index",
MultiModal.BASE_ZINDEX + modalCount * 20
);
}; };
function Plugin(method, target) { function Plugin(method, target) {
return this.each(function() { return this.each(function() {
var $this = $(this); var $this = $(this);
var data = $this.data('multi-modal-plugin'); var data = $this.data("multi-modal-plugin");
if (!data) if (!data)
$this.data('multi-modal-plugin', (data = new MultiModal(this))); $this.data("multi-modal-plugin", (data = new MultiModal(this)));
if(method) if (method) data[method](target);
data[method](target);
}); });
} }
$.fn.multiModal = Plugin; $.fn.multiModal = Plugin;
$.fn.multiModal.Constructor = MultiModal; $.fn.multiModal.Constructor = MultiModal;
$(document).on('show.bs.modal', function(e) { $(document).on("show.bs.modal", function(e) {
$(document).multiModal('show', e.target); $(document).multiModal("show", e.target);
}); });
$(document).on('hidden.bs.modal', function(e) { $(document).on("hidden.bs.modal", function(e) {
$(document).multiModal('hidden', e.target); $(document).multiModal("hidden", e.target);
}); });
}(jQuery, window)); })(jQuery, window);

View File

@ -1,13 +1,18 @@
function updatescores() { function updatescores() {
$.get(script_root + '/api/v1/scoreboard', function (response) { $.get(script_root + "/api/v1/scoreboard", function(response) {
var teams = response.data; var teams = response.data;
var table = $('#scoreboard tbody'); var table = $("#scoreboard tbody");
table.empty(); table.empty();
for (var i = 0; i < teams.length; i++) { for (var i = 0; i < teams.length; i++) {
var row = "<tr>\n" + var row =
"<th scope=\"row\" class=\"text-center\">{0}</th>".format(i + 1) + "<tr>\n" +
"<td><a href=\"{0}/team/{1}\">{2}</a></td>".format(script_root, teams['standings'][i].id, htmlentities(teams['standings'][i].team)) + '<th scope="row" class="text-center">{0}</th>'.format(i + 1) +
"<td>{0}</td>".format(teams['standings'][i].score) + '<td><a href="{0}/team/{1}">{2}</a></td>'.format(
script_root,
teams["standings"][i].id,
htmlentities(teams["standings"][i].team)
) +
"<td>{0}</td>".format(teams["standings"][i].score) +
"</tr>"; "</tr>";
table.append(row); table.append(row);
} }
@ -17,9 +22,11 @@ function updatescores () {
function cumulativesum(arr) { function cumulativesum(arr) {
var result = arr.concat(); var result = arr.concat();
for (var i = 0; i < arr.length; i++) { for (var i = 0; i < arr.length; i++) {
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; }); result[i] = arr.slice(0, i + 1).reduce(function(p, i) {
return p + i;
});
} }
return result return result;
} }
function UTCtoDate(utc) { function UTCtoDate(utc) {
@ -29,12 +36,12 @@ function UTCtoDate(utc){
} }
function scoregraph() { function scoregraph() {
$.get(script_root + '/api/v1/scoreboard/top/10', function( response ) { $.get(script_root + "/api/v1/scoreboard/top/10", function(response) {
var places = response.data; var places = response.data;
if (Object.keys(places).length === 0) { if (Object.keys(places).length === 0) {
// Replace spinner // Replace spinner
$('#score-graph').html( $("#score-graph").html(
'<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>' '<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>'
); );
return; return;
@ -45,56 +52,57 @@ function scoregraph () {
for (var i = 0; i < teams.length; i++) { for (var i = 0; i < teams.length; i++) {
var team_score = []; var team_score = [];
var times = []; var times = [];
for(var j = 0; j < places[teams[i]]['solves'].length; j++){ for (var j = 0; j < places[teams[i]]["solves"].length; j++) {
team_score.push(places[teams[i]]['solves'][j].value); team_score.push(places[teams[i]]["solves"][j].value);
var date = moment(places[teams[i]]['solves'][j].date); var date = moment(places[teams[i]]["solves"][j].date);
times.push(date.toDate()); times.push(date.toDate());
} }
team_score = cumulativesum(team_score); team_score = cumulativesum(team_score);
var trace = { var trace = {
x: times, x: times,
y: team_score, y: team_score,
mode: 'lines+markers', mode: "lines+markers",
name: places[teams[i]]['name'], name: places[teams[i]]["name"],
marker: { marker: {
color: colorhash(places[teams[i]]['name'] + places[teams[i]]['id']), color: colorhash(places[teams[i]]["name"] + places[teams[i]]["id"])
}, },
line: { line: {
color: colorhash(places[teams[i]]['name'] + places[teams[i]]['id']), color: colorhash(places[teams[i]]["name"] + places[teams[i]]["id"])
} }
}; };
traces.push(trace); traces.push(trace);
} }
traces.sort(function(a, b) { traces.sort(function(a, b) {
var scorediff = b['y'][b['y'].length - 1] - a['y'][a['y'].length - 1]; var scorediff = b["y"][b["y"].length - 1] - a["y"][a["y"].length - 1];
if (!scorediff) { if (!scorediff) {
return a['x'][a['x'].length - 1] - b['x'][b['x'].length - 1]; return a["x"][a["x"].length - 1] - b["x"][b["x"].length - 1];
} }
return scorediff; return scorediff;
}); });
var layout = { var layout = {
title: 'Top 10 Teams', title: "Top 10 Teams",
paper_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: "rgba(0,0,0,0)",
hovermode: 'closest', hovermode: "closest",
xaxis: { xaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
yaxis: { yaxis: {
showgrid: false, showgrid: false,
showspikes: true, showspikes: true
}, },
legend: { legend: {
"orientation": "h" orientation: "h"
} }
}; };
$('#score-graph').empty(); // Remove spinners $("#score-graph").empty(); // Remove spinners
document.getElementById('score-graph').fn = 'CTFd_scoreboard_' + (new Date).toISOString().slice(0,19); document.getElementById("score-graph").fn =
Plotly.newPlot('score-graph', traces, layout, { "CTFd_scoreboard_" + new Date().toISOString().slice(0, 19);
Plotly.newPlot("score-graph", traces, layout, {
// displayModeBar: false, // displayModeBar: false,
displaylogo: false displaylogo: false
}); });
@ -110,5 +118,5 @@ setInterval(update, 300000); // Update scores every 5 minutes
scoregraph(); scoregraph();
window.onresize = function() { window.onresize = function() {
Plotly.Plots.resize(document.getElementById('score-graph')); Plotly.Plots.resize(document.getElementById("score-graph"));
}; };

Some files were not shown because too many files have changed in this diff Show More