Git based wiki inspired by Gollum
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
9.9KB

  1. import logging
  2. import os
  3. import time
  4. from threading import Lock
  5. import rethinkdb as rdb
  6. from flask import Flask, request, render_template, url_for, redirect, flash
  7. from flask.ext.login import LoginManager, login_required
  8. from flask.ext.assets import Environment, Bundle
  9. from recaptcha.client import captcha
  10. from werkzeug.routing import BaseConverter
  11. from session import RedisSessionInterface
  12. import config
  13. from wiki import Wiki
  14. from util import to_canonical, remove_ext, mkdir_safe, gravatar_url
  15. from models import Site, User, CurrentUser
  16. from ratelimit import get_view_rate_limit, ratelimiter
  17. from services import db
  18. # Flask instance container
  19. instances = {}
  20. # Flask extension objects
  21. login_manager = LoginManager()
  22. assets = Environment()
  23. class SubdomainDispatcher(object):
  24. """
  25. Application factory
  26. """
  27. def __init__(self, domain, create_app):
  28. self.domain = domain
  29. self.create_app = create_app
  30. self.lock = Lock()
  31. def get_application(self, host):
  32. host = host.split(':')[0]
  33. assert host.endswith(self.domain), 'Configuration error'
  34. subdomain = host[:-len(self.domain)].rstrip('.')
  35. with self.lock:
  36. app = instances.get(subdomain)
  37. if app is None:
  38. app = self.create_app(subdomain)
  39. instances[subdomain] = app
  40. return app
  41. def __call__(self, environ, start_response):
  42. app = self.get_application(environ['HTTP_HOST'])
  43. return app(environ, start_response)
  44. def init_db(dbname):
  45. """
  46. Assures DB has minimal setup
  47. """
  48. if not dbname in rdb.db_list().run(db):
  49. print "Creating DB %s" % dbname
  50. rdb.db_create(dbname).run(db)
  51. for tbl in ['sites', 'users', 'pages']:
  52. if not tbl in rdb.table_list().run(db):
  53. rdb.table_create(tbl).run(db)
  54. if not 'name' in rdb.table('sites').index_list().run(db):
  55. rdb.table('sites').index_create('name').run(db)
  56. for i in ['username', 'email']:
  57. if not i in rdb.table('users').index_list().run(db):
  58. rdb.table('users').index_create(i).run(db)
  59. s = Site()
  60. if not s.get_by_name('_'):
  61. s.create(name='_', repo='_')
  62. class RegexConverter(BaseConverter):
  63. """
  64. Enables Regex matching on endpoints
  65. """
  66. def __init__(self, url_map, *items):
  67. super(RegexConverter, self).__init__(url_map)
  68. self.regex = items[0]
  69. def redirect_url(referrer=None):
  70. if not referrer:
  71. referrer = request.referrer
  72. return request.args.get('next') or referrer or url_for('index')
  73. def validate_captcha():
  74. response = captcha.submit(
  75. request.form['recaptcha_challenge_field'],
  76. request.form['recaptcha_response_field'],
  77. config.flask['RECAPTCHA_PRIVATE_KEY'],
  78. request.remote_addr)
  79. return response.is_valid
  80. def format_subdomain(s):
  81. s = s.lower()
  82. s = to_canonical(s)
  83. if s in ['www']:
  84. # Not allowed
  85. s = ""
  86. return s
  87. def make_app(subdomain):
  88. subdomain = format_subdomain(subdomain)
  89. if subdomain and not Wiki.is_registered(subdomain):
  90. return redirect("http://%s/_new/?site=%s" % (config.hostname, subdomain))
  91. return create_app(subdomain)
  92. def create_app(subdomain=None):
  93. app = Flask(__name__)
  94. app.config.update(config.flask)
  95. app.debug = (config.ENV is not 'PROD')
  96. app.secret_key = config.secret_key
  97. app.static_path = os.sep + 'static'
  98. app.session_interface = RedisSessionInterface()
  99. app.url_map.converters['regex'] = RegexConverter
  100. login_manager.init_app(app)
  101. login_manager.login_view = 'login'
  102. @login_manager.user_loader
  103. def load_user(user_id):
  104. return CurrentUser(user_id)
  105. assets.init_app(app)
  106. if 'js_common' not in assets._named_bundles:
  107. js = Bundle('vendor/jquery/jquery.js',
  108. 'vendor/components-underscore/underscore.js',
  109. 'vendor/components-bootstrap/js/bootstrap.js',
  110. 'vendor/handlebars/handlebars.js',
  111. 'vendor/showdown/src/showdown.js',
  112. 'js/html-sanitizer-minified.js',
  113. 'js/wmd.js',
  114. 'vendor/highlightjs/highlight.pack.js',
  115. filters='uglifyjs', output='packed-common.js')
  116. assets.register('js_common', js)
  117. if 'js_editor' not in assets._named_bundles:
  118. js = Bundle('js/ace/ace.js',
  119. 'js/ace/mode-markdown.js',
  120. 'vendor/keymaster/keymaster.js',
  121. 'js/dillinger.js',
  122. filters='uglifyjs', output='packed-editor.js')
  123. assets.register('js_editor', js)
  124. repo_dir = config.repos['dir']
  125. repo_name = subdomain if subdomain else "_"
  126. w = Wiki(repo_dir + "/" + repo_name)
  127. @app.after_request
  128. def inject_x_rate_headers(response):
  129. limit = get_view_rate_limit()
  130. if limit and limit.send_x_headers:
  131. h = response.headers
  132. h.add('X-RateLimit-Remaining', str(limit.remaining))
  133. h.add('X-RateLimit-Limit', str(limit.limit))
  134. h.add('X-RateLimit-Reset', str(limit.reset))
  135. return response
  136. @app.template_filter('datetime')
  137. def _jinja2_filter_datetime(ts):
  138. return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts))
  139. @app.errorhandler(404)
  140. def page_not_found(e):
  141. return render_template('errors/404.html'), 404
  142. @app.errorhandler(500)
  143. def page_error(e):
  144. logging.exception(e)
  145. return render_template('errors/500.html'), 500
  146. @app.route("/")
  147. @ratelimiter(limit=50, per=60)
  148. def root():
  149. return render('home')
  150. #return redirect('/home')
  151. @app.route("/account/")
  152. @login_required
  153. def account():
  154. return render_template('account/index.html')
  155. @app.route("/_new/", methods=['GET', 'POST'])
  156. @login_required
  157. def new_wiki():
  158. if request.method == 'POST':
  159. wiki_name = to_canonical(request.form['name'])
  160. if Wiki.is_registered(wiki_name):
  161. flash("Site already exists")
  162. return redirect(redirect_url())
  163. else:
  164. s = Site()
  165. s.create(name=wiki_name, repo=wiki_name, founder=CurrentUser.get('id'))
  166. instances.pop(wiki_name, None)
  167. return redirect('http://%s.%s' % (wiki_name, config.hostname))
  168. else:
  169. return render_template('_new/index.html')
  170. @app.route("/logout/")
  171. def logout():
  172. User.logout()
  173. return redirect(url_for('root'))
  174. @app.route("/commit/<sha>/<name>")
  175. def commit_sha(name, sha):
  176. cname = to_canonical(name)
  177. data = w.get_page(cname, sha=sha)
  178. if data:
  179. return render_template('page/page.html', page=data)
  180. else:
  181. return redirect('/create/'+cname)
  182. @app.route("/compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
  183. def compare(name, fsha, dots, lsha):
  184. diff = w.compare(name, fsha, lsha)
  185. return render_template('page/compare.html', name=name, diff=diff)
  186. @app.route("/register", methods=['GET', 'POST'])
  187. def register():
  188. if request.method == 'POST':
  189. if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')):
  190. return redirect(url_for('root'))
  191. else:
  192. # Login failed
  193. return redirect(url_for('register'))
  194. else:
  195. return render_template('account/register.html')
  196. @app.route("/login", methods=['GET', 'POST'])
  197. def login():
  198. if request.method == 'POST':
  199. if User.auth(request.form['email'], request.form['password']):
  200. return redirect(redirect_url(referrer=url_for('root')))
  201. else:
  202. flash("Email or Password invalid")
  203. return redirect("/login")
  204. else:
  205. return render_template('account/login.html')
  206. @app.route("/history/<name>")
  207. def history(name):
  208. history = w.get_history(name)
  209. return render_template('page/history.html', name=name, history=history)
  210. @app.route("/edit/<name>", methods=['GET', 'POST'])
  211. @login_required
  212. def edit(name):
  213. data = w.get_page(name)
  214. cname = to_canonical(name)
  215. if request.method == 'POST':
  216. edit_cname = to_canonical(request.form['name'])
  217. if edit_cname.lower() != cname.lower():
  218. w.rename_page(cname, edit_cname)
  219. w.write_page(edit_cname, request.form['content'], message=request.form['message'],
  220. username=CurrentUser.get('username'))
  221. return redirect("/" + edit_cname)
  222. else:
  223. if data:
  224. name = remove_ext(data['name'])
  225. content = data['data']
  226. return render_template('page/edit.html', name=name, content=content)
  227. else:
  228. return redirect('/create/'+cname)
  229. @app.route("/delete/<name>", methods=['POST'])
  230. @login_required
  231. def delete(name):
  232. pass
  233. @app.route("/create/", methods=['GET', 'POST'])
  234. @app.route("/create/<name>", methods=['GET', 'POST'])
  235. @login_required
  236. def create(name=None):
  237. cname = ""
  238. if name:
  239. cname = to_canonical(name)
  240. if w.get_page(cname):
  241. # Page exists, edit instead
  242. return redirect("/edit/" + cname)
  243. if request.method == 'POST':
  244. w.write_page(request.form['name'], request.form['content'], message=request.form['message'], create=True)
  245. return redirect("/" + cname)
  246. else:
  247. return render_template('page/edit.html', name=cname, content="")
  248. @app.route("/<name>")
  249. def render(name):
  250. cname = to_canonical(name)
  251. if cname != name:
  252. return redirect('/' + cname)
  253. data = w.get_page(cname)
  254. if data:
  255. return render_template('page/page.html', name=cname, page=data)
  256. else:
  257. return redirect('/create/'+cname)
  258. return app