3.0.0a2 dev (#1528)

# 3.0.0a2 / 2020-07-09

**General**

* Accept additional profile fields during registration (affiliation, website, country)
  * This does not add additional inputs. Themes or additional JavaScript can add the form inputs.

**Admin Panel**

* Redesign the challenge creation form to use a radio button with challenge type selection instead of a select input

**API**

* Admins can no longer ban themselves through `PATCH /api/v1/users/[user_id]`

**Themes**

* Spinner centering has been switched from a hard coded margin in CSS to flexbox CSS classes from Bootstrap

**Plugins**

* Revert plugin menu (`register_admin_plugin_menu_bar`, `register_user_page_menu_bar`) changes to 2.x code

**Miscellaneous**

* Fix issue with `Configs.ctf_name` returning incorrect value
* Add prerender step back into challenges.js
* Better handling of missing challenge types. Missing challenge types no longer bring down all other challenges.
This commit is contained in:
Kevin Chung
2020-07-09 13:40:35 -04:00
committed by GitHub
parent 1bccbf1fdd
commit 1725e632cf
37 changed files with 399 additions and 163 deletions

View File

@@ -1,3 +1,32 @@
# 3.0.0a2 / 2020-07-09
**General**
- Accept additional profile fields during registration (affiliation, website, country)
- This does not add additional inputs. Themes or additional JavaScript can add the form inputs.
**Admin Panel**
- Redesign the challenge creation form to use a radio button with challenge type selection instead of a select input
**API**
- Admins can no longer ban themselves through `PATCH /api/v1/users/[user_id]`
**Themes**
- Spinner centering has been switched from a hard coded margin in CSS to flexbox CSS classes from Bootstrap
**Plugins**
- Revert plugin menu (`register_admin_plugin_menu_bar`, `register_user_page_menu_bar`) changes to 2.x code
**Miscellaneous**
- Fix issue with `Configs.ctf_name` returning incorrect value
- Add prerender step back into challenges.js
- Better handling of missing challenge types. Missing challenge types no longer bring down all other challenges.
# 3.0.0a1 / 2020-07-01
**General**

View File

@@ -26,7 +26,7 @@ from CTFd.utils.migrations import create_database, migrations, stamp_latest_revi
from CTFd.utils.sessions import CachingSessionInterface
from CTFd.utils.updates import update_check
__version__ = "3.0.0a1"
__version__ = "3.0.0a2"
class CTFdRequest(Request):

View File

@@ -1,8 +1,8 @@
from flask import render_template, request, url_for
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 get_chal_class
from CTFd.plugins.challenges import CHALLENGE_CLASSES, get_chal_class
from CTFd.utils.decorators import admins_only
@@ -44,7 +44,14 @@ def challenges_detail(challenge_id):
.all()
)
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
challenge_class = get_chal_class(challenge.type)
try:
challenge_class = get_chal_class(challenge.type)
except KeyError:
abort(
500,
f"The underlying challenge type ({challenge.type}) is not installed. This challenge can not be loaded.",
)
update_j2 = render_template(
challenge_class.templates["update"].lstrip("/"), challenge=challenge
@@ -67,4 +74,5 @@ def challenges_detail(challenge_id):
@admin.route("/admin/challenges/new")
@admins_only
def challenges_new():
return render_template("admin/challenges/new.html")
types = CHALLENGE_CLASSES.keys()
return render_template("admin/challenges/new.html", types=types)

View File

@@ -187,7 +187,13 @@ class ChallengeList(Resource):
# Fallthrough to continue
continue
challenge_type = get_chal_class(challenge.type)
try:
challenge_type = get_chal_class(challenge.type)
except KeyError:
# Challenge type does not exist. Fall through to next challenge.
continue
# Challenge passes all checks, add it to response
response.append(
{
"id": challenge.id,
@@ -268,7 +274,13 @@ class Challenge(Resource):
and_(Challenges.state != "hidden", Challenges.state != "locked"),
).first_or_404()
chal_class = get_chal_class(chal.type)
try:
chal_class = get_chal_class(chal.type)
except KeyError:
abort(
500,
f"The underlying challenge type ({chal.type}) is not installed. This challenge can not be loaded.",
)
if chal.requirements:
requirements = chal.requirements.get("prerequisites", [])

View File

@@ -1,6 +1,6 @@
from typing import List
from flask import abort, request
from flask import abort, request, session
from flask_restx import Namespace, Resource
from CTFd.api.v1.helpers.models import build_model_filters
@@ -218,6 +218,16 @@ class UserPublic(Resource):
user = Users.query.filter_by(id=user_id).first_or_404()
data = request.get_json()
data["id"] = user_id
# Admins should not be able to ban themselves
if data["id"] == session["id"] and (
data.get("banned") is True or data.get("banned") == "true"
):
return (
{"success": False, "errors": {"id": "You cannot ban yourself"}},
400,
)
schema = UserSchema(view="admin", instance=user, partial=True)
response = schema.load(data)
if response.errors:

View File

@@ -22,6 +22,7 @@ from CTFd.utils.logging import log
from CTFd.utils.modes import TEAMS_MODE
from CTFd.utils.security.auth import login_user, logout_user
from CTFd.utils.security.signing import unserialize
from CTFd.utils.validators import ValidationError
auth = Blueprint("auth", __name__)
@@ -189,6 +190,10 @@ def register():
email_address = request.form.get("email", "").strip().lower()
password = request.form.get("password", "").strip()
website = request.form.get("website")
affiliation = request.form.get("affiliation")
country = request.form.get("country")
name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first()
emails = (
@@ -201,6 +206,25 @@ def register():
valid_email = validators.validate_email(email_address)
team_name_email_check = validators.validate_email(name)
if country:
try:
validators.validate_country_code(country)
valid_country = True
except ValidationError:
valid_country = False
else:
valid_country = True
if website:
valid_website = validators.validate_url(website)
else:
valid_website = True
if affiliation:
valid_affiliation = len(affiliation) < 128
else:
valid_affiliation = True
if not valid_email:
errors.append("Please enter a valid email address")
if email.check_email_is_whitelisted(email_address) is False:
@@ -221,6 +245,12 @@ def register():
errors.append("Pick a shorter password")
if name_len:
errors.append("Pick a longer user name")
if valid_website is False:
errors.append("Websites must be a proper URL starting with http or https")
if valid_country is False:
errors.append("Invalid country")
if valid_affiliation is False:
errors.append("Please provide a shorter affiliation")
if len(errors) > 0:
return render_template(
@@ -233,6 +263,14 @@ def register():
else:
with app.app_context():
user = Users(name=name, email=email_address, password=password)
if website:
user.website = website
if affiliation:
user.affiliation = affiliation
if country:
user.country = country
db.session.add(user)
db.session.commit()
db.session.flush()

View File

@@ -45,7 +45,7 @@ class _ConfigsWrapper:
@property
def ctf_name(self):
return get_config("theme_header", default="CTFd")
return get_config("ctf_name", default="CTFd")
@property
def theme_header(self):

View File

@@ -1,4 +1,5 @@
from flask import render_template
from werkzeug.exceptions import InternalServerError
# 404
@@ -13,7 +14,10 @@ def forbidden(error):
# 500
def general_error(error):
return render_template("errors/500.html"), 500
if error.description == InternalServerError.description:
error.description = "An Internal Server Error has occurred"
return render_template("errors/500.html", error=error.description), 500
# 502

View File

@@ -1,4 +1,5 @@
import datetime
from collections import defaultdict
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
@@ -77,7 +78,22 @@ class Challenges(db.Model):
hints = db.relationship("Hints", backref="challenge")
flags = db.relationship("Flags", backref="challenge")
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
class alt_defaultdict(defaultdict):
"""
This slightly modified defaultdict is intended to allow SQLAlchemy to
not fail when querying Challenges that contain a missing challenge type.
e.g. Challenges.query.all() should not fail if `type` is `a_missing_type`
"""
def __missing__(self, key):
return self["standard"]
__mapper_args__ = {
"polymorphic_identity": "standard",
"polymorphic_on": type,
"_polymorphic_map": alt_defaultdict(),
}
@property
def html(self):

View File

@@ -4,7 +4,7 @@ import os
from collections import namedtuple
from flask import current_app as app
from flask import send_file, send_from_directory, url_for
from flask import send_file, send_from_directory
from CTFd.utils.config.pages import get_pages
from CTFd.utils.decorators import admins_only as admins_only_wrapper
@@ -114,9 +114,6 @@ def register_admin_plugin_menu_bar(title, route):
:param route: A string that is the href used by the link
:return:
"""
if (route.startswith("http://") or route.startswith("https://")) is False:
route = url_for("views.static_html", route=route)
am = Menu(title=title, route=route)
app.admin_plugin_menu_bar.append(am)
@@ -138,9 +135,6 @@ def register_user_page_menu_bar(title, route):
:param route: A string that is the href used by the link
:return:
"""
if (route.startswith("http://") or route.startswith("https://")) is False:
route = url_for("views.static_html", route=route)
p = Menu(title=title, route=route)
app.plugin_menu_bar.append(p)

View File

@@ -11,7 +11,6 @@
.CodeMirror-scroll {
overflow-y: hidden;
overflow-x: auto;
height: 200px;
}
.editor-toolbar {

View File

@@ -237,17 +237,6 @@ function handleChallengeOptions(event) {
});
}
function createChallenge(_event) {
const challenge = $(this)
.find("option:selected")
.data("meta");
if (challenge === undefined) {
$("#create-chal-entry-div").empty();
return;
}
loadChalTemplate(challenge);
}
$(() => {
$(".preview-challenge").click(function(_e) {
window.challenge = new Object();
@@ -430,29 +419,12 @@ $(() => {
$(".edit-flag").click(editFlagModal);
$.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) {
$("#create-chals-select").empty();
const data = response.data;
const chal_type_amt = Object.keys(data).length;
if (chal_type_amt > 1) {
const option = "<option> -- </option>";
$("#create-chals-select").append(option);
for (const key in data) {
const challenge = data[key];
const option = $("<option/>");
option.attr("value", challenge.type);
option.text(challenge.name);
option.data("meta", challenge);
$("#create-chals-select").append(option);
}
$("#create-chals-select-div").show();
$("#create-chals-select").val("standard");
loadChalTemplate(data["standard"]);
} else if (chal_type_amt == 1) {
const key = Object.keys(data)[0];
$("#create-chals-select").empty();
loadChalTemplate(data[key]);
}
});
loadChalTemplate(data["standard"]);
$("#create-chals-select").change(createChallenge);
$("#create-chals-select input[name=type]").change(function() {
let challenge = data[this.value];
loadChalTemplate(challenge);
});
});
});

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

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,20 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.card-radio:checked + .card {
background-color: transparent !important;
border-color: #a3d39c;
box-shadow: 0 0 0 0.1rem #a3d39c;
transition: background-color 0.3s, border-color 0.3s;
}
.card-radio:checked + .card .card-radio-clone{
visibility: visible !important;
}
.card:hover {
cursor: pointer;
}
</style>
{% endblock %}
{% block content %}
@@ -73,20 +87,32 @@
</div>
</div>
<div class="container">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-md-6 offset-md-3">
<div id="create-chals-select-div" style="display: none;">
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
<select class="form-control custom-select" id="create-chals-select">
</select>
<div class="col-md-2 offset-md-2">
<h5 class="text-center pb-3">
Challenge Types
</h5>
<div id="create-chals-select">
{% for type in types %}
<label class="w-100">
<input type="radio" name="type" class="card-radio d-none" value="{{ type }}" {% if type == "standard" %}checked{% endif %}/>
<div class="card rounded-0">
<div class="card-body">
<span class="card-title">
<div class="form-check">
<input class="form-check-input card-radio-clone" type="radio" style="visibility: hidden;" checked>
<label class="form-check-label">{{ type }}</label>
</div>
</span>
</div>
</div>
<br>
<div id="create-chal-entry-div">
</div>
</div>
</label>
{% endfor %}
</div>
</div>
<div class="col-md-6">
<div id="create-chal-entry-div">
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
</div>
</div>
<div class="container">
<div class="row">
<div class="row d-flex align-items-center">
<div class="col-md-4 text-right">
<h5><b>{{ user_count }}</b> users registered</h5>
{% if get_config('user_mode') == 'teams' %}
@@ -26,8 +26,8 @@
</div>
<div class="col-md-8">
<div id="solves-graph">
<div class="text-center">
<div id="solves-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
@@ -38,8 +38,8 @@
<div class="row">
<div class="col-md-12">
<div id="score-distribution-graph">
<div class="text-center">
<div id="score-distribution-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
@@ -50,8 +50,8 @@
<div class="row">
<div class="col-md-12">
<div id="solve-percentages-graph">
<div class="text-center">
<div id="solve-percentages-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
@@ -62,8 +62,8 @@
<div class="row">
<div class="col-md-6">
<div id="keys-pie-graph">
<div class="text-center">
<div id="keys-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
@@ -73,8 +73,8 @@
</div>
</div>
<div class="col-md-6">
<div id="categories-pie-graph">
<div class="text-center">
<div id="categories-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>

View File

@@ -169,18 +169,18 @@
<div class="container">
<div class="row min-vh-25">
{% if solves %}
<div id="keys-pie-graph" class="col-md-6">
<div class="text-center">
<div id="keys-pie-graph" class="col-md-6 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="categories-pie-graph" class="col-md-6">
<div class="text-center">
<div id="categories-pie-graph" class="col-md-6 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="score-graph" class="col-md-12">
<div class="text-center">
<div id="score-graph" class="col-md-12 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>

View File

@@ -160,18 +160,18 @@
<div class="container">
<div class="row min-vh-25">
{% if solves %}
<div id="keys-pie-graph" class="col-md-6">
<div class="text-center">
<div id="keys-pie-graph" class="col-md-6 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="categories-pie-graph" class="col-md-6">
<div class="text-center">
<div id="categories-pie-graph" class="col-md-6 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
<div id="score-graph" class="col-md-12">
<div class="text-center">
<div id="score-graph" class="col-md-12 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>

View File

@@ -35,7 +35,6 @@ table > thead > tr > td {
}
.fa-spin.spinner {
margin-top: 225px;
text-align: center;
opacity: 0.5;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,8 +22,10 @@
</div>
<div id='challenges-board'>
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="min-vh-50 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>

View File

@@ -8,7 +8,7 @@
<div class="pt-5 mt-5 text-center">
<h1>500</h1>
<hr class="w-50">
<h2>An Internal Server Error has occurred</h2>
<h2>{{ error }}</h2>
</div>
</div>
</div>

View File

@@ -25,23 +25,23 @@
{% with form = Forms.auth.RegistrationForm() %}
<form method="post" accept-charset="utf-8" autocomplete="off" role="form">
<div class="form-group">
{{ form.name.label }}
<b>{{ form.name.label }}</b>
{{ form.name(class="form-control", value=name) }}
</div>
<div class="form-group">
{{ form.email.label }}
<b>{{ form.email.label }}</b>
{{ form.email(class="form-control", value=email) }}
</div>
<div class="form-group">
{{ form.password.label }}
<b>{{ form.password.label }}</b>
{{ form.password(class="form-control", value=password) }}
</div>
{{ form.nonce() }}
<div class="row pt-3">
<div class="col-md-12">
{{ form.submit(class="btn btn-md btn-primary btn-outlined float-right") }}
</div>
</div>
{{ form.nonce() }}
</form>
{% endwith %}
</div>

View File

@@ -9,12 +9,13 @@
<div class="container">
{% include "components/errors.html" %}
<div id="score-graph" class="row">
<div id="score-graph" class="row d-flex align-items-center">
<div class="col-md-12 text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
{% if standings %}
<div id="scoreboard" class="row">
<div class="col-md-12">
<table class="table table-striped">
@@ -53,6 +54,7 @@
</table>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -26,37 +26,37 @@
<form id="user-profile-form" method="post" accept-charset="utf-8" autocomplete="off" role="form"
class="form-horizontal">
<div class="form-group">
{{ form.name.label }}
<b>{{ form.name.label }}</b>
{{ form.name(class="form-control", value=name) }}
</div>
<div class="form-group">
{{ form.email.label }}
<b>{{ form.email.label }}</b>
{{ form.email(class="form-control", value=email) }}
</div>
<hr>
<div class="form-group">
{{ form.confirm.label }}
<b>{{ form.confirm.label }}</b>
{{ form.confirm(class="form-control") }}
</div>
<div class="form-group">
{{ form.password.label }}
<b>{{ form.password.label }}</b>
{{ form.password(class="form-control") }}
</div>
<hr>
<div class="form-group">
{{ form.affiliation.label }}
<b>{{ form.affiliation.label }}</b>
{{ form.affiliation(class="form-control", value=affiliation or "") }}
</div>
<div class="form-group">
{{ form.website.label }}
<b>{{ form.website.label }}</b>
{{ form.website(class="form-control", value=website or "") }}
</div>
<div class="form-group">
{{ form.country.label }}
<b>{{ form.country.label }}</b>
{{ form.country(class="form-control custom-select", value=country) }}
</div>
@@ -73,7 +73,7 @@
{% with form = Forms.self.TokensForm() %}
<form method="POST" id="user-token-form">
<div class="form-group">
{{ form.expiration.label }}
<b>{{ form.expiration.label }}</b>
{{ form.expiration(class="form-control") }}
</div>

View File

@@ -200,20 +200,28 @@
</div>
{% if solves %}
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="row">
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="keys-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="categories-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<br class="clearfix">
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<br class="clearfix">
<div class="col-md-12 d-none d-md-block d-lg-block">
<div id="score-graph" class="w-100 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>

View File

@@ -97,20 +97,28 @@
</div>
{% if solves %}
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="row">
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="keys-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="categories-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<br class="clearfix">
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<br class="clearfix">
<div class="col-md-12 d-none d-md-block d-lg-block">
<div id="score-graph" class="w-100 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>

View File

@@ -66,20 +66,28 @@
{% include "components/errors.html" %}
{% if user.solves %}
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="row">
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="keys-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="categories-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<br class="clearfix">
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<br class="clearfix">
<div class="col-md-12 d-none d-md-block d-lg-block">
<div id="score-graph" class="w-100 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>

View File

@@ -66,20 +66,28 @@
{% include "components/errors.html" %}
{% if user.solves %}
<div id="keys-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="row">
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="keys-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<div id="categories-pie-graph" class="w-50 mr-0 pr-0 float-left d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<div class="col-md-6 d-none d-md-block d-lg-block">
<div id="categories-pie-graph" class="d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>
<br class="clearfix">
<div id="score-graph" class="w-100 float-right d-none d-md-block d-lg-block">
<div class="text-center">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
<br class="clearfix">
<div class="col-md-12 d-none d-md-block d-lg-block">
<div id="score-graph" class="w-100 d-flex align-items-center">
<div class="text-center w-100">
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
</div>
</div>
</div>
</div>

View File

@@ -26,7 +26,7 @@ author = u"Kevin Chung"
# The short X.Y version
version = u""
# The full version, including alpha/beta/rc tags
release = u"2.5.0"
release = u"3.0.0a2"
# -- General configuration ---------------------------------------------------

View File

@@ -1,6 +1,6 @@
{
"name": "ctfd",
"version": "2.5.0",
"version": "3.0.0a2",
"description": "CTFd is a Capture The Flag framework focusing on ease of use and customizability. It comes with everything you need to run a CTF and it's easy to customize with plugins and themes.",
"main": "index.js",
"directories": {

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user
def test_api_self_ban():
"""PATCH /api/v1/users/<user_id> should not allow a user to ban themselves"""
app = create_ctfd()
with app.app_context():
with login_as_user(app, name="admin") as client:
r = client.patch("/api/v1/users/1", json={"banned": True})
resp = r.get_json()
assert r.status_code == 400
assert resp["success"] == False
assert resp["errors"] == {"id": "You cannot ban yourself"}
destroy_ctfd(app)

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from CTFd.models import Challenges
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
def test_missing_challenge_type():
"""Test that missing challenge types don't cause total challenge rendering failure"""
app = create_ctfd(enable_plugins=True)
with app.app_context():
register_user(app)
client = login_as_user(app, name="admin", password="password")
challenge_data = {
"name": "name",
"category": "category",
"description": "description",
"value": 100,
"decay": 20,
"minimum": 1,
"state": "visible",
"type": "dynamic",
}
r = client.post("/api/v1/challenges", json=challenge_data)
assert r.get_json().get("data")["id"] == 1
assert r.get_json().get("data")["type"] == "dynamic"
chal_count = Challenges.query.count()
assert chal_count == 1
# Delete the dynamic challenge type
from CTFd.plugins.challenges import CHALLENGE_CLASSES
del CHALLENGE_CLASSES["dynamic"]
r = client.get("/admin/challenges")
assert r.status_code == 200
assert b"dynamic" in r.data
r = client.get("/admin/challenges/1")
assert r.status_code == 500
assert b"The underlying challenge type (dynamic) is not installed" in r.data
challenge_data = {
"name": "name",
"category": "category",
"description": "description",
"value": 100,
"state": "visible",
"type": "standard",
}
r = client.post("/api/v1/challenges", json=challenge_data)
r = client.get("/challenges")
assert r.status_code == 200
# We should still see the one visible standard challenge
r = client.get("/api/v1/challenges")
assert r.status_code == 200
assert len(r.json["data"]) == 1
assert r.json["data"][0]["type"] == "standard"
# We cannot load the broken challenge
r = client.get("/api/v1/challenges/1")
assert r.status_code == 500
assert (
"The underlying challenge type (dynamic) is not installed"
in r.json["message"]
)
# We can load other challenges
r = client.get("/api/v1/challenges/2")
assert r.status_code == 200
destroy_ctfd(app)

View File

@@ -148,7 +148,7 @@ def test_register_admin_plugin_menu_bar():
menu_item = get_admin_plugin_menu_bar()[0]
assert menu_item.title == "test_admin_plugin_name"
assert menu_item.route == "http://localhost/test_plugin"
assert menu_item.route == "/test_plugin"
destroy_ctfd(app)
@@ -172,7 +172,7 @@ def test_register_user_page_menu_bar():
menu_item = get_user_page_menu_bar()[0]
assert menu_item.title == "test_user_menu_link"
assert menu_item.route == "http://localhost/test_user_href"
assert menu_item.route == "/test_user_href"
destroy_ctfd(app)