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
*.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
cache: pip
dist: xenial
cache:
- pip
- yarn
services:
- mysql
- postgresql
@ -7,26 +10,25 @@ services:
addons:
apt:
sources:
- mysql-5.7-trusty
- deadsnakes
packages:
- mysql-server
- mysql-client
- python3.6
- python3-pip
env:
- TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd'
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
- TESTING_DATABASE_URL='sqlite://'
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
python:
- 2.7
- 3.6
before_install:
- sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
- sudo mysql_upgrade -u root -ppassword
- sudo service mysql restart
- sudo rm -f /etc/boto.cfg
- export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
- export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- python3.6 -m pip install black>=19.3b0
install:
- pip install -r development.txt
- yarn global add prettier@1.17.0
before_script:
- psql -c 'create database ctfd;' -U postgres
script:

View File

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

View File

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

View File

@ -8,44 +8,48 @@ import os
import six
@admin.route('/admin/challenges')
@admin.route("/admin/challenges")
@admins_only
def challenges_listing():
challenges = Challenges.query.all()
return render_template('admin/challenges/challenges.html', challenges=challenges)
return render_template("admin/challenges/challenges.html", challenges=challenges)
@admin.route('/admin/challenges/<int:challenge_id>')
@admin.route("/admin/challenges/<int:challenge_id>")
@admins_only
def challenges_detail(challenge_id):
challenges = dict(Challenges.query.with_entities(Challenges.id, Challenges.name).all())
challenges = dict(
Challenges.query.with_entities(Challenges.id, Challenges.name).all()
)
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
solves = Solves.query.filter_by(challenge_id=challenge.id).all()
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
challenge_class = get_chal_class(challenge.type)
with open(os.path.join(app.root_path, challenge_class.templates['update'].lstrip('/')), 'rb') as update:
with open(
os.path.join(app.root_path, challenge_class.templates["update"].lstrip("/")),
"rb",
) as update:
tpl = update.read()
if six.PY3 and isinstance(tpl, binary_type):
tpl = tpl.decode('utf-8')
update_j2 = render_template_string(
tpl,
challenge=challenge
)
tpl = tpl.decode("utf-8")
update_j2 = render_template_string(tpl, challenge=challenge)
update_script = url_for('views.static_html', route=challenge_class.scripts['update'].lstrip('/'))
update_script = url_for(
"views.static_html", route=challenge_class.scripts["update"].lstrip("/")
)
return render_template(
'admin/challenges/challenge.html',
"admin/challenges/challenge.html",
update_template=update_j2,
update_script=update_script,
challenge=challenge,
challenges=challenges,
solves=solves,
flags=flags
flags=flags,
)
@admin.route('/admin/challenges/new')
@admin.route("/admin/challenges/new")
@admins_only
def challenges_new():
return render_template('admin/challenges/new.html')
return render_template("admin/challenges/new.html")

View File

