##// END OF EJS Templates
db: move Session.remove to outer wsgi layer and also add it...
dan -
r669:d3c76065 stable
parent child Browse files
Show More
@@ -1,474 +1,497 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError, HTTPFound
35 35 from pyramid.events import ApplicationCreated
36 36 import pyramid.httpexceptions as httpexceptions
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 from rhodecode.model import meta
42 43 from rhodecode.config import patches
43 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 45 from rhodecode.config.environment import (
45 46 load_environment, load_pyramid_environment)
46 47 from rhodecode.lib.middleware import csrf
47 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 49 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
49 50 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 51 from rhodecode.lib.middleware.vcs import VCSMiddleware
51 52 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 53 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
53 54 from rhodecode.subscribers import scan_repositories_if_enabled
54 55
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 # this is used to avoid avoid the route lookup overhead in routesmiddleware
60 61 # for certain routes which won't go to pylons to - eg. static files, debugger
61 62 # it is only needed for the pylons migration and can be removed once complete
62 63 class SkippableRoutesMiddleware(RoutesMiddleware):
63 64 """ Routes middleware that allows you to skip prefixes """
64 65
65 66 def __init__(self, *args, **kw):
66 67 self.skip_prefixes = kw.pop('skip_prefixes', [])
67 68 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
68 69
69 70 def __call__(self, environ, start_response):
70 71 for prefix in self.skip_prefixes:
71 72 if environ['PATH_INFO'].startswith(prefix):
72 73 # added to avoid the case when a missing /_static route falls
73 74 # through to pylons and causes an exception as pylons is
74 75 # expecting wsgiorg.routingargs to be set in the environ
75 76 # by RoutesMiddleware.
76 77 if 'wsgiorg.routing_args' not in environ:
77 78 environ['wsgiorg.routing_args'] = (None, {})
78 79 return self.app(environ, start_response)
79 80
80 81 return super(SkippableRoutesMiddleware, self).__call__(
81 82 environ, start_response)
82 83
83 84
84 85 def make_app(global_conf, static_files=True, **app_conf):
85 86 """Create a Pylons WSGI application and return it
86 87
87 88 ``global_conf``
88 89 The inherited configuration for this application. Normally from
89 90 the [DEFAULT] section of the Paste ini file.
90 91
91 92 ``app_conf``
92 93 The application's local configuration. Normally specified in
93 94 the [app:<name>] section of the Paste ini file (where <name>
94 95 defaults to main).
95 96
96 97 """
97 98 # Apply compatibility patches
98 99 patches.kombu_1_5_1_python_2_7_11()
99 100 patches.inspect_getargspec()
100 101
101 102 # Configure the Pylons environment
102 103 config = load_environment(global_conf, app_conf)
103 104
104 105 # The Pylons WSGI app
105 106 app = PylonsApp(config=config)
106 107 if rhodecode.is_test:
107 108 app = csrf.CSRFDetector(app)
108 109
109 110 expected_origin = config.get('expected_origin')
110 111 if expected_origin:
111 112 # The API can be accessed from other Origins.
112 113 app = csrf.OriginChecker(app, expected_origin,
113 114 skip_urls=[routes.util.url_for('api')])
114 115
115 116 # Establish the Registry for this application
116 117 app = RegistryManager(app)
117 118
118 119 app.config = config
119 120
120 121 return app
121 122
122 123
123 124 def make_pyramid_app(global_config, **settings):
124 125 """
125 126 Constructs the WSGI application based on Pyramid and wraps the Pylons based
126 127 application.
127 128
128 129 Specials:
129 130
130 131 * We migrate from Pylons to Pyramid. While doing this, we keep both
131 132 frameworks functional. This involves moving some WSGI middlewares around
132 133 and providing access to some data internals, so that the old code is
133 134 still functional.
134 135
135 136 * The application can also be integrated like a plugin via the call to
136 137 `includeme`. This is accompanied with the other utility functions which
137 138 are called. Changing this should be done with great care to not break
138 139 cases when these fragments are assembled from another place.
139 140
140 141 """
141 142 # The edition string should be available in pylons too, so we add it here
142 143 # before copying the settings.
143 144 settings.setdefault('rhodecode.edition', 'Community Edition')
144 145
145 146 # As long as our Pylons application does expect "unprepared" settings, make
146 147 # sure that we keep an unmodified copy. This avoids unintentional change of
147 148 # behavior in the old application.
148 149 settings_pylons = settings.copy()
149 150
150 151 sanitize_settings_and_apply_defaults(settings)
151 152 config = Configurator(settings=settings)
152 153 add_pylons_compat_data(config.registry, global_config, settings_pylons)
153 154
154 155 load_pyramid_environment(global_config, settings)
155 156
156 157 includeme_first(config)
157 158 includeme(config)
158 159 pyramid_app = config.make_wsgi_app()
159 160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
160 161 pyramid_app.config = config
162
163 # creating the app uses a connection - return it after we are done
164 meta.Session.remove()
165
161 166 return pyramid_app
162 167
163 168
164 169 def make_not_found_view(config):
165 170 """
166 171 This creates the view which should be registered as not-found-view to
167 172 pyramid. Basically it contains of the old pylons app, converted to a view.
168 173 Additionally it is wrapped by some other middlewares.
169 174 """
170 175 settings = config.registry.settings
171 176 vcs_server_enabled = settings['vcs.server.enable']
172 177
173 178 # Make pylons app from unprepared settings.
174 179 pylons_app = make_app(
175 180 config.registry._pylons_compat_global_config,
176 181 **config.registry._pylons_compat_settings)
177 182 config.registry._pylons_compat_config = pylons_app.config
178 183
179 184 # Appenlight monitoring.
180 185 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
181 186 pylons_app, settings)
182 187
183 188 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
184 189 # a view to handle the request. Therefore we wrap it around the pylons app.
185 190 if vcs_server_enabled:
186 191 pylons_app = VCSMiddleware(
187 192 pylons_app, settings, appenlight_client, registry=config.registry)
188 193
189 194 pylons_app_as_view = wsgiapp(pylons_app)
190 195
191 196 # Protect from VCS Server error related pages when server is not available
192 197 if not vcs_server_enabled:
193 198 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
194 199
195 200 def pylons_app_with_error_handler(context, request):
196 201 """
197 202 Handle exceptions from rc pylons app:
198 203
199 204 - old webob type exceptions get converted to pyramid exceptions
200 205 - pyramid exceptions are passed to the error handler view
201 206 """
202 207 def is_vcs_response(response):
203 208 return 'X-RhodeCode-Backend' in response.headers
204 209
205 210 def is_http_error(response):
206 211 # webob type error responses
207 212 return (400 <= response.status_int <= 599)
208 213
209 214 def is_error_handling_needed(response):
210 215 return is_http_error(response) and not is_vcs_response(response)
211 216
212 217 try:
213 218 response = pylons_app_as_view(context, request)
214 219 if is_error_handling_needed(response):
215 220 response = webob_to_pyramid_http_response(response)
216 221 return error_handler(response, request)
217 222 except HTTPError as e: # pyramid type exceptions
218 223 return error_handler(e, request)
219 224 except Exception:
220 225 if settings.get('debugtoolbar.enabled', False):
221 226 raise
222 227 return error_handler(HTTPInternalServerError(), request)
223 228 return response
224 229
225 230 return pylons_app_with_error_handler
226 231
227 232
228 233 def add_pylons_compat_data(registry, global_config, settings):
229 234 """
230 235 Attach data to the registry to support the Pylons integration.
231 236 """
232 237 registry._pylons_compat_global_config = global_config
233 238 registry._pylons_compat_settings = settings
234 239
235 240
236 241 def webob_to_pyramid_http_response(webob_response):
237 242 ResponseClass = httpexceptions.status_map[webob_response.status_int]
238 243 pyramid_response = ResponseClass(webob_response.status)
239 244 pyramid_response.status = webob_response.status
240 245 pyramid_response.headers.update(webob_response.headers)
241 246 if pyramid_response.headers['content-type'] == 'text/html':
242 247 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
243 248 return pyramid_response
244 249
245 250
246 251 def error_handler(exception, request):
247 252 # TODO: dan: replace the old pylons error controller with this
248 253 from rhodecode.model.settings import SettingsModel
249 254 from rhodecode.lib.utils2 import AttributeDict
250 255
251 256 try:
252 257 rc_config = SettingsModel().get_all_settings()
253 258 except Exception:
254 259 log.exception('failed to fetch settings')
255 260 rc_config = {}
256 261
257 262 base_response = HTTPInternalServerError()
258 263 # prefer original exception for the response since it may have headers set
259 264 if isinstance(exception, HTTPError):
260 265 base_response = exception
261 266
262 267 c = AttributeDict()
263 268 c.error_message = base_response.status
264 269 c.error_explanation = base_response.explanation or str(base_response)
265 270 c.visual = AttributeDict()
266 271
267 272 c.visual.rhodecode_support_url = (
268 273 request.registry.settings.get('rhodecode_support_url') or
269 274 request.route_url('rhodecode_support')
270 275 )
271 276 c.redirect_time = 0
272 277 c.rhodecode_name = rc_config.get('rhodecode_title', '')
273 278 if not c.rhodecode_name:
274 279 c.rhodecode_name = 'Rhodecode'
275 280
276 281 response = render_to_response(
277 282 '/errors/error_document.html', {'c': c}, request=request,
278 283 response=base_response)
279 284
280 285 return response
281 286
282 287
283 288 def includeme(config):
284 289 settings = config.registry.settings
285 290
286 291 # plugin information
287 292 config.registry.rhodecode_plugins = OrderedDict()
288 293
289 294 config.add_directive(
290 295 'register_rhodecode_plugin', register_rhodecode_plugin)
291 296
292 297 if asbool(settings.get('appenlight', 'false')):
293 298 config.include('appenlight_client.ext.pyramid_tween')
294 299
295 300 # Includes which are required. The application would fail without them.
296 301 config.include('pyramid_mako')
297 302 config.include('pyramid_beaker')
298 303 config.include('rhodecode.channelstream')
299 304 config.include('rhodecode.admin')
300 305 config.include('rhodecode.authentication')
301 306 config.include('rhodecode.integrations')
302 307 config.include('rhodecode.login')
303 308 config.include('rhodecode.tweens')
304 309 config.include('rhodecode.api')
305 310 config.include('rhodecode.svn_support')
306 311 config.add_route(
307 312 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
308 313
309 314 # Add subscribers.
310 315 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 316
312 317 # Set the authorization policy.
313 318 authz_policy = ACLAuthorizationPolicy()
314 319 config.set_authorization_policy(authz_policy)
315 320
316 321 # Set the default renderer for HTML templates to mako.
317 322 config.add_mako_renderer('.html')
318 323
319 324 # include RhodeCode plugins
320 325 includes = aslist(settings.get('rhodecode.includes', []))
321 326 for inc in includes:
322 327 config.include(inc)
323 328
324 329 # This is the glue which allows us to migrate in chunks. By registering the
325 330 # pylons based application as the "Not Found" view in Pyramid, we will
326 331 # fallback to the old application each time the new one does not yet know
327 332 # how to handle a request.
328 333 config.add_notfound_view(make_not_found_view(config))
329 334
330 335 if not settings.get('debugtoolbar.enabled', False):
331 336 # if no toolbar, then any exception gets caught and rendered
332 337 config.add_view(error_handler, context=Exception)
333 338
334 339 config.add_view(error_handler, context=HTTPError)
335 340
336 341
337 342 def includeme_first(config):
338 343 # redirect automatic browser favicon.ico requests to correct place
339 344 def favicon_redirect(context, request):
340 345 return HTTPFound(
341 346 request.static_path('rhodecode:public/images/favicon.ico'))
342 347
343 348 config.add_view(favicon_redirect, route_name='favicon')
344 349 config.add_route('favicon', '/favicon.ico')
345 350
346 351 config.add_static_view(
347 352 '_static/deform', 'deform:static')
348 353 config.add_static_view(
349 354 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
350 355
351 356
352 357 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
353 358 """
354 359 Apply outer WSGI middlewares around the application.
355 360
356 361 Part of this has been moved up from the Pylons layer, so that the
357 362 data is also available if old Pylons code is hit through an already ported
358 363 view.
359 364 """
360 365 settings = config.registry.settings
361 366
362 367 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
363 368 pyramid_app = HttpsFixup(pyramid_app, settings)
364 369
365 370 # Add RoutesMiddleware to support the pylons compatibility tween during
366 371 # migration to pyramid.
367 372 pyramid_app = SkippableRoutesMiddleware(
368 373 pyramid_app, config.registry._pylons_compat_config['routes.map'],
369 374 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
370 375
371 376 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
372 377
373 378 if settings['gzip_responses']:
374 379 pyramid_app = make_gzip_middleware(
375 380 pyramid_app, settings, compress_level=1)
376 381
377 return pyramid_app
382
383 # this should be the outer most middleware in the wsgi stack since
384 # middleware like Routes make database calls
385 def pyramid_app_with_cleanup(environ, start_response):
386 try:
387 return pyramid_app(environ, start_response)
388 finally:
389 # Dispose current database session and rollback uncommitted
390 # transactions.
391 meta.Session.remove()
392
393 # In a single threaded mode server, on non sqlite db we should have
394 # '0 Current Checked out connections' at the end of a request,
395 # if not, then something, somewhere is leaving a connection open
396 pool = meta.Base.metadata.bind.engine.pool
397 log.debug('sa pool status: %s', pool.status())
398
399
400 return pyramid_app_with_cleanup
378 401
379 402
380 403 def sanitize_settings_and_apply_defaults(settings):
381 404 """
382 405 Applies settings defaults and does all type conversion.
383 406
384 407 We would move all settings parsing and preparation into this place, so that
385 408 we have only one place left which deals with this part. The remaining parts
386 409 of the application would start to rely fully on well prepared settings.
387 410
388 411 This piece would later be split up per topic to avoid a big fat monster
389 412 function.
390 413 """
391 414
392 415 # Pyramid's mako renderer has to search in the templates folder so that the
393 416 # old templates still work. Ported and new templates are expected to use
394 417 # real asset specifications for the includes.
395 418 mako_directories = settings.setdefault('mako.directories', [
396 419 # Base templates of the original Pylons application
397 420 'rhodecode:templates',
398 421 ])
399 422 log.debug(
400 423 "Using the following Mako template directories: %s",
401 424 mako_directories)
402 425
403 426 # Default includes, possible to change as a user
404 427 pyramid_includes = settings.setdefault('pyramid.includes', [
405 428 'rhodecode.lib.middleware.request_wrapper',
406 429 ])
407 430 log.debug(
408 431 "Using the following pyramid.includes: %s",
409 432 pyramid_includes)
410 433
411 434 # TODO: johbo: Re-think this, usually the call to config.include
412 435 # should allow to pass in a prefix.
413 436 settings.setdefault('rhodecode.api.url', '/_admin/api')
414 437
415 438 # Sanitize generic settings.
416 439 _list_setting(settings, 'default_encoding', 'UTF-8')
417 440 _bool_setting(settings, 'is_test', 'false')
418 441 _bool_setting(settings, 'gzip_responses', 'false')
419 442
420 443 # Call split out functions that sanitize settings for each topic.
421 444 _sanitize_appenlight_settings(settings)
422 445 _sanitize_vcs_settings(settings)
423 446
424 447 return settings
425 448
426 449
427 450 def _sanitize_appenlight_settings(settings):
428 451 _bool_setting(settings, 'appenlight', 'false')
429 452
430 453
431 454 def _sanitize_vcs_settings(settings):
432 455 """
433 456 Applies settings defaults and does type conversion for all VCS related
434 457 settings.
435 458 """
436 459 _string_setting(settings, 'vcs.svn.compatible_version', '')
437 460 _string_setting(settings, 'git_rev_filter', '--all')
438 461 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
439 462 _string_setting(settings, 'vcs.server', '')
440 463 _string_setting(settings, 'vcs.server.log_level', 'debug')
441 464 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
442 465 _bool_setting(settings, 'startup.import_repos', 'false')
443 466 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
444 467 _bool_setting(settings, 'vcs.server.enable', 'true')
445 468 _bool_setting(settings, 'vcs.start_server', 'false')
446 469 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
447 470 _int_setting(settings, 'vcs.connection_timeout', 3600)
448 471
449 472
450 473 def _int_setting(settings, name, default):
451 474 settings[name] = int(settings.get(name, default))
452 475
453 476
454 477 def _bool_setting(settings, name, default):
455 478 input = settings.get(name, default)
456 479 if isinstance(input, unicode):
457 480 input = input.encode('utf8')
458 481 settings[name] = asbool(input)
459 482
460 483
461 484 def _list_setting(settings, name, default):
462 485 raw_value = settings.get(name, default)
463 486
464 487 old_separator = ','
465 488 if old_separator in raw_value:
466 489 # If we get a comma separated list, pass it to our own function.
467 490 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
468 491 else:
469 492 # Otherwise we assume it uses pyramids space/newline separation.
470 493 settings[name] = aslist(raw_value)
471 494
472 495
473 496 def _string_setting(settings, name, default):
474 497 settings[name] = settings.get(name, default).lower()
@@ -1,274 +1,278 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import json
22 22 import logging
23 23 import urlparse
24 24 import threading
25 25 from BaseHTTPServer import BaseHTTPRequestHandler
26 26 from SocketServer import TCPServer
27 27 from routes.util import URLGenerator
28 28
29 29 import Pyro4
30 30 import pylons
31 31 import rhodecode
32 32
33 from rhodecode.model import meta
33 34 from rhodecode.lib import hooks_base
34 35 from rhodecode.lib.utils2 import (
35 36 AttributeDict, safe_str, get_routes_generator_for_server_url)
36 37
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41
41 42 class HooksHttpHandler(BaseHTTPRequestHandler):
42 43 def do_POST(self):
43 44 method, extras = self._read_request()
44 45 try:
45 46 result = self._call_hook(method, extras)
46 47 except Exception as e:
47 48 result = {
48 49 'exception': e.__class__.__name__,
49 50 'exception_args': e.args
50 51 }
51 52 self._write_response(result)
52 53
53 54 def _read_request(self):
54 55 length = int(self.headers['Content-Length'])
55 56 body = self.rfile.read(length).decode('utf-8')
56 57 data = json.loads(body)
57 58 return data['method'], data['extras']
58 59
59 60 def _write_response(self, result):
60 61 self.send_response(200)
61 62 self.send_header("Content-type", "text/json")
62 63 self.end_headers()
63 64 self.wfile.write(json.dumps(result))
64 65
65 66 def _call_hook(self, method, extras):
66 67 hooks = Hooks()
68 try:
67 69 result = getattr(hooks, method)(extras)
70 finally:
71 meta.Session.remove()
68 72 return result
69 73
70 74 def log_message(self, format, *args):
71 75 """
72 76 This is an overriden method of BaseHTTPRequestHandler which logs using
73 77 logging library instead of writing directly to stderr.
74 78 """
75 79
76 80 message = format % args
77 81
78 82 # TODO: mikhail: add different log levels support
79 83 log.debug(
80 84 "%s - - [%s] %s", self.client_address[0],
81 85 self.log_date_time_string(), message)
82 86
83 87
84 88 class DummyHooksCallbackDaemon(object):
85 89 def __init__(self):
86 90 self.hooks_module = Hooks.__module__
87 91
88 92 def __enter__(self):
89 93 log.debug('Running dummy hooks callback daemon')
90 94 return self
91 95
92 96 def __exit__(self, exc_type, exc_val, exc_tb):
93 97 log.debug('Exiting dummy hooks callback daemon')
94 98
95 99
96 100 class ThreadedHookCallbackDaemon(object):
97 101
98 102 _callback_thread = None
99 103 _daemon = None
100 104 _done = False
101 105
102 106 def __init__(self):
103 107 self._prepare()
104 108
105 109 def __enter__(self):
106 110 self._run()
107 111 return self
108 112
109 113 def __exit__(self, exc_type, exc_val, exc_tb):
110 114 self._stop()
111 115
112 116 def _prepare(self):
113 117 raise NotImplementedError()
114 118
115 119 def _run(self):
116 120 raise NotImplementedError()
117 121
118 122 def _stop(self):
119 123 raise NotImplementedError()
120 124
121 125
122 126 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
123 127 """
124 128 Context manager which will run a callback daemon in a background thread.
125 129 """
126 130
127 131 hooks_uri = None
128 132
129 133 def _prepare(self):
130 134 log.debug("Preparing callback daemon and registering hook object")
131 135 self._daemon = Pyro4.Daemon()
132 136 hooks_interface = Hooks()
133 137 self.hooks_uri = str(self._daemon.register(hooks_interface))
134 138 log.debug("Hooks uri is: %s", self.hooks_uri)
135 139
136 140 def _run(self):
137 141 log.debug("Running event loop of callback daemon in background thread")
138 142 callback_thread = threading.Thread(
139 143 target=self._daemon.requestLoop,
140 144 kwargs={'loopCondition': lambda: not self._done})
141 145 callback_thread.daemon = True
142 146 callback_thread.start()
143 147 self._callback_thread = callback_thread
144 148
145 149 def _stop(self):
146 150 log.debug("Waiting for background thread to finish.")
147 151 self._done = True
148 152 self._callback_thread.join()
149 153 self._daemon.close()
150 154 self._daemon = None
151 155 self._callback_thread = None
152 156
153 157
154 158 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
155 159 """
156 160 Context manager which will run a callback daemon in a background thread.
157 161 """
158 162
159 163 hooks_uri = None
160 164
161 165 IP_ADDRESS = '127.0.0.1'
162 166
163 167 # From Python docs: Polling reduces our responsiveness to a shutdown
164 168 # request and wastes cpu at all other times.
165 169 POLL_INTERVAL = 0.1
166 170
167 171 def _prepare(self):
168 172 log.debug("Preparing callback daemon and registering hook object")
169 173
170 174 self._done = False
171 175 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
172 176 _, port = self._daemon.server_address
173 177 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
174 178
175 179 log.debug("Hooks uri is: %s", self.hooks_uri)
176 180
177 181 def _run(self):
178 182 log.debug("Running event loop of callback daemon in background thread")
179 183 callback_thread = threading.Thread(
180 184 target=self._daemon.serve_forever,
181 185 kwargs={'poll_interval': self.POLL_INTERVAL})
182 186 callback_thread.daemon = True
183 187 callback_thread.start()
184 188 self._callback_thread = callback_thread
185 189
186 190 def _stop(self):
187 191 log.debug("Waiting for background thread to finish.")
188 192 self._daemon.shutdown()
189 193 self._callback_thread.join()
190 194 self._daemon = None
191 195 self._callback_thread = None
192 196
193 197
194 198 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
195 199 callback_daemon = None
196 200 protocol = protocol.lower() if protocol else None
197 201
198 202 if use_direct_calls:
199 203 callback_daemon = DummyHooksCallbackDaemon()
200 204 extras['hooks_module'] = callback_daemon.hooks_module
201 205 else:
202 206 if protocol == 'pyro4':
203 207 callback_daemon = Pyro4HooksCallbackDaemon()
204 208 elif protocol == 'http':
205 209 callback_daemon = HttpHooksCallbackDaemon()
206 210 else:
207 211 log.error('Unsupported callback daemon protocol "%s"', protocol)
208 212 raise Exception('Unsupported callback daemon protocol.')
209 213
210 214 extras['hooks_uri'] = callback_daemon.hooks_uri
211 215 extras['hooks_protocol'] = protocol
212 216
213 217 return callback_daemon, extras
214 218
215 219
216 220 class Hooks(object):
217 221 """
218 222 Exposes the hooks for remote call backs
219 223 """
220 224
221 225 @Pyro4.callback
222 226 def repo_size(self, extras):
223 227 log.debug("Called repo_size of Hooks object")
224 228 return self._call_hook(hooks_base.repo_size, extras)
225 229
226 230 @Pyro4.callback
227 231 def pre_pull(self, extras):
228 232 log.debug("Called pre_pull of Hooks object")
229 233 return self._call_hook(hooks_base.pre_pull, extras)
230 234
231 235 @Pyro4.callback
232 236 def post_pull(self, extras):
233 237 log.debug("Called post_pull of Hooks object")
234 238 return self._call_hook(hooks_base.post_pull, extras)
235 239
236 240 @Pyro4.callback
237 241 def pre_push(self, extras):
238 242 log.debug("Called pre_push of Hooks object")
239 243 return self._call_hook(hooks_base.pre_push, extras)
240 244
241 245 @Pyro4.callback
242 246 def post_push(self, extras):
243 247 log.debug("Called post_push of Hooks object")
244 248 return self._call_hook(hooks_base.post_push, extras)
245 249
246 250 def _call_hook(self, hook, extras):
247 251 extras = AttributeDict(extras)
248 252 pylons_router = get_routes_generator_for_server_url(extras.server_url)
249 253 pylons.url._push_object(pylons_router)
250 254
251 255 try:
252 256 result = hook(extras)
253 257 except Exception as error:
254 258 log.exception('Exception when handling hook %s', hook)
255 259 error_args = error.args
256 260 return {
257 261 'status': 128,
258 262 'output': '',
259 263 'exception': type(error).__name__,
260 264 'exception_args': error_args,
261 265 }
262 266 finally:
263 267 pylons.url._pop_object()
264 268
265 269 return {
266 270 'status': result.status,
267 271 'output': result.output,
268 272 }
269 273
270 274 def __enter__(self):
271 275 return self
272 276
273 277 def __exit__(self, exc_type, exc_val, exc_tb):
274 278 pass
@@ -1,445 +1,448 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import logging
28 28 import importlib
29 29 from functools import wraps
30 30
31 31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 32 from webob.exc import (
33 33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 34
35 35 import rhodecode
36 36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 39 from rhodecode.lib.exceptions import (
40 40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 41 NotAllowedToCreateUserError)
42 42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 43 from rhodecode.lib.middleware import appenlight
44 44 from rhodecode.lib.middleware.utils import scm_app
45 45 from rhodecode.lib.utils import (
46 46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 49 from rhodecode.model import meta
50 50 from rhodecode.model.db import User, Repository
51 51 from rhodecode.model.scm import ScmModel
52 52 from rhodecode.model.settings import SettingsModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def initialize_generator(factory):
58 58 """
59 59 Initializes the returned generator by draining its first element.
60 60
61 61 This can be used to give a generator an initializer, which is the code
62 62 up to the first yield statement. This decorator enforces that the first
63 63 produced element has the value ``"__init__"`` to make its special
64 64 purpose very explicit in the using code.
65 65 """
66 66
67 67 @wraps(factory)
68 68 def wrapper(*args, **kwargs):
69 69 gen = factory(*args, **kwargs)
70 70 try:
71 71 init = gen.next()
72 72 except StopIteration:
73 73 raise ValueError('Generator must yield at least one element.')
74 74 if init != "__init__":
75 75 raise ValueError('First yielded element must be "__init__".')
76 76 return gen
77 77 return wrapper
78 78
79 79
80 80 class SimpleVCS(object):
81 81 """Common functionality for SCM HTTP handlers."""
82 82
83 83 SCM = 'unknown'
84 84
85 85 def __init__(self, application, config, registry):
86 86 self.registry = registry
87 87 self.application = application
88 88 self.config = config
89 89 # base path of repo locations
90 90 self.basepath = get_rhodecode_base_path()
91 91 # authenticate this VCS request using authfunc
92 92 auth_ret_code_detection = \
93 93 str2bool(self.config.get('auth_ret_code_detection', False))
94 94 self.authenticate = BasicAuth(
95 95 '', authenticate, registry, config.get('auth_ret_code'),
96 96 auth_ret_code_detection)
97 97 self.ip_addr = '0.0.0.0'
98 98
99 99 @property
100 100 def scm_app(self):
101 101 custom_implementation = self.config.get('vcs.scm_app_implementation')
102 102 if custom_implementation and custom_implementation != 'pyro4':
103 103 log.info(
104 104 "Using custom implementation of scm_app: %s",
105 105 custom_implementation)
106 106 scm_app_impl = importlib.import_module(custom_implementation)
107 107 else:
108 108 scm_app_impl = scm_app
109 109 return scm_app_impl
110 110
111 111 def _get_by_id(self, repo_name):
112 112 """
113 113 Gets a special pattern _<ID> from clone url and tries to replace it
114 114 with a repository_name for support of _<ID> non changable urls
115 115
116 116 :param repo_name:
117 117 """
118 118
119 119 data = repo_name.split('/')
120 120 if len(data) >= 2:
121 121 from rhodecode.model.repo import RepoModel
122 122 by_id_match = RepoModel().get_repo_by_id(repo_name)
123 123 if by_id_match:
124 124 data[1] = by_id_match.repo_name
125 125
126 126 return safe_str('/'.join(data))
127 127
128 128 def _invalidate_cache(self, repo_name):
129 129 """
130 130 Set's cache for this repository for invalidation on next access
131 131
132 132 :param repo_name: full repo name, also a cache key
133 133 """
134 134 ScmModel().mark_for_invalidation(repo_name)
135 135
136 136 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
137 137 db_repo = Repository.get_by_repo_name(repo_name)
138 138 if not db_repo:
139 139 log.debug('Repository `%s` not found inside the database.',
140 140 repo_name)
141 141 return False
142 142
143 143 if db_repo.repo_type != scm_type:
144 144 log.warning(
145 145 'Repository `%s` have incorrect scm_type, expected %s got %s',
146 146 repo_name, db_repo.repo_type, scm_type)
147 147 return False
148 148
149 149 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
150 150
151 151 def valid_and_active_user(self, user):
152 152 """
153 153 Checks if that user is not empty, and if it's actually object it checks
154 154 if he's active.
155 155
156 156 :param user: user object or None
157 157 :return: boolean
158 158 """
159 159 if user is None:
160 160 return False
161 161
162 162 elif user.active:
163 163 return True
164 164
165 165 return False
166 166
167 167 def _check_permission(self, action, user, repo_name, ip_addr=None):
168 168 """
169 169 Checks permissions using action (push/pull) user and repository
170 170 name
171 171
172 172 :param action: push or pull action
173 173 :param user: user instance
174 174 :param repo_name: repository name
175 175 """
176 176 # check IP
177 177 inherit = user.inherit_default_permissions
178 178 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
179 179 inherit_from_default=inherit)
180 180 if ip_allowed:
181 181 log.info('Access for IP:%s allowed', ip_addr)
182 182 else:
183 183 return False
184 184
185 185 if action == 'push':
186 186 if not HasPermissionAnyMiddleware('repository.write',
187 187 'repository.admin')(user,
188 188 repo_name):
189 189 return False
190 190
191 191 else:
192 192 # any other action need at least read permission
193 193 if not HasPermissionAnyMiddleware('repository.read',
194 194 'repository.write',
195 195 'repository.admin')(user,
196 196 repo_name):
197 197 return False
198 198
199 199 return True
200 200
201 201 def _check_ssl(self, environ, start_response):
202 202 """
203 203 Checks the SSL check flag and returns False if SSL is not present
204 204 and required True otherwise
205 205 """
206 206 org_proto = environ['wsgi._org_proto']
207 207 # check if we have SSL required ! if not it's a bad request !
208 208 require_ssl = str2bool(
209 209 SettingsModel().get_ui_by_key('push_ssl').ui_value)
210 210 if require_ssl and org_proto == 'http':
211 211 log.debug('proto is %s and SSL is required BAD REQUEST !',
212 212 org_proto)
213 213 return False
214 214 return True
215 215
216 216 def __call__(self, environ, start_response):
217 217 try:
218 218 return self._handle_request(environ, start_response)
219 219 except Exception:
220 220 log.exception("Exception while handling request")
221 221 appenlight.track_exception(environ)
222 222 return HTTPInternalServerError()(environ, start_response)
223 223 finally:
224 224 meta.Session.remove()
225 225
226 226 def _handle_request(self, environ, start_response):
227 227
228 228 if not self._check_ssl(environ, start_response):
229 229 reason = ('SSL required, while RhodeCode was unable '
230 230 'to detect this as SSL request')
231 231 log.debug('User not allowed to proceed, %s', reason)
232 232 return HTTPNotAcceptable(reason)(environ, start_response)
233 233
234 234 ip_addr = get_ip_addr(environ)
235 235 username = None
236 236
237 237 # skip passing error to error controller
238 238 environ['pylons.status_code_redirect'] = True
239 239
240 240 # ======================================================================
241 241 # EXTRACT REPOSITORY NAME FROM ENV
242 242 # ======================================================================
243 243 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
244 244 repo_name = self._get_repository_name(environ)
245 245 environ['REPO_NAME'] = repo_name
246 246 log.debug('Extracted repo name is %s', repo_name)
247 247
248 248 # check for type, presence in database and on filesystem
249 249 if not self.is_valid_and_existing_repo(
250 250 repo_name, self.basepath, self.SCM):
251 251 return HTTPNotFound()(environ, start_response)
252 252
253 253 # ======================================================================
254 254 # GET ACTION PULL or PUSH
255 255 # ======================================================================
256 256 action = self._get_action(environ)
257 257
258 258 # ======================================================================
259 259 # CHECK ANONYMOUS PERMISSION
260 260 # ======================================================================
261 261 if action in ['pull', 'push']:
262 262 anonymous_user = User.get_default_user()
263 263 username = anonymous_user.username
264 264 if anonymous_user.active:
265 265 # ONLY check permissions if the user is activated
266 266 anonymous_perm = self._check_permission(
267 267 action, anonymous_user, repo_name, ip_addr)
268 268 else:
269 269 anonymous_perm = False
270 270
271 271 if not anonymous_user.active or not anonymous_perm:
272 272 if not anonymous_user.active:
273 273 log.debug('Anonymous access is disabled, running '
274 274 'authentication')
275 275
276 276 if not anonymous_perm:
277 277 log.debug('Not enough credentials to access this '
278 278 'repository as anonymous user')
279 279
280 280 username = None
281 281 # ==============================================================
282 282 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
283 283 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
284 284 # ==============================================================
285 285
286 286 # try to auth based on environ, container auth methods
287 287 log.debug('Running PRE-AUTH for container based authentication')
288 288 pre_auth = authenticate(
289 289 '', '', environ, VCS_TYPE, registry=self.registry)
290 290 if pre_auth and pre_auth.get('username'):
291 291 username = pre_auth['username']
292 292 log.debug('PRE-AUTH got %s as username', username)
293 293
294 294 # If not authenticated by the container, running basic auth
295 295 if not username:
296 296 self.authenticate.realm = get_rhodecode_realm()
297 297
298 298 try:
299 299 result = self.authenticate(environ)
300 300 except (UserCreationError, NotAllowedToCreateUserError) as e:
301 301 log.error(e)
302 302 reason = safe_str(e)
303 303 return HTTPNotAcceptable(reason)(environ, start_response)
304 304
305 305 if isinstance(result, str):
306 306 AUTH_TYPE.update(environ, 'basic')
307 307 REMOTE_USER.update(environ, result)
308 308 username = result
309 309 else:
310 310 return result.wsgi_application(environ, start_response)
311 311
312 312 # ==============================================================
313 313 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
314 314 # ==============================================================
315 315 user = User.get_by_username(username)
316 316 if not self.valid_and_active_user(user):
317 317 return HTTPForbidden()(environ, start_response)
318 318 username = user.username
319 319 user.update_lastactivity()
320 320 meta.Session().commit()
321 321
322 322 # check user attributes for password change flag
323 323 user_obj = user
324 324 if user_obj and user_obj.username != User.DEFAULT_USER and \
325 325 user_obj.user_data.get('force_password_change'):
326 326 reason = 'password change required'
327 327 log.debug('User not allowed to authenticate, %s', reason)
328 328 return HTTPNotAcceptable(reason)(environ, start_response)
329 329
330 330 # check permissions for this repository
331 331 perm = self._check_permission(action, user, repo_name, ip_addr)
332 332 if not perm:
333 333 return HTTPForbidden()(environ, start_response)
334 334
335 335 # extras are injected into UI object and later available
336 336 # in hooks executed by rhodecode
337 337 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
338 338 extras = vcs_operation_context(
339 339 environ, repo_name=repo_name, username=username,
340 340 action=action, scm=self.SCM,
341 341 check_locking=check_locking)
342 342
343 343 # ======================================================================
344 344 # REQUEST HANDLING
345 345 # ======================================================================
346 346 str_repo_name = safe_str(repo_name)
347 347 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
348 348 log.debug('Repository path is %s', repo_path)
349 349
350 350 fix_PATH()
351 351
352 352 log.info(
353 353 '%s action on %s repo "%s" by "%s" from %s',
354 354 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
355 355
356 356 return self._generate_vcs_response(
357 357 environ, start_response, repo_path, repo_name, extras, action)
358 358
359 359 @initialize_generator
360 360 def _generate_vcs_response(
361 361 self, environ, start_response, repo_path, repo_name, extras,
362 362 action):
363 363 """
364 364 Returns a generator for the response content.
365 365
366 366 This method is implemented as a generator, so that it can trigger
367 367 the cache validation after all content sent back to the client. It
368 368 also handles the locking exceptions which will be triggered when
369 369 the first chunk is produced by the underlying WSGI application.
370 370 """
371 371 callback_daemon, extras = self._prepare_callback_daemon(extras)
372 372 config = self._create_config(extras, repo_name)
373 373 log.debug('HOOKS extras is %s', extras)
374 374 app = self._create_wsgi_app(repo_path, repo_name, config)
375 375
376 376 try:
377 377 with callback_daemon:
378 378 try:
379 379 response = app(environ, start_response)
380 380 finally:
381 381 # This statement works together with the decorator
382 382 # "initialize_generator" above. The decorator ensures that
383 383 # we hit the first yield statement before the generator is
384 384 # returned back to the WSGI server. This is needed to
385 385 # ensure that the call to "app" above triggers the
386 386 # needed callback to "start_response" before the
387 387 # generator is actually used.
388 388 yield "__init__"
389 389
390 390 for chunk in response:
391 391 yield chunk
392 392 except Exception as exc:
393 393 # TODO: johbo: Improve "translating" back the exception.
394 394 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
395 395 exc = HTTPLockedRC(*exc.args)
396 396 _code = rhodecode.CONFIG.get('lock_ret_code')
397 397 log.debug('Repository LOCKED ret code %s!', (_code,))
398 398 elif getattr(exc, '_vcs_kind', None) == 'requirement':
399 399 log.debug(
400 400 'Repository requires features unknown to this Mercurial')
401 401 exc = HTTPRequirementError(*exc.args)
402 402 else:
403 403 raise
404 404
405 405 for chunk in exc(environ, start_response):
406 406 yield chunk
407 407 finally:
408 408 # invalidate cache on push
409 try:
409 410 if action == 'push':
410 411 self._invalidate_cache(repo_name)
412 finally:
413 meta.Session.remove()
411 414
412 415 def _get_repository_name(self, environ):
413 416 """Get repository name out of the environmnent
414 417
415 418 :param environ: WSGI environment
416 419 """
417 420 raise NotImplementedError()
418 421
419 422 def _get_action(self, environ):
420 423 """Map request commands into a pull or push command.
421 424
422 425 :param environ: WSGI environment
423 426 """
424 427 raise NotImplementedError()
425 428
426 429 def _create_wsgi_app(self, repo_path, repo_name, config):
427 430 """Return the WSGI app that will finally handle the request."""
428 431 raise NotImplementedError()
429 432
430 433 def _create_config(self, extras, repo_name):
431 434 """Create a Pyro safe config representation."""
432 435 raise NotImplementedError()
433 436
434 437 def _prepare_callback_daemon(self, extras):
435 438 return prepare_callback_daemon(
436 439 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 440 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438 441
439 442
440 443 def _should_check_locking(query_string):
441 444 # this is kind of hacky, but due to how mercurial handles client-server
442 445 # server see all operation on commit; bookmarks, phases and
443 446 # obsolescence marker in different transaction, we don't want to check
444 447 # locking on those
445 448 return query_string not in ['cmd=listkeys']
@@ -1,86 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import pylons
24 24 import rhodecode
25 25
26 26 from pylons.i18n.translation import _get_translator
27 27 from pylons.util import ContextObj
28 28 from routes.util import URLGenerator
29 29
30 30 from rhodecode.lib.base import attach_context_attributes, get_auth_user
31 31 from rhodecode.model import meta
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 def pylons_compatibility_tween_factory(handler, registry):
37 37 def pylons_compatibility_tween(request):
38 38 """
39 39 While migrating from pylons to pyramid we need to call some pylons code
40 40 from pyramid. For example while rendering an old template that uses the
41 41 'c' or 'h' objects. This tween sets up the needed pylons globals.
42 42 """
43 try:
44 43 config = rhodecode.CONFIG
45 44 environ = request.environ
46 45 session = request.session
47 46 session_key = (config['pylons.environ_config']
48 47 .get('session', 'beaker.session'))
49 48
50 49 # Setup pylons globals.
51 50 pylons.config._push_object(config)
52 51 pylons.request._push_object(request)
53 52 pylons.session._push_object(session)
54 53 environ[session_key] = session
55 54 pylons.url._push_object(URLGenerator(config['routes.map'],
56 55 environ))
57 56
58 57 # TODO: Maybe we should use the language from pyramid.
59 58 translator = _get_translator(config.get('lang'))
60 59 pylons.translator._push_object(translator)
61 60
62 61 # Get the rhodecode auth user object and make it available.
63 62 auth_user = get_auth_user(environ)
64 63 request.user = auth_user
65 64 environ['rc_auth_user'] = auth_user
66 65
67 66 # Setup the pylons context object ('c')
68 67 context = ContextObj()
69 68 context.rhodecode_user = auth_user
70 69 attach_context_attributes(context, request)
71 70 pylons.tmpl_context._push_object(context)
72 71 return handler(request)
73 finally:
74 # Dispose current database session and rollback uncommitted
75 # transactions.
76 meta.Session.remove()
77 72
78 73 return pylons_compatibility_tween
79 74
80 75
81 76 def includeme(config):
82 77 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
83 78 'pyramid.events.BeforeRender')
84 79 config.add_subscriber('rhodecode.subscribers.add_localizer',
85 80 'pyramid.events.NewRequest')
86 81 config.add_tween('rhodecode.tweens.pylons_compatibility_tween_factory')
General Comments 0
You need to be logged in to leave comments. Login now