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/>.
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
29 from werkzeug
.middleware
.shared_data
import SharedDataMiddleware
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
,
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
49 from mediagoblin
.tools
.transition
import DISABLE_GLOBALS
52 _log
= logging
.getLogger(__name__
)
55 class Context(object):
57 MediaGoblin context object.
59 If a web request is being used, a Flask Request object is used
60 instead, otherwise (celery tasks, etc), attach things to this
63 Usually appears as "ctx" in utilities as first argument.
68 class MediaGoblinApp(object):
70 WSGI application of MediaGoblin
72 ... this is the heart of the program!
74 def __init__(self
, config_path
, setup_celery
=True):
76 Initialize the application based on a configuration file.
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
84 _log
.info("GNU MediaGoblin %s main server starting", __version__
)
85 _log
.debug("Using config file %s", config_path
)
90 # Open and setup the config
91 self
.global_config
, self
.app_config
= setup_global_and_app_config(config_path
)
95 setup_crypto(self
.app_config
)
97 ##########################################
98 # Setup other connections / useful objects
99 ##########################################
101 # Setup Session Manager, not needed in celery
102 self
.session_manager
= session
.SessionManager()
104 # load all available locales
107 # Set up plugins -- need to do this early so that plugins can
109 _log
.info("Setting up plugins.")
112 # Set up the database
114 self
.db_manager
= setup_database(self
)
116 self
.db
= setup_database(self
)
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()
124 self
.theme_registry
, self
.current_theme
= register_themes(self
.app_config
)
126 # Get the template environment
127 self
.template_loader
= get_jinja_loader(
128 self
.app_config
.get('local_templates'),
130 PluginManager().get_template_paths()
133 # Check if authentication plugin is enabled and respond accordingly.
134 self
.auth
= check_auth_enabled()
136 self
.app_config
['allow_comments'] = False
138 # Set up storage systems
139 self
.public_store
, self
.queue_store
= setup_storage()
142 self
.url_map
= get_url_map()
144 # set up staticdirector tool
145 self
.staticdirector
= get_staticdirector(self
.app_config
)
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)
154 setup_celery_from_config(self
.app_config
, self
.global_config
)
156 #######################################################
157 # Insert appropriate things into mediagoblin.mg_globals
159 # certain properties need to be accessed globally eg from
160 # validators, etc, which might not access to the request
163 # Note, we are trying to transition this out;
164 # run with environment variable DISABLE_GLOBALS=true
166 #######################################################
168 if not DISABLE_GLOBALS
:
169 setup_globals(app
=self
)
171 # Workbench *currently* only used by celery, so this only
172 # matters in always eager mode :)
173 self
.workbench_manager
= setup_workbench()
175 # instantiate application meddleware
176 self
.meddleware
= [common
.import_component(m
)(self
)
177 for m
in meddleware
.ENABLED_MEDDLEWARE
]
180 def gen_context(self
, ctx
=None, **kwargs
):
182 Attach contextual information to request, or generate a context object
184 This avoids global variables; various utilities and contextual
185 information (current translation, etc) are attached to this
189 with self
.db_manager
.session_scope() as db
:
190 yield self
._gen
_context
(db
, ctx
)
192 yield self
._gen
_context
(self
.db
, ctx
)
194 def _gen_context(self
, db
, ctx
, **kwargs
):
198 # Is a context provided?
205 # Attach self as request.app
206 # Also attach a few utilities from request.app for convenience?
211 ctx
.staticdirect
= self
.staticdirector
213 # Do special things if this is a request
214 # --------------------------------------
215 if isinstance(ctx
, Request
):
216 ctx
= self
._request
_only
_gen
_context
(ctx
)
220 def _request_only_gen_context(self
, request
):
222 Requests get some extra stuff attached to them that's not relevant
225 # Do we really want to load this via middleware? Maybe?
226 request
.session
= self
.session_manager
.load_session_from_cookie(request
)
228 request
.locale
= translate
.get_locale_from_request(request
)
230 # This should be moved over for certain, but how to deal with
232 request
.template_env
= template
.get_jinja_env(
233 self
, self
.template_loader
, request
.locale
)
235 mg_request
.setup_user_in_request(request
)
237 ## Routing / controller loading stuff
238 request
.map_adapter
= self
.url_map
.bind_to_environ(request
.environ
)
240 def build_proxy(endpoint
, **kw
):
242 qualified
= kw
.pop('qualified')
246 return request
.map_adapter
.build(
249 force_external
=qualified
)
251 request
.urlgen
= build_proxy
255 def call_backend(self
, environ
, start_response
):
256 request
= Request(environ
)
258 # Compatibility with django, use request.args preferrably
259 request
.GET
= request
.args
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
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"]
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':
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
)
281 def _finish_call_backend(self
, request
, environ
, start_response
):
282 # Log user out if authentication_disabled
283 no_auth_logout(request
)
285 request
.controller_name
= None
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(
296 exc
.get_description(environ
))(environ
, start_response
)
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
303 ## TODO: get rid of meddleware, turn it into hooks only
304 # pass the request through our meddleware classes
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(
313 e
.get_description(environ
))(environ
, start_response
)
315 request
= hook_transform("modify_request", request
)
317 request
.start_response
= start_response
319 # get the Http response from the controller
321 response
= controller(request
)
322 except HTTPException
as e
:
323 response
= render_http_exception(
324 request
, e
, e
.get_description(environ
))
326 # pass the response through the meddlewares
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
))
334 self
.session_manager
.save_session_to_cookie(
338 return response(environ
, start_response
)
340 def __call__(self
, environ
, start_response
):
341 ## If more errors happen that look like unclean sessions:
342 # self.db.check_session_clean()
345 return self
.call_backend(environ
, start_response
)
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()
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
361 if not mediagoblin_config
:
362 raise IOError("Usable mediagoblin config not found.")
363 del app_config
['config']
365 mgoblin_app
= MediaGoblinApp(mediagoblin_config
)
366 mgoblin_app
.call_backend
= SharedDataMiddleware(mgoblin_app
.call_backend
,
368 mgoblin_app
= hook_transform('wrap_wsgi', mgoblin_app
)