let tael = document.getElementsByTagName('textarea');
for (let i = 0; i < tael.length; i++) {
if (tael[i].className.indexOf('markdown_preview') >= 0) {
- attach_showdown_preview(tael[i].id, 1);
+ attach_markdown_preview(tael[i].id, 1);
}
}
$(document).ready(function(){
$('textarea.markdown-content').each(function(idx, e) {
- attach_showdown_preview(e.id, 0);
+ attach_markdown_preview(e.id, 0);
});
$('input.toggle-checkbox').each(function(idx, e) {
--- /dev/null
+// Functions to generate markdown previews for form fields
+
+function attach_markdown_preview(objid, admin) {
+ obj = document.getElementById(objid);
+
+ if (!obj) {
+ alert('Could not locate object ' + objid + ' in DOM');
+ return;
+ }
+
+ newdiv = document.createElement('div');
+ newdiv.className = 'markdownpreview col-lg-12';
+
+ if (admin) {
+ obj.style.cssFloat = 'left';
+ obj.style.marginRight = '10px';
+ obj.style.width = newdiv.style.width = "400px";
+ obj.style.height = newdiv.style.height = "200px";
+ newdiv.className = newdiv.className + ' adminmarkdownpreview';
+ }
+
+ obj.preview_div = newdiv;
+
+ obj.parentNode.insertBefore(newdiv, obj.nextSibling);
+
+ obj.infospan_html_base = admin ? '' : 'This field supports <a href="https://daringfireball.net/projects/markdown/basics" target="_blank" rel="noopener">markdown</a>. See below for a preview.';
+
+ obj.infospan = document.createElement('span');
+ obj.infospan.innerHTML = obj.infospan_html_base;
+ obj.parentNode.insertBefore(obj.infospan, newdiv);
+
+ /* First force one update to happen right away */
+ _do_update_markdown(obj);
+
+ obj.addEventListener('keyup', function(e) {
+ update_markdown(this);
+ });
+}
+
+var __update_queue = {};
+var __interval_setup = false;
+function update_markdown(obj) {
+ if (!__interval_setup) {
+ __interval_setup = true;
+
+ /* Global interval ticker running the update queue */
+ setInterval(function() {
+ /* This is where we actually update things */
+ for (var id in __update_queue) {
+ /* First remove it from the queue, so we can absorb another request while we run */
+ delete __update_queue[id];
+ obj = document.getElementById(id);
+
+ _do_update_markdown(obj);
+ }
+ }, 2000); /* Maximum update interval is 2 seconds */
+ }
+
+ if (obj.value == obj.preview_div.value)
+ return;
+
+ /* Just flag that this needs to be done, and the ticker will pick it up */
+ __update_queue[obj.id] = true;
+}
+
+
+function _do_update_markdown(obj) {
+ if (obj.value == '') {
+ /* Short-circuit the empty field case */
+ obj.infospan.innerHTML = '';
+ return;
+ }
+
+ fetch('/account/mdpreview/', {
+ method: 'POST',
+ body: obj.value,
+ headers: {
+ 'x-preview': 'md',
+ },
+ credentials: 'same-origin', /* for older browsers */
+ }).then(function(response) {
+ if (response.ok) {
+ return response.text().then(function(text) {
+ obj.preview_div.innerHTML = text;
+ });
+ } else {
+ console.warn('md preview failed');
+ }
+ });
+}
+++ /dev/null
-// Functions to generate showdown previews for
-// the django admin interface
-
-var converter = null;
-
-function attach_showdown_preview(objid, admin) {
- if (!converter) {
- converter = new Showdown.converter();
- }
- obj = document.getElementById(objid);
-
- if (!obj) {
- alert('Could not locate object ' + objid + ' in DOM');
- return;
- }
-
- newdiv = document.createElement('div');
- newdiv.className = 'markdownpreview col-lg-12';
-
- if (admin) {
- obj.style.cssFloat = 'left';
- obj.style.marginRight = '10px';
- obj.style.width = newdiv.style.width = "400px";
- obj.style.height = newdiv.style.height = "200px";
- newdiv.className = newdiv.className + ' adminmarkdownpreview';
- }
-
- obj.preview_div = newdiv;
-
- obj.parentNode.insertBefore(newdiv, obj.nextSibling);
-
- obj.infospan_html_base = admin ? '' : 'This field supports <a href="https://daringfireball.net/projects/markdown/basics" target="_blank" rel="noopener">markdown</a>. See below for a preview.';
-
- obj.infospan = document.createElement('span');
- obj.infospan.innerHTML = obj.infospan_html_base;
- obj.parentNode.insertBefore(obj.infospan, newdiv);
-
- update_markdown(obj, newdiv);
-
- window.onkeyup = function() {
- /* Using a timer make sure we only update max 4 times / second */
- if (obj.current_timeout) {
- clearTimeout(obj.current_timeout);
- }
- obj.current_timeout = setTimeout(function() {
- e = document.getElementsByTagName('textarea');
- for (i= 0; i < e.length; i++) {
- if (e[i].preview_div) {
- update_markdown(e[i], e[i].preview_div);
- }
- }
- }, 250);
- };
-}
-
-/*
- * Use regexp to do trivial HTML cleaning. The actual cleaning will happen
- * serverside later, so it doesn't matter that the regexps are far from
- * perfect - it should just be enough to alert the user that he/she is
- * using invalid markup.
- */
-var _update_markdown_reopen = new RegExp("<([^\s/][^>]*)>", "g");
-var _update_markdown_reclose = new RegExp("</([^>]+)>", "g");
-function update_markdown(src, dest) {
- if (src.value != src.lastvalue) {
- src.lastvalue = src.value;
- if (_update_markdown_reclose.test(src.value) || _update_markdown_reopen.test(src.value)) {
- dest.innerHTML = converter.makeHtml(src.value.replace(_update_markdown_reopen, '[HTML REMOVED]').replace(_update_markdown_reclose,'[HTML REMOVED2]'));
- if (!src.last_had_html) {
- src.last_had_html = true;
- src.infospan.innerHTML = src.infospan_html_base + '<br/><span style="color: red;">You seem to be using HTML in your input - this will be filtered. Please use markdown instead!</span>';
- }
- }
- else {
- dest.innerHTML = converter.makeHtml(src.value);
- if (src.last_had_html) {
- src.last_had_html = false;
- src.infospan.innerHTML = src.infospan_html_base;
- }
- }
- }
-}
url(r'^(?P<objtype>news|events|products|organisations|services)/(?P<item>\d+|new)/$', pgweb.account.views.submitted_item_form),
url(r'^organisations/confirm/([0-9a-f]+)/$', pgweb.account.views.confirm_org_email),
+ # Markdown preview (silly to have in /account/, but that's where all the markdown forms are so meh)
+ url(r'^mdpreview/', pgweb.account.views.markdown_preview),
+
# Organisation information
url(r'^orglist/$', pgweb.account.views.orglist),
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from pgweb.util.decorators import login_required, script_sources, frame_sources, content_sources
+from django.views.decorators.csrf import csrf_exempt
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import default_token_generator
from pgweb.util.misc import send_template_mail, generate_random_token, get_client_ip
from pgweb.util.helpers import HttpSimpleResponse, simple_form
from pgweb.util.moderation import ModerationState
+from pgweb.util.markup import pgmarkdown
from pgweb.news.models import NewsArticle
from pgweb.events.models import Event
return _submitted_item_withdraw(request, objtype, model, obj)
+@login_required
+@csrf_exempt
+def markdown_preview(request):
+ if request.method != 'POST':
+ return HttpResponse("POST only please", status=405)
+
+ if request.headers.get('x-preview', None) != 'md':
+ raise Http404()
+
+ return HttpResponse(pgmarkdown(request.body.decode('utf8', 'ignore')))
+
+
def login(request):
return authviews.LoginView.as_view(template_name='account/login.html',
authentication_form=PgwebAuthenticationForm,
--- /dev/null
+# Filter wrapping the python markdown library into a django template filter
+from django import template
+from django.utils.encoding import force_text
+from django.utils.safestring import mark_safe
+
+from pgweb.util.markup import pgmarkdown
+
+register = template.Library()
+
+
+@register.filter(is_safe=True)
+def markdown(value, args=''):
+ allow_images = False
+ allow_relative_links = False
+
+ if args:
+ for a in args.split(','):
+ if a == 'allowimages':
+ allow_images = True
+ elif a == 'allowrelativelinks':
+ allow_relative_links = True
+ else:
+ raise ValueError("Invalid argument to markdown: {}".format(a))
+
+ return mark_safe(pgmarkdown(
+ force_text(value),
+ allow_images=allow_images,
+ allow_relative_links=allow_relative_links,
+ ))
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.admin',
- 'django_markwhat',
'django.contrib.staticfiles',
'pgweb.core.apps.CoreAppConfig',
'pgweb.mailqueue',
from django.shortcuts import render, get_object_or_404
from django.core.exceptions import PermissionDenied
-from django.core.validators import ValidationError
from django.http import HttpResponseRedirect, Http404
from django.template.loader import get_template
import django.utils.xmlutils
from pgweb.util.moderation import ModerationState
import io
-import re
import difflib
-import markdown
from pgweb.mailqueue.util import send_simple_mail
-
-
-_re_img = re.compile('<img ', re.I)
-_re_html_open = re.compile(r'<([^\s/][^>]*)>')
+from pgweb.util.markup import pgmarkdown
def MarkdownValidator(val):
- if _re_html_open.search(val):
- raise ValidationError('Embedding HTML in markdown is not allowed')
-
- out = markdown.markdown(val)
-
- # We find images with a regexp, because it works... For now, nothing more advanced
- # is needed.
- if _re_img.search(out):
- raise ValidationError('Image references are not allowed in this field')
-
- return val
+ return pgmarkdown(val)
def simple_form(instancetype, itemid, request, formclass, formtemplate='base/form.html', redirect='/account/', navsection='account', fixedfields=None, createifempty=False, extracontext={}):
--- /dev/null
+import markdown
+from bleach.sanitizer import Cleaner
+from bleach.html5lib_shim import Filter
+
+
+# Tags and attributes generated by markdown (anything that's not
+# generated by markdown is clearly manually added html)
+# This list is from the bleach-allowlist module, but adding a dependency
+# on it just to get two arrays seems silly.
+
+_markdown_tags = [
+ "h1", "h2", "h3", "h4", "h5", "h6",
+ "b", "i", "strong", "em", "tt",
+ "p", "br",
+ "span", "div", "blockquote", "code", "pre", "hr",
+ "ul", "ol", "li", "dd", "dt",
+ # "img", # img is optional in our markdown validation
+ "a",
+ "sub", "sup",
+]
+
+_markdown_attrs = {
+ "*": ["id"],
+ "img": ["src", "alt", "title"],
+ "a": ["href", "alt", "title"],
+}
+
+
+# Prevent relative links, by simply removing any href tag that does not have
+# a : in it.
+class RelativeLinkFilter(Filter):
+ def __iter__(self):
+ for token in Filter.__iter__(self):
+ if token['type'] in ['StartTag', 'EmptyTag'] and token['data']:
+ if (None, 'href') in token['data']:
+ # This means a href attribute with no namespace
+ if ':' not in token['data'][(None, 'href')]:
+ # Relative link!
+ del token['data'][(None, 'href')]
+ yield token
+
+
+def pgmarkdown(value, allow_images=False, allow_relative_links=False):
+ tags = _markdown_tags
+ filters = []
+
+ if allow_images:
+ tags.append('img')
+ if not allow_relative_links:
+ filters.append(RelativeLinkFilter)
+
+ cleaner = Cleaner(tags=tags, attributes=_markdown_attrs, filters=filters)
+
+ return cleaner.clean(markdown.markdown(value))
import datetime
-import markdown
+from pgweb.util.markup import pgmarkdown
class ModerateModel(models.Model):
if k in getattr(self, 'rendered_preview_fields', []):
yield self.render_preview_field(k, val)
elif k in getattr(self, 'markdown_fields', []):
- yield markdown.markdown(val)
+ yield pgmarkdown(val)
else:
yield None
Django>=2.2,<2.3
-django-markdown==0.8.4
psycopg2==2.8.5
pycryptodomex>=3.4.7,<3.5
-django_markwhat==1.6.2
+Markdown==3.0.1
requests-oauthlib==1.0.0
cvss==2.1
pytidylib==0.3.2
pycodestyle==2.4.0
pynliner==0.8.0
Babel==2.6.0
+bleach==3.1.4
{% block extrahead %}
{{ block.super }}
-<link rel="stylesheet" type="text/css" href="/media/css/showdown_preview.css?{{gitrev}}" />
-<script type="text/javascript" src="/media/showdown/showdown.js?{{gitrev}}"></script>
-<script type="text/javascript" src="/media/js/showdown_preview.js?{{gitrev}}"></script>
+<link rel="stylesheet" type="text/css" href="/media/css/markdown_preview.css?{{gitrev}}" />
+<script type="text/javascript" src="/media/js/markdown_preview.js?{{gitrev}}"></script>
<script type="text/javascript" src="/media/js/admin_pgweb.js?{{gitrev}}"></script>
{%endblock%}
{% block extrahead %}
{{ block.super }}
-<link rel="stylesheet" type="text/css" href="/media/css/showdown_preview.css?{{gitrev}}" />
+<link rel="stylesheet" type="text/css" href="/media/css/markdown_preview.css?{{gitrev}}" />
{%endblock%}
{%block extrascript%}
-<script type="text/javascript" src="/media/showdown/showdown.js?{{gitrev}}"></script>
-<script type="text/javascript" src="/media/js/showdown_preview.js?{{gitrev}}"></script>
+<script type="text/javascript" src="/media/js/markdown_preview.js?{{gitrev}}"></script>
<script type="text/javascript" src="/media/js/forms.js?{{gitrev}}"></script>
{%if recaptcha%}
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>
{%extends "base/page.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}Software Catalogue - {{category.catname}}{%endblock%}
{%block contents%}
</thead>
<tbody>
<tr>
- <td>{{product.description|markdown:"safe"}}</td>
+ <td>{{product.description|markdown}}</td>
<td>{{product.licencetype}}</td>
<td>{{product.price}}</td>
<td><a href="{{product.org.url}}" target="_blank" rel="noopener">{{product.org.name}}</a></td>
{%extends "base/page.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}{{title}}{%endblock%}
{%block contents%}
<div>Location: <strong>{{event.locationstring}}</strong></div>
{%if event.language%}<div>Language: <strong>{{event.language}}</strong></div>{%endif%}
<div class="newseventwrap">
-{{event.summary|markdown:"safe"|striptags}}
+{{event.summary|markdown|striptags}}
</div>
{%endfor%}
{%extends "base/page.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}{{obj.title}}{%endblock%}
{%block contents%}
<h1>{{obj.title}}</h1>
<div class="eventdate">Date: <strong>{{obj.displaydate|safe}}</strong></div>
<div>Location: {{obj.locationstring}}</div>
{%if obj.language%}<div>Language: {{obj.language}}</div>{%endif%}
-{{obj.details|markdown:"safe"}}
+{{obj.details|markdown}}
{%if obj.has_organisation%}
<p>Posted by {{obj.org}}{%if obj.org.email%} ({{obj.org.email}}){%endif%}.</p>
{%else%}
-{%load markup%}
-{{obj.summary|markdown:"safe"}}
+{%load pgmarkdown%}
+{{obj.summary|markdown}}
{%extends "base/page.html"%}
-{% load markup %}
+{% load pgmarkdown %}
{%block title%}Feature Description{%endblock%}
{%block contents%}
<h1>Feature Description</h1>
{% if feature.featuredescription_is_url %}
For more information, please visit : <a href="{{ feature.featurelink }}">{{ feature.featurelink }}</a>
{% else %}
- {{ feature.featuredescription|markdown:"safe" }}
+ {{ feature.featuredescription|markdown }}
{% endif %}
</p>
{%endblock%}
{%extends "base/page.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}{{obj.title}}{%endblock%}
{%block contents%}
<h1>{{obj.title}}</h1>
{%endfor%}
</div>
-{{obj.content|markdown:"safe"}}
+{{obj.content|markdown}}
{%if obj.is_migrated%}
<p><em>This post has been migrated from a previous version of the PostgreSQL
website. We apologise for any formatting issues caused by the migration.</em></p>
{%extends "news/mail/base.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}{{news.title}}{%endblock%}
{%block content%}
{%extends "news/mail/base.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}{{news.title}}{%endblock%}
{%block content%}
{%extends "base/page.html"%}
-{%load markup%}
+{%load pgmarkdown%}
{%block title%}News Archive{%if tag%} - {{tag.name}}{%endif%}{%endblock%}
{%block contents%}
<h1><a href="/about/newsarchive/">News Archive</a>{%if tag%} - {{tag.name}}{%endif%} <i class="far fa-newspaper"></i></h1>
<div class="newsdate">Posted on <strong>{{obj.displaydate}}</strong>{% if obj.org.name != '_migrated' %} by {{ obj.org.name }}{% endif %}
{%for t in obj.tags.all%}<span class="badge badge-pill badge-secondary"><i class="fa fa-tag"></i> {{t}}</span> {%endfor%}
</div>
-{{obj.content|markdown:"safe"|striptags|truncatewords:20}}
+{{obj.content|markdown|striptags|truncatewords:20}}
<p><a href="/about/news/{{obj.title|slugify}}-{{obj.id}}/">Read more...</a></p>
{%endfor%}
-{%load markup%}
-{{obj.content|markdown:"safe"}}
+{%load pgmarkdown%}
+{{obj.content|markdown}}