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:
Kevin Chung
2021-06-17 10:33:01 -04:00
committed by GitHub
parent b875738b13
commit 614f086540
7 changed files with 126 additions and 29 deletions

View File

@@ -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>")

View File

@@ -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):

View File

@@ -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}

View File

@@ -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>
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
</div>
</div>
</div>
</div>
<div class="form-group float-right"> <div class="form-group float-right pt-3">
{{ form.nonce() }} {{ form.nonce() }}
<button class="btn btn-primary" id="save-page"> <button class="btn btn-primary" id="save-page">
Save Save
</button> </button>
</div> </div>
</div>
<div role="tabpanel" class="tab-pane content" id="content-preview">
</div>
</div>
</div>
</div>
</form> </form>
{% endwith %} {% endwith %}
</div> </div>

View File

@@ -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

View File

@@ -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)

View 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 ###