Commit | Line | Data |
---|---|---|
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 | 17 | import os |
ec97c937 | 18 | import logging |
31a8ff42 | 19 | |
3d914332 E |
20 | from mediagoblin.routing import get_url_map |
21 | from mediagoblin.tools.routing import endpoint_to_controller | |
7742dcc1 | 22 | |
f1d06e1d | 23 | from werkzeug.wrappers import Request |
e5e2c5e7 | 24 | from werkzeug.exceptions import HTTPException |
fd61aac7 | 25 | from werkzeug.routing import RequestRedirect |
19baab1b | 26 | from werkzeug.wsgi import SharedDataMiddleware |
31a8ff42 | 27 | |
7742dcc1 | 28 | from mediagoblin import meddleware, __version__ |
26583b2c | 29 | from mediagoblin.db.util import check_db_up_to_date |
c7424612 | 30 | from mediagoblin.tools import common, session, translate, template |
785b287f | 31 | from mediagoblin.tools.response import render_http_exception |
828fc630 | 32 | from mediagoblin.tools.theme import register_themes |
152a3bfa | 33 | from mediagoblin.tools import request as mg_request |
c81186dd | 34 | from mediagoblin.media_types.tools import media_type_warning |
6e7ce8d1 | 35 | from mediagoblin.mg_globals import setup_globals |
073b61fe | 36 | from mediagoblin.init.celery import setup_celery_from_config |
29b6f917 | 37 | from mediagoblin.init.plugins import setup_plugins |
50854db0 | 38 | from 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 | 41 | from mediagoblin.tools.pluginapi import PluginManager, hook_transform |
5907154a | 42 | from mediagoblin.tools.crypto import setup_crypto |
c9dec8b3 | 43 | from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout |
90e342f9 | 44 | |
b88ca698 CAW |
45 | from mediagoblin.tools.transition import DISABLE_GLOBALS |
46 | ||
31a8ff42 | 47 | |
ec97c937 E |
48 | _log = logging.getLogger(__name__) |
49 | ||
50 | ||
d7149900 CAW |
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 | ||
8e1e744d | 64 | class 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 | 344 | def 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 | ||
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) |