diff --git a/CTFd/admin/pages.py b/CTFd/admin/pages.py index 777e87ea..9dc3d4d3 100644 --- a/CTFd/admin/pages.py +++ b/CTFd/admin/pages.py @@ -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/") diff --git a/CTFd/forms/pages.py b/CTFd/forms/pages.py index c9ba76ba..3069b593 100644 --- a/CTFd/forms/pages.py +++ b/CTFd/forms/pages.py @@ -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): diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index 6da84c59..6b241fcb 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -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} diff --git a/CTFd/themes/admin/templates/editor.html b/CTFd/themes/admin/templates/editor.html index 423c346e..462cfe59 100644 --- a/CTFd/themes/admin/templates/editor.html +++ b/CTFd/themes/admin/templates/editor.html @@ -1,6 +1,11 @@ {% extends "admin/base.html" %} {% block stylesheets %} + {% endblock %} {% block content %} @@ -19,7 +24,8 @@ {% 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) %}
@@ -43,6 +49,16 @@
+
+
+ {{ form.format.label }} + {{ form.format(class="form-control custom-select", placeholder="Route", value=format) }} + + {{ form.format.description }} + +
+
+
@@ -60,7 +76,7 @@
-
+

@@ -96,23 +112,22 @@
-
+
{{ form.content(id="admin-pages-editor", class="d-none") }}
+ +
+ {{ form.nonce() }} + +
-
+
- -
- {{ form.nonce() }} - -
- {% endwith %}
diff --git a/CTFd/utils/config/pages.py b/CTFd/utils/config/pages.py index 751261c3..40c505e0 100644 --- a/CTFd/utils/config/pages.py +++ b/CTFd/utils/config/pages.py @@ -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 diff --git a/CTFd/views.py b/CTFd/views.py index 7edfdd8f..8882396e 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -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) diff --git a/migrations/versions/07dfbe5e1edc_add_format_to_pages.py b/migrations/versions/07dfbe5e1edc_add_format_to_pages.py new file mode 100644 index 00000000..bedd74c4 --- /dev/null +++ b/migrations/versions/07dfbe5e1edc_add_format_to_pages.py @@ -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 ###