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.

298 lines
9.6KB

  1. from __future__ import absolute_import
  2. import sys
  3. # Set default encoding to UTF-8
  4. reload(sys)
  5. # noinspection PyUnresolvedReferences
  6. sys.setdefaultencoding('utf-8')
  7. import functools
  8. import base64
  9. import time
  10. import json
  11. import traceback
  12. import six.moves.http_client as httplib
  13. from functools import update_wrapper
  14. import click
  15. from flask import Flask, request, render_template, url_for, redirect, g
  16. from flask_cache import Cache
  17. from flask_login import LoginManager, current_user, logout_user
  18. from flask_sqlalchemy import SQLAlchemy
  19. from flask_assets import Environment, Bundle
  20. from flask_ldap_login import LDAPLoginManager
  21. from werkzeug.routing import BaseConverter
  22. from werkzeug.exceptions import HTTPException
  23. from sqlalchemy.ext.declarative import declarative_base
  24. from realms.modules.search.models import Search
  25. from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url, to_dict, is_su, in_virtualenv
  26. from realms.lib.hook import HookModelMeta, HookMixin
  27. from realms.version import __version__
  28. class Application(Flask):
  29. def __call__(self, environ, start_response):
  30. path_info = environ.get('PATH_INFO')
  31. if path_info and len(path_info) > 1 and path_info.endswith('/'):
  32. environ['PATH_INFO'] = path_info[:-1]
  33. scheme = environ.get('HTTP_X_SCHEME')
  34. if scheme:
  35. environ['wsgi.url_scheme'] = scheme
  36. real_ip = environ.get('HTTP_X_REAL_IP')
  37. if real_ip:
  38. environ['REMOTE_ADDR'] = real_ip
  39. return super(Application, self).__call__(environ, start_response)
  40. def discover(self):
  41. import_name = 'realms.modules'
  42. fromlist = (
  43. 'assets',
  44. 'commands',
  45. 'models',
  46. 'views',
  47. 'hooks'
  48. )
  49. start_time = time.time()
  50. __import__(import_name, fromlist=fromlist)
  51. for module_name in self.config['MODULES']:
  52. sources = __import__('%s.%s' % (import_name, module_name), fromlist=fromlist)
  53. if hasattr(sources, 'init'):
  54. sources.init(self)
  55. # Blueprint
  56. if hasattr(sources, 'views'):
  57. self.register_blueprint(sources.views.blueprint, url_prefix=self.config['RELATIVE_PATH'])
  58. # Click
  59. if hasattr(sources, 'commands'):
  60. if sources.commands.cli.name == 'cli':
  61. sources.commands.cli.name = module_name
  62. cli.add_command(sources.commands.cli)
  63. # Hooks
  64. if hasattr(sources, 'hooks'):
  65. if hasattr(sources.hooks, 'before_request'):
  66. self.before_request(sources.hooks.before_request)
  67. if hasattr(sources.hooks, 'before_first_request'):
  68. self.before_first_request(sources.hooks.before_first_request)
  69. # print >> sys.stderr, ' * Ready in %.2fms' % (1000.0 * (time.time() - start_time))
  70. def make_response(self, rv):
  71. if rv is None:
  72. rv = '', httplib.NO_CONTENT
  73. elif not isinstance(rv, tuple):
  74. rv = rv,
  75. rv = list(rv)
  76. if isinstance(rv[0], (list, dict)):
  77. rv[0] = self.response_class(json.dumps(rv[0]), mimetype='application/json')
  78. return super(Application, self).make_response(tuple(rv))
  79. class Assets(Environment):
  80. default_filters = {'js': 'rjsmin', 'css': 'cleancss'}
  81. default_output = {'js': 'assets/%(version)s.js', 'css': 'assets/%(version)s.css'}
  82. def register(self, name, *args, **kwargs):
  83. ext = args[0].split('.')[-1]
  84. filters = kwargs.get('filters', self.default_filters[ext])
  85. output = kwargs.get('output', self.default_output[ext])
  86. return super(Assets, self).register(name, Bundle(*args, filters=filters, output=output))
  87. class MyLDAPLoginManager(LDAPLoginManager):
  88. @property
  89. def attrlist(self):
  90. # the parent method doesn't always work
  91. return None
  92. class RegexConverter(BaseConverter):
  93. """ Enables Regex matching on endpoints
  94. """
  95. def __init__(self, url_map, *items):
  96. super(RegexConverter, self).__init__(url_map)
  97. self.regex = items[0]
  98. def redirect_url(referrer=None):
  99. if not referrer:
  100. referrer = request.referrer
  101. return request.args.get('next') or referrer or url_for('index')
  102. def error_handler(e):
  103. try:
  104. if isinstance(e, HTTPException):
  105. status_code = e.code
  106. message = e.description if e.description != type(e).description else None
  107. tb = None
  108. else:
  109. status_code = httplib.INTERNAL_SERVER_ERROR
  110. message = None
  111. tb = traceback.format_exc() if current_user.admin else None
  112. if request.is_xhr or request.accept_mimetypes.best in ['application/json', 'text/javascript']:
  113. response = {
  114. 'message': message,
  115. 'traceback': tb
  116. }
  117. else:
  118. response = render_template('errors/error.html',
  119. title=httplib.responses[status_code],
  120. status_code=status_code,
  121. message=message,
  122. traceback=tb)
  123. except HTTPException as e2:
  124. return error_handler(e2)
  125. return response, status_code
  126. def create_app(config=None):
  127. app = Application(__name__)
  128. app.config.from_object('realms.config.conf')
  129. app.url_map.converters['regex'] = RegexConverter
  130. app.url_map.strict_slashes = False
  131. login_manager.init_app(app)
  132. db.init_app(app)
  133. cache.init_app(app)
  134. assets.init_app(app)
  135. search.init_app(app)
  136. ldap.init_app(app)
  137. db.Model = declarative_base(metaclass=HookModelMeta, cls=HookMixin)
  138. app.register_error_handler(HTTPException, error_handler)
  139. @app.before_request
  140. def init_g():
  141. g.assets = dict(css=['main.css'], js=['main.js'])
  142. @app.template_filter('datetime')
  143. def _jinja2_filter_datetime(ts, fmt=None):
  144. return time.strftime(
  145. fmt or app.config.get('DATETIME_FORMAT', '%b %d, %Y %I:%M %p'),
  146. time.localtime(ts)
  147. )
  148. @app.template_filter('b64encode')
  149. def _jinja2_filter_b64encode(s):
  150. return base64.urlsafe_b64encode(s).rstrip("=")
  151. @app.errorhandler(404)
  152. def page_not_found(e):
  153. return render_template('errors/404.html'), 404
  154. if app.config.get('RELATIVE_PATH'):
  155. @app.route("/")
  156. def root():
  157. return redirect(url_for(app.config.get('ROOT_ENDPOINT')))
  158. app.discover()
  159. # This will be removed at some point
  160. with app.app_context():
  161. if app.config.get('DB_URI'):
  162. db.metadata.create_all(db.get_engine(app))
  163. return app
  164. # Init plugins here if possible
  165. login_manager = LoginManager()
  166. db = SQLAlchemy()
  167. cache = Cache()
  168. assets = Assets()
  169. search = Search()
  170. ldap = MyLDAPLoginManager()
  171. assets.register('main.js',
  172. 'vendor/jquery/dist/jquery.js',
  173. 'vendor/components-bootstrap/js/bootstrap.js',
  174. 'vendor/handlebars/handlebars.js',
  175. 'vendor/js-yaml/dist/js-yaml.js',
  176. 'vendor/markdown-it/dist/markdown-it.js',
  177. 'vendor/markdown-it-anchor/index.0',
  178. 'js/html-sanitizer-minified.js', # don't minify?
  179. 'vendor/highlightjs/highlight.pack.js',
  180. 'vendor/parsleyjs/dist/parsley.js',
  181. 'vendor/datatables/media/js/jquery.dataTables.js',
  182. 'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.js',
  183. 'js/hbs-helpers.js',
  184. 'js/mdr.js',
  185. 'js/main.js')
  186. assets.register('main.css',
  187. 'vendor/bootswatch-dist/css/bootstrap.css',
  188. 'vendor/components-font-awesome/css/font-awesome.css',
  189. 'vendor/highlightjs/styles/github.css',
  190. 'vendor/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css',
  191. 'css/style.css')
  192. def with_appcontext(f):
  193. """Wraps a callback so that it's guaranteed to be executed with the
  194. script's application context. If callbacks are registered directly
  195. to the ``app.cli`` object then they are wrapped with this function
  196. by default unless it's disabled.
  197. """
  198. @click.pass_context
  199. def decorator(__ctx, *args, **kwargs):
  200. with create_app().app_context():
  201. return __ctx.invoke(f, *args, **kwargs)
  202. return update_wrapper(decorator, f)
  203. class AppGroup(click.Group):
  204. """This works similar to a regular click :class:`~click.Group` but it
  205. changes the behavior of the :meth:`command` decorator so that it
  206. automatically wraps the functions in :func:`with_appcontext`.
  207. Not to be confused with :class:`FlaskGroup`.
  208. """
  209. def command(self, *args, **kwargs):
  210. """This works exactly like the method of the same name on a regular
  211. :class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
  212. unless it's disabled by passing ``with_appcontext=False``.
  213. """
  214. wrap_for_ctx = kwargs.pop('with_appcontext', True)
  215. def decorator(f):
  216. if wrap_for_ctx:
  217. f = with_appcontext(f)
  218. return click.Group.command(self, *args, **kwargs)(f)
  219. return decorator
  220. def group(self, *args, **kwargs):
  221. """This works exactly like the method of the same name on a regular
  222. :class:`click.Group` but it defaults the group class to
  223. :class:`AppGroup`.
  224. """
  225. kwargs.setdefault('cls', AppGroup)
  226. return click.Group.group(self, *args, **kwargs)
  227. cli = AppGroup()
  228. # Decorator to be used in modules instead of click.group
  229. cli_group = functools.partial(click.group, cls=AppGroup)