From 692b8166ca93a30b0697410001ec0c5e00e4f77a Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 27 Jan 2016 22:38:16 -0600 Subject: [PATCH 01/64] Removed short names from task list. Only display point values --- templates/tasks.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/tasks.html b/templates/tasks.html index 867f9f2..fa8ea59 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -14,7 +14,7 @@ {% if task %}
- {{ task.short_name }}{{ task.score }} + {{ task.score }}
{% else %} @@ -29,4 +29,4 @@ \ No newline at end of file + type='text/javascript'> From cea92f187cc7c37946dd1a9eb5244f7eab356cea Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 27 Jan 2016 22:41:44 -0600 Subject: [PATCH 02/64] Modified description text. --- templates/task.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/task.html b/templates/task.html index a8de2bb..d39dc93 100755 --- a/templates/task.html +++ b/templates/task.html @@ -3,7 +3,7 @@

{{ task.name }}

-
({{ category }}{{ score }}, +
({{ score }} points, {{ lang.task.solution_format % solutions }})

@@ -35,4 +35,4 @@

({{ category }}{{ score }}, \ No newline at end of file + type='text/javascript'> From 1ab38ffec18d26dc9bb22a1dbd0a33593ef95ac9 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 27 Jan 2016 22:53:32 -0600 Subject: [PATCH 03/64] Updated task text. --- templates/task.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/task.html b/templates/task.html index d39dc93..8a8e3b3 100755 --- a/templates/task.html +++ b/templates/task.html @@ -3,7 +3,7 @@

{{ task.name }}

-
({{ score }} points, +
({{ score }} points, {{ lang.task.solution_format % solutions }})

From 55ffb1f658176e021cd0592584757a03478106f3 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 27 Jan 2016 22:56:49 -0600 Subject: [PATCH 04/64] Hide attatchment text if there is no attachment. --- templates/task.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/task.html b/templates/task.html index 8a8e3b3..be76d39 100755 --- a/templates/task.html +++ b/templates/task.html @@ -14,11 +14,13 @@

({{ score }} points, {{ task.desc|safe }} {% endif %}

+ {% if task.file %}

{{ lang.task.attachment }}: {{ task.file }}

+ {% endif %} From bef6bbd5a94422d4e0fb9581b60af85716657654 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 00:10:43 -0600 Subject: [PATCH 05/64] Modified the database structure. Modified tasks and categories database layout to allow multiple tasks with same point value. Added buildTables.sh to create tables (prevents table not found exceptions). Modified the tasks retrieval and grid building code. Modified the tasks template code. --- buildTables.sh | 4 ++ server.py | 92 ++++++++++++++++++++++---------------------- templates/tasks.html | 3 +- 3 files changed, 51 insertions(+), 48 deletions(-) create mode 100755 buildTables.sh diff --git a/buildTables.sh b/buildTables.sh new file mode 100755 index 0000000..53db0d1 --- /dev/null +++ b/buildTables.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +sqlite3 ctf.db 'CREATE TABLE categories ( id INTEGER PRIMARY KEY, short_name TEXT, name TEXT );' +sqlite3 ctf.db 'CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT, desc TEXT, file TEXT, flag TEXT, score INT, category INT, FOREIGN KEY(category) REFERENCES categories(id));' diff --git a/server.py b/server.py index 6c19389..60cde3b 100755 --- a/server.py +++ b/server.py @@ -38,7 +38,7 @@ def decorated_function(*args, **kwargs): def get_user(): """Looks up the current user in the database""" - login = 'user_id' in session + login = 'user_id' in session if login: return (True, db['users'].find_one(id=session['user_id'])) @@ -47,8 +47,8 @@ def get_user(): def get_task(category, score): """Finds a task with a given category and score""" - task = db.query('''select t.* from tasks t, categories c, cat_task ct - where t.id = ct.task_id and c.id = ct.cat_id + task = db.query('''select t.* from tasks t, categories c, cat_task ct + where t.id = ct.task_id and c.id = ct.cat_id and t.score=:score and lower(c.short_name)=:cat''', score=score, cat=category) return list(task)[0] @@ -56,12 +56,12 @@ def get_task(category, score): def get_flags(): """Returns the flags of the current user""" - flags = db.query('''select f.task_id from flags f + flags = db.query('''select f.task_id from flags f where f.user_id = :user_id''', user_id=session['user_id']) return [f['task_id'] for f in list(flags)] -@app.route('/error/') +@app.route('/error/') def error(msg): """Displays an error message""" @@ -72,7 +72,7 @@ def error(msg): login, user = get_user() - render = render_template('frame.html', lang=lang, page='error.html', + render = render_template('frame.html', lang=lang, page='error.html', message=message, login=login, user=user) return make_response(render) @@ -105,9 +105,9 @@ def register(): """Displays the register form""" # Render template - render = render_template('frame.html', lang=lang, + render = render_template('frame.html', lang=lang, page='register.html', login=False) - return make_response(render) + return make_response(render) @app.route('/register/submit', methods = ['POST']) def register_submit(): @@ -124,9 +124,9 @@ def register_submit(): user_found = db['users'].find_one(username=username) if user_found: return redirect('/error/already_registered') - - new_user = dict(hidden=0, username=username, - password=generate_password_hash(password)) + + new_user = dict(hidden=0, username=username, + password=generate_password_hash(password), isAdmin=False) db['users'].insert(new_user) # Set up the user id for this session @@ -143,37 +143,35 @@ def tasks(): flags = get_flags() categories = db['categories'] + catCount = categories.count() + + tasks = db.query("SELECT * FROM tasks ORDER BY category, score"); - tasks = db.query('''select c.id as cat_id, t.id as id, c.short_name, - t.score, t.row from categories c, tasks t, cat_task c_t - where c.id = c_t.cat_id and t.id = c_t.task_id''') tasks = list(tasks) grid = [] - # Find the max row number - max_row = max(t['row'] for t in tasks) - - for row in range(max_row + 1): - row_tasks = [] - for cat in categories: + rowCount = 0 + currentCat = 0 + currentCatCount = 0 + for i, task in enumerate(tasks): + cat = task["category"] - 1 + if currentCat != cat: + currentCat = cat + currentCatCount = 0 - # Find the task with the correct row - for task in tasks: - if task['row'] == row and task['cat_id'] == cat['id']: - break - else: - task = None + if currentCatCount >= rowCount: + row = [None] * catCount + grid.append(row) - row_tasks.append(task) - - grid.append(row_tasks) + grid[currentCatCount][cat] = task + currentCatCount += 1 # Render template - render = render_template('frame.html', lang=lang, page='tasks.html', - login=login, user=user, categories=categories, grid=grid, + render = render_template('frame.html', lang=lang, page='tasks.html', + login=login, user=user, categories=categories, grid=grid, flags=flags) - return make_response(render) + return make_response(render) @app.route('/tasks//') @login_required @@ -193,7 +191,7 @@ def task(category, score): solutions = len(list(solutions)) # Render template - render = render_template('frame.html', lang=lang, page='task.html', + render = render_template('frame.html', lang=lang, page='task.html', task_done=task_done, login=login, solutions=solutions, user=user, category=category, task=task, score=score) return make_response(render) @@ -217,7 +215,7 @@ def submit(category, score, flag): timestamp = int(time.time() * 1000) # Insert flag - new_flag = dict(task_id=task['id'], user_id=session['user_id'], + new_flag = dict(task_id=task['id'], user_id=session['user_id'], score=score, timestamp=timestamp) db['flags'].insert(new_flag) @@ -231,17 +229,17 @@ def scoreboard(): """Displays the scoreboard""" login, user = get_user() - scores = db.query('''select u.username, ifnull(sum(f.score), 0) as score, - max(timestamp) as last_submit from users u left join flags f - on u.id = f.user_id where u.hidden = 0 group by u.username + scores = db.query('''select u.username, ifnull(sum(f.score), 0) as score, + max(timestamp) as last_submit from users u left join flags f + on u.id = f.user_id where u.hidden = 0 group by u.username order by score desc, last_submit asc''') scores = list(scores) # Render template - render = render_template('frame.html', lang=lang, page='scoreboard.html', + render = render_template('frame.html', lang=lang, page='scoreboard.html', login=login, user=user, scores=scores) - return make_response(render) + return make_response(render) @app.route('/about') @login_required @@ -251,9 +249,9 @@ def about(): login, user = get_user() # Render template - render = render_template('frame.html', lang=lang, page='about.html', + render = render_template('frame.html', lang=lang, page='about.html', login=login, user=user) - return make_response(render) + return make_response(render) @app.route('/logout') @login_required @@ -270,7 +268,7 @@ def index(): login, user = get_user() # Render template - render = render_template('frame.html', lang=lang, + render = render_template('frame.html', lang=lang, page='main.html', login=login, user=user) return make_response(render) @@ -296,13 +294,13 @@ def index(): # Setup the flags table at first execution if 'flags' not in db.tables: db.query('''create table flags ( - task_id INTEGER, - user_id INTEGER, - score INTEGER, - timestamp BIGINT, + task_id INTEGER, + user_id INTEGER, + score INTEGER, + timestamp BIGINT, PRIMARY KEY (task_id, user_id))''') # Start web server - app.run(host=config['host'], port=config['port'], + app.run(host=config['host'], port=config['port'], debug=config['debug'], threaded=True) diff --git a/templates/tasks.html b/templates/tasks.html index fa8ea59..03e7118 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -6,13 +6,14 @@ {{ category.name }}
{% endfor %} +
{% for row in grid %}
{% for task in row %} {% if task %} - +
{{ task.score }}
From cbdec0a9dbcff7a1681fee04d8cab4e4883132be Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 00:27:22 -0600 Subject: [PATCH 06/64] Updated task route to accommodate database changes. --- server.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/server.py b/server.py index 60cde3b..839eba6 100755 --- a/server.py +++ b/server.py @@ -44,13 +44,12 @@ def get_user(): return (False, None) -def get_task(category, score): +def get_task(tid): """Finds a task with a given category and score""" - task = db.query('''select t.* from tasks t, categories c, cat_task ct - where t.id = ct.task_id and c.id = ct.cat_id - and t.score=:score and lower(c.short_name)=:cat''', - score=score, cat=category) + task = db.query("SELECT t.*, c.name cat_name FROM tasks t JOIN categories c on c.id = t.category WHERE t.id = :tid", + tid=tid) + return list(task)[0] def get_flags(): @@ -173,14 +172,14 @@ def tasks(): flags=flags) return make_response(render) -@app.route('/tasks//') +@app.route('/tasks//') @login_required -def task(category, score): +def task(tid): """Displays a task with a given category and score""" login, user = get_user() - task = get_task(category, score) + task = get_task(tid) if not task: return redirect('/error/task_not_found') @@ -193,7 +192,7 @@ def task(category, score): # Render template render = render_template('frame.html', lang=lang, page='task.html', task_done=task_done, login=login, solutions=solutions, - user=user, category=category, task=task, score=score) + user=user, category=task["cat_name"], task=task, score=task["score"]) return make_response(render) @app.route('/submit///') From 608ac046fcca1155d946c20f5f8f02fb9555be66 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 00:58:08 -0600 Subject: [PATCH 07/64] Removed unneeded enumerate. --- server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server.py b/server.py index 839eba6..ac2809e 100755 --- a/server.py +++ b/server.py @@ -153,7 +153,7 @@ def tasks(): rowCount = 0 currentCat = 0 currentCatCount = 0 - for i, task in enumerate(tasks): + for task in tasks: cat = task["category"] - 1 if currentCat != cat: currentCat = cat @@ -195,16 +195,16 @@ def task(tid): user=user, category=task["cat_name"], task=task, score=task["score"]) return make_response(render) -@app.route('/submit///') +@app.route('/submit//') @login_required -def submit(category, score, flag): +def submit(tid, flag): """Handles the submission of flags""" print "ok" login, user = get_user() - task = get_task(category, score) + task = get_task(tid) flags = get_flags() task_done = task['id'] in flags @@ -215,7 +215,7 @@ def submit(category, score, flag): # Insert flag new_flag = dict(task_id=task['id'], user_id=session['user_id'], - score=score, timestamp=timestamp) + score=task["score"], timestamp=timestamp) db['flags'].insert(new_flag) result['success'] = True From 395a768e0002acb53364b5ebd7229f89cc36e632 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 10:47:18 -0600 Subject: [PATCH 08/64] Updated tasks code to show add buttons for categories and tasks. --- server.py | 17 ++++++++++-- templates/tasks.html | 65 ++++++++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/server.py b/server.py index ac2809e..385d138 100755 --- a/server.py +++ b/server.py @@ -153,19 +153,30 @@ def tasks(): rowCount = 0 currentCat = 0 currentCatCount = 0 + for task in tasks: cat = task["category"] - 1 - if currentCat != cat: - currentCat = cat - currentCatCount = 0 if currentCatCount >= rowCount: row = [None] * catCount grid.append(row) + rowCount += 1 + + if currentCat != cat: + endTask = { "end": True, "category": currentCat }; + grid[currentCatCount][currentCat] = endTask + currentCat = cat + currentCatCount = 0 grid[currentCatCount][cat] = task currentCatCount += 1 + #add the final endTask element + endTask = { "end": True, "category": currentCat }; + grid[currentCatCount][currentCat] = endTask + + + # Render template render = render_template('frame.html', lang=lang, page='tasks.html', login=login, user=user, categories=categories, grid=grid, diff --git a/templates/tasks.html b/templates/tasks.html index 03e7118..dd63074 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -1,33 +1,46 @@
- + + {% for row in grid %} +
+ {% for task in row %} + {% if task.id %} + +
+ {{ task.score }} +
+
+ {% elif task.end and user.isAdmin %} + +
+ + +
+
+ {% else %} +
+   +
+ {% endif %} + {% endfor %} +
+ {% endfor %} - {% for row in grid %} -
- {% for task in row %} - {% if task %} - -
- {{ task.score }} -
-
- {% else %} -
-   -
- {% endif %} - {% endfor %} -
- {% endfor %} -
+ type='text/javascript'> From b743c341574c81844254f0fc511d99694e54c486 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 10:51:52 -0600 Subject: [PATCH 09/64] Added admin_required decorator and associated error --- lang.json | 6 ++++-- server.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lang.json b/lang.json index 53952cd..ce9d9cf 100755 --- a/lang.json +++ b/lang.json @@ -28,9 +28,10 @@ "submit": "Submit", "success": "Correct flag!", "failure": "Incorrect flag", + "points_format": "%d points", "solution_format": "solved by %d", "no_description": "(none)", - "placeholder": "flag{insert_flag_here}" + "placeholder": "Flag goes here" }, "scoreboard": { "title": "Scoreboard", @@ -41,10 +42,11 @@ "title": "Error", "unknown": "Unknown error", "login_required": "You need to be logged in to see this page", + "login_required": "You need to be an admin to see this page", "invalid_credentials": "Invalid username or password", "already_registered": "This user is already registered", "empty_user": "Empty username is not allowed", "task_not_found": "TBD" } } -} \ No newline at end of file +} diff --git a/server.py b/server.py index 385d138..8c53e08 100755 --- a/server.py +++ b/server.py @@ -35,6 +35,19 @@ def decorated_function(*args, **kwargs): return f(*args, **kwargs) return decorated_function +def admin_required(f): + """Ensures that an user is logged in""" + + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return redirect(url_for('error', msg='login_required')) + user = get_user() + if user["isAdmin"] == False: + return redirect(url_for('error', msg='admin_required')) + return f(*args, **kwargs) + return decorated_function + def get_user(): """Looks up the current user in the database""" From 5ce38cfc3533ceceaa82d64aaa9ed135c2b0949c Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 11:11:52 -0600 Subject: [PATCH 10/64] Updated get_user code, calls, and template. --- server.py | 29 ++++++++++++++--------------- templates/frame.html | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/server.py b/server.py index 8c53e08..ebd4042 100755 --- a/server.py +++ b/server.py @@ -53,9 +53,9 @@ def get_user(): login = 'user_id' in session if login: - return (True, db['users'].find_one(id=session['user_id'])) + return db['users'].find_one(id=session['user_id']) - return (False, None) + return None def get_task(tid): """Finds a task with a given category and score""" @@ -82,10 +82,10 @@ def error(msg): else: message = lang['error']['unknown'] - login, user = get_user() + user = get_user() render = render_template('frame.html', lang=lang, page='error.html', - message=message, login=login, user=user) + message=message, user=user) return make_response(render) def session_login(username): @@ -151,7 +151,7 @@ def register_submit(): def tasks(): """Displays all the tasks in a grid""" - login, user = get_user() + user = get_user() flags = get_flags() categories = db['categories'] @@ -192,8 +192,7 @@ def tasks(): # Render template render = render_template('frame.html', lang=lang, page='tasks.html', - login=login, user=user, categories=categories, grid=grid, - flags=flags) + user=user, categories=categories, grid=grid, flags=flags) return make_response(render) @app.route('/tasks//') @@ -201,7 +200,7 @@ def tasks(): def task(tid): """Displays a task with a given category and score""" - login, user = get_user() + user = get_user() task = get_task(tid) if not task: @@ -226,7 +225,7 @@ def submit(tid, flag): print "ok" - login, user = get_user() + user = get_user() task = get_task(tid) flags = get_flags() @@ -251,7 +250,7 @@ def submit(tid, flag): def scoreboard(): """Displays the scoreboard""" - login, user = get_user() + user = get_user() scores = db.query('''select u.username, ifnull(sum(f.score), 0) as score, max(timestamp) as last_submit from users u left join flags f on u.id = f.user_id where u.hidden = 0 group by u.username @@ -261,7 +260,7 @@ def scoreboard(): # Render template render = render_template('frame.html', lang=lang, page='scoreboard.html', - login=login, user=user, scores=scores) + user=user, scores=scores) return make_response(render) @app.route('/about') @@ -269,11 +268,11 @@ def scoreboard(): def about(): """Displays the about menu""" - login, user = get_user() + user = get_user() # Render template render = render_template('frame.html', lang=lang, page='about.html', - login=login, user=user) + user=user) return make_response(render) @app.route('/logout') @@ -288,11 +287,11 @@ def logout(): def index(): """Displays the main page""" - login, user = get_user() + user = get_user() # Render template render = render_template('frame.html', lang=lang, - page='main.html', login=login, user=user) + page='main.html', user=user) return make_response(render) if __name__ == '__main__': diff --git a/templates/frame.html b/templates/frame.html index 8b0cc59..d355868 100755 --- a/templates/frame.html +++ b/templates/frame.html @@ -22,7 +22,7 @@ {{ lang.frame.title }}
- {% if not login %} + {% if not user %} - \ No newline at end of file + From 0d3505c47d4f3b422eea2e103a3bce03d2453f7c Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 12:00:27 -0600 Subject: [PATCH 11/64] Fixed missing closing tag --- templates/frame.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/frame.html b/templates/frame.html index d355868..27cf481 100755 --- a/templates/frame.html +++ b/templates/frame.html @@ -44,6 +44,7 @@ {{ lang.frame.logged_in }} {{ user.username }} | {{ lang.frame.logout}} + {% endif %} From 791b3b0f61fed2f205adc6696b1b54453a75adbf Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 12:03:54 -0600 Subject: [PATCH 12/64] Added functionaly to add categories --- server.py | 22 ++++++++++++++++++++++ templates/addcat.html | 14 ++++++++++++++ 2 files changed, 36 insertions(+) create mode 100755 templates/addcat.html diff --git a/server.py b/server.py index ebd4042..9fd37aa 100755 --- a/server.py +++ b/server.py @@ -195,6 +195,28 @@ def tasks(): user=user, categories=categories, grid=grid, flags=flags) return make_response(render) +@app.route('/addcat/', methods=['GET']) +@admin_required +def addcat(): + if request.method == 'GET': + user = get_user() + render = render_template('frame.html', lang=lang, user=user, page='addcat.html') + return make_response(render) + + +@app.route('/addcat/', methods=['POST']) +@admin_required +def addcatsubmit(): + if request.method == 'POST': + name = request.form['name'] + if name: + categories = db['categories'] + categories.insert(dict(name=name)) + return redirect('/tasks') + else: + return redirect('/error/form') + + @app.route('/tasks//') @login_required def task(tid): diff --git a/templates/addcat.html b/templates/addcat.html new file mode 100755 index 0000000..6afc2a7 --- /dev/null +++ b/templates/addcat.html @@ -0,0 +1,14 @@ +
+
+
+

