import os import json from flask import Flask, request, render_template, redirect, url_for from urllib.request import urlopen from urllib.parse import urlencode import html import re # To sanitize Flask input fields from markupsafe import Markup, escape # To sanitize Markdown input import pypandoc # import bleach # To read the Markdown metadat import markdown from filter_registry import pad_content_filters import pad_filters APP = Flask(__name__) APP.config.from_pyfile('settings.py') # --- # My attempt as showing helpful error messages. # the class holds an error string to show in the error.html tempalte # for now mostly for when the space in the yaml meta is missing. class MarkdownRenderError(RuntimeError): def __init__(self, kind, message, original): super().__init__(message) self.kind = kind self.original = original def get_pad_content(pad_name, ext=""): if ext: pad_name = f'{ pad_name }{ ext }' # print(pad_name) arguments = { 'padID' : pad_name, 'apikey' : APP.config['PAD_API_KEY'] } api_call = 'getText' response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())) # create pad in case it does not yet exist if response['code'] == 1 and 'padID does not exist' == response['message']: api_call = 'createPad' urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()) api_call = 'getText' response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())) content = response['data']['text'] # print("before: " + content) # print( "GET PAD CONTENT" ) for f in pad_content_filters: content = f(content) # print("after: " + content) return content def all_pads(): arguments = { 'apikey' : APP.config['PAD_API_KEY'], } api_call = 'listAllPads' response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())) return response # get all pads that end in .md so we can show them on the frontpage def all_publications(): pads = all_pads() pubs = [] if(pads and pads['data'] and pads['data']['padIDs']): for pad in pads['data']['padIDs']: if pad.endswith('.md'): name = pad.removesuffix('.md') pubs.append(name) return pubs else: return [] def create_pad_on_first_run(name, ext): pads = all_pads() pad = name+ext if pad not in pads['data']['padIDs']: # Select default template if 'md' in ext: default_template = 'templates/default.md' elif 'css' in ext: default_template = 'templates/default.css' default_template = open(default_template).read() # Create pad and add the default template arguments = { 'padID' : pad, 'apikey' : APP.config['PAD_API_KEY'], 'text' : default_template } api_call = 'createPad' json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())) def md_to_html(md_pad_content): # Convert Markdown to HTML # html = markdown.markdown(md_pad_content, extensions=['meta', 'attr_list']) # attr_list does not work # missing a sapce in the metadata block causes a crash, # try to show a helpful error message... # Maybe, we could fix the yaml instead? try: html = pypandoc.convert_text(md_pad_content, 'html', format='md') except RuntimeError as exc: message = str(exc) if "YAML" in message or "YAML metadata" in message: raise MarkdownRenderError( kind="yaml", message="Invalid YAML metadata. Use `key: value` and add a blank line.", original=str(exc) ) from exc raise MarkdownRenderError( kind="markdown", message = "Markdown conversion failed. Please check the markdown for errors.", original = str(exc) ) from exc # Sanitize the Markdown # html = bleach.clean(html) # Another built-in Flask way to sanitize # html = escape(html) html = Markup(html) return html def get_md_metadata(md_pad_content): # Read the metadata from the Markdown md = markdown.Markdown(extensions=['meta']) md.convert(md_pad_content) metadata = md.Meta return metadata def get_app_root(): if APP.config['APPLICATION_ROOT'] == '/': app_root = '' elif APP.config['APPLICATION_ROOT'].endswith('/'): app_root = APP.config['APPLICATION_ROOT'][:-1] else: app_root = APP.config['APPLICATION_ROOT'] return app_root def get_meta(metadata, key, default=None): return metadata.get(key, [default])[0] def render_markdown_error(name, error): print("ERROR", error) return render_template( 'error.html', name=name.strip(), error_text=str(error), error=error, lang="en", title="Markdown error" ), 400 def clean_string(input_string): input_string = input_string.lower() snake_case_string = re.sub(r'[\s!]+', '_', input_string) return snake_case_string.strip('_') @APP.template_filter('prettify') # use it in a template with | prettify def prettify_string(input_string): space_string = input_string.replace("_", " ") return space_string.title() # --- @APP.route('/', methods=['GET', 'POST']) def index(): name = False if request.values.get('name'): name = escape(request.values.get('name')) # Returns a Markup() object, which is "None" when False if name: # This is when the environment is "created" # The pads are filled with the default templates (pad, stylesheet, template) exts = ['.md', '.css'] # clean up the name so we dont get x number of different publications when people mistype name = clean_string(name) for ext in exts: create_pad_on_first_run(name, ext) return redirect(url_for("pad", name=name)) else: pubs = all_publications() return render_template('start.html', pubs = pubs, home_pad_url=APP.config['HOME_PAD_URL']) @APP.route('//') def main(name): return redirect(url_for("pad", name=name)) @APP.route('//pad/') def pad(name): url = f"{ APP.config['PAD_URL'] }/{ name }.md" return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) @APP.route('//stylesheet/') def stylesheet(name): url = f"{ APP.config['PAD_URL'] }/{ name }.css" return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) @APP.route('//html/') def html(name): app_root = get_app_root() url = f"{ app_root }/{ name }/preview.html" return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) @APP.route('//pdf/') def pdf(name): app_root = get_app_root() pagedjs_base = f"{ app_root }/{name}/pagedjs.html" impose = request.args.get("impose") == "1" single_page = request.args.get("single") == "1" params = {} if impose: params["impose"] = "1" if single_page: params["single"] = "1" query = f"?{ urlencode(params) }" if params else "" url = f"{ pagedjs_base }{ query }" return render_template('pdf.html', url=url, pagedjs_base=pagedjs_base, name=name.strip(), pad_url=APP.config['PAD_URL']) # @APP.route('//impose/') # def impose(name): # app_root = get_app_root() # url = f"{ app_root }/{name}/imposed.html" # return render_template('pdf.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) # ////////////////// # RENDERED RESOURCES # ////////////////// # (These are not saved as a file on the server) @APP.route('//stylesheet.css') def css(name): css = get_pad_content(name, '.css') # Insert CSS sanitizer here. return css, 200, {'Content-Type': 'text/css; charset=utf-8'} @APP.route('//preview.html') def preview(name): # TO GENERATE THE PREVIEW WEBPAGE md_pad_content = get_pad_content(name, ext='.md') try: html = md_to_html(md_pad_content) except MarkdownRenderError as exc: return render_markdown_error(name, exc) metadata = get_md_metadata(md_pad_content) lang = get_meta(metadata, 'language', 'en') title = get_meta(metadata, 'title', 'Untitled') return render_template('preview.html', name=name.strip(), pad_content=html, lang=lang, title=title) @APP.route('//pagedjs.html') def pagedjs(name): # TO GENERATE THE PAGED.JS WEBPAGE md_pad_content = get_pad_content(name, ext='.md') try: html = md_to_html(md_pad_content) except MarkdownRenderError as exc: return render_markdown_error(name, exc) metadata = get_md_metadata(md_pad_content) lang = get_meta(metadata, 'language', 'en') title = get_meta(metadata, 'title', 'Untitled') cover = get_meta(metadata, 'cover', None) impose = request.args.get("impose") == "1" single_page = request.args.get("single") == "1" return render_template( 'pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, cover=cover, impose=impose, single_page=single_page ) # @APP.route('//imposed.html') # def imposed(name): # # TO GENERATE THE IMPOSED WEBPAGE # md_pad_content = get_pad_content(name, ext='.md') # try: # html = md_to_html(md_pad_content) # except MarkdownRenderError as exc: # return render_markdown_error(name, exc) # metadata = get_md_metadata(md_pad_content) # lang = get_meta(metadata, 'language', 'en') # title = get_meta(metadata, 'title', 'Untitled') # cover = get_meta(metadata, 'cover', None) # impose = True #request.args.get("impose") == "true" # return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, cover=cover, impose=impose) # ////////////////// if __name__ == '__main__': APP.debug = True APP.env = "development" # APP.debug = False # APP.env = "production" APP.run(host="0.0.0.0", port=APP.config["PORTNUMBER"], threaded=True)