Use urljoin to create proper feed media URLs
[larjonas-mediagoblin.git] / mediagoblin / app.py
blobb98469625e74cc732814b911e29a71ad64b96c55
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
17 import os
18 import logging
19 from contextlib import contextmanager
21 from mediagoblin.routing import get_url_map
22 from mediagoblin.tools.routing import endpoint_to_controller
24 from werkzeug.wrappers import Request
25 from werkzeug.exceptions import HTTPException
26 from werkzeug.routing import RequestRedirect
27 from werkzeug.wsgi import SharedDataMiddleware
29 from mediagoblin import meddleware, __version__
30 from mediagoblin.db.util import check_db_up_to_date
31 from mediagoblin.tools import common, session, translate, template
32 from mediagoblin.tools.response import render_http_exception
33 from mediagoblin.tools.theme import register_themes
34 from mediagoblin.tools import request as mg_request
35 from mediagoblin.media_types.tools import media_type_warning
36 from mediagoblin.mg_globals import setup_globals
37 from mediagoblin.init.celery import setup_celery_from_config
38 from mediagoblin.init.plugins import setup_plugins
39 from mediagoblin.init import (get_jinja_loader, get_staticdirector,
40 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
41 setup_storage)
42 from mediagoblin.tools.pluginapi import PluginManager, hook_transform
43 from mediagoblin.tools.crypto import setup_crypto
44 from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
46 from mediagoblin.tools.transition import DISABLE_GLOBALS
49 _log = logging.getLogger(__name__)
52 class Context(object):
53 """
54 MediaGoblin context object.
56 If a web request is being used, a Flask Request object is used
57 instead, otherwise (celery tasks, etc), attach things to this
58 object.
60 Usually appears as "ctx" in utilities as first argument.
61 """
62 pass
65 class MediaGoblinApp(object):
66 """
67 WSGI application of MediaGoblin
69 ... this is the heart of the program!
70 """
71 def __init__(self, config_path, setup_celery=True):
72 """
73 Initialize the application based on a configuration file.
75 Arguments:
76 - config_path: path to the configuration file we're opening.
77 - setup_celery: whether or not to setup celery during init.
78 (Note: setting 'celery_setup_elsewhere' also disables
79 setting up celery.)
80 """
81 _log.info("GNU MediaGoblin %s main server starting", __version__)
82 _log.debug("Using config file %s", config_path)
83 ##############
84 # Setup config
85 ##############
87 # Open and setup the config
88 self.global_config, self.app_config = setup_global_and_app_config(config_path)
90 media_type_warning()
92 setup_crypto(self.app_config)
94 ##########################################
95 # Setup other connections / useful objects
96 ##########################################
98 # Setup Session Manager, not needed in celery
99 self.session_manager = session.SessionManager()
101 # load all available locales
102 setup_locales()
104 # Set up plugins -- need to do this early so that plugins can
105 # affect startup.
106 _log.info("Setting up plugins.")
107 setup_plugins()
109 # Set up the database
110 if DISABLE_GLOBALS:
111 self.db_manager = setup_database(self)
112 else:
113 self.db = setup_database(self)
115 # Quit app if need to run dbupdate
116 ## NOTE: This is currently commented out due to session errors..
117 ## We'd like to re-enable!
118 # check_db_up_to_date()
120 # Register themes
121 self.theme_registry, self.current_theme = register_themes(self.app_config)
123 # Get the template environment
124 self.template_loader = get_jinja_loader(
125 self.app_config.get('local_templates'),
126 self.current_theme,
127 PluginManager().get_template_paths()
130 # Check if authentication plugin is enabled and respond accordingly.
131 self.auth = check_auth_enabled()
132 if not self.auth:
133 self.app_config['allow_comments'] = False
135 # Set up storage systems
136 self.public_store, self.queue_store = setup_storage()
138 # set up routing
139 self.url_map = get_url_map()
141 # set up staticdirector tool
142 self.staticdirector = get_staticdirector(self.app_config)
144 # Setup celery, if appropriate
145 if setup_celery and not self.app_config.get('celery_setup_elsewhere'):
146 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
147 setup_celery_from_config(
148 self.app_config, self.global_config,
149 force_celery_always_eager=True)
150 else:
151 setup_celery_from_config(self.app_config, self.global_config)
153 #######################################################
154 # Insert appropriate things into mediagoblin.mg_globals
156 # certain properties need to be accessed globally eg from
157 # validators, etc, which might not access to the request
158 # object.
160 # Note, we are trying to transition this out;
161 # run with environment variable DISABLE_GLOBALS=true
162 # to work on it
163 #######################################################
165 if not DISABLE_GLOBALS:
166 setup_globals(app=self)
168 # Workbench *currently* only used by celery, so this only
169 # matters in always eager mode :)
170 self.workbench_manager = setup_workbench()
172 # instantiate application meddleware
173 self.meddleware = [common.import_component(m)(self)
174 for m in meddleware.ENABLED_MEDDLEWARE]
176 @contextmanager
177 def gen_context(self, ctx=None, **kwargs):
179 Attach contextual information to request, or generate a context object
181 This avoids global variables; various utilities and contextual
182 information (current translation, etc) are attached to this
183 object.
185 if DISABLE_GLOBALS:
186 with self.db_manager.session_scope() as db:
187 yield self._gen_context(db, ctx)
188 else:
189 yield self._gen_context(self.db, ctx)
191 def _gen_context(self, db, ctx, **kwargs):
192 # Set up context
193 # --------------
195 # Is a context provided?
196 if ctx is None:
197 ctx = Context()
199 # Attach utilities
200 # ----------------
202 # Attach self as request.app
203 # Also attach a few utilities from request.app for convenience?
204 ctx.app = self
206 ctx.db = db
208 ctx.staticdirect = self.staticdirector
210 # Do special things if this is a request
211 # --------------------------------------
212 if isinstance(ctx, Request):
213 ctx = self._request_only_gen_context(ctx)
215 return ctx
217 def _request_only_gen_context(self, request):
219 Requests get some extra stuff attached to them that's not relevant
220 otherwise.
222 # Do we really want to load this via middleware? Maybe?
223 request.session = self.session_manager.load_session_from_cookie(request)
225 request.locale = translate.get_locale_from_request(request)
227 # This should be moved over for certain, but how to deal with
228 # request.locale?
229 request.template_env = template.get_jinja_env(
230 self, self.template_loader, request.locale)
232 mg_request.setup_user_in_request(request)
234 ## Routing / controller loading stuff
235 request.map_adapter = self.url_map.bind_to_environ(request.environ)
237 def build_proxy(endpoint, **kw):
238 try:
239 qualified = kw.pop('qualified')
240 except KeyError:
241 qualified = False
243 return request.map_adapter.build(
244 endpoint,
245 values=dict(**kw),
246 force_external=qualified)
248 request.urlgen = build_proxy
250 return request
252 def call_backend(self, environ, start_response):
253 request = Request(environ)
255 # Compatibility with django, use request.args preferrably
256 request.GET = request.args
258 # By using fcgi, mediagoblin can run under a base path
259 # like /mediagoblin/. request.path_info contains the
260 # path inside mediagoblin. If the something needs the
261 # full path of the current page, that should include
262 # the basepath.
263 # Note: urlgen and routes are fine!
264 request.full_path = environ["SCRIPT_NAME"] + request.path
265 # python-routes uses SCRIPT_NAME. So let's use that too.
266 # The other option would be:
267 # request.full_path = environ["SCRIPT_URL"]
269 # Fix up environ for urlgen
270 # See bug: https://bitbucket.org/bbangert/routes/issue/55/cache_hostinfo-breaks-on-https-off
271 if environ.get('HTTPS', '').lower() == 'off':
272 environ.pop('HTTPS')
274 ## Attach utilities to the request object
275 with self.gen_context(request) as request:
276 return self._finish_call_backend(request, environ, start_response)
278 def _finish_call_backend(self, request, environ, start_response):
279 # Log user out if authentication_disabled
280 no_auth_logout(request)
282 request.controller_name = None
283 try:
284 found_rule, url_values = request.map_adapter.match(return_rule=True)
285 request.matchdict = url_values
286 except RequestRedirect as response:
287 # Deal with 301 responses eg due to missing final slash
288 return response(environ, start_response)
289 except HTTPException as exc:
290 # Stop and render exception
291 return render_http_exception(
292 request, exc,
293 exc.get_description(environ))(environ, start_response)
295 controller = endpoint_to_controller(found_rule)
296 # Make a reference to the controller's symbolic name on the request...
297 # used for lazy context modification
298 request.controller_name = found_rule.endpoint
300 ## TODO: get rid of meddleware, turn it into hooks only
301 # pass the request through our meddleware classes
302 try:
303 for m in self.meddleware:
304 response = m.process_request(request, controller)
305 if response is not None:
306 return response(environ, start_response)
307 except HTTPException as e:
308 return render_http_exception(
309 request, e,
310 e.get_description(environ))(environ, start_response)
312 request = hook_transform("modify_request", request)
314 request.start_response = start_response
316 # get the Http response from the controller
317 try:
318 response = controller(request)
319 except HTTPException as e:
320 response = render_http_exception(
321 request, e, e.get_description(environ))
323 # pass the response through the meddlewares
324 try:
325 for m in self.meddleware[::-1]:
326 m.process_response(request, response)
327 except HTTPException as e:
328 response = render_http_exception(
329 request, e, e.get_description(environ))
331 self.session_manager.save_session_to_cookie(
332 request.session,
333 request, response)
335 return response(environ, start_response)
337 def __call__(self, environ, start_response):
338 ## If more errors happen that look like unclean sessions:
339 # self.db.check_session_clean()
341 try:
342 return self.call_backend(environ, start_response)
343 finally:
344 if not DISABLE_GLOBALS:
345 # Reset the sql session, so that the next request
346 # gets a fresh session
347 self.db.reset_after_request()
350 def paste_app_factory(global_config, **app_config):
351 configs = app_config['config'].split()
352 mediagoblin_config = None
353 for config in configs:
354 if os.path.exists(config) and os.access(config, os.R_OK):
355 mediagoblin_config = config
356 break
358 if not mediagoblin_config:
359 raise IOError("Usable mediagoblin config not found.")
360 del app_config['config']
362 mgoblin_app = MediaGoblinApp(mediagoblin_config)
363 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
364 exports=app_config)
365 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
367 return mgoblin_app
370 def paste_server_selector(wsgi_app, global_config=None, **app_config):
372 Select between gunicorn and paste depending on what ia available
374 # See if we can import the gunicorn server...
375 # otherwise we'll use the paste server
376 try:
377 import gunicorn
378 except ImportError:
379 gunicorn = None
381 if gunicorn is None:
382 # use paste
383 from paste.httpserver import server_runner
385 cleaned_app_config = dict(
386 [(key, app_config[key])
387 for key in app_config
388 if key in ["host", "port", "handler", "ssl_pem", "ssl_context",
389 "server_version", "protocol_version", "start_loop",
390 "daemon_threads", "socket_timeout", "use_threadpool",
391 "threadpool_workers", "threadpool_options",
392 "request_queue_size"]])
394 return server_runner(wsgi_app, global_config, **cleaned_app_config)
395 else:
396 # use gunicorn
397 from gunicorn.app.pasterapp import PasterServerApplication
398 return PasterServerApplication(wsgi_app, global_config, **app_config)