mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
Pages functionality improved (#267)
* Pages now support Markdown * Pages now have a preview tab * Adding a media library to Pages
This commit is contained in:
@@ -53,6 +53,28 @@ def admin_pages_view(route):
|
|||||||
return render_template('admin/pages.html', routes=pages, css=utils.get_config('css'))
|
return render_template('admin/pages.html', routes=pages, css=utils.get_config('css'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_pages.route('/admin/media', methods=['GET', 'POST', 'DELETE'])
|
||||||
|
@admins_only
|
||||||
|
def admin_pages_media():
|
||||||
|
if request.method == 'POST':
|
||||||
|
files = request.files.getlist('files[]')
|
||||||
|
|
||||||
|
uploaded = []
|
||||||
|
for f in files:
|
||||||
|
data = utils.upload_file(file=f, chalid=None)
|
||||||
|
if data:
|
||||||
|
uploaded.append({'id': data[0], 'location': data[1]})
|
||||||
|
return jsonify({'results': uploaded})
|
||||||
|
elif request.method == 'DELETE':
|
||||||
|
file_ids = request.form.getlist('file_ids[]')
|
||||||
|
for file_id in file_ids:
|
||||||
|
utils.delete_file(file_id)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
files = [{'id': f.id, 'location': f.location} for f in Files.query.filter_by(chal=None).all()]
|
||||||
|
return jsonify({'results': files})
|
||||||
|
|
||||||
|
|
||||||
@admin_pages.route('/admin/page/<pageroute>/delete', methods=['POST'])
|
@admin_pages.route('/admin/page/<pageroute>/delete', methods=['POST'])
|
||||||
@admins_only
|
@admins_only
|
||||||
def delete_page(pageroute):
|
def delete_page(pageroute):
|
||||||
|
|||||||
@@ -4,10 +4,66 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/static/admin/css/vendor/codemirror.min.css">
|
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/static/admin/css/vendor/codemirror.min.css">
|
||||||
<style>
|
<style>
|
||||||
.row-fluid { margin: 25px; padding-bottom: 25px; }
|
.row-fluid { margin: 25px; padding-bottom: 25px; }
|
||||||
|
.media-item-wrapper { height: 120px; margin: 5px;
|
||||||
|
float: left; border: 1px solid #eee; text-align: center; text-overflow: ellipsis; overflow: hidden;}
|
||||||
|
.media-item-wrapper > a > i {line-height: 90px;}
|
||||||
|
.media-item-title{ font-size: 10px; }
|
||||||
|
#media-item{display: none;}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="media-modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h3 class="text-center">Media Library</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-md-offset-1" id="media-library-list">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<h4 class="text-center">Media Details</h4>
|
||||||
|
<div id="media-item">
|
||||||
|
<div class="row text-center" id="media-icon">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row text-center" id="media-filename">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row form-group">
|
||||||
|
Link: <input class="form-control" type="text" id="media-link">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row form-group text-center">
|
||||||
|
<button class="btn btn-success" id="media-insert">Insert</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<form id="media-library-upload">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="media-files">Upload Files
|
||||||
|
</label>
|
||||||
|
<input type="file" name="files[]" id="media-files" multiple>
|
||||||
|
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||||
|
</form>
|
||||||
|
<div class="pull-right">
|
||||||
|
<button type="submit" class="btn btn-primary" onclick="uploadfiles();">Upload</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for error in errors %}
|
{% for error in errors %}
|
||||||
@@ -32,25 +88,157 @@
|
|||||||
<br>
|
<br>
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
|
||||||
<h3>Content: </h3>
|
<h3>Content: </h3>
|
||||||
<p class="help-block">This is the HTML content of your page</p>
|
<p class="help-block">This is the HTML content of your page</p>
|
||||||
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
|
|
||||||
<button class="btn btn-theme btn-outlined create-challenge pull-right">
|
<ul class="nav nav-tabs" role="tablist" id="content-edit">
|
||||||
{% if page is defined %}
|
<li role="presentation" class="active"><a href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
|
||||||
Update
|
<li role="presentation"><a href="#content-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
|
||||||
{% else %}
|
</ul>
|
||||||
Create
|
|
||||||
{% endif %}
|
<div class="tab-content">
|
||||||
</button>
|
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:400px">
|
||||||
|
<div class="form-group">
|
||||||
|
<br>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" aria-label="...">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-default" id="media-button"><i class="fa fa-camera-retro" aria-hidden="true"></i> Media Library</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-theme btn-outlined create-challenge pull-right">
|
||||||
|
{% if page is defined %}
|
||||||
|
Update
|
||||||
|
{% else %}
|
||||||
|
Create
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<script src="{{ request.script_root }}/static/admin/js/utils.js"></script>
|
||||||
<script src="{{ request.script_root }}/static/admin/js/vendor/codemirror.min.js"></script>
|
<script src="{{ request.script_root }}/static/admin/js/vendor/codemirror.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function uploadfiles(){
|
||||||
|
var form = $('#media-library-upload')[0];
|
||||||
|
var formData = new FormData(form);
|
||||||
|
console.log(formData);
|
||||||
|
$.ajax({
|
||||||
|
url: script_root + '/admin/media',
|
||||||
|
data: formData,
|
||||||
|
type: 'POST',
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: function(data){
|
||||||
|
refreshfiles(data);
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshfiles(data){
|
||||||
|
var data = data.results;
|
||||||
|
var list = $('#media-library-list');
|
||||||
|
var mapping = {
|
||||||
|
// Image Files
|
||||||
|
'png': 'fa-file-image-o',
|
||||||
|
'jpg': 'fa-file-image-o',
|
||||||
|
'jpeg': 'fa-file-image-o',
|
||||||
|
'gif': 'fa-file-image-o',
|
||||||
|
'bmp': 'fa-file-image-o',
|
||||||
|
'svg': 'fa-file-image-o',
|
||||||
|
|
||||||
|
// Text Files
|
||||||
|
'txt': 'fa-file-text-o',
|
||||||
|
|
||||||
|
// Video Files
|
||||||
|
'mov': 'fa-file-video-o',
|
||||||
|
'mp4': 'fa-file-video-o',
|
||||||
|
'wmv': 'fa-file-video-o',
|
||||||
|
'flv': 'fa-file-video-o',
|
||||||
|
'mkv': 'fa-file-video-o',
|
||||||
|
'avi': 'fa-file-video-o',
|
||||||
|
|
||||||
|
// PDF Files
|
||||||
|
'pdf': 'fa-file-pdf-o',
|
||||||
|
|
||||||
|
// Audio Files
|
||||||
|
'mp3': 'fa-file-sound-o',
|
||||||
|
'wav': 'fa-file-sound-o',
|
||||||
|
'aac': 'fa-file-sound-o',
|
||||||
|
|
||||||
|
// Archive Files
|
||||||
|
'zip': 'fa-file-archive-o',
|
||||||
|
'gz': 'fa-file-archive-o',
|
||||||
|
'tar': 'fa-file-archive-o',
|
||||||
|
'7z': 'fa-file-archive-o',
|
||||||
|
'rar': 'fa-file-archive-o',
|
||||||
|
|
||||||
|
// Code Files
|
||||||
|
'py': 'fa-file-code-o',
|
||||||
|
'c': 'fa-file-code-o',
|
||||||
|
'cpp': 'fa-file-code-o',
|
||||||
|
'html': 'fa-file-code-o',
|
||||||
|
'js': 'fa-file-code-o',
|
||||||
|
'rb': 'fa-file-code-o',
|
||||||
|
'go': 'fa-file-code-o',
|
||||||
|
}
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var f = data[i];
|
||||||
|
var ext = f.location.split('.').pop();
|
||||||
|
var fname = f.location.split('/')[1];
|
||||||
|
|
||||||
|
var wrapper = $('<div>').attr('class', 'media-item-wrapper col-md-2')
|
||||||
|
|
||||||
|
var link = $('<a>');
|
||||||
|
link.attr('href', '#');
|
||||||
|
|
||||||
|
if (mapping[ext] == undefined)
|
||||||
|
link.append('<i class="fa fa-file-o fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
|
||||||
|
else
|
||||||
|
link.append('<i class="fa {0} fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
|
||||||
|
|
||||||
|
link.click(function(e){
|
||||||
|
var media_div = $(this).parent();
|
||||||
|
var icon = $(this).find('.fa')[0];
|
||||||
|
var f_loc = media_div.attr('data-location');
|
||||||
|
var fname = media_div.attr('data-filename');
|
||||||
|
$('#media-link').val(f_loc);
|
||||||
|
$('#media-filename').text(fname);
|
||||||
|
|
||||||
|
$('#media-icon').empty()
|
||||||
|
if ($(icon).hasClass('fa-file-image-o')){
|
||||||
|
$('#media-icon').append($('<img>').attr('src', f_loc).attr('width', '100%'));
|
||||||
|
} else {
|
||||||
|
// icon is empty so we need to pull outerHTML
|
||||||
|
$('#media-icon').append(icon.outerHTML);
|
||||||
|
}
|
||||||
|
$('#media-item').show();
|
||||||
|
});
|
||||||
|
wrapper.append(link);
|
||||||
|
wrapper.attr('data-location', script_root + '/files/' + f.location);
|
||||||
|
wrapper.attr('data-id', f.id);
|
||||||
|
wrapper.attr('data-filename', fname);
|
||||||
|
list.append(wrapper);
|
||||||
|
|
||||||
|
wrapper.append($('<span>').attr('class', 'media-item-title').text(fname));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var editor = CodeMirror.fromTextArea(document.getElementById("admin-pages-editor"), {
|
var editor = CodeMirror.fromTextArea(document.getElementById("admin-pages-editor"), {
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
@@ -59,8 +247,43 @@
|
|||||||
theme: 'elegant'
|
theme: 'elegant'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function insert_at_cursor(editor, text) {
|
||||||
|
var doc = editor.getDoc();
|
||||||
|
var cursor = doc.getCursor();
|
||||||
|
doc.replaceRange(text, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#media-insert').click(function(e){
|
||||||
|
var tag = $('#media-icon').children()[0].nodeName.toLowerCase();
|
||||||
|
console.log(tag);
|
||||||
|
var link = $('#media-link').val();
|
||||||
|
var fname = $('#media-filename').text();
|
||||||
|
if (tag == 'img'){
|
||||||
|
var entry = ''.format(fname, link);
|
||||||
|
} else if (tag == 'i'){
|
||||||
|
var entry = '[{0}]({1})'.format(fname, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
insert_at_cursor(editor, entry);
|
||||||
|
});
|
||||||
|
|
||||||
$('#page-edit').submit(function (e){
|
$('#page-edit').submit(function (e){
|
||||||
$(this).attr('action', '{{ request.script_root }}/admin/pages/'+$('#route').val());
|
$(this).attr('action', '{{ request.script_root }}/admin/pages/'+$('#route').val());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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(){
|
||||||
|
$.get(script_root + '/admin/media', function(data){
|
||||||
|
$('#media-library-list').empty();
|
||||||
|
refreshfiles(data);
|
||||||
|
$('#media-modal').modal();
|
||||||
|
});
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
import mistune
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
@@ -32,6 +33,7 @@ from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Co
|
|||||||
|
|
||||||
cache = Cache()
|
cache = Cache()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
|
markdown = mistune.Markdown()
|
||||||
|
|
||||||
|
|
||||||
def init_logs(app):
|
def init_logs(app):
|
||||||
@@ -348,7 +350,7 @@ def upload_file(file, chalid):
|
|||||||
db_f = Files(chalid, (md5hash + '/' + filename))
|
db_f = Files(chalid, (md5hash + '/' + filename))
|
||||||
db.session.add(db_f)
|
db.session.add(db_f)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return db_f.id, (md5hash + '/' + filename)
|
||||||
|
|
||||||
|
|
||||||
def delete_file(file_id):
|
def delete_file(file_id):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from jinja2.exceptions import TemplateNotFound
|
|||||||
from passlib.hash import bcrypt_sha256
|
from passlib.hash import bcrypt_sha256
|
||||||
|
|
||||||
from CTFd.models import db, Teams, Solves, Awards, Files, Pages
|
from CTFd.models import db, Teams, Solves, Awards, Files, Pages
|
||||||
from CTFd.utils import cache
|
from CTFd.utils import cache, markdown
|
||||||
from CTFd import utils
|
from CTFd import utils
|
||||||
|
|
||||||
views = Blueprint('views', __name__)
|
views = Blueprint('views', __name__)
|
||||||
@@ -118,7 +118,7 @@ def static_html(template):
|
|||||||
return render_template('%s.html' % template)
|
return render_template('%s.html' % template)
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
page = Pages.query.filter_by(route=template).first_or_404()
|
page = Pages.query.filter_by(route=template).first_or_404()
|
||||||
return render_template('page.html', content=page.html)
|
return render_template('page.html', content=markdown(page.html))
|
||||||
|
|
||||||
|
|
||||||
@views.route('/teams', defaults={'page': '1'})
|
@views.route('/teams', defaults={'page': '1'})
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ requests==2.13.0
|
|||||||
PyMySQL==0.7.10
|
PyMySQL==0.7.10
|
||||||
gunicorn==19.7.0
|
gunicorn==19.7.0
|
||||||
dataset==0.8.0
|
dataset==0.8.0
|
||||||
|
mistune==0.7.4
|
||||||
|
|||||||
@@ -119,3 +119,10 @@ def gen_tracking(db, ip, team):
|
|||||||
db.session.add(tracking)
|
db.session.add(tracking)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return tracking
|
return tracking
|
||||||
|
|
||||||
|
|
||||||
|
def gen_page(db, route, html):
|
||||||
|
page = Pages(route, html)
|
||||||
|
db.session.add(page)
|
||||||
|
db.session.commit()
|
||||||
|
return page
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ def test_submitting_incorrect_flag():
|
|||||||
|
|
||||||
def test_submitting_unicode_flag():
|
def test_submitting_unicode_flag():
|
||||||
"""Test that users can submit a unicode flag"""
|
"""Test that users can submit a unicode flag"""
|
||||||
print("Test that users can submit a flag")
|
|
||||||
app = create_ctfd()
|
app = create_ctfd()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
register_user(app)
|
register_user(app)
|
||||||
@@ -266,3 +265,17 @@ def test_submitting_unicode_flag():
|
|||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
resp = json.loads(r.data.decode('utf8'))
|
resp = json.loads(r.data.decode('utf8'))
|
||||||
assert resp.get('status') == 1 and resp.get('message') == "Correct"
|
assert resp.get('status') == 1 and resp.get('message') == "Correct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pages_routing_and_rendering():
|
||||||
|
"""Test that pages are routing and rendering"""
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
html = '''##The quick brown fox jumped over the lazy dog'''
|
||||||
|
route = 'test'
|
||||||
|
page = gen_page(app.db, route, html)
|
||||||
|
|
||||||
|
with app.test_client() as client:
|
||||||
|
r = client.get('/test')
|
||||||
|
output = r.get_data(as_text=True)
|
||||||
|
assert "<h2>The quick brown fox jumped over the lazy dog</h2>" in output
|
||||||
|
|||||||
Reference in New Issue
Block a user