Skip to content

Commit f48a0cd

Browse files
authoredMar 29, 2017
Hints (CTFd#232)
* Switching to Flask-Migrate to create tables/database. Adding Hints & Unlocks. * Adding db.create_all call for sqlite db's (sqlite is not properly handled with alembic yet) * Python 3 testing works properly with 3.5 * Adding admin side of hints * Hints are viewable for users
1 parent 9a9b775 commit f48a0cd

20 files changed

+651
-184
lines changed
 

‎.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
language: python
22
python:
33
- 2.7
4-
- 3.6
4+
- 3.5
55
install:
66
- pip install -r development.txt
77
script:

‎CTFd/__init__.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,26 @@ def create_app(config='CTFd.config.Config'):
3434
if url.drivername == 'postgres':
3535
url.drivername = 'postgresql'
3636

37+
## Creates database if the database database does not exist
38+
if not database_exists(url):
39+
create_database(url)
40+
41+
## Register database
3742
db.init_app(app)
3843

39-
try:
40-
if not (url.drivername.startswith('sqlite') or database_exists(url)):
41-
create_database(url)
42-
db.create_all()
43-
except OperationalError:
44-
db.create_all()
45-
except ProgrammingError: ## Database already exists
46-
pass
47-
else:
44+
## Register Flask-Migrate
45+
migrate.init_app(app, db)
46+
47+
## This creates tables instead of db.create_all()
48+
## Allows migrations to happen properly
49+
migrate_upgrade()
50+
51+
## Alembic sqlite support is lacking so we should just create_all anyway
52+
if url.drivername.startswith('sqlite'):
4853
db.create_all()
4954

5055
app.db = db
5156

52-
migrate.init_app(app, db)
53-
5457
cache.init_app(app)
5558
app.cache = cache
5659

‎CTFd/admin/challenges.py

+84-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
22
from CTFd.utils import admins_only, is_admin, cache
3-
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
3+
from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError
44
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
55
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
66

@@ -87,6 +87,66 @@ def admin_delete_tags(tagid):
8787
return '1'
8888

8989

90+
@admin_challenges.route('/admin/hints', defaults={'hintid': None}, methods=['POST', 'GET'])
91+
@admin_challenges.route('/admin/hints/<int:hintid>', methods=['GET', 'POST', 'DELETE'])
92+
@admins_only
93+
def admin_hints(hintid):
94+
if hintid:
95+
hint = Hints.query.filter_by(id=hintid).first_or_404()
96+
97+
if request.method == 'POST':
98+
hint.hint = request.form.get('hint')
99+
hint.chal = int(request.form.get('chal'))
100+
hint.cost = int(request.form.get('cost'))
101+
db.session.commit()
102+
103+
elif request.method == 'DELETE':
104+
db.session.delete(hint)
105+
db.session.commit()
106+
db.session.close()
107+
return ('', 204)
108+
109+
json_data = {
110+
'hint': hint.hint,
111+
'type': hint.type,
112+
'chal': hint.chal,
113+
'cost': hint.cost,
114+
'id': hint.id
115+
}
116+
db.session.close()
117+
return jsonify(json_data)
118+
else:
119+
if request.method == 'GET':
120+
hints = Hints.query.all()
121+
json_data = []
122+
for hint in hints:
123+
json_data.append({
124+
'hint': hint.hint,
125+
'type': hint.type,
126+
'chal': hint.chal,
127+
'cost': hint.cost,
128+
'id': hint.id
129+
})
130+
return jsonify({'results': json_data})
131+
elif request.method == 'POST':
132+
hint = request.form.get('hint')
133+
chalid = int(request.form.get('chal'))
134+
cost = int(request.form.get('cost'))
135+
hint_type = request.form.get('type', 0)
136+
hint = Hints(chal=chalid, hint=hint, cost=cost)
137+
db.session.add(hint)
138+
db.session.commit()
139+
json_data = {
140+
'hint': hint.hint,
141+
'type': hint.type,
142+
'chal': hint.chal,
143+
'cost': hint.cost,
144+
'id': hint.id
145+
}
146+
db.session.close()
147+
return jsonify(json_data)
148+
149+
90150
@admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
91151
@admins_only
92152
def admin_files(chalid):
@@ -125,13 +185,34 @@ def admin_get_values(chalid, prop):
125185
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
126186
json_data = {'keys': []}
127187
for x in chal_keys:
128-
json_data['keys'].append({'id': x.id, 'key': x.flag, 'type': x.key_type, 'type_name': get_key_class(x.key_type).name})
188+
json_data['keys'].append({
189+
'id': x.id,
190+
'key': x.flag,
191+
'type': x.key_type,
192+
'type_name': get_key_class(x.key_type).name
193+
})
129194
return jsonify(json_data)
130195
elif prop == 'tags':
131196
tags = Tags.query.filter_by(chal=chalid).all()
132197
json_data = {'tags': []}
133198
for x in tags:
134-
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
199+
json_data['tags'].append({
200+
'id': x.id,
201+
'chal': x.chal,
202+
'tag': x.tag
203+
})
204+
return jsonify(json_data)
205+
elif prop == 'hints':
206+
hints = Hints.query.filter_by(chal=chalid)
207+
json_data = {'hints': []}
208+
for hint in hints:
209+
json_data['hints'].append({
210+
'hint': hint.hint,
211+
'type': hint.type,
212+
'chal': hint.chal,
213+
'cost': hint.cost,
214+
'id': hint.id
215+
})
135216
return jsonify(json_data)
136217

137218

‎CTFd/challenges.py

+54-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,57 @@
66
from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint
77
from sqlalchemy.sql import or_
88

9-
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards
9+
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards, Hints, Unlocks
1010
from CTFd.plugins.keys import get_key_class
1111
from CTFd.plugins.challenges import get_chal_class
1212

1313
from CTFd import utils
1414

1515
challenges = Blueprint('challenges', __name__)
1616

17+
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
18+
def hints_view(hintid):
19+
hint = Hints.query.filter_by(id=hintid).first_or_404()
20+
chal = Challenges.query.filter_by(id=hint.chal).first()
21+
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
22+
if request.method == 'GET':
23+
if unlock:
24+
return jsonify({
25+
'hint': hint.hint,
26+
'chal': hint.chal,
27+
'cost': hint.cost
28+
})
29+
else:
30+
return jsonify({
31+
'chal': hint.chal,
32+
'cost': hint.cost
33+
})
34+
elif request.method == 'POST':
35+
if not unlock:
36+
team = Teams.query.filter_by(id=session['id']).first()
37+
if team.score() < hint.cost:
38+
return jsonify({'errors': 'Not enough points'})
39+
unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
40+
award = Awards(teamid=session['id'], name='Hint for {}'.format(chal.name), value=(-hint.cost))
41+
db.session.add(unlock)
42+
db.session.add(award)
43+
db.session.commit()
44+
json_data = {
45+
'hint': hint.hint,
46+
'chal': hint.chal,
47+
'cost': hint.cost
48+
}
49+
db.session.close()
50+
return jsonify(json_data)
51+
else:
52+
json_data = {
53+
'hint': hint.hint,
54+
'chal': hint.chal,
55+
'cost': hint.cost
56+
}
57+
db.session.close()
58+
return jsonify(json_data)
59+
1760

1861
@challenges.route('/challenges', methods=['GET'])
1962
def challenges_view():
@@ -57,6 +100,14 @@ def chals():
57100
for x in chals:
58101
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()]
59102
files = [str(f.location) for f in Files.query.filter_by(chal=x.id).all()]
103+
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=session['id'])])
104+
hints = []
105+
for hint in Hints.query.filter_by(chal=x.id).all():
106+
if hint.id in unlocked_hints:
107+
hints.append({'id':hint.id, 'cost':hint.cost, 'hint':hint.hint})
108+
else:
109+
hints.append({'id':hint.id, 'cost':hint.cost})
110+
# hints = [{'id':hint.id, 'cost':hint.cost} for hint in Hints.query.filter_by(chal=x.id).all()]
60111
chal_type = get_chal_class(x.type)
61112
json['game'].append({
62113
'id': x.id,
@@ -66,7 +117,8 @@ def chals():
66117
'description': x.description,
67118
'category': x.category,
68119
'files': files,
69-
'tags': tags
120+
'tags': tags,
121+
'hints': hints
70122
})
71123

