mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
321 comments functionality (#1596)
* Add a comments functionality for admins to discuss challenges, users, teams, pages * Adds `/api/v1/comments` * Adds a `CommentBox.vue` component for the Admin Panel * Closes #321
This commit is contained in:
@@ -3,6 +3,7 @@ from flask_restx import Api
|
|||||||
|
|
||||||
from CTFd.api.v1.awards import awards_namespace
|
from CTFd.api.v1.awards import awards_namespace
|
||||||
from CTFd.api.v1.challenges import challenges_namespace
|
from CTFd.api.v1.challenges import challenges_namespace
|
||||||
|
from CTFd.api.v1.comments import comments_namespace
|
||||||
from CTFd.api.v1.config import configs_namespace
|
from CTFd.api.v1.config import configs_namespace
|
||||||
from CTFd.api.v1.files import files_namespace
|
from CTFd.api.v1.files import files_namespace
|
||||||
from CTFd.api.v1.flags import flags_namespace
|
from CTFd.api.v1.flags import flags_namespace
|
||||||
@@ -48,3 +49,4 @@ CTFd_API_v1.add_namespace(configs_namespace, "/configs")
|
|||||||
CTFd_API_v1.add_namespace(pages_namespace, "/pages")
|
CTFd_API_v1.add_namespace(pages_namespace, "/pages")
|
||||||
CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks")
|
CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks")
|
||||||
CTFd_API_v1.add_namespace(tokens_namespace, "/tokens")
|
CTFd_API_v1.add_namespace(tokens_namespace, "/tokens")
|
||||||
|
CTFd_API_v1.add_namespace(comments_namespace, "/comments")
|
||||||
|
|||||||
141
CTFd/api/v1/comments.py
Normal file
141
CTFd/api/v1/comments.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from flask import request, session
|
||||||
|
from flask_restx import Namespace, Resource
|
||||||
|
|
||||||
|
from CTFd.api.v1.helpers.models import build_model_filters
|
||||||
|
from CTFd.api.v1.helpers.request import validate_args
|
||||||
|
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||||
|
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||||
|
from CTFd.constants import RawEnum
|
||||||
|
from CTFd.models import (
|
||||||
|
ChallengeComments,
|
||||||
|
Comments,
|
||||||
|
PageComments,
|
||||||
|
TeamComments,
|
||||||
|
UserComments,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
from CTFd.schemas.comments import CommentSchema
|
||||||
|
from CTFd.utils.decorators import admins_only
|
||||||
|
|
||||||
|
comments_namespace = Namespace("comments", description="Endpoint to retrieve Comments")
|
||||||
|
|
||||||
|
|
||||||
|
CommentModel = sqlalchemy_to_pydantic(Comments)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentDetailedSuccessResponse(APIDetailedSuccessResponse):
|
||||||
|
data: CommentModel
|
||||||
|
|
||||||
|
|
||||||
|
class CommentListSuccessResponse(APIListSuccessResponse):
|
||||||
|
data: List[CommentModel]
|
||||||
|
|
||||||
|
|
||||||
|
comments_namespace.schema_model(
|
||||||
|
"CommentDetailedSuccessResponse", CommentDetailedSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
comments_namespace.schema_model(
|
||||||
|
"CommentListSuccessResponse", CommentListSuccessResponse.apidoc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_comment_model(data):
|
||||||
|
model = Comments
|
||||||
|
if "challenge_id" in data:
|
||||||
|
model = ChallengeComments
|
||||||
|
elif "user_id" in data:
|
||||||
|
model = UserComments
|
||||||
|
elif "team_id" in data:
|
||||||
|
model = TeamComments
|
||||||
|
elif "page_id" in data:
|
||||||
|
model = PageComments
|
||||||
|
else:
|
||||||
|
model = Comments
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
@comments_namespace.route("")
|
||||||
|
class CommentList(Resource):
|
||||||
|
@admins_only
|
||||||
|
@comments_namespace.doc(
|
||||||
|
description="Endpoint to list Comment objects in bulk",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "CommentListSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@validate_args(
|
||||||
|
{
|
||||||
|
"challenge_id": (int, None),
|
||||||
|
"user_id": (int, None),
|
||||||
|
"team_id": (int, None),
|
||||||
|
"page_id": (int, None),
|
||||||
|
"q": (str, None),
|
||||||
|
"field": (RawEnum("CommentFields", {"content": "content"}), None,),
|
||||||
|
},
|
||||||
|
location="query",
|
||||||
|
)
|
||||||
|
def get(self, query_args):
|
||||||
|
q = query_args.pop("q", None)
|
||||||
|
field = str(query_args.pop("field", None))
|
||||||
|
CommentModel = get_comment_model(data=query_args)
|
||||||
|
filters = build_model_filters(model=CommentModel, query=q, field=field)
|
||||||
|
|
||||||
|
comments = CommentModel.query.filter_by(**query_args).filter(*filters).all()
|
||||||
|
schema = CommentSchema(many=True)
|
||||||
|
response = schema.dump(comments)
|
||||||
|
|
||||||
|
if response.errors:
|
||||||
|
return {"success": False, "errors": response.errors}, 400
|
||||||
|
|
||||||
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
|
@admins_only
|
||||||
|
@comments_namespace.doc(
|
||||||
|
description="Endpoint to create a Comment object",
|
||||||
|
responses={
|
||||||
|
200: ("Success", "CommentDetailedSuccessResponse"),
|
||||||
|
400: (
|
||||||
|
"An error occured processing the provided or stored data",
|
||||||
|
"APISimpleErrorResponse",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def post(self):
|
||||||
|
req = request.get_json()
|
||||||
|
# Always force author IDs to be the actual user
|
||||||
|
req["author_id"] = session["id"]
|
||||||
|
CommentModel = get_comment_model(data=req)
|
||||||
|
|
||||||
|
m = CommentModel(**req)
|
||||||
|
db.session.add(m)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
schema = CommentSchema()
|
||||||
|
|
||||||
|
response = schema.dump(m)
|
||||||
|
db.session.close()
|
||||||
|
|
||||||
|
return {"success": True, "data": response.data}
|
||||||
|
|
||||||
|
|
||||||
|
@comments_namespace.route("/<comment_id>")
|
||||||
|
class Comment(Resource):
|
||||||
|
@admins_only
|
||||||
|
@comments_namespace.doc(
|
||||||
|
description="Endpoint to delete a specific Comment object",
|
||||||
|
responses={200: ("Success", "APISimpleSuccessResponse")},
|
||||||
|
)
|
||||||
|
def delete(self, comment_id):
|
||||||
|
comment = Comments.query.filter_by(id=comment_id).first_or_404()
|
||||||
|
db.session.delete(comment)
|
||||||
|
db.session.commit()
|
||||||
|
db.session.close()
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
@@ -77,6 +77,7 @@ class Challenges(db.Model):
|
|||||||
tags = db.relationship("Tags", backref="challenge")
|
tags = db.relationship("Tags", backref="challenge")
|
||||||
hints = db.relationship("Hints", backref="challenge")
|
hints = db.relationship("Hints", backref="challenge")
|
||||||
flags = db.relationship("Flags", backref="challenge")
|
flags = db.relationship("Flags", backref="challenge")
|
||||||
|
comments = db.relationship("ChallengeComments", backref="challenge")
|
||||||
|
|
||||||
class alt_defaultdict(defaultdict):
|
class alt_defaultdict(defaultdict):
|
||||||
"""
|
"""
|
||||||
@@ -739,3 +740,44 @@ class Tokens(db.Model):
|
|||||||
|
|
||||||
class UserTokens(Tokens):
|
class UserTokens(Tokens):
|
||||||
__mapper_args__ = {"polymorphic_identity": "user"}
|
__mapper_args__ = {"polymorphic_identity": "user"}
|
||||||
|
|
||||||
|
|
||||||
|
class Comments(db.Model):
|
||||||
|
__tablename__ = "comments"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
type = db.Column(db.String(80), default="standard")
|
||||||
|
content = db.Column(db.Text)
|
||||||
|
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||||
|
author_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
|
author = db.relationship("Users", foreign_keys="Comments.author_id", lazy="select")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
from CTFd.utils.config.pages import build_html
|
||||||
|
from CTFd.utils.helpers import markup
|
||||||
|
|
||||||
|
return markup(build_html(self.content, sanitize=True))
|
||||||
|
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeComments(Comments):
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "challenge"}
|
||||||
|
challenge_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserComments(Comments):
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "user"}
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"))
|
||||||
|
|
||||||
|
|
||||||
|
class TeamComments(Comments):
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "team"}
|
||||||
|
team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE"))
|
||||||
|
|
||||||
|
|
||||||
|
class PageComments(Comments):
|
||||||
|
__mapper_args__ = {"polymorphic_identity": "page"}
|
||||||
|
page_id = db.Column(db.Integer, db.ForeignKey("pages.id", ondelete="CASCADE"))
|
||||||
|
|||||||
14
CTFd/schemas/comments.py
Normal file
14
CTFd/schemas/comments.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from CTFd.models import Comments, ma
|
||||||
|
from CTFd.schemas.users import UserSchema
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSchema(ma.ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = Comments
|
||||||
|
include_fk = True
|
||||||
|
dump_only = ("id", "date", "html", "author", "author_id", "type")
|
||||||
|
|
||||||
|
author = fields.Nested(UserSchema(only=("name",)))
|
||||||
|
html = fields.String()
|
||||||
155
CTFd/themes/admin/assets/js/components/comments/CommentBox.vue
Normal file
155
CTFd/themes/admin/assets/js/components/comments/CommentBox.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="comment">
|
||||||
|
<textarea
|
||||||
|
class="form-control mb-2"
|
||||||
|
rows="2"
|
||||||
|
id="comment-input"
|
||||||
|
placeholder="Add comment"
|
||||||
|
v-model.lazy="comment"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success btn-outlined float-right"
|
||||||
|
type="submit"
|
||||||
|
@click="submitComment()"
|
||||||
|
>
|
||||||
|
Comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comments">
|
||||||
|
<transition-group name="comment-card">
|
||||||
|
<div
|
||||||
|
class="comment-card card mb-2"
|
||||||
|
v-for="comment in comments.slice().reverse()"
|
||||||
|
:key="comment.id"
|
||||||
|
>
|
||||||
|
<div class="card-body pl-0 pb-0 pt-2 pr-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close float-right"
|
||||||
|
aria-label="Close"
|
||||||
|
@click="deleteComment(comment.id)"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-text" v-html="comment.html"></div>
|
||||||
|
<small class="text-muted float-left">
|
||||||
|
<span>
|
||||||
|
<a :href="`${urlRoot}/admin/users/${comment.author_id}`">{{
|
||||||
|
comment.author.name
|
||||||
|
}}</a>
|
||||||
|
</span>
|
||||||
|
</small>
|
||||||
|
<small class="text-muted float-right">
|
||||||
|
<span class="float-right">{{ toLocalTime(comment.date) }}</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CTFd from "core/CTFd";
|
||||||
|
import { default as helpers } from "core/helpers";
|
||||||
|
import Moment from "moment";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// These props are passed to the api via query string.
|
||||||
|
// See this.getArgs()
|
||||||
|
type: String,
|
||||||
|
id: Number
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
comment: "",
|
||||||
|
comments: [],
|
||||||
|
urlRoot: CTFd.config.urlRoot
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toLocalTime(date) {
|
||||||
|
return Moment(date)
|
||||||
|
.local()
|
||||||
|
.format("MMMM Do, h:mm:ss A");
|
||||||
|
},
|
||||||
|
getArgs: function() {
|
||||||
|
let args = {};
|
||||||
|
args[`${this.$props.type}_id`] = this.$props.id;
|
||||||
|
return args;
|
||||||
|
},
|
||||||
|
loadComments: function() {
|
||||||
|
let apiArgs = this.getArgs();
|
||||||
|
helpers.comments.get_comments(apiArgs).then(response => {
|
||||||
|
this.comments = response.data;
|
||||||
|
return this.comments;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitComment: function() {
|
||||||
|
let comment = this.comment.trim();
|
||||||
|
if (comment.length > 0) {
|
||||||
|
helpers.comments.add_comment(
|
||||||
|
comment,
|
||||||
|
"challenge",
|
||||||
|
this.getArgs(),
|
||||||
|
() => {
|
||||||
|
this.loadComments();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.comment = "";
|
||||||
|
},
|
||||||
|
deleteComment: function(commentId) {
|
||||||
|
if (confirm("Are you sure you'd like to delete this comment?")) {
|
||||||
|
helpers.comments.delete_comment(commentId).then(response => {
|
||||||
|
if (response.success === true) {
|
||||||
|
for (let i = this.comments.length - 1; i >= 0; --i) {
|
||||||
|
if (this.comments[i].id == commentId) {
|
||||||
|
this.comments.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadComments();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card .close {
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
.card:hover .close {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.close:hover {
|
||||||
|
opacity: 0.75 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-card-leave {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
.comment-card-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.comment-card-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.comment-card-enter-active,
|
||||||
|
.comment-card-move,
|
||||||
|
.comment-card-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,8 @@ import { addFile, deleteFile } from "../challenges/files";
|
|||||||
import { addTag, deleteTag } from "../challenges/tags";
|
import { addTag, deleteTag } from "../challenges/tags";
|
||||||
import { addRequirement, deleteRequirement } from "../challenges/requirements";
|
import { addRequirement, deleteRequirement } from "../challenges/requirements";
|
||||||
import { bindMarkdownEditors } from "../styles";
|
import { bindMarkdownEditors } from "../styles";
|
||||||
|
import Vue from "vue/dist/vue.esm.browser";
|
||||||
|
import CommentBox from "../components/comments/CommentBox.vue";
|
||||||
import {
|
import {
|
||||||
showHintModal,
|
showHintModal,
|
||||||
editHint,
|
editHint,
|
||||||
@@ -423,6 +425,14 @@ $(() => {
|
|||||||
$("#flags-create-select").change(flagTypeSelect);
|
$("#flags-create-select").change(flagTypeSelect);
|
||||||
$(".edit-flag").click(editFlagModal);
|
$(".edit-flag").click(editFlagModal);
|
||||||
|
|
||||||
|
// Insert CommentBox element
|
||||||
|
const commentBox = Vue.extend(CommentBox);
|
||||||
|
let vueContainer = document.createElement("div");
|
||||||
|
document.querySelector("#comment-box").appendChild(vueContainer);
|
||||||
|
new commentBox({
|
||||||
|
propsData: { type: "challenge", id: window.CHALLENGE_ID }
|
||||||
|
}).$mount(vueContainer);
|
||||||
|
|
||||||
$.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) {
|
$.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
loadChalTemplate(data["standard"]);
|
loadChalTemplate(data["standard"]);
|
||||||
|
|||||||
217
CTFd/themes/admin/static/js/core.min.js
vendored
217
CTFd/themes/admin/static/js/core.min.js
vendored
File diff suppressed because one or more lines are too long
15
CTFd/themes/admin/static/js/graphs.min.js
vendored
15
CTFd/themes/admin/static/js/graphs.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
CTFd/themes/admin/static/js/helpers.min.js
vendored
2
CTFd/themes/admin/static/js/helpers.min.js
vendored
@@ -1 +1 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{"./CTFd/themes/core/assets/js/helpers.js":function(e,r,o){Object.defineProperty(r,"__esModule",{value:!0}),r.default=void 0;var p=n(o("./node_modules/jquery/dist/jquery.js")),c=n(o("./CTFd/themes/core/assets/js/ezq.js")),t=o("./CTFd/themes/core/assets/js/utils.js");function n(e){return e&&e.__esModule?e:{default:e}}function f(e,r){return function(e){if(Array.isArray(e))return e}(e)||function(e,r){var o=[],t=!0,n=!1,s=void 0;try{for(var i,a=e[Symbol.iterator]();!(t=(i=a.next()).done)&&(o.push(i.value),!r||o.length!==r);t=!0);}catch(e){n=!0,s=e}finally{try{t||null==a.return||a.return()}finally{if(n)throw s}}return o}(e,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var s={files:{upload:function(r,e,o){var t=window.CTFd;r instanceof p.default&&(r=r[0]);var n=new FormData(r);n.append("nonce",t.config.csrfNonce);for(var s=0,i=Object.entries(e);s<i.length;s++){var a=f(i[s],2),l=a[0],d=a[1];n.append(l,d)}var u=c.default.ezProgressBar({width:0,title:"Upload Progress"});p.default.ajax({url:t.config.urlRoot+"/api/v1/files",data:n,type:"POST",cache:!1,contentType:!1,processData:!1,xhr:function(){var e=p.default.ajaxSettings.xhr();return e.upload.onprogress=function(e){if(e.lengthComputable){var r=e.loaded/e.total*100;u=c.default.ezProgressBar({target:u,width:r})}},e},success:function(e){r.reset(),u=c.default.ezProgressBar({target:u,width:100}),setTimeout(function(){u.modal("hide")},500),o&&o(e)}})}},utils:{htmlEntities:t.htmlEntities,colorHash:t.colorHash,copyToClipboard:t.copyToClipboard},ezq:c.default};r.default=s},"./node_modules/markdown-it/lib/helpers/index.js":function(e,r,o){r.parseLinkLabel=o("./node_modules/markdown-it/lib/helpers/parse_link_label.js"),r.parseLinkDestination=o("./node_modules/markdown-it/lib/helpers/parse_link_destination.js"),r.parseLinkTitle=o("./node_modules/markdown-it/lib/helpers/parse_link_title.js")},"./node_modules/markdown-it/lib/helpers/parse_link_destination.js":function(e,r,o){var a=o("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,r,o){var t,n,s=r,i={ok:!1,pos:0,lines:0,str:""};if(60===e.charCodeAt(r)){for(r++;r<o;){if(10===(t=e.charCodeAt(r)))return i;if(62===t)return i.pos=r+1,i.str=a(e.slice(s+1,r)),i.ok=!0,i;92===t&&r+1<o?r+=2:r++}return i}for(n=0;r<o&&32!==(t=e.charCodeAt(r))&&!(t<32||127===t);)if(92===t&&r+1<o)r+=2;else{if(40===t&&n++,41===t){if(0===n)break;n--}r++}return s===r||0!==n||(i.str=a(e.slice(s,r)),i.lines=0,i.pos=r,i.ok=!0),i}},"./node_modules/markdown-it/lib/helpers/parse_link_label.js":function(e,r,o){e.exports=function(e,r,o){var t,n,s,i,a=-1,l=e.posMax,d=e.pos;for(e.pos=r+1,t=1;e.pos<l;){if(93===(s=e.src.charCodeAt(e.pos))&&0===--t){n=!0;break}if(i=e.pos,e.md.inline.skipToken(e),91===s)if(i===e.pos-1)t++;else if(o)return e.pos=d,-1}return n&&(a=e.pos),e.pos=d,a}},"./node_modules/markdown-it/lib/helpers/parse_link_title.js":function(e,r,o){var l=o("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,r,o){var t,n,s=0,i=r,a={ok:!1,pos:0,lines:0,str:""};if(o<=r)return a;if(34!==(n=e.charCodeAt(r))&&39!==n&&40!==n)return a;for(r++,40===n&&(n=41);r<o;){if((t=e.charCodeAt(r))===n)return a.pos=r+1,a.lines=s,a.str=l(e.slice(i+1,r)),a.ok=!0,a;10===t?s++:92===t&&r+1<o&&(r++,10===e.charCodeAt(r)&&s++),r++}return a}}}]);
|
(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{"./CTFd/themes/core/assets/js/helpers.js":function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var d=r(n("./node_modules/jquery/dist/jquery.js")),p=r(n("./CTFd/themes/core/assets/js/ezq.js")),o=n("./CTFd/themes/core/assets/js/utils.js");function r(e){return e&&e.__esModule?e:{default:e}}function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function f(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=[],o=!0,r=!1,i=void 0;try{for(var s,a=e[Symbol.iterator]();!(o=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);o=!0);}catch(e){r=!0,i=e}finally{try{o||null==a.return||a.return()}finally{if(r)throw i}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var i={files:{upload:function(t,e,n){var o=window.CTFd;t instanceof d.default&&(t=t[0]);var r=new FormData(t);r.append("nonce",o.config.csrfNonce);for(var i=0,s=Object.entries(e);i<s.length;i++){var a=f(s[i],2),l=a[0],c=a[1];r.append(l,c)}var u=p.default.ezProgressBar({width:0,title:"Upload Progress"});d.default.ajax({url:o.config.urlRoot+"/api/v1/files",data:r,type:"POST",cache:!1,contentType:!1,processData:!1,xhr:function(){var e=d.default.ajaxSettings.xhr();return e.upload.onprogress=function(e){if(e.lengthComputable){var t=e.loaded/e.total*100;u=p.default.ezProgressBar({target:u,width:t})}},e},success:function(e){t.reset(),u=p.default.ezProgressBar({target:u,width:100}),setTimeout(function(){u.modal("hide")},500),n&&n(e)}})}},comments:{get_comments:function(e){return window.CTFd.fetch("/api/v1/comments?"+d.default.param(e),{method:"GET",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then(function(e){return e.json()})},add_comment:function(e,t,n,o){var r=window.CTFd,i=function(t){for(var e=1;e<arguments.length;e++)if(e%2){var n=null!=arguments[e]?arguments[e]:{},o=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(o=o.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),o.forEach(function(e){s(t,e,n[e])})}else Object.defineProperties(t,Object.getOwnPropertyDescriptors(arguments[e]));return t}({content:e,type:t},n);r.fetch("/api/v1/comments",{method:"POST",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(i)}).then(function(e){return e.json()}).then(function(e){o&&o(e)})},delete_comment:function(e){return window.CTFd.fetch("/api/v1/comments/".concat(e),{method:"DELETE",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then(function(e){return e.json()})}},utils:{htmlEntities:o.htmlEntities,colorHash:o.colorHash,copyToClipboard:o.copyToClipboard},ezq:p.default};t.default=i},"./node_modules/markdown-it/lib/helpers/index.js":function(e,t,n){t.parseLinkLabel=n("./node_modules/markdown-it/lib/helpers/parse_link_label.js"),t.parseLinkDestination=n("./node_modules/markdown-it/lib/helpers/parse_link_destination.js"),t.parseLinkTitle=n("./node_modules/markdown-it/lib/helpers/parse_link_title.js")},"./node_modules/markdown-it/lib/helpers/parse_link_destination.js":function(e,t,n){var a=n("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,t,n){var o,r,i=t,s={ok:!1,pos:0,lines:0,str:""};if(60===e.charCodeAt(t)){for(t++;t<n;){if(10===(o=e.charCodeAt(t)))return s;if(62===o)return s.pos=t+1,s.str=a(e.slice(i+1,t)),s.ok=!0,s;92===o&&t+1<n?t+=2:t++}return s}for(r=0;t<n&&32!==(o=e.charCodeAt(t))&&!(o<32||127===o);)if(92===o&&t+1<n)t+=2;else{if(40===o&&r++,41===o){if(0===r)break;r--}t++}return i===t||0!==r||(s.str=a(e.slice(i,t)),s.lines=0,s.pos=t,s.ok=!0),s}},"./node_modules/markdown-it/lib/helpers/parse_link_label.js":function(e,t,n){e.exports=function(e,t,n){var o,r,i,s,a=-1,l=e.posMax,c=e.pos;for(e.pos=t+1,o=1;e.pos<l;){if(93===(i=e.src.charCodeAt(e.pos))&&0===--o){r=!0;break}if(s=e.pos,e.md.inline.skipToken(e),91===i)if(s===e.pos-1)o++;else if(n)return e.pos=c,-1}return r&&(a=e.pos),e.pos=c,a}},"./node_modules/markdown-it/lib/helpers/parse_link_title.js":function(e,t,n){var l=n("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,t,n){var o,r,i=0,s=t,a={ok:!1,pos:0,lines:0,str:""};if(n<=t)return a;if(34!==(r=e.charCodeAt(t))&&39!==r&&40!==r)return a;for(t++,40===r&&(r=41);t<n;){if((o=e.charCodeAt(t))===r)return a.pos=t+1,a.lines=i,a.str=l(e.slice(s+1,t)),a.ok=!0,a;10===o?i++:92===o&&t+1<n&&(t++,10===e.charCodeAt(t)&&i++),t++}return a}}}]);
|
||||||
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
20
CTFd/themes/admin/static/js/vendor.bundle.min.js
vendored
20
CTFd/themes/admin/static/js/vendor.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -45,7 +45,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<nav class="nav nav-tabs nav-fill" id="challenge-properties" role="tablist">
|
<nav class="nav nav-tabs nav-fill" id="challenge-properties" role="tablist">
|
||||||
<a class="nav-item nav-link active" data-toggle="tab" href="#solves" role="tab" >Solves</a>
|
<a class="nav-item nav-link active" data-toggle="tab" href="#comments" role="tab" >Comments</a>
|
||||||
<a class="nav-item nav-link" data-toggle="tab" href="#flags" role="tab">Flags</a>
|
<a class="nav-item nav-link" data-toggle="tab" href="#flags" role="tab">Flags</a>
|
||||||
<a class="nav-item nav-link" data-toggle="tab" href="#files" role="tab">Files</a>
|
<a class="nav-item nav-link" data-toggle="tab" href="#files" role="tab">Files</a>
|
||||||
<a class="nav-item nav-link" data-toggle="tab" href="#tags" role="tab">Tags</a>
|
<a class="nav-item nav-link" data-toggle="tab" href="#tags" role="tab">Tags</a>
|
||||||
@@ -54,11 +54,14 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div class="tab-pane fade show active" id="solves" role="tabpanel">
|
<div class="tab-pane fade show active" id="comments" role="tabpanel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h3 class="text-center py-3 d-block">Solves</h3>
|
<h3 class="text-center py-3 d-block">
|
||||||
{% include "admin/modals/challenges/solves.html" %}
|
Comments
|
||||||
|
</h3>
|
||||||
|
<div id="comment-box">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,8 +63,63 @@ const files = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const comments = {
|
||||||
|
get_comments: extra_args => {
|
||||||
|
const CTFd = window.CTFd;
|
||||||
|
return CTFd.fetch("/api/v1/comments?" + $.param(extra_args), {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
add_comment: (comment, type, extra_args, cb) => {
|
||||||
|
const CTFd = window.CTFd;
|
||||||
|
let body = {
|
||||||
|
content: comment,
|
||||||
|
type: type,
|
||||||
|
...extra_args
|
||||||
|
};
|
||||||
|
CTFd.fetch("/api/v1/comments", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (cb) {
|
||||||
|
cb(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delete_comment: comment_id => {
|
||||||
|
const CTFd = window.CTFd;
|
||||||
|
return CTFd.fetch(`/api/v1/comments/${comment_id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(function(response) {
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const helpers = {
|
const helpers = {
|
||||||
files,
|
files,
|
||||||
|
comments,
|
||||||
utils,
|
utils,
|
||||||
ezq
|
ezq
|
||||||
};
|
};
|
||||||
|
|||||||
146
CTFd/themes/core/static/js/core.min.js
vendored
146
CTFd/themes/core/static/js/core.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
CTFd/themes/core/static/js/helpers.min.js
vendored
2
CTFd/themes/core/static/js/helpers.min.js
vendored
@@ -1 +1 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{"./CTFd/themes/core/assets/js/helpers.js":function(e,r,o){Object.defineProperty(r,"__esModule",{value:!0}),r.default=void 0;var p=n(o("./node_modules/jquery/dist/jquery.js")),c=n(o("./CTFd/themes/core/assets/js/ezq.js")),t=o("./CTFd/themes/core/assets/js/utils.js");function n(e){return e&&e.__esModule?e:{default:e}}function f(e,r){return function(e){if(Array.isArray(e))return e}(e)||function(e,r){var o=[],t=!0,n=!1,s=void 0;try{for(var i,a=e[Symbol.iterator]();!(t=(i=a.next()).done)&&(o.push(i.value),!r||o.length!==r);t=!0);}catch(e){n=!0,s=e}finally{try{t||null==a.return||a.return()}finally{if(n)throw s}}return o}(e,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var s={files:{upload:function(r,e,o){var t=window.CTFd;r instanceof p.default&&(r=r[0]);var n=new FormData(r);n.append("nonce",t.config.csrfNonce);for(var s=0,i=Object.entries(e);s<i.length;s++){var a=f(i[s],2),l=a[0],d=a[1];n.append(l,d)}var u=c.default.ezProgressBar({width:0,title:"Upload Progress"});p.default.ajax({url:t.config.urlRoot+"/api/v1/files",data:n,type:"POST",cache:!1,contentType:!1,processData:!1,xhr:function(){var e=p.default.ajaxSettings.xhr();return e.upload.onprogress=function(e){if(e.lengthComputable){var r=e.loaded/e.total*100;u=c.default.ezProgressBar({target:u,width:r})}},e},success:function(e){r.reset(),u=c.default.ezProgressBar({target:u,width:100}),setTimeout(function(){u.modal("hide")},500),o&&o(e)}})}},utils:{htmlEntities:t.htmlEntities,colorHash:t.colorHash,copyToClipboard:t.copyToClipboard},ezq:c.default};r.default=s},"./node_modules/markdown-it/lib/helpers/index.js":function(e,r,o){r.parseLinkLabel=o("./node_modules/markdown-it/lib/helpers/parse_link_label.js"),r.parseLinkDestination=o("./node_modules/markdown-it/lib/helpers/parse_link_destination.js"),r.parseLinkTitle=o("./node_modules/markdown-it/lib/helpers/parse_link_title.js")},"./node_modules/markdown-it/lib/helpers/parse_link_destination.js":function(e,r,o){var a=o("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,r,o){var t,n,s=r,i={ok:!1,pos:0,lines:0,str:""};if(60===e.charCodeAt(r)){for(r++;r<o;){if(10===(t=e.charCodeAt(r)))return i;if(62===t)return i.pos=r+1,i.str=a(e.slice(s+1,r)),i.ok=!0,i;92===t&&r+1<o?r+=2:r++}return i}for(n=0;r<o&&32!==(t=e.charCodeAt(r))&&!(t<32||127===t);)if(92===t&&r+1<o)r+=2;else{if(40===t&&n++,41===t){if(0===n)break;n--}r++}return s===r||0!==n||(i.str=a(e.slice(s,r)),i.lines=0,i.pos=r,i.ok=!0),i}},"./node_modules/markdown-it/lib/helpers/parse_link_label.js":function(e,r,o){e.exports=function(e,r,o){var t,n,s,i,a=-1,l=e.posMax,d=e.pos;for(e.pos=r+1,t=1;e.pos<l;){if(93===(s=e.src.charCodeAt(e.pos))&&0===--t){n=!0;break}if(i=e.pos,e.md.inline.skipToken(e),91===s)if(i===e.pos-1)t++;else if(o)return e.pos=d,-1}return n&&(a=e.pos),e.pos=d,a}},"./node_modules/markdown-it/lib/helpers/parse_link_title.js":function(e,r,o){var l=o("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,r,o){var t,n,s=0,i=r,a={ok:!1,pos:0,lines:0,str:""};if(o<=r)return a;if(34!==(n=e.charCodeAt(r))&&39!==n&&40!==n)return a;for(r++,40===n&&(n=41);r<o;){if((t=e.charCodeAt(r))===n)return a.pos=r+1,a.lines=s,a.str=l(e.slice(i+1,r)),a.ok=!0,a;10===t?s++:92===t&&r+1<o&&(r++,10===e.charCodeAt(r)&&s++),r++}return a}}}]);
|
(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{"./CTFd/themes/core/assets/js/helpers.js":function(e,t,n){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var d=r(n("./node_modules/jquery/dist/jquery.js")),p=r(n("./CTFd/themes/core/assets/js/ezq.js")),o=n("./CTFd/themes/core/assets/js/utils.js");function r(e){return e&&e.__esModule?e:{default:e}}function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function f(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=[],o=!0,r=!1,i=void 0;try{for(var s,a=e[Symbol.iterator]();!(o=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);o=!0);}catch(e){r=!0,i=e}finally{try{o||null==a.return||a.return()}finally{if(r)throw i}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var i={files:{upload:function(t,e,n){var o=window.CTFd;t instanceof d.default&&(t=t[0]);var r=new FormData(t);r.append("nonce",o.config.csrfNonce);for(var i=0,s=Object.entries(e);i<s.length;i++){var a=f(s[i],2),l=a[0],c=a[1];r.append(l,c)}var u=p.default.ezProgressBar({width:0,title:"Upload Progress"});d.default.ajax({url:o.config.urlRoot+"/api/v1/files",data:r,type:"POST",cache:!1,contentType:!1,processData:!1,xhr:function(){var e=d.default.ajaxSettings.xhr();return e.upload.onprogress=function(e){if(e.lengthComputable){var t=e.loaded/e.total*100;u=p.default.ezProgressBar({target:u,width:t})}},e},success:function(e){t.reset(),u=p.default.ezProgressBar({target:u,width:100}),setTimeout(function(){u.modal("hide")},500),n&&n(e)}})}},comments:{get_comments:function(e){return window.CTFd.fetch("/api/v1/comments?"+d.default.param(e),{method:"GET",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then(function(e){return e.json()})},add_comment:function(e,t,n,o){var r=window.CTFd,i=function(t){for(var e=1;e<arguments.length;e++)if(e%2){var n=null!=arguments[e]?arguments[e]:{},o=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(o=o.concat(Object.getOwnPropertySymbols(n).filter(function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable}))),o.forEach(function(e){s(t,e,n[e])})}else Object.defineProperties(t,Object.getOwnPropertyDescriptors(arguments[e]));return t}({content:e,type:t},n);r.fetch("/api/v1/comments",{method:"POST",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify(i)}).then(function(e){return e.json()}).then(function(e){o&&o(e)})},delete_comment:function(e){return window.CTFd.fetch("/api/v1/comments/".concat(e),{method:"DELETE",credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then(function(e){return e.json()})}},utils:{htmlEntities:o.htmlEntities,colorHash:o.colorHash,copyToClipboard:o.copyToClipboard},ezq:p.default};t.default=i},"./node_modules/markdown-it/lib/helpers/index.js":function(e,t,n){t.parseLinkLabel=n("./node_modules/markdown-it/lib/helpers/parse_link_label.js"),t.parseLinkDestination=n("./node_modules/markdown-it/lib/helpers/parse_link_destination.js"),t.parseLinkTitle=n("./node_modules/markdown-it/lib/helpers/parse_link_title.js")},"./node_modules/markdown-it/lib/helpers/parse_link_destination.js":function(e,t,n){var a=n("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,t,n){var o,r,i=t,s={ok:!1,pos:0,lines:0,str:""};if(60===e.charCodeAt(t)){for(t++;t<n;){if(10===(o=e.charCodeAt(t)))return s;if(62===o)return s.pos=t+1,s.str=a(e.slice(i+1,t)),s.ok=!0,s;92===o&&t+1<n?t+=2:t++}return s}for(r=0;t<n&&32!==(o=e.charCodeAt(t))&&!(o<32||127===o);)if(92===o&&t+1<n)t+=2;else{if(40===o&&r++,41===o){if(0===r)break;r--}t++}return i===t||0!==r||(s.str=a(e.slice(i,t)),s.lines=0,s.pos=t,s.ok=!0),s}},"./node_modules/markdown-it/lib/helpers/parse_link_label.js":function(e,t,n){e.exports=function(e,t,n){var o,r,i,s,a=-1,l=e.posMax,c=e.pos;for(e.pos=t+1,o=1;e.pos<l;){if(93===(i=e.src.charCodeAt(e.pos))&&0===--o){r=!0;break}if(s=e.pos,e.md.inline.skipToken(e),91===i)if(s===e.pos-1)o++;else if(n)return e.pos=c,-1}return r&&(a=e.pos),e.pos=c,a}},"./node_modules/markdown-it/lib/helpers/parse_link_title.js":function(e,t,n){var l=n("./node_modules/markdown-it/lib/common/utils.js").unescapeAll;e.exports=function(e,t,n){var o,r,i=0,s=t,a={ok:!1,pos:0,lines:0,str:""};if(n<=t)return a;if(34!==(r=e.charCodeAt(t))&&39!==r&&40!==r)return a;for(t++,40===r&&(r=41);t<n;){if((o=e.charCodeAt(t))===r)return a.pos=t+1,a.lines=i,a.str=l(e.slice(s+1,t)),a.ok=!0,a;10===o?i++:92===o&&t+1<n&&(t++,10===e.charCodeAt(t)&&i++),t++}return a}}}]);
|
||||||
@@ -6,9 +6,9 @@ from CTFd.utils import markdown
|
|||||||
from CTFd.utils.security.sanitize import sanitize_html
|
from CTFd.utils.security.sanitize import sanitize_html
|
||||||
|
|
||||||
|
|
||||||
def build_html(html):
|
def build_html(html, sanitize=False):
|
||||||
html = markdown(html)
|
html = markdown(html)
|
||||||
if current_app.config["HTML_SANITIZATION"] is True:
|
if current_app.config["HTML_SANITIZATION"] is True or sanitize is True:
|
||||||
html = sanitize_html(html)
|
html = sanitize_html(html)
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
|||||||
108
tests/api/v1/test_comments.py
Normal file
108
tests/api/v1/test_comments.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from CTFd.models import Comments
|
||||||
|
from tests.helpers import (
|
||||||
|
create_ctfd,
|
||||||
|
destroy_ctfd,
|
||||||
|
gen_challenge,
|
||||||
|
gen_comment,
|
||||||
|
login_as_user,
|
||||||
|
register_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_post_comments():
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
gen_challenge(app.db)
|
||||||
|
with login_as_user(app, "admin") as admin:
|
||||||
|
r = admin.post(
|
||||||
|
"/api/v1/comments",
|
||||||
|
json={
|
||||||
|
"content": "this is a challenge comment",
|
||||||
|
"type": "challenge",
|
||||||
|
"challenge_id": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Check that POST response has comment data
|
||||||
|
assert r.status_code == 200
|
||||||
|
resp = r.get_json()
|
||||||
|
assert resp["data"]["content"] == "this is a challenge comment"
|
||||||
|
assert "this is a challenge comment" in resp["data"]["html"]
|
||||||
|
assert resp["data"]["type"] == "challenge"
|
||||||
|
|
||||||
|
# Check that the comment shows up in the list of comments for the given challenge
|
||||||
|
r = admin.get("/api/v1/comments?challenge_id=1", json="")
|
||||||
|
assert r.status_code == 200
|
||||||
|
resp = r.get_json()
|
||||||
|
assert resp["data"][0]["content"] == "this is a challenge comment"
|
||||||
|
assert "this is a challenge comment" in resp["data"][0]["html"]
|
||||||
|
assert resp["data"][0]["type"] == "challenge"
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_post_comments_with_invalid_author_id():
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
gen_challenge(app.db)
|
||||||
|
register_user(app)
|
||||||
|
with login_as_user(app, "admin") as admin:
|
||||||
|
r = admin.post(
|
||||||
|
"/api/v1/comments",
|
||||||
|
json={
|
||||||
|
"content": "this is a challenge comment",
|
||||||
|
"type": "challenge",
|
||||||
|
"challenge_id": 1,
|
||||||
|
"author_id": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Check that POST response has comment data
|
||||||
|
assert r.status_code == 200
|
||||||
|
resp = r.get_json()
|
||||||
|
assert resp["data"]["author_id"] == 1
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_get_comments():
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
gen_challenge(app.db)
|
||||||
|
with login_as_user(app, "admin") as admin:
|
||||||
|
gen_comment(
|
||||||
|
app.db,
|
||||||
|
content="this is a challenge comment",
|
||||||
|
author_id=1,
|
||||||
|
challenge_id=1,
|
||||||
|
)
|
||||||
|
r = admin.get("/api/v1/comments", json="")
|
||||||
|
|
||||||
|
# Check that the comment shows up in the list of all comments
|
||||||
|
assert r.status_code == 200
|
||||||
|
resp = r.get_json()
|
||||||
|
assert resp["data"][0]["content"] == "this is a challenge comment"
|
||||||
|
assert "this is a challenge comment" in resp["data"][0]["html"]
|
||||||
|
assert resp["data"][0]["type"] == "challenge"
|
||||||
|
destroy_ctfd(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_delete_comments():
|
||||||
|
app = create_ctfd()
|
||||||
|
with app.app_context():
|
||||||
|
gen_challenge(app.db)
|
||||||
|
with login_as_user(app, "admin") as admin:
|
||||||
|
gen_comment(
|
||||||
|
app.db,
|
||||||
|
content="this is a challenge comment",
|
||||||
|
author_id=1,
|
||||||
|
challenge_id=1,
|
||||||
|
)
|
||||||
|
assert Comments.query.count() == 1
|
||||||
|
|
||||||
|
# Check that the comment can be deleted
|
||||||
|
r = admin.delete("/api/v1/comments/1", json="")
|
||||||
|
assert r.status_code == 200
|
||||||
|
resp = r.get_json()
|
||||||
|
assert Comments.query.count() == 0
|
||||||
|
assert resp["success"] is True
|
||||||
|
destroy_ctfd(app)
|
||||||
@@ -17,21 +17,26 @@ from CTFd.cache import cache, clear_standings
|
|||||||
from CTFd.config import TestingConfig
|
from CTFd.config import TestingConfig
|
||||||
from CTFd.models import (
|
from CTFd.models import (
|
||||||
Awards,
|
Awards,
|
||||||
|
ChallengeComments,
|
||||||
ChallengeFiles,
|
ChallengeFiles,
|
||||||
Challenges,
|
Challenges,
|
||||||
|
Comments,
|
||||||
Fails,
|
Fails,
|
||||||
Files,
|
Files,
|
||||||
Flags,
|
Flags,
|
||||||
Hints,
|
Hints,
|
||||||
Notifications,
|
Notifications,
|
||||||
|
PageComments,
|
||||||
PageFiles,
|
PageFiles,
|
||||||
Pages,
|
Pages,
|
||||||
Solves,
|
Solves,
|
||||||
Tags,
|
Tags,
|
||||||
|
TeamComments,
|
||||||
Teams,
|
Teams,
|
||||||
Tokens,
|
Tokens,
|
||||||
Tracking,
|
Tracking,
|
||||||
Unlocks,
|
Unlocks,
|
||||||
|
UserComments,
|
||||||
Users,
|
Users,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -435,6 +440,24 @@ def gen_token(db, type="user", user_id=None, expiration=None):
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def gen_comment(db, content="comment", author_id=None, type="challenge", **kwargs):
|
||||||
|
if type == "challenge":
|
||||||
|
model = ChallengeComments
|
||||||
|
elif type == "user":
|
||||||
|
model = UserComments
|
||||||
|
elif type == "team":
|
||||||
|
model = TeamComments
|
||||||
|
elif type == "page":
|
||||||
|
model = PageComments
|
||||||
|
else:
|
||||||
|
model = Comments
|
||||||
|
|
||||||
|
comment = model(content=content, author_id=author_id, type=type, **kwargs)
|
||||||
|
db.session.add(comment)
|
||||||
|
db.session.commit()
|
||||||
|
return comment
|
||||||
|
|
||||||
|
|
||||||
def simulate_user_activity(db, user):
|
def simulate_user_activity(db, user):
|
||||||
gen_tracking(db, user_id=user.id)
|
gen_tracking(db, user_id=user.id)
|
||||||
gen_award(db, user_id=user.id)
|
gen_award(db, user_id=user.id)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ function getJSConfig(root, type, entries, mode) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
entry: out,
|
entry: out,
|
||||||
|
mode: mode,
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
|
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
|
||||||
publicPath: '/' + root + '/static/' + type,
|
publicPath: '/' + root + '/static/' + type,
|
||||||
@@ -147,7 +148,16 @@ function getJSConfig(root, type, entries, mode) {
|
|||||||
},
|
},
|
||||||
cacheBusting: true,
|
cacheBusting: true,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
// This rule is magically used to load the <style> section of VueJS SFC.
|
||||||
|
// Don't really understand what magic Vue is using here but it works.
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -184,6 +194,7 @@ function getCSSConfig(root, type, entries, mode) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
entry: out,
|
entry: out,
|
||||||
|
mode: mode,
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
|
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
|
||||||
publicPath: '/' + root + '/static/' + type,
|
publicPath: '/' + root + '/static/' + type,
|
||||||
@@ -208,13 +219,6 @@ function getCSSConfig(root, type, entries, mode) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// test: /\.css$/,
|
|
||||||
// use: [
|
|
||||||
// 'vue-style-loader',
|
|
||||||
// 'css-loader'
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
test: /\.(s?)css$/,
|
test: /\.(s?)css$/,
|
||||||
use: [
|
use: [
|
||||||
|
|||||||
Reference in New Issue
Block a user