This commit is contained in:
Matthew Scragg 2014-08-30 10:06:12 -05:00
parent b02d3db684
commit 86f0549e44
24 changed files with 710 additions and 398 deletions

View file

@ -1,5 +1,6 @@
from wtforms import Form, StringField, PasswordField, validators
from flask_wtf import Form, RecaptchaField
from wtforms import StringField, PasswordField, validators
from realms import config
class RegistrationForm(Form):
username = StringField('Username', [validators.Length(min=4, max=25)])
@ -10,9 +11,12 @@ class RegistrationForm(Form):
])
confirm = PasswordField('Repeat Password')
if config.RECAPTCHA_ENABLE:
setattr(RegistrationForm, 'recaptcha', RecaptchaField("You Human?"))
class LoginForm(Form):
email = StringField('Email', [validators.DataRequired])
password = PasswordField('Password', [validators.DataRequired])
email = StringField('Email', [validators.DataRequired()])
password = PasswordField('Password', [validators.DataRequired()])

View file

@ -1,23 +1,15 @@
from flask.ext.login import UserMixin, logout_user, login_user
from realms import config, login_manager
from realms.lib.services import db
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
from realms import config, login_manager, db
from realms.lib.model import Model
from realms.lib.util import gravatar_url
from itsdangerous import URLSafeSerializer, BadSignature
from hashlib import sha256
import json
import bcrypt
FIELD_MAP = dict(
u='username',
e='email',
p='password',
nv='not_verified',
a='admin',
b='banned')
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
return User.get_by_id(user_id)
@login_manager.token_loader
@ -29,7 +21,7 @@ def load_token(token):
return False
# User key *could* be stored in payload to avoid user lookup in db
user = User.get(payload.get('id'))
user = User.get_by_id(payload.get('id'))
if not user:
return False
@ -43,68 +35,78 @@ def load_token(token):
return False
class User(UserMixin):
class AnonUser(AnonymousUserMixin):
username = 'Anon'
email = ''
username = None
email = None
password = None
def __init__(self, email, data=None):
self.id = email
for k, v in data.items():
setattr(self, FIELD_MAP.get(k, k), v)
class User(Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, unique=True)
email = db.Column(db.String, unique=True)
password = db.Column(db.String)
hidden_fields = ['password']
readonly_fields = ['email', 'password']
def get_auth_token(self):
key = sha256(self.password).hexdigest()
return User.signer(key).dumps(dict(id=self.username))
return User.signer(key).dumps(dict(id=self.id))
@property
def avatar(self):
return gravatar_url(self.email)
@staticmethod
def create(username, email, password):
User.set(email, dict(u=username, e=email, p=User.hash(password), nv=1))
u = User()
u.username = username
u.email = email
u.password = User.hash_password(password)
u.save()
@staticmethod
def get_by_username(username):
return User.query.filter_by(username=username).first()
@staticmethod
def get_by_email(email):
return User.query.filter_by(email=email).first()
@staticmethod
def signer(salt):
"""
Signed with app secret salted with sha256 of password hash of user (client secret)
"""
return URLSafeSerializer(config.SECRET + salt)
@staticmethod
def set(email, data):
db.set('u:%s' % email, json.dumps(data, separators=(',', ':')))
@staticmethod
def get(email):
data = db.get('u:%s', email)
try:
data = json.loads(data)
except ValueError:
return None
if data:
return User(email, data)
else:
return None
return URLSafeSerializer(config.SECRET_KEY + salt)
@staticmethod
def auth(email, password):
user = User.get(email)
user = User.query.filter_by(email=email).first()
if not user:
# User doesn't exist
return False
if bcrypt.checkpw(password, user.password):
if User.check_password(password, user.password):
# Password is good, log in user
login_user(user, remember=True)
return user
else:
# Password check failed
return False
@staticmethod
def hash(password):
return bcrypt.hashpw(password, bcrypt.gensalt(log_rounds=12))
def hash_password(password):
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))
@staticmethod
def check_password(password, hashed):
return bcrypt.hashpw(password.encode('utf-8'), hashed.encode('utf-8')) == hashed
@classmethod
def logout(cls):
logout_user()
login_manager.anonymous_user = AnonUser

View file

