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