@ -4,8 +4,8 @@ from CTFd.models import Notifications
from CTFd.admin import admin
@admin.route('/admin/notifications')
@admin.route("/admin/notifications")
@admins_only
def notifications():
notifs = Notifications.query.order_by(Notifications.id.desc()).all()
return render_template('admin/notifications.html', notifications=notifs)
return render_template("admin/notifications.html", notifications=notifs)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
from flask_restplus import Namespace
statistics_namespace = Namespace('statistics', description="Endpoint to retrieve Statistics")
statistics_namespace = Namespace(
"statistics", description="Endpoint to retrieve Statistics"
)
from CTFd.api.v1.statistics import challenges # noqa: F401
from CTFd.api.v1.statistics import teams # noqa: F401

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,88 +1,77 @@
from flask import session, request, abort
from flask_restplus import Namespace, Resource
from CTFd.models import db, Users, Solves, Awards, Tracking, Unlocks, Submissions, Notifications
from CTFd.utils.decorators import (
authed_only,
admins_only,
authed,
ratelimit
from CTFd.models import (
db,
Users,
Solves,
Awards,
Tracking,
Unlocks,
Submissions,
Notifications,
)
from CTFd.utils.decorators import authed_only, admins_only, authed, ratelimit
from CTFd.cache import clear_standings
from CTFd.utils.config import get_mail_provider
from CTFd.utils.email import sendmail, user_created_notification
from CTFd.utils.user import get_current_user, is_admin
from CTFd.utils.decorators.visibility import check_account_visibility
from CTFd.utils.config.visibility import (
accounts_visible,
scores_visible
)
from CTFd.utils.config.visibility import accounts_visible, scores_visible
from CTFd.schemas.submissions import SubmissionSchema
from CTFd.schemas.awards import AwardSchema
from CTFd.schemas.users import UserSchema
users_namespace = Namespace('users', description="Endpoint to retrieve Users")
users_namespace = Namespace("users", description="Endpoint to retrieve Users")
@users_namespace.route('')
@users_namespace.route("")
class UserList(Resource):
@check_account_visibility
def get(self):
users = Users.query.filter_by(banned=False, hidden=False)
response = UserSchema(view='user', many=True).dump(users)
response = UserSchema(view="user", many=True).dump(users)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
return {
'success': True,
'data': response.data
return {"success": True, "data": response.data}
@users_namespace.doc(
params={
"notify": "Whether to send the created user an email with their credentials"
}
@users_namespace.doc(params={'notify': 'Whether to send the created user an email with their credentials'})
)
@admins_only
def post(self):
req = request.get_json()
schema = UserSchema('admin')
schema = UserSchema("admin")
response = schema.load(req)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
db.session.add(response.data)
db.session.commit()
if request.args.get('notify'):
if request.args.get("notify"):
name = response.data.name
email = response.data.email
password = req.get('password')
password = req.get("password")
user_created_notification(
addr=email,
name=name,
password=password
)
user_created_notification(addr=email, name=name, password=password)
clear_standings()
response = schema.dump(response.data)
return {
'success': True,
'data': response.data
}
return {"success": True, "data": response.data}
@users_namespace.route('/<int:user_id>')
@users_namespace.param('user_id', "User ID")
@users_namespace.route("/<int:user_id>")
@users_namespace.param("user_id", "User ID")
class UserPublic(Resource):
@check_account_visibility
def get(self, user_id):
@ -91,36 +80,25 @@ class UserPublic(Resource):
if (user.banned or user.hidden) and is_admin() is False:
abort(404)
response = UserSchema(
view=session.get('type', 'user')
).dump(user)
response = UserSchema(view=session.get("type", "user")).dump(user)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
response.data['place'] = user.place
response.data['score'] = user.score
response.data["place"] = user.place
response.data["score"] = user.score
return {
'success': True,
'data': response.data
}
return {"success": True, "data": response.data}
@admins_only
def patch(self, user_id):
user = Users.query.filter_by(id=user_id).first_or_404()
data = request.get_json()
data['id'] = user_id
schema = UserSchema(view='admin', instance=user, partial=True)
data["id"] = user_id
schema = UserSchema(view="admin", instance=user, partial=True)
response = schema.load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
db.session.commit()
@ -130,10 +108,7 @@ class UserPublic(Resource):
clear_standings()
return {
'success': True,
'data': response
}
return {"success": True, "data": response}
@admins_only
def delete(self, user_id):
@ -149,35 +124,27 @@ class UserPublic(Resource):
clear_standings()
return {
'success': True
}
return {"success": True}
@users_namespace.route('/me')
@users_namespace.route("/me")
class UserPrivate(Resource):
@authed_only
def get(self):
user = get_current_user()
response = UserSchema('self').dump(user).data
response['place'] = user.place
response['score'] = user.score
return {
'success': True,
'data': response
}
response = UserSchema("self").dump(user).data
response["place"] = user.place
response["score"] = user.score
return {"success": True, "data": response}
@authed_only
def patch(self):
user = get_current_user()
data = request.get_json()
schema = UserSchema(view='self', instance=user, partial=True)
schema = UserSchema(view="self", instance=user, partial=True)
response = schema.load(data)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
db.session.commit()
@ -186,17 +153,14 @@ class UserPrivate(Resource):
clear_standings()
return {
'success': True,
'data': response.data
}
return {"success": True, "data": response.data}
@users_namespace.route('/<user_id>/solves')
@users_namespace.param('user_id', "User ID or 'me'")
@users_namespace.route("/<user_id>/solves")
@users_namespace.param("user_id", "User ID or 'me'")
class UserSolves(Resource):
def get(self, user_id):
if user_id == 'me':
if user_id == "me":
if not authed():
abort(403)
user = get_current_user()
@ -210,26 +174,20 @@ class UserSolves(Resource):
abort(404)
solves = user.get_solves(admin=is_admin())
view = 'user' if not is_admin() else 'admin'
view = "user" if not is_admin() else "admin"
response = SubmissionSchema(view=view, many=True).dump(solves)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
return {
'success': True,
'data': response.data
}
return {"success": True, "data": response.data}
@users_namespace.route('/<user_id>/fails')
@users_namespace.param('user_id', "User ID or 'me'")
@users_namespace.route("/<user_id>/fails")
@users_namespace.param("user_id", "User ID or 'me'")
class UserFails(Resource):
def get(self, user_id):
if user_id == 'me':
if user_id == "me":
if not authed():
abort(403)
user = get_current_user()
@ -243,13 +201,10 @@ class UserFails(Resource):
abort(404)
fails = user.get_fails(admin=is_admin())
view = 'user' if not is_admin() else 'admin'
view = "user" if not is_admin() else "admin"
response = SubmissionSchema(view=view, many=True).dump(fails)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
if is_admin():
data = response.data
@ -257,20 +212,14 @@ class UserFails(Resource):
data = []
count = len(response.data)
return {
'success': True,
'data': data,
'meta': {
'count': count
}
}
return {"success": True, "data": data, "meta": {"count": count}}
@users_namespace.route('/<user_id>/awards')
@users_namespace.param('user_id', "User ID or 'me'")
@users_namespace.route("/<user_id>/awards")
@users_namespace.param("user_id", "User ID or 'me'")
class UserAwards(Resource):
def get(self, user_id):
if user_id == 'me':
if user_id == "me":
if not authed():
abort(403)
user = get_current_user()
@ -284,57 +233,37 @@ class UserAwards(Resource):
abort(404)
awards = user.get_awards(admin=is_admin())
view = 'user' if not is_admin() else 'admin'
view = "user" if not is_admin() else "admin"
response = AwardSchema(view=view, many=True).dump(awards)
if response.errors:
return {
'success': False,
'errors': response.errors
}, 400
return {"success": False, "errors": response.errors}, 400
return {
'success': True,
'data': response.data
}
return {"success": True, "data": response.data}
@users_namespace.route('/<int:user_id>/email')
@users_namespace.param('user_id', "User ID")
@users_namespace.route("/<int:user_id>/email")
@users_namespace.param("user_id", "User ID")
class UserEmails(Resource):
@admins_only
@ratelimit(method="POST", limit=10, interval=60)
def post(self, user_id):
req = request.get_json()
text = req.get('text', '').strip()
text = req.get("text", "").strip()
user = Users.query.filter_by(id=user_id).first_or_404()
if get_mail_provider() is None:
return {
'success': False,
'errors': {
"": [
"Email settings not configured"
]
}
}, 400
if not text:
return {
'success': False,
'errors': {
"text": [
"Email text cannot be empty"
]
}
}, 400
result, response = sendmail(
addr=user.email,
text=text
return (
{"success": False, "errors": {"": ["Email settings not configured"]}},
400,
)
return {
'success': result,
'data': {}
}
if not text:
return (
{"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 requests
auth = Blueprint('auth', __name__)
auth = Blueprint("auth", __name__)
@auth.route('/confirm', methods=['POST', 'GET'])
@auth.route('/confirm/<data>', methods=['GET'])
@auth.route("/confirm", methods=["POST", "GET"])
@auth.route("/confirm/<data>", methods=["GET"])
@ratelimit(method="POST", limit=10, interval=60)
def confirm(data=None):
if not get_config('verify_emails'):
if not get_config("verify_emails"):
# If the CTF doesn't care about confirming email addresses then redierct to challenges
return redirect(url_for('challenges.listing'))
return redirect(url_for("challenges.listing"))
# User is confirming email account
if data and request.method == "GET":
try:
user_email = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired):
return render_template('confirm.html', errors=['Your confirmation link has expired'])
return render_template(
"confirm.html", errors=["Your confirmation link has expired"]
)
except (BadSignature, TypeError, base64.binascii.Error):
return render_template('confirm.html', errors=['Your confirmation token is invalid'])
return render_template(
"confirm.html", errors=["Your confirmation token is invalid"]
)
user = Users.query.filter_by(email=user_email).first_or_404()
user.verified = True
log('registrations', format="[{date}] {ip} - successful confirmation for {name}", name=user.name)
log(
"registrations",
format="[{date}] {ip} - successful confirmation for {name}",
name=user.name,
)
db.session.commit()
db.session.close()
if current_user.authed():
return redirect(url_for('challenges.listing'))
return redirect(url_for('auth.login'))
return redirect(url_for("challenges.listing"))
return redirect(url_for("auth.login"))
# User is trying to start or restart the confirmation flow
if not current_user.authed():
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
user = Users.query.filter_by(id=session['id']).first_or_404()
user = Users.query.filter_by(id=session["id"]).first_or_404()
if user.verified:
return redirect(url_for('views.settings'))
return redirect(url_for("views.settings"))
if data is None:
if request.method == "POST":
# User wants to resend their confirmation email
email.verify_email_address(user.email)
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
return render_template('confirm.html', user=user, infos=['Your confirmation email has been resent!'])
log(
"registrations",
format="[{date}] {ip} - {name} initiated a confirmation email resend",
)
return render_template(
"confirm.html",
user=user,
infos=["Your confirmation email has been resent!"],
)
elif request.method == "GET":
# User has been directed to the confirm page
return render_template('confirm.html', user=user)
return render_template("confirm.html", user=user)
@auth.route('/reset_password', methods=['POST', 'GET'])
@auth.route('/reset_password/<data>', methods=['POST', 'GET'])
@auth.route("/reset_password", methods=["POST", "GET"])
@auth.route("/reset_password/<data>", methods=["POST", "GET"])
@ratelimit(method="POST", limit=10, interval=60)
def reset_password(data=None):
if data is not None:
try:
name = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired):
return render_template('reset_password.html', errors=['Your link has expired'])
return render_template(
"reset_password.html", errors=["Your link has expired"]
)
except (BadSignature, TypeError, base64.binascii.Error):
return render_template('reset_password.html', errors=['Your reset token is invalid'])
return render_template(
"reset_password.html", errors=["Your reset token is invalid"]
)
if request.method == "GET":
return render_template('reset_password.html', mode='set')
return render_template("reset_password.html", mode="set")
if request.method == "POST":
user = Users.query.filter_by(name=name).first_or_404()
user.password = request.form['password'].strip()
user.password = request.form["password"].strip()
db.session.commit()
log('logins', format="[{date}] {ip} - successful password reset for {name}", name=name)
log(
"logins",
format="[{date}] {ip} - successful password reset for {name}",
name=name,
)
db.session.close()
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
if request.method == 'POST':
email_address = request.form['email'].strip()
if request.method == "POST":
email_address = request.form["email"].strip()
team = Users.query.filter_by(email=email_address).first()
get_errors()
if config.can_send_mail() is False:
return render_template(
'reset_password.html',
errors=['Email could not be sent due to server misconfiguration']
"reset_password.html",
errors=["Email could not be sent due to server misconfiguration"],
)
if not team:
return render_template(
'reset_password.html',
errors=['If that account exists you will receive an email, please check your inbox']
"reset_password.html",
errors=[
"If that account exists you will receive an email, please check your inbox"
],
)
email.forgot_password(email_address, team.name)
return render_template(
'reset_password.html',
errors=['If that account exists you will receive an email, please check your inbox']
"reset_password.html",
errors=[
"If that account exists you will receive an email, please check your inbox"
],
)
return render_template('reset_password.html')
return render_template("reset_password.html")
@auth.route('/register', methods=['POST', 'GET'])
@auth.route("/register", methods=["POST", "GET"])
@check_registration_visibility
@ratelimit(method="POST", limit=10, interval=5)
def register():
errors = get_errors()
if request.method == 'POST':
name = request.form['name']
email_address = request.form['email']
password = request.form['password']
if request.method == "POST":
name = request.form["name"]
email_address = request.form["email"]
password = request.form["password"]
name_len = len(name) == 0
names = Users.query.add_columns('name', 'id').filter_by(name=name).first()
emails = Users.query.add_columns('email', 'id').filter_by(email=email_address).first()
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
Users.query.add_columns("email", "id")
.filter_by(email=email_address)
.first()
)
pass_short = len(password) == 0
pass_long = len(password) > 128
valid_email = validators.validate_email(request.form['email'])
valid_email = validators.validate_email(request.form["email"])
team_name_email_check = validators.validate_email(name)
if not valid_email:
@ -148,36 +179,36 @@ def register():
if email.check_email_is_whitelisted(email_address) is False:
errors.append(
"Only email addresses under {domains} may register".format(
domains=get_config('domain_whitelist')
domains=get_config("domain_whitelist")
)
)
if names:
errors.append('That user name is already taken')
errors.append("That user name is already taken")
if team_name_email_check is True:
errors.append('Your user name cannot be an email address')
errors.append("Your user name cannot be an email address")
if emails:
errors.append('That email has already been used')
errors.append("That email has already been used")
if pass_short:
errors.append('Pick a longer password')
errors.append("Pick a longer password")
if pass_long:
errors.append('Pick a shorter password')
errors.append("Pick a shorter password")
if name_len:
errors.append('Pick a longer user name')
errors.append("Pick a longer user name")
if len(errors) > 0:
return render_template(
'register.html',
"register.html",
errors=errors,
name=request.form['name'],
email=request.form['email'],
password=request.form['password']
name=request.form["name"],
email=request.form["email"],
password=request.form["password"],
)
else:
with app.app_context():
user = Users(
name=name.strip(),
email=email_address.lower(),
password=password.strip()
password=password.strip(),
)
db.session.add(user)
db.session.commit()
@ -185,31 +216,40 @@ def register():
login_user(user)
if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email.
log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}")
if config.can_send_mail() and get_config(
"verify_emails"
): # Confirming users is enabled and we can send email.
log(
"registrations",
format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}",
)
email.verify_email_address(user.email)
db.session.close()
return redirect(url_for('auth.confirm'))
return redirect(url_for("auth.confirm"))
else: # Don't care about confirming users
if config.can_send_mail(): # We want to notify the user that they have registered.
if (
config.can_send_mail()
): # We want to notify the user that they have registered.
email.sendmail(
request.form['email'],
"You've successfully registered for {}".format(get_config('ctf_name'))
request.form["email"],
"You've successfully registered for {}".format(
get_config("ctf_name")
),
)
log('registrations', "[{date}] {ip} - {name} registered with {email}")
log("registrations", "[{date}] {ip} - {name} registered with {email}")
db.session.close()
return redirect(url_for('challenges.listing'))
return redirect(url_for("challenges.listing"))
else:
return render_template('register.html', errors=errors)
return render_template("register.html", errors=errors)
@auth.route('/login', methods=['POST', 'GET'])
@auth.route("/login", methods=["POST", "GET"])
@ratelimit(method="POST", limit=10, interval=5)
def login():
errors = get_errors()
if request.method == 'POST':
name = request.form['name']
if request.method == "POST":
name = request.form["name"]
# Check if the user submitted an email address or a team name
if validators.validate_email(name) is True:
@ -218,107 +258,112 @@ def login():
user = Users.query.filter_by(name=name).first()
if user:
if user and verify_password(request.form['password'], user.password):
if user and verify_password(request.form["password"], user.password):
session.regenerate()
login_user(user)
log('logins', "[{date}] {ip} - {name} logged in")
log("logins", "[{date}] {ip} - {name} logged in")
db.session.close()
if request.args.get('next') and validators.is_safe_url(request.args.get('next')):
return redirect(request.args.get('next'))
return redirect(url_for('challenges.listing'))
if request.args.get("next") and validators.is_safe_url(
request.args.get("next")
):
return redirect(request.args.get("next"))
return redirect(url_for("challenges.listing"))
else:
# This user exists but the password is wrong
log('logins', "[{date}] {ip} - submitted invalid password for {name}")
log("logins", "[{date}] {ip} - submitted invalid password for {name}")
errors.append("Your username or password is incorrect")
db.session.close()
return render_template('login.html', errors=errors)
return render_template("login.html", errors=errors)
else:
# This user just doesn't exist
log('logins', "[{date}] {ip} - submitted invalid account information")
log("logins", "[{date}] {ip} - submitted invalid account information")
errors.append("Your username or password is incorrect")
db.session.close()
return render_template('login.html', errors=errors)
return render_template("login.html", errors=errors)
else:
db.session.close()
return render_template('login.html', errors=errors)
return render_template("login.html", errors=errors)
@auth.route('/oauth')
@auth.route("/oauth")
def oauth_login():
endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \
or get_config('oauth_authorization_endpoint') \
or 'https://auth.majorleaguecyber.org/oauth/authorize'
endpoint = (
get_app_config("OAUTH_AUTHORIZATION_ENDPOINT")
or get_config("oauth_authorization_endpoint")
or "https://auth.majorleaguecyber.org/oauth/authorize"
)
if get_config('user_mode') == 'teams':
scope = 'profile team'
if get_config("user_mode") == "teams":
scope = "profile team"
else:
scope = 'profile'
scope = "profile"
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id")
if client_id is None:
error_for(
endpoint='auth.login',
message='OAuth Settings not configured. '
'Ask your CTF administrator to configure MajorLeagueCyber integration.'
endpoint="auth.login",
message="OAuth Settings not configured. "
"Ask your CTF administrator to configure MajorLeagueCyber integration.",
)
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
endpoint=endpoint,
client_id=client_id,
scope=scope,
state=session['nonce']
endpoint=endpoint, client_id=client_id, scope=scope, state=session["nonce"]
)
return redirect(redirect_url)
@auth.route('/redirect', methods=['GET'])
@auth.route("/redirect", methods=["GET"])
@ratelimit(method="GET", limit=10, interval=60)
def oauth_redirect():
oauth_code = request.args.get('code')
state = request.args.get('state')
if session['nonce'] != state:
log('logins', "[{date}] {ip} - OAuth State validation mismatch")
error_for(endpoint='auth.login', message='OAuth State validation mismatch.')
return redirect(url_for('auth.login'))
oauth_code = request.args.get("code")
state = request.args.get("state")
if session["nonce"] != state:
log("logins", "[{date}] {ip} - OAuth State validation mismatch")
error_for(endpoint="auth.login", message="OAuth State validation mismatch.")
return redirect(url_for("auth.login"))
if oauth_code:
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \
or get_config('oauth_token_endpoint') \
or 'https://auth.majorleaguecyber.org/oauth/token'
url = (
get_app_config("OAUTH_TOKEN_ENDPOINT")
or get_config("oauth_token_endpoint")
or "https://auth.majorleaguecyber.org/oauth/token"
)
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
client_secret = get_app_config('OAUTH_CLIENT_SECRET') or get_config('oauth_client_secret')
headers = {
'content-type': 'application/x-www-form-urlencoded'
}
client_id = get_app_config("OAUTH_CLIENT_ID") or get_config("oauth_client_id")
client_secret = get_app_config("OAUTH_CLIENT_SECRET") or get_config(
"oauth_client_secret"
)
headers = {"content-type": "application/x-www-form-urlencoded"}
data = {
'code': oauth_code,
'client_id': client_id,
'client_secret': client_secret,
'grant_type': 'authorization_code'
"code": oauth_code,
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "authorization_code",
}
token_request = requests.post(url, data=data, headers=headers)
if token_request.status_code == requests.codes.ok:
token = token_request.json()['access_token']
user_url = get_app_config('OAUTH_API_ENDPOINT') \
or get_config('oauth_api_endpoint') \
or 'https://api.majorleaguecyber.org/user'
token = token_request.json()["access_token"]
user_url = (
get_app_config("OAUTH_API_ENDPOINT")
or get_config("oauth_api_endpoint")
or "https://api.majorleaguecyber.org/user"
)
headers = {
'Authorization': 'Bearer ' + str(token),
'Content-type': 'application/json'
"Authorization": "Bearer " + str(token),
"Content-type": "application/json",
}
api_data = requests.get(url=user_url, headers=headers).json()
user_id = api_data['id']
user_name = api_data['name']
user_email = api_data['email']
user_id = api_data["id"]
user_name = api_data["name"]
user_email = api_data["email"]
user = Users.query.filter_by(email=user_email).first()
if user is None:
@ -328,29 +373,25 @@ def oauth_redirect():
name=user_name,
email=user_email,
oauth_id=user_id,
verified=True
verified=True,
)
db.session.add(user)
db.session.commit()
else:
log('logins', "[{date}] {ip} - Public registration via MLC blocked")
log("logins", "[{date}] {ip} - Public registration via MLC blocked")
error_for(
endpoint='auth.login',
message='Public registration is disabled. Please try again later.'
endpoint="auth.login",
message="Public registration is disabled. Please try again later.",
)
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
if get_config('user_mode') == TEAMS_MODE:
team_id = api_data['team']['id']
team_name = api_data['team']['name']
if get_config("user_mode") == TEAMS_MODE:
team_id = api_data["team"]["id"]
team_name = api_data["team"]["name"]
team = Teams.query.filter_by(oauth_id=team_id).first()
if team is None:
team = Teams(
name=team_name,
oauth_id=team_id,
captain_id=user.id
)
team = Teams(name=team_name, oauth_id=team_id, captain_id=user.id)
db.session.add(team)
db.session.commit()
@ -364,25 +405,21 @@ def oauth_redirect():
login_user(user)
return redirect(url_for('challenges.listing'))
return redirect(url_for("challenges.listing"))
else:
log('logins', "[{date}] {ip} - OAuth token retrieval failure")
error_for(
endpoint='auth.login',
message='OAuth token retrieval failure.'
)
return redirect(url_for('auth.login'))
log("logins", "[{date}] {ip} - OAuth token retrieval failure")
error_for(endpoint="auth.login", message="OAuth token retrieval failure.")
return redirect(url_for("auth.login"))
else:
log('logins', "[{date}] {ip} - Received redirect without OAuth code")
log("logins", "[{date}] {ip} - Received redirect without OAuth code")
error_for(
endpoint='auth.login',
message='Received redirect without OAuth code.'
endpoint="auth.login", message="Received redirect without OAuth code."
)
return redirect(url_for('auth.login'))
return redirect(url_for("auth.login"))
@auth.route('/logout')
@auth.route("/logout")
def logout():
if current_user.authed():
logout_user()
return redirect(url_for('views.static_html'))
return redirect(url_for("views.static_html"))

