mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-18 14:34:21 +01:00
2.2.0 (#1188)
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:
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user