mirror of https://github.com/JohnHammond/CTFd.git
Merge Dev into Master (#591)
* Chals endpoint seperation (#572) * Separate the logic of ctftime and email confirmations and admin checking into decorators * Separate the chals endpoint into /chals and /chals/:id. Closes #552, #435. * Challenges are now loaded directly from the server before being displayed to the user. * Challenge modals now use `{{ description }}` instead of `{{ desc }}`. * 403 is now a more common status code and can indicate that a CTF has not begun or that you are not logged in. This is in addition to CSRF failures. * Update tests to new behavior * Fixing glitch if an entry chal or team id isn't defined * Markdown it (#574) * Replace Marked with Markdown-It * Update modal change (#576) * Switch update modals to use nunjucks instead of JS to load in data. * Fix previewing challenges after hitting the challenge update button. * Fix edit-files issue with an unnecessary request. * Fix solves button * Closes #592selenium-screenshot-testing
parent
d17e599193
commit
51d098080f
|
@ -11,16 +11,22 @@ from CTFd.plugins.keys import get_key_class
|
||||||
from CTFd.plugins.challenges import get_chal_class
|
from CTFd.plugins.challenges import get_chal_class
|
||||||
|
|
||||||
from CTFd import utils
|
from CTFd import utils
|
||||||
|
from CTFd.utils.decorators import (
|
||||||
|
authed_only,
|
||||||
|
admins_only,
|
||||||
|
during_ctf_time_only,
|
||||||
|
require_verified_emails,
|
||||||
|
viewable_without_authentication
|
||||||
|
)
|
||||||
from CTFd.utils import text_type
|
from CTFd.utils import text_type
|
||||||
|
|
||||||
challenges = Blueprint('challenges', __name__)
|
challenges = Blueprint('challenges', __name__)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
|
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
|
||||||
|
@during_ctf_time_only
|
||||||
|
@authed_only
|
||||||
def hints_view(hintid):
|
def hints_view(hintid):
|
||||||
if utils.ctf_started() is False:
|
|
||||||
if utils.is_admin() is False:
|
|
||||||
abort(403)
|
|
||||||
hint = Hints.query.filter_by(id=hintid).first_or_404()
|
hint = Hints.query.filter_by(id=hintid).first_or_404()
|
||||||
chal = Challenges.query.filter_by(id=hint.chal).first()
|
chal = Challenges.query.filter_by(id=hint.chal).first()
|
||||||
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
|
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
|
||||||
|
@ -68,95 +74,91 @@ def hints_view(hintid):
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/challenges', methods=['GET'])
|
@challenges.route('/challenges', methods=['GET'])
|
||||||
|
@during_ctf_time_only
|
||||||
|
@require_verified_emails
|
||||||
|
@viewable_without_authentication()
|
||||||
def challenges_view():
|
def challenges_view():
|
||||||
infos = []
|
infos = []
|
||||||
errors = []
|
errors = []
|
||||||
start = utils.get_config('start') or 0
|
start = utils.get_config('start') or 0
|
||||||
end = utils.get_config('end') or 0
|
end = utils.get_config('end') or 0
|
||||||
|
|
||||||
if utils.ctf_paused():
|
if utils.ctf_paused():
|
||||||
infos.append('{} is paused'.format(utils.ctf_name()))
|
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
|
|
||||||
if utils.view_after_ctf(): # But we are allowed to view after the CTF ends
|
|
||||||
pass
|
|
||||||
else: # We are NOT allowed to view after the CTF ends
|
|
||||||
if utils.get_config('start') and not utils.ctf_started():
|
|
||||||
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('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
|
||||||
|
|
||||||
if utils.get_config('verify_emails'):
|
if not utils.ctftime():
|
||||||
if utils.authed():
|
# It is not CTF time
|
||||||
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
|
if utils.view_after_ctf(): # But we are allowed to view after the CTF ends
|
||||||
return redirect(url_for('auth.confirm_user'))
|
pass
|
||||||
|
else: # We are NOT allowed to view after the CTF ends
|
||||||
|
if utils.get_config('start') and not utils.ctf_started():
|
||||||
|
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('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||||
|
|
||||||
if utils.user_can_view_challenges(): # Do we allow unauthenticated users?
|
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||||
if utils.get_config('start') and not utils.ctf_started():
|
|
||||||
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('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
|
||||||
else:
|
|
||||||
return redirect(url_for('auth.login', next='challenges'))
|
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/chals', methods=['GET'])
|
@challenges.route('/chals', methods=['GET'])
|
||||||
|
@during_ctf_time_only
|
||||||
|
@require_verified_emails
|
||||||
|
@viewable_without_authentication(status_code=403)
|
||||||
def chals():
|
def chals():
|
||||||
if not utils.is_admin():
|
db_chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
|
||||||
if not utils.ctftime():
|
response = {'game': []}
|
||||||
if utils.view_after_ctf():
|
for chal in db_chals:
|
||||||
pass
|
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||||
else:
|
chal_type = get_chal_class(chal.type)
|
||||||
abort(403)
|
response['game'].append({
|
||||||
|
'id': chal.id,
|
||||||
|
'type': chal_type.name,
|
||||||
|
'name': chal.name,
|
||||||
|
'value': chal.value,
|
||||||
|
'category': chal.category,
|
||||||
|
'tags': tags,
|
||||||
|
'template': chal_type.templates['modal'],
|
||||||
|
'script': chal_type.scripts['modal'],
|
||||||
|
})
|
||||||
|
|
||||||
if utils.get_config('verify_emails'):
|
db.session.close()
|
||||||
if utils.authed():
|
return jsonify(response)
|
||||||
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
if utils.user_can_view_challenges() and (utils.ctf_started() or utils.is_admin()):
|
|
||||||
teamid = session.get('id')
|
|
||||||
chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
|
|
||||||
json = {'game': []}
|
|
||||||
for x in chals:
|
|
||||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()]
|
|
||||||
files = [str(f.location) for f in Files.query.filter_by(chal=x.id).all()]
|
|
||||||
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
|
|
||||||
hints = []
|
|
||||||
for hint in Hints.query.filter_by(chal=x.id).all():
|
|
||||||
if hint.id in unlocked_hints or utils.ctf_ended():
|
|
||||||
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
|
|
||||||
else:
|
|
||||||
hints.append({'id': hint.id, 'cost': hint.cost})
|
|
||||||
chal_type = get_chal_class(x.type)
|
|
||||||
json['game'].append({
|
|
||||||
'id': x.id,
|
|
||||||
'type': chal_type.name,
|
|
||||||
'name': x.name,
|
|
||||||
'value': x.value,
|
|
||||||
'description': x.description,
|
|
||||||
'category': x.category,
|
|
||||||
'files': files,
|
|
||||||
'tags': tags,
|
|
||||||
'hints': hints,
|
|
||||||
'template': chal_type.templates['modal'],
|
|
||||||
'script': chal_type.scripts['modal'],
|
|
||||||
})
|
|
||||||
|
|
||||||
db.session.close()
|
@challenges.route('/chals/<int:chal_id>', methods=['GET'])
|
||||||
return jsonify(json)
|
@during_ctf_time_only
|
||||||
else:
|
@require_verified_emails
|
||||||
db.session.close()
|
@viewable_without_authentication(status_code=403)
|
||||||
abort(403)
|
def chal_view(chal_id):
|
||||||
|
teamid = session.get('id')
|
||||||
|
|
||||||
|
chal = Challenges.query.filter_by(id=chal_id).first_or_404()
|
||||||
|
chal_class = get_chal_class(chal.type)
|
||||||
|
|
||||||
|
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||||
|
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
|
||||||
|
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
|
||||||
|
hints = []
|
||||||
|
|
||||||
|
for hint in Hints.query.filter_by(chal=chal.id).all():
|
||||||
|
if hint.id in unlocked_hints or utils.ctf_ended():
|
||||||
|
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
|
||||||
|
else:
|
||||||
|
hints.append({'id': hint.id, 'cost': hint.cost})
|
||||||
|
|
||||||
|
challenge, response = chal_class.read(challenge=chal)
|
||||||
|
|
||||||
|
response['files'] = files
|
||||||
|
response['tags'] = tags
|
||||||
|
response['hints'] = hints
|
||||||
|
|
||||||
|
db.session.close()
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/chals/solves')
|
@challenges.route('/chals/solves')
|
||||||
|
@viewable_without_authentication(status_code=403)
|
||||||
def solves_per_chal():
|
def solves_per_chal():
|
||||||
if not utils.user_can_view_challenges():
|
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
|
||||||
|
|
||||||
chals = Challenges.query\
|
chals = Challenges.query\
|
||||||
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
|
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
|
||||||
.order_by(Challenges.value)\
|
.order_by(Challenges.value)\
|
||||||
|
@ -195,55 +197,28 @@ def solves_per_chal():
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/solves')
|
@challenges.route('/solves')
|
||||||
@challenges.route('/solves/<int:teamid>')
|
@authed_only
|
||||||
def solves(teamid=None):
|
def solves_private():
|
||||||
solves = None
|
solves = None
|
||||||
awards = None
|
awards = None
|
||||||
if teamid is None:
|
|
||||||
if utils.is_admin():
|
if utils.is_admin():
|
||||||
solves = Solves.query.filter_by(teamid=session['id']).all()
|
solves = Solves.query.filter_by(teamid=session['id']).all()
|
||||||
elif utils.user_can_view_challenges():
|
elif utils.user_can_view_challenges():
|
||||||
if utils.authed():
|
if utils.authed():
|
||||||
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.teamid == session['id'], Teams.banned == False).all()
|
solves = Solves.query\
|
||||||
else:
|
.join(Teams, Solves.teamid == Teams.id)\
|
||||||
return jsonify({'solves': []})
|
.filter(Solves.teamid == session['id'], Teams.banned == False)\
|
||||||
|
.all()
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('auth.login', next='solves'))
|
return jsonify({'solves': []})
|
||||||
else:
|
else:
|
||||||
if utils.authed() and session['id'] == teamid:
|
return redirect(url_for('auth.login', next='solves'))
|
||||||
solves = Solves.query.filter_by(teamid=teamid)
|
|
||||||
awards = Awards.query.filter_by(teamid=teamid)
|
|
||||||
|
|
||||||
freeze = utils.get_config('freeze')
|
|
||||||
if freeze:
|
|
||||||
freeze = utils.unix_time_to_utc(freeze)
|
|
||||||
if teamid != session.get('id'):
|
|
||||||
solves = solves.filter(Solves.date < freeze)
|
|
||||||
awards = awards.filter(Awards.date < freeze)
|
|
||||||
|
|
||||||
solves = solves.all()
|
|
||||||
awards = awards.all()
|
|
||||||
elif utils.hide_scores():
|
|
||||||
# Use empty values to hide scores
|
|
||||||
solves = []
|
|
||||||
awards = []
|
|
||||||
else:
|
|
||||||
solves = Solves.query.filter_by(teamid=teamid)
|
|
||||||
awards = Awards.query.filter_by(teamid=teamid)
|
|
||||||
|
|
||||||
freeze = utils.get_config('freeze')
|
|
||||||
if freeze:
|
|
||||||
freeze = utils.unix_time_to_utc(freeze)
|
|
||||||
if teamid != session.get('id'):
|
|
||||||
solves = solves.filter(Solves.date < freeze)
|
|
||||||
awards = awards.filter(Awards.date < freeze)
|
|
||||||
|
|
||||||
solves = solves.all()
|
|
||||||
awards = awards.all()
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
json = {'solves': []}
|
response = {'solves': []}
|
||||||
for solve in solves:
|
for solve in solves:
|
||||||
json['solves'].append({
|
response['solves'].append({
|
||||||
'chal': solve.chal.name,
|
'chal': solve.chal.name,
|
||||||
'chalid': solve.chalid,
|
'chalid': solve.chalid,
|
||||||
'team': solve.teamid,
|
'team': solve.teamid,
|
||||||
|
@ -253,7 +228,7 @@ def solves(teamid=None):
|
||||||
})
|
})
|
||||||
if awards:
|
if awards:
|
||||||
for award in awards:
|
for award in awards:
|
||||||
json['solves'].append({
|
response['solves'].append({
|
||||||
'chal': award.name,
|
'chal': award.name,
|
||||||
'chalid': None,
|
'chalid': None,
|
||||||
'team': award.teamid,
|
'team': award.teamid,
|
||||||
|
@ -261,69 +236,126 @@ def solves(teamid=None):
|
||||||
'category': award.category or "Award",
|
'category': award.category or "Award",
|
||||||
'time': utils.unix_time(award.date)
|
'time': utils.unix_time(award.date)
|
||||||
})
|
})
|
||||||
json['solves'].sort(key=lambda k: k['time'])
|
response['solves'].sort(key=lambda k: k['time'])
|
||||||
return jsonify(json)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/maxattempts')
|
@challenges.route('/solves/<int:teamid>')
|
||||||
def attempts():
|
def solves_public(teamid=None):
|
||||||
if not utils.user_can_view_challenges():
|
solves = None
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
awards = None
|
||||||
chals = Challenges.query.add_columns('id').all()
|
|
||||||
json = {'maxattempts': []}
|
if utils.authed() and session['id'] == teamid:
|
||||||
for chal, chalid in chals:
|
solves = Solves.query.filter_by(teamid=teamid)
|
||||||
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
|
awards = Awards.query.filter_by(teamid=teamid)
|
||||||
if fails >= int(utils.get_config("max_tries")) and int(utils.get_config("max_tries")) > 0:
|
|
||||||
json['maxattempts'].append({'chalid': chalid})
|
freeze = utils.get_config('freeze')
|
||||||
return jsonify(json)
|
if freeze:
|
||||||
|
freeze = utils.unix_time_to_utc(freeze)
|
||||||
|
if teamid != session.get('id'):
|
||||||
|
solves = solves.filter(Solves.date < freeze)
|
||||||
|
awards = awards.filter(Awards.date < freeze)
|
||||||
|
|
||||||
|
solves = solves.all()
|
||||||
|
awards = awards.all()
|
||||||
|
elif utils.hide_scores():
|
||||||
|
# Use empty values to hide scores
|
||||||
|
solves = []
|
||||||
|
awards = []
|
||||||
|
else:
|
||||||
|
solves = Solves.query.filter_by(teamid=teamid)
|
||||||
|
awards = Awards.query.filter_by(teamid=teamid)
|
||||||
|
|
||||||
|
freeze = utils.get_config('freeze')
|
||||||
|
if freeze:
|
||||||
|
freeze = utils.unix_time_to_utc(freeze)
|
||||||
|
if teamid != session.get('id'):
|
||||||
|
solves = solves.filter(Solves.date < freeze)
|
||||||
|
awards = awards.filter(Awards.date < freeze)
|
||||||
|
|
||||||
|
solves = solves.all()
|
||||||
|
awards = awards.all()
|
||||||
|
db.session.close()
|
||||||
|
|
||||||
|
response = {'solves': []}
|
||||||
|
for solve in solves:
|
||||||
|
response['solves'].append({
|
||||||
|
'chal': solve.chal.name,
|
||||||
|
'chalid': solve.chalid,
|
||||||
|
'team': solve.teamid,
|
||||||
|
'value': solve.chal.value,
|
||||||
|
'category': solve.chal.category,
|
||||||
|
'time': utils.unix_time(solve.date)
|
||||||
|
})
|
||||||
|
if awards:
|
||||||
|
for award in awards:
|
||||||
|
response['solves'].append({
|
||||||
|
'chal': award.name,
|
||||||
|
'chalid': None,
|
||||||
|
'team': award.teamid,
|
||||||
|
'value': award.value,
|
||||||
|
'category': award.category or "Award",
|
||||||
|
'time': utils.unix_time(award.date)
|
||||||
|
})
|
||||||
|
response['solves'].sort(key=lambda k: k['time'])
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/fails')
|
@challenges.route('/fails')
|
||||||
@challenges.route('/fails/<int:teamid>')
|
@authed_only
|
||||||
def fails(teamid=None):
|
def fails_private():
|
||||||
if teamid is None:
|
fails = WrongKeys.query.filter_by(teamid=session['id']).count()
|
||||||
fails = WrongKeys.query.filter_by(teamid=session['id']).count()
|
solves = Solves.query.filter_by(teamid=session['id']).count()
|
||||||
solves = Solves.query.filter_by(teamid=session['id']).count()
|
|
||||||
else:
|
|
||||||
if utils.authed() and session['id'] == teamid:
|
|
||||||
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
|
||||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
|
||||||
elif utils.hide_scores():
|
|
||||||
fails = 0
|
|
||||||
solves = 0
|
|
||||||
else:
|
|
||||||
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
|
||||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
json = {'fails': str(fails), 'solves': str(solves)}
|
response = {
|
||||||
return jsonify(json)
|
'fails': str(fails),
|
||||||
|
'solves': str(solves)
|
||||||
|
}
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
|
@challenges.route('/fails/<int:teamid>')
|
||||||
|
def fails_public(teamid=None):
|
||||||
|
if utils.authed() and session['id'] == teamid:
|
||||||
|
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
||||||
|
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||||
|
elif utils.hide_scores():
|
||||||
|
fails = 0
|
||||||
|
solves = 0
|
||||||
|
else:
|
||||||
|
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
||||||
|
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||||
|
db.session.close()
|
||||||
|
response = {
|
||||||
|
'fails': str(fails),
|
||||||
|
'solves': str(solves)
|
||||||
|
}
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
|
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
|
||||||
|
@during_ctf_time_only
|
||||||
|
@viewable_without_authentication(status_code=403)
|
||||||
def who_solved(chalid):
|
def who_solved(chalid):
|
||||||
if not utils.user_can_view_challenges():
|
response = {'teams': []}
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
|
||||||
|
|
||||||
json = {'teams': []}
|
|
||||||
if utils.hide_scores():
|
if utils.hide_scores():
|
||||||
return jsonify(json)
|
return jsonify(response)
|
||||||
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
|
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
|
||||||
for solve in solves:
|
for solve in solves:
|
||||||
json['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
||||||
return jsonify(json)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@challenges.route('/chal/<int:chalid>', methods=['POST'])
|
@challenges.route('/chal/<int:chalid>', methods=['POST'])
|
||||||
|
@during_ctf_time_only
|
||||||
|
@viewable_without_authentication()
|
||||||
def chal(chalid):
|
def chal(chalid):
|
||||||
if utils.ctf_paused():
|
if utils.ctf_paused():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 3,
|
'status': 3,
|
||||||
'message': '{} is paused'.format(utils.ctf_name())
|
'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():
|
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
|
||||||
if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin():
|
if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin():
|
||||||
team = Teams.query.filter_by(id=session['id']).first()
|
team = Teams.query.filter_by(id=session['id']).first()
|
||||||
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
|
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
// Markdown Preview
|
// Markdown Preview
|
||||||
$('#desc-edit').on('shown.bs.tab', function (event) {
|
$('#desc-edit').on('shown.bs.tab', function (event) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
if (event.target.hash == '#desc-preview'){
|
if (event.target.hash == '#desc-preview'){
|
||||||
$(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true}));
|
var editor_value = $('#desc-editor').val();
|
||||||
|
$(event.target.hash).html(
|
||||||
|
md.render(editor_value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#new-desc-edit').on('shown.bs.tab', function (event) {
|
$('#new-desc-edit').on('shown.bs.tab', function (event) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
if (event.target.hash == '#new-desc-preview'){
|
if (event.target.hash == '#new-desc-preview'){
|
||||||
$(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true}));
|
var editor_value = $('#new-desc-editor').val();
|
||||||
|
$(event.target.hash).html(
|
||||||
|
md.render(editor_value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#solve-attempts-checkbox").change(function() {
|
$("#solve-attempts-checkbox").change(function() {
|
||||||
|
|
|
@ -22,8 +22,4 @@ $(".input-field").bind({
|
||||||
$label.removeClass('input--hide' );
|
$label.removeClass('input--hide' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var content = $('.chal-desc').text();
|
|
||||||
var decoded = $('<textarea/>').html(content).val()
|
|
||||||
|
|
||||||
$('.chal-desc').html(marked(content, {'gfm':true, 'breaks':true}));
|
|
|
@ -25,7 +25,7 @@
|
||||||
<span class='badge badge-info chal-tag'>{{tag}}</span>
|
<span class='badge badge-info chal-tag'>{{tag}}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<span class="chal-desc">{{ desc }}</span>
|
<span class="chal-desc">{{ description | safe }}</span>
|
||||||
<div class="chal-hints hint-row row">
|
<div class="chal-hints hint-row row">
|
||||||
{% for hint in hints %}
|
{% for hint in hints %}
|
||||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||||
|
|
|
@ -18,37 +18,31 @@ $('#limit_max_attempts').change(function() {
|
||||||
|
|
||||||
// Markdown Preview
|
// Markdown Preview
|
||||||
$('#desc-edit').on('shown.bs.tab', function (event) {
|
$('#desc-edit').on('shown.bs.tab', function (event) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
if (event.target.hash == '#desc-preview'){
|
if (event.target.hash == '#desc-preview'){
|
||||||
$(event.target.hash).html(marked($('#desc-editor').val(), {'gfm':true, 'breaks':true}))
|
var editor_value = $('#desc-editor').val();
|
||||||
|
$(event.target.hash).html(
|
||||||
|
md.render(editor_value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#new-desc-edit').on('shown.bs.tab', function (event) {
|
$('#new-desc-edit').on('shown.bs.tab', function (event) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
if (event.target.hash == '#new-desc-preview'){
|
if (event.target.hash == '#new-desc-preview'){
|
||||||
$(event.target.hash).html(marked($('#new-desc-editor').val(), {'gfm':true, 'breaks':true}))
|
var editor_value = $('#new-desc-editor').val();
|
||||||
|
$(event.target.hash).html(
|
||||||
|
md.render(editor_value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadchal(id, update) {
|
function loadchal(id, update) {
|
||||||
$.get(script_root + '/admin/chal/' + id, function(obj){
|
$.get(script_root + '/admin/chal/' + id, function(obj){
|
||||||
$('#desc-write-link').click(); // Switch to Write tab
|
$('#desc-write-link').click(); // Switch to Write tab
|
||||||
$('.chal-title').text(obj.name);
|
|
||||||
$('.chal-name').val(obj.name);
|
|
||||||
$('.chal-desc-editor').val(obj.description);
|
|
||||||
$('.chal-value').val(obj.value);
|
|
||||||
if (parseInt(obj.max_attempts) > 0){
|
|
||||||
$('.chal-attempts').val(obj.max_attempts);
|
|
||||||
$('#limit_max_attempts').prop('checked', true);
|
|
||||||
$('#chal-attempts-group').show();
|
|
||||||
}
|
|
||||||
$('.chal-category').val(obj.category);
|
|
||||||
$('.chal-id').val(obj.id);
|
|
||||||
$('.chal-hidden').prop('checked', false);
|
|
||||||
if (obj.hidden) {
|
|
||||||
$('.chal-hidden').prop('checked', true);
|
|
||||||
}
|
|
||||||
//$('#update-challenge .chal-delete').attr({
|
|
||||||
// 'href': '/admin/chal/close/' + (id + 1)
|
|
||||||
//})
|
|
||||||
if (typeof update === 'undefined')
|
if (typeof update === 'undefined')
|
||||||
$('#update-challenge').modal();
|
$('#update-challenge').modal();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,93 +1,94 @@
|
||||||
<div id="update-challenge" class="modal fade" tabindex="-1">
|
<div id="update-challenge" class="modal fade" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h3 class="chal-title text-center"></h3>
|
<h3 class="chal-title text-center">{{ name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form method="POST" action="{{ script_root }}/admin/chal/update">
|
<form method="POST" action="{{ script_root }}/admin/chal/update">
|
||||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="name">Name
|
<label for="name">Name
|
||||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name">
|
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name" value="{{ name }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="category">Category
|
<label for="category">Category
|
||||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||||
</label>
|
</label>
|
||||||
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category">
|
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category" value="{{ category }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
|
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
|
||||||
Write
|
Write
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
|
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
|
||||||
Preview
|
Preview
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="message-text" class="control-label">Message:
|
<label for="message-text" class="control-label">Message:
|
||||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||||
</label>
|
</label>
|
||||||
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10"></textarea>
|
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ description }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="value">Value
|
<label for="value">Value
|
||||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
|
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
|
||||||
</label>
|
</label>
|
||||||
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
|
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" value="{{ value }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox">
|
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox" {% if max_attempts %}checked{% endif %}>
|
||||||
Limit challenge attempts
|
Limit challenge attempts
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="chal-attempts-group" style="display:none;">
|
<div class="form-group" id="chal-attempts-group" {% if not max_attempts %}style="display:none;"{% endif %}>
|
||||||
<label for="value">Max Attempts</label>
|
<label for="value">Max Attempts</label>
|
||||||
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value">
|
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value" value="{{ max_attempts }}">
|
||||||
</div>
|
</div>
|
||||||
<input class="chal-id" type='hidden' name='id' placeholder='ID'>
|
<input class="chal-id" type='hidden' name='id' placeholder='ID' value="{{ id }}">
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input class="chal-hidden" name="hidden" type="checkbox">
|
<input class="chal-hidden" name="hidden" type="checkbox"
|
||||||
Hidden
|
{% if hidden %}checked{% endif %}>
|
||||||
</label>
|
Hidden
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
|
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -5,79 +5,50 @@ function load_chal_template(id, success_cb){
|
||||||
var obj = $.grep(challenges['game'], function (e) {
|
var obj = $.grep(challenges['game'], function (e) {
|
||||||
return e.id == id;
|
return e.id == id;
|
||||||
})[0];
|
})[0];
|
||||||
$.get(script_root + obj.type_data.templates.update, function(template_data){
|
$.get(script_root + "/admin/chal/" + id, function (challenge_data) {
|
||||||
var template = nunjucks.compile(template_data);
|
$.get(script_root + obj.type_data.templates.update, function (template_data) {
|
||||||
$("#update-modals-entry-div").html(template.render({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
var template = nunjucks.compile(template_data);
|
||||||
$.ajax({
|
|
||||||
url: script_root + obj.type_data.scripts.update,
|
challenge_data['nonce'] = $('#nonce').val();
|
||||||
dataType: "script",
|
challenge_data['script_root'] = script_root;
|
||||||
success: success_cb,
|
|
||||||
cache: false,
|
$("#update-modals-entry-div").html(template.render(challenge_data));
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: script_root + obj.type_data.scripts.update,
|
||||||
|
dataType: "script",
|
||||||
|
success: success_cb,
|
||||||
|
cache: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_challenge(id){
|
|
||||||
var obj = $.grep(challenges['game'], function (e) {
|
|
||||||
return e.id == id;
|
|
||||||
})[0];
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function load_challenge_preview(id){
|
function load_challenge_preview(id){
|
||||||
loadchal(id, function(){
|
render_challenge_preview(id);
|
||||||
var chal = get_challenge(id);
|
|
||||||
var modal_template = chal.type_data.templates.modal;
|
|
||||||
var modal_script = chal.type_data.scripts.modal;
|
|
||||||
|
|
||||||
render_challenge_preview(chal, modal_template, modal_script);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render_challenge_preview(chal, modal_template, modal_script){
|
function render_challenge_preview(chal_id){
|
||||||
var preview_window = $('#challenge-preview');
|
var preview_window = $('#challenge-preview');
|
||||||
$.get(script_root + modal_template, function (template_data) {
|
var md = window.markdownit({
|
||||||
preview_window.empty();
|
html: true,
|
||||||
var template = nunjucks.compile(template_data);
|
|
||||||
var data = {
|
|
||||||
id: chal.id,
|
|
||||||
name: chal.name,
|
|
||||||
value: chal.value,
|
|
||||||
tags: chal.tags,
|
|
||||||
desc: chal.description,
|
|
||||||
files: chal.files,
|
|
||||||
hints: chal.hints,
|
|
||||||
script_root: script_root
|
|
||||||
};
|
|
||||||
|
|
||||||
var challenge = template.render(data);
|
|
||||||
|
|
||||||
preview_window.append(challenge);
|
|
||||||
|
|
||||||
$.getScript(script_root + modal_script, function () {
|
|
||||||
preview_window.modal();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
$.get(script_root + "/admin/chal/" + chal_id, function(challenge_data){
|
||||||
|
$.get(script_root + challenge_data.type_data.templates.modal, function (template_data) {
|
||||||
|
preview_window.empty();
|
||||||
|
var template = nunjucks.compile(template_data);
|
||||||
|
|
||||||
|
challenge_data['description'] = md.render(challenge_data['description']);
|
||||||
|
challenge_data['script_root'] = script_root;
|
||||||
|
|
||||||
function loadchal(chalid, cb){
|
var challenge = template.render(challenge_data);
|
||||||
$.get(script_root + "/admin/chal/"+chalid, {
|
|
||||||
}, function (data) {
|
|
||||||
var categories = [];
|
|
||||||
var challenge = $.parseJSON(JSON.stringify(data));
|
|
||||||
|
|
||||||
for (var i = challenges['game'].length - 1; i >= 0; i--) {
|
preview_window.append(challenge);
|
||||||
if (challenges['game'][i]['id'] == challenge.id) {
|
|
||||||
challenges['game'][i] = challenge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cb) {
|
$.getScript(script_root + challenge_data.type_data.scripts.modal, function () {
|
||||||
cb();
|
preview_window.modal();
|
||||||
}
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +81,9 @@ loadchals(function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadhint(hintid) {
|
function loadhint(hintid) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
ezq({
|
ezq({
|
||||||
title: "Unlock Hint?",
|
title: "Unlock Hint?",
|
||||||
body: "Are you sure you want to open this hint?",
|
body: "Are you sure you want to open this hint?",
|
||||||
|
@ -124,7 +98,7 @@ function loadhint(hintid) {
|
||||||
} else {
|
} else {
|
||||||
ezal({
|
ezal({
|
||||||
title: "Hint",
|
title: "Hint",
|
||||||
body: marked(data.hint, {'gfm': true, 'breaks': true}),
|
body: md.render(data.hint),
|
||||||
button: "Got it!"
|
button: "Got it!"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,11 +87,14 @@ $(document).ready(function () {
|
||||||
|
|
||||||
|
|
||||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
var target = $(e.target).attr("href");
|
var target = $(e.target).attr("href");
|
||||||
if (target == '#hint-preview') {
|
if (target == '#hint-preview') {
|
||||||
var obj = $('#hint-modal-hint');
|
var obj = $('#hint-modal-hint');
|
||||||
var data = marked(obj.val());
|
var data = md.render(obj.val());
|
||||||
$('#hint-preview').html(data, {'gfm': true, 'breaks': true});
|
$('#hint-preview').html(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -113,7 +113,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/markdown-it.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||||
|
|
|
@ -372,13 +372,6 @@
|
||||||
submit_form();
|
submit_form();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Markdown Preview
|
|
||||||
$('#content-edit').on('shown.bs.tab', function (event) {
|
|
||||||
if (event.target.hash == '#content-preview') {
|
|
||||||
$(event.target.hash).html(marked(editor.getValue(), {'gfm': true, 'breaks': true}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#media-button').click(function () {
|
$('#media-button').click(function () {
|
||||||
$.get(script_root + '/admin/media', function (data) {
|
$.get(script_root + '/admin/media', function (data) {
|
||||||
$('#media-library-list').empty();
|
$('#media-library-list').empty();
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/markdown-it.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||||
|
|
|
@ -19,53 +19,52 @@ function loadchalbyname(chalname) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChalWindow(obj) {
|
function updateChalWindow(obj) {
|
||||||
$.get(script_root + obj.template, function(template_data){
|
$.get(script_root + "/chals/" + obj.id, function(challenge_data){
|
||||||
$('#chal-window').empty();
|
$.get(script_root + obj.template, function (template_data) {
|
||||||
var template = nunjucks.compile(template_data);
|
$('#chal-window').empty();
|
||||||
var solves = obj.solves == 1 ? " Solve" : " Solves";
|
var template = nunjucks.compile(template_data);
|
||||||
var solves = obj.solves + solves;
|
var solves = obj.solves == 1 ? " Solve" : " Solves";
|
||||||
|
var solves = obj.solves + solves;
|
||||||
|
|
||||||
var nonce = $('#nonce').val();
|
var nonce = $('#nonce').val();
|
||||||
var wrapper = {
|
|
||||||
id: obj.id,
|
|
||||||
name: obj.name,
|
|
||||||
value: obj.value,
|
|
||||||
tags: obj.tags,
|
|
||||||
desc: obj.description,
|
|
||||||
solves: solves,
|
|
||||||
files: obj.files,
|
|
||||||
hints: obj.hints,
|
|
||||||
script_root: script_root
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#chal-window').append(template.render(wrapper));
|
var md = window.markdownit({
|
||||||
$.getScript(script_root + obj.script,
|
html: true,
|
||||||
function() {
|
});
|
||||||
// Handle Solves tab
|
|
||||||
$('.chal-solves').click(function (e) {
|
challenge_data['description'] = md.render(challenge_data['description']);
|
||||||
getsolves($('#chal-id').val())
|
challenge_data['script_root'] = script_root;
|
||||||
|
challenge_data['solves'] = solves;
|
||||||
|
|
||||||
|
$('#chal-window').append(template.render(challenge_data));
|
||||||
|
$.getScript(script_root + obj.script,
|
||||||
|
function () {
|
||||||
|
// Handle Solves tab
|
||||||
|
$('.chal-solves').click(function (e) {
|
||||||
|
getsolves($('#chal-id').val())
|
||||||
|
});
|
||||||
|
$('.nav-tabs a').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).tab('show')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle modal toggling
|
||||||
|
$('#chal-window').on('hide.bs.modal', function (event) {
|
||||||
|
$("#answer-input").removeClass("wrong");
|
||||||
|
$("#answer-input").removeClass("correct");
|
||||||
|
$("#incorrect-key").slideUp();
|
||||||
|
$("#correct-key").slideUp();
|
||||||
|
$("#already-solved").slideUp();
|
||||||
|
$("#too-fast").slideUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
// $('pre code').each(function(i, block) {
|
||||||
|
// hljs.highlightBlock(block);
|
||||||
|
// });
|
||||||
|
|
||||||
|
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
|
||||||
|
$('#chal-window').modal();
|
||||||
});
|
});
|
||||||
$('.nav-tabs a').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$(this).tab('show')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle modal toggling
|
|
||||||
$('#chal-window').on('hide.bs.modal', function (event) {
|
|
||||||
$("#answer-input").removeClass("wrong");
|
|
||||||
$("#answer-input").removeClass("correct");
|
|
||||||
$("#incorrect-key").slideUp();
|
|
||||||
$("#correct-key").slideUp();
|
|
||||||
$("#already-solved").slideUp();
|
|
||||||
$("#too-fast").slideUp();
|
|
||||||
});
|
|
||||||
|
|
||||||
// $('pre code').each(function(i, block) {
|
|
||||||
// hljs.highlightBlock(block);
|
|
||||||
// });
|
|
||||||
|
|
||||||
window.location.replace(window.location.href.split('#')[0] + '#' + obj.name);
|
|
||||||
$('#chal-window').modal();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -282,6 +281,9 @@ function loadchals(cb) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadhint(hintid){
|
function loadhint(hintid){
|
||||||
|
var md = window.markdownit({
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
ezq({
|
ezq({
|
||||||
title: "Unlock Hint?",
|
title: "Unlock Hint?",
|
||||||
body: "Are you sure you want to open this hint?",
|
body: "Are you sure you want to open this hint?",
|
||||||
|
@ -294,9 +296,10 @@ function loadhint(hintid){
|
||||||
button: "Okay"
|
button: "Okay"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
ezal({
|
ezal({
|
||||||
title: "Hint",
|
title: "Hint",
|
||||||
body: marked(data.hint, {'gfm': true, 'breaks': true}),
|
body: md.render(data.hint),
|
||||||
button: "Got it!"
|
button: "Got it!"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -122,7 +122,7 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/jquery.min.js"></script>
|
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/jquery.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/marked.min.js"></script>
|
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/markdown-it.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/bootstrap.bundle.min.js"></script>
|
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/style.js"></script>
|
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/style.js"></script>
|
||||||
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/ezq.js"></script>
|
<script src="{{ request.script_root }}/themes/{{ ctf_theme() }}/static/js/ezq.js"></script>
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1 class="text-center">403</h1>
|
<h1 class="text-center">Forbidden</h1>
|
||||||
<h2 class="text-center">An authorization error has occured</h2>
|
<h2 class="text-center">{{ error }}</h2>
|
||||||
<h2 class="text-center">Please try again</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,6 +58,7 @@ class CTFdSerializer(JSONSerializer):
|
||||||
Slightly modified datafreeze serializer so that we can properly
|
Slightly modified datafreeze serializer so that we can properly
|
||||||
export the CTFd database into a zip file.
|
export the CTFd database into a zip file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
for path, result in self.buckets.items():
|
for path, result in self.buckets.items():
|
||||||
result = self.wrap(result)
|
result = self.wrap(result)
|
||||||
|
@ -125,19 +126,19 @@ def init_logs(app):
|
||||||
def init_errors(app):
|
def init_errors(app):
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(error):
|
def page_not_found(error):
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html', error=error.description), 404
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(error):
|
def forbidden(error):
|
||||||
return render_template('errors/403.html'), 403
|
return render_template('errors/403.html', error=error.description), 403
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def general_error(error):
|
def general_error(error):
|
||||||
return render_template('errors/500.html'), 500
|
return render_template('errors/500.html', error=error.description), 500
|
||||||
|
|
||||||
@app.errorhandler(502)
|
@app.errorhandler(502)
|
||||||
def gateway_error(error):
|
def gateway_error(error):
|
||||||
return render_template('errors/502.html'), 502
|
return render_template('errors/502.html', error=error.description), 502
|
||||||
|
|
||||||
|
|
||||||
def init_utils(app):
|
def init_utils(app):
|
||||||
|
@ -287,6 +288,7 @@ def admins_only(f):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
return redirect(url_for('auth.login', next=request.path))
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,6 +299,7 @@ def authed_only(f):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('auth.login', next=request.path))
|
return redirect(url_for('auth.login', next=request.path))
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
@ -322,7 +325,9 @@ def ratelimit(method="POST", limit=50, interval=300, key_prefix="rl"):
|
||||||
else:
|
else:
|
||||||
cache.set(key, int(current) + 1, timeout=interval)
|
cache.set(key, int(current) + 1, timeout=interval)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
return ratelimit_decorator
|
return ratelimit_decorator
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
from flask import request, redirect, url_for, session, abort
|
||||||
|
from CTFd import utils
|
||||||
|
import functools
|
||||||
|
|
||||||
|
|
||||||
|
def during_ctf_time_only(f):
|
||||||
|
'''
|
||||||
|
Decorator to restrict an endpoint to only be seen during a CTF
|
||||||
|
:param f:
|
||||||
|
:return:
|
||||||
|
'''
|
||||||
|
@functools.wraps(f)
|
||||||
|
def during_ctf_time_only_wrapper(*args, **kwargs):
|
||||||
|
if utils.ctftime() or utils.is_admin():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
if utils.ctf_ended():
|
||||||
|
if utils.view_after_ctf():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
error = '{} has ended'.format(utils.ctf_name())
|
||||||
|
abort(403, description=error)
|
||||||
|
|
||||||
|
if utils.ctf_started() is False:
|
||||||
|
error = '{} has not started yet'.format(utils.ctf_name())
|
||||||
|
abort(403, description=error)
|
||||||
|
return during_ctf_time_only_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_verified_emails(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def require_verified_emails_wrapper(*args, **kwargs):
|
||||||
|
if utils.get_config('verify_emails'):
|
||||||
|
if utils.authed():
|
||||||
|
if utils.is_admin() is False and utils.is_verified() is False: # User is not confirmed
|
||||||
|
return redirect(url_for('auth.confirm_user'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return require_verified_emails_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def viewable_without_authentication(status_code=None):
|
||||||
|
def viewable_without_authentication_decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def viewable_without_authentication_wrapper(*args, **kwargs):
|
||||||
|
if utils.user_can_view_challenges():
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
if status_code:
|
||||||
|
if status_code == 403:
|
||||||
|
error = "An authorization error has occured"
|
||||||
|
abort(status_code, description=error)
|
||||||
|
return redirect(url_for('auth.login', next=request.path))
|
||||||
|
return viewable_without_authentication_wrapper
|
||||||
|
return viewable_without_authentication_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def authed_only(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def authed_only_wrapper(*args, **kwargs):
|
||||||
|
if session.get('id'):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return redirect(url_for('auth.login', next=request.path))
|
||||||
|
|
||||||
|
return authed_only_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def admins_only(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def admins_only_wrapper(*args, **kwargs):
|
||||||
|
if session.get('admin'):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return redirect(url_for('auth.login', next=request.path))
|
||||||
|
|
||||||
|
return admins_only_wrapper
|
|
@ -96,6 +96,11 @@ def test_admins_can_access_challenges_before_ctftime():
|
||||||
|
|
||||||
with freeze_time("2017-10-2"):
|
with freeze_time("2017-10-2"):
|
||||||
client = login_as_user(app, name='admin', password='password')
|
client = login_as_user(app, name='admin', password='password')
|
||||||
|
|
||||||
|
r = client.get('/challenges')
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert "has not started" in r.get_data(as_text=True)
|
||||||
|
|
||||||
r = client.get('/chals')
|
r = client.get('/chals')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
|
|
@ -276,7 +276,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
|
||||||
chal_id = chal.id
|
chal_id = chal.id
|
||||||
flag = gen_flag(app.db, chal=chal.id, flag=u'flag')
|
flag = gen_flag(app.db, chal=chal.id, flag=u'flag')
|
||||||
|
|
||||||
with freeze_time("2017-10-3"):
|
with freeze_time("2017-10-3"): # CTF has not started yet.
|
||||||
client = login_as_user(app)
|
client = login_as_user(app)
|
||||||
r = client.get('/chals')
|
r = client.get('/chals')
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
@ -288,8 +288,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
|
||||||
}
|
}
|
||||||
r = client.post('/chal/{}'.format(chal_id), data=data)
|
r = client.post('/chal/{}'.format(chal_id), data=data)
|
||||||
data = r.get_data(as_text=True)
|
data = r.get_data(as_text=True)
|
||||||
data = json.loads(data)
|
assert r.status_code == 403
|
||||||
assert data['status'] == -1
|
|
||||||
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
solve_count = app.db.session.query(app.db.func.count(Solves.id)).first()[0]
|
||||||
assert solve_count == 0
|
assert solve_count == 0
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
|
@ -48,7 +48,8 @@ def test_verify_emails_config():
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
|
|
||||||
r = client.get('/chals')
|
r = client.get('/chals')
|
||||||
assert r.status_code == 403
|
assert r.location == "http://localhost/confirm"
|
||||||
|
assert r.status_code == 302
|
||||||
|
|
||||||
user = Teams.query.filter_by(id=2).first()
|
user = Teams.query.filter_by(id=2).first()
|
||||||
user.verified = True
|
user.verified = True
|
||||||
|
@ -90,7 +91,8 @@ def test_verify_and_view_unregistered():
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
|
|
||||||
r = client.get('/chals')
|
r = client.get('/chals')
|
||||||
assert r.status_code == 403
|
assert r.location == "http://localhost/confirm"
|
||||||
|
assert r.status_code == 302
|
||||||
|
|
||||||
user = Teams.query.filter_by(id=2).first()
|
user = Teams.query.filter_by(id=2).first()
|
||||||
user.verified = True
|
user.verified = True
|
||||||
|
|
|
@ -45,6 +45,18 @@ def test_viewing_challenges():
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_viewing_challenge():
|
||||||
|
"""Test that users can see individual challenges"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
gen_challenge(app.db)
|
||||||
|
r = client.get('/chals/1')
|
||||||
|
assert json.loads(r.get_data(as_text=True))
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_chals_solves():
|
def test_chals_solves():
|
||||||
'''Test that the /chals/solves endpoint works properly'''
|
'''Test that the /chals/solves endpoint works properly'''
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
|
|
|
@ -223,7 +223,7 @@ def test_user_get_solves_per_chal():
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_user_get_solves():
|
def test_user_get_private_solves():
|
||||||
"""Can a registered user load /solves"""
|
"""Can a registered user load /solves"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
@ -234,7 +234,62 @@ def test_user_get_solves():
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_user_get_team_page():
|
def test_user_get_public_solves():
|
||||||
|
"""Can a registered user load /solves/2"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/solves/2')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_another_public_solves():
|
||||||
|
"""Can a registered user load public solves page of another user"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/solves/1')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_private_fails():
|
||||||
|
"""Can a registered user load /fails"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/solves')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_public_fails():
|
||||||
|
"""Can a registered user load /fails/2"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/fails/2')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_another_public_fails():
|
||||||
|
"""Can a registered user load public fails page of another user"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/fails/1')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_public_team_page():
|
||||||
"""Can a registered user load their public profile (/team/2)"""
|
"""Can a registered user load their public profile (/team/2)"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
@ -245,6 +300,17 @@ def test_user_get_team_page():
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_get_another_public_team_page():
|
||||||
|
"""Can a registered user load the public profile of another user (/team/1)"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
register_user(app)
|
||||||
|
client = login_as_user(app)
|
||||||
|
r = client.get('/team/1')
|
||||||
|
assert r.status_code == 200
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
def test_user_get_private_team_page():
|
def test_user_get_private_team_page():
|
||||||
"""Can a registered user load their private team page (/team)"""
|
"""Can a registered user load their private team page (/team)"""
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
|
@ -304,7 +370,7 @@ def test_user_get_logout():
|
||||||
client = login_as_user(app)
|
client = login_as_user(app)
|
||||||
client.get('/logout', follow_redirects=True)
|
client.get('/logout', follow_redirects=True)
|
||||||
r = client.get('/challenges')
|
r = client.get('/challenges')
|
||||||
assert r.location == "http://localhost/login?next=challenges"
|
assert r.location == "http://localhost/login?next=%2Fchallenges"
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
destroy_ctfd(app)
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue