docs: Add chapter on upgrading, inc. system Python upgrades [#972].
[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
a4768df0 19from contextlib import contextmanager
31a8ff42 20
3d914332
E
21from mediagoblin.routing import get_url_map
22from mediagoblin.tools.routing import endpoint_to_controller
7742dcc1 23
f1d06e1d 24from werkzeug.wrappers import Request
e5e2c5e7 25from werkzeug.exceptions import HTTPException
fd61aac7 26from werkzeug.routing import RequestRedirect
ad3a0aea
BS
27try:
28 # Werkzeug >= 0.15.0
29 from werkzeug.middleware.shared_data import SharedDataMiddleware
30except ImportError:
31 from werkzeug.wsgi import SharedDataMiddleware
7742dcc1 32from mediagoblin import meddleware, __version__
26583b2c 33from mediagoblin.db.util import check_db_up_to_date
c7424612 34from mediagoblin.tools import common, session, translate, template
785b287f 35from mediagoblin.tools.response import render_http_exception
828fc630 36from mediagoblin.tools.theme import register_themes
152a3bfa 37from mediagoblin.tools import request as mg_request
c81186dd 38from mediagoblin.media_types.tools import media_type_warning
6e7ce8d1 39from mediagoblin.mg_globals import setup_globals
073b61fe 40from mediagoblin.init.celery import setup_celery_from_config
29b6f917 41from mediagoblin.init.plugins import setup_plugins
50854db0 42from mediagoblin.init import (get_jinja_loader, get_staticdirector,
6ef75af5 43 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
9e1fa239 44 setup_storage)
c5d8d301 45from mediagoblin.tools.pluginapi import PluginManager, hook_transform
5907154a 46from mediagoblin.tools.crypto import setup_crypto
c9dec8b3 47from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
90e342f9 48
b88ca698
CAW
49from mediagoblin.tools.transition import DISABLE_GLOBALS
50
31a8ff42 51
ec97c937
E
52_log = logging.getLogger(__name__)
53
54
d7149900
CAW
55class 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
8e1e744d 68class MediaGoblinApp(object):
31a8ff42 69 """
3f5cf663
CAW
70 WSGI application of MediaGoblin
71
72 ... this is the heart of the program!
31a8ff42 73 """
3f5cf663
CAW
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 """
ec97c937 84 _log.info("GNU MediaGoblin %s main server starting", __version__)
3f369674 85 _log.debug("Using config file %s", config_path)
3f5cf663
CAW
86 ##############
87 # Setup config
88 ##############
89
90 # Open and setup the config
b8e2ab2f 91 self.global_config, self.app_config = setup_global_and_app_config(config_path)
3f5cf663 92
c81186dd
RE
93 media_type_warning()
94
b8e2ab2f 95 setup_crypto(self.app_config)
5907154a 96
3f5cf663
CAW
97 ##########################################
98 # Setup other connections / useful objects
99 ##########################################
100
b0ee3aae
E
101 # Setup Session Manager, not needed in celery
102 self.session_manager = session.SessionManager()
103
6ef75af5
SS
104 # load all available locales
105 setup_locales()
106
29b6f917
WKG
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
3f5cf663 112 # Set up the database
c060353e 113 if DISABLE_GLOBALS:
7c563e91 114 self.db_manager = setup_database(self)
c060353e 115 else:
7c563e91 116 self.db = setup_database(self)
ff94114c 117
26583b2c 118 # Quit app if need to run dbupdate
31f8909f
CAW
119 ## NOTE: This is currently commented out due to session errors..
120 ## We'd like to re-enable!
121 # check_db_up_to_date()
26583b2c 122
828fc630 123 # Register themes
b8e2ab2f 124 self.theme_registry, self.current_theme = register_themes(self.app_config)
828fc630 125
5afdd7a1 126 # Get the template environment
42ef819c 127 self.template_loader = get_jinja_loader(
b8e2ab2f 128 self.app_config.get('local_templates'),
8545dd50 129 self.current_theme,
05e007c1 130 PluginManager().get_template_paths()
8545dd50 131 )
0c8a30e6 132
744f1c83
RE
133 # Check if authentication plugin is enabled and respond accordingly.
134 self.auth = check_auth_enabled()
1bce0c15 135 if not self.auth:
b8e2ab2f 136 self.app_config['allow_comments'] = False
744f1c83 137
5afdd7a1 138 # Set up storage systems
dccef262 139 self.public_store, self.queue_store = setup_storage()
5afdd7a1
CAW
140
141 # set up routing
48cf435d 142 self.url_map = get_url_map()
31a8ff42 143
582c4d5f 144 # set up staticdirector tool
b8e2ab2f 145 self.staticdirector = get_staticdirector(self.app_config)
3f5cf663
CAW
146
147 # Setup celery, if appropriate
b8e2ab2f 148 if setup_celery and not self.app_config.get('celery_setup_elsewhere'):
d9a31a39 149 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
3f5cf663 150 setup_celery_from_config(
b8e2ab2f 151 self.app_config, self.global_config,
3f5cf663
CAW
152 force_celery_always_eager=True)
153 else:
b8e2ab2f 154 setup_celery_from_config(self.app_config, self.global_config)
3f5cf663
CAW
155
156 #######################################################
157 # Insert appropriate things into mediagoblin.mg_globals
158 #
df9809c2
CAW
159 # certain properties need to be accessed globally eg from
160 # validators, etc, which might not access to the request
161 # object.
b88ca698
CAW
162 #
163 # Note, we are trying to transition this out;
164 # run with environment variable DISABLE_GLOBALS=true
165 # to work on it
3f5cf663
CAW
166 #######################################################
167
b88ca698
CAW
168 if not DISABLE_GLOBALS:
169 setup_globals(app=self)
1fd97db3
CAW
170
171 # Workbench *currently* only used by celery, so this only
172 # matters in always eager mode :)
b8e2ab2f 173 self.workbench_manager = setup_workbench()
df9809c2 174
ce5ae8da
CAW
175 # instantiate application meddleware
176 self.meddleware = [common.import_component(m)(self)
177 for m in meddleware.ENABLED_MEDDLEWARE]
0c8a30e6 178
a4768df0 179 @contextmanager
b8e2ab2f 180 def gen_context(self, ctx=None, **kwargs):
d7149900
CAW
181 """
182 Attach contextual information to request, or generate a context object
f1d06e1d 183
d7149900
CAW
184 This avoids global variables; various utilities and contextual
185 information (current translation, etc) are attached to this
186 object.
187 """
a4768df0
CAW
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
b8e2ab2f 194 def _gen_context(self, db, ctx, **kwargs):
d7149900
CAW
195 # Set up context
196 # --------------
31a8ff42 197
d7149900 198 # Is a context provided?
b88ca698 199 if ctx is None:
d7149900
CAW
200 ctx = Context()
201
202 # Attach utilities
203 # ----------------
871fc591 204
3d0557bf
CAW
205 # Attach self as request.app
206 # Also attach a few utilities from request.app for convenience?
d7149900 207 ctx.app = self
0c8a30e6 208
a4768df0 209 ctx.db = db
c060353e 210
d7149900
CAW
211 ctx.staticdirect = self.staticdirector
212
b88ca698
CAW
213 # Do special things if this is a request
214 # --------------------------------------
215 if isinstance(ctx, Request):
216 ctx = self._request_only_gen_context(ctx)
217
d7149900
CAW
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)
3d0557bf 227
1ec7ff2a 228 request.locale = translate.get_locale_from_request(request)
d7149900
CAW
229
230 # This should be moved over for certain, but how to deal with
231 # request.locale?
1ec7ff2a 232 request.template_env = template.get_jinja_env(
753cfc3b 233 self, self.template_loader, request.locale)
7742dcc1 234
d7149900
CAW
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
7742dcc1
JW
240 def build_proxy(endpoint, **kw):
241 try:
242 qualified = kw.pop('qualified')
243 except KeyError:
244 qualified = False
245
d7149900 246 return request.map_adapter.build(
7742dcc1
JW
247 endpoint,
248 values=dict(**kw),
249 force_external=qualified)
250
251 request.urlgen = build_proxy
252
d7149900
CAW
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
a4768df0
CAW
278 with self.gen_context(request) as request:
279 return self._finish_call_backend(request, environ, start_response)
d7149900 280
a4768df0 281 def _finish_call_backend(self, request, environ, start_response):
5101c469 282 # Log user out if authentication_disabled
c9dec8b3
RE
283 no_auth_logout(request)
284
f7a5c7c7 285 request.controller_name = None
1ec7ff2a 286 try:
d7149900 287 found_rule, url_values = request.map_adapter.match(return_rule=True)
1ec7ff2a 288 request.matchdict = url_values
fd61aac7
SS
289 except RequestRedirect as response:
290 # Deal with 301 responses eg due to missing final slash
291 return response(environ, start_response)
1ec7ff2a 292 except HTTPException as exc:
785b287f
SS
293 # Stop and render exception
294 return render_http_exception(
295 request, exc,
296 exc.get_description(environ))(environ, start_response)
1ec7ff2a 297
05501c57 298 controller = endpoint_to_controller(found_rule)
98dacfe6 299 # Make a reference to the controller's symbolic name on the request...
38103094 300 # used for lazy context modification
98dacfe6 301 request.controller_name = found_rule.endpoint
91cf6738 302
d7149900 303 ## TODO: get rid of meddleware, turn it into hooks only
91cf6738 304 # pass the request through our meddleware classes
785b287f
SS
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)
91cf6738 314
b1fbf67e
CAW
315 request = hook_transform("modify_request", request)
316
31a8ff42
CAW
317 request.start_response = start_response
318
785b287f
SS
319 # get the Http response from the controller
320 try:
a4768df0 321 response = controller(request)
785b287f
SS
322 except HTTPException as e:
323 response = render_http_exception(
324 request, e, e.get_description(environ))
0c8a30e6 325
785b287f
SS
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:
6a28bc4e 331 response = render_http_exception(
785b287f 332 request, e, e.get_description(environ))
0c8a30e6 333
d7149900
CAW
334 self.session_manager.save_session_to_cookie(
335 request.session,
336 request, response)
c7424612 337
e824570a
E
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
2bc8ff0d 344 try:
e824570a
E
345 return self.call_backend(environ, start_response)
346 finally:
c060353e
CAW
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()
31a8ff42
CAW
351
352
5784c4e9 353def paste_app_factory(global_config, **app_config):
91903aa6
CAW
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.")
19baab1b 363 del app_config['config']
91903aa6
CAW
364
365 mgoblin_app = MediaGoblinApp(mediagoblin_config)
19baab1b
BP
366 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
367 exports=app_config)
c5d8d301 368 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
f3f53028 369
c4d71564 370 return mgoblin_app