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:
Kevin Chung
2017-06-03 14:25:31 -04:00
committed by GitHub
parent 59afacce69
commit 6d9d03e35e
7 changed files with 280 additions and 12 deletions

View File

@@ -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):

View File

@@ -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">&times;</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,9 +88,34 @@
<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>
<ul class="nav nav-tabs" role="tablist" id="content-edit">
<li role="presentation" class="active"><a href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
<li role="presentation"><a href="#content-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
</ul>
<div class="tab-content">
<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> <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>
<button class="btn btn-theme btn-outlined create-challenge pull-right"> <button class="btn btn-theme btn-outlined create-challenge pull-right">
{% if page is defined %} {% if page is defined %}
Update Update
@@ -43,14 +124,121 @@
{% endif %} {% endif %}
</button> </button>
</div> </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 = '![{0}]({1})'.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 %}

View File

@@ -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):

View File

@@ -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'})

View File

@@ -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

View File

@@ -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

View File

@@ -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