# -*- 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 = 'shop.db' DATADIR = 'data' DEBUG = False SECRET_KEY = 'development key' USERNAME = 'admin' PASSWORD = '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).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=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().decode('utf-8').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().encode('utf-8')) 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.encode('utf-8')) 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().decode('utf-8').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).encode('utf-8')) 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().decode('utf-8').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).encode('utf-8')) 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()