mirror of https://github.com/JohnHammond/CTFd.git
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
parent
3d23ece370
commit
6833378c36
|
@ -70,3 +70,6 @@ CTFd/uploads
|
|||
|
||||
# CTFd Exports
|
||||
*.zip
|
||||
|
||||
# JS
|
||||
node_modules/
|
|
@ -0,0 +1,10 @@
|
|||
CTFd/themes/**/vendor/
|
||||
*.html
|
||||
*.njk
|
||||
*.png
|
||||
*.svg
|
||||
*.ico
|
||||
*.ai
|
||||
*.svg
|
||||
*.mp3
|
||||
*.webm
|
18
.travis.yml
18
.travis.yml
|
@ -1,5 +1,8 @@
|
|||
language: python
|
||||
cache: pip
|
||||
dist: xenial
|
||||
cache:
|
||||
- pip
|
||||
- yarn
|
||||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
|
@ -7,26 +10,25 @@ services:
|
|||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- mysql-5.7-trusty
|
||||
- deadsnakes
|
||||
packages:
|
||||
- mysql-server
|
||||
- mysql-client
|
||||
- python3.6
|
||||
- python3-pip
|
||||
env:
|
||||
- TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd'
|
||||
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
|
||||
- TESTING_DATABASE_URL='sqlite://'
|
||||
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
before_install:
|
||||
- sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
|
||||
- sudo mysql_upgrade -u root -ppassword
|
||||
- sudo service mysql restart
|
||||
- sudo rm -f /etc/boto.cfg
|
||||
- export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
||||
- export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
- python3.6 -m pip install black>=19.3b0
|
||||
install:
|
||||
- pip install -r development.txt
|
||||
- yarn global add prettier@1.17.0
|
||||
before_script:
|
||||
- psql -c 'create database ctfd;' -U postgres
|
||||
script:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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": {}}
|
||||
|
|
327
CTFd/auth.py
327
CTFd/auth.py
|
@ -28,119 +28,150 @@ from CTFd.utils.helpers import error_for, get_errors
|
|||
import base64
|
||||
import requests
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
auth = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
@auth.route('/confirm', methods=['POST', 'GET'])
|
||||
@auth.route('/confirm/<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"))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
118
CTFd/config.py
118
CTFd/config.py
|
@ -1,12 +1,12 @@
|
|||
import os
|
||||
|
||||
''' GENERATE SECRET KEY '''
|
||||
""" GENERATE SECRET KEY """
|
||||
|
||||
if not os.getenv('SECRET_KEY'):
|
||||
if not os.getenv("SECRET_KEY"):
|
||||
# Attempt to read the secret from the secret file
|
||||
# This will fail if the secret has not been written
|
||||
try:
|
||||
with open('.ctfd_secret_key', 'rb') as secret:
|
||||
with open(".ctfd_secret_key", "rb") as secret:
|
||||
key = secret.read()
|
||||
except (OSError, IOError):
|
||||
key = None
|
||||
|
@ -16,14 +16,14 @@ if not os.getenv('SECRET_KEY'):
|
|||
# Attempt to write the secret file
|
||||
# This will fail if the filesystem is read-only
|
||||
try:
|
||||
with open('.ctfd_secret_key', 'wb') as secret:
|
||||
with open(".ctfd_secret_key", "wb") as secret:
|
||||
secret.write(key)
|
||||
secret.flush()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
|
||||
''' SERVER SETTINGS '''
|
||||
""" SERVER SETTINGS """
|
||||
|
||||
|
||||
class Config(object):
|
||||
|
@ -31,7 +31,7 @@ class Config(object):
|
|||
CTFd Configuration Object
|
||||
"""
|
||||
|
||||
'''
|
||||
"""
|
||||
=== REQUIRED SETTINGS ===
|
||||
|
||||
SECRET_KEY:
|
||||
|
@ -61,21 +61,27 @@ class Config(object):
|
|||
REDIS_URL is the URL to connect to a Redis server.
|
||||
e.g. redis://user:password@localhost:6379
|
||||
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
|
||||
'''
|
||||
SECRET_KEY = os.getenv('SECRET_KEY') or key
|
||||
DATABASE_URL = os.getenv('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))
|
||||
REDIS_URL = os.getenv('REDIS_URL')
|
||||
"""
|
||||
SECRET_KEY = os.getenv("SECRET_KEY") or key
|
||||
DATABASE_URL = os.getenv("DATABASE_URL") or "sqlite:///{}/ctfd.db".format(
|
||||
os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
REDIS_URL = os.getenv("REDIS_URL")
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||
CACHE_REDIS_URL = REDIS_URL
|
||||
if CACHE_REDIS_URL:
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_TYPE = "redis"
|
||||
else:
|
||||
CACHE_TYPE = 'filesystem'
|
||||
CACHE_DIR = os.path.join(os.path.dirname(__file__), os.pardir, '.data', 'filesystem_cache')
|
||||
CACHE_THRESHOLD = 0 # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.
|
||||
CACHE_TYPE = "filesystem"
|
||||
CACHE_DIR = os.path.join(
|
||||
os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache"
|
||||
)
|
||||
CACHE_THRESHOLD = (
|
||||
0
|
||||
) # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing.
|
||||
|
||||
'''
|
||||
"""
|
||||
=== SECURITY ===
|
||||
|
||||
SESSION_COOKIE_HTTPONLY:
|
||||
|
@ -91,23 +97,25 @@ class Config(object):
|
|||
|
||||
CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based
|
||||
solely on IP addresses unless you know what you are doing.
|
||||
'''
|
||||
SESSION_COOKIE_HTTPONLY = (not os.getenv("SESSION_COOKIE_HTTPONLY")) # Defaults True
|
||||
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or 'Lax'
|
||||
PERMANENT_SESSION_LIFETIME = int(os.getenv("PERMANENT_SESSION_LIFETIME") or 604800) # 7 days in seconds
|
||||
"""
|
||||
SESSION_COOKIE_HTTPONLY = not os.getenv("SESSION_COOKIE_HTTPONLY") # Defaults True
|
||||
SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax"
|
||||
PERMANENT_SESSION_LIFETIME = int(
|
||||
os.getenv("PERMANENT_SESSION_LIFETIME") or 604800
|
||||
) # 7 days in seconds
|
||||
TRUSTED_PROXIES = [
|
||||
r'^127\.0\.0\.1$',
|
||||
r"^127\.0\.0\.1$",
|
||||
# Remove the following proxies if you do not trust the local network
|
||||
# For example if you are running a CTF on your laptop and the teams are
|
||||
# all on the same network
|
||||
r'^::1$',
|
||||
r'^fc00:',
|
||||
r'^10\.',
|
||||
r'^172\.(1[6-9]|2[0-9]|3[0-1])\.',
|
||||
r'^192\.168\.'
|
||||
r"^::1$",
|
||||
r"^fc00:",
|
||||
r"^10\.",
|
||||
r"^172\.(1[6-9]|2[0-9]|3[0-1])\.",
|
||||
r"^192\.168\.",
|
||||
]
|
||||
|
||||
'''
|
||||
"""
|
||||
=== EMAIL ===
|
||||
|
||||
MAILFROM_ADDR:
|
||||
|
@ -139,7 +147,7 @@ class Config(object):
|
|||
|
||||
MAILGUN_BASE_URL
|
||||
Mailgun base url to send email over Mailgun
|
||||
'''
|
||||
"""
|
||||
MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io"
|
||||
MAIL_SERVER = os.getenv("MAIL_SERVER") or None
|
||||
MAIL_PORT = os.getenv("MAIL_PORT")
|
||||
|
@ -151,15 +159,17 @@ class Config(object):
|
|||
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY")
|
||||
MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL")
|
||||
|
||||
'''
|
||||
"""
|
||||
=== LOGS ===
|
||||
LOG_FOLDER:
|
||||
The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins.
|
||||
The default location is the CTFd/logs folder.
|
||||
'''
|
||||
LOG_FOLDER = os.getenv('LOG_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
||||
"""
|
||||
LOG_FOLDER = os.getenv("LOG_FOLDER") or os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "logs"
|
||||
)
|
||||
|
||||
'''
|
||||
"""
|
||||
=== UPLOADS ===
|
||||
|
||||
UPLOAD_PROVIDER:
|
||||
|
@ -180,16 +190,18 @@ class Config(object):
|
|||
AWS_S3_ENDPOINT_URL:
|
||||
A URL pointing to a custom S3 implementation.
|
||||
|
||||
'''
|
||||
UPLOAD_PROVIDER = os.getenv('UPLOAD_PROVIDER') or 'filesystem'
|
||||
UPLOAD_FOLDER = os.getenv('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||
if UPLOAD_PROVIDER == 's3':
|
||||
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
AWS_S3_BUCKET = os.getenv('AWS_S3_BUCKET')
|
||||
AWS_S3_ENDPOINT_URL = os.getenv('AWS_S3_ENDPOINT_URL')
|
||||
"""
|
||||
UPLOAD_PROVIDER = os.getenv("UPLOAD_PROVIDER") or "filesystem"
|
||||
UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER") or os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "uploads"
|
||||
)
|
||||
if UPLOAD_PROVIDER == "s3":
|
||||
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
|
||||
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
|
||||
AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET")
|
||||
AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL")
|
||||
|
||||
'''
|
||||
"""
|
||||
=== OPTIONAL ===
|
||||
|
||||
REVERSE_PROXY:
|
||||
|
@ -216,33 +228,35 @@ class Config(object):
|
|||
APPLICATION_ROOT:
|
||||
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
|
||||
Example: /ctfd
|
||||
'''
|
||||
"""
|
||||
REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False
|
||||
TEMPLATES_AUTO_RELOAD = (not os.getenv("TEMPLATES_AUTO_RELOAD")) # Defaults True
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None # Defaults False
|
||||
SWAGGER_UI = '/' if os.getenv("SWAGGER_UI") is not None else False # Defaults False
|
||||
UPDATE_CHECK = (not os.getenv("UPDATE_CHECK")) # Defaults True
|
||||
APPLICATION_ROOT = os.getenv('APPLICATION_ROOT') or '/'
|
||||
TEMPLATES_AUTO_RELOAD = not os.getenv("TEMPLATES_AUTO_RELOAD") # Defaults True
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = (
|
||||
os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None
|
||||
) # Defaults False
|
||||
SWAGGER_UI = "/" if os.getenv("SWAGGER_UI") is not None else False # Defaults False
|
||||
UPDATE_CHECK = not os.getenv("UPDATE_CHECK") # Defaults True
|
||||
APPLICATION_ROOT = os.getenv("APPLICATION_ROOT") or "/"
|
||||
|
||||
'''
|
||||
"""
|
||||
=== OAUTH ===
|
||||
|
||||
MajorLeagueCyber Integration
|
||||
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here
|
||||
'''
|
||||
"""
|
||||
OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID")
|
||||
OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET")
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA'
|
||||
SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA"
|
||||
PRESERVE_CONTEXT_ON_EXCEPTION = False
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('TESTING_DATABASE_URL') or 'sqlite://'
|
||||
SERVER_NAME = 'localhost'
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://"
|
||||
SERVER_NAME = "localhost"
|
||||
UPDATE_CHECK = False
|
||||
REDIS_URL = None
|
||||
CACHE_TYPE = 'simple'
|
||||
CACHE_TYPE = "simple"
|
||||
CACHE_THRESHOLD = 500
|
||||
SAFE_MODE = True
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
@ -421,18 +419,13 @@ class Users(db.Model):
|
|||
|
||||
|
||||
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()
|
||||
|
||||
|
@ -603,39 +603,41 @@ class Teams(db.Model):
|
|||
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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/")
|
||||
|
|
|
@ -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/"
|
||||
)
|
||||
|
|
|
@ -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/")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,4 +5,4 @@ class ChallengeSchema(ma.ModelSchema):
|
|||
class Meta:
|
||||
model = Challenges
|
||||
include_fk = True
|
||||
dump_only = ('id',)
|
||||
dump_only = ("id",)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
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");
|
||||
|
@ -20,29 +27,45 @@ function renderSubmissionResponse(response, cb) {
|
|||
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");
|
||||
|
@ -51,9 +74,9 @@ function renderSubmissionResponse(response, cb) {
|
|||
}, 3000);
|
||||
}
|
||||
setTimeout(function() {
|
||||
$('.alert').slideUp();
|
||||
$('#submit-key').removeClass("disabled-button");
|
||||
$('#submit-key').prop('disabled', false);
|
||||
$(".alert").slideUp();
|
||||
$("#submit-key").removeClass("disabled-button");
|
||||
$("#submit-key").prop("disabled", false);
|
||||
}, 3000);
|
||||
|
||||
if (cb) {
|
||||
|
@ -62,35 +85,42 @@ function renderSubmissionResponse(response, cb) {
|
|||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.preview-challenge').click(function (e) {
|
||||
$(".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,12 +129,12 @@ $(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);
|
||||
$("#submit-key").addClass("disabled-button");
|
||||
$("#submit-key").prop("disabled", true);
|
||||
window.challenge.submit(function(data) {
|
||||
renderSubmissionResponse(data)
|
||||
renderSubmissionResponse(data);
|
||||
}, true);
|
||||
// Preview passed as true
|
||||
});
|
||||
|
@ -117,62 +147,74 @@ $(document).ready(function () {
|
|||
|
||||
$(".input-field").bind({
|
||||
focus: function() {
|
||||
$(this).parent().addClass('input--filled');
|
||||
$(this)
|
||||
.parent()
|
||||
.addClass("input--filled");
|
||||
$label = $(this).siblings(".input-label");
|
||||
},
|
||||
blur: function() {
|
||||
if ($(this).val() === '') {
|
||||
$(this).parent().removeClass('input--filled');
|
||||
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>"),
|
||||
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) {
|
||||
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;
|
||||
});
|
||||
});
|
|
@ -1,2 +1 @@
|
|||
$(document).ready(function () {
|
||||
});
|
||||
$(document).ready(function() {});
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
$(document).ready(function() {
|
||||
$('#file-add-form').submit(function (e) {
|
||||
$("#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,
|
||||
|
@ -36,35 +36,35 @@ $(document).ready(function () {
|
|||
// 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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
$(document).ready(function() {
|
||||
$('#flag-add-button').click(function (e) {
|
||||
$.get(script_root + '/api/v1/flags/types', function (response) {
|
||||
$("#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) {
|
||||
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) {
|
||||
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();
|
||||
$("#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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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) {
|
||||
return response.json();
|
||||
|
@ -14,7 +14,7 @@ function hint(id) {
|
|||
function loadhint(hintid) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
linkify: true,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
hint(hintid).then(function(response) {
|
||||
|
@ -35,39 +35,45 @@ function loadhint(hintid) {
|
|||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#hint-add-button').click(function (e) {
|
||||
$('#hint-edit-modal form').find("input, textarea").val("");
|
||||
$("#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) {
|
||||
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,66 +82,72 @@ $(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';
|
||||
var method = "POST";
|
||||
var url = "/api/v1/hints";
|
||||
if (params.id) {
|
||||
method = 'PATCH';
|
||||
url = '/api/v1/hints/' + 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) {
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.success) {
|
||||
// TODO: Refresh hints on submit.
|
||||
window.location.reload();
|
||||
|
|
|
@ -4,28 +4,33 @@ window.challenge = new Object();
|
|||
|
||||
function load_chal_template(challenge) {
|
||||
$.getScript(script_root + challenge.scripts.view, function() {
|
||||
console.log('loaded renderer');
|
||||
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}));
|
||||
$("#create-chal-entry-div").html(
|
||||
template.render({ nonce: nonce, script_root: script_root })
|
||||
);
|
||||
$.getScript(script_root + challenge.scripts.create, function() {
|
||||
console.log('loaded');
|
||||
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,7 +39,7 @@ function load_chal_template(challenge){
|
|||
});
|
||||
}
|
||||
|
||||
$.get(script_root + '/api/v1/challenges/types', function(response){
|
||||
$.get(script_root + "/api/v1/challenges/types", function(response) {
|
||||
$("#create-chals-select").empty();
|
||||
var data = response.data;
|
||||
var chal_type_amt = Object.keys(data).length;
|
||||
|
@ -44,9 +49,9 @@ $.get(script_root + '/api/v1/challenges/types', function(response){
|
|||
for (var key in data) {
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
$(document).ready(function() {
|
||||
$('#prerequisite-add-form').submit(function (e) {
|
||||
$("#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();
|
||||
}
|
||||
|
|
|
@ -1,51 +1,55 @@
|
|||
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) {
|
||||
$("#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) {
|
||||
})
|
||||
.then(function(response) {
|
||||
if (response.success) {
|
||||
var tpl = "<span class='badge badge-primary mx-1 challenge-tag'>" +
|
||||
var tpl =
|
||||
"<span class='badge badge-primary mx-1 challenge-tag'>" +
|
||||
"<span>{0}</span>" +
|
||||
"<a class='btn-fa delete-tag' tag-id='{1}' onclick='delete_tag(this)'>×</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("");
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
var target = "/api/v1/configs";
|
||||
var method = "PATCH";
|
||||
|
||||
var params = {};
|
||||
|
||||
|
@ -107,15 +121,17 @@ 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();
|
||||
});
|
||||
}
|
||||
|
@ -125,21 +141,23 @@ function upload_logo(form) {
|
|||
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!",
|
||||
|
@ -158,19 +176,21 @@ function remove_logo() {
|
|||
body: "Are you sure you'd like to remove the CTF logo?",
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
@ -178,53 +198,52 @@ function remove_logo() {
|
|||
}
|
||||
|
||||
$(function() {
|
||||
$('.config-section > form:not(.form-upload)').submit(function(e){
|
||||
$(".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,
|
||||
|
@ -251,19 +270,15 @@ $(function () {
|
|||
// 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();
|
||||
});
|
|
@ -3,15 +3,15 @@ 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,
|
||||
|
@ -32,13 +32,11 @@ function upload_files(form, cb) {
|
|||
// 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);
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
$(document).ready(function() {
|
||||
$('#notifications_form').submit(function(e){
|
||||
$("#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() {
|
||||
window.location.reload();
|
||||
|
@ -23,20 +25,22 @@ $(document).ready(function () {
|
|||
});
|
||||
});
|
||||
|
||||
$('.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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,70 +1,83 @@
|
|||
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));
|
||||
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.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)
|
||||
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'
|
||||
}));
|
||||
$("#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) {
|
||||
var data = response.data;
|
||||
|
@ -84,88 +97,91 @@ 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';
|
||||
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 = '';
|
||||
$("#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',
|
||||
CTFd.fetch("/api/v1/files/" + file_id, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(function(response) {
|
||||
if (response.status === 200) {
|
||||
response.json().then(function(object) {
|
||||
|
@ -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();
|
||||
$("#media-button").click(function() {
|
||||
$("#media-library-list").empty();
|
||||
refresh_files(function() {
|
||||
$('#media-modal').modal('show');
|
||||
$("#media-modal").modal("show");
|
||||
});
|
||||
// get_page_files().then(function (data) {
|
||||
// var files = data;
|
||||
|
|
|
@ -1,59 +1,57 @@
|
|||
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) {
|
||||
return CTFd.fetch("/api/v1/files?type=page", {
|
||||
credentials: "same-origin"
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
$(document).ready(function() {
|
||||
$('.delete-page').click(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) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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,14 +11,14 @@ 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
|
||||
return solves[b].solves - solves[a].solves;
|
||||
});
|
||||
|
||||
$.each(solves_order, function(key, 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() {
|
||||
|
@ -191,11 +211,11 @@ function update() {
|
|||
$(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'));
|
||||
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"));
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
$(".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 () {
|
||||
$(".form-control").each(function() {
|
||||
if ($(this).val()) {
|
||||
$(this).addClass("input-filled-valid");
|
||||
}
|
||||
|
@ -28,7 +27,7 @@ $(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;
|
||||
}
|
||||
|
@ -38,10 +37,10 @@ $(function () {
|
|||
|
||||
$("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();
|
||||
});
|
|
@ -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');
|
||||
$(".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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,35 +1,39 @@
|
|||
$(document).ready(function() {
|
||||
$('.edit-team').click(function (e) {
|
||||
$('#team-info-modal').modal('toggle');
|
||||
$(".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>"),
|
||||
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) {
|
||||
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',
|
||||
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',
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@ $(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;
|
||||
|
||||
|
@ -28,85 +32,94 @@ $(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) {
|
||||
|
@ -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'));
|
||||
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
|
||||
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
|
||||
Plotly.Plots.resize(document.getElementById("score-graph"));
|
||||
};
|
||||
});
|
|
@ -1,72 +1,77 @@
|
|||
$(document).ready(function() {
|
||||
$('#team-info-form').submit(function (e) {
|
||||
$("#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();
|
||||
$("#team-info-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#team-info-form > #results').append(
|
||||
$("#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();
|
||||
$("#team-captain-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#team-captain-form > #results').append(
|
||||
$("#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");
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,37 +1,39 @@
|
|||
$(document).ready(function() {
|
||||
$('#team-info-form').submit(function (e) {
|
||||
$("#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();
|
||||
$("#team-info-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#team-info-form > #results').append(
|
||||
$("#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");
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1 @@
|
|||
$(document).ready(function () {
|
||||
|
||||
});
|
||||
$(document).ready(function() {});
|
||||
|
|
|
@ -1,114 +1,126 @@
|
|||
$(document).ready(function() {
|
||||
$('.delete-user').click(function(e){
|
||||
$(".delete-user").click(function(e) {
|
||||
ezq({
|
||||
title: "Delete User",
|
||||
body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(USER_NAME) + "</strong>"),
|
||||
body: "Are you sure you want to delete {0}".format(
|
||||
"<strong>" + htmlentities(USER_NAME) + "</strong>"
|
||||
),
|
||||
success: function() {
|
||||
CTFd.fetch('/api/v1/users/' + USER_ID, {
|
||||
method: 'DELETE',
|
||||
}).then(function (response) {
|
||||
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();
|
||||
$("#user-award-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#user-award-form > #results').append(
|
||||
$("#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();
|
||||
$("#user-mail-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#user-mail-form > #results').append(
|
||||
$("#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',
|
||||
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',
|
||||
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),
|
||||
|
@ -196,17 +218,19 @@ $(document).ready(function () {
|
|||
title: "Mark Correct",
|
||||
body: body,
|
||||
success: function() {
|
||||
CTFd.fetch('/api/v1/submissions', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
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 () {
|
|||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,8 +1,12 @@
|
|||
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;
|
||||
|
||||
|
@ -28,86 +32,95 @@ 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) {
|
||||
|
@ -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() {
|
||||
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'));
|
||||
Plotly.Plots.resize(document.getElementById("keys-pie-graph"));
|
||||
Plotly.Plots.resize(document.getElementById("categories-pie-graph"));
|
||||
Plotly.Plots.resize(document.getElementById("score-graph"));
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
$(document).ready(function() {
|
||||
$('#user-info-form').submit(function(e){
|
||||
$("#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();
|
||||
$("#user-info-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#user-info-form > #results').append(
|
||||
$("#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");
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
|
@ -1,40 +1,42 @@
|
|||
$(document).ready(function() {
|
||||
$('#user-info-form').submit(function (e) {
|
||||
$("#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();
|
||||
$("#user-info-form > #results").empty();
|
||||
Object.keys(response.errors).forEach(function(key, index) {
|
||||
$('#user-info-form > #results').append(
|
||||
$("#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");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
$(document).ready(function () {
|
||||
|
||||
});
|
||||
$(document).ready(function() {});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
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);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ function loadchal(id) {
|
|||
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!",
|
||||
|
@ -35,26 +35,28 @@ function updateChalWindow(obj) {
|
|||
|
||||
$.getScript(script_root + obj.script, function() {
|
||||
$.get(script_root + obj.template, function(template_data) {
|
||||
$('#challenge-window').empty();
|
||||
$("#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,10 +65,10 @@ 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);
|
||||
$("#submit-key").addClass("disabled-button");
|
||||
$("#submit-key").prop("disabled", true);
|
||||
window.challenge.submit(function(data) {
|
||||
renderSubmissionResponse(data);
|
||||
loadchals(function() {
|
||||
|
@ -83,22 +85,28 @@ function updateChalWindow(obj) {
|
|||
|
||||
$(".input-field").bind({
|
||||
focus: function() {
|
||||
$(this).parent().addClass('input--filled');
|
||||
$(this)
|
||||
.parent()
|
||||
.addClass("input--filled");
|
||||
$label = $(this).siblings(".input-label");
|
||||
},
|
||||
blur: function() {
|
||||
if ($(this).val() === '') {
|
||||
$(this).parent().removeClass('input--filled');
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -110,22 +118,28 @@ $("#submission-input").keyup(function (event) {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
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");
|
||||
|
@ -133,29 +147,45 @@ function renderSubmissionResponse(response, cb) {
|
|||
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");
|
||||
|
@ -164,9 +194,9 @@ function renderSubmissionResponse(response, cb) {
|
|||
}, 3000);
|
||||
}
|
||||
setTimeout(function() {
|
||||
$('.alert').slideUp();
|
||||
$('#submit-key').removeClass("disabled-button");
|
||||
$('#submit-key').prop('disabled', false);
|
||||
$(".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,19 +242,28 @@ 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
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -231,7 +273,7 @@ function loadchals(cb) {
|
|||
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();
|
||||
|
@ -318,8 +378,10 @@ var load_location_hash = function () {
|
|||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -333,14 +395,14 @@ $(function () {
|
|||
});
|
||||
});
|
||||
|
||||
$('.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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,53 +1,56 @@
|
|||
var modal = '<div class="modal fade" tabindex="-1" role="dialog">' +
|
||||
var modal =
|
||||
'<div class="modal fade" tabindex="-1" role="dialog">' +
|
||||
' <div class="modal-dialog" role="document">' +
|
||||
' <div class="modal-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">×</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) {
|
||||
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;
|
||||
|
@ -56,23 +59,26 @@ function ezal(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() {
|
||||
args.success();
|
||||
});
|
||||
|
||||
obj.modal('show');
|
||||
obj.modal("show");
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
@ -80,16 +86,16 @@ function ezq(args){
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
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) {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function unlock(params) {
|
||||
return CTFd.fetch('/api/v1/unlocks', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
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) {
|
||||
|
@ -29,7 +28,7 @@ function unlock(params){
|
|||
function loadhint(hintid) {
|
||||
var md = window.markdownit({
|
||||
html: true,
|
||||
linkify: true,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
hint(hintid).then(function(response) {
|
||||
|
|
|
@ -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;
|
||||
var modalCount = $(".modal:visible").length;
|
||||
if (modalCount > 0)
|
||||
$('.modal-backdrop').not(':first').addClass('hidden');
|
||||
$(".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) {
|
||||
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)));
|
||||
$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);
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
function updatescores() {
|
||||
$.get(script_root + '/api/v1/scoreboard', function (response) {
|
||||
$.get(script_root + "/api/v1/scoreboard", function(response) {
|
||||
var teams = response.data;
|
||||
var table = $('#scoreboard tbody');
|
||||
var table = $("#scoreboard tbody");
|
||||
table.empty();
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
var row = "<tr>\n" +
|
||||
"<th scope=\"row\" class=\"text-center\">{0}</th>".format(i + 1) +
|
||||
"<td><a href=\"{0}/team/{1}\">{2}</a></td>".format(script_root, teams['standings'][i].id, htmlentities(teams['standings'][i].team)) +
|
||||
"<td>{0}</td>".format(teams['standings'][i].score) +
|
||||
var row =
|
||||
"<tr>\n" +
|
||||
'<th scope="row" class="text-center">{0}</th>'.format(i + 1) +
|
||||
'<td><a href="{0}/team/{1}">{2}</a></td>'.format(
|
||||
script_root,
|
||||
teams["standings"][i].id,
|
||||
htmlentities(teams["standings"][i].team)
|
||||
) +
|
||||
"<td>{0}</td>".format(teams["standings"][i].score) +
|
||||
"</tr>";
|
||||
table.append(row);
|
||||
}
|
||||
|
@ -17,9 +22,11 @@ function updatescores () {
|
|||
function cumulativesum(arr) {
|
||||
var result = arr.concat();
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
result[i] = arr.slice(0, i + 1).reduce(function(p, i){ return p + i; });
|
||||
result[i] = arr.slice(0, i + 1).reduce(function(p, i) {
|
||||
return p + i;
|
||||
});
|
||||
}
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
function UTCtoDate(utc) {
|
||||
|
@ -29,12 +36,12 @@ function UTCtoDate(utc){
|
|||
}
|
||||
|
||||
function scoregraph() {
|
||||
$.get(script_root + '/api/v1/scoreboard/top/10', function( response ) {
|
||||
$.get(script_root + "/api/v1/scoreboard/top/10", function(response) {
|
||||
var places = response.data;
|
||||
|
||||
if (Object.keys(places).length === 0) {
|
||||
// Replace spinner
|
||||
$('#score-graph').html(
|
||||
$("#score-graph").html(
|
||||
'<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>'
|
||||
);
|
||||
return;
|
||||
|
@ -45,56 +52,57 @@ function scoregraph () {
|
|||
for (var i = 0; i < teams.length; i++) {
|
||||
var team_score = [];
|
||||
var times = [];
|
||||
for(var j = 0; j < places[teams[i]]['solves'].length; j++){
|
||||
team_score.push(places[teams[i]]['solves'][j].value);
|
||||
var date = moment(places[teams[i]]['solves'][j].date);
|
||||
for (var j = 0; j < places[teams[i]]["solves"].length; j++) {
|
||||
team_score.push(places[teams[i]]["solves"][j].value);
|
||||
var date = moment(places[teams[i]]["solves"][j].date);
|
||||
times.push(date.toDate());
|
||||
}
|
||||
team_score = cumulativesum(team_score);
|
||||
var trace = {
|
||||
x: times,
|
||||
y: team_score,
|
||||
mode: 'lines+markers',
|
||||
name: places[teams[i]]['name'],
|
||||
mode: "lines+markers",
|
||||
name: places[teams[i]]["name"],
|
||||
marker: {
|
||||
color: colorhash(places[teams[i]]['name'] + places[teams[i]]['id']),
|
||||
color: colorhash(places[teams[i]]["name"] + places[teams[i]]["id"])
|
||||
},
|
||||
line: {
|
||||
color: colorhash(places[teams[i]]['name'] + places[teams[i]]['id']),
|
||||
color: colorhash(places[teams[i]]["name"] + places[teams[i]]["id"])
|
||||
}
|
||||
};
|
||||
traces.push(trace);
|
||||
}
|
||||
|
||||
traces.sort(function(a, b) {
|
||||
var scorediff = b['y'][b['y'].length - 1] - a['y'][a['y'].length - 1];
|
||||
var scorediff = b["y"][b["y"].length - 1] - a["y"][a["y"].length - 1];
|
||||
if (!scorediff) {
|
||||
return a['x'][a['x'].length - 1] - b['x'][b['x'].length - 1];
|
||||
return a["x"][a["x"].length - 1] - b["x"][b["x"].length - 1];
|
||||
}
|
||||
return scorediff;
|
||||
});
|
||||
|
||||
var layout = {
|
||||
title: 'Top 10 Teams',
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
hovermode: 'closest',
|
||||
title: "Top 10 Teams",
|
||||
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(); // Remove spinners
|
||||
document.getElementById('score-graph').fn = 'CTFd_scoreboard_' + (new Date).toISOString().slice(0,19);
|
||||
Plotly.newPlot('score-graph', traces, layout, {
|
||||
$("#score-graph").empty(); // Remove spinners
|
||||
document.getElementById("score-graph").fn =
|
||||
"CTFd_scoreboard_" + new Date().toISOString().slice(0, 19);
|
||||
Plotly.newPlot("score-graph", traces, layout, {
|
||||
// displayModeBar: false,
|
||||
displaylogo: false
|
||||
});
|
||||
|
@ -110,5 +118,5 @@ setInterval(update, 300000); // Update scores every 5 minutes
|
|||
scoregraph();
|
||||
|
||||
window.onresize = function() {
|
||||
Plotly.Plots.resize(document.getElementById('score-graph'));
|
||||
Plotly.Plots.resize(document.getElementById("score-graph"));
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue