2.2.0 / 2019-12-22
==================

## Notice
2.2.0 focuses on updating the front end of CTFd to use more modern programming practices and changes some aspects of core CTFd design. If your current installation is using a custom theme or custom plugin with ***any*** kind of JavaScript, it is likely that you will need to upgrade that theme/plugin to be useable with v2.2.0. 

**General**
* Team size limits can now be enforced from the configuration panel
* Access tokens functionality for API usage
* Admins can now choose how to deliver their notifications
    * Toast (new default)
    * Alert
    * Background
    * Sound On / Sound Off
* There is now a notification counter showing how many unread notifications were received
* Setup has been redesigned to have multiple steps
    * Added Description
    * Added Start time and End time,
    * Added MajorLeagueCyber integration
    * Added Theme and color selection
* Fixes issue where updating dynamic challenges could change the value to an incorrect value
* Properly use a less restrictive regex to validate email addresses
* Bump Python dependencies to latest working versions
* Admins can now give awards to team members from the team's admin panel page

**API**
* Team member removals (`DELETE /api/v1/teams/[team_id]/members`) from the admin panel will now delete the removed members's Submissions, Awards, Unlocks

**Admin Panel**
* Admins can now user a color input box to specify a theme color which is injected as part of the CSS configuration. Theme developers can use this CSS value to change colors and styles accordingly.
* Challenge updates will now alert you if the challenge doesn't have a flag
* Challenge entry now allows you to upload files and enter simple flags from the initial challenge creation page

**Themes**
* Significant JavaScript and CSS rewrite to use ES6, Webpack, yarn, and babel
* Theme asset specially generated URLs
    * Static theme assets are now loaded with either .dev.extension or .min.extension depending on production or development (i.e. debug server)
    * Static theme assets are also given a `d` GET parameter that changes per server start. Used to bust browser caches.
* Use `defer` for script tags to not block page rendering
* Only show the MajorLeagueCyber button if configured in configuration
* The admin panel now links to https://help.ctfd.io/ in the top right
* Create an `ezToast()` function to use [Bootstrap's toasts](https://getbootstrap.com/docs/4.3/components/toasts/)
* The user-facing navbar now features icons
* Awards shown on a user's profile can now have award icons
* The default MarkdownIt render created by CTFd will now open links in new tabs
* Country flags can now be shown on the user pages

**Deployment**
* Switch `Dockerfile` from `python:2.7-alpine` to `python:3.7-alpine`
* Add `SERVER_SENT_EVENTS` config value to control whether Notifications are enabled
* Challenge ID is now recorded in the submission log

**Plugins**
* Add an endpoint parameter to `register_plugin_assets_directory()` and `register_plugin_asset()` to control what endpoint Flask uses for the added route

**Miscellaneous**
* `CTFd.utils.email.sendmail()` now allows the caller to specify subject as an argument
    * The subject allows for injecting custom variable via the new `CTFd.utils.formatters.safe_format()` function
* Admin user information is now error checked during setup
* Added yarn to the toolchain and the yarn dev, yarn build, yarn verify, and yarn clean scripts
* Prevent old CTFd imports from being imported
This commit is contained in:
Kevin Chung
2019-12-22 23:17:34 -05:00
committed by GitHub
parent 6d192a7c14
commit b8d0f80d01
453 changed files with 41266 additions and 17454 deletions

View File

@@ -42,6 +42,42 @@ class DynamicValueChallenge(BaseChallenge):
static_folder="assets",
)
@classmethod
def calculate_value(cls, challenge):
Model = get_model()
solve_count = (
Solves.query.join(Model, Solves.account_id == Model.id)
.filter(
Solves.challenge_id == challenge.id,
Model.hidden == False,
Model.banned == False,
)
.count()
)
# If the solve count is 0 we shouldn't manipulate the solve count to
# let the math update back to normal
if solve_count != 0:
# We subtract -1 to allow the first solver to get max point value
solve_count -= 1
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (
((challenge.minimum - challenge.initial) / (challenge.decay ** 2))
* (solve_count ** 2)
) + challenge.initial
value = math.ceil(value)
if value < challenge.minimum:
value = challenge.minimum
challenge.value = value
db.session.commit()
return challenge
@staticmethod
def create(request):
"""
@@ -106,34 +142,7 @@ class DynamicValueChallenge(BaseChallenge):
value = float(value)
setattr(challenge, attr, value)
Model = get_model()
solve_count = (
Solves.query.join(Model, Solves.account_id == Model.id)
.filter(
Solves.challenge_id == challenge.id,
Model.hidden == False,
Model.banned == False,
)
.count()
)
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (
((challenge.minimum - challenge.initial) / (challenge.decay ** 2))
* (solve_count ** 2)
) + challenge.initial
value = math.ceil(value)
if value < challenge.minimum:
value = challenge.minimum
challenge.value = value
db.session.commit()
return challenge
return DynamicValueChallenge.calculate_value(challenge)
@staticmethod
def delete(challenge):
@@ -185,12 +194,10 @@ class DynamicValueChallenge(BaseChallenge):
:param request: The request the user submitted
:return:
"""
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
data = request.form or request.get_json()
submission = data["submission"].strip()
Model = get_model()
solve = Solves(
user_id=user.id,
team_id=team.id if team else None,
@@ -199,35 +206,9 @@ class DynamicValueChallenge(BaseChallenge):
provided=submission,
)
db.session.add(solve)
solve_count = (
Solves.query.join(Model, Solves.account_id == Model.id)
.filter(
Solves.challenge_id == challenge.id,
Model.hidden == False,
Model.banned == False,
)
.count()
)
# We subtract -1 to allow the first solver to get max point value
solve_count -= 1
# It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division
value = (
((chal.minimum - chal.initial) / (chal.decay ** 2)) * (solve_count ** 2)
) + chal.initial
value = math.ceil(value)
if value < chal.minimum:
value = chal.minimum
chal.value = value
db.session.commit()
db.session.close()
DynamicValueChallenge.calculate_value(challenge)
@staticmethod
def fail(user, team, challenge, request):

View File

@@ -16,34 +16,13 @@
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
</div>
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
<li class="nav-item">
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home"
role="tab" data-toggle="tab">
Write
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#desc-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="desc-write">
<div class="form-group">
<label for="message-text" class="control-label">Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description"
rows="10">{{ challenge.description }}</textarea>
</div>
</div>
<div role="tabpanel" class="tab-pane content" id="desc-preview"
style="height:214px; overflow-y: scroll;">
</div>
<div class="form-group">
<label for="message-text" class="control-label">Message<br>
<small class="form-text text-muted">
Use this to give a brief introduction to your challenge.
</small>
</label>
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
</div>
<div class="form-group">
@@ -98,7 +77,7 @@
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
</label>
<select class="form-control" name="state">
<select class="form-control custom-select" name="state">
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
</select>

View File

@@ -1,51 +0,0 @@
$('#submit-key').click(function (e) {
submitkey($('#chalid').val(), $('#answer').val())
});
$('#submit-keys').click(function (e) {
e.preventDefault();
$('#update-keys').modal('hide');
});
$('#limit_max_attempts').change(function() {
if(this.checked) {
$('#chal-attempts-group').show();
} else {
$('#chal-attempts-group').hide();
$('#chal-attempts-input').val('');
}
});
// Markdown Preview
$('#desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#desc-preview') {
var editor_value = $('#desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
$('#new-desc-edit').on('shown.bs.tab', function (event) {
if (event.target.hash == '#new-desc-preview') {
var editor_value = $('#new-desc-editor').val();
$(event.target.hash).html(
window.challenge.render(editor_value)
);
}
});
function loadchal(id, update) {
$.get(script_root + '/admin/chal/' + id, function(obj){
$('#desc-write-link').click(); // Switch to Write tab
if (typeof update === 'undefined')
$('#update-challenge').modal();
});
}
function openchal(id){
loadchal(id);
}
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});

View File

@@ -1,57 +1,40 @@
window.challenge.data = undefined;
CTFd._internal.challenge.data = undefined
window.challenge.renderer = new markdownit({
html: true,
linkify: true,
});
window.challenge.preRender = function () {
};
window.challenge.render = function (markdown) {
return window.challenge.renderer.render(markdown);
};
CTFd._internal.challenge.renderer = CTFd.lib.markdown();
window.challenge.postRender = function () {
CTFd._internal.challenge.preRender = function () { }
};
CTFd._internal.challenge.render = function (markdown) {
return CTFd._internal.challenge.renderer.render(markdown)
}
window.challenge.submit = function (cb, preview) {
var challenge_id = parseInt($('#challenge-id').val());
var submission = $('#submission-input').val();
var url = "/api/v1/challenges/attempt";
CTFd._internal.challenge.postRender = function () { }
CTFd._internal.challenge.submit = function (preview) {
var challenge_id = parseInt(CTFd.lib.$('#challenge-id').val())
var submission = CTFd.lib.$('#submission-input').val()
var body = {
'challenge_id': challenge_id,
'submission': submission,
}
var params = {}
if (preview) {
url += "?preview=true";
params['preview'] = true
}
var params = {
'challenge_id': challenge_id,
'submission': submission
};
CTFd.fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
}).then(function (response) {
return CTFd.api.post_challenge_attempt(params, body).then(function (response) {
if (response.status === 429) {
// User was ratelimited but process response
return response.json();
return response
}
if (response.status === 403) {
// User is not logged in or CTF is paused.
return response.json();
return response
}
return response.json();
}).then(function (response) {
cb(response);
});
};
return response
})
};