@ -13,24 +13,55 @@ def logout_page():
return redirect(url_for(config.ROOT_ENDPOINT))
@blueprint.route("/login")
@blueprint.route("/login", methods=['GET', 'POST'])
def login():
if request.method == "POST":
form = RegistrationForm()
form = LoginForm()
# TODO
if request.method == "POST":
if not form.validate():
flash('Form invalid')
flash('Form invalid', 'warning')
return redirect(url_for('auth.login'))
if User.auth(request.form['email'], request.form['password']):
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
flash('Email or Password Incorrect', 'warning')
return redirect(url_for('auth.login'))
return render_template("auth/login.html")
return render_template("auth/login.html", form=form)
@blueprint.route("/register")
@blueprint.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if request.method == "POST":
if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.register'))
if User.get_by_username(request.form['username']):
flash('Username is taken', 'warning')
return redirect(url_for('auth.register'))
if User.get_by_email(request.form['email']):
flash('Email is taken', 'warning')
return redirect(url_for('auth.register'))
User.create(request.form['username'], request.form['email'], request.form['password'])
User.auth(request.form['email'], request.form['password'])
return redirect(request.args.get("next") or url_for(config.ROOT_ENDPOINT))
else:
return render_template("auth/register.html")
return render_template("auth/register.html", form=form)
@blueprint.route("/settings", methods=['GET', 'POST'])
def settings():
return render_template("auth/settings.html")
@blueprint.route("/logout")
def logout():
User.logout()
return redirect("/")

View file

@ -1,8 +1,7 @@
from realms.lib.assets import register
from realms import assets
register(
'editor',
'js/ace/ace.js',
'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js')
assets.register('editor',
'js/ace/ace.js',
'js/ace/mode-markdown.js',
'vendor/keymaster/keymaster.js',
'js/dillinger.js')

View file

@ -0,0 +1,165 @@
import os
import re
import lxml.html
from lxml.html.clean import Cleaner
import ghdiff
import gittle.utils
from gittle import Gittle
from dulwich.repo import NotGitRepository
from werkzeug.utils import escape, unescape
from realms.lib.util import to_canonical
from realms import cache
class MyGittle(Gittle):
def file_history(self, path):
"""Returns all commits where given file was modified
"""
versions = []
commits_info = self.commit_info()
seen_shas = set()
for commit in commits_info:
try:
files = self.get_commit_files(commit['sha'], paths=[path])
file_path, file_data = files.items()[0]
except IndexError:
continue
file_sha = file_data['sha']
if file_sha in seen_shas:
continue
else:
seen_shas.add(file_sha)
versions.append(dict(author=commit['author']['name'],
time=commit['time'],
file_sha=file_sha,
sha=commit['sha'],
message=commit['message']))
return versions
def mv_fs(self, file_pair):
old_name, new_name = file_pair
os.rename(self.path + "/" + old_name, self.path + "/" + new_name)
class Wiki():
path = None
base_path = '/'
default_ref = 'master'
default_committer_name = 'Anon'
default_committer_email = 'anon@anon.anon'
index_page = 'home'
repo = None
def __init__(self, path):
try:
self.repo = MyGittle(path)
except NotGitRepository:
self.repo = MyGittle.init(path)
self.path = path
def revert_page(self, name, commit_sha, message, username):
page = self.get_page(name, commit_sha)
if not page:
# Page not found
return None
commit_info = gittle.utils.git.commit_info(self.repo[commit_sha.encode('latin-1')])
message = commit_info['message']
return self.write_page(name, page['data'], message=message, username=username)
def write_page(self, name, content, message=None, create=False, username=None, email=None):
def escape_repl(m):
if m.group(1):
return "```" + escape(m.group(1)) + "```"
def unescape_repl(m):
if m.group(1):
return "```" + unescape(m.group(1)) + "```"
# prevents p tag from being added, we remove this later
content = '<div>' + content + '</div>'
content = re.sub(r"```(.*?)```", escape_repl, content, flags=re.DOTALL)
tree = lxml.html.fromstring(content)
cleaner = Cleaner(remove_unknown_tags=False,
kill_tags=set(['style']),
safe_attrs_only=False)
tree = cleaner.clean_html(tree)
content = lxml.html.tostring(tree, encoding='utf-8', method='html')
# remove added div tags
content = content[5:-6]
# FIXME this is for block quotes, doesn't work for double ">"
content = re.sub(r"(\n&gt;)", "\n>", content)
content = re.sub(r"(^&gt;)", ">", content)
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
cname = to_canonical(name)
filename = self.cname_to_filename(cname)
with open(self.path + "/" + filename, 'w') as f:
f.write(content)
if create:
self.repo.add(filename)
if not message:
message = "Updated %s" % name
if not username:
username = self.default_committer_name
if not email:
email = self.default_committer_email
ret = self.repo.commit(name=username,
email=email,
message=message,
files=[filename])
cache.delete_memoized(Wiki.get_page, cname)
return ret
def rename_page(self, old_name, new_name):
old_name, new_name = map(self.cname_to_filename, [old_name, new_name])
self.repo.mv([(old_name, new_name)])
self.repo.commit(name=self.default_committer_name,
email=self.default_committer_email,
message="Moving %s to %s" % (old_name, new_name),
files=[old_name])
cache.delete_memoized(Wiki.get_page, old_name)
cache.delete_memoized(Wiki.get_page, new_name)
@cache.memoize()
def get_page(self, name, sha='HEAD'):
# commit = gittle.utils.git.commit_info(self.repo[sha])
name = self.cname_to_filename(name).encode('latin-1')
sha = sha.encode('latin-1')
try:
return self.repo.get_commit_files(sha, paths=[name]).get(name)
except KeyError:
# HEAD doesn't exist yet
return None
def compare(self, name, old_sha, new_sha):
old = self.get_page(name, sha=old_sha)
new = self.get_page(name, sha=new_sha)
return ghdiff.diff(old['data'], new['data'])
def get_history(self, name):
return self.repo.file_history(self.cname_to_filename(name))
@staticmethod
def cname_to_filename(cname):
return cname.lower() + ".md"

