# -*- coding: utf-8 -*- # all the imports import sqlite3, time, datetime, hashlib, os, re from shutil import copyfile, move from flask import ( Flask, request, session, g, redirect, url_for, abort, render_template, flash, ) from revprox import ReverseProxied # configuration DATABASE = "/data/shop.db" DATADIR = "/data" DEBUG = False SECRET_KEY = os.getenv("SECRET_KEY", "development key") USERNAME = os.getenv("ADMIN_USER", "admin") PASSWORD = os.getenv("ADMIN_PASSWD", "default") URLFINDER = re.compile("((news|telnet|nttp|file|http|ftp|https)://[^ ]+)") URLPARSER = re.compile(r"(\[)([^\]]+)(\])\(([^\)]+)\)") BOLDFINDER = re.compile(r"\*([^\*]+)\*") CODEFINDER = re.compile(r"\`([^\`]+)\`") # 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) def connect_db(): if not os.path.exists(app.config["DATABASE"]): db = sqlite3.connect(app.config["DATABASE"]) for command in open("schema.sql", "rt").read().split(";"): db.execute(command) db.commit() return sqlite3.connect(app.config["DATABASE"]) def password_hash(s): return hashlib.sha224(s.encode("utf8")).hexdigest() def get_username(id): cur = g.db.execute("select * from users") for row in cur.fetchall(): if id == row[0]: return row[1] return None def get_userid(name): cur = g.db.execute("select * from users") for row in cur.fetchall(): if name == row[1]: return row[0] return None def get_shares(id): cur = g.db.execute("select * from shares") shares = [] for row in cur.fetchall(): if id == row[1]: shares.append(row[0]) return shares def get_shop_date(id): date = "" cur = g.db.execute("select * from shops") for row in cur.fetchall(): if id == row[0]: 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") return date def get_shop_backup_date(id): date = "" cur = g.db.execute("select * from shops") for row in cur.fetchall(): if id == row[0]: 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") return date def scan_for_new_documents(id): user = get_username(id) data_dir = os.path.join(DATADIR, user) if not os.path.exists(data_dir): return cur = g.db.execute("select * from shops") existing = [] non_existing = [] for row in cur.fetchall(): if row[2] != id: continue existing.append(row[1]) for row in os.listdir(data_dir): if row.endswith(".md"): if row[:-3] not in existing: non_existing.append(row[:-3]) for shop in non_existing: g.db.execute("insert into shops (shop,owner) values (?, ?)", [shop, id]) g.db.commit() def markdown_parse(s): s = s.decode("utf8") s = BOLDFINDER.sub(r'*\1*', s) s = CODEFINDER.sub(r'`\1`', s) return s def urlify(s): if URLPARSER.search(s): return URLPARSER.sub(r'[\2]', s) return URLFINDER.sub(r'\1', s) @app.before_request def before_request(): g.db = connect_db() @app.teardown_request def teardown_request(exception): db = getattr(g, "db", None) if db is not None: db.close() @app.route("/shop/") def show_shop(shopid): if not session.get("logged_in"): return redirect(url_for("login", error=None)) try: shopid = int(shopid) except ValueError: return redirect(url_for("login", error=None)) has_access = False cur = g.db.execute("select * from shops") shared = get_shares(session.get("user")) for row in cur.fetchall(): if row[0] == shopid: if row[2] == session.get("user") or row[0] in shared: has_access = True shopname = row[1] break if not has_access: return redirect(url_for("list_shops")) data_dir = os.path.join(DATADIR, get_username(row[2])) data_file = os.path.join(data_dir, row[1] + ".md") if not os.path.exists(data_file): open(data_file, "wt").close() entries = [] content = open(data_file, "rt").read() # .decode('utf-8') for i, row in enumerate(open(data_file, "rt").read().split("\n")): # any parsing magick would be here row = row.rstrip() if row == "": continue icon = " " extra_class = "noitem" if "[ ]" in row: icon = u" " extra_class = "" if "[x]" in row: icon = u"\u2714" extra_class = "" row = urlify(row).encode("ascii", "xmlcharrefreplace") row = markdown_parse(row) if row.startswith("#"): row = "" + row + "" if row.startswith(">"): row = "" + row + "" entries.append(dict(row=i, text=row, icon=icon, extra_class=extra_class)) shared_to = [] cur = g.db.execute("select * from shares") for row in cur.fetchall(): if row[0] == shopid: shared_to.append(get_username(row[1])) # invalidate autosort in 60 minutes: if session.get("sort_update"): if time.time() - session.get("sort_update") > 3600: session["sort_view"] = False session["sort_update"] = time.time() return render_template( "show_shop.html", entries=entries, shop=shopname, shopid=shopid, content=content, shares=shared_to, date=get_shop_date(shopid), date_bkp=get_shop_backup_date(shopid), autosort=session.get("sort_view", False), ) @app.route("/") def list_shops(): if not session.get("logged_in"): return redirect(url_for("login", error=None)) scan_for_new_documents(session.get("user")) cur = g.db.execute("select * from shops order by shop") entries = [] 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) ) 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) ) return render_template("list_shops.html", entries=entries) @app.route("/add", methods=["POST"]) 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] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") count = 0 contents_file = open(data_file, "at") for row in request.form["add_md"].split("\n"): if row.strip() == "": continue count += 1 contents_file.write("[ ] %s\n" % row.strip()) contents_file.close() flash("Added %d items" % (count)) return redirect(url_for("show_shop", shopid=shopid)) @app.route("/edit", methods=["POST"]) 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] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") backup = data_file + ".bkp" copyfile(data_file, backup) contents_file = open(data_file, "wt") contents_list = request.form["edit_md"].split("\n") while contents_list[-1].strip() == "": contents_list.pop() for row in contents_list: contents_file.write("%s\n" % (row,)) contents_file.close() flash("Saved new file.") return redirect(url_for("show_shop", shopid=shopid)) @app.route("/restore", methods=["POST"]) 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] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") backup = data_file + ".bkp" backup_tmp = data_file + ".tmp" if not os.path.exists(backup): flash("Backup does not exist") return redirect(url_for("show_shop", shopid=shopid)) copyfile(data_file, backup_tmp) copyfile(backup, data_file) move(backup_tmp, backup) flash("Backup restored") return redirect(url_for("show_shop", shopid=shopid)) @app.route("/toggle", methods=["POST"]) 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] ownername = get_username(ownerid) req_row = None for key in request.form: if key.startswith("item"): req_row = int(key[4:]) if key == "toggleAll": # Special meaning: toggle all rows req_row = -1 if key == "unTickAll": # Special meaning: untick all rows req_row = -2 if req_row == None: return redirect(url_for("show_shop", shopid=shopid)) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") backup = data_file + ".bkp" contents_file = open(data_file, "rt") contents = contents_file.read().split("\n") contents_file.close() changed = False 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 row != contents[i]: changed = True if changed: if req_row == -1 or req_row == -2: copyfile(data_file, backup) contents_file = open(data_file, "wt") contents_file.write("\n".join(contents)) contents_file.close() return redirect(url_for("show_shop", shopid=shopid)) @app.route("/remove_toggled", methods=["POST"]) 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] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") backup = data_file + ".bkp" contents_file = open(data_file, "rt") contents = [] changed = False for i, row in enumerate(contents_file.read().split("\n")): if "[x]" not in row: contents.append(row) else: changed = True contents_file.close() if changed: copyfile(data_file, backup) contents_file = open(data_file, "wt") contents_file.write("\n".join(contents)) contents_file.close() # ~ flash('successfully posted %s (%d)'%(row,req_row)) return redirect(url_for("show_shop", shopid=shopid)) @app.route("/add_shop", methods=["POST"]) def add_shop(): if not session.get("logged_in"): abort(401) import re, string pattern = re.compile("[\W]+") shopname = pattern.sub("", request.form["shop"]) if shopname == "": flash("Shop name empty!") return redirect(url_for("list_shops")) cur = g.db.execute("select * from shops order by shop") for row in cur.fetchall(): 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.commit() new_dir = os.path.join(DATADIR, get_username(session["user"])) new_file = os.path.join(new_dir, shopname + ".md") if not os.path.exists(new_dir): os.mkdir(new_dir) open(new_file, "at") flash("successfully created new shop: " + shopname) return redirect(url_for("list_shops")) @app.route("/add_share", methods=["POST"]) def add_share(): if not session.get("logged_in"): abort(401) import re, string pattern = re.compile("[\W]+") username = pattern.sub("", request.form["share"]) shopid = pattern.sub("", request.form["shopid"]) if username == "": flash("User name empty!") return redirect(url_for("show_shop", shopid=shopid)) userid = get_userid(username) 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] if session.get("user") != ownerid: flash("Not your shop!") return redirect(url_for("show_shop", shopid=shopid)) g.db.execute("insert into shares (shopid,userid) values (?, ?)", [shopid, userid]) g.db.commit() flash("Shared to %s" % (username)) return redirect(url_for("show_shop", shopid=shopid)) @app.route("/remove_share", methods=["POST"]) def remove_share(): if not session.get("logged_in"): abort(401) import re, string pattern = re.compile("[\W]+") username = pattern.sub("", request.form["user"]) shopid = pattern.sub("", request.form["shopid"]) if username == "": flash("User name empty!") return redirect(url_for("show_shop", shopid=shopid)) userid = get_userid(username) 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] if session.get("user") != ownerid: flash("Not your shop!") return redirect(url_for("show_shop", shopid=shopid)) g.db.execute("delete from shares where shopid=? and userid=?", [shopid, userid]) g.db.commit() return redirect(url_for("show_shop", shopid=shopid)) @app.route("/remove_shop", methods=["POST"]) 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] ownername = get_username(ownerid) data_dir = os.path.join(DATADIR, ownername) data_file = os.path.join(data_dir, shopname + ".md") if session.get("user") != ownerid: flash("Not your shop!") return redirect(url_for("show_shop", shopid=shopid)) # remove shop DB g.db.execute("delete from shops where id=?", [shopid]) g.db.commit() # backup data, and remove backup = data_file + ".bkp" copyfile(data_file, backup) os.remove(data_file) # remove shares g.db.execute("delete from shares where shopid=?", [shopid]) g.db.commit() flash("successfully deleted shop %s" % (shopname)) return redirect(url_for("list_shops")) @app.route("/sort_flip", methods=["POST"]) def sort_flip(): if not session.get("sort_view"): session["sort_view"] = True session["sort_update"] = time.time() else: session["sort_view"] = False session["sort_update"] = time.time() shopid = int(request.form["shopid"]) return redirect(url_for("show_shop", shopid=shopid)) @app.route("/login", methods=["GET", "POST"]) def login(): error = None if request.method == "POST": cur = g.db.execute("select * from users") for row in cur.fetchall(): if request.form["username"] == row[1]: if password_hash(request.form["password"]) == row[2]: session["logged_in"] = True session["user"] = row[0] # scan_for_new_documents(row[0]) return redirect(url_for("list_shops")) error = "Invalid user/pass" return render_template("login.html", error=error) @app.route("/register", methods=["GET", "POST"]) def register(): error = None if not app.config["register"]: return "" if request.method == "POST": import re, string pattern = re.compile("[\W]+") username = pattern.sub("", request.form["username"]) password = password_hash(request.form["password"]) if len(username) == 0: error = "No username given" return render_template("register.html", error=error) if len(request.form["password"]) < 5: error = "Password too short" return render_template("register.html", error=error) cur = g.db.execute("select * from users") for row in cur.fetchall(): 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.commit() flash('successfully registered user "%s". Now login.' % username) return redirect(url_for("login")) return render_template("register.html", error=error) @app.route("/profile", methods=["GET", "POST"]) def profile(): if not session.get("logged_in"): return redirect(url_for("login")) error = None user = get_username(session.get("user")) if request.method == "POST": import re, 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.commit() flash("successfully updated profile.") return redirect(url_for("profile")) return render_template("profile.html", error=error, user=user) @app.route("/logout") def logout(): session.pop("logged_in", None) session.pop("user", None) flash("You were logged out") return render_template("login.html", error=None) if __name__ == "__main__": app.run()