diff --git a/app.py b/app.py index 12f2880..9469a56 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,8 @@ from gevent import monkey, pywsgi +from realms import config, app + monkey.patch_all() import logging -from realms import app, config if __name__ == '__main__': diff --git a/realms/__init__.py b/realms/__init__.py index 9a21be5..c4f9688 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -1,16 +1,17 @@ import logging -import os import time -from tldextract import tldextract +import sys +import os -from flask import Flask, g, request, render_template, url_for, redirect, flash, session, current_app +from flask import Flask, request, render_template, url_for, redirect, session from flask.ctx import _AppCtxGlobals +from flask.ext.script import Manager from flask.ext.login import LoginManager, login_required from flask.ext.assets import Environment, Bundle from werkzeug.routing import BaseConverter from werkzeug.utils import cached_property -import config +from realms import config from realms.lib.ratelimit import get_view_rate_limit, ratelimiter from realms.lib.session import RedisSessionInterface from realms.lib.wiki import Wiki @@ -19,13 +20,26 @@ from realms.lib.services import db from models import Site, User, CurrentUser +wikis = {} + + class AppCtxGlobals(_AppCtxGlobals): + @cached_property + def current_wiki(self): + subdomain = format_subdomain(self.current_site) + if not subdomain: + subdomain = "_" + + if not wikis.get(subdomain): + wikis[subdomain] = Wiki("%s/%s" % (config.REPO_DIR, subdomain)) + + return wikis[subdomain] + @cached_property def current_site(self): - ext = tldextract.extract(request.host) - print ext - return ext.subdomain + host = request.host.split(':')[0] + return host[:-len(config.DOMAIN)].rstrip('.') @cached_property def current_user(self): @@ -53,6 +67,41 @@ class Application(Flask): return super(Application, self).__call__(environ, start_response) + def discover(self): + """ + Pattern taken from guildwork.com + """ + IMPORT_NAME = 'realms.modules' + FROMLIST = ( + 'assets', + 'models', + 'search', + 'perms', + 'broadcasts', + 'commands', + 'notifications', + 'requests', + 'tasks', + 'views', + ) + + start_time = time.time() + + __import__(IMPORT_NAME, fromlist=FROMLIST) + + for module_name in self.config['MODULES']: + sources = __import__('%s.%s' % (IMPORT_NAME, module_name), fromlist=FROMLIST) + + # Blueprint + if hasattr(sources, 'views'): + self.register_blueprint(sources.views.blueprint) + + # Flask-Script + if hasattr(sources, 'commands'): + manager.add_command(module_name, sources.commands.manager) + + print >> sys.stderr, ' * Ready in %.2fms' % (1000.0 * (time.time() - start_time)) + def init_db(dbname): """ @@ -77,23 +126,24 @@ def redirect_url(referrer=None): def format_subdomain(s): - if not config.repos['enable_subrepos']: + if not config.REPO_ENABLE_SUBDOMAIN: return "" s = s.lower() s = to_canonical(s) - if s in config.repos['forbidden_subrepos']: + if s in config.REPO_FORBIDDEN_NAMES: # Not allowed s = "" return s app = Application(__name__) -app.config.update(config.FLASK) -app.debug = (config.ENV is not 'PROD') -app.secret_key = config.SECRET_KEY -app.static_path = os.sep + 'static' +app.config.from_object('realms.config') app.session_interface = RedisSessionInterface() app.url_map.converters['regex'] = RegexConverter +app.url_map.strict_slashes = False +app.debug = True + +manager = Manager(app) # Flask extension objects login_manager = LoginManager() @@ -137,8 +187,6 @@ else: filters='closure_js', output='packed-editor.js') assets.register('js_editor', js) -repo_dir = config.REPO_DIR - @app.after_request def inject_x_rate_headers(response): @@ -155,6 +203,7 @@ def inject_x_rate_headers(response): def _jinja2_filter_datetime(ts): return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) + @app.errorhandler(404) def page_not_found(e): return render_template('errors/404.html'), 404 @@ -167,160 +216,18 @@ def page_error(e): @app.route("/") -@ratelimiter(limit=50, per=60) def root(): - return g.current_site - return render('home') + return redirect(url_for(config.ROOT_ENDPOINT)) -@app.route("/home") -def home(): - return redirect(url_for('root')) - @app.route("/_account/") @login_required def account(): return render_template('account/index.html') +if 'devserver' not in sys.argv or os.environ.get('WERKZEUG_RUN_MAIN'): + app.discover() -@app.route("/_new/", methods=['GET', 'POST']) -@login_required -def new_wiki(): - if request.method == 'POST': - wiki_name = to_canonical(request.form['name']) +print app.url_map - if Wiki.is_registered(wiki_name): - flash("Site already exists") - return redirect(redirect_url()) - else: - s = Site() - s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id')) - return redirect('http://%s.%s' % (wiki_name, config.hostname)) - else: - return render_template('_new/index.html') - - -@app.route("/_logout/") -def logout(): - User.logout() - return redirect(url_for('root')) - - -@app.route("/_commit//") -def commit_sha(name, sha): - cname = to_canonical(name) - - data = Wiki.get_page(cname, sha=sha) - if data: - return render_template('page/page.html', name=name, page=data, commit=sha) - else: - return redirect('/_create/'+cname) - - -@app.route("/_compare//") -def compare(name, fsha, dots, lsha): - diff = Wiki.compare(name, fsha, lsha) - return render_template('page/compare.html', name=name, diff=diff, old=fsha, new=lsha) - - -@app.route("/_revert", methods=['POST']) -def revert(): - if request.method == 'POST': - 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.get('username')) - flash('Page reverted', 'success') - return redirect("/" + cname) - - -@app.route("/_register", methods=['GET', 'POST']) -def register(): - if request.method == 'POST': - if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')): - return redirect(url_for('root')) - else: - # Login failed - return redirect(url_for('register')) - else: - return render_template('account/register.html') - - -@app.route("/_login", methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - if User.auth(request.form['email'], request.form['password']): - return redirect(redirect_url(referrer=url_for('root'))) - else: - flash("Email or Password invalid") - return redirect("/_login") - else: - return render_template('account/login.html') - - -@app.route("/_history/") -def history(name): - history = Wiki.get_history(name) - return render_template('page/history.html', name=name, history=history) - - -@app.route("/_edit/", methods=['GET', 'POST']) -def edit(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(): - Wiki.rename_page(cname, edit_cname) - Wiki.write_page(edit_cname, - request.form['content'], - message=request.form['message'], - username=g.current_user.get('username')) - return redirect("/" + edit_cname) - else: - if data: - name = remove_ext(data['name']) - content = data['data'] - return render_template('page/edit.html', name=name, content=content) - else: - return redirect('/_create/'+cname) - - -@app.route("/_delete/", methods=['POST']) -@login_required -def delete(name): - pass - - -@app.route("/_create/", methods=['GET', 'POST']) -@app.route("/_create/", methods=['GET', 'POST']) -def create(name=None): - cname = "" - if name: - cname = to_canonical(name) - if Wiki.get_page(cname): - # Page exists, edit instead - return redirect("/edit/" + cname) - if request.method == 'POST': - Wiki.write_page(request.form['name'], - request.form['content'], - message=request.form['message'], - create=True, - username=g.current_user.get('username')) - return redirect("/" + cname) - else: - return render_template('page/edit.html', name=cname, content="") - - -@app.route("/") -def render(name): - cname = to_canonical(name) - if cname != name: - return redirect('/' + cname) - - data = Wiki.get_page(cname) - if data: - return render_template('page/page.html', name=cname, page=data) - else: - return redirect('/_create/'+cname) diff --git a/realms/config/__init__.py b/realms/config/__init__.py new file mode 100644 index 0000000..52a50a4 --- /dev/null +++ b/realms/config/__init__.py @@ -0,0 +1,38 @@ +import socket + +HOSTNAME = socket.gethostname() + +DOMAIN = 'realms.dev' +ENV = 'DEV' +PORT = 10000 + +DB_URI = 'postgresql://realms:dbpassword@localhost:5432/realms' + +REDIS_HOST = '127.0.0.1' +REDIS_PORT = 6379 + +SECRET_KEY = 'K3dRq1q9eN72GJDkgvyshFVwlqHHCyPI' + +REPO_DIR = '/home/deploy/repos' +REPO_MAIN_NAME = '_' +REPO_FORBIDDEN_NAMES = ['api', 'www'] +REPO_ENABLE_SUBDOMAIN = True + + +RECAPTCHA_PUBLIC_KEY = '6LfoxeESAAAAAGNaeWnISh0GTgDk0fBnr6Bo2Tfk' +RECAPTCHA_PRIVATE_KEY = '6LfoxeESAAAAABFzdCs0hNIIyeb42mofV-Ndd2_2' +RECAPTCHA_OPTIONS = {'theme': 'clean'} + +ROOT_ENDPOINT = 'wiki.page' +WIKI_HOME = 'home' + +MODULES = [ + 'wiki', + 'auth' +] + +if ENV is 'PROD': + pass +else: + DEBUG = True + ASSETS_DEBUG = True \ No newline at end of file diff --git a/realms/lib/services.py b/realms/lib/services.py index 0069cce..4d54fe0 100644 --- a/realms/lib/services.py +++ b/realms/lib/services.py @@ -1,8 +1,9 @@ import redis -from realms import config from sqlalchemy import create_engine # Default DB connection +from realms import config + db = create_engine(config.DB_URI, encoding='utf8', echo=True) # Default Cache connection diff --git a/realms/lib/util.py b/realms/lib/util.py index dd9ee3c..227f0d2 100644 --- a/realms/lib/util.py +++ b/realms/lib/util.py @@ -59,11 +59,12 @@ def to_dict(cur, first=False): else: return ret + def validate_captcha(): response = captcha.submit( request.form['recaptcha_challenge_field'], request.form['recaptcha_response_field'], - config.flask['RECAPTCHA_PRIVATE_KEY'], + config.RECAPTCHA_PRIVATE_KEY, request.remote_addr) return response.is_valid @@ -125,4 +126,4 @@ def to_canonical(s): def gravatar_url(email): - return "https://www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest() \ No newline at end of file + return "//www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest() \ No newline at end of file diff --git a/realms/lib/wiki.py b/realms/lib/wiki.py index 634887e..d79904c 100644 --- a/realms/lib/wiki.py +++ b/realms/lib/wiki.py @@ -1,6 +1,7 @@ import os import re import lxml.html +from lxml.html.clean import Cleaner import ghdiff import gittle.utils from gittle import Gittle @@ -92,7 +93,9 @@ class Wiki(): tree = lxml.html.fromstring(content) - cleaner = lxml.html.Cleaner(remove_unknown_tags=False, kill_tags=set(['style']), safe_attrs_only=False) + 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') diff --git a/realms/modules/__init__.py b/realms/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realms/modules/auth/__init__.py b/realms/modules/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realms/modules/auth/views.py b/realms/modules/auth/views.py new file mode 100644 index 0000000..1190f03 --- /dev/null +++ b/realms/modules/auth/views.py @@ -0,0 +1,35 @@ +from flask import render_template, redirect, request, url_for, flash, Blueprint +from realms import redirect_url +from realms.models import User + +blueprint = Blueprint('auth', __name__) + + +@blueprint.route("/logout/") +def logout(): + User.logout() + return redirect(url_for('root')) + + +@blueprint.route("/register/", methods=['GET', 'POST']) +def register(): + if request.method == 'POST': + if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')): + return redirect(url_for('root')) + else: + # Login failed + return redirect(url_for('.register')) + else: + return render_template('auth/register.html') + + +@blueprint.route("/login/", methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + if User.auth(request.form['email'], request.form['password']): + return redirect(redirect_url(referrer=url_for('root'))) + else: + flash("Email or Password invalid") + return redirect(url_for(".login")) + else: + return render_template('auth/login.html') \ No newline at end of file diff --git a/realms/modules/wiki/__init__.py b/realms/modules/wiki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realms/modules/wiki/tests.py b/realms/modules/wiki/tests.py new file mode 100644 index 0000000..fedb4bd --- /dev/null +++ b/realms/modules/wiki/tests.py @@ -0,0 +1,5 @@ +import realms + +c = realms.app.test_client() +print c.get('/wiki/_create') +print c.get('/wiki/_create/blah') \ No newline at end of file diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py new file mode 100644 index 0000000..fe31a4e --- /dev/null +++ b/realms/modules/wiki/views.py @@ -0,0 +1,122 @@ +from flask import g, render_template, request, redirect, Blueprint, flash, url_for +from flask.ext.login import login_required +from realms import app, redirect_url, config +from realms.lib.util import to_canonical, remove_ext +from realms.lib.wiki import Wiki +from realms.models import Site + +blueprint = Blueprint('wiki', __name__) + + +@blueprint.route("/wiki/_new/", methods=['GET', 'POST']) +@login_required +def new_wiki(): + if request.method == 'POST': + wiki_name = to_canonical(request.form['name']) + + if Wiki.is_registered(wiki_name): + flash("Site already exists") + return redirect(redirect_url()) + else: + s = Site() + s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id')) + return redirect('http://%s.%s' % (wiki_name, config.HOSTNAME)) + else: + return render_template('wiki/new.html') + + +@blueprint.route("/wiki/_commit//") +def commit_sha(name, sha): + cname = to_canonical(name) + + data = g.current_wiki.get_page(cname, sha=sha) + if data: + return render_template('wiki/page.html', name=name, page=data, commit=sha) + else: + return redirect(url_for('.create', name=cname)) + + +@blueprint.route("/wiki/_compare//") +def compare(name, fsha, dots, lsha): + diff = g.current_wiki.compare(name, fsha, lsha) + return render_template('wiki/compare.html', name=name, diff=diff, old=fsha, new=lsha) + + +@blueprint.route("/wiki/_revert", methods=['POST']) +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('.page', name=cname)) + +@blueprint.route("/wiki/_history/") +def history(name): + history = g.current_wiki.get_history(name) + return render_template('wiki/history.html', name=name, history=history) + + +@blueprint.route("/wiki/_edit/", methods=['GET', 'POST']) +def edit(name): + data = g.current_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) + g.current_wiki.write_page(edit_cname, + request.form['content'], + message=request.form['message'], + username=g.current_user.get('username')) + return redirect(url_for('.page', name=edit_cname)) + else: + if data: + name = remove_ext(data['name']) + content = data['data'] + return render_template('wiki/edit.html', name=name, content=content) + else: + return redirect(url_for('.create', name=cname)) + + +@blueprint.route("/wiki/_delete/", methods=['POST']) +@login_required +def delete(name): + pass + + +@blueprint.route("/wiki/_create/", defaults={'name': None}, methods=['GET', 'POST']) +@blueprint.route("/wiki/_create/", methods=['GET', 'POST']) +def create(name): + cname = "" + if name: + cname = to_canonical(name) + if g.current_wiki.get_page(cname): + # Page exists, edit instead + return redirect(url_for('.edit', name=cname)) + + 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')) + return redirect(url_for('.page', name=cname)) + else: + return render_template('wiki/edit.html', name=cname, content="") + + +@blueprint.route("/wiki/", defaults={'name': 'home'}) +@blueprint.route("/wiki/") +def page(name): + cname = to_canonical(name) + if cname != name: + return redirect(url_for('.page', name=cname)) + + data = g.current_wiki.get_page(cname) + + if data: + return render_template('wiki/page.html', name=cname, page=data) + else: + return redirect(url_for('.create', name=cname)) \ No newline at end of file diff --git a/realms/templates/account/index.html b/realms/templates/auth/index.html similarity index 100% rename from realms/templates/account/index.html rename to realms/templates/auth/index.html diff --git a/realms/templates/account/login.html b/realms/templates/auth/login.html similarity index 100% rename from realms/templates/account/login.html rename to realms/templates/auth/login.html diff --git a/realms/templates/account/register.html b/realms/templates/auth/register.html similarity index 100% rename from realms/templates/account/register.html rename to realms/templates/auth/register.html diff --git a/realms/templates/layout.html b/realms/templates/layout.html index 360207a..089a7f2 100644 --- a/realms/templates/layout.html +++ b/realms/templates/layout.html @@ -7,7 +7,7 @@ Realms - + diff --git a/realms/templates/page/compare.html b/realms/templates/wiki/compare.html similarity index 100% rename from realms/templates/page/compare.html rename to realms/templates/wiki/compare.html diff --git a/realms/templates/page/create.html b/realms/templates/wiki/create.html similarity index 100% rename from realms/templates/page/create.html rename to realms/templates/wiki/create.html diff --git a/realms/templates/page/edit.html b/realms/templates/wiki/edit.html similarity index 100% rename from realms/templates/page/edit.html rename to realms/templates/wiki/edit.html diff --git a/realms/templates/page/history.html b/realms/templates/wiki/history.html similarity index 100% rename from realms/templates/page/history.html rename to realms/templates/wiki/history.html diff --git a/realms/templates/_new/index.html b/realms/templates/wiki/new.html similarity index 100% rename from realms/templates/_new/index.html rename to realms/templates/wiki/new.html diff --git a/realms/templates/page/page.html b/realms/templates/wiki/page.html similarity index 100% rename from realms/templates/page/page.html rename to realms/templates/wiki/page.html diff --git a/requirements.txt b/requirements.txt index 31346d0..bfca8c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Flask==0.10.1 Flask-Assets==0.8 Flask-Bcrypt==0.5.2 Flask-Login==0.2.7 +Flask-Script==0.6.3 beautifulsoup4==4.3.2 boto==2.17.0 closure==20121212