c04585961f8066260c6695bbd0519442ec22dddf
[mediagoblin.git] / mediagoblin / app.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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
17 import os
18 import logging
19
20 from mediagoblin.routing import get_url_map
21 from mediagoblin.tools.routing import endpoint_to_controller
22
23 from werkzeug.wrappers import Request
24 from werkzeug.exceptions import HTTPException
25 from werkzeug.routing import RequestRedirect
26 from werkzeug.wsgi import SharedDataMiddleware
27
28 from mediagoblin import meddleware, __version__
29 from mediagoblin.db.util import check_db_up_to_date
30 from mediagoblin.tools import common, session, translate, template
31 from mediagoblin.tools.response import render_http_exception
32 from mediagoblin.tools.theme import register_themes
33 from mediagoblin.tools import request as mg_request
34 from mediagoblin.media_types.tools import media_type_warning
35 from mediagoblin.mg_globals import setup_globals
36 from mediagoblin.init.celery import setup_celery_from_config
37 from mediagoblin.init.plugins import setup_plugins
38 from mediagoblin.init import (get_jinja_loader, get_staticdirector,
39 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
40 setup_storage)
41 from mediagoblin.tools.pluginapi import PluginManager, hook_transform
42 from mediagoblin.tools.crypto import setup_crypto
43 from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
44
45 from mediagoblin.tools.transition import DISABLE_GLOBALS
46
47
48 _log = logging.getLogger(__name__)
49
50
51 class 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
64 class MediaGoblinApp(object):
65 """
66 WSGI application of MediaGoblin
67
68 ... this is the heart of the program!
69 """
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 """
80 _log.info("GNU MediaGoblin %s main server starting", __version__)
81 _log.debug("Using config file %s", config_path)
82 ##############
83 # Setup config
84 ##############
85
86 # Open and setup the config
87 global_config, app_config = setup_global_and_app_config(config_path)
88
89 media_type_warning()
90
91 setup_crypto()
92
93 ##########################################
94 # Setup other connections / useful objects
95 ##########################################
96
97 # Setup Session Manager, not needed in celery
98 self.session_manager = session.SessionManager()
99
100 # load all available locales
101 setup_locales()
102
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
108 # Set up the database
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'])
113
114 # Quit app if need to run dbupdate
115 ## NOTE: This is currently commented out due to session errors..
116 ## We'd like to re-enable!
117 # check_db_up_to_date()
118
119 # Register themes
120 self.theme_registry, self.current_theme = register_themes(app_config)
121
122 # Get the template environment
123 self.template_loader = get_jinja_loader(
124 app_config.get('local_templates'),
125 self.current_theme,
126 PluginManager().get_template_paths()
127 )
128
129 # Check if authentication plugin is enabled and respond accordingly.
130 self.auth = check_auth_enabled()
131 if not self.auth:
132 app_config['allow_comments'] = False
133
134 # Set up storage systems
135 self.public_store, self.queue_store = setup_storage()
136
137 # set up routing
138 self.url_map = get_url_map()
139
140 # set up staticdirector tool
141 self.staticdirector = get_staticdirector(app_config)
142
143 # Setup celery, if appropriate
144 if setup_celery and not app_config.get('celery_setup_elsewhere'):
145 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
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 #
155 # certain properties need to be accessed globally eg from
156 # validators, etc, which might not access to the request
157 # object.
158 #
159 # Note, we are trying to transition this out;
160 # run with environment variable DISABLE_GLOBALS=true
161 # to work on it
162 #######################################################
163
164 if not DISABLE_GLOBALS:
165 setup_globals(app=self)
166
167 # Workbench *currently* only used by celery, so this only
168 # matters in always eager mode :)
169 setup_workbench()
170
171 # instantiate application meddleware
172 self.meddleware = [common.import_component(m)(self)
173 for m in meddleware.ENABLED_MEDDLEWARE]
174
175 def gen_context(self, ctx=None):
176 """
177 Attach contextual information to request, or generate a context object
178
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 # --------------
185
186 # Is a context provided?
187 if ctx is None:
188 ctx = Context()
189
190 # Attach utilities
191 # ----------------
192
193 # Attach self as request.app
194 # Also attach a few utilities from request.app for convenience?
195 ctx.app = self
196
197 if not DISABLE_GLOBALS:
198 ctx.db = self.db
199
200 ctx.staticdirect = self.staticdirector
201
202 # Do special things if this is a request
203 # --------------------------------------
204 if isinstance(ctx, Request):
205 ctx = self._request_only_gen_context(ctx)
206
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)
216
217 request.locale = translate.get_locale_from_request(request)
218
219 # This should be moved over for certain, but how to deal with
220 # request.locale?
221 request.template_env = template.get_jinja_env(
222 self.template_loader, request.locale)
223
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
229 def build_proxy(endpoint, **kw):
230 try:
231 qualified = kw.pop('qualified')
232 except KeyError:
233 qualified = False
234
235 return request.map_adapter.build(
236 endpoint,
237 values=dict(**kw),
238 force_external=qualified)
239
240 request.urlgen = build_proxy
241
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
269 # Log user out if authentication_disabled
270 no_auth_logout(request)
271
272 request.controller_name = None
273 try:
274 found_rule, url_values = request.map_adapter.match(return_rule=True)
275 request.matchdict = url_values
276 except RequestRedirect as response:
277 # Deal with 301 responses eg due to missing final slash
278 return response(environ, start_response)
279 except HTTPException as exc:
280 # Stop and render exception
281 return render_http_exception(
282 request, exc,
283 exc.get_description(environ))(environ, start_response)
284
285 controller = endpoint_to_controller(found_rule)
286 # Make a reference to the controller's symbolic name on the request...
287 # used for lazy context modification
288 request.controller_name = found_rule.endpoint
289
290 ## TODO: get rid of meddleware, turn it into hooks only
291 # pass the request through our meddleware classes
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)
301
302 request = hook_transform("modify_request", request)
303
304 request.start_response = start_response
305
306 # get the Http response from the controller
307 try:
308 if DISABLE_GLOBALS:
309 with self.db_manager.session_scope() as request.db:
310 response = controller(request)
311 else:
312 response = controller(request)
313 except HTTPException as e:
314 response = render_http_exception(
315 request, e, e.get_description(environ))
316
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:
322 response = render_http_exception(
323 request, e, e.get_description(environ))
324
325 self.session_manager.save_session_to_cookie(
326 request.session,
327 request, response)
328
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
335 try:
336 return self.call_backend(environ, start_response)
337 finally:
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()
342
343
344 def paste_app_factory(global_config, **app_config):
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.")
354 del app_config['config']
355
356 mgoblin_app = MediaGoblinApp(mediagoblin_config)
357 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
358 exports=app_config)
359 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
360
361 return mgoblin_app
362
363
364 def 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)