Give our sqlalchemy objects access to their own session
[mediagoblin.git] / mediagoblin / app.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
e5572c60
ML
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
571198c9 17import os
ec97c937 18import logging
31a8ff42 19
3d914332
E
20from mediagoblin.routing import get_url_map
21from mediagoblin.tools.routing import endpoint_to_controller
7742dcc1 22
f1d06e1d 23from werkzeug.wrappers import Request
e5e2c5e7 24from werkzeug.exceptions import HTTPException
fd61aac7 25from werkzeug.routing import RequestRedirect
19baab1b 26from werkzeug.wsgi import SharedDataMiddleware
31a8ff42 27
7742dcc1 28from mediagoblin import meddleware, __version__
26583b2c 29from mediagoblin.db.util import check_db_up_to_date
c7424612 30from mediagoblin.tools import common, session, translate, template
785b287f 31from mediagoblin.tools.response import render_http_exception
828fc630 32from mediagoblin.tools.theme import register_themes
152a3bfa 33from mediagoblin.tools import request as mg_request
c81186dd 34from mediagoblin.media_types.tools import media_type_warning
6e7ce8d1 35from mediagoblin.mg_globals import setup_globals
073b61fe 36from mediagoblin.init.celery import setup_celery_from_config
29b6f917 37from mediagoblin.init.plugins import setup_plugins
50854db0 38from mediagoblin.init import (get_jinja_loader, get_staticdirector,
6ef75af5 39 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
9e1fa239 40 setup_storage)
c5d8d301 41from mediagoblin.tools.pluginapi import PluginManager, hook_transform
5907154a 42from mediagoblin.tools.crypto import setup_crypto
c9dec8b3 43from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
90e342f9 44
31a8ff42 45
ec97c937
E
46_log = logging.getLogger(__name__)
47
48
d7149900
CAW
49class Context(object):
50 """
51 MediaGoblin context object.
52
53 If a web request is being used, a Flask Request object is used
54 instead, otherwise (celery tasks, etc), attach things to this
55 object.
56
57 Usually appears as "ctx" in utilities as first argument.
58 """
59 pass
60
61
8e1e744d 62class MediaGoblinApp(object):
31a8ff42 63 """
3f5cf663
CAW
64 WSGI application of MediaGoblin
65
66 ... this is the heart of the program!
31a8ff42 67 """
3f5cf663
CAW
68 def __init__(self, config_path, setup_celery=True):
69 """
70 Initialize the application based on a configuration file.
71
72 Arguments:
73 - config_path: path to the configuration file we're opening.
74 - setup_celery: whether or not to setup celery during init.
75 (Note: setting 'celery_setup_elsewhere' also disables
76 setting up celery.)
77 """
ec97c937 78 _log.info("GNU MediaGoblin %s main server starting", __version__)
3f369674 79 _log.debug("Using config file %s", config_path)
3f5cf663
CAW
80 ##############
81 # Setup config
82 ##############
83
84 # Open and setup the config
fe289be4 85 global_config, app_config = setup_global_and_app_config(config_path)
3f5cf663 86
c81186dd
RE
87 media_type_warning()
88
5907154a
E
89 setup_crypto()
90
3f5cf663
CAW
91 ##########################################
92 # Setup other connections / useful objects
93 ##########################################
94
b0ee3aae
E
95 # Setup Session Manager, not needed in celery
96 self.session_manager = session.SessionManager()
97
6ef75af5
SS
98 # load all available locales
99 setup_locales()
100
29b6f917
WKG
101 # Set up plugins -- need to do this early so that plugins can
102 # affect startup.
103 _log.info("Setting up plugins.")
104 setup_plugins()
105
3f5cf663 106 # Set up the database
4a698535 107 self.db = setup_database(app_config['run_migrations'])
ff94114c 108
26583b2c 109 # Quit app if need to run dbupdate
31f8909f
CAW
110 ## NOTE: This is currently commented out due to session errors..
111 ## We'd like to re-enable!
112 # check_db_up_to_date()
26583b2c 113
828fc630 114 # Register themes
975be468 115 self.theme_registry, self.current_theme = register_themes(app_config)
828fc630 116
5afdd7a1 117 # Get the template environment
42ef819c 118 self.template_loader = get_jinja_loader(
3b47da8e 119 app_config.get('local_templates'),
8545dd50 120 self.current_theme,
05e007c1 121 PluginManager().get_template_paths()
8545dd50 122 )
0c8a30e6 123
744f1c83
RE
124 # Check if authentication plugin is enabled and respond accordingly.
125 self.auth = check_auth_enabled()
1bce0c15
RE
126 if not self.auth:
127 app_config['allow_comments'] = False
744f1c83 128
5afdd7a1 129 # Set up storage systems
dccef262 130 self.public_store, self.queue_store = setup_storage()
5afdd7a1
CAW
131
132 # set up routing
48cf435d 133 self.url_map = get_url_map()
31a8ff42 134
582c4d5f 135 # set up staticdirector tool
c85c9dc7 136 self.staticdirector = get_staticdirector(app_config)
3f5cf663
CAW
137
138 # Setup celery, if appropriate
139 if setup_celery and not app_config.get('celery_setup_elsewhere'):
d9a31a39 140 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
3f5cf663
CAW
141 setup_celery_from_config(
142 app_config, global_config,
143 force_celery_always_eager=True)
144 else:
145 setup_celery_from_config(app_config, global_config)
146
147 #######################################################
148 # Insert appropriate things into mediagoblin.mg_globals
149 #
df9809c2
CAW
150 # certain properties need to be accessed globally eg from
151 # validators, etc, which might not access to the request
152 # object.
3f5cf663
CAW
153 #######################################################
154
243c3843 155 setup_globals(app=self)
1fd97db3
CAW
156
157 # Workbench *currently* only used by celery, so this only
158 # matters in always eager mode :)
7664b4db 159 setup_workbench()
df9809c2 160
ce5ae8da
CAW
161 # instantiate application meddleware
162 self.meddleware = [common.import_component(m)(self)
163 for m in meddleware.ENABLED_MEDDLEWARE]
0c8a30e6 164
d7149900
CAW
165 def gen_context(self, ctx=None):
166 """
167 Attach contextual information to request, or generate a context object
f1d06e1d 168
d7149900
CAW
169 This avoids global variables; various utilities and contextual
170 information (current translation, etc) are attached to this
171 object.
172 """
173 # Set up context
174 # --------------
31a8ff42 175
d7149900
CAW
176 # Is a context provided?
177 if ctx is not None:
178 # Do special things if this is a request
179 if isinstance(ctx, Request):
180 ctx = self._request_only_gen_context(ctx)
05788ef4 181
d7149900
CAW
182 else:
183 ctx = Context()
184
185 # Attach utilities
186 # ----------------
871fc591 187
3d0557bf
CAW
188 # Attach self as request.app
189 # Also attach a few utilities from request.app for convenience?
d7149900 190 ctx.app = self
0c8a30e6 191
d7149900
CAW
192 ctx.db = self.db
193 ctx.staticdirect = self.staticdirector
194
195 return ctx
196
197 def _request_only_gen_context(self, request):
198 """
199 Requests get some extra stuff attached to them that's not relevant
200 otherwise.
201 """
202 # Do we really want to load this via middleware? Maybe?
203 request.session = self.session_manager.load_session_from_cookie(request)
3d0557bf 204
1ec7ff2a 205 request.locale = translate.get_locale_from_request(request)
d7149900
CAW
206
207 # This should be moved over for certain, but how to deal with
208 # request.locale?
1ec7ff2a
JW
209 request.template_env = template.get_jinja_env(
210 self.template_loader, request.locale)
7742dcc1 211
d7149900
CAW
212 mg_request.setup_user_in_request(request)
213
214 ## Routing / controller loading stuff
215 request.map_adapter = self.url_map.bind_to_environ(request.environ)
216
7742dcc1
JW
217 def build_proxy(endpoint, **kw):
218 try:
219 qualified = kw.pop('qualified')
220 except KeyError:
221 qualified = False
222
d7149900 223 return request.map_adapter.build(
7742dcc1
JW
224 endpoint,
225 values=dict(**kw),
226 force_external=qualified)
227
228 request.urlgen = build_proxy
229
d7149900
CAW
230 return request
231
232 def call_backend(self, environ, start_response):
233 request = Request(environ)
234
235 # Compatibility with django, use request.args preferrably
236 request.GET = request.args
237
238 # By using fcgi, mediagoblin can run under a base path
239 # like /mediagoblin/. request.path_info contains the
240 # path inside mediagoblin. If the something needs the
241 # full path of the current page, that should include
242 # the basepath.
243 # Note: urlgen and routes are fine!
244 request.full_path = environ["SCRIPT_NAME"] + request.path
245 # python-routes uses SCRIPT_NAME. So let's use that too.
246 # The other option would be:
247 # request.full_path = environ["SCRIPT_URL"]
248
249 # Fix up environ for urlgen
250 # See bug: https://bitbucket.org/bbangert/routes/issue/55/cache_hostinfo-breaks-on-https-off
251 if environ.get('HTTPS', '').lower() == 'off':
252 environ.pop('HTTPS')
253
254 ## Attach utilities to the request object
255 request = self.gen_context(request)
256
5101c469 257 # Log user out if authentication_disabled
c9dec8b3
RE
258 no_auth_logout(request)
259
f7a5c7c7 260 request.controller_name = None
1ec7ff2a 261 try:
d7149900 262 found_rule, url_values = request.map_adapter.match(return_rule=True)
1ec7ff2a 263 request.matchdict = url_values
fd61aac7
SS
264 except RequestRedirect as response:
265 # Deal with 301 responses eg due to missing final slash
266 return response(environ, start_response)
1ec7ff2a 267 except HTTPException as exc:
785b287f
SS
268 # Stop and render exception
269 return render_http_exception(
270 request, exc,
271 exc.get_description(environ))(environ, start_response)
1ec7ff2a 272
05501c57 273 controller = endpoint_to_controller(found_rule)
98dacfe6 274 # Make a reference to the controller's symbolic name on the request...
38103094 275 # used for lazy context modification
98dacfe6 276 request.controller_name = found_rule.endpoint
91cf6738 277
d7149900 278 ## TODO: get rid of meddleware, turn it into hooks only
91cf6738 279 # pass the request through our meddleware classes
785b287f
SS
280 try:
281 for m in self.meddleware:
282 response = m.process_request(request, controller)
283 if response is not None:
284 return response(environ, start_response)
285 except HTTPException as e:
286 return render_http_exception(
287 request, e,
288 e.get_description(environ))(environ, start_response)
91cf6738 289
b1fbf67e
CAW
290 request = hook_transform("modify_request", request)
291
31a8ff42
CAW
292 request.start_response = start_response
293
785b287f
SS
294 # get the Http response from the controller
295 try:
296 response = controller(request)
297 except HTTPException as e:
298 response = render_http_exception(
299 request, e, e.get_description(environ))
0c8a30e6 300
785b287f
SS
301 # pass the response through the meddlewares
302 try:
303 for m in self.meddleware[::-1]:
304 m.process_response(request, response)
305 except HTTPException as e:
6a28bc4e 306 response = render_http_exception(
785b287f 307 request, e, e.get_description(environ))
0c8a30e6 308
d7149900
CAW
309 self.session_manager.save_session_to_cookie(
310 request.session,
311 request, response)
c7424612 312
e824570a
E
313 return response(environ, start_response)
314
315 def __call__(self, environ, start_response):
316 ## If more errors happen that look like unclean sessions:
317 # self.db.check_session_clean()
318
2bc8ff0d 319 try:
e824570a
E
320 return self.call_backend(environ, start_response)
321 finally:
322 # Reset the sql session, so that the next request
323 # gets a fresh session
2bc8ff0d 324 self.db.reset_after_request()
31a8ff42
CAW
325
326
5784c4e9 327def paste_app_factory(global_config, **app_config):
91903aa6
CAW
328 configs = app_config['config'].split()
329 mediagoblin_config = None
330 for config in configs:
331 if os.path.exists(config) and os.access(config, os.R_OK):
332 mediagoblin_config = config
333 break
334
335 if not mediagoblin_config:
336 raise IOError("Usable mediagoblin config not found.")
19baab1b 337 del app_config['config']
91903aa6
CAW
338
339 mgoblin_app = MediaGoblinApp(mediagoblin_config)
19baab1b
BP
340 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
341 exports=app_config)
c5d8d301 342 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
f3f53028 343
c4d71564 344 return mgoblin_app
227a81b5
CAW
345
346
347def paste_server_selector(wsgi_app, global_config=None, **app_config):
348 """
349 Select between gunicorn and paste depending on what ia available
350 """
351 # See if we can import the gunicorn server...
352 # otherwise we'll use the paste server
353 try:
354 import gunicorn
355 except ImportError:
356 gunicorn = None
357
358 if gunicorn is None:
359 # use paste
360 from paste.httpserver import server_runner
361
362 cleaned_app_config = dict(
363 [(key, app_config[key])
364 for key in app_config
365 if key in ["host", "port", "handler", "ssl_pem", "ssl_context",
366 "server_version", "protocol_version", "start_loop",
367 "daemon_threads", "socket_timeout", "use_threadpool",
368 "threadpool_workers", "threadpool_options",
369 "request_queue_size"]])
370
371 return server_runner(wsgi_app, global_config, **cleaned_app_config)
372 else:
373 # use gunicorn
374 from gunicorn.app.pasterapp import PasterServerApplication
375 return PasterServerApplication(wsgi_app, global_config, **app_config)