{{ lang.catnew.title }}

+
+ + + +
+
+ +
+
+ From 8d0bb7b33ec50021a5267221269afe3fbae8d76f Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 12:07:37 -0600 Subject: [PATCH 13/64] Moved admin check to grid build from template. --- server.py | 12 ++++++------ templates/tasks.html | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 9fd37aa..82e3f3d 100755 --- a/server.py +++ b/server.py @@ -176,8 +176,9 @@ def tasks(): rowCount += 1 if currentCat != cat: - endTask = { "end": True, "category": currentCat }; - grid[currentCatCount][currentCat] = endTask + if user['isAdmin']: + endTask = { "end": True, "category": currentCat }; + grid[currentCatCount][currentCat] = endTask currentCat = cat currentCatCount = 0 @@ -185,10 +186,9 @@ def tasks(): currentCatCount += 1 #add the final endTask element - endTask = { "end": True, "category": currentCat }; - grid[currentCatCount][currentCat] = endTask - - + if user['isAdmin']: + endTask = { "end": True, "category": currentCat }; + grid[currentCatCount][currentCat] = endTask # Render template render = render_template('frame.html', lang=lang, page='tasks.html', diff --git a/templates/tasks.html b/templates/tasks.html index dd63074..f62618e 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -25,7 +25,7 @@ {{ task.score }} - {% elif task.end and user.isAdmin %} + {% elif task.end %}
+ From e67b0b1c86d4803c6ea19080ee6d50714f500552 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 12:17:51 -0600 Subject: [PATCH 14/64] Added logic to allow add-box for empty categories --- server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 82e3f3d..270ed58 100755 --- a/server.py +++ b/server.py @@ -177,7 +177,7 @@ def tasks(): if currentCat != cat: if user['isAdmin']: - endTask = { "end": True, "category": currentCat }; + endTask = { "end": True, "category": currentCat } grid[currentCatCount][currentCat] = endTask currentCat = cat currentCatCount = 0 @@ -187,9 +187,16 @@ def tasks(): #add the final endTask element if user['isAdmin']: - endTask = { "end": True, "category": currentCat }; + endTask = { "end": True, "category": currentCat } grid[currentCatCount][currentCat] = endTask + #if any None in first row, add end task + for i, t in enumerate(grid[0]): + if t is None: + endTask = { "end": True, "category": i } + grid[0][i] = endTask + + # Render template render = render_template('frame.html', lang=lang, page='tasks.html', user=user, categories=categories, grid=grid, flags=flags) From 6b515e0bc4f8c6b4e84dd09cf8458a91b753d06d Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 17:24:42 -0600 Subject: [PATCH 15/64] Added task creation functionality. Also misc tweaks to fit new database layout. --- lang.json | 15 ++++++++++- server.py | 60 +++++++++++++++++++++++++++++++++---------- templates/addcat.html | 4 +-- templates/task.html | 4 +-- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/lang.json b/lang.json index ce9d9cf..df2e15e 100755 --- a/lang.json +++ b/lang.json @@ -33,6 +33,18 @@ "no_description": "(none)", "placeholder": "Flag goes here" }, + "addcat" : { + "title": "Add a Category", + "name_label": "Category Name: " + }, + "addtask" : { + "title": "Add a Task", + "cat_label": "Category: ", + "name_label": "Task Name: ", + "description_label": "Description: ", + "flag_label": "Flag: ", + "score_label": "Score: " + }, "scoreboard": { "title": "Scoreboard", "player": "Player", @@ -46,7 +58,8 @@ "invalid_credentials": "Invalid username or password", "already_registered": "This user is already registered", "empty_user": "Empty username is not allowed", - "task_not_found": "TBD" + "task_not_found": "TBD", + "form": "Invalid form input" } } } diff --git a/server.py b/server.py index 270ed58..07adb57 100755 --- a/server.py +++ b/server.py @@ -77,7 +77,7 @@ def get_flags(): def error(msg): """Displays an error message""" - if msg in lang: + if msg in lang['error']: message = lang['error'][msg] else: message = lang['error']['unknown'] @@ -205,24 +205,58 @@ def tasks(): @app.route('/addcat/', methods=['GET']) @admin_required def addcat(): - if request.method == 'GET': - user = get_user() - render = render_template('frame.html', lang=lang, user=user, page='addcat.html') - return make_response(render) - + user = get_user() + render = render_template('frame.html', lang=lang, user=user, page='addcat.html') + return make_response(render) @app.route('/addcat/', methods=['POST']) @admin_required def addcatsubmit(): - if request.method == 'POST': + try: name = request.form['name'] - if name: - categories = db['categories'] - categories.insert(dict(name=name)) - return redirect('/tasks') - else: - return redirect('/error/form') + except KeyError: + return redirect('/error/form') + else: + categories = db['categories'] + categories.insert(dict(name=name)) + return redirect('/tasks') + +@app.route('/addtask//', methods=['GET']) +@admin_required +def addtask(cat): + category = db.query('SELECT * FROM categories LIMIT 1 OFFSET :cat', cat=cat) + category = list(category) + category = category[0] + user = get_user() + + render = render_template('frame.html', lang=lang, user=user, + cat_name=category['name'], cat_id=category['id'], page='addtask.html') + return make_response(render) + +@app.route('/addtask//', methods=['POST']) +@admin_required +def addtasksubmit(cat): + try: + name = request.form['name'] + desc = request.form['desc'] + category = int(request.form['category']) + score = int(request.form['score']) + flag = request.form['flag'] + except KeyError: + return redirect('/error/form') + + else: + tasks = db['tasks'] + task = dict( + name=name, + desc=desc, + category=category, + score=score, + flag=flag) + + tasks.insert(task) + return redirect('/tasks') @app.route('/tasks//') @login_required diff --git a/templates/addcat.html b/templates/addcat.html index 6afc2a7..f0c6e91 100755 --- a/templates/addcat.html +++ b/templates/addcat.html @@ -1,9 +1,9 @@
-

