diff --git a/.travis.yml b/.travis.yml index 202fc150..49a25aa5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,3 +16,5 @@ before_script: script: - pep8 --ignore E501,E712 CTFd/ tests/ - nosetests -d +after_success: + - codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index 650b65d0..9a821387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,50 @@ +1.1.0 / TBD +================== + +**Themes** + +* The original theme has been replaced by the core theme. The core theme is written in Bootstrap v4.0.0-beta.2 and significantly reduces the amount of custom styles/classes used. +* Challenges can now be previewed from the admin panel. +* The modals to modify files, flags, tags, and hints are no longer controlled by Challenge Type Plugins and are defined in CTFd itself. +* The admin graphs and admin statistics pages have been combined. +* Percentage solved for challenges has been moved to the new statistics page. +* The scoregraph on the scoreboard has been cleaned up to better fit the page width. +* Score graphs now use user-specific colors. +* Hints can now be previewed from the admin panel. +* Various confirmation modals have been replaced with `ezq.js`, a simple Bootstrap modal wrapper. +* Fixed a bug where challenge buttons on the challenge board would load before being styled as solved. + +**Database** + +* `Keys.key_type` has been renamed to `Keys.type`. +* Pages Improvements: + * Page previews are now independent of the editor page. + * Pages now have a title which refer to the link's name on the navbar. + * Pages can now be drafts which cannot be seen by regular users. + * Pages can now require authentication to view. + * CSS editing has been moved to the config panel. + +**Challenge Type Plugins** + +* Handlebars has been replaced with Nunjucks which means Challenge Type Plugins using Handlebars must be updated to work with 1.1.0 + +**General** + +* CTFs can now be paused to prevent solves. +* A new authed_only decorator is available to restrict pages to logged-in users. +* CTFd will now check for updates on start against `versioning.ctfd.io`. Admins will see in the admin panel that CTFd can be updated. +* A ratelimit function has been implemented. Authentication and email related functions are now ratelimited. +* Code coverage from codecov. +* Admins can now see the reason why an email to a team failed to send. + + 1.0.5 / 2017-10-25 ================== * Challenge Type Plugins now have a static interface which should be implemented by all challenge types. * Challenge Type Plugins are now self-contained in the plugin system meaning you no longer need to manipulate themes in order to register Challenge Type Plugins. * Challenge Type plugins should implement the create, read, update, delete, attempt, solve, and fail static methods. - * Challenge Type plugins now use strings for both their IDs and names. + * Challenge Type plugins now use strings for both their IDs and names. * Challenge Type plugins now contain references to their related modal template files. * Plugins can now register directories and files to be served by CTFd * `CTFd.plugins.register_plugin_assets_directory` registers a directory to be served diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 36f48609..65f31dbe 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -10,7 +10,7 @@ from sqlalchemy_utils import database_exists, create_database from sqlalchemy_utils.functions import get_tables from six.moves import input -from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp +from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp, update_check from CTFd import utils # Hack to support Unicode in Python 2 properly @@ -18,7 +18,7 @@ if sys.version_info[0] < 3: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '1.0.5' +__version__ = '1.1.0a1' class ThemeLoader(FileSystemLoader): @@ -33,7 +33,7 @@ class ThemeLoader(FileSystemLoader): # Check if the template requested is for the admin panel if template.startswith('admin/'): - template = template.lstrip('admin/') + template = template[6:] # Strip out admin/ template = "/".join(['admin', 'templates', template]) return super(ThemeLoader, self).get_source(environment, template) @@ -109,10 +109,13 @@ def create_app(config='CTFd.config.Config'): exit() app.db = db + app.VERSION = __version__ cache.init_app(app) app.cache = cache + update_check() + version = utils.get_config('ctf_version') # Upgrading from an older version of CTFd @@ -123,7 +126,7 @@ def create_app(config='CTFd.config.Config'): exit() if not utils.get_config('ctf_theme'): - utils.set_config('ctf_theme', 'original') + utils.set_config('ctf_theme', 'core') from CTFd.views import views from CTFd.challenges import challenges diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index 93b05496..a26a789c 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -10,7 +10,8 @@ from sqlalchemy.sql import not_ from sqlalchemy.exc import IntegrityError from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf -from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError +from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, \ + DatabaseError from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.admin.statistics import admin_statistics @@ -22,14 +23,13 @@ from CTFd.admin.teams import admin_teams from CTFd import utils - admin = Blueprint('admin', __name__) @admin.route('/admin', methods=['GET']) def admin_view(): if is_admin(): - return redirect(url_for('admin_statistics.admin_graphs')) + return redirect(url_for('admin_statistics.admin_stats')) return redirect(url_for('auth.login')) @@ -107,63 +107,55 @@ def admin_config(): freeze = int(request.form['freeze']) try: - view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) - view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None)) - hide_scores = bool(request.form.get('hide_scores', None)) - prevent_registration = bool(request.form.get('prevent_registration', None)) - prevent_name_change = bool(request.form.get('prevent_name_change', None)) - view_after_ctf = bool(request.form.get('view_after_ctf', None)) - verify_emails = bool(request.form.get('verify_emails', None)) - mail_tls = bool(request.form.get('mail_tls', None)) - mail_ssl = bool(request.form.get('mail_ssl', None)) - mail_useauth = bool(request.form.get('mail_useauth', None)) - workshop_mode = bool(request.form.get('workshop_mode', None)) - except (ValueError, TypeError): - view_challenges_unregistered = None - view_scoreboard_if_authed = None - hide_scores = None - prevent_registration = None - prevent_name_change = None - view_after_ctf = None - verify_emails = None - mail_tls = None - mail_ssl = None - mail_useauth = None - workshop_mode = None + # Set checkbox config values + view_challenges_unregistered = 'view_challenges_unregistered' in request.form + view_scoreboard_if_authed = 'view_scoreboard_if_authed' in request.form + hide_scores = 'hide_scores' in request.form + prevent_registration = 'prevent_registration' in request.form + prevent_name_change = 'prevent_name_change' in request.form + view_after_ctf = 'view_after_ctf' in request.form + verify_emails = 'verify_emails' in request.form + mail_tls = 'mail_tls' in request.form + mail_ssl = 'mail_ssl' in request.form + mail_useauth = 'mail_useauth' in request.form + workshop_mode = 'workshop_mode' in request.form + paused = 'paused' in request.form finally: - view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered) - view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) - hide_scores = utils.set_config('hide_scores', hide_scores) - prevent_registration = utils.set_config('prevent_registration', prevent_registration) - prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change) - view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf) - verify_emails = utils.set_config('verify_emails', verify_emails) - mail_tls = utils.set_config('mail_tls', mail_tls) - mail_ssl = utils.set_config('mail_ssl', mail_ssl) - mail_useauth = utils.set_config('mail_useauth', mail_useauth) - workshop_mode = utils.set_config('workshop_mode', workshop_mode) + utils.set_config('view_challenges_unregistered', view_challenges_unregistered) + utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed) + utils.set_config('hide_scores', hide_scores) + utils.set_config('prevent_registration', prevent_registration) + utils.set_config('prevent_name_change', prevent_name_change) + utils.set_config('view_after_ctf', view_after_ctf) + utils.set_config('verify_emails', verify_emails) + utils.set_config('mail_tls', mail_tls) + utils.set_config('mail_ssl', mail_ssl) + utils.set_config('mail_useauth', mail_useauth) + utils.set_config('workshop_mode', workshop_mode) + utils.set_config('paused', paused) - mail_server = utils.set_config("mail_server", request.form.get('mail_server', None)) - mail_port = utils.set_config("mail_port", request.form.get('mail_port', None)) + utils.set_config("mail_server", request.form.get('mail_server', None)) + utils.set_config("mail_port", request.form.get('mail_port', None)) if request.form.get('mail_useauth', None) and (request.form.get('mail_u', None) or request.form.get('mail_p', None)): if len(request.form.get('mail_u')) > 0: - mail_username = utils.set_config("mail_username", request.form.get('mail_u', None)) + utils.set_config("mail_username", request.form.get('mail_u', None)) if len(request.form.get('mail_p')) > 0: - mail_password = utils.set_config("mail_password", request.form.get('mail_p', None)) + utils.set_config("mail_password", request.form.get('mail_p', None)) elif request.form.get('mail_useauth', None) is None: utils.set_config("mail_username", None) utils.set_config("mail_password", None) - ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None)) - ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) + utils.set_config("ctf_name", request.form.get('ctf_name', None)) + utils.set_config("ctf_theme", request.form.get('ctf_theme', None)) + utils.set_config('css', request.form.get('css', None)) - mailfrom_addr = utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None)) - mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) - mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) + utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None)) + utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) + utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) - db_freeze = utils.set_config("freeze", freeze) + utils.set_config("freeze", freeze) db_start = Config.query.filter_by(key='start').first() db_start.value = start @@ -180,11 +172,13 @@ def admin_config(): cache.clear() return redirect(url_for('admin.admin_config')) - with app.app_context(): - cache.clear() + # Clear the cache so that we don't get stale values + cache.clear() + ctf_name = utils.get_config('ctf_name') ctf_theme = utils.get_config('ctf_theme') hide_scores = utils.get_config('hide_scores') + css = utils.get_config('css') mail_server = utils.get_config('mail_server') mail_port = utils.get_config('mail_port') @@ -211,6 +205,7 @@ def admin_config(): verify_emails = utils.get_config('verify_emails') workshop_mode = utils.get_config('workshop_mode') + paused = utils.get_config('paused') db.session.commit() db.session.close() @@ -218,28 +213,32 @@ def admin_config(): themes = utils.get_themes() themes.remove(ctf_theme) - return render_template('admin/config.html', - ctf_name=ctf_name, - ctf_theme_config=ctf_theme, - start=start, - end=end, - freeze=freeze, - hide_scores=hide_scores, - mail_server=mail_server, - mail_port=mail_port, - mail_useauth=mail_useauth, - mail_username=mail_username, - mail_password=mail_password, - mail_tls=mail_tls, - mail_ssl=mail_ssl, - view_challenges_unregistered=view_challenges_unregistered, - view_scoreboard_if_authed=view_scoreboard_if_authed, - prevent_registration=prevent_registration, - mailfrom_addr=mailfrom_addr, - mg_base_url=mg_base_url, - mg_api_key=mg_api_key, - prevent_name_change=prevent_name_change, - verify_emails=verify_emails, - view_after_ctf=view_after_ctf, - themes=themes, - workshop_mode=workshop_mode) + return render_template( + 'admin/config.html', + ctf_name=ctf_name, + ctf_theme_config=ctf_theme, + css=css, + start=start, + end=end, + freeze=freeze, + hide_scores=hide_scores, + mail_server=mail_server, + mail_port=mail_port, + mail_useauth=mail_useauth, + mail_username=mail_username, + mail_password=mail_password, + mail_tls=mail_tls, + mail_ssl=mail_ssl, + view_challenges_unregistered=view_challenges_unregistered, + view_scoreboard_if_authed=view_scoreboard_if_authed, + prevent_registration=prevent_registration, + mailfrom_addr=mailfrom_addr, + mg_base_url=mg_base_url, + mg_api_key=mg_api_key, + prevent_name_change=prevent_name_change, + verify_emails=verify_emails, + view_after_ctf=view_after_ctf, + themes=themes, + workshop_mode=workshop_mode, + paused=paused + ) diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index 74ad5617..dc37d1d2 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -33,18 +33,8 @@ def admin_chals(): if request.method == 'POST': chals = Challenges.query.add_columns('id', 'type', 'name', 'value', 'description', 'category', 'hidden', 'max_attempts').order_by(Challenges.value).all() - teams_with_points = db.session.query(Solves.teamid).join(Teams).filter( - Teams.banned == False).group_by(Solves.teamid).count() - json_data = {'game': []} for x in chals: - solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter( - Solves.chalid == x[1], Teams.banned == False).count() - if teams_with_points > 0: - percentage = (float(solve_count) / float(teams_with_points)) - else: - percentage = 0.0 - type_class = CHALLENGE_CLASSES.get(x.type) type_name = type_class.name if type_class else None @@ -58,7 +48,6 @@ def admin_chals(): 'max_attempts': x.max_attempts, 'type': x.type, 'type_name': type_name, - 'percentage_solved': percentage, 'type_data': { 'id': type_class.id, 'name': type_class.name, @@ -70,17 +59,23 @@ def admin_chals(): db.session.close() return jsonify(json_data) else: - return render_template('admin/chals.html') + challenges = Challenges.query.all() + return render_template('admin/challenges.html', challenges=challenges) -@admin_challenges.route('/admin/chals/', methods=['GET', 'POST']) +@admin_challenges.route('/admin/chal/', methods=['GET', 'POST']) @admins_only def admin_chal_detail(chalid): + chal = Challenges.query.filter_by(id=chalid).first_or_404() + chal_class = get_chal_class(chal.type) + if request.method == 'POST': - pass + status, message = chal_class.attempt(chal, request) + if status: + return jsonify({'status': 1, 'message': message}) + else: + return jsonify({'status': 0, 'message': message}) elif request.method == 'GET': - chal = Challenges.query.filter_by(id=chalid).first_or_404() - chal_class = get_chal_class(chal.type) obj, data = chal_class.read(chal) return jsonify(data) @@ -211,11 +206,11 @@ def admin_get_values(chalid, prop): chal_keys = Keys.query.filter_by(chal=challenge.id).all() json_data = {'keys': []} for x in chal_keys: - key_class = get_key_class(x.key_type) + key_class = get_key_class(x.type) json_data['keys'].append({ 'id': x.id, 'key': x.flag, - 'type': x.key_type, + 'type': x.type, 'type_name': key_class.name, 'templates': key_class.templates, }) diff --git a/CTFd/admin/keys.py b/CTFd/admin/keys.py index c3de516a..cdd25e25 100644 --- a/CTFd/admin/keys.py +++ b/CTFd/admin/keys.py @@ -35,13 +35,13 @@ def admin_keys_view(keyid): if request.method == 'GET': if keyid: saved_key = Keys.query.filter_by(id=keyid).first_or_404() - key_class = get_key_class(saved_key.key_type) + key_class = get_key_class(saved_key.type) json_data = { 'id': saved_key.id, 'key': saved_key.flag, 'data': saved_key.data, 'chal': saved_key.chal, - 'type': saved_key.key_type, + 'type': saved_key.type, 'type_name': key_class.name, 'templates': key_class.templates, } @@ -60,7 +60,7 @@ def admin_keys_view(keyid): k = Keys.query.filter_by(id=keyid).first() k.flag = flag k.data = data - k.key_type = key_type + k.type = key_type db.session.commit() db.session.close() return '1' diff --git a/CTFd/admin/pages.py b/CTFd/admin/pages.py index 9751faa0..55a256d4 100644 --- a/CTFd/admin/pages.py +++ b/CTFd/admin/pages.py @@ -1,5 +1,5 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, cache +from CTFd.utils import admins_only, is_admin, cache, markdown from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd import utils @@ -7,65 +7,88 @@ from CTFd import utils admin_pages = Blueprint('admin_pages', __name__) -@admin_pages.route('/admin/css', methods=['GET', 'POST']) -@admins_only -def admin_css(): - if request.method == 'POST': - css = request.form['css'] - css = utils.set_config('css', css) - with app.app_context(): - cache.clear() - return '1' - return '0' - - @admin_pages.route('/admin/pages', methods=['GET', 'POST']) @admins_only def admin_pages_view(): - route = request.args.get('route') + page_id = request.args.get('id') + page_op = request.args.get('operation') - if request.method == 'GET' and request.args.get('mode') == 'create': + if request.method == 'GET' and page_op == 'preview': + page = Pages.query.filter_by(id=page_id).first_or_404() + return render_template('page.html', content=markdown(page.html)) + + if request.method == 'GET' and page_op == 'create': return render_template('admin/editor.html') - if route and request.method == 'GET': - page = Pages.query.filter_by(route=route).first() + if page_id and request.method == 'GET': + page = Pages.query.filter_by(id=page_id).first() return render_template('admin/editor.html', page=page) if request.method == 'POST': + page_form_id = request.form.get('id') + title = request.form['title'] html = request.form['html'] route = request.form['route'].lstrip('/') - page = Pages.query.filter_by(route=route).first() + auth_required = 'auth_required' in request.form + + if page_op == 'preview': + page = Pages(title, route, html, draft=False) + return render_template('page.html', content=markdown(page.html)) + + page = Pages.query.filter_by(id=page_form_id).first() + errors = [] if not route: errors.append('Missing URL route') + if errors: - page = Pages(html, route) + page = Pages(title, html, route) return render_template('/admin/editor.html', page=page) + if page: + page.title = title page.route = route page.html = html + page.auth_required = auth_required + + if page_op == 'publish': + page.draft = False + db.session.commit() db.session.close() - with app.app_context(): - cache.clear() - return redirect(url_for('admin_pages.admin_pages_view')) - page = Pages(route, html) + + cache.clear() + + return jsonify({ + 'result': 'success', + 'operation': page_op + }) + + if page_op == 'publish': + page = Pages(title, route, html, draft=False, auth_required=auth_required) + elif page_op == 'save': + page = Pages(title, route, html, auth_required=auth_required) + db.session.add(page) db.session.commit() db.session.close() - with app.app_context(): - cache.clear() - return redirect(url_for('admin_pages.admin_pages_view')) + + cache.clear() + + return jsonify({ + 'result': 'success', + 'operation': page_op + }) pages = Pages.query.all() - return render_template('admin/pages.html', routes=pages, css=utils.get_config('css')) + return render_template('admin/pages.html', pages=pages) @admin_pages.route('/admin/pages/delete', methods=['POST']) @admins_only def delete_page(): - route = request.form['route'] - page = Pages.query.filter_by(route=route).first_or_404() + id = request.form['id'] + page = Pages.query.filter_by(id=id).first_or_404() db.session.delete(page) db.session.commit() db.session.close() diff --git a/CTFd/admin/scoreboard.py b/CTFd/admin/scoreboard.py index 8f86fc4d..6057f961 100644 --- a/CTFd/admin/scoreboard.py +++ b/CTFd/admin/scoreboard.py @@ -20,7 +20,9 @@ def admin_scoreboard_view(): def admin_scores(): score = db.func.sum(Challenges.value).label('score') quickest = db.func.max(Solves.date).label('quickest') - teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest) + teams = db.session.query(Solves.teamid, Teams.name, score)\ + .join(Teams).join(Challenges).filter(Teams.banned == False)\ + .group_by(Solves.teamid).order_by(score.desc(), quickest) db.session.close() json_data = {'teams': []} for i, x in enumerate(teams): diff --git a/CTFd/admin/statistics.py b/CTFd/admin/statistics.py index 73febb8e..1d553137 100644 --- a/CTFd/admin/statistics.py +++ b/CTFd/admin/statistics.py @@ -7,12 +7,6 @@ from CTFd import utils admin_statistics = Blueprint('admin_statistics', __name__) -@admin_statistics.route('/admin/graphs') -@admins_only -def admin_graphs(): - return render_template('admin/graphs.html') - - @admin_statistics.route('/admin/graphs/') @admins_only def admin_graph(graph_type): @@ -32,15 +26,47 @@ def admin_graph(graph_type): for chal, count, name in solves: json_data[name] = count return jsonify(json_data) + elif graph_type == "solve-percentages": + chals = Challenges.query.add_columns('id', 'name', 'hidden', 'max_attempts').order_by(Challenges.value).all() + + teams_with_points = db.session.query(Solves.teamid)\ + .join(Teams)\ + .filter(Teams.banned == False)\ + .group_by(Solves.teamid)\ + .count() + + percentage_data = [] + for x in chals: + solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id)\ + .filter(Solves.chalid == x[1], Teams.banned == False)\ + .count() + + if teams_with_points > 0: + percentage = (float(solve_count) / float(teams_with_points)) + else: + percentage = 0.0 + + percentage_data.append({ + 'id': x.id, + 'name': x.name, + 'percentage': percentage, + }) + + percentage_data = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True) + json_data = {'percentages': percentage_data} + return jsonify(json_data) @admin_statistics.route('/admin/statistics', methods=['GET']) @admins_only def admin_stats(): teams_registered = db.session.query(db.func.count(Teams.id)).first()[0] - wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0] - solve_count = db.session.query(db.func.count(Solves.id)).first()[0] + + wrong_count = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count() + solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count() + challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0] + ip_count = db.session.query(db.func.count(Tracking.ip.distinct())).first()[0] solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \ .join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \ @@ -61,13 +87,17 @@ def admin_stats(): db.session.commit() db.session.close() - return render_template('admin/statistics.html', team_count=teams_registered, - wrong_count=wrong_count, - solve_count=solve_count, - challenge_count=challenge_count, - solve_data=solve_data, - most_solved=most_solved, - least_solved=least_solved) + return render_template( + 'admin/statistics.html', + team_count=teams_registered, + ip_count=ip_count, + wrong_count=wrong_count, + solve_count=solve_count, + challenge_count=challenge_count, + solve_data=solve_data, + most_solved=most_solved, + least_solved=least_solved + ) @admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET']) diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index 60b4b1b9..a1674d09 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -1,5 +1,5 @@ from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint -from CTFd.utils import admins_only, is_admin, cache +from CTFd.utils import admins_only, is_admin, cache, ratelimit from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from passlib.hash import bcrypt_sha256 from sqlalchemy.sql import not_ @@ -184,13 +184,21 @@ def admin_team(teamid): @admin_teams.route('/admin/team//mail', methods=['POST']) @admins_only +@ratelimit(method="POST", limit=10, interval=60) def email_user(teamid): - message = request.form.get('msg', None) - team = Teams.query.filter(Teams.id == teamid).first() - if message and team: - if utils.sendmail(team.email, message): - return '1' - return '0' + msg = request.form.get('msg', None) + team = Teams.query.filter(Teams.id == teamid).first_or_404() + if msg and team: + result, response = utils.sendmail(team.email, msg) + return jsonify({ + 'result': result, + 'message': response + }) + else: + return jsonify({ + 'result': False, + 'message': "Missing information" + }) @admin_teams.route('/admin/team//ban', methods=['POST']) @@ -236,6 +244,7 @@ def delete_team(teamid): def admin_solves(teamid="all"): if teamid == "all": solves = Solves.query.all() + awards = [] else: solves = Solves.query.filter_by(teamid=teamid).all() awards = Awards.query.filter_by(teamid=teamid).all() diff --git a/CTFd/auth.py b/CTFd/auth.py index e73471ba..d3e708e3 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -9,12 +9,14 @@ from passlib.hash import bcrypt_sha256 from CTFd.models import db, Teams from CTFd import utils +from CTFd.utils import ratelimit auth = Blueprint('auth', __name__) @auth.route('/confirm', methods=['POST', 'GET']) @auth.route('/confirm/', methods=['GET']) +@ratelimit(method="POST", limit=10, interval=60) def confirm_user(data=None): if not utils.get_config('verify_emails'): # If the CTF doesn't care about confirming email addresses then redierct to challenges @@ -75,6 +77,7 @@ def confirm_user(data=None): @auth.route('/reset_password', methods=['POST', 'GET']) @auth.route('/reset_password/', methods=['POST', 'GET']) +@ratelimit(method="POST", limit=10, interval=60) def reset_password(data=None): logger = logging.getLogger('logins') if data is not None and request.method == "GET": @@ -115,16 +118,8 @@ def reset_password(data=None): 'reset_password.html', errors=['If that account exists you will receive an email, please check your inbox'] ) - s = TimedSerializer(app.config['SECRET_KEY']) - token = s.dumps(team.name) - text = """ -Did you initiate a password reset? -{0}/{1} - -""".format(url_for('auth.reset_password', _external=True), utils.base64encode(token, urlencode=True)) - - utils.sendmail(email, text) + utils.forgot_password(email, team.name) return render_template( 'reset_password.html', @@ -134,6 +129,7 @@ Did you initiate a password reset? @auth.route('/register', methods=['POST', 'GET']) +@ratelimit(method="POST", limit=10, interval=5) def register(): logger = logging.getLogger('regs') if not utils.can_register(): @@ -209,6 +205,7 @@ def register(): @auth.route('/login', methods=['POST', 'GET']) +@ratelimit(method="POST", limit=10, interval=5) def login(): logger = logging.getLogger('logins') if request.method == 'POST': diff --git a/CTFd/challenges.py b/CTFd/challenges.py index f7831155..07fb9404 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -18,8 +18,9 @@ challenges = Blueprint('challenges', __name__) @challenges.route('/hints/', methods=['GET', 'POST']) def hints_view(hintid): - if not utils.ctf_started(): - abort(403) + if utils.ctf_started() is False: + if utils.is_admin() is False: + abort(403) hint = Hints.query.filter_by(id=hintid).first_or_404() chal = Challenges.query.filter_by(id=hint.chal).first() unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first() @@ -37,7 +38,7 @@ def hints_view(hintid): }) elif request.method == 'POST': if unlock is None: # The user does not have an unlock. - if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()): + if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()) or utils.is_admin() is True: # It's ctftime or the CTF has ended (but we allow views after) team = Teams.query.filter_by(id=session['id']).first() if team.score() < hint.cost: @@ -68,9 +69,12 @@ def hints_view(hintid): @challenges.route('/challenges', methods=['GET']) def challenges_view(): + infos = [] errors = [] start = utils.get_config('start') or 0 end = utils.get_config('end') or 0 + if utils.ctf_paused(): + infos.append('{} is paused'.format(utils.ctf_name())) if not utils.is_admin(): # User is not an admin if not utils.ctftime(): # It is not CTF time @@ -81,7 +85,7 @@ def challenges_view(): errors.append('{} has not started yet'.format(utils.ctf_name())) if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf(): errors.append('{} has ended'.format(utils.ctf_name())) - return render_template('chals.html', errors=errors, start=int(start), end=int(end)) + return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) if utils.get_config('verify_emails'): if utils.authed(): @@ -93,7 +97,7 @@ def challenges_view(): errors.append('{} has not started yet'.format(utils.ctf_name())) if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf(): errors.append('{} has ended'.format(utils.ctf_name())) - return render_template('chals.html', errors=errors, start=int(start), end=int(end)) + return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end)) else: return redirect(url_for('auth.login', next='challenges')) @@ -311,6 +315,11 @@ def who_solved(chalid): @challenges.route('/chal/', methods=['POST']) def chal(chalid): + if utils.ctf_paused(): + return jsonify({ + 'status': 3, + 'message': '{} is paused'.format(utils.ctf_name()) + }) if utils.ctf_ended() and not utils.view_after_ctf(): abort(403) if not utils.user_can_view_challenges(): diff --git a/CTFd/config.py b/CTFd/config.py index 24f850ef..ad556ddb 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -121,6 +121,11 @@ class Config(object): else: CACHE_TYPE = 'simple' + ''' + UPDATE_CHECK specifies whether or not CTFd will check whether or not there is a new version of CTFd + ''' + UPDATE_CHECK = True + class TestingConfig(Config): SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA' @@ -129,3 +134,6 @@ class TestingConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://' SERVER_NAME = 'localhost' + UPDATE_CHECK = False + CACHE_REDIS_URL = None + CACHE_TYPE = 'simple' diff --git a/CTFd/models.py b/CTFd/models.py index 7121b65b..8334b4ad 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -30,12 +30,18 @@ db = SQLAlchemy() class Pages(db.Model): id = db.Column(db.Integer, primary_key=True) - route = db.Column(db.String(80), unique=True) + auth_required = db.Column(db.Boolean) + title = db.Column(db.String(80)) + route = db.Column(db.Text, unique=True) html = db.Column(db.Text) + draft = db.Column(db.Boolean) - def __init__(self, route, html): + def __init__(self, title, route, html, draft=True, auth_required=False): + self.title = title self.route = route self.html = html + self.draft = draft + self.auth_required = auth_required def __repr__(self): return "".format(self.route) @@ -131,14 +137,14 @@ class Files(db.Model): class Keys(db.Model): id = db.Column(db.Integer, primary_key=True) chal = db.Column(db.Integer, db.ForeignKey('challenges.id')) - key_type = db.Column(db.String(80)) + type = db.Column(db.String(80)) flag = db.Column(db.Text) data = db.Column(db.Text) - def __init__(self, chal, flag, key_type): + def __init__(self, chal, flag, type): self.chal = chal self.flag = flag - self.key_type = key_type + self.type = type def __repr__(self): return "".format(self.flag, self.chal) diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py index b5b8d9b4..6239ccec 100644 --- a/CTFd/plugins/__init__.py +++ b/CTFd/plugins/__init__.py @@ -9,11 +9,12 @@ from CTFd.utils import ( admins_only as admins_only_wrapper, override_template as utils_override_template, register_plugin_script as utils_register_plugin_script, - register_plugin_stylesheet as utils_register_plugin_stylesheet + register_plugin_stylesheet as utils_register_plugin_stylesheet, + pages as db_pages ) -Menu = namedtuple('Menu', ['name', 'route']) +Menu = namedtuple('Menu', ['title', 'route']) ADMIN_PLUGIN_MENU_BAR = [] USER_PAGE_MENU_BAR = [] @@ -82,7 +83,7 @@ def register_plugin_stylesheet(*args, **kwargs): utils_register_plugin_stylesheet(*args, **kwargs) -def register_admin_plugin_menu_bar(name, route): +def register_admin_plugin_menu_bar(title, route): """ Registers links on the Admin Panel menubar/navbar @@ -90,7 +91,7 @@ def register_admin_plugin_menu_bar(name, route): :param route: A string that is the href used by the link :return: """ - am = Menu(name=name, route=route) + am = Menu(title=title, route=route) ADMIN_PLUGIN_MENU_BAR.append(am) @@ -103,7 +104,7 @@ def get_admin_plugin_menu_bar(): return ADMIN_PLUGIN_MENU_BAR -def register_user_page_menu_bar(name, route): +def register_user_page_menu_bar(title, route): """ Registers links on the User side menubar/navbar @@ -111,7 +112,7 @@ def register_user_page_menu_bar(name, route): :param route: A string that is the href used by the link :return: """ - p = Menu(name=name, route=route) + p = Menu(title=title, route=route) USER_PAGE_MENU_BAR.append(p) @@ -121,7 +122,7 @@ def get_user_page_menu_bar(): :return: Returns a list of Menu namedtuples. They have name, and route attributes. """ - return USER_PAGE_MENU_BAR + return db_pages() + USER_PAGE_MENU_BAR def init_plugins(app): diff --git a/CTFd/plugins/challenges/__init__.py b/CTFd/plugins/challenges/__init__.py index 055a2ae6..ce3af7a4 100644 --- a/CTFd/plugins/challenges/__init__.py +++ b/CTFd/plugins/challenges/__init__.py @@ -14,10 +14,10 @@ class BaseChallenge(object): class CTFdStandardChallenge(BaseChallenge): id = "standard" # Unique identifier used to register challenges name = "standard" # Name of a challenge type - templates = { # Handlebars templates used for each aspect of challenge editing & viewing - 'create': '/plugins/challenges/assets/standard-challenge-create.hbs', - 'update': '/plugins/challenges/assets/standard-challenge-update.hbs', - 'modal': '/plugins/challenges/assets/standard-challenge-modal.hbs', + templates = { # Nunjucks templates used for each aspect of challenge editing & viewing + 'create': '/plugins/challenges/assets/standard-challenge-create.njk', + 'update': '/plugins/challenges/assets/standard-challenge-update.njk', + 'modal': '/plugins/challenges/assets/standard-challenge-modal.njk', } scripts = { # Scripts that are loaded when a template is loaded 'create': '/plugins/challenges/assets/standard-challenge-create.js', @@ -33,12 +33,10 @@ class CTFdStandardChallenge(BaseChallenge): :param request: :return: """ - files = request.files.getlist('files[]') - # Create challenge chal = Challenges( name=request.form['name'], - description=request.form['desc'], + description=request.form['description'], value=request.form['value'], category=request.form['category'], type=request.form['chaltype'] @@ -63,6 +61,7 @@ class CTFdStandardChallenge(BaseChallenge): db.session.commit() + files = request.files.getlist('files[]') for f in files: utils.upload_file(file=f, chalid=chal.id) @@ -105,7 +104,7 @@ class CTFdStandardChallenge(BaseChallenge): :return: """ challenge.name = request.form['name'] - challenge.description = request.form['desc'] + challenge.description = request.form['description'] challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0 challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0 challenge.category = request.form['category'] @@ -146,7 +145,7 @@ class CTFdStandardChallenge(BaseChallenge): provided_key = request.form['key'].strip() chal_keys = Keys.query.filter_by(chal=chal.id).all() for chal_key in chal_keys: - if get_key_class(chal_key.key_type).compare(chal_key.flag, provided_key): + if get_key_class(chal_key.type).compare(chal_key.flag, provided_key): return True, 'Correct' return False, 'Incorrect' diff --git a/CTFd/plugins/challenges/assets/standard-challenge-create.hbs b/CTFd/plugins/challenges/assets/standard-challenge-create.hbs deleted file mode 100644 index 87fcff4a..00000000 --- a/CTFd/plugins/challenges/assets/standard-challenge-create.hbs +++ /dev/null @@ -1,111 +0,0 @@ -
-
-
- - -
-
- - -
- - -
-
-
- - -
-
-
-
-
- -
- - -
-
-
-
- - -
-
-
-
- -
-
- -
-
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
- -
- -
- -
-
-
- - Attach multiple files using Control+Click or Cmd+Click. - -
-
-
- - -
- -
-
-
diff --git a/CTFd/plugins/challenges/assets/standard-challenge-create.njk b/CTFd/plugins/challenges/assets/standard-challenge-create.njk new file mode 100644 index 00000000..688c0fdd --- /dev/null +++ b/CTFd/plugins/challenges/assets/standard-challenge-create.njk @@ -0,0 +1,101 @@ +
+
+
+ + +
+
+ + +
+ + + +
+
+
+ + +
+
+
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ + + Attach multiple files using Control+Click or Cmd+Click. +
+ + + + +
+ +
+
+
diff --git a/CTFd/plugins/challenges/assets/standard-challenge-modal.hbs b/CTFd/plugins/challenges/assets/standard-challenge-modal.hbs deleted file mode 100644 index 29f90d49..00000000 --- a/CTFd/plugins/challenges/assets/standard-challenge-modal.hbs +++ /dev/null @@ -1,94 +0,0 @@ - \ No newline at end of file diff --git a/CTFd/plugins/challenges/assets/standard-challenge-modal.njk b/CTFd/plugins/challenges/assets/standard-challenge-modal.njk new file mode 100644 index 00000000..a9b4e47a --- /dev/null +++ b/CTFd/plugins/challenges/assets/standard-challenge-modal.njk @@ -0,0 +1,103 @@ + \ No newline at end of file diff --git a/CTFd/plugins/challenges/assets/standard-challenge-update.hbs b/CTFd/plugins/challenges/assets/standard-challenge-update.hbs deleted file mode 100644 index fc10fe5c..00000000 --- a/CTFd/plugins/challenges/assets/standard-challenge-update.hbs +++ /dev/null @@ -1,267 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/CTFd/plugins/challenges/assets/standard-challenge-update.js b/CTFd/plugins/challenges/assets/standard-challenge-update.js index 060bc557..4d7ee39a 100644 --- a/CTFd/plugins/challenges/assets/standard-challenge-update.js +++ b/CTFd/plugins/challenges/assets/standard-challenge-update.js @@ -1,232 +1,13 @@ -//http://stackoverflow.com/a/2648463 - wizardry! -String.prototype.format = String.prototype.f = function() { - var s = this, - i = arguments.length; - - while (i--) { - s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]); - } - return s; -}; - -function load_hint_modal(method, hintid){ - $('#hint-modal-hint').val(''); - $('#hint-modal-cost').val(''); - if (method == 'create'){ - $('#hint-modal-submit').attr('action', '/admin/hints'); - $('#hint-modal-title').text('Create Hint'); - $("#hint-modal").modal(); - } else if (method == 'update'){ - $.get(script_root + '/admin/hints/' + hintid, function(data){ - $('#hint-modal-submit').attr('action', '/admin/hints/' + hintid); - $('#hint-modal-hint').val(data.hint); - $('#hint-modal-cost').val(data.cost); - $('#hint-modal-title').text('Update Hint'); - $("#hint-modal-button").text('Update Hint'); - $("#hint-modal").modal(); - }); - } -} - -function submitkey(chal, key) { - $.post(script_root + "/admin/chal/" + chal, { - key: key, - nonce: $('#nonce').val() - }, function (data) { - alert(data) - }) -} - -function create_key(chal, key, key_type) { - $.post(script_root + "/admin/keys", { - chal: chal, - key: key, - key_type: key_type, - nonce: $('#nonce').val() - }, function (data) { - if (data == "1"){ - loadkeys(chal); - $("#create-keys").modal('toggle'); - } - }); -} - -function loadkeys(chal){ - $.get(script_root + '/admin/chal/' + chal + '/keys', function(data){ - $('#keys-chal').val(chal); - keys = $.parseJSON(JSON.stringify(data)); - keys = keys['keys']; - $('#current-keys').empty(); - $.get(script_root + "/themes/admin/static/js/templates/admin-keys-table.hbs", function(data){ - var template = Handlebars.compile(data); - var wrapper = {keys: keys, script_root: script_root}; - $('#current-keys').append(template(wrapper)); - }); - }); -} - -function updatekeys(){ - keys = []; - vals = []; - chal = $('#keys-chal').val() - $('.current-key').each(function(){ - keys.push($(this).val()); - }) - $('#current-keys input[name*="key_type"]:checked').each(function(){ - vals.push($(this).val()); - }) - $.post(script_root + '/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()}) - loadchal(chal, true) - $('#update-keys').modal('hide'); -} -function deletekey(key_id){ - $.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){ - if (data == "1") { - $('tr[name={0}]'.format(key_id)).remove(); - } - }); -} -function updatekey(){ - var key_id = $('#key-id').val(); - var chal = $('#keys-chal').val(); - var key_data = $('#key-data').val(); - var key_type = $('#key-type').val(); - var nonce = $('#nonce').val(); - $.post(script_root + '/admin/keys/'+key_id, { - 'chal':chal, - 'key':key_data, - 'key_type': key_type, - 'nonce': nonce - }, function(data){ - if (data == "1") { - loadkeys(chal); - $('#edit-keys').modal('toggle'); - } - }); -} - -function loadtags(chal){ - $('#tags-chal').val(chal) - $('#current-tags').empty() - $('#chal-tags').empty() - $.get(script_root + '/admin/tags/'+chal, function(data){ - tags = $.parseJSON(JSON.stringify(data)) - tags = tags['tags'] - for (var i = 0; i < tags.length; i++) { - tag = ""+tags[i].tag+"×" - $('#current-tags').append(tag) - }; - $('.delete-tag').click(function(e){ - deletetag(e.target.name) - $(e.target).parent().remove() - }); - }); -} - -function deletetag(tagid){ - $.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()}); -} +// function deletechal(chalid){ +// $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid}); +// } -function edithint(hintid){ - $.get(script_root + '/admin/hints/' + hintid, function(data){ - console.log(data); - }) -} -function deletehint(hintid){ - $.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){ - if (jqXHR.status == 204){ - var chalid = $('.chal-id').val(); - loadhints(chalid); - } - }); -} - - -function loadhints(chal){ - $.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){ - var table = $('#hintsboard > tbody'); - table.empty(); - for (var i = 0; i < data.hints.length; i++) { - var hint = data.hints[i] - var hint_row = "" + - "{0}".format(hint.hint) + - "{0}".format(hint.cost) + - "" + - "".format(hint.id)+ - "".format(hint.id)+ - "" + - ""; - table.append(hint_row); - } - }); -} - - -function deletechal(chalid){ - $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid}); -} - -function updatetags(){ - tags = []; - chal = $('#tags-chal').val() - $('#chal-tags > span > span').each(function(i, e){ - tags.push($(e).text()) - }); - $.post(script_root + '/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()}); - $('#update-tags').modal('toggle'); -} - -function updatefiles(){ - chal = $('#files-chal').val(); - var form = $('#update-files form')[0]; - var formData = new FormData(form); - $.ajax({ - url: script_root + '/admin/files/'+chal, - data: formData, - type: 'POST', - cache: false, - contentType: false, - processData: false, - success: function(data){ - form.reset(); - loadfiles(chal); - $('#update-files').modal('hide'); - } - }); -} - -function loadfiles(chal){ - $('#update-files form').attr('action', script_root+'/admin/files/'+chal) - $.get(script_root + '/admin/files/' + chal, function(data){ - $('#files-chal').val(chal) - files = $.parseJSON(JSON.stringify(data)); - files = files['files'] - $('#current-files').empty() - for(x=0; x'+''+filename+'Delete') - } - }); -} - -function deletefile(chal, file, elem){ - $.post(script_root + '/admin/files/' + chal,{ - 'nonce': $('#nonce').val(), - 'method': 'delete', - 'file': file - }, function (data){ - if (data == "1") { - elem.parent().remove() - } - }); -} $('#submit-key').click(function (e) { submitkey($('#chalid').val(), $('#answer').val()) @@ -237,42 +18,9 @@ $('#submit-keys').click(function (e) { $('#update-keys').modal('hide'); }); -$('#submit-tags').click(function (e) { - e.preventDefault(); - updatetags() -}); -$('#submit-files').click(function (e) { - e.preventDefault(); - updatefiles() -}); -$('#delete-chal form').submit(function(e){ - e.preventDefault(); - $.post(script_root + '/admin/chal/delete', $(this).serialize(), function(data){ - console.log(data) - if (data){ - loadchals(); - } - else { - alert('There was an error'); - } - }) - $("#delete-chal").modal("hide"); - $("#update-challenge").modal("hide"); -}); -$(".tag-insert").keyup(function (e) { - if (e.keyCode == 13) { - tag = $('.tag-insert').val() - tag = tag.replace(/'/g, ''); - if (tag.length > 0){ - tag = ""+tag+"×" - $('#chal-tags').append(tag) - } - $('.tag-insert').val("") - } -}); $('#limit_max_attempts').change(function() { if(this.checked) { @@ -295,67 +43,11 @@ $('#new-desc-edit').on('shown.bs.tab', function (event) { } }); -// Open New Challenge modal when New Challenge button is clicked -// $('.create-challenge').click(function (e) { -// $('#create-challenge').modal(); -// }); -$('#create-key').click(function(e){ - $.get(script_root + '/admin/key_types', function(data){ - $("#create-keys-select").empty(); - var option = ""; - $("#create-keys-select").append(option); - for (var key in data){ - var option = "".format(key, data[key]); - $("#create-keys-select").append(option); - } - $("#create-keys").modal(); - }); -}); - -$('#create-keys-select').change(function(){ - var key_type_name = $(this).find("option:selected").text(); - - $.get(script_root + '/admin/key_types/' + key_type_name, function(key_data){ - $.get(script_root + key_data.templates.create, function(template_data){ - var template = Handlebars.compile(template_data); - $("#create-keys-entry-div").html(template()); - $("#create-keys-button-div").show(); - }); - }) -}); - - -$('#create-keys-submit').click(function (e) { - e.preventDefault(); - var chalid = $('#create-keys').find('.chal-id').val(); - var key_data = $('#create-keys').find('input[name=key]').val(); - var key_type = $('#create-keys-select').val(); - create_key(chalid, key_data, key_type); -}); - - -$('#create-hint').click(function(e){ - e.preventDefault(); - load_hint_modal('create'); -}); - -$('#hint-modal-submit').submit(function (e) { - e.preventDefault(); - var params = {} - $(this).serializeArray().map(function(x){ - params[x.name] = x.value; - }); - $.post(script_root + $(this).attr('action'), params, function(data){ - loadhints(params['chal']); - }); - $("#hint-modal").modal('hide'); -}); - function loadchal(id, update) { - $.get(script_root + '/admin/chals/' + id, function(obj){ - $('#desc-write-link').click() // Switch to Write tab + $.get(script_root + '/admin/chal/' + id, function(obj){ + $('#desc-write-link').click(); // Switch to Write tab $('.chal-title').text(obj.name); $('.chal-name').val(obj.name); $('.chal-desc').val(obj.description); diff --git a/CTFd/plugins/challenges/assets/standard-challenge-update.njk b/CTFd/plugins/challenges/assets/standard-challenge-update.njk new file mode 100644 index 00000000..606c3664 --- /dev/null +++ b/CTFd/plugins/challenges/assets/standard-challenge-update.njk @@ -0,0 +1,93 @@ + \ No newline at end of file diff --git a/CTFd/plugins/keys/__init__.py b/CTFd/plugins/keys/__init__.py index 3a634400..92a0c22b 100644 --- a/CTFd/plugins/keys/__init__.py +++ b/CTFd/plugins/keys/__init__.py @@ -18,9 +18,9 @@ class BaseKey(object): class CTFdStaticKey(BaseKey): id = 0 name = "static" - templates = { # Handlebars templates used for key editing & viewing - 'create': '/plugins/keys/assets/static/create-static-modal.hbs', - 'update': '/plugins/keys/assets/static/edit-static-modal.hbs', + templates = { # Nunjucks templates used for key editing & viewing + 'create': '/plugins/keys/assets/static/create-static-modal.njk', + 'update': '/plugins/keys/assets/static/edit-static-modal.njk', } @staticmethod @@ -36,9 +36,9 @@ class CTFdStaticKey(BaseKey): class CTFdRegexKey(BaseKey): id = 1 name = "regex" - templates = { # Handlebars templates used for key editing & viewing - 'create': '/plugins/keys/assets/regex/create-regex-modal.hbs', - 'update': '/plugins/keys/assets/regex/edit-regex-modal.hbs', + templates = { # Nunjucks templates used for key editing & viewing + 'create': '/plugins/keys/assets/regex/create-regex-modal.njk', + 'update': '/plugins/keys/assets/regex/edit-regex-modal.njk', } @staticmethod diff --git a/CTFd/plugins/keys/assets/regex/create-regex-modal.hbs b/CTFd/plugins/keys/assets/regex/create-regex-modal.njk similarity index 100% rename from CTFd/plugins/keys/assets/regex/create-regex-modal.hbs rename to CTFd/plugins/keys/assets/regex/create-regex-modal.njk diff --git a/CTFd/plugins/keys/assets/regex/edit-regex-modal.hbs b/CTFd/plugins/keys/assets/regex/edit-regex-modal.njk similarity index 56% rename from CTFd/plugins/keys/assets/regex/edit-regex-modal.hbs rename to CTFd/plugins/keys/assets/regex/edit-regex-modal.njk index d059a783..c088b9d8 100644 --- a/CTFd/plugins/keys/assets/regex/edit-regex-modal.hbs +++ b/CTFd/plugins/keys/assets/regex/edit-regex-modal.njk @@ -1,17 +1,26 @@