app.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  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 logging
  10. import logging.config
  11. import os
  12. import sys
  13. import time
  14. from datetime import datetime
  15. from functools import partial
  16. from flask import Flask, request
  17. from flask_login import current_user
  18. from sqlalchemy import event
  19. from sqlalchemy.engine import Engine
  20. from sqlalchemy.exc import OperationalError, ProgrammingError
  21. from flaskbb._compat import iteritems, string_types
  22. from flaskbb.auth.views import auth
  23. # extensions
  24. from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
  25. db, debugtoolbar, limiter, login_manager, mail,
  26. redis_store, themes, whooshee)
  27. from flaskbb.forum.views import forum
  28. from flaskbb.management.views import management
  29. from flaskbb.message.views import message
  30. from flaskbb.plugins import spec
  31. from flaskbb.plugins.manager import FlaskBBPluginManager
  32. from flaskbb.plugins.models import PluginRegistry
  33. from flaskbb.plugins.utils import remove_zombie_plugins_from_db, template_hook
  34. # models
  35. from flaskbb.user.models import Guest, User
  36. # views
  37. from flaskbb.user.views import user
  38. # various helpers
  39. from flaskbb.utils.helpers import (app_config_from_env, crop_title,
  40. format_date, forum_is_unread,
  41. get_alembic_locations, get_flaskbb_config,
  42. is_online, mark_online, render_markup,
  43. render_template, time_since, time_utcnow,
  44. topic_is_unread)
  45. # permission checks (here they are used for the jinja filters)
  46. from flaskbb.utils.requirements import (CanBanUser, CanEditUser, IsAdmin,
  47. IsAtleastModerator, TplCanDeletePost,
  48. TplCanDeleteTopic, TplCanEditPost,
  49. TplCanModerate, TplCanPostReply,
  50. TplCanPostTopic)
  51. # whooshees
  52. from flaskbb.utils.search import (ForumWhoosheer, PostWhoosheer,
  53. TopicWhoosheer, UserWhoosheer)
  54. # app specific configurations
  55. from flaskbb.utils.settings import flaskbb_config
  56. from flaskbb.utils.translations import FlaskBBDomain
  57. logger = logging.getLogger(__name__)
  58. def create_app(config=None, instance_path=None):
  59. """Creates the app.
  60. :param instance_path: An alternative instance path for the application.
  61. By default the folder ``'instance'`` next to the
  62. package or module is assumed to be the instance
  63. path.
  64. See :ref:`Instance Folders <flask:instance-folders>`.
  65. :param config: The configuration file or object.
  66. The environment variable is weightet as the heaviest.
  67. For example, if the config is specified via an file
  68. and a ENVVAR, it will load the config via the file and
  69. later overwrite it from the ENVVAR.
  70. """
  71. app = Flask(
  72. "flaskbb",
  73. instance_path=instance_path,
  74. instance_relative_config=True
  75. )
  76. # instance folders are not automatically created by flask
  77. if not os.path.exists(app.instance_path):
  78. os.makedirs(app.instance_path)
  79. configure_app(app, config)
  80. configure_celery_app(app, celery)
  81. configure_extensions(app)
  82. load_plugins(app)
  83. configure_blueprints(app)
  84. configure_template_filters(app)
  85. configure_context_processors(app)
  86. configure_before_handlers(app)
  87. configure_errorhandlers(app)
  88. configure_migrations(app)
  89. configure_translations(app)
  90. app.pluggy.hook.flaskbb_additional_setup(app=app, pluggy=app.pluggy)
  91. return app
  92. def configure_app(app, config):
  93. """Configures FlaskBB."""
  94. # Use the default config and override it afterwards
  95. app.config.from_object('flaskbb.configs.default.DefaultConfig')
  96. config = get_flaskbb_config(app, config)
  97. # Path
  98. if isinstance(config, string_types):
  99. app.config.from_pyfile(config)
  100. # Module
  101. else:
  102. # try to update the config from the object
  103. app.config.from_object(config)
  104. # Add the location of the config to the config
  105. app.config["CONFIG_PATH"] = config
  106. # Environment
  107. # Get config file from envvar
  108. app.config.from_envvar("FLASKBB_SETTINGS", silent=True)
  109. # Parse the env for FLASKBB_ prefixed env variables and set
  110. # them on the config object
  111. app_config_from_env(app, prefix="FLASKBB_")
  112. # Setting up logging as early as possible
  113. configure_logging(app)
  114. if not isinstance(config, string_types) and config is not None:
  115. config_name = "{}.{}".format(config.__module__, config.__name__)
  116. else:
  117. config_name = config
  118. logger.info("Using config from: {}".format(config_name))
  119. app.pluggy = FlaskBBPluginManager('flaskbb', implprefix='flaskbb_')
  120. def configure_celery_app(app, celery):
  121. """Configures the celery app."""
  122. app.config.update({'BROKER_URL': app.config["CELERY_BROKER_URL"]})
  123. celery.conf.update(app.config)
  124. TaskBase = celery.Task
  125. class ContextTask(TaskBase):
  126. def __call__(self, *args, **kwargs):
  127. with app.app_context():
  128. return TaskBase.__call__(self, *args, **kwargs)
  129. celery.Task = ContextTask
  130. def configure_blueprints(app):
  131. app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])
  132. app.register_blueprint(user, url_prefix=app.config["USER_URL_PREFIX"])
  133. app.register_blueprint(auth, url_prefix=app.config["AUTH_URL_PREFIX"])
  134. app.register_blueprint(
  135. management, url_prefix=app.config["ADMIN_URL_PREFIX"]
  136. )
  137. app.register_blueprint(
  138. message, url_prefix=app.config["MESSAGE_URL_PREFIX"]
  139. )
  140. app.pluggy.hook.flaskbb_load_blueprints(app=app)
  141. def configure_extensions(app):
  142. """Configures the extensions."""
  143. # Flask-WTF CSRF
  144. csrf.init_app(app)
  145. # Flask-SQLAlchemy
  146. db.init_app(app)
  147. # Flask-Alembic
  148. alembic.init_app(app, command_name="db")
  149. # Flask-Mail
  150. mail.init_app(app)
  151. # Flask-Cache
  152. cache.init_app(app)
  153. # Flask-Debugtoolbar
  154. debugtoolbar.init_app(app)
  155. # Flask-Themes
  156. themes.init_themes(app, app_identifier="flaskbb")
  157. # Flask-And-Redis
  158. redis_store.init_app(app)
  159. # Flask-Limiter
  160. limiter.init_app(app)
  161. # Flask-Whooshee
  162. whooshee.init_app(app)
  163. # not needed for unittests - and it will speed up testing A LOT
  164. if not app.testing:
  165. whooshee.register_whoosheer(PostWhoosheer)
  166. whooshee.register_whoosheer(TopicWhoosheer)
  167. whooshee.register_whoosheer(ForumWhoosheer)
  168. whooshee.register_whoosheer(UserWhoosheer)
  169. # Flask-Login
  170. login_manager.login_view = app.config["LOGIN_VIEW"]
  171. login_manager.refresh_view = app.config["REAUTH_VIEW"]
  172. login_manager.login_message_category = app.config["LOGIN_MESSAGE_CATEGORY"]
  173. login_manager.needs_refresh_message_category = \
  174. app.config["REFRESH_MESSAGE_CATEGORY"]
  175. login_manager.anonymous_user = Guest
  176. @login_manager.user_loader
  177. def load_user(user_id):
  178. """Loads the user. Required by the `login` extension."""
  179. user_instance = User.query.filter_by(id=user_id).first()
  180. if user_instance:
  181. return user_instance
  182. else:
  183. return None
  184. login_manager.init_app(app)
  185. # Flask-Allows
  186. allows.init_app(app)
  187. allows.identity_loader(lambda: current_user)
  188. def configure_template_filters(app):
  189. """Configures the template filters."""
  190. filters = {}
  191. filters['markup'] = render_markup
  192. filters['format_date'] = format_date
  193. filters['time_since'] = time_since
  194. filters['is_online'] = is_online
  195. filters['crop_title'] = crop_title
  196. filters['forum_is_unread'] = forum_is_unread
  197. filters['topic_is_unread'] = topic_is_unread
  198. permissions = [
  199. ('is_admin', IsAdmin),
  200. ('is_moderator', IsAtleastModerator),
  201. ('is_admin_or_moderator', IsAtleastModerator),
  202. ('can_edit_user', CanEditUser),
  203. ('can_ban_user', CanBanUser),
  204. ]
  205. filters.update([
  206. (name, partial(perm, request=request)) for name, perm in permissions
  207. ])
  208. # these create closures
  209. filters['can_moderate'] = TplCanModerate(request)
  210. filters['post_reply'] = TplCanPostReply(request)
  211. filters['edit_post'] = TplCanEditPost(request)
  212. filters['delete_post'] = TplCanDeletePost(request)
  213. filters['post_topic'] = TplCanPostTopic(request)
  214. filters['delete_topic'] = TplCanDeleteTopic(request)
  215. app.jinja_env.filters.update(filters)
  216. app.jinja_env.globals["run_hook"] = template_hook
  217. app.pluggy.hook.flaskbb_jinja_directives(app=app)
  218. def configure_context_processors(app):
  219. """Configures the context processors."""
  220. @app.context_processor
  221. def inject_flaskbb_config():
  222. """Injects the ``flaskbb_config`` config variable into the
  223. templates.
  224. """
  225. return dict(flaskbb_config=flaskbb_config, format_date=format_date)
  226. @app.context_processor
  227. def inject_now():
  228. """Injects the current time."""
  229. return dict(now=datetime.utcnow())
  230. def configure_before_handlers(app):
  231. """Configures the before request handlers."""
  232. @app.before_request
  233. def update_lastseen():
  234. """Updates `lastseen` before every reguest if the user is
  235. authenticated."""
  236. if current_user.is_authenticated:
  237. current_user.lastseen = time_utcnow()
  238. db.session.add(current_user)
  239. db.session.commit()
  240. if app.config["REDIS_ENABLED"]:
  241. @app.before_request
  242. def mark_current_user_online():
  243. if current_user.is_authenticated:
  244. mark_online(current_user.username)
  245. else:
  246. mark_online(request.remote_addr, guest=True)
  247. app.pluggy.hook.flaskbb_request_processors(app=app)
  248. def configure_errorhandlers(app):
  249. """Configures the error handlers."""
  250. @app.errorhandler(403)
  251. def forbidden_page(error):
  252. return render_template("errors/forbidden_page.html"), 403
  253. @app.errorhandler(404)
  254. def page_not_found(error):
  255. return render_template("errors/page_not_found.html"), 404
  256. @app.errorhandler(500)
  257. def server_error_page(error):
  258. return render_template("errors/server_error.html"), 500
  259. app.pluggy.hook.flaskbb_errorhandlers(app=app)
  260. def configure_migrations(app):
  261. """Configure migrations."""
  262. plugin_dirs = app.pluggy.hook.flaskbb_load_migrations()
  263. version_locations = get_alembic_locations(plugin_dirs)
  264. app.config['ALEMBIC']['version_locations'] = version_locations
  265. def configure_translations(app):
  266. """Configure translations."""
  267. # we have to initialize the extension after we have loaded the plugins
  268. # because we of the 'flaskbb_load_translations' hook
  269. babel.init_app(app=app, default_domain=FlaskBBDomain(app))
  270. @babel.localeselector
  271. def get_locale():
  272. # if a user is logged in, use the locale from the user settings
  273. if current_user and \
  274. current_user.is_authenticated and current_user.language:
  275. return current_user.language
  276. # otherwise we will just fallback to the default language
  277. return flaskbb_config["DEFAULT_LANGUAGE"]
  278. def configure_logging(app):
  279. """Configures logging."""
  280. if app.config.get('USE_DEFAULT_LOGGING'):
  281. configure_default_logging(app)
  282. if app.config.get('LOG_CONF_FILE'):
  283. logging.config.fileConfig(
  284. app.config['LOG_CONF_FILE'], disable_existing_loggers=False
  285. )
  286. if app.config["SQLALCHEMY_ECHO"]:
  287. # Ref: http://stackoverflow.com/a/8428546
  288. @event.listens_for(Engine, "before_cursor_execute")
  289. def before_cursor_execute(
  290. conn, cursor, statement, parameters, context, executemany
  291. ):
  292. conn.info.setdefault('query_start_time', []).append(time.time())
  293. @event.listens_for(Engine, "after_cursor_execute")
  294. def after_cursor_execute(
  295. conn, cursor, statement, parameters, context, executemany
  296. ):
  297. total = time.time() - conn.info['query_start_time'].pop(-1)
  298. app.logger.debug("Total Time: %f", total)
  299. def configure_default_logging(app):
  300. # TODO: Remove this once Flask 0.13 is released
  301. app.config["LOGGER_NAME"] = "flask.app"
  302. # Load default logging config
  303. logging.config.dictConfig(app.config["LOG_DEFAULT_CONF"])
  304. if app.config["SEND_LOGS"]:
  305. configure_mail_logs(app)
  306. def configure_mail_logs(app, formatter):
  307. from logging.handlers import SMTPHandler
  308. formatter = logging.Formatter(
  309. "%(asctime)s %(levelname)-7s %(name)-25s %(message)s"
  310. )
  311. mail_handler = SMTPHandler(
  312. app.config['MAIL_SERVER'], app.config['MAIL_DEFAULT_SENDER'],
  313. app.config['ADMINS'], 'application error, no admins specified',
  314. (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
  315. )
  316. mail_handler.setLevel(logging.ERROR)
  317. mail_handler.setFormatter(formatter)
  318. app.logger.addHandler(mail_handler)
  319. def load_plugins(app):
  320. app.pluggy.add_hookspecs(spec)
  321. # have to find all the flaskbb modules that are loaded this way
  322. # otherwise sys.modules might change while we're iterating it
  323. # because of imports and that makes Python very unhappy
  324. # we are not interested in duplicated plugins or invalid ones
  325. # ('None' - appears on py2) and thus using a set
  326. flaskbb_modules = set(
  327. module for name, module in iteritems(sys.modules)
  328. if name.startswith('flaskbb')
  329. )
  330. for module in flaskbb_modules:
  331. app.pluggy.register(module, internal=True)
  332. try:
  333. with app.app_context():
  334. plugins = PluginRegistry.query.all()
  335. except (OperationalError, ProgrammingError) as exc:
  336. logger.debug("Database is not setup correctly or has not been "
  337. "setup yet.", exc_info=exc)
  338. return
  339. for plugin in plugins:
  340. if not plugin.enabled:
  341. app.pluggy.set_blocked(plugin.name)
  342. app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
  343. app.pluggy.hook.flaskbb_extensions(app=app)
  344. loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
  345. registered_names = set([p.name for p in plugins])
  346. unregistered = [
  347. PluginRegistry(name=name) for name in loaded_names - registered_names
  348. # ignore internal FlaskBB modules
  349. if not name.startswith('flaskbb.') and name != 'flaskbb'
  350. ]
  351. with app.app_context():
  352. db.session.add_all(unregistered)
  353. db.session.commit()
  354. removed = 0
  355. if app.config["REMOVE_DEAD_PLUGINS"]:
  356. removed = remove_zombie_plugins_from_db()
  357. logger.info("Removed Plugins: {}".format(removed))
  358. # we need a copy of it because of
  359. # RuntimeError: dictionary changed size during iteration
  360. tasks = celery.tasks.copy()
  361. disabled_plugins = [
  362. p.__package__ for p in app.pluggy.get_disabled_plugins()
  363. ]
  364. for task_name, task in iteritems(tasks):
  365. if task.__module__.split(".")[0] in disabled_plugins:
  366. logger.debug("Unregistering task: '{}'".format(task))
  367. celery.tasks.unregister(task_name)