View File

@ -4,7 +4,7 @@ from flask_caching import Cache
cache = Cache()
def make_cache_key(path=None, key_prefix='view/%s'):
def make_cache_key(path=None, key_prefix="view/%s"):
"""
This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses.
Over time this function may be replaced with a cleaner custom cache implementation.
@ -20,6 +20,7 @@ def make_cache_key(path=None, key_prefix='view/%s'):
def clear_config():
from CTFd.utils import _get_config, get_app_config
cache.delete_memoized(_get_config)
cache.delete_memoized(get_app_config)
@ -28,17 +29,15 @@ def clear_standings():
from CTFd.utils.scores import get_standings
from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList
from CTFd.api import api
cache.delete_memoized(get_standings)
cache.delete(
make_cache_key(path=api.name + '.' + ScoreboardList.endpoint)
)
cache.delete(
make_cache_key(path=api.name + '.' + ScoreboardDetail.endpoint)
)
cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint))
cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint))
cache.delete_memoized(ScoreboardList.get)
def clear_pages():
from CTFd.utils.config.pages import get_page, get_pages
cache.delete_memoized(get_pages)
cache.delete_memoized(get_page)

View File

@ -1,21 +1,18 @@
from flask import (
render_template,
Blueprint,
)
from flask import render_template, Blueprint
from CTFd.utils.decorators import (
during_ctf_time_only,
require_verified_emails,
require_team
require_team,
)
from CTFd.utils.decorators.visibility import check_challenge_visibility
from CTFd.utils import config, get_config
from CTFd.utils.dates import ctf_ended, ctf_paused, view_after_ctf
from CTFd.utils.helpers import get_errors, get_infos
challenges = Blueprint('challenges', __name__)
challenges = Blueprint("challenges", __name__)
@challenges.route('/challenges', methods=['GET'])
@challenges.route("/challenges", methods=["GET"])
@during_ctf_time_only
@require_verified_emails
@check_challenge_visibility
@ -23,14 +20,16 @@ challenges = Blueprint('challenges', __name__)
def listing():
infos = get_infos()
errors = get_errors()
start = get_config('start') or 0
end = get_config('end') or 0
start = get_config("start") or 0
end = get_config("end") or 0
if ctf_paused():
infos.append('{} is paused'.format(config.ctf_name()))
infos.append("{} is paused".format(config.ctf_name()))
# CTF has ended but we want to allow view_after_ctf. Show error but let JS load challenges.
if ctf_ended() and view_after_ctf():
infos.append('{} has ended'.format(config.ctf_name()))
infos.append("{} has ended".format(config.ctf_name()))
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
return render_template(
"challenges.html", infos=infos, errors=errors, start=int(start), end=int(end)
)

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,12 @@ from CTFd.utils.plugins import (
register_script as utils_register_plugin_script,
register_stylesheet as utils_register_plugin_stylesheet,
register_admin_script as utils_register_admin_plugin_script,
register_admin_stylesheet as utils_register_admin_plugin_stylesheet
register_admin_stylesheet as utils_register_admin_plugin_stylesheet,
)
from CTFd.utils.config.pages import get_pages
Menu = namedtuple('Menu', ['title', 'route'])
Menu = namedtuple("Menu", ["title", "route"])
def register_plugin_assets_directory(app, base_path, admins_only=False):
@ -27,12 +27,12 @@ def register_plugin_assets_directory(app, base_path, admins_only=False):
:param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public
:return:
"""
base_path = base_path.strip('/')
base_path = base_path.strip("/")
def assets_handler(path):
return send_from_directory(base_path, path)
rule = '/' + base_path + '/<path:path>'
rule = "/" + base_path + "/<path:path>"
app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler)
@ -45,14 +45,14 @@ def register_plugin_asset(app, asset_path, admins_only=False):
:param boolean admins_only: Whether or not this file should be accessible to the public
:return:
"""
asset_path = asset_path.strip('/')
asset_path = asset_path.strip("/")
def asset_handler():
return send_file(asset_path)
if admins_only:
asset_handler = admins_only_wrapper(asset_handler)
rule = '/' + asset_path
rule = "/" + asset_path
app.add_url_rule(rule=rule, endpoint=asset_path, view_func=asset_handler)
@ -170,14 +170,14 @@ def init_plugins(app):
app.admin_plugin_menu_bar = []
app.plugin_menu_bar = []
if app.config.get('SAFE_MODE', False) is False:
if app.config.get("SAFE_MODE", False) is False:
modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
blacklist = {'__pycache__'}
blacklist = {"__pycache__"}
for module in modules:
module_name = os.path.basename(module)
if os.path.isdir(module) and module_name not in blacklist:
module = '.' + module_name
module = importlib.import_module(module, package='CTFd.plugins')
module = "." + module_name
module = importlib.import_module(module, package="CTFd.plugins")
module.load(app)
print(" * Loaded module, %s" % module)

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
var months = {
'January': 1,
'February': 2,
'March': 3,
'April': 4,
'May': 5,
'June': 6,
'July': 7,
'August': 8,
'September': 9,
'October': 10,
'November': 11,
'December': 12,
January: 1,
February: 2,
March: 3,
April: 4,
May: 5,
June: 6,
July: 7,
August: 8,
September: 9,
October: 10,
November: 11,
December: 12
};
function load_timestamp(place, timestamp) {
@ -18,35 +18,39 @@ function load_timestamp(place, timestamp) {
var timestamp = parseInt(timestamp);
}
var m = moment(timestamp * 1000);
console.log('Loading ' + place);
console.log("Loading " + place);
console.log(timestamp);
console.log(m.toISOString());
console.log(m.unix());
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 year = $('#' + place + '-year').val(m.year());
var hour = $('#' + place + '-hour').val(m.hour());
var minute = $('#' + place + '-minute').val(m.minute());
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 year = $("#" + place + "-year").val(m.year());
var hour = $("#" + place + "-hour").val(m.hour());
var minute = $("#" + place + "-minute").val(m.minute());
load_date_values(place);
}
function load_date_values(place) {
var month = $('#' + place + '-month').val();
var day = $('#' + place + '-day').val();
var year = $('#' + place + '-year').val();
var hour = $('#' + place + '-hour').val();
var minute = $('#' + place + '-minute').val();
var timezone = $('#' + place + '-timezone').val();
var month = $("#" + place + "-month").val();
var day = $("#" + place + "-day").val();
var year = $("#" + place + "-year").val();
var hour = $("#" + place + "-hour").val();
var minute = $("#" + place + "-minute").val();
var timezone = $("#" + place + "-timezone").val();
var utc = convert_date_to_moment(month, day, year, hour, minute, timezone);
if (isNaN(utc.unix())) {
$('#' + place).val('');
$('#' + place + '-local').val('');
$('#' + place + '-zonetime').val('');
$("#" + place).val("");
$("#" + place + "-local").val("");
$("#" + place + "-zonetime").val("");
} else {
$('#' + place).val(utc.unix());
$('#' + place + '-local').val(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"));
$("#" + place).val(utc.unix());
$("#" + place + "-local").val(
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
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);
return m;
}
function update_configs(obj){
var target = '/api/v1/configs';
var method = 'PATCH';
function update_configs(obj) {
var target = "/api/v1/configs";
var method = "PATCH";
var params = {};
@ -95,7 +109,7 @@ function update_configs(obj){
}
}
Object.keys(obj).forEach(function (x) {
Object.keys(obj).forEach(function(x) {
if (obj[x] === "true") {
params[x] = true;
} else if (obj[x] === "false") {
@ -107,39 +121,43 @@ function update_configs(obj){
CTFd.fetch(target, {
method: method,
credentials: 'same-origin',
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
}).then(function(response) {
return response.json()
}).then(function(data) {
})
.then(function(response) {
return response.json();
})
.then(function(data) {
window.location.reload();
});
}
function upload_logo(form) {
upload_files(form, function (response) {
upload_files(form, function(response) {
var upload = response.data[0];
if (upload.location) {
var params = {
'value': upload.location
value: upload.location
};
CTFd.fetch('/api/v1/configs/ctf_logo', {
method: 'PATCH',
credentials: 'same-origin',
CTFd.fetch("/api/v1/configs/ctf_logo", {
method: "PATCH",
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
}).then(function (response) {
})
.then(function(response) {
return response.json();
}).then(function (response) {
})
.then(function(response) {
if (response.success) {
window.location.reload()
window.location.reload();
} else {
ezal({
title: "Error!",
@ -156,87 +174,88 @@ function remove_logo() {
ezq({
title: "Remove logo",
body: "Are you sure you'd like to remove the CTF logo?",
success: function () {
success: function() {
var params = {
'value': null
value: null
};
CTFd.fetch('/api/v1/configs/ctf_logo', {
method: 'PATCH',
credentials: 'same-origin',
CTFd.fetch("/api/v1/configs/ctf_logo", {
method: "PATCH",
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
}).then(function (response) {
})
.then(function(response) {
return response.json();
}).then(function (data) {
})
.then(function(data) {
window.location.reload();
});
}
});
}
$(function () {
$('.config-section > form:not(.form-upload)').submit(function(e){
$(function() {
$(".config-section > form:not(.form-upload)").submit(function(e) {
e.preventDefault();
var obj = $(this).serializeJSON();
update_configs(obj);
});
$('#logo-upload').submit(function(e){
$("#logo-upload").submit(function(e) {
e.preventDefault();
var form = e.target;
upload_logo(form);
});
$('.start-date').change(function () {
load_date_values('start');
$(".start-date").change(function() {
load_date_values("start");
});
$('.end-date').change(function () {
load_date_values('end');
$(".end-date").change(function() {
load_date_values("end");
});
$('.freeze-date').change(function () {
load_date_values('freeze');
$(".freeze-date").change(function() {
load_date_values("freeze");
});
$('#export-button').click(function (e) {
$("#export-button").click(function(e) {
e.preventDefault();
var href = script_root + '/admin/export';
window.location.href = $('#export-button').attr('href');
var href = script_root + "/admin/export";
window.location.href = $("#export-button").attr("href");
});
$('#import-button').click(function (e) {
$("#import-button").click(function(e) {
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();
form_data.append('backup', import_file);
form_data.append('nonce', csrf_nonce);
form_data.append("backup", import_file);
form_data.append("nonce", csrf_nonce);
var pg = ezpg({
width: 0,
title: "Upload Progress",
title: "Upload Progress"
});
$.ajax({
url: script_root + '/admin/import',
type: 'POST',
url: script_root + "/admin/import",
type: "POST",
data: form_data,
processData: false,
contentType: false,
statusCode: {
500: function (resp) {
500: function(resp) {
console.log(resp.responseText);
alert(resp.responseText);
}
},
xhr: function () {
xhr: function() {
var xhr = $.ajaxSettings.xhr();
xhr.upload.onprogress = function (e) {
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var width = (e.loaded / e.total) * 100;
pg = ezpg({
@ -247,23 +266,19 @@ $(function () {
};
return xhr;
},
success: function (data) {
success: function(data) {
// Refresh modal
pg = ezpg({
target: pg,
width: 100,
width: 100
});
setTimeout(
function () {
pg.modal('hide');
}, 500
);
setTimeout(function() {
pg.modal("hide");
}, 500);
setTimeout(
function () {
setTimeout(function() {
window.location.reload();
}, 700
);
}, 700);
}
});
});
@ -271,30 +286,32 @@ $(function () {
var hash = window.location.hash;
if (hash) {
hash = hash.replace("<>[]'\"", "");
$('ul.nav a[href="' + hash + '"]').tab('show');
$('ul.nav a[href="' + hash + '"]').tab("show");
}
$('.nav-pills a').click(function (e) {
$(this).tab('show');
$(".nav-pills a").click(function(e) {
$(this).tab("show");
window.location.hash = this.hash;
});
var start = $('#start').val();
var end = $('#end').val();
var freeze = $('#freeze').val();
var start = $("#start").val();
var end = $("#end").val();
var freeze = $("#freeze").val();
if (start) {
load_timestamp('start', start);
load_timestamp("start", start);
}
if (end) {
load_timestamp('end', end);
load_timestamp("end", end);
}
if (freeze) {
load_timestamp('freeze', freeze);
load_timestamp("freeze", freeze);
}
// Toggle username and password based on stored value
$('#mail_useauth').change(function () {
$('#mail_username_password').toggle(this.checked);
}).change();
$("#mail_useauth")
.change(function() {
$("#mail_username_password").toggle(this.checked);
})
.change();
});

View File

@ -3,21 +3,21 @@ function upload_files(form, cb) {
form = form[0];
}
var formData = new FormData(form);
formData.append('nonce', csrf_nonce);
formData.append("nonce", csrf_nonce);
var pg = ezpg({
width: 0,
title: "Upload Progress",
title: "Upload Progress"
});
$.ajax({
url: script_root + '/api/v1/files',
url: script_root + "/api/v1/files",
data: formData,
type: 'POST',
type: "POST",
cache: false,
contentType: false,
processData: false,
xhr: function () {
xhr: function() {
var xhr = $.ajaxSettings.xhr();
xhr.upload.onprogress = function (e) {
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var width = (e.loaded / e.total) * 100;
pg = ezpg({
@ -28,17 +28,15 @@ function upload_files(form, cb) {
};
return xhr;
},
success: function (data) {
success: function(data) {
// Refresh modal
pg = ezpg({
target: pg,
width: 100,
width: 100
});
setTimeout(
function () {
pg.modal('hide');
}, 500
);
setTimeout(function() {
pg.modal("hide");
}, 500);
if (cb) {
cb(data);

View File

@ -1,3 +1,3 @@
$(document).ready(function(){
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,19 @@
$(document).ready(function () {
$(document).ready(function() {
function scoregraph() {
var times = [];
var scores = [];
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/solves', function (solve_data) {
$.get(script_root + '/api/v1/teams/' + TEAM_ID + '/awards', function (award_data) {
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/solves", function(
solve_data
) {
$.get(script_root + "/api/v1/teams/" + TEAM_ID + "/awards", function(
award_data
) {
var solves = solve_data.data;
var awards = award_data.data;
var total = solves.concat(awards);
total.sort(function (a, b) {
total.sort(function(a, b) {
return new Date(a.date) - new Date(b.date);
});
@ -28,88 +32,97 @@ $(document).ready(function () {
{
x: times,
y: scores,
type: 'scatter',
type: "scatter",
marker: {
color: colorhash(TEAM_NAME + TEAM_ID),
color: colorhash(TEAM_NAME + TEAM_ID)
},
line: {
color: colorhash(TEAM_NAME + TEAM_ID),
color: colorhash(TEAM_NAME + TEAM_ID)
},
fill: 'tozeroy'
fill: "tozeroy"
}
];
var layout = {
title: 'Score over Time',
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
hovermode: 'closest',
title: "Score over Time",
paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: "rgba(0,0,0,0)",
hovermode: "closest",
xaxis: {
showgrid: false,
showspikes: true,
showspikes: true
},
yaxis: {
showgrid: false,
showspikes: true,
showspikes: true
},
legend: {
"orientation": "h"
orientation: "h"
}
};
$('#score-graph').empty();
document.getElementById('score-graph').fn = 'CTFd_score_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
Plotly.newPlot('score-graph', data, layout);
$("#score-graph").empty();
document.getElementById("score-graph").fn =
"CTFd_score_team_" +
TEAM_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("score-graph", data, layout);
});
});
}
function keys_percentage_graph() {
var base_url = script_root + '/api/v1/teams/' + TEAM_ID;
$.get(base_url + '/fails', function (fails) {
$.get(base_url + '/solves', function (solves) {
var base_url = script_root + "/api/v1/teams/" + TEAM_ID;
$.get(base_url + "/fails", function(fails) {
$.get(base_url + "/solves", function(solves) {
var solves_count = solves.data.length;
var fails_count = fails.data.length;
var graph_data = [{
var graph_data = [
{
values: [solves_count, fails_count],
labels: ['Solves', 'Fails'],
labels: ["Solves", "Fails"],
marker: {
colors: [
"rgb(0, 209, 64)",
"rgb(207, 38, 0)"
]
colors: ["rgb(0, 209, 64)", "rgb(207, 38, 0)"]
},
hole: .4,
type: 'pie'
}];
hole: 0.4,
type: "pie"
}
];
var layout = {
title: 'Solve Percentages',
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
title: "Solve Percentages",
paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: "rgba(0,0,0,0)",
legend: {
"orientation": "h"
orientation: "h"
}
};
$('#keys-pie-graph').empty();
document.getElementById('keys-pie-graph').fn = 'CTFd_submissions_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
Plotly.newPlot('keys-pie-graph', graph_data, layout);
$("#keys-pie-graph").empty();
document.getElementById("keys-pie-graph").fn =
"CTFd_submissions_team_" +
TEAM_ID +
"_" +
new Date().toISOString().slice(0, 19);
Plotly.newPlot("keys-pie-graph", graph_data, layout);
});
});
}
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 categories = [];
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) {
return categories.indexOf(elem) == pos;
});
@ -121,28 +134,34 @@ $(document).ready(function () {
count++;
}
}
counts.push(count)
counts.push(count);
}
var data = [{
var data = [
{
values: counts,
labels: keys,
hole: .4,
type: 'pie'
}];
hole: 0.4,
type: "pie"
}
];
var layout = {
title: 'Category Breakdown',
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
title: "Category Breakdown",
paper_bgcolor: "rgba(0,0,0,0)",
plot_bgcolor: "rgba(0,0,0,0)",
legend: {
"orientation": "v"
orientation: "v"
}
};
$('#categories-pie-graph').empty();
document.getElementById('categories-pie-graph').fn = 'CTFd_categories_team_' + TEAM_ID + '_' + (new Date).toISOString().slice(0, 19);
Plotly.newPlot('categories-pie-graph', data, layout);
$("#categories-pie-graph").empty();
document.getElementById("categories-pie-graph").fn =
"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();
scoregraph();
window.onresize = function () {
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
Plotly.Plots.resize(document.getElementById('score-graph'));
window.onresize = function() {
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
Plotly.Plots.resize(document.getElementById("score-graph"));
};
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,40 +1,42 @@
$(document).ready(function () {
$('#user-info-form').submit(function (e) {
$(document).ready(function() {
$("#user-info-form").submit(function(e) {
e.preventDefault();
var params = $('#user-info-form').serializeJSON(true);
var url = '/api/v1/users';
var params = $("#user-info-form").serializeJSON(true);
var url = "/api/v1/users";
if (params.notify) {
url += '?notify=true'
url += "?notify=true";
}
delete params.notify;
CTFd.fetch(url, {
method: 'POST',
credentials: 'same-origin',
method: "POST",
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
}).then(function (response) {
})
.then(function(response) {
return response.json();
}).then(function (response) {
})
.then(function(response) {
if (response.success) {
var user_id = response.data.id;
window.location = script_root + '/admin/users/' + user_id;
window.location = script_root + "/admin/users/" + user_id;
} else {
$('#user-info-form > #results').empty();
Object.keys(response.errors).forEach(function (key, index) {
$('#user-info-form > #results').append(
$("#user-info-form > #results").empty();
Object.keys(response.errors).forEach(function(key, index) {
$("#user-info-form > #results").append(
ezbadge({
type: 'error',
type: "error",
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);
input.addClass('input-filled-invalid');
input.removeClass('input-filled-valid');
input.addClass("input-filled-invalid");
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 {
font-family: 'Lato', 'LatoOffline', sans-serif;
html,
body,
.container {
font-family: "Lato", "LatoOffline", sans-serif;
}
h1, h2 {
font-family: 'Raleway', 'RalewayOffline', sans-serif;
h1,
h2 {
font-family: "Raleway", "RalewayOffline", sans-serif;
font-weight: 500;
letter-spacing: 2px;
}
@ -39,7 +42,7 @@ table > thead > tr > td {
.jumbotron {
background-color: #343a40;
color: #FFF;
color: #fff;
border-radius: 0;
text-align: center;
}
@ -108,12 +111,12 @@ table > thead > tr > td {
}
.btn-info {
background-color: #5B7290 !important;
border-color: #5B7290 !important;
background-color: #5b7290 !important;
border-color: #5b7290 !important;
}
.badge-info {
background-color: #5B7290 !important;
background-color: #5b7290 !important;
}
.alert {

View File

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

View File

@ -1,11 +1,13 @@
html, body, .container {
html,
body,
.container {
height: 100% !important;
font-family: 'Lato', 'LatoOffline', sans-serif;
font-family: "Lato", "LatoOffline", sans-serif;
}
h1, h2 {
font-family: 'Raleway', 'RalewayOffline', sans-serif;
h1,
h2 {
font-family: "Raleway", "RalewayOffline", sans-serif;
font-weight: 500;
letter-spacing: 2px;
}
@ -43,11 +45,11 @@ img {
font-size: 24px;
letter-spacing: -0.04rem;
line-height: 15px;
color: #FFF;
color: #fff;
}
.navbar-inverse .navbar-nav > li > a {
color: #FFF !important;
color: #fff !important;
}
.navbar li > a {
@ -81,36 +83,36 @@ img {
padding-right: 70px;
}
#chal > form{
#chal > form {
width: 400px;
margin: 0 auto;
}
.reveal-modal{
.reveal-modal {
text-align: center;
}
.chal-desc{
.chal-desc {
text-align: left;
}
table{
table {
width: 100%;
}
#challenges button{
#challenges button {
margin: 8px;
}
.row > h1{
.row > h1 {
text-align: center;
}
#challenges{
#challenges {
line-height: 66px;
}
#score-graph{
#score-graph {
height: 450px;
display: block;
clear: both;
@ -128,17 +130,17 @@ table{
opacity: 0.5;
}
#keys-pie-graph{
#keys-pie-graph {
width: 50%;
float: left;
}
#categories-pie-graph{
#categories-pie-graph {
width: 50%;
float: left;
}
.logo{
.logo {
margin: 0 auto;
width: 100%;
max-width: 500px;
@ -146,8 +148,8 @@ table{
display: block;
}
@media only screen and (min-width: 40.063em){
.top-bar .dropdown{
@media only screen and (min-width: 40.063em) {
.top-bar .dropdown {
display: block;
padding: 0 15px 5px;
width: 200% !important;
@ -164,11 +166,11 @@ table{
margin-bottom: 0;
vertical-align: middle;
white-space: nowrap;
font-size:14px;
line-height:20px;
font-weight:700;
text-transform:uppercase;
padding:8px 20px;
font-size: 14px;
line-height: 20px;
font-weight: 700;
text-transform: uppercase;
padding: 8px 20px;
}
.btn-outlined {
@ -179,7 +181,7 @@ table{
}
.file-wrapper {
background-color: #5B7290;
background-color: #5b7290;
}
.file-wrapper:hover {
@ -188,11 +190,10 @@ table{
.theme-background {
background-color: #545454 !important;
}
.solved-challenge {
background-color: #8EDC9D !important;
background-color: #8edc9d !important;
}
.panel-theme {
@ -203,7 +204,7 @@ table{
border-color: #545454;
background-color: #545454;
opacity: 1;
color: #FFF;
color: #fff;
text-align: center;
}
@ -217,7 +218,7 @@ table{
.btn-outlined.btn-theme:hover,
.btn-outlined.btn-theme:active {
color: #FFF;
color: #fff;
background: #545454;
border-color: #545454;
}
@ -230,7 +231,7 @@ table{
.jumbotron {
background-color: #545454;
color: #FFF;
color: #fff;
padding: 0px 0px 25px;
}
@ -351,7 +352,7 @@ table{
}
.label-content {
color: #8B8C8B;
color: #8b8c8b;
padding: 0.25em 0;
-webkit-transition: -webkit-transform 0.3s;
transition: transform 0.3s;

View File

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

View File

@ -5,11 +5,11 @@ var templates = {};
window.challenge = new Object();
function loadchal(id) {
var obj = $.grep(challenges, function (e) {
var obj = $.grep(challenges, function(e) {
return e.id == id;
})[0];
if (obj.type === 'hidden') {
if (obj.type === "hidden") {
ezal({
title: "Challenge Hidden!",
body: "You haven't unlocked this challenge yet!",
@ -22,7 +22,7 @@ function loadchal(id) {
}
function loadchalbyname(chalname) {
var obj = $.grep(challenges, function (e) {
var obj = $.grep(challenges, function(e) {
return e.name == chalname;
})[0];
@ -30,31 +30,33 @@ function loadchalbyname(chalname) {
}
function updateChalWindow(obj) {
$.get(script_root + "/api/v1/challenges/" + obj.id, function (response) {
$.get(script_root + "/api/v1/challenges/" + obj.id, function(response) {
var challenge_data = response.data;
$.getScript(script_root + obj.script, function () {
$.get(script_root + obj.template, function (template_data) {
$('#challenge-window').empty();
$.getScript(script_root + obj.script, function() {
$.get(script_root + obj.template, function(template_data) {
$("#challenge-window").empty();
var template = nunjucks.compile(template_data);
window.challenge.data = challenge_data;
window.challenge.preRender();
challenge_data['description'] = window.challenge.render(challenge_data['description']);
challenge_data['script_root'] = script_root;
challenge_data["description"] = window.challenge.render(
challenge_data["description"]
);
challenge_data["script_root"] = script_root;
$('#challenge-window').append(template.render(challenge_data));
$("#challenge-window").append(template.render(challenge_data));
$('.challenge-solves').click(function (e) {
getsolves($('#challenge-id').val())
$(".challenge-solves").click(function(e) {
getsolves($("#challenge-id").val());
});
$('.nav-tabs a').click(function (e) {
$(".nav-tabs a").click(function(e) {
e.preventDefault();
$(this).tab('show')
$(this).tab("show");
});
// 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("correct");
$("#incorrect-key").slideUp();
@ -63,110 +65,138 @@ function updateChalWindow(obj) {
$("#too-fast").slideUp();
});
$('#submit-key').click(function (e) {
$("#submit-key").click(function(e) {
e.preventDefault();
$('#submit-key').addClass("disabled-button");
$('#submit-key').prop('disabled', true);
window.challenge.submit(function (data) {
$("#submit-key").addClass("disabled-button");
$("#submit-key").prop("disabled", true);
window.challenge.submit(function(data) {
renderSubmissionResponse(data);
loadchals(function () {
loadchals(function() {
marksolves();
});
});
});
$("#submission-input").keyup(function (event) {
$("#submission-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
$(".input-field").bind({
focus: function () {
$(this).parent().addClass('input--filled');
focus: function() {
$(this)
.parent()
.addClass("input--filled");
$label = $(this).siblings(".input-label");
},
blur: function () {
if ($(this).val() === '') {
$(this).parent().removeClass('input--filled');
blur: function() {
if ($(this).val() === "") {
$(this)
.parent()
.removeClass("input--filled");
$label = $(this).siblings(".input-label");
$label.removeClass('input--hide');
$label.removeClass("input--hide");
}
}
});
window.challenge.postRender();
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
$('#challenge-window').modal();
window.location.replace(
window.location.href.split("#")[0] + "#" + obj.name
);
$("#challenge-window").modal();
});
});
});
}
$("#submission-input").keyup(function (event) {
$("#submission-input").keyup(function(event) {
if (event.keyCode == 13) {
$("#submit-key").click();
}
});
function renderSubmissionResponse(response, cb) {
var result = response.data;
var result_message = $('#result-message');
var result_notification = $('#result-notification');
var result_message = $("#result-message");
var result_notification = $("#result-notification");
var answer_input = $("#submission-input");
result_notification.removeClass();
result_message.text(result.message);
if (result.status === "authentication_required") {
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash;
return
}
else if (result.status === "incorrect") { // Incorrect key
result_notification.addClass('alert alert-danger alert-dismissable text-center');
window.location =
script_root +
"/login?next=" +
script_root +
window.location.pathname +
window.location.hash;
return;
} else if (result.status === "incorrect") {
// Incorrect key
result_notification.addClass(
"alert alert-danger alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.removeClass("correct");
answer_input.addClass("wrong");
setTimeout(function () {
setTimeout(function() {
answer_input.removeClass("wrong");
}, 3000);
}
else if (result.status === "correct") { // Challenge Solved
result_notification.addClass('alert alert-success alert-dismissable text-center');
} else if (result.status === "correct") {
// Challenge Solved
result_notification.addClass(
"alert alert-success alert-dismissable text-center"
);
result_notification.slideDown();
$('.challenge-solves').text((parseInt($('.challenge-solves').text().split(" ")[0]) + 1 + " Solves"));
$(".challenge-solves").text(
parseInt(
$(".challenge-solves")
.text()
.split(" ")[0]
) +
1 +
" Solves"
);
answer_input.val("");
answer_input.removeClass("wrong");
answer_input.addClass("correct");
}
else if (result.status === "already_solved") { // Challenge already solved
result_notification.addClass('alert alert-info alert-dismissable text-center');
} else if (result.status === "already_solved") {
// Challenge already solved
result_notification.addClass(
"alert alert-info alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.addClass("correct");
}
else if (result.status === "paused") { // CTF is paused
result_notification.addClass('alert alert-warning alert-dismissable text-center');
} else if (result.status === "paused") {
// CTF is paused
result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown();
}
else if (result.status === "ratelimited") { // Keys per minute too high
result_notification.addClass('alert alert-warning alert-dismissable text-center');
} else if (result.status === "ratelimited") {
// Keys per minute too high
result_notification.addClass(
"alert alert-warning alert-dismissable text-center"
);
result_notification.slideDown();
answer_input.addClass("too-fast");
setTimeout(function () {
setTimeout(function() {
answer_input.removeClass("too-fast");
}, 3000);
}
setTimeout(function () {
$('.alert').slideUp();
$('#submit-key').removeClass("disabled-button");
$('#submit-key').prop('disabled', false);
setTimeout(function() {
$(".alert").slideUp();
$("#submit-key").removeClass("disabled-button");
$("#submit-key").prop("disabled", false);
}, 3000);
if (cb) {
@ -175,13 +205,15 @@ function renderSubmissionResponse(response, 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;
for (var i = solves.length - 1; i >= 0; i--) {
var id = solves[i].challenge_id;
var btn = $('button[value="' + id + '"]');
btn.addClass('solved-challenge');
btn.prepend("<i class='fas fa-check corner-button-check'></i>")
btn.addClass("solved-challenge");
btn.prepend("<i class='fas fa-check corner-button-check'></i>");
}
if (cb) {
cb();
@ -191,13 +223,14 @@ function marksolves(cb) {
function load_user_solves(cb) {
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;
for (var i = solves.length - 1; i >= 0; i--) {
var chal_id = solves[i].challenge_id;
user_solves.push(chal_id);
}
if (cb) {
cb();
@ -209,29 +242,38 @@ function load_user_solves(cb) {
}
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;
$('.challenge-solves').text(
(parseInt(data.length) + " Solves")
);
var box = $('#challenge-solves-names');
$(".challenge-solves").text(parseInt(data.length) + " Solves");
var box = $("#challenge-solves-names");
box.empty();
for (var i = 0; i < data.length; i++) {
var id = data[i].account_id;
var name = data[i].name;
var date = moment(data[i].date).local().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));
var date = moment(data[i].date)
.local()
.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
)
);
}
});
}
function loadchals(cb) {
$.get(script_root + "/api/v1/challenges", function (response) {
$.get(script_root + "/api/v1/challenges", function(response) {
var categories = [];
challenges = response.data;
$('#challenges-board').empty();
$("#challenges-board").empty();
for (var i = challenges.length - 1; i >= 0; i--) {
challenges[i].solves = 0;
@ -240,17 +282,21 @@ function loadchals(cb) {
categories.push(category);
var categoryid = category.replace(/ /g, "-").hashCode();
var categoryrow = $('' +
var categoryrow = $(
"" +
'<div id="{0}-row" class="pt-5">'.format(categoryid) +
'<div class="category-header col-md-12 mb-3">' +
'</div>' +
"</div>" +
'<div class="category-challenges col-md-12">' +
'<div class="challenges-row col-md-12"></div>' +
'</div>' +
'</div>');
categoryrow.find(".category-header").append($("<h3>" + category + "</h3>"));
"</div>" +
"</div>"
);
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 chalid = chalinfo.name.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) {
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 {
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 chalscore = $("<span>{0}</span>".format(chalinfo.value));
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);
}
@ -278,10 +334,12 @@ function loadchals(cb) {
chalbutton.append(chalscore);
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);
getsolves(this.value);
});
@ -292,17 +350,19 @@ function loadchals(cb) {
});
}
$('#submit-key').click(function (e) {
submitkey($('#challenge-id').val(), $('#submission-input').val(), $('#nonce').val())
$("#submit-key").click(function(e) {
submitkey(
$("#challenge-id").val(),
$("#submission-input").val(),
$("#nonce").val()
);
});
$('.challenge-solves').click(function (e) {
getsolves($('#challenge-id').val())
$(".challenge-solves").click(function(e) {
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("correct");
$("#incorrect-key").slideUp();
@ -311,15 +371,17 @@ $('#challenge-window').on('hide.bs.modal', function (event) {
$("#too-fast").slideUp();
});
var load_location_hash = function () {
var load_location_hash = function() {
if (window.location.hash.length > 0) {
loadchalbyname(decodeURIComponent(window.location.hash.substring(1)));
}
};
function update(cb) {
load_user_solves(function () { // Load the user's solved challenge ids
loadchals(function () { // Load the full list of challenges
load_user_solves(function() {
// Load the user's solved challenge ids
loadchals(function() {
// Load the full list of challenges
if (cb) {
cb();
}
@ -327,20 +389,20 @@ function update(cb) {
});
}
$(function () {
update(function () {
$(function() {
update(function() {
load_location_hash();
});
});
$('.nav-tabs a').click(function (e) {
$(".nav-tabs a").click(function(e) {
e.preventDefault();
$(this).tab('show')
$(this).tab("show");
});
$('#challenge-window').on('hidden.bs.modal', function () {
$('.nav-tabs a:first').tab('show');
history.replaceState('', document.title, window.location.pathname);
$("#challenge-window").on("hidden.bs.modal", function() {
$(".nav-tabs a:first").tab("show");
history.replaceState("", document.title, window.location.pathname);
});
setInterval(update, 300000);

View File

@ -3,18 +3,22 @@ var wc = new WindowController();
var sound = new Howl({
src: [
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() {
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);
wc.broadcast('notification', data);
wc.broadcast("notification", data);
render(data);
}, false);
},
false
);
}
function disconnect() {
@ -36,7 +40,7 @@ wc.notification = function(data) {
render(data);
};
wc.masterDidChange = function () {
wc.masterDidChange = function() {
if (this.isMaster) {
connect();
} else {

View File

@ -1,95 +1,101 @@
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-content">' +
' <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">' +
' <span aria-hidden="true">&times;</span>' +
' </button>' +
' </div>' +
" </button>" +
" </div>" +
' <div class="modal-body">' +
' <p>\{1\}</p>' +
' </div>' +
" <p>{1}</p>" +
" </div>" +
' <div class="modal-footer">' +
' </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>" +
" </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" +
" \{0\}\n" +
" <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\n" +
" {0}\n" +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>\n' +
"</div>";
function ezal(args){
function ezal(args) {
var res = modal.format(args.title, args.body);
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);
$('main').append(obj);
obj.find(".modal-footer").append(button);
$("main").append(obj);
obj.modal('show');
obj.modal("show");
$(obj).on('hidden.bs.modal', function (e) {
$(this).modal('dispose');
$(obj).on("hidden.bs.modal", function(e) {
$(this).modal("dispose");
});
return obj;
}
function ezq(args){
function ezq(args) {
var res = modal.format(args.title, args.body);
var obj = $(res);
var deny = '<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>');
var deny =
'<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(confirm);
obj.find(".modal-footer").append(deny);
obj.find(".modal-footer").append(confirm);
$('main').append(obj);
$("main").append(obj);
$(obj).on('hidden.bs.modal', function (e) {
$(this).modal('dispose');
$(obj).on("hidden.bs.modal", function(e) {
$(this).modal("dispose");
});
$(confirm).click(function(){
$(confirm).click(function() {
args.success();
});
obj.modal('show');
obj.modal("show");
return obj;
}
function ezpg(args){
if (args.target){
function ezpg(args) {
if (args.target) {
var obj = $(args.target);
var pbar = obj.find('.progress-bar');
pbar.css('width', args.width + '%');
var pbar = obj.find(".progress-bar");
pbar.css("width", args.width + "%");
return obj;
}
var bar = progress.format(args.width);
var res = modal.format(args.title, bar);
var obj = $(res);
$('main').append(obj);
obj.modal('show');
$("main").append(obj);
obj.modal("show");
return obj;
}
@ -98,9 +104,9 @@ function ezbadge(args) {
var type = args.type;
var body = args.body;
var tpl = undefined;
if (type === 'success') {
if (type === "success") {
tpl = success_template;
} else if (type === 'error') {
} else if (type === "error") {
tpl = error_template;
}

View File

@ -1,27 +1,26 @@
function hint(id) {
return CTFd.fetch('/api/v1/hints/' + id, {
method: 'GET',
credentials: 'same-origin',
return CTFd.fetch("/api/v1/hints/" + id, {
method: "GET",
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
}
}).then(function (response) {
}).then(function(response) {
return response.json();
});
}
function unlock(params){
return CTFd.fetch('/api/v1/unlocks', {
method: 'POST',
credentials: 'same-origin',
function unlock(params) {
return CTFd.fetch("/api/v1/unlocks", {
method: "POST",
credentials: "same-origin",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(params)
}).then(function (response) {
}).then(function(response) {
return response.json();
});
}
@ -29,10 +28,10 @@ function unlock(params){
function loadhint(hintid) {
var md = window.markdownit({
html: true,
linkify: true,
linkify: true
});
hint(hintid).then(function (response) {
hint(hintid).then(function(response) {
if (response.data.content) {
ezal({
title: "Hint",
@ -43,12 +42,12 @@ function loadhint(hintid) {
ezq({
title: "Unlock Hint?",
body: "Are you sure you want to open this hint?",
success: function () {
success: function() {
var params = {
target: hintid,
type: "hints"
};
unlock(params).then(function (response) {
unlock(params).then(function(response) {
if (response.success) {
hint(hintid).then(function(response) {
ezal({

View File

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

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