{{ lang.catnew.title }}

+

{{ lang.addcat.title }}

- +
diff --git a/templates/task.html b/templates/task.html index be76d39..4db538d 100755 --- a/templates/task.html +++ b/templates/task.html @@ -1,7 +1,6 @@
-
+

{{ task.name }}

({{ score }} points, {{ lang.task.solution_format % solutions }})
@@ -21,6 +20,7 @@
({{ score }} points, filename='files/'+task.file) }}">{{ task.file }}

{% endif %} + From 58086e36c13cdf0f49023b827b9728498fb48970 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 17:26:59 -0600 Subject: [PATCH 16/64] Client side changes to match server side --- static/css/ctf.css | 14 +++++++++++++- static/js/submit.js | 11 ++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/static/css/ctf.css b/static/css/ctf.css index 8678835..33218b3 100755 --- a/static/css/ctf.css +++ b/static/css/ctf.css @@ -88,6 +88,18 @@ background-size: cover; color: #333; } +.add-box { + margin: 50px; + width: 60px; + height: 60px; + padding-top: 3px; + border-radius: 50%; + text-align:center; + background-color: rgba(0,0,0,0.25); + font-size: 30pt; + color: white; +} + .chall-category { background-color: rgba(0, 0, 0, 0.5); color: white; @@ -127,4 +139,4 @@ background-size: cover; .task-box h5 { text-align: center; -} \ No newline at end of file +} diff --git a/static/js/submit.js b/static/js/submit.js index 93c6047..6ca2e27 100755 --- a/static/js/submit.js +++ b/static/js/submit.js @@ -1,13 +1,10 @@ $("#flag-submission").click(function() { + var flag = $("#flag-input").val(); - var cat = $(".task-box").data("category"); - var score = $(".task-box").data("score"); - var flag = $("#flag-input").val(); - - console.log("/submit/" + cat + "/" + score + "/" + btoa(flag)); + var id = $("#task-id").val() $.ajax({ - url: "/submit/" + cat + "/" + score + "/" + btoa(flag) + url: "/submit/" + id + "/" + btoa(flag) }).done(function(data) { console.log(data); @@ -21,4 +18,4 @@ $("#flag-submission").click(function() { $("#flag-input").val($(".lang").data("failure")); } }); -}); \ No newline at end of file +}); From 67e88c84013270805f17cf91639c66c40f3df282 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 17:27:39 -0600 Subject: [PATCH 17/64] Task addition template --- templates/addtask.html | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 templates/addtask.html diff --git a/templates/addtask.html b/templates/addtask.html new file mode 100755 index 0000000..db450e9 --- /dev/null +++ b/templates/addtask.html @@ -0,0 +1,28 @@ +
+
+
+