View file

@ -1,5 +0,0 @@
import realms
c = realms.app.test_client()
print c.get('/wiki/_create')
print c.get('/wiki/_create/blah')

View file

@ -1,15 +1,19 @@
from flask import g, render_template, request, redirect, Blueprint, flash, url_for
from flask import g, render_template, request, redirect, Blueprint, flash, url_for, current_app
from flask.ext.login import login_required
from realms.lib.util import to_canonical, remove_ext
from realms import config
from realms.modules.wiki.models import Wiki
from realms import config, current_user
blueprint = Blueprint('wiki', __name__, url_prefix=config.RELATIVE_PATH)
wiki = Wiki(config.WIKI_PATH)
@blueprint.route("/_commit/<sha>/<name>")
def commit(name, sha):
cname = to_canonical(name)
data = g.current_wiki.get_page(cname, sha=sha)
data = wiki.get_page(cname, sha=sha)
if data:
return render_template('wiki/page.html', name=name, page=data, commit=sha)
else:
@ -18,41 +22,42 @@ def commit(name, sha):
@blueprint.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha):
diff = g.current_wiki.compare(name, fsha, lsha)
diff = wiki.compare(name, fsha, lsha)
return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha)
@blueprint.route("/_revert", methods=['POST'])
@login_required
def revert():
if request.method == 'POST':
name = request.form.get('name')
commit = request.form.get('commit')
cname = to_canonical(name)
g.current_wiki.revert_page(name, commit, message="Reverting %s" % cname,
username=g.current_user.get('username'))
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))
name = request.form.get('name')
commit = request.form.get('commit')
cname = to_canonical(name)
wiki.revert_page(name, commit, message="Reverting %s" % cname,
username=g.current_user.username)
flash('Page reverted', 'success')
return redirect(url_for('wiki.page', name=cname))
@blueprint.route("/_history/<name>")
def history(name):
history = g.current_wiki.get_history(name)
history = wiki.get_history(name)
return render_template('wiki/history.html', name=name, history=history, wiki_home=url_for('wiki.page'))
@blueprint.route("/_edit/<name>", methods=['GET', 'POST'])
@login_required
def edit(name):
data = g.current_wiki.get_page(name)
data = wiki.get_page(name)
cname = to_canonical(name)
if request.method == 'POST':
edit_cname = to_canonical(request.form['name'])
if edit_cname.lower() != cname.lower():
g.current_wiki.rename_page(cname, edit_cname)
wiki.rename_page(cname, edit_cname)
g.current_wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=g.current_user.get('username'))
wiki.write_page(edit_cname,
request.form['content'],
message=request.form['message'],
username=g.current_user.username)
else:
if data:
name = remove_ext(data['name'])
@ -64,22 +69,24 @@ def edit(name):
@blueprint.route("/_delete/<name>", methods=['POST'])
@login_required
def delete(name):
pass
@blueprint.route("/_create/", defaults={'name': None}, methods=['GET', 'POST'])
@blueprint.route("/_create/<name>", methods=['GET', 'POST'])
@login_required
def create(name):
if request.method == 'POST':
g.current_wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'],
create=True,
username=g.current_user.get('username'))
wiki.write_page(request.form['name'],
request.form['content'],
message=request.form['message'],
create=True,
username=g.current_user.username)
else:
cname = to_canonical(name) if name else ""
if cname and g.current_wiki.get_page(cname):
if cname and wiki.get_page(cname):
# Page exists, edit instead
return redirect(url_for('wiki.edit', name=cname))
@ -94,7 +101,7 @@ def page(name):
if cname != name:
return redirect(url_for('wiki.page', name=cname))
data = g.current_wiki.get_page(cname)
data = wiki.get_page(cname)
if data:
return render_template('wiki/page.html', name=cname, page=data)