app.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.app
  4. ~~~~~~~~~~~
  5. manages the app creation and configuration process
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import os
  10. import logging
  11. import time
  12. from functools import partial
  13. from sqlalchemy import event
  14. from sqlalchemy.engine import Engine
  15. from sqlalchemy.exc import OperationalError
  16. from flask import Flask, request
  17. from flask_login import current_user
  18. from flaskbb._compat import string_types
  19. # views
  20. from flaskbb.user.views import user
  21. from flaskbb.message.views import message
  22. from flaskbb.auth.views import auth
  23. from flaskbb.management.views import management
  24. from flaskbb.forum.views import forum
  25. # models
  26. from flaskbb.user.models import User, Guest
  27. # extensions
  28. from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
  29. db, debugtoolbar, limiter, login_manager, mail,
  30. redis_store, themes, whooshee)
  31. # various helpers
  32. from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
  33. crop_title, is_online, mark_online,
  34. forum_is_unread, topic_is_unread,
  35. render_template, render_markup,
  36. app_config_from_env)
  37. from flaskbb.utils.translations import FlaskBBDomain
  38. # permission checks (here they are used for the jinja filters)
  39. from flaskbb.utils.requirements import (IsAdmin, IsAtleastModerator,
  40. CanBanUser, CanEditUser,
  41. TplCanModerate, TplCanDeletePost,
  42. TplCanDeleteTopic, TplCanEditPost,
  43. TplCanPostTopic, TplCanPostReply)
  44. # whooshees
  45. from flaskbb.utils.search import (PostWhoosheer, TopicWhoosheer,
  46. ForumWhoosheer, UserWhoosheer)
  47. # app specific configurations
  48. from flaskbb.utils.settings import flaskbb_config
  49. from flaskbb.plugins.models import PluginRegistry
  50. from flaskbb.plugins.manager import FlaskBBPluginManager
  51. from flaskbb.plugins import spec
  52. def create_app(config=None):
  53. """Creates the app.
  54. :param config: The configuration file or object.
  55. The environment variable is weightet as the heaviest.
  56. For example, if the config is specified via an file
  57. and a ENVVAR, it will load the config via the file and
  58. later overwrite it from the ENVVAR.
  59. """
  60. app = Flask("flaskbb")
  61. configure_app(app, config)
  62. configure_celery_app(app, celery)
  63. configure_extensions(app)
  64. load_plugins(app)
  65. configure_blueprints(app)
  66. configure_template_filters(app)
  67. configure_context_processors(app)
  68. configure_before_handlers(app)
  69. configure_errorhandlers(app)
  70. configure_logging(app)
  71. app.pluggy.hook.flaskbb_additional_setup(app=app, pluggy=app.pluggy)
  72. return app
  73. def configure_app(app, config):
  74. """Configures FlaskBB."""
  75. # Use the default config and override it afterwards
  76. app.config.from_object('flaskbb.configs.default.DefaultConfig')
  77. if isinstance(config, string_types) and \
  78. os.path.exists(os.path.abspath(config)):
  79. config = os.path.abspath(config)
  80. app.config.from_pyfile(config)
  81. else:
  82. # try to update the config from the object
  83. app.config.from_object(config)
  84. # Add the location of the config to the config
  85. app.config["CONFIG_PATH"] = config
  86. # try to update the config via the environment variable
  87. app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
  88. # Parse the env for FLASKBB_ prefixed env variables and set
  89. # them on the config object
  90. app_config_from_env(app, prefix="FLASKBB_")
  91. app.pluggy = FlaskBBPluginManager('flaskbb', implprefix='flaskbb_')
  92. def configure_celery_app(app, celery):
  93. """Configures the celery app."""
  94. app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})
  95. celery.conf.update(app.config)
  96. TaskBase = celery.Task
  97. class ContextTask(TaskBase):
  98. abstract = True
  99. def __call__(self, *args, **kwargs):
  100. with app.app_context():
  101. return TaskBase.__call__(self, *args, **kwargs)
  102. celery.Task = ContextTask
  103. def configure_blueprints(app):
  104. app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
  105. app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
  106. app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
  107. app.register_blueprint(
  108. management, url_prefix=app.config["ADMIN_URL_PREFIX"]
  109. )
  110. app.register_blueprint(
  111. message, url_prefix=app.config["MESSAGE_URL_PREFIX"]
  112. )
  113. app.pluggy.hook.flaskbb_load_blueprints(app=app)
  114. def configure_extensions(app):
  115. """Configures the extensions."""
  116. # Flask-WTF CSRF
  117. csrf.init_app(app)
  118. # Flask-SQLAlchemy
  119. db.init_app(app)
  120. # Flask-Alembic
  121. alembic.init_app(app, command_name="db")
  122. # Flask-Mail
  123. mail.init_app(app)
  124. # Flask-Cache
  125. cache.init_app(app)
  126. # Flask-Debugtoolbar
  127. debugtoolbar.init_app(app)
  128. # Flask-Themes
  129. themes.init_themes(app, app_identifier="flaskbb")
  130. # Flask-And-Redis
  131. redis_store.init_app(app)
  132. # Flask-Limiter
  133. limiter.init_app(app)
  134. # Flask-Whooshee
  135. whooshee.init_app(app)
  136. # not needed for unittests - and it will speed up testing A LOT
  137. if not app.testing:
  138. whooshee.register_whoosheer(PostWhoosheer)
  139. whooshee.register_whoosheer(TopicWhoosheer)
  140. whooshee.register_whoosheer(ForumWhoosheer)
  141. whooshee.register_whoosheer(UserWhoosheer)
  142. # Flask-Login
  143. login_manager.login_view = app.config["LOGIN_VIEW"]
  144. login_manager.refresh_view = app.config["REAUTH_VIEW"]
  145. login_manager.login_message_category = app.config["LOGIN_MESSAGE_CATEGORY"]
  146. login_manager.needs_refresh_message_category = \
  147. app.config["REFRESH_MESSAGE_CATEGORY"]
  148. login_manager.anonymous_user = Guest
  149. @login_manager.user_loader
  150. def load_user(user_id):
  151. """Loads the user. Required by the `login` extension."""
  152. user_instance = User.query.filter_by(id=user_id).first()
  153. if user_instance:
  154. return user_instance
  155. else:
  156. return None
  157. login_manager.init_app(app)
  158. # Flask-BabelEx
  159. babel.init_app(app=app, default_domain=FlaskBBDomain(app))
  160. @babel.localeselector
  161. def get_locale():
  162. # if a user is logged in, use the locale from the user settings
  163. if current_user and \
  164. current_user.is_authenticated and current_user.language:
  165. return current_user.language
  166. # otherwise we will just fallback to the default language
  167. return flaskbb_config["DEFAULT_LANGUAGE"]
  168. # Flask-Allows
  169. allows.init_app(app)
  170. allows.identity_loader(lambda: current_user)
  171. def configure_template_filters(app):
  172. """Configures the template filters."""
  173. filters = {}
  174. filters['markup'] = render_markup
  175. filters['format_date'] = format_date
  176. filters['time_since'] = time_since
  177. filters['is_online'] = is_online
  178. filters['crop_title'] = crop_title
  179. filters['forum_is_unread'] = forum_is_unread
  180. filters['topic_is_unread'] = topic_is_unread
  181. permissions = [
  182. ('is_admin', IsAdmin),
  183. ('is_moderator', IsAtleastModerator),
  184. ('is_admin_or_moderator', IsAtleastModerator),
  185. ('can_edit_user', CanEditUser),
  186. ('can_ban_user', CanBanUser),
  187. ]
  188. filters.update([
  189. (name, partial(perm, request=request)) for name, perm in permissions
  190. ])
  191. # these create closures
  192. filters['can_moderate'] = TplCanModerate(request)
  193. filters['post_reply'] = TplCanPostReply(request)
  194. filters['edit_post'] = TplCanEditPost(request)
  195. filters['delete_post'] = TplCanDeletePost(request)
  196. filters['post_topic'] = TplCanPostTopic(request)
  197. filters['delete_topic'] = TplCanDeleteTopic(request)
  198. app.jinja_env.filters.update(filters)
  199. app.pluggy.hook.flaskbb_jinja_directives(app=app)
  200. def configure_context_processors(app):
  201. """Configures the context processors."""
  202. @app.context_processor
  203. def inject_flaskbb_config():
  204. """Injects the ``flaskbb_config`` config variable into the
  205. templates.
  206. """
  207. return dict(flaskbb_config=flaskbb_config)
  208. def configure_before_handlers(app):
  209. """Configures the before request handlers."""
  210. @app.before_request
  211. def update_lastseen():
  212. """Updates `lastseen` before every reguest if the user is
  213. authenticated."""
  214. if current_user.is_authenticated:
  215. current_user.lastseen = time_utcnow()
  216. db.session.add(current_user)
  217. db.session.commit()
  218. if app.config["REDIS_ENABLED"]:
  219. @app.before_request
  220. def mark_current_user_online():
  221. if current_user.is_authenticated:
  222. mark_online(current_user.username)
  223. else:
  224. mark_online(request.remote_addr, guest=True)
  225. app.pluggy.hook.flaskbb_request_processors(app=app)
  226. def configure_errorhandlers(app):
  227. """Configures the error handlers."""
  228. @app.errorhandler(403)
  229. def forbidden_page(error):
  230. return render_template("errors/forbidden_page.html"), 403
  231. @app.errorhandler(404)
  232. def page_not_found(error):
  233. return render_template("errors/page_not_found.html"), 404
  234. @app.errorhandler(500)
  235. def server_error_page(error):
  236. return render_template("errors/server_error.html"), 500
  237. app.pluggy.hook.flaskbb_errorhandlers(app=app)
  238. def configure_logging(app):
  239. """Configures logging."""
  240. logs_folder = os.path.join(app.root_path, os.pardir, "logs")
  241. from logging.handlers import SMTPHandler
  242. formatter = logging.Formatter(
  243. '%(asctime)s %(levelname)s: %(message)s '
  244. '[in %(pathname)s:%(lineno)d]')
  245. info_log = os.path.join(logs_folder, app.config['INFO_LOG'])
  246. info_file_handler = logging.handlers.RotatingFileHandler(
  247. info_log,
  248. maxBytes=100000,
  249. backupCount=10
  250. )
  251. info_file_handler.setLevel(logging.INFO)
  252. info_file_handler.setFormatter(formatter)
  253. app.logger.addHandler(info_file_handler)
  254. error_log = os.path.join(logs_folder, app.config['ERROR_LOG'])
  255. error_file_handler = logging.handlers.RotatingFileHandler(
  256. error_log,
  257. maxBytes=100000,
  258. backupCount=10
  259. )
  260. error_file_handler.setLevel(logging.ERROR)
  261. error_file_handler.setFormatter(formatter)
  262. app.logger.addHandler(error_file_handler)
  263. if app.config["SEND_LOGS"]:
  264. mail_handler = \
  265. SMTPHandler(
  266. app.config['MAIL_SERVER'],
  267. app.config['MAIL_DEFAULT_SENDER'],
  268. app.config['ADMINS'],
  269. 'application error, no admins specified',
  270. (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
  271. )
  272. mail_handler.setLevel(logging.ERROR)
  273. mail_handler.setFormatter(formatter)
  274. app.logger.addHandler(mail_handler)
  275. if app.config["SQLALCHEMY_ECHO"]:
  276. # Ref: http://stackoverflow.com/a/8428546
  277. @event.listens_for(Engine, "before_cursor_execute")
  278. def before_cursor_execute(conn, cursor, statement,
  279. parameters, context, executemany):
  280. conn.info.setdefault('query_start_time', []).append(time.time())
  281. @event.listens_for(Engine, "after_cursor_execute")
  282. def after_cursor_execute(conn, cursor, statement,
  283. parameters, context, executemany):
  284. total = time.time() - conn.info['query_start_time'].pop(-1)
  285. app.logger.debug("Total Time: %f", total)
  286. def load_plugins(app):
  287. app.pluggy.add_hookspecs(spec)
  288. try:
  289. with app.app_context():
  290. plugins = PluginRegistry.query.all()
  291. except OperationalError:
  292. return
  293. for plugin in plugins:
  294. if not plugin.enabled:
  295. app.pluggy.set_blocked(plugin.name)
  296. app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
  297. app.pluggy.hook.flaskbb_extensions(app=app)
  298. loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
  299. registered_names = set([p.name for p in plugins])
  300. unregistered = [
  301. PluginRegistry(name=name) for name in loaded_names - registered_names
  302. ]
  303. with app.app_context():
  304. db.session.add_all(unregistered)
  305. db.session.commit()
  306. app.logger.debug(
  307. "Plugins Found: {}".format(app.pluggy.list_name_plugin())
  308. )
  309. app.logger.debug(
  310. "Disabled Plugins: {}".format(app.pluggy.list_disabled_plugins())
  311. )