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