{{ lang.addtask.title }}

+
{{ lang.addtask.cat_label}}{{ cat_name }}
+
+ + + + + + + + + + + + + + + + +
+
+ +
+
+
From 8fd1c18f8b85253d98045d5b5d2654a8fe8b72a5 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 29 Jan 2016 17:29:56 -0600 Subject: [PATCH 18/64] Removed short_name from categories table. --- buildTables.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildTables.sh b/buildTables.sh index 53db0d1..a9395fe 100755 --- a/buildTables.sh +++ b/buildTables.sh @@ -1,4 +1,4 @@ #!/bin/bash -sqlite3 ctf.db 'CREATE TABLE categories ( id INTEGER PRIMARY KEY, short_name TEXT, name TEXT );' +sqlite3 ctf.db 'CREATE TABLE categories ( id INTEGER PRIMARY KEY, name TEXT );' sqlite3 ctf.db 'CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT, desc TEXT, file TEXT, flag TEXT, score INT, category INT, FOREIGN KEY(category) REFERENCES categories(id));' From 72d7a78afaac95dc7a0a4fbec1639c2bed205602 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 31 Jan 2016 18:03:25 -0600 Subject: [PATCH 19/64] Added percentage ring around tasks. --- server.py | 19 +++++++++-- static/css/ctf.css | 75 +++++++++++++++++++++++++++++++++++++------- templates/tasks.html | 8 ++--- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/server.py b/server.py index 07adb57..da8be5c 100755 --- a/server.py +++ b/server.py @@ -152,11 +152,13 @@ def tasks(): """Displays all the tasks in a grid""" user = get_user() - flags = get_flags() + userCount = db['users'].count() categories = db['categories'] catCount = categories.count() + flags = db['flags'] + tasks = db.query("SELECT * FROM tasks ORDER BY category, score"); tasks = list(tasks) @@ -182,6 +184,19 @@ def tasks(): currentCat = cat currentCatCount = 0 + + percentComplete = (float(flags.count(task_id=task['id'])) / userCount) * 100 + + #hax for bad css (if 100, nothing will show) + if percentComplete == 100: + percentComplete = 99.99 + + task['percentComplete'] = percentComplete + + isComplete = bool(flags.count(task_id=task['id'], user_id=user['id'])) + + task['isComplete'] = isComplete + grid[currentCatCount][cat] = task currentCatCount += 1 @@ -199,7 +214,7 @@ def tasks(): # Render template render = render_template('frame.html', lang=lang, page='tasks.html', - user=user, categories=categories, grid=grid, flags=flags) + user=user, categories=categories, grid=grid) return make_response(render) @app.route('/addcat/', methods=['GET']) diff --git a/static/css/ctf.css b/static/css/ctf.css index 33218b3..d869541 100755 --- a/static/css/ctf.css +++ b/static/css/ctf.css @@ -76,18 +76,60 @@ background-size: cover; * Tasks page */ + +span { + z-index:100; + position:relative; + width: 130px; + left: 0; + top: 40px; + text-align:center; + font-size: 20pt +} .chall-box { - margin: 20px; - width: 120px; - height: 120px; - padding-top: 45px; - border-radius: 50%; - text-align:center; - background-color: rgba(255, 255, 255, 0.75); - font-size: 16pt; - color: #333; + margin: 20px; + display: inline-block; + position: relative; + width: 120px; + height: 120px; + border-radius: 50%; + background-color: #e5e5e5; + background-image: linear-gradient(to right, transparent 50%, green 0); + color: black; + text-align: center; + +} + +@keyframes spin { + to { transform: rotate(.5turn); } +} +@keyframes bg { + 50% { background: green; } +} + +.chall-box::before { + content: ''; + position: absolute; + top: 0; left: 50%; + width: 50%; height: 100%; + border-radius: 0 100% 100% 0 / 50%; + background-color: inherit; + transform-origin: left; + animation: spin 50s linear infinite, bg 100s step-end infinite; + animation-play-state: paused; + animation-delay: inherit; +} + +.chall-box::after{ + content: ''; + top: 7px; left: 7px; + width: 106px; height: 106px; + position:absolute; + border-radius: 100%; + background-color: #ccc; } + .add-box { margin: 50px; width: 60px; @@ -101,13 +143,20 @@ background-size: cover; } .chall-category { + margin: 20px; + width: 119px; + height: 119px; + padding-top: 45px; + border-radius: 50%; + text-align: center; background-color: rgba(0, 0, 0, 0.5); + font-size: 16pt; color: white; } .chall-done { - -webkit-box-shadow: 0 0 10px 3px #51A351; - box-shadow: 0 0 10px 3px #51A351; + -webkit-box-shadow: 0 0 10px 5px #51A351; + box-shadow: 0 0 10px 5px #51A351; } .chall-highlight { @@ -116,6 +165,10 @@ background-size: cover; } .chall-hidden { + margin: 20px; + width: 120px; + height: 120px; + padding-top: 45px; background-color: rgba(255, 255, 255, 0); } diff --git a/templates/tasks.html b/templates/tasks.html index f62618e..8e5e439 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -2,7 +2,7 @@
{% for category in categories %} -
+
{{ category.name }}
{% endfor %} @@ -21,8 +21,8 @@ {% for task in row %} {% if task.id %}
-
- {{ task.score }} +
+ {{ task.score }}
{% elif task.end %} @@ -32,7 +32,7 @@
{% else %} -
+
 
