mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 05:54:19 +01:00
Allow Page editor to write HTML directly (#1915)
* Works on #1493 * Adds a new column for Pages to specify format * Separate out `build_html` into `build_html` and `build_markdown` * Add config variables into pages: `ctf_name`, `ctf_description`, `ctf_start`, `ctf_end`, `ctf_freeze` * The time variables are represented as ISO8601 timestamps
This commit is contained in:
@@ -4,7 +4,6 @@ from CTFd.admin import admin
|
|||||||
from CTFd.models import Pages
|
from CTFd.models import Pages
|
||||||
from CTFd.schemas.pages import PageSchema
|
from CTFd.schemas.pages import PageSchema
|
||||||
from CTFd.utils import markdown
|
from CTFd.utils import markdown
|
||||||
from CTFd.utils.config.pages import build_html
|
|
||||||
from CTFd.utils.decorators import admins_only
|
from CTFd.utils.decorators import admins_only
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ def pages_preview():
|
|||||||
data = {"content": request.form.get("content")}
|
data = {"content": request.form.get("content")}
|
||||||
schema = PageSchema()
|
schema = PageSchema()
|
||||||
page = schema.load(data)
|
page = schema.load(data)
|
||||||
return render_template("page.html", content=build_html(page.data.content))
|
return render_template("page.html", content=page.data.html)
|
||||||
|
|
||||||
|
|
||||||
@admin.route("/admin/pages/<int:page_id>")
|
@admin.route("/admin/pages/<int:page_id>")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from wtforms import (
|
|||||||
BooleanField,
|
BooleanField,
|
||||||
HiddenField,
|
HiddenField,
|
||||||
MultipleFileField,
|
MultipleFileField,
|
||||||
|
SelectField,
|
||||||
StringField,
|
StringField,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,13 @@ class PageEditForm(BaseForm):
|
|||||||
hidden = BooleanField("Hidden")
|
hidden = BooleanField("Hidden")
|
||||||
auth_required = BooleanField("Authentication Required")
|
auth_required = BooleanField("Authentication Required")
|
||||||
content = TextAreaField("Content")
|
content = TextAreaField("Content")
|
||||||
|
format = SelectField(
|
||||||
|
"Format",
|
||||||
|
choices=[("markdown", "Markdown"), ("html", "HTML")],
|
||||||
|
default="markdown",
|
||||||
|
validators=[InputRequired()],
|
||||||
|
description="The markup format used to render the page",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PageFilesUploadForm(BaseForm):
|
class PageFilesUploadForm(BaseForm):
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class Notifications(db.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
from CTFd.utils.config.pages import build_html
|
from CTFd.utils.config.pages import build_markdown
|
||||||
from CTFd.utils.helpers import markup
|
from CTFd.utils.helpers import markup
|
||||||
|
|
||||||
return markup(build_html(self.content))
|
return markup(build_markdown(self.content))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Notifications, self).__init__(**kwargs)
|
super(Notifications, self).__init__(**kwargs)
|
||||||
@@ -57,10 +57,22 @@ class Pages(db.Model):
|
|||||||
draft = db.Column(db.Boolean)
|
draft = db.Column(db.Boolean)
|
||||||
hidden = db.Column(db.Boolean)
|
hidden = db.Column(db.Boolean)
|
||||||
auth_required = db.Column(db.Boolean)
|
auth_required = db.Column(db.Boolean)
|
||||||
|
format = db.Column(db.String(80), default="markdown")
|
||||||
# TODO: Use hidden attribute
|
# TODO: Use hidden attribute
|
||||||
|
|
||||||
files = db.relationship("PageFiles", backref="page")
|
files = db.relationship("PageFiles", backref="page")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
from CTFd.utils.config.pages import build_html, build_markdown
|
||||||
|
|
||||||
|
if self.format == "markdown":
|
||||||
|
return build_markdown(self.content)
|
||||||
|
elif self.format == "html":
|
||||||
|
return build_html(self.content)
|
||||||
|
else:
|
||||||
|
return build_markdown(self.content)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Pages, self).__init__(**kwargs)
|
super(Pages, self).__init__(**kwargs)
|
||||||
|
|
||||||
@@ -105,10 +117,10 @@ class Challenges(db.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
from CTFd.utils.config.pages import build_html
|
from CTFd.utils.config.pages import build_markdown
|
||||||
from CTFd.utils.helpers import markup
|
from CTFd.utils.helpers import markup
|
||||||
|
|
||||||
return markup(build_html(self.description))
|
return markup(build_markdown(self.description))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Challenges, self).__init__(**kwargs)
|
super(Challenges, self).__init__(**kwargs)
|
||||||
@@ -144,10 +156,10 @@ class Hints(db.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
from CTFd.utils.config.pages import build_html
|
from CTFd.utils.config.pages import build_markdown
|
||||||
from CTFd.utils.helpers import markup
|
from CTFd.utils.helpers import markup
|
||||||
|
|
||||||
return markup(build_html(self.content))
|
return markup(build_markdown(self.content))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Hints, self).__init__(**kwargs)
|
super(Hints, self).__init__(**kwargs)
|
||||||
@@ -848,10 +860,10 @@ class Comments(db.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
from CTFd.utils.config.pages import build_html
|
from CTFd.utils.config.pages import build_markdown
|
||||||
from CTFd.utils.helpers import markup
|
from CTFd.utils.helpers import markup
|
||||||
|
|
||||||
return markup(build_html(self.content, sanitize=True))
|
return markup(build_markdown(self.content, sanitize=True))
|
||||||
|
|
||||||
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{% extends "admin/base.html" %}
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
|
<style>
|
||||||
|
.CodeMirror {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -19,7 +24,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set content = page.content if page is defined else "" %}
|
{% set content = page.content if page is defined else "" %}
|
||||||
{% with form = Forms.pages.PageEditForm(content=content) %}
|
{% set format = page.format if page is defined %}
|
||||||
|
{% with form = Forms.pages.PageEditForm(content=content, format=format) %}
|
||||||
<form id="page-edit" method="POST">
|
<form id="page-edit" method="POST">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@@ -43,6 +49,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<b>{{ form.format.label }}</b>
|
||||||
|
{{ form.format(class="form-control custom-select", placeholder="Route", value=format) }}
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ form.format.description }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
|
|
||||||
@@ -60,7 +76,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:400px">
|
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:600px">
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
@@ -96,23 +112,22 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group h-100">
|
||||||
{{ form.content(id="admin-pages-editor", class="d-none") }}
|
{{ form.content(id="admin-pages-editor", class="d-none") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group float-right pt-3">
|
||||||
|
{{ form.nonce() }}
|
||||||
|
<button class="btn btn-primary" id="save-page">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
|
<div role="tabpanel" class="tab-pane content" id="content-preview">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group float-right">
|
|
||||||
{{ form.nonce() }}
|
|
||||||
<button class="btn btn-primary" id="save-page">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,48 @@ from flask import current_app
|
|||||||
|
|
||||||
from CTFd.cache import cache
|
from CTFd.cache import cache
|
||||||
from CTFd.models import Pages, db
|
from CTFd.models import Pages, db
|
||||||
from CTFd.utils import markdown
|
from CTFd.utils import get_config, markdown
|
||||||
|
from CTFd.utils.dates import isoformat, unix_time_to_utc
|
||||||
|
from CTFd.utils.formatters import safe_format
|
||||||
from CTFd.utils.security.sanitize import sanitize_html
|
from CTFd.utils.security.sanitize import sanitize_html
|
||||||
|
|
||||||
|
|
||||||
|
def format_variables(content):
|
||||||
|
ctf_name = get_config("ctf_name")
|
||||||
|
ctf_description = get_config("ctf_description")
|
||||||
|
ctf_start = get_config("start")
|
||||||
|
if ctf_start:
|
||||||
|
ctf_start = isoformat(unix_time_to_utc(int(ctf_start)))
|
||||||
|
|
||||||
|
ctf_end = get_config("end")
|
||||||
|
if ctf_end:
|
||||||
|
ctf_end = isoformat(unix_time_to_utc(int(ctf_end)))
|
||||||
|
|
||||||
|
ctf_freeze = get_config("freeze")
|
||||||
|
if ctf_freeze:
|
||||||
|
ctf_freeze = isoformat(unix_time_to_utc(int(ctf_freeze)))
|
||||||
|
|
||||||
|
content = safe_format(
|
||||||
|
content,
|
||||||
|
ctf_name=ctf_name,
|
||||||
|
ctf_description=ctf_description,
|
||||||
|
ctf_start=ctf_start,
|
||||||
|
ctf_end=ctf_end,
|
||||||
|
ctf_freeze=ctf_freeze,
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def build_html(html, sanitize=False):
|
def build_html(html, sanitize=False):
|
||||||
html = markdown(html)
|
html = format_variables(html)
|
||||||
|
if current_app.config["HTML_SANITIZATION"] is True or sanitize is True:
|
||||||
|
html = sanitize_html(html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def build_markdown(md, sanitize=False):
|
||||||
|
html = markdown(md)
|
||||||
|
html = format_variables(html)
|
||||||
if current_app.config["HTML_SANITIZATION"] is True or sanitize 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
|
||||||
@@ -31,6 +67,5 @@ def get_page(route):
|
|||||||
if page:
|
if page:
|
||||||
# Convert the row into a transient ORM object so this change isn't commited accidentally
|
# Convert the row into a transient ORM object so this change isn't commited accidentally
|
||||||
p = Pages(**page)
|
p = Pages(**page)
|
||||||
p.content = build_html(p.content)
|
|
||||||
return p
|
return p
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from CTFd.utils import config, get_config, set_config
|
|||||||
from CTFd.utils import user as current_user
|
from CTFd.utils import user as current_user
|
||||||
from CTFd.utils import validators
|
from CTFd.utils import validators
|
||||||
from CTFd.utils.config import is_setup
|
from CTFd.utils.config import is_setup
|
||||||
from CTFd.utils.config.pages import build_html, get_page
|
from CTFd.utils.config.pages import build_markdown, get_page
|
||||||
from CTFd.utils.config.visibility import challenges_visible
|
from CTFd.utils.config.visibility import challenges_visible
|
||||||
from CTFd.utils.dates import ctf_ended, ctftime, view_after_ctf
|
from CTFd.utils.dates import ctf_ended, ctftime, view_after_ctf
|
||||||
from CTFd.utils.decorators import authed_only
|
from CTFd.utils.decorators import authed_only
|
||||||
@@ -348,7 +348,7 @@ def static_html(route):
|
|||||||
if page.auth_required and authed() is False:
|
if page.auth_required and authed() is False:
|
||||||
return redirect(url_for("auth.login", next=request.full_path))
|
return redirect(url_for("auth.login", next=request.full_path))
|
||||||
|
|
||||||
return render_template("page.html", content=page.content)
|
return render_template("page.html", content=page.html)
|
||||||
|
|
||||||
|
|
||||||
@views.route("/tos")
|
@views.route("/tos")
|
||||||
@@ -358,7 +358,7 @@ def tos():
|
|||||||
if tos_url:
|
if tos_url:
|
||||||
return redirect(tos_url)
|
return redirect(tos_url)
|
||||||
elif tos_text:
|
elif tos_text:
|
||||||
return render_template("page.html", content=build_html(tos_text))
|
return render_template("page.html", content=build_markdown(tos_text))
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ def privacy():
|
|||||||
if privacy_url:
|
if privacy_url:
|
||||||
return redirect(privacy_url)
|
return redirect(privacy_url)
|
||||||
elif privacy_text:
|
elif privacy_text:
|
||||||
return render_template("page.html", content=build_html(privacy_text))
|
return render_template("page.html", content=build_markdown(privacy_text))
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|||||||
28
migrations/versions/07dfbe5e1edc_add_format_to_pages.py
Normal file
28
migrations/versions/07dfbe5e1edc_add_format_to_pages.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Add format to Pages
|
||||||
|
|
||||||
|
Revision ID: 07dfbe5e1edc
|
||||||
|
Revises: 75e8ab9a0014
|
||||||
|
Create Date: 2021-06-15 19:57:37.410152
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "07dfbe5e1edc"
|
||||||
|
down_revision = "75e8ab9a0014"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("pages", sa.Column("format", sa.String(length=80), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("pages", "format")
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user