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.schemas.pages import PageSchema
from CTFd.utils import markdown
from CTFd.utils.config.pages import build_html
from CTFd.utils.decorators import admins_only
@@ -29,7 +28,7 @@ def pages_preview():
data = {"content": request.form.get("content")}
schema = PageSchema()
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>")

View File

@@ -2,6 +2,7 @@ from wtforms import (
BooleanField,
HiddenField,
MultipleFileField,
SelectField,
StringField,
TextAreaField,
)
@@ -22,6 +23,13 @@ class PageEditForm(BaseForm):
hidden = BooleanField("Hidden")
auth_required = BooleanField("Authentication Required")
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):

View File

@@ -39,10 +39,10 @@ class Notifications(db.Model):
@property
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
return markup(build_html(self.content))
return markup(build_markdown(self.content))
def __init__(self, *args, **kwargs):
super(Notifications, self).__init__(**kwargs)
@@ -57,10 +57,22 @@ class Pages(db.Model):
draft = db.Column(db.Boolean)
hidden = db.Column(db.Boolean)
auth_required = db.Column(db.Boolean)
format = db.Column(db.String(80), default="markdown")
# TODO: Use hidden attribute
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):
super(Pages, self).__init__(**kwargs)
@@ -105,10 +117,10 @@ class Challenges(db.Model):
@property
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
return markup(build_html(self.description))
return markup(build_markdown(self.description))
def __init__(self, *args, **kwargs):
super(Challenges, self).__init__(**kwargs)
@@ -144,10 +156,10 @@ class Hints(db.Model):
@property
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
return markup(build_html(self.content))
return markup(build_markdown(self.content))
def __init__(self, *args, **kwargs):
super(Hints, self).__init__(**kwargs)
@@ -848,10 +860,10 @@ class Comments(db.Model):
@property
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
return markup(build_html(self.content, sanitize=True))
return markup(build_markdown(self.content, sanitize=True))
__mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type}

View File

@@ -1,6 +1,11 @@
{% extends "admin/base.html" %}
{% block stylesheets %}
<style>
.CodeMirror {
height: 100%;
}
</style>
{% endblock %}
{% block content %}
@@ -19,7 +24,8 @@
</div>
{% 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">
<div class="form-group">
<div class="col-md-12">
@@ -43,6 +49,16 @@
</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="col-md-12">
@@ -60,7 +76,7 @@
</ul>
<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>
<div class="form-inline">
@@ -96,23 +112,22 @@
<br>
<div class="form-group">
<div class="form-group h-100">
{{ form.content(id="admin-pages-editor", class="d-none") }}
</div>
<div class="form-group float-right pt-3">
{{ form.nonce() }}
<button class="btn btn-primary" id="save-page">
Save
</button>
</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 class="form-group float-right">
{{ form.nonce() }}
<button class="btn btn-primary" id="save-page">
Save
</button>
</div>
</form>
{% endwith %}
</div>

View File

@@ -2,12 +2,48 @@ from flask import current_app
from CTFd.cache import cache
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
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):
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:
html = sanitize_html(html)
return html
@@ -31,6 +67,5 @@ def get_page(route):
if page:
# Convert the row into a transient ORM object so this change isn't commited accidentally
p = Pages(**page)
p.content = build_html(p.content)
return p
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 validators
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.dates import ctf_ended, ctftime, view_after_ctf
from CTFd.utils.decorators import authed_only
@@ -348,7 +348,7 @@ def static_html(route):
if page.auth_required and authed() is False:
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")
@@ -358,7 +358,7 @@ def tos():
if tos_url:
return redirect(tos_url)
elif tos_text:
return render_template("page.html", content=build_html(tos_text))
return render_template("page.html", content=build_markdown(tos_text))
else:
abort(404)
@@ -370,7 +370,7 @@ def privacy():
if privacy_url:
return redirect(privacy_url)
elif privacy_text:
return render_template("page.html", content=build_html(privacy_text))
return render_template("page.html", content=build_markdown(privacy_text))
else:
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 ###