{% endif %} From 8a8881c8314f81b1bc13341c2e45eade894d4355 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 31 Jan 2016 23:28:01 -0600 Subject: [PATCH 20/64] Added class to chall-box span --- static/css/ctf.css | 2 +- templates/tasks.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/css/ctf.css b/static/css/ctf.css index d869541..ea3f6b9 100755 --- a/static/css/ctf.css +++ b/static/css/ctf.css @@ -77,7 +77,7 @@ background-size: cover; */ -span { +.chall-box-score { z-index:100; position:relative; width: 130px; diff --git a/templates/tasks.html b/templates/tasks.html index 8e5e439..e1ba1a2 100755 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -22,7 +22,7 @@ {% if task.id %}
- {{ task.score }} + {{ task.score }}
{% elif task.end %} From db1dfc7c9801f176d011b2e028a31026f097453e Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 31 Jan 2016 23:32:00 -0600 Subject: [PATCH 21/64] Added basic edit functionality. Added sanitization to add and edit task functions. --- lang.json | 3 ++- server.py | 47 ++++++++++++++++++++++++++++++++++++++--- templates/addtask.html | 2 +- templates/edittask.html | 29 +++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100755 templates/edittask.html diff --git a/lang.json b/lang.json index df2e15e..4465b88 100755 --- a/lang.json +++ b/lang.json @@ -38,7 +38,8 @@ "name_label": "Category Name: " }, "addtask" : { - "title": "Add a Task", + "add_title": "Add a Task", + "edit_title": "Edit a Task", "cat_label": "Category: ", "name_label": "Task Name: ", "description_label": "Description: ", diff --git a/server.py b/server.py index da8be5c..3d6e9f5 100755 --- a/server.py +++ b/server.py @@ -9,6 +9,8 @@ from base64 import b64decode from functools import wraps +from htmllaundry import sanitize +from htmllaundry import strip_markup from flask import Flask from flask import jsonify @@ -228,7 +230,7 @@ def addcat(): @admin_required def addcatsubmit(): try: - name = request.form['name'] + name = strip_markup(request.form['name']) except KeyError: return redirect('/error/form') else: @@ -253,8 +255,8 @@ def addtask(cat): @admin_required def addtasksubmit(cat): try: - name = request.form['name'] - desc = request.form['desc'] + name = strip_markup(request.form['name']) + desc = sanitize(request.form['desc']) category = int(request.form['category']) score = int(request.form['score']) flag = request.form['flag'] @@ -273,6 +275,45 @@ def addtasksubmit(cat): tasks.insert(task) return redirect('/tasks') +@app.route('/tasks//edit', methods=['GET']) +@admin_required +def edittask(tid): + user = get_user() + + task = db["tasks"].find_one(id=tid); + category = db["categories"].find_one(id=task['category']) + + render = render_template('frame.html', lang=lang, user=user, + cat_name=category['name'], cat_id=category['id'], + page='edittask.html', task=task) + return make_response(render) + +@app.route('/tasks//edit', methods=['POST']) +@admin_required +def edittasksubmit(tid): + try: + name = strip_markup(request.form['name']) + desc = sanitize(request.form['desc']) + category = int(request.form['category']) + score = int(request.form['score']) + flag = request.form['flag'] + except KeyError: + return redirect('/error/form') + + else: + tasks = db['tasks'] + task = dict( + id=tid, + name=name, + desc=desc, + category=category, + score=score, + flag=flag) + + tasks.update(task, ['id']) + return redirect('/tasks') + + @app.route('/tasks//') @login_required def task(tid): diff --git a/templates/addtask.html b/templates/addtask.html index db450e9..d9286c0 100755 --- a/templates/addtask.html +++ b/templates/addtask.html @@ -1,7 +1,7 @@
-

{{ lang.addtask.title }}

+

{{ lang.addtask.add_title }}

{{ lang.addtask.cat_label}}{{ cat_name }}
diff --git a/templates/edittask.html b/templates/edittask.html new file mode 100755 index 0000000..94633a8 --- /dev/null +++ b/templates/edittask.html @@ -0,0 +1,29 @@ +
+
+
+

{{ lang.addtask.edit_title }}

+
{{ lang.addtask.cat_label}}{{ cat_name }}
+ + + + + + + + + + + + + + + + + + + +
+ +
+
+
From 3e9a5c06b51e70bcd367bd8e177afaa19dd81536 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 31 Jan 2016 23:43:21 -0600 Subject: [PATCH 22/64] Added edit button on task if admin. --- static/css/ctf.css | 10 ++++++++++ static/fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes static/fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes templates/task.html | 5 +++++ 4 files changed, 15 insertions(+) create mode 100644 static/fonts/glyphicons-halflings-regular.ttf create mode 100644 static/fonts/glyphicons-halflings-regular.woff diff --git a/static/css/ctf.css b/static/css/ctf.css index ea3f6b9..2a749e5 100755 --- a/static/css/ctf.css +++ b/static/css/ctf.css @@ -193,3 +193,13 @@ background-size: cover; .task-box h5 { text-align: center; } + +#task-edit-button { + position: absolute; + right: 0; + top: 0; + padding: 10px; + font-size: 20pt; + background-color: #ccc; + color: black; +} diff --git a/static/fonts/glyphicons-halflings-regular.ttf b/static/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1413fc609ab6f21774de0cb7e01360095584f65b GIT binary patch literal 45404 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 G`u_lkUM?>H literal 0 HcmV?d00001 diff --git a/templates/task.html b/templates/task.html index 4db538d..62141a5 100755 --- a/templates/task.html +++ b/templates/task.html @@ -1,6 +1,11 @@
+ {% if user.isAdmin %}
+ + + + {% endif %}

{{ task.name }}

({{ score }} points, {{ lang.task.solution_format % solutions }})
From eb2dac754f8263e3ab6293b1df42e602c6ba57de Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 31 Jan 2016 23:44:53 -0600 Subject: [PATCH 23/64] Fixed if isAdmin wrong line --- templates/task.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/task.html b/templates/task.html index 62141a5..f5b514c 100755 --- a/templates/task.html +++ b/templates/task.html @@ -1,7 +1,7 @@
- {% if user.isAdmin %}
+ {% if user.isAdmin %} From 641cbaf5311f67d45ac7c82e2e94f70bc480f338 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 1 Feb 2016 00:16:32 -0600 Subject: [PATCH 24/64] Added file upload functionality to task creation --- server.py | 15 +++++++++++++++ templates/addtask.html | 5 ++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 3d6e9f5..13923b5 100755 --- a/server.py +++ b/server.py @@ -6,6 +6,9 @@ import json import random import time +import hashlib +import datetime +import os from base64 import b64decode from functools import wraps @@ -263,6 +266,7 @@ def addtasksubmit(cat): except KeyError: return redirect('/error/form') + else: tasks = db['tasks'] task = dict( @@ -271,6 +275,17 @@ def addtasksubmit(cat): category=category, score=score, flag=flag) + file = request.files['file'] + + if file: + filename, ext = os.path.splitext(file.filename) + #hash current time for file name + filename = hashlib.md5(str(datetime.datetime.utcnow())).hexdigest() + #if upload has extension, append to filename + if ext: + filename = filename + ext + file.save(os.path.join("static/files/", filename)) + task["file"] = filename tasks.insert(task) return redirect('/tasks') diff --git a/templates/addtask.html b/templates/addtask.html index d9286c0..93fcf36 100755 --- a/templates/addtask.html +++ b/templates/addtask.html @@ -3,7 +3,7 @@

{{ lang.addtask.add_title }}

{{ lang.addtask.cat_label}}{{ cat_name }}
-
+ @@ -15,6 +15,9 @@
{{ lang.addtask.cat_label}}{{ cat_name }}
+ + + From 04ca62b0f15755edf3884ed9a08a049b3bce65d1 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 1 Feb 2016 00:20:16 -0600 Subject: [PATCH 25/64] Remove description

wrapping --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 13923b5..6db5cea 100755 --- a/server.py +++ b/server.py @@ -259,7 +259,7 @@ def addtask(cat): def addtasksubmit(cat): try: name = strip_markup(request.form['name']) - desc = sanitize(request.form['desc']) + desc = sanitize(request.form['desc'], wrap=None) category = int(request.form['category']) score = int(request.form['score']) flag = request.form['flag'] @@ -308,7 +308,7 @@ def edittask(tid): def edittasksubmit(tid): try: name = strip_markup(request.form['name']) - desc = sanitize(request.form['desc']) + desc = sanitize(request.form['desc'], wrap=None) category = int(request.form['category']) score = int(request.form['score']) flag = request.form['flag'] From 1381cae1f994d2d550e89451e81966170dcbd08c Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 1 Feb 2016 00:40:02 -0600 Subject: [PATCH 26/64] Updated and added more SQL for delete cascade. Supports deleting tasks. --- buildTables.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/buildTables.sh b/buildTables.sh index a9395fe..b1ad1a3 100755 --- a/buildTables.sh +++ b/buildTables.sh @@ -1,4 +1,8 @@ #!/bin/bash sqlite3 ctf.db 'CREATE TABLE categories ( id INTEGER PRIMARY KEY, name TEXT );' -sqlite3 ctf.db 'CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT, desc TEXT, file TEXT, flag TEXT, score INT, category INT, FOREIGN KEY(category) REFERENCES categories(id));' +sqlite3 ctf.db 'CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT, desc TEXT, file TEXT, flag TEXT, score INT, category INT, FOREIGN KEY(category) REFERENCES categories(id) ON DELETE CASCADE);' + +sqlite3 ctf.db 'CREATE TABLE users (id INTEGER NOT NULL, username TEXT, "isAdmin" BOOLEAN, password TEXT, hidden INTEGER,PRIMARY KEY (id))'; + +sqlite3 ctf.db 'CREATE TABLE flags (task_id INTEGER, user_id INTEGER, score INTEGER, timestamp BIGINT, ip TEXT, PRIMARY KEY (task_id, user_id), FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE);' From 2922844107f810bc6d2b9de2241991f507f7a394 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 1 Feb 2016 01:00:09 -0600 Subject: [PATCH 27/64] Disable task flag input when solved. --- templates/task.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/task.html b/templates/task.html index f5b514c..c28c364 100755 --- a/templates/task.html +++ b/templates/task.html @@ -27,6 +27,7 @@
({{ score }} points, {% endif %}