diff --git a/README.md b/README.md index 6a4dfb3..6ee2617 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ A simple shopping list app: Note! Register page is enabled by default, but there is no link to it anywhere. * Disable registering with env variable: `ENABLE_REGISTER=false` +* MARKDOWN_STYLE=dokuwiki or markdown + * markdown syntax, unticked = `[ ]`, ticked = `[x]` + * dokuwiki syntax (todo plugin), unticked = `item`, ticked = `item` +* SECRET_KEY=somerandomstring +* SESSION_COOKIE_NAME=name for cookies + ## Running diff --git a/code/revprox.py b/code/revprox.py index 9649e61..3914560 100644 --- a/code/revprox.py +++ b/code/revprox.py @@ -1,7 +1,7 @@ class ReverseProxied(object): - '''Wrap the application in this middleware and configure the - front-end server to add these headers, to let you quietly bind - this to a URL other than / and to an HTTP scheme that is + """Wrap the application in this middleware and configure the + front-end server to add these headers, to let you quietly bind + this to a URL other than / and to an HTTP scheme that is different than what is used locally. In nginx: @@ -14,19 +14,20 @@ class ReverseProxied(object): } :param app: the WSGI application - ''' + """ + def __init__(self, app): self.app = app def __call__(self, environ, start_response): - script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + script_name = environ.get("HTTP_X_SCRIPT_NAME", "") if script_name: - environ['SCRIPT_NAME'] = script_name - path_info = environ['PATH_INFO'] + environ["SCRIPT_NAME"] = script_name + path_info = environ["PATH_INFO"] if path_info.startswith(script_name): - environ['PATH_INFO'] = path_info[len(script_name):] + environ["PATH_INFO"] = path_info[len(script_name) :] - scheme = environ.get('HTTP_X_SCHEME', '') + scheme = environ.get("HTTP_X_SCHEME", "") if scheme: - environ['wsgi.url_scheme'] = scheme + environ["wsgi.url_scheme"] = scheme return self.app(environ, start_response) diff --git a/code/shop.py b/code/shop.py index 58b000b..42a18fa 100644 --- a/code/shop.py +++ b/code/shop.py @@ -1,17 +1,23 @@ # -*- coding: utf-8 -*- # all the imports -import sqlite3, time, datetime, hashlib, os, re +import datetime +import hashlib +import os +import re +import sqlite3 +import time from shutil import copyfile, move + from flask import ( Flask, - request, - session, + abort, + flash, g, redirect, - url_for, - abort, render_template, - flash, + request, + session, + url_for, ) from revprox import ReverseProxied @@ -22,17 +28,20 @@ DEBUG = False SECRET_KEY = os.getenv("SECRET_KEY", "development key") USERNAME = os.getenv("ADMIN_USER", "admin") PASSWORD = os.getenv("ADMIN_PASSWD", "default") +MARKDOWN_STYLE = os.getenv("MARKDOWN_STYLE", "markdown") URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)") URLPARSER = re.compile(r"(\[)([^\]]+)(\])\(([^\)]+)\)") BOLDFINDER = re.compile(r"\*([^\*]+)\*") CODEFINDER = re.compile(r"\`([^\`]+)\`") +CHECKBOX_DOKUWIKI_TICKED = re.compile(r"]*)>([^<]+)") +CHECKBOX_DOKUWIKI_UNTICKED = re.compile(r"([^<]+)") +SHOPNAME_INVALIDCHARS = re.compile("[^A-Za-z0-9_-]+") # create our little application :) app = Flask(__name__) app.config.from_object(__name__) app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "mdshop") app.config["register"] = os.getenv("ENABLE_REGISTER", "true") != "false" -print(app.config) app.wsgi_app = ReverseProxied(app.wsgi_app) @@ -82,9 +91,7 @@ def get_shop_date(id): data_dir = os.path.join(DATADIR, get_username(row[2])) data_file = os.path.join(data_dir, row[1] + ".md") if os.path.exists(data_file): - date = datetime.datetime.fromtimestamp( - os.path.getmtime(data_file) - ).strftime("%m/%d %H:%M") + date = datetime.datetime.fromtimestamp(os.path.getmtime(data_file)).strftime("%m/%d %H:%M") return date @@ -96,9 +103,7 @@ def get_shop_backup_date(id): data_dir = os.path.join(DATADIR, get_username(row[2])) data_file = os.path.join(data_dir, row[1] + ".md.bkp") if os.path.exists(data_file): - date = datetime.datetime.fromtimestamp( - os.path.getmtime(data_file) - ).strftime("%m/%d %H:%M") + date = datetime.datetime.fromtimestamp(os.path.getmtime(data_file)).strftime("%m/%d %H:%M") return date @@ -127,6 +132,8 @@ def markdown_parse(s): s = s.decode("utf8") s = BOLDFINDER.sub(r'*\1*', s) s = CODEFINDER.sub(r'`\1`', s) + s = CHECKBOX_DOKUWIKI_TICKED.sub(r"\2 (\1)", s) + s = CHECKBOX_DOKUWIKI_UNTICKED.sub(r"\1", s) return s @@ -136,6 +143,37 @@ def urlify(s): return URLFINDER.sub(r'\1', s) +def checkbox_add(item): + if MARKDOWN_STYLE == "markdown": + return f"[ ] {item}" + "\n" + if MARKDOWN_STYLE == "dokuwiki": + return f"{item}" + "\n" + + +def untick(item): + if MARKDOWN_STYLE == "markdown": + return item.replace("[x]", "[ ]") + if MARKDOWN_STYLE == "dokuwiki": + return CHECKBOX_DOKUWIKI_TICKED.sub(r"\2", item) + return item + + +def tick(item, user=""): + if MARKDOWN_STYLE == "markdown": + return item.replace("[ ]", "[x]") + if MARKDOWN_STYLE == "dokuwiki": + return CHECKBOX_DOKUWIKI_UNTICKED.sub(r"\1".format(user), item) + return item + + +def is_ticked(item): + return "[x]" in item or "" in item + + @app.before_request def before_request(): g.db = connect_db() @@ -180,11 +218,11 @@ def show_shop(shopid): continue icon = " " extra_class = "noitem" - if "[ ]" in row: - icon = u" " + if is_unticked(row): + icon = " " extra_class = "" - if "[x]" in row: - icon = u"\u2714" + if is_ticked(row): + icon = "\u2714" extra_class = "" row = urlify(row).encode("ascii", "xmlcharrefreplace") row = markdown_parse(row) @@ -227,17 +265,13 @@ def list_shops(): for row in cur.fetchall(): if session.get("user") == row[2]: # owner date = get_shop_date(row[0]) - entries.append( - dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date) - ) + entries.append(dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date)) cur = g.db.execute("select * from shops order by shop") shares = get_shares(session.get("user")) for row in cur.fetchall(): if row[0] in shares: # Has been shared to date = get_shop_date(row[0]) - entries.append( - dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date) - ) + entries.append(dict(shop=row[1], shopid=row[0], owner=get_username(row[2]), date=date)) return render_template("list_shops.html", entries=entries) @@ -247,12 +281,8 @@ def add_items(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") @@ -262,7 +292,7 @@ def add_items(): if row.strip() == "": continue count += 1 - contents_file.write("[ ] %s\n" % row.strip()) + contents_file.write(checkbox_add(row.strip())) contents_file.close() flash("Added %d items" % (count)) return redirect(url_for("show_shop", shopid=shopid)) @@ -273,12 +303,8 @@ def edit_md(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") @@ -300,12 +326,8 @@ def restore_md(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") @@ -326,13 +348,10 @@ def toggle_item(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) + user = get_username(session.get("user")) req_row = None for key in request.form: if key.startswith("item"): @@ -355,10 +374,10 @@ def toggle_item(): for i, row in enumerate(contents): if i == req_row or req_row < 0: if req_row != -2: # no ticking if unticking all - if "[ ]" in row: - contents[i] = row.replace("[ ]", "[x]") - if "[x]" in row: - contents[i] = row.replace("[x]", "[ ]") + if is_unticked(row): + contents[i] = tick(row, user) + if is_ticked(row): + contents[i] = untick(row) if row != contents[i]: changed = True if changed: @@ -375,12 +394,8 @@ def remove_toggled(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") @@ -389,7 +404,7 @@ def remove_toggled(): contents = [] changed = False for i, row in enumerate(contents_file.read().split("\n")): - if "[x]" not in row: + if not is_ticked(row): contents.append(row) else: changed = True @@ -407,10 +422,11 @@ def remove_toggled(): def add_shop(): if not session.get("logged_in"): abort(401) - import re, string + import re + import string + + shopname = SHOPNAME_INVALIDCHARS.sub("", request.form["shop"]) - pattern = re.compile("[\W]+") - shopname = pattern.sub("", request.form["shop"]) if shopname == "": flash("Shop name empty!") return redirect(url_for("list_shops")) @@ -419,9 +435,7 @@ def add_shop(): if shopname == row[1]: flash("Shop already exists! " + shopname) return redirect(url_for("list_shops")) - g.db.execute( - "insert into shops (shop,owner) values (?, ?)", [shopname, session["user"]] - ) + g.db.execute("insert into shops (shop,owner) values (?, ?)", [shopname, session["user"]]) g.db.commit() new_dir = os.path.join(DATADIR, get_username(session["user"])) new_file = os.path.join(new_dir, shopname + ".md") @@ -436,11 +450,11 @@ def add_shop(): def add_share(): if not session.get("logged_in"): abort(401) - import re, string + import re + import string - pattern = re.compile("[\W]+") - username = pattern.sub("", request.form["share"]) - shopid = pattern.sub("", request.form["shopid"]) + username = SHOPNAME_INVALIDCHARS.sub("", request.form["share"]) + shopid = SHOPNAME_INVALIDCHARS.sub("", request.form["shopid"]) if username == "": flash("User name empty!") return redirect(url_for("show_shop", shopid=shopid)) @@ -448,9 +462,7 @@ def add_share(): if userid == None: flash("No such user!") return redirect(url_for("show_shop", shopid=shopid)) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] if session.get("user") != ownerid: flash("Not your shop!") return redirect(url_for("show_shop", shopid=shopid)) @@ -464,11 +476,11 @@ def add_share(): def remove_share(): if not session.get("logged_in"): abort(401) - import re, string + import re + import string - pattern = re.compile("[\W]+") - username = pattern.sub("", request.form["user"]) - shopid = pattern.sub("", request.form["shopid"]) + username = SHOPNAME_INVALIDCHARS.sub("", request.form["user"]) + shopid = SHOPNAME_INVALIDCHARS.sub("", request.form["shopid"]) if username == "": flash("User name empty!") return redirect(url_for("show_shop", shopid=shopid)) @@ -476,9 +488,7 @@ def remove_share(): if userid == None: flash("No such user!") return redirect(url_for("show_shop", shopid=shopid)) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] if session.get("user") != ownerid: flash("Not your shop!") return redirect(url_for("show_shop", shopid=shopid)) @@ -493,12 +503,8 @@ def remove_shop(): if not session.get("logged_in"): abort(401) shopid = int(request.form["shopid"]) - ownerid = g.db.execute( - "select owner from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] - shopname = g.db.execute( - "select shop from shops where id=?", (request.form["shopid"],) - ).fetchall()[0][0] + ownerid = g.db.execute("select owner from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] + shopname = g.db.execute("select shop from shops where id=?", (request.form["shopid"],)).fetchall()[0][0] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") @@ -553,10 +559,10 @@ def register(): if not app.config["register"]: return "" if request.method == "POST": - import re, string + import re + import string - pattern = re.compile("[\W]+") - username = pattern.sub("", request.form["username"]) + username = SHOPNAME_INVALIDCHARS.sub("", request.form["username"]) password = password_hash(request.form["password"]) if len(username) == 0: error = "No username given" @@ -569,9 +575,7 @@ def register(): if username == row[1]: error = "Username already exists" return render_template("register.html", error=error) - g.db.execute( - "insert into users (user,pass) values (?, ?)", [username, password] - ) + g.db.execute("insert into users (user,pass) values (?, ?)", [username, password]) g.db.commit() flash('successfully registered user "%s". Now login.' % username) return redirect(url_for("login")) @@ -585,16 +589,14 @@ def profile(): error = None user = get_username(session.get("user")) if request.method == "POST": - import re, string + import re + import string - pattern = re.compile("[\W]+") password = password_hash(request.form["password"]) if len(request.form["password"]) < 5: error = "Password too short" return render_template("profile.html", error=error, user=user) - g.db.execute( - "update users set pass=? where id=?", [password, session.get("user")] - ) + g.db.execute("update users set pass=? where id=?", [password, session.get("user")]) g.db.commit() flash("successfully updated profile.") return redirect(url_for("profile"))