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'))
|
||||
|
||||
|
||||
@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'])
|
||||
@admins_only
|
||||
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">
|
||||
<style>
|
||||
.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>
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
{% for error in errors %}
|
||||
@@ -32,25 +88,157 @@
|
||||
<br>
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h3>Content: </h3>
|
||||
<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">
|
||||
{% if page is defined %}
|
||||
Update
|
||||
{% else %}
|
||||
Create
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
{% if page is defined %}
|
||||
Update
|
||||
{% else %}
|
||||
Create
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% 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>
|
||||
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"), {
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
@@ -59,8 +247,43 @@
|
||||
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){
|
||||
$(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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,6 +5,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import mistune
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
@@ -32,6 +33,7 @@ from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Co
|
||||
|
||||
cache = Cache()
|
||||
migrate = Migrate()
|
||||
markdown = mistune.Markdown()
|
||||
|
||||
|
||||
def init_logs(app):
|
||||
@@ -348,7 +350,7 @@ def upload_file(file, chalid):
|
||||
db_f = Files(chalid, (md5hash + '/' + filename))
|
||||
db.session.add(db_f)
|
||||
db.session.commit()
|
||||
return True
|
||||
return db_f.id, (md5hash + '/' + filename)
|
||||
|
||||
|
||||
def delete_file(file_id):
|
||||
|
||||
@@ -6,7 +6,7 @@ from jinja2.exceptions import TemplateNotFound
|
||||
from passlib.hash import bcrypt_sha256
|
||||
|
||||
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
|
||||
|
||||
views = Blueprint('views', __name__)
|
||||
@@ -118,7 +118,7 @@ def static_html(template):
|
||||
return render_template('%s.html' % template)
|
||||
except TemplateNotFound:
|
||||
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'})
|
||||
|
||||
@@ -13,3 +13,4 @@ requests==2.13.0
|
||||
PyMySQL==0.7.10
|
||||
gunicorn==19.7.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.commit()
|
||||
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():
|
||||
"""Test that users can submit a unicode flag"""
|
||||
print("Test that users can submit a flag")
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app)
|
||||
@@ -266,3 +265,17 @@ def test_submitting_unicode_flag():
|
||||
assert r.status_code == 200
|
||||
resp = json.loads(r.data.decode('utf8'))
|
||||
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