72124
db.session.close()

‎CTFd/config.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Config(object):
3232
3333
http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys
3434
'''
35-
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///ctfd.db'
35+
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(__file__))
3636

3737

3838
'''
@@ -133,4 +133,4 @@ class TestingConfig(Config):
133133
PRESERVE_CONTEXT_ON_EXCEPTION = False
134134
TESTING = True
135135
DEBUG = True
136-
SQLALCHEMY_DATABASE_URI = 'sqlite://'
136+
SQLALCHEMY_DATABASE_URI = 'sqlite://'

‎CTFd/models.py

+33
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ def __repr__(self):
7676
return '<chal %r>' % self.name
7777

7878

79+
class Hints(db.Model):
80+
id = db.Column(db.Integer, primary_key=True)
81+
type = db.Column(db.Integer, default=0)
82+
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
83+
hint = db.Column(db.Text)
84+
cost = db.Column(db.Integer, default=0)
85+
86+
def __init__(self, chal, hint, cost=0, type=0):
87+
self.chal = chal
88+
self.hint = hint
89+
self.cost = cost
90+
self.type = type
91+
92+
def __repr__(self):
93+
return '<hint %r>' % self.hint
94+
95+
7996
class Awards(db.Model):
8097
id = db.Column(db.Integer, primary_key=True)
8198
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
@@ -244,6 +261,22 @@ def __repr__(self):
244261
return '<wrong %r>' % self.flag
245262

246263

264+
class Unlocks(db.Model):
265+
id = db.Column(db.Integer, primary_key=True)
266+
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
267+
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
268+
itemid = db.Column(db.Integer)
269+
model = db.Column(db.String(32))
270+
271+
def __init__(self, model, teamid, itemid):
272+
self.model = model
273+
self.teamid = teamid
274+
self.itemid = itemid
275+
276+
def __repr__(self):
277+
return '<unlock %r>' % self.teamid
278+
279+
247280
class Tracking(db.Model):
248281
id = db.Column(db.Integer, primary_key=True)
249282
ip = db.Column(db.BigInteger)

‎CTFd/static/admin/js/chalboard.js

+77
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ function load_edit_key_modal(key_id, key_type_name) {
3838
});
3939
}
4040

41+
function load_hint_modal(method, hintid){
42+
$('#hint-modal-hint').val('');
43+
$('#hint-modal-cost').val('');
44+
if (method == 'create'){
45+
$('#hint-modal-submit').attr('action', '/admin/hints');
46+
$('#hint-modal-title').text('Create Hint');
47+
$("#hint-modal").modal();
48+
} else if (method == 'update'){
49+
$.get(script_root + '/admin/hints/' + hintid, function(data){
50+
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
51+
$('#hint-modal-hint').val(data.hint);
52+
$('#hint-modal-cost').val(data.cost);
53+
$('#hint-modal-title').text('Update Hint');
54+
$("#hint-modal-button").text('Update Hint');
55+
$("#hint-modal").modal();
56+
});
57+
}
58+
}
59+
4160
function loadchal(id, update) {
4261
// $('#chal *').show()
4362
// $('#chal > h1').hide()
@@ -169,6 +188,44 @@ function deletetag(tagid){
169188
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
170189
}
171190

191+
192+
function edithint(hintid){
193+
$.get(script_root + '/admin/hints/' + hintid, function(data){
194+
console.log(data);
195+
})
196+
}
197+
198+
199+
function deletehint(hintid){
200+
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
201+
if (jqXHR.status == 204){
202+
var chalid = $('.chal-id').val();
203+
loadhints(chalid);
204+
}
205+
});
206+
}
207+
208+
209+
function loadhints(chal){
210+
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
211+
var table = $('#hintsboard > tbody');
212+
table.empty();
213+
for (var i = 0; i < data.hints.length; i++) {
214+
var hint = data.hints[i]
215+
var hint_row = "<tr>" +
216+
"<td class='hint-entry'>{0}</td>".format(hint.hint) +
217+
"<td class='hint-cost'>{0}</td>".format(hint.cost) +
218+
"<td class='hint-settings'><span>" +
219+
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
220+
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
221+
"</span></td>" +
222+
"</tr>";
223+
table.append(hint_row);
224+
}
225+
});
226+
}
227+
228+
172229
function deletechal(chalid){
173230
$.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
174231
}
@@ -255,6 +312,7 @@ function loadchals(){
255312
$('#challenges button').click(function (e) {
256313
loadchal(this.value);
257314
loadkeys(this.value);
315+
loadhints(this.value);
258316
loadtags(this.value);
259317
loadfiles(this.value);
260318
});
@@ -373,6 +431,25 @@ $('#create-keys-submit').click(function (e) {
373431
create_key(chalid, key_data, key_type);
374432
});
375433

434+
435+
$('#create-hint').click(function(e){
436+
e.preventDefault();
437+
load_hint_modal('create');
438+
});
439+
440+
$('#hint-modal-submit').submit(function (e) {
441+
e.preventDefault();
442+
var params = {}
443+
$(this).serializeArray().map(function(x){
444+
params[x.name] = x.value;
445+
});
446+
$.post(script_root + $(this).attr('action'), params, function(data){
447+
loadhints(params['chal']);
448+
});
449+
$("#hint-modal").modal('hide');
450+
});
451+
452+
376453
$(function(){
377454
loadchals();
378455
})

‎CTFd/static/admin/js/utils.js

+19
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,23 @@ Handlebars.registerHelper('if_eq', function(a, b, opts) {
6060
} else {
6161
return opts.inverse(this);
6262
}
63+
});
64+
65+
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
66+
jQuery.each(["put", "delete"], function(i, method) {
67+
jQuery[method] = function(url, data, callback, type) {
68+
if (jQuery.isFunction(data)) {
69+
type = type || callback;
70+
callback = data;
71+
data = undefined;
72+
}
73+
74+
return jQuery.ajax({
75+
url: url,
76+
type: method,
77+
dataType: type,
78+
data: data,
79+
success: callback
80+
});
81+
};
6382
});

‎CTFd/static/original/js/chalboard.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function updateChalWindow(obj) {
4242
desc: marked(obj.description, {'gfm':true, 'breaks':true}),
4343
solves: solves,
4444
files: obj.files,
45+
hints: obj.hints
4546
};
4647

4748
$('#chal-window').append(template(wrapper));
@@ -226,7 +227,7 @@ function loadchals(refresh) {
226227
var chalid = chalinfo.name.replace(/ /g,"-").hashCode();
227228
var catid = chalinfo.category.replace(/ /g,"-").hashCode();
228229
var chalwrap = $("<div id='{0}' class='challenge-wrapper col-md-2'></div>".format(chalid));
229-
var chalbutton = $("<button class='challenge-button trigger theme-background hide-text' value='{0}' data-toggle='modal' data-target='#chal-window'></div>".format(chalinfo.id));
230+
var chalbutton = $("<button class='challenge-button trigger theme-background hide-text' value='{0}'></div>".format(chalinfo.id));
230231
var chalheader = $("<h5>{0}</h5>".format(chalinfo.name));
231232
var chalscore = $("<span>{0}</span>".format(chalinfo.value));
232233
chalbutton.append(chalheader);
@@ -254,6 +255,19 @@ function loadchals(refresh) {
254255
});
255256
}
256257

258+
function loadhint(hintid){
259+
if (confirm("Are you sure you want to open this hint?")){
260+
$.post(script_root + "/hints/" + hintid, {'nonce': $('#nonce').val()}, function(data){
261+
if (data.errors){
262+
alert(data.errors);
263+
} else {
264+
$('#hint-modal-body').html(marked(data.hint, {'gfm':true, 'breaks':true}));
265+
$('#hint-modal').modal();
266+
}
267+
});
268+
}
269+
}
270+
257271
$('#submit-key').click(function (e) {
258272
submitkey($('#chal-id').val(), $('#answer-input').val(), $('#nonce').val())
259273
});
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
(function($, window) {
2+
'use strict';
3+
4+
var MultiModal = function(element) {
5+
this.$element = $(element);
6+
this.modalCount = 0;
7+
};
8+
9+
MultiModal.BASE_ZINDEX = 1040;
10+
11+
MultiModal.prototype.show = function(target) {
12+
var that = this;
13+
var $target = $(target);
14+
var modalIndex = that.modalCount++;
15+
16+
$target.css('z-index', MultiModal.BASE_ZINDEX + (modalIndex * 20) + 10);
17+
18+
window.setTimeout(function() {
19+
if(modalIndex > 0)
20+
$('.modal-backdrop').not(':first').addClass('hidden');
21+
22+
that.adjustBackdrop();
23+
});
24+
};
25+
26+
MultiModal.prototype.hidden = function(target) {
27+
this.modalCount--;
28+
29+
if(this.modalCount) {
30+
this.adjustBackdrop();
31+
$('body').addClass('modal-open');
32+
}
33+
};
34+
35+
MultiModal.prototype.adjustBackdrop = function() {
36+
var modalIndex = this.modalCount - 1;
37+
$('.modal-backdrop:first').css('z-index', MultiModal.BASE_ZINDEX + (modalIndex * 20));
38+
};
39+
40+
function Plugin(method, target) {
41+
return this.each(function() {
42+
var $this = $(this);
43+
var data = $this.data('multi-modal-plugin');
44+
45+
if(!data)
46+
$this.data('multi-modal-plugin', (data = new MultiModal(this)));
47+
48+
if(method)
49+
data[method](target);
50+
});
51+
}
52+
53+
$.fn.multiModal = Plugin;
54+
$.fn.multiModal.Constructor = MultiModal;
55+
56+
$(document).on('show.bs.modal', function(e) {
57+
$(document).multiModal('show', e.target);
58+
});
59+
60+
$(document).on('hidden.bs.modal', function(e) {
61+
$(document).multiModal('hidden', e.target);
62+
});
63+
}(jQuery, window));

‎CTFd/static/original/js/templates/challenges/standard/standard-challenge-modal.hbs

+23
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@
2020
{{/each}}
2121
</div>
2222
<span class="chal-desc">{{{ desc }}}</span>
23+
<div class="chal-hints file-row row">
24+
{{#each hints}}
25+
<div class='col-md-12 file-button-wrapper'>
26+
<a onclick="javascript:loadhint({{this.id}})">
27+
{{#if this.hint }}
28+
<label class='challenge-wrapper file-wrapper hide-text'>
29+
View Hint
30+
</label>
31+
{{else}}
32+
{{#if this.cost}}
33+
<label class='challenge-wrapper file-wrapper hide-text'>
34+
Unlock Hint for {{this.cost}} points
35+
</label>
36+
{{else}}
37+
<label class='challenge-wrapper file-wrapper hide-text'>
38+
View Hint
39+
</label>
40+
{{/if}}
41+
{{/if}}
42+
</a>
43+
</div>
44+
{{/each}}
45+
</div>
2346
<div class="chal-files file-row row">
2447
{{#each files}}
2548
<div class='col-md-3 file-button-wrapper'>

‎CTFd/static/original/js/utils.js

+19
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,23 @@ Handlebars.registerHelper('if_eq', function(a, b, opts) {
6060
} else {
6161
return opts.inverse(this);
6262
}
63+
});
64+
65+
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
66+
jQuery.each(["put", "delete"], function(i, method) {
67+
jQuery[method] = function(url, data, callback, type) {
68+
if (jQuery.isFunction(data)) {
69+
type = type || callback;
70+
callback = data;
71+
data = undefined;
72+
}
73+
74+
return jQuery.ajax({
75+
url: url,
76+
type: method,
77+
dataType: type,
78+
data: data,
79+
success: callback
80+
});
81+
};
6382
});

‎CTFd/templates/admin/chals.html

+56-23
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<div class="modal-dialog">
2929
<div class="modal-content">
3030
<div class="modal-header">
31+
<button type="button" class="close" data-dismiss="modal">&times;</button>
3132
<h3>New Challenge</h3>
3233
</div>
3334
<div class="modal-body">
@@ -162,6 +163,7 @@ <h3 class="chal-title text-center"></h3>
162163
<div class="form-group">
163164
<a href="#" data-toggle="modal" data-target="#update-tags" class="btn btn-primary">Tags</a>
164165
<a href="#" data-toggle="modal" data-target="#update-files" class="btn btn-primary">Files</a>
166+
<a href="#" data-toggle="modal" data-target="#update-hints" class="btn btn-primary">Hints</a>
165167
<a href="#" data-toggle="modal" data-target="#update-keys" class="btn btn-primary">Keys</a>
166168
<a href="#" data-toggle="modal" data-target="#delete-chal" class="btn btn-danger">Delete</a>
167169
</div>
@@ -179,6 +181,7 @@ <h3 class="chal-title text-center"></h3>
179181
<div class="modal-dialog">
180182
<div class="modal-content">
181183
<div class="modal-header text-center">
184+
<button type="button" class="close" data-dismiss="modal">&times;</button>
182185
<h3>Delete Challenge</h3>
183186
</div>
184187
<div class="modal-body">
@@ -202,6 +205,7 @@ <h3>Delete Challenge</h3>
202205
<div class="modal-content">
203206
<input type="hidden" class="chal-id" name="chal-id">
204207
<div class="modal-header text-center">
208+
<button type="button" class="close" data-dismiss="modal">&times;</button>
205209
<h3>Create Key</h3>
206210
</div>
207211
<div class="modal-body">
@@ -275,44 +279,74 @@ <h3>Files</h3>
275279
</div>
276280
</div>
277281

278-
<div id="update-tags" class="modal fade" tabindex="-1">
282+
283+
<div id="hint-modal" class="modal fade" tabindex="-1">
279284
<div class="modal-dialog">
280285
<div class="modal-content">
281286
<div class="modal-header text-center">
282287
<button type="button" class="close" data-dismiss="modal">&times;</button>
283-
<h3>Tags</h3>
288+
<h3 id="hint-modal-title"></h3>
284289
</div>
285290
<div class="modal-body">
286-
<div class="form-group">
287-
<label for="tag-insert">Value</label>
288-
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
289-
</div>
290-
<input name='nonce' type='hidden' value="{{ nonce }}">
291-
<input id="tags-chal" name='chal' type='hidden'>
292-
293-
<div id="current-tags">
291+
<form id="hint-modal-submit" method='POST'>
292+
<input type="hidden" class="chal-id" name="chal">
293+
<div class="form-group">
294+
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint" placeholder="Hint"></textarea>
295+
</div>
296+
<div class="form-group">
297+
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
298+
</div>
299+
<div class="row" style="text-align:center;margin-top:20px">
300+
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
301+
<button class="btn btn-theme btn-outlined" id="hint-modal-button">Add Hint</button>
302+
</div>
303+
</form>
304+
</div>
305+
</div>
306+
</div>
307+
</div>
294308

309+
<div id="update-hints" class="modal fade" tabindex="-1">
310+
<div class="modal-dialog">
311+
<div class="modal-content">
312+
<div class="modal-header text-center">
313+
<button type="button" class="close" data-dismiss="modal">&times;</button>
314+
<h3>Hints</h3>
315+
</div>
316+
<div class="modal-body">
317+
<div class="row" style="text-align:center">
318+
<a href="#" id="create-hint" class="btn btn-primary" style="margin-bottom:15px;">New Hint</a>
295319
</div>
296-
<br/>
297-
<div id="chal-tags">
298-
</div>
299-
<div class="row" style="text-align:center;margin-top:20px">
300-
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
301-
<button class="btn btn-theme btn-outlined" id="submit-tags">Update</button>
320+
<div class='current-hints'>
321+
<table id="hintsboard" class="table table-striped">
322+
<thead>
323+
<tr>
324+
<td class="text-center"><b>Hint</b></td>
325+
<td class="text-center"><b>Cost</b></td>
326+
<td class="text-center"><b>Settings</b></td>
327+
</tr>
328+
</thead>
329+
<tbody class="text-center">
330+
</tbody>
331+
</table>
302332
</div>
303333
</div>
304334
</div>
305335
</div>
306336
</div>
307337

308-
<div id="email-user" class="modal fade" tabindex="-1">
338+
<div id="update-tags" class="modal fade" tabindex="-1">
309339
<div class="modal-dialog">
310340
<div class="modal-content">
311341
<div class="modal-header text-center">
342+
<button type="button" class="close" data-dismiss="modal">&times;</button>
312343
<h3>Tags</h3>
313344
</div>
314345
<div class="modal-body">
315-
<input type="text" class="tag-insert" maxlength="80" placeholder="Type tag and press Enter">
346+
<div class="form-group">
347+
<label for="tag-insert">Value</label>
348+
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
349+
</div>
316350
<input name='nonce' type='hidden' value="{{ nonce }}">
317351
<input id="tags-chal" name='chal' type='hidden'>
318352

@@ -321,12 +355,11 @@ <h3>Tags</h3>
321355
</div>
322356
<br/>
323357
<div id="chal-tags">
324-
325358
</div>
326-
<br/>
327-
328-
<a href="#" id="submit-tags" class="button">Update</a>
329-
<a class="close-reveal-modal">&#215;</a>
359+
<div class="row" style="text-align:center;margin-top:20px">
360+
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
361+
<button class="btn btn-theme btn-outlined" id="submit-tags">Update</button>
362+
</div>
330363
</div>
331364
</div>
332365
</div>

‎CTFd/templates/original/chals.html

+14
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,27 @@ <h1>Challenges</h1>
103103

104104
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
105105

106+
<div class="modal fade" id="hint-modal" tabindex="-1" role="dialog">
107+
<div class="modal-dialog">
108+
<div class="modal-content">
109+
<div class="modal-header text-center">
110+
<button type="button" class="close" data-dismiss="modal">&times;</button>
111+
<h3>Hint</h3>
112+
</div>
113+
<div class="modal-body" id="hint-modal-body">
114+
</div>
115+
</div>
116+
</div>
117+
</div>
118+
106119
<div class="modal fade" id="chal-window" tabindex="-1" role="dialog">
107120
</div>
108121
{% endif %}
109122
{% endblock %}
110123

111124
{% block scripts %}
112125
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/utils.js"></script>
126+
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/multi-modal.js"></script>
113127
{% if not errors %}<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/chalboard.js"></script>{% endif %}
114128
<script src="{{ request.script_root }}/static/{{ ctf_theme() }}/js/style.js"></script>
115129
{% endblock %}

‎migrations/env.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
# Interpret the config file for Python logging.
1212
# This line sets up loggers basically.
13-
fileConfig(config.config_file_name)
13+
## http://stackoverflow.com/questions/42427487/using-alembic-config-main-redirects-log-output
14+
# fileConfig(config.config_file_name)
1415
logger = logging.getLogger('alembic.env')
1516

1617
# add your model's MetaData object here

‎migrations/versions/87733981ca0e_adds_challenge_types_and_uses_keys_table.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ def upgrade():
6464

6565
## Drop flags from challenges
6666
print("Dropping flags column from challenges")
67-
op.drop_column('challenges', 'flags')
67+
try:
68+
op.drop_column('challenges', 'flags')
69+
except sa.exc.OperationalError:
70+
print("Failed to drop flags. Likely due to SQLite")
71+
6872

6973
print("Finished")
7074
# ### end Alembic commands ###
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Adding Hints and Unlocks tables
2+
3+
Revision ID: c7225db614c1
4+
Revises: d6514ec92738
5+
Create Date: 2017-03-23 01:31:43.940187
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'c7225db614c1'
14+
down_revision = 'd6514ec92738'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table('hints',
22+
sa.Column('id', sa.Integer(), nullable=False),
23+
sa.Column('type', sa.Integer(), nullable=True),
24+
sa.Column('chal', sa.Integer(), nullable=True),
25+
sa.Column('hint', sa.Text(), nullable=True),
26+
sa.Column('cost', sa.Integer(), nullable=True),
27+
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
28+
sa.PrimaryKeyConstraint('id')
29+
)
30+
op.create_table('unlocks',
31+
sa.Column('id', sa.Integer(), nullable=False),
32+
sa.Column('teamid', sa.Integer(), nullable=True),
33+
sa.Column('date', sa.DateTime(), nullable=True),
34+
sa.Column('itemid', sa.Integer(), nullable=True),
35+
sa.Column('model', sa.String(length=32), nullable=True),
36+
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
37+
sa.PrimaryKeyConstraint('id')
38+
)
39+
# ### end Alembic commands ###
40+
41+
42+
def downgrade():
43+
# ### commands auto generated by Alembic - please adjust! ###
44+
op.drop_table('unlocks')
45+
op.drop_table('hints')
46+
# ### end Alembic commands ###

‎migrations/versions/cb3cfcc47e2f_base.py

+121-136
Original file line numberDiff line numberDiff line change
@@ -18,142 +18,127 @@
1818

1919

2020
def upgrade():
21-
app = create_app()
22-
engine = sa.create_engine(app.config.get('SQLALCHEMY_DATABASE_URI'))
23-
# ### commands auto generated by Alembic - please adjust! ###
24-
if not engine.dialect.has_table(engine, 'challenges'):
25-
op.create_table('challenges',
26-
sa.Column('id', sa.Integer(), nullable=False),
27-
sa.Column('name', sa.String(length=80), nullable=True),
28-
sa.Column('description', sa.Text(), nullable=True),
29-
sa.Column('value', sa.Integer(), nullable=True),
30-
sa.Column('category', sa.String(length=80), nullable=True),
31-
sa.Column('flags', sa.Text(), nullable=True),
32-
sa.Column('hidden', sa.Boolean(), nullable=True),
33-
sa.PrimaryKeyConstraint('id')
34-
)
35-
36-
if not engine.dialect.has_table(engine, 'config'):
37-
op.create_table('config',
38-
sa.Column('id', sa.Integer(), nullable=False),
39-
sa.Column('key', sa.Text(), nullable=True),
40-
sa.Column('value', sa.Text(), nullable=True),
41-
sa.PrimaryKeyConstraint('id')
42-
)
43-
44-
if not engine.dialect.has_table(engine, 'containers'):
45-
op.create_table('containers',
46-
sa.Column('id', sa.Integer(), nullable=False),
47-
sa.Column('name', sa.String(length=80), nullable=True),
48-
sa.Column('buildfile', sa.Text(), nullable=True),
49-
sa.PrimaryKeyConstraint('id')
50-
)
51-
52-
if not engine.dialect.has_table(engine, 'pages'):
53-
op.create_table('pages',
54-
sa.Column('id', sa.Integer(), nullable=False),
55-
sa.Column('route', sa.String(length=80), nullable=True),
56-
sa.Column('html', sa.Text(), nullable=True),
57-
sa.PrimaryKeyConstraint('id'),
58-
sa.UniqueConstraint('route')
59-
)
60-
61-
if not engine.dialect.has_table(engine, 'teams'):
62-
op.create_table('teams',
63-
sa.Column('id', sa.Integer(), nullable=False),
64-
sa.Column('name', sa.String(length=128), nullable=True),
65-
sa.Column('email', sa.String(length=128), nullable=True),
66-
sa.Column('password', sa.String(length=128), nullable=True),
67-
sa.Column('website', sa.String(length=128), nullable=True),
68-
sa.Column('affiliation', sa.String(length=128), nullable=True),
69-
sa.Column('country', sa.String(length=32), nullable=True),
70-
sa.Column('bracket', sa.String(length=32), nullable=True),
71-
sa.Column('banned', sa.Boolean(), nullable=True),
72-
sa.Column('verified', sa.Boolean(), nullable=True),
73-
sa.Column('admin', sa.Boolean(), nullable=True),
74-
sa.Column('joined', sa.DateTime(), nullable=True),
75-
sa.PrimaryKeyConstraint('id'),
76-
sa.UniqueConstraint('email'),
77-
sa.UniqueConstraint('name')
78-
)
79-
80-
if not engine.dialect.has_table(engine, 'awards'):
81-
op.create_table('awards',
82-
sa.Column('id', sa.Integer(), nullable=False),
83-
sa.Column('teamid', sa.Integer(), nullable=True),
84-
sa.Column('name', sa.String(length=80), nullable=True),
85-
sa.Column('description', sa.Text(), nullable=True),
86-
sa.Column('date', sa.DateTime(), nullable=True),
87-
sa.Column('value', sa.Integer(), nullable=True),
88-
sa.Column('category', sa.String(length=80), nullable=True),
89-
sa.Column('icon', sa.Text(), nullable=True),
90-
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
91-
sa.PrimaryKeyConstraint('id')
92-
)
93-
94-
if not engine.dialect.has_table(engine, 'files'):
95-
op.create_table('files',
96-
sa.Column('id', sa.Integer(), nullable=False),
97-
sa.Column('chal', sa.Integer(), nullable=True),
98-
sa.Column('location', sa.Text(), nullable=True),
99-
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
100-
sa.PrimaryKeyConstraint('id')
101-
)
102-
103-
if not engine.dialect.has_table(engine, 'keys'):
104-
op.create_table('keys',
105-
sa.Column('id', sa.Integer(), nullable=False),
106-
sa.Column('chal', sa.Integer(), nullable=True),
107-
sa.Column('key_type', sa.Integer(), nullable=True),
108-
sa.Column('flag', sa.Text(), nullable=True),
109-
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
110-
sa.PrimaryKeyConstraint('id')
111-
)
112-
113-
if not engine.dialect.has_table(engine, 'solves'):
114-
op.create_table('solves',
115-
sa.Column('id', sa.Integer(), nullable=False),
116-
sa.Column('chalid', sa.Integer(), nullable=True),
117-
sa.Column('teamid', sa.Integer(), nullable=True),
118-
sa.Column('ip', sa.Integer(), nullable=True),
119-
sa.Column('flag', sa.Text(), nullable=True),
120-
sa.Column('date', sa.DateTime(), nullable=True),
121-
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
122-
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
123-
sa.PrimaryKeyConstraint('id'),
124-
sa.UniqueConstraint('chalid', 'teamid')
125-
)
126-
127-
if not engine.dialect.has_table(engine, 'tags'):
128-
op.create_table('tags',
129-
sa.Column('id', sa.Integer(), nullable=False),
130-
sa.Column('chal', sa.Integer(), nullable=True),
131-
sa.Column('tag', sa.String(length=80), nullable=True),
132-
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
133-
sa.PrimaryKeyConstraint('id')
134-
)
135-
136-
if not engine.dialect.has_table(engine, 'tracking'):
137-
op.create_table('tracking',
138-
sa.Column('id', sa.Integer(), nullable=False),
139-
sa.Column('ip', sa.BigInteger(), nullable=True),
140-
sa.Column('team', sa.Integer(), nullable=True),
141-
sa.Column('date', sa.DateTime(), nullable=True),
142-
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
143-
sa.PrimaryKeyConstraint('id')
144-
)
145-
146-
if not engine.dialect.has_table(engine, 'wrong_keys'):
147-
op.create_table('wrong_keys',
148-
sa.Column('id', sa.Integer(), nullable=False),
149-
sa.Column('chalid', sa.Integer(), nullable=True),
150-
sa.Column('teamid', sa.Integer(), nullable=True),
151-
sa.Column('date', sa.DateTime(), nullable=True),
152-
sa.Column('flag', sa.Text(), nullable=True),
153-
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
154-
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
155-
sa.PrimaryKeyConstraint('id')
156-
)
21+
op.create_table('challenges',
22+
sa.Column('id', sa.Integer(), nullable=False),
23+
sa.Column('name', sa.String(length=80), nullable=True),
24+
sa.Column('description', sa.Text(), nullable=True),
25+
sa.Column('value', sa.Integer(), nullable=True),
26+
sa.Column('category', sa.String(length=80), nullable=True),
27+
sa.Column('flags', sa.Text(), nullable=True),
28+
sa.Column('hidden', sa.Boolean(), nullable=True),
29+
sa.PrimaryKeyConstraint('id')
30+
)
31+
32+
op.create_table('config',
33+
sa.Column('id', sa.Integer(), nullable=False),
34+
sa.Column('key', sa.Text(), nullable=True),
35+
sa.Column('value', sa.Text(), nullable=True),
36+
sa.PrimaryKeyConstraint('id')
37+
)
38+
39+
op.create_table('containers',
40+
sa.Column('id', sa.Integer(), nullable=False),
41+
sa.Column('name', sa.String(length=80), nullable=True),
42+
sa.Column('buildfile', sa.Text(), nullable=True),
43+
sa.PrimaryKeyConstraint('id')
44+
)
45+
46+
op.create_table('pages',
47+
sa.Column('id', sa.Integer(), nullable=False),
48+
sa.Column('route', sa.String(length=80), nullable=True),
49+
sa.Column('html', sa.Text(), nullable=True),
50+
sa.PrimaryKeyConstraint('id'),
51+
sa.UniqueConstraint('route')
52+
)
53+
54+
op.create_table('teams',
55+
sa.Column('id', sa.Integer(), nullable=False),
56+
sa.Column('name', sa.String(length=128), nullable=True),
57+
sa.Column('email', sa.String(length=128), nullable=True),
58+
sa.Column('password', sa.String(length=128), nullable=True),
59+
sa.Column('website', sa.String(length=128), nullable=True),
60+
sa.Column('affiliation', sa.String(length=128), nullable=True),
61+
sa.Column('country', sa.String(length=32), nullable=True),
62+
sa.Column('bracket', sa.String(length=32), nullable=True),
63+
sa.Column('banned', sa.Boolean(), nullable=True),
64+
sa.Column('verified', sa.Boolean(), nullable=True),
65+
sa.Column('admin', sa.Boolean(), nullable=True),
66+
sa.Column('joined', sa.DateTime(), nullable=True),
67+
sa.PrimaryKeyConstraint('id'),
68+
sa.UniqueConstraint('email'),
69+
sa.UniqueConstraint('name')
70+
)
71+
72+
op.create_table('awards',
73+
sa.Column('id', sa.Integer(), nullable=False),
74+
sa.Column('teamid', sa.Integer(), nullable=True),
75+
sa.Column('name', sa.String(length=80), nullable=True),
76+
sa.Column('description', sa.Text(), nullable=True),
77+
sa.Column('date', sa.DateTime(), nullable=True),
78+
sa.Column('value', sa.Integer(), nullable=True),
79+
sa.Column('category', sa.String(length=80), nullable=True),
80+
sa.Column('icon', sa.Text(), nullable=True),
81+
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
82+
sa.PrimaryKeyConstraint('id')
83+
)
84+
85+
op.create_table('files',
86+
sa.Column('id', sa.Integer(), nullable=False),
87+
sa.Column('chal', sa.Integer(), nullable=True),
88+
sa.Column('location', sa.Text(), nullable=True),
89+
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
90+
sa.PrimaryKeyConstraint('id')
91+
)
92+
93+
op.create_table('keys',
94+
sa.Column('id', sa.Integer(), nullable=False),
95+
sa.Column('chal', sa.Integer(), nullable=True),
96+
sa.Column('key_type', sa.Integer(), nullable=True),
97+
sa.Column('flag', sa.Text(), nullable=True),
98+
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
99+
sa.PrimaryKeyConstraint('id')
100+
)
101+
102+
op.create_table('solves',
103+
sa.Column('id', sa.Integer(), nullable=False),
104+
sa.Column('chalid', sa.Integer(), nullable=True),
105+
sa.Column('teamid', sa.Integer(), nullable=True),
106+
sa.Column('ip', sa.Integer(), nullable=True),
107+
sa.Column('flag', sa.Text(), nullable=True),
108+
sa.Column('date', sa.DateTime(), nullable=True),
109+
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
110+
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
111+
sa.PrimaryKeyConstraint('id'),
112+
sa.UniqueConstraint('chalid', 'teamid')
113+
)
114+
115+
op.create_table('tags',
116+
sa.Column('id', sa.Integer(), nullable=False),
117+
sa.Column('chal', sa.Integer(), nullable=True),
118+
sa.Column('tag', sa.String(length=80), nullable=True),
119+
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
120+
sa.PrimaryKeyConstraint('id')
121+
)
122+
123+
op.create_table('tracking',
124+
sa.Column('id', sa.Integer(), nullable=False),
125+
sa.Column('ip', sa.BigInteger(), nullable=True),
126+
sa.Column('team', sa.Integer(), nullable=True),
127+
sa.Column('date', sa.DateTime(), nullable=True),
128+
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
129+
sa.PrimaryKeyConstraint('id')
130+
)
131+
132+
op.create_table('wrong_keys',
133+
sa.Column('id', sa.Integer(), nullable=False),
134+
sa.Column('chalid', sa.Integer(), nullable=True),
135+
sa.Column('teamid', sa.Integer(), nullable=True),
136+
sa.Column('date', sa.DateTime(), nullable=True),
137+
sa.Column('flag', sa.Text(), nullable=True),
138+
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
139+
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
140+
sa.PrimaryKeyConstraint('id')
141+
)
157142
# ### end Alembic commands ###
158143

159144

‎serve.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from CTFd import create_app
22

33
app = create_app()
4-
app.run(debug=True, threaded=True, host="0.0.0.0", port=4000)
4+
app.run(debug=True, threaded=True, host="127.0.0.1", port=4000)

‎tests/test_user_facing.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,5 @@ def test_viewing_challenges():
203203
client = login_as_user(app)
204204
gen_challenge(app.db)
205205
r = client.get('/chals')
206-
chals = json.loads(r.data)
207-
assert len(chals['game']) == 1
206+
chals = json.loads(r.get_data(as_text=True))
207+
assert len(chals['game']) == 1

0 commit comments

Comments
 (0)
Please sign in to comment.