mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
Merge pull request #2344 from CTFd/1691-challenge-preview-improvements
* Changes challenge previews to be in a full page context using `page.html` * Closes #1691
This commit is contained in:
@@ -3,7 +3,10 @@ from flask import abort, render_template, request, url_for
|
||||
from CTFd.admin import admin
|
||||
from CTFd.models import Challenges, Flags, Solves
|
||||
from CTFd.plugins.challenges import CHALLENGE_CLASSES, get_chal_class
|
||||
from CTFd.schemas.tags import TagSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.security.signing import serialize
|
||||
from CTFd.utils.user import get_current_team, get_current_user
|
||||
|
||||
|
||||
@admin.route("/admin/challenges")
|
||||
@@ -71,6 +74,43 @@ def challenges_detail(challenge_id):
|
||||
)
|
||||
|
||||
|
||||
@admin.route("/admin/challenges/preview/<int:challenge_id>")
|
||||
@admins_only
|
||||
def challenges_preview(challenge_id):
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
user = get_current_user()
|
||||
team = get_current_team()
|
||||
|
||||
files = []
|
||||
for f in challenge.files:
|
||||
token = {
|
||||
"user_id": user.id,
|
||||
"team_id": team.id if team else None,
|
||||
"file_id": f.id,
|
||||
}
|
||||
files.append(url_for("views.files", path=f.location, token=serialize(token)))
|
||||
|
||||
tags = [
|
||||
tag["value"] for tag in TagSchema("user", many=True).dump(challenge.tags).data
|
||||
]
|
||||
|
||||
content = render_template(
|
||||
chal_class.templates["view"].lstrip("/"),
|
||||
solves=None,
|
||||
solved_by_me=False,
|
||||
files=files,
|
||||
tags=tags,
|
||||
hints=challenge.hints,
|
||||
max_attempts=challenge.max_attempts,
|
||||
attempts=0,
|
||||
challenge=challenge,
|
||||
)
|
||||
return render_template(
|
||||
"admin/challenges/preview.html", content=content, challenge=challenge
|
||||
)
|
||||
|
||||
|
||||
@admin.route("/admin/challenges/new")
|
||||
@admins_only
|
||||
def challenges_new():
|
||||
|
||||
@@ -16,111 +16,6 @@ import TagsList from "../components/tags/TagsList.vue";
|
||||
import ChallengeFilesList from "../components/files/ChallengeFilesList.vue";
|
||||
import HintsList from "../components/hints/HintsList.vue";
|
||||
import NextChallenge from "../components/next/NextChallenge.vue";
|
||||
import hljs from "highlight.js";
|
||||
|
||||
const displayHint = data => {
|
||||
ezAlert({
|
||||
title: "Hint",
|
||||
body: data.html,
|
||||
button: "Got it!"
|
||||
});
|
||||
};
|
||||
|
||||
const loadHint = id => {
|
||||
CTFd.api.get_hint({ hintId: id, preview: true }).then(response => {
|
||||
if (response.data.content) {
|
||||
displayHint(response.data);
|
||||
return;
|
||||
}
|
||||
// displayUnlock(id);
|
||||
});
|
||||
};
|
||||
|
||||
function renderSubmissionResponse(response, cb) {
|
||||
var result = response.data;
|
||||
|
||||
var result_message = $("#result-message");
|
||||
var result_notification = $("#result-notification");
|
||||
var answer_input = $("#submission-input");
|
||||
result_notification.removeClass();
|
||||
result_message.text(result.message);
|
||||
|
||||
if (result.status === "authentication_required") {
|
||||
window.location =
|
||||
CTFd.config.urlRoot +
|
||||
"/login?next=" +
|
||||
CTFd.config.urlRoot +
|
||||
window.location.pathname +
|
||||
window.location.hash;
|
||||
return;
|
||||
} else if (result.status === "incorrect") {
|
||||
// Incorrect key
|
||||
result_notification.addClass(
|
||||
"alert alert-danger alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.removeClass("correct");
|
||||
answer_input.addClass("wrong");
|
||||
setTimeout(function() {
|
||||
answer_input.removeClass("wrong");
|
||||
}, 3000);
|
||||
} else if (result.status === "correct") {
|
||||
// Challenge Solved
|
||||
result_notification.addClass(
|
||||
"alert alert-success alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
$(".challenge-solves").text(
|
||||
parseInt(
|
||||
$(".challenge-solves")
|
||||
.text()
|
||||
.split(" ")[0]
|
||||
) +
|
||||
1 +
|
||||
" Solves"
|
||||
);
|
||||
|
||||
answer_input.val("");
|
||||
answer_input.removeClass("wrong");
|
||||
answer_input.addClass("correct");
|
||||
} else if (result.status === "already_solved") {
|
||||
// Challenge already solved
|
||||
result_notification.addClass(
|
||||
"alert alert-info alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("correct");
|
||||
} else if (result.status === "paused") {
|
||||
// CTF is paused
|
||||
result_notification.addClass(
|
||||
"alert alert-warning alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
} else if (result.status === "ratelimited") {
|
||||
// Keys per minute too high
|
||||
result_notification.addClass(
|
||||
"alert alert-warning alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("too-fast");
|
||||
setTimeout(function() {
|
||||
answer_input.removeClass("too-fast");
|
||||
}, 3000);
|
||||
}
|
||||
setTimeout(function() {
|
||||
$(".alert").slideUp();
|
||||
$("#challenge-submit").removeClass("disabled-button");
|
||||
$("#challenge-submit").prop("disabled", false);
|
||||
}, 3000);
|
||||
|
||||
if (cb) {
|
||||
cb(result);
|
||||
}
|
||||
}
|
||||
|
||||
function loadChalTemplate(challenge) {
|
||||
CTFd._internal.challenge = {};
|
||||
@@ -244,87 +139,13 @@ function handleChallengeOptions(event) {
|
||||
|
||||
$(() => {
|
||||
$(".preview-challenge").click(function(_e) {
|
||||
CTFd._internal.challenge = {};
|
||||
$.get(
|
||||
CTFd.config.urlRoot + "/api/v1/challenges/" + window.CHALLENGE_ID,
|
||||
function(response) {
|
||||
// Preview should not show any solves
|
||||
var challenge_data = response.data;
|
||||
challenge_data["solves"] = null;
|
||||
|
||||
$.getScript(
|
||||
CTFd.config.urlRoot + challenge_data.type_data.scripts.view,
|
||||
function() {
|
||||
const challenge = CTFd._internal.challenge;
|
||||
|
||||
// Inject challenge data into the plugin
|
||||
challenge.data = response.data;
|
||||
|
||||
$("#challenge-window").empty();
|
||||
|
||||
// Call preRender function in plugin
|
||||
challenge.preRender();
|
||||
|
||||
$("#challenge-window").append(challenge_data.view);
|
||||
|
||||
$("#challenge-window #challenge-input").addClass("form-control");
|
||||
$("#challenge-window #challenge-submit").addClass(
|
||||
"btn btn-md btn-outline-secondary float-right"
|
||||
);
|
||||
|
||||
$(".challenge-solves").hide();
|
||||
$(".nav-tabs a").click(function(e) {
|
||||
e.preventDefault();
|
||||
$(this).tab("show");
|
||||
});
|
||||
|
||||
// Handle modal toggling
|
||||
$("#challenge-window").on("hide.bs.modal", function(_event) {
|
||||
$("#challenge-input").removeClass("wrong");
|
||||
$("#challenge-input").removeClass("correct");
|
||||
$("#incorrect-key").slideUp();
|
||||
$("#correct-key").slideUp();
|
||||
$("#already-solved").slideUp();
|
||||
$("#too-fast").slideUp();
|
||||
});
|
||||
|
||||
$(".load-hint").on("click", function(_event) {
|
||||
loadHint($(this).data("hint-id"));
|
||||
});
|
||||
|
||||
$("#challenge-submit").click(function(e) {
|
||||
e.preventDefault();
|
||||
$("#challenge-submit").addClass("disabled-button");
|
||||
$("#challenge-submit").prop("disabled", true);
|
||||
CTFd._internal.challenge
|
||||
.submit(true)
|
||||
.then(renderSubmissionResponse);
|
||||
// Preview passed as true
|
||||
});
|
||||
|
||||
$("#challenge-input").keyup(function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
$("#challenge-submit").click();
|
||||
}
|
||||
});
|
||||
|
||||
challenge.postRender();
|
||||
|
||||
$("#challenge-window")
|
||||
.find("pre code")
|
||||
.each(function(_idx) {
|
||||
hljs.highlightBlock(this);
|
||||
});
|
||||
|
||||
window.location.replace(
|
||||
window.location.href.split("#")[0] + "#preview"
|
||||
);
|
||||
|
||||
$("#challenge-window").modal();
|
||||
}
|
||||
);
|
||||
}
|
||||
let url = `${CTFd.config.urlRoot}/admin/challenges/preview/${
|
||||
window.CHALLENGE_ID
|
||||
}`;
|
||||
$("#challenge-window").html(
|
||||
`<iframe src="${url}" height="100%" width="100%" frameBorder=0></iframe>`
|
||||
);
|
||||
$("#challenge-modal").modal();
|
||||
});
|
||||
|
||||
$(".comments-challenge").click(function(_event) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,24 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="modal fade" id="challenge-window" role="dialog">
|
||||
<div class="modal fade" id="challenge-modal" role="dialog">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="text-center w-100 p-0 m-0">Challenge Preview</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="challenge-window" style="height: 80vh;">
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h5>
|
||||
Preview may be slightly different from the user-facing interface
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="challenge-comments-window" role="dialog">
|
||||
|
||||
205
CTFd/themes/admin/templates/challenges/preview.html
Normal file
205
CTFd/themes/admin/templates/challenges/preview.html
Normal file
@@ -0,0 +1,205 @@
|
||||
{% extends "page.html" %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var CHALLENGE_ID = {{ challenge.id }};
|
||||
</script>
|
||||
{% if "beta" in Configs.ctf_theme %}
|
||||
{{ Assets.js("assets/js/challenges.js") }}
|
||||
<script type="module">
|
||||
CTFd.config.preview = true;
|
||||
CTFd.pages.challenge.displayChallenge(CHALLENGE_ID);
|
||||
</script>
|
||||
{% else %}
|
||||
{# TODO: Remove this in CTFd 4.0 when old themes have been deprecated #}
|
||||
<script type="module">
|
||||
const displayHint = data => {
|
||||
alert(data.html);
|
||||
};
|
||||
|
||||
const loadHint = id => {
|
||||
CTFd.api.get_hint({ hintId: id, preview: true }).then(response => {
|
||||
if (response.data.content) {
|
||||
displayHint(response.data);
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function renderSubmissionResponse(response) {
|
||||
const result = response.data;
|
||||
|
||||
const result_message = $("#result-message");
|
||||
const result_notification = $("#result-notification");
|
||||
const answer_input = $("#challenge-input");
|
||||
result_notification.removeClass();
|
||||
result_message.text(result.message);
|
||||
|
||||
const next_btn = $(
|
||||
`<div class='col-md-12 pb-3'><button class='btn btn-info w-100'>Next Challenge</button></div>`
|
||||
).click(function () {
|
||||
$("#challenge-window").modal("toggle");
|
||||
setTimeout(function () {
|
||||
loadChal(CTFd._internal.challenge.data.next_id);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
if (result.status === "authentication_required") {
|
||||
window.location =
|
||||
CTFd.config.urlRoot +
|
||||
"/login?next=" +
|
||||
CTFd.config.urlRoot +
|
||||
window.location.pathname +
|
||||
window.location.hash;
|
||||
return;
|
||||
} else if (result.status === "incorrect") {
|
||||
// Incorrect key
|
||||
result_notification.addClass(
|
||||
"alert alert-danger alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.removeClass("correct");
|
||||
answer_input.addClass("wrong");
|
||||
setTimeout(function () {
|
||||
answer_input.removeClass("wrong");
|
||||
}, 3000);
|
||||
} else if (result.status === "correct") {
|
||||
// Challenge Solved
|
||||
result_notification.addClass(
|
||||
"alert alert-success alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
if (
|
||||
$(".challenge-solves")
|
||||
.text()
|
||||
.trim()
|
||||
) {
|
||||
// Only try to increment solves if the text isn't hidden
|
||||
$(".challenge-solves").text(
|
||||
parseInt(
|
||||
$(".challenge-solves")
|
||||
.text()
|
||||
.split(" ")[0]
|
||||
) +
|
||||
1 +
|
||||
" Solves"
|
||||
);
|
||||
}
|
||||
|
||||
answer_input.val("");
|
||||
answer_input.removeClass("wrong");
|
||||
answer_input.addClass("correct");
|
||||
|
||||
if (CTFd._internal.challenge.data.next_id) {
|
||||
$(".submit-row").html(next_btn);
|
||||
}
|
||||
} else if (result.status === "already_solved") {
|
||||
// Challenge already solved
|
||||
result_notification.addClass(
|
||||
"alert alert-info alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("correct");
|
||||
|
||||
if (CTFd._internal.challenge.data.next_id) {
|
||||
$(".submit-row").html(next_btn);
|
||||
}
|
||||
} else if (result.status === "paused") {
|
||||
// CTF is paused
|
||||
result_notification.addClass(
|
||||
"alert alert-warning alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
} else if (result.status === "ratelimited") {
|
||||
// Keys per minute too high
|
||||
result_notification.addClass(
|
||||
"alert alert-warning alert-dismissable text-center"
|
||||
);
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("too-fast");
|
||||
setTimeout(function () {
|
||||
answer_input.removeClass("too-fast");
|
||||
}, 3000);
|
||||
}
|
||||
setTimeout(function () {
|
||||
$(".alert").slideUp();
|
||||
$("#challenge-submit").removeClass("disabled-button");
|
||||
$("#challenge-submit").prop("disabled", false);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
CTFd._internal.challenge = {};
|
||||
$.get(
|
||||
CTFd.config.urlRoot + "/api/v1/challenges/" + window.CHALLENGE_ID,
|
||||
function (response) {
|
||||
// Preview should not show any solves
|
||||
var challenge_data = response.data;
|
||||
challenge_data["solves"] = null;
|
||||
|
||||
$.getScript(
|
||||
CTFd.config.urlRoot + challenge_data.type_data.scripts.view,
|
||||
function () {
|
||||
const challenge = CTFd._internal.challenge;
|
||||
|
||||
// Inject challenge data into the plugin
|
||||
challenge.data = response.data;
|
||||
challenge.preRender();
|
||||
|
||||
$("#challenge-input").addClass("form-control");
|
||||
$("#challenge-submit").addClass(
|
||||
"btn btn-md btn-outline-secondary float-right"
|
||||
);
|
||||
|
||||
$(".challenge-solves").hide();
|
||||
$(".nav-tabs a").click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab("show");
|
||||
});
|
||||
|
||||
// Handle modal toggling
|
||||
$("#challenge-window").on("hide.bs.modal", function (_event) {
|
||||
$("#challenge-input").removeClass("wrong");
|
||||
$("#challenge-input").removeClass("correct");
|
||||
$("#incorrect-key").slideUp();
|
||||
$("#correct-key").slideUp();
|
||||
$("#already-solved").slideUp();
|
||||
$("#too-fast").slideUp();
|
||||
});
|
||||
|
||||
$(".load-hint").on("click", function (_event) {
|
||||
loadHint($(this).data("hint-id"));
|
||||
});
|
||||
|
||||
$("#challenge-submit").click(function (e) {
|
||||
e.preventDefault();
|
||||
$("#challenge-submit").addClass("disabled-button");
|
||||
$("#challenge-submit").prop("disabled", true);
|
||||
CTFd._internal.challenge
|
||||
.submit(true)
|
||||
.then(renderSubmissionResponse);
|
||||
// Preview passed as true
|
||||
});
|
||||
|
||||
$("#challenge-input").keyup(function (event) {
|
||||
if (event.keyCode == 13) {
|
||||
$("#challenge-submit").click();
|
||||
}
|
||||
});
|
||||
|
||||
challenge.postRender();
|
||||
|
||||
$("pre code")
|
||||
.each(function (_idx) {
|
||||
hljs.highlightBlock(this);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user