##// END OF EJS Templates
core: added ext_json as custom renderer using our ext_json lib for pyramid.
marcink -
r1664:0db868e6 default
parent child Browse files
Show More
@@ -0,0 +1,37 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
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 General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib.ext_json import json
22
23
24 def pyramid_ext_json(info):
25 """
26 Custom json renderer for pyramid to use our ext_json lib
27 """
28 def _render(value, system):
29 request = system.get('request')
30 if request is not None:
31 response = request.response
32 ct = response.content_type
33 if ct == response.default_content_type:
34 response.content_type = 'application/json'
35 return json.dumps(value)
36
37 return _render
@@ -1,508 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
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 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 47 from rhodecode.lib.middleware import csrf
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.error_handling import (
50 50 PylonsErrorHandlingMiddleware)
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import (
56 56 scan_repositories_if_enabled, write_metadata_if_needed,
57 57 write_js_routes_if_enabled, create_largeobjects_dirs_if_needed)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 # this is used to avoid avoid the route lookup overhead in routesmiddleware
64 64 # for certain routes which won't go to pylons to - eg. static files, debugger
65 65 # it is only needed for the pylons migration and can be removed once complete
66 66 class SkippableRoutesMiddleware(RoutesMiddleware):
67 67 """ Routes middleware that allows you to skip prefixes """
68 68
69 69 def __init__(self, *args, **kw):
70 70 self.skip_prefixes = kw.pop('skip_prefixes', [])
71 71 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
72 72
73 73 def __call__(self, environ, start_response):
74 74 for prefix in self.skip_prefixes:
75 75 if environ['PATH_INFO'].startswith(prefix):
76 76 # added to avoid the case when a missing /_static route falls
77 77 # through to pylons and causes an exception as pylons is
78 78 # expecting wsgiorg.routingargs to be set in the environ
79 79 # by RoutesMiddleware.
80 80 if 'wsgiorg.routing_args' not in environ:
81 81 environ['wsgiorg.routing_args'] = (None, {})
82 82 return self.app(environ, start_response)
83 83
84 84 return super(SkippableRoutesMiddleware, self).__call__(
85 85 environ, start_response)
86 86
87 87
88 88 def make_app(global_conf, static_files=True, **app_conf):
89 89 """Create a Pylons WSGI application and return it
90 90
91 91 ``global_conf``
92 92 The inherited configuration for this application. Normally from
93 93 the [DEFAULT] section of the Paste ini file.
94 94
95 95 ``app_conf``
96 96 The application's local configuration. Normally specified in
97 97 the [app:<name>] section of the Paste ini file (where <name>
98 98 defaults to main).
99 99
100 100 """
101 101 # Apply compatibility patches
102 102 patches.kombu_1_5_1_python_2_7_11()
103 103 patches.inspect_getargspec()
104 104
105 105 # Configure the Pylons environment
106 106 config = load_environment(global_conf, app_conf)
107 107
108 108 # The Pylons WSGI app
109 109 app = PylonsApp(config=config)
110 110 if rhodecode.is_test:
111 111 app = csrf.CSRFDetector(app)
112 112
113 113 expected_origin = config.get('expected_origin')
114 114 if expected_origin:
115 115 # The API can be accessed from other Origins.
116 116 app = csrf.OriginChecker(app, expected_origin,
117 117 skip_urls=[routes.util.url_for('api')])
118 118
119 119 # Establish the Registry for this application
120 120 app = RegistryManager(app)
121 121
122 122 app.config = config
123 123
124 124 return app
125 125
126 126
127 127 def make_pyramid_app(global_config, **settings):
128 128 """
129 129 Constructs the WSGI application based on Pyramid and wraps the Pylons based
130 130 application.
131 131
132 132 Specials:
133 133
134 134 * We migrate from Pylons to Pyramid. While doing this, we keep both
135 135 frameworks functional. This involves moving some WSGI middlewares around
136 136 and providing access to some data internals, so that the old code is
137 137 still functional.
138 138
139 139 * The application can also be integrated like a plugin via the call to
140 140 `includeme`. This is accompanied with the other utility functions which
141 141 are called. Changing this should be done with great care to not break
142 142 cases when these fragments are assembled from another place.
143 143
144 144 """
145 145 # The edition string should be available in pylons too, so we add it here
146 146 # before copying the settings.
147 147 settings.setdefault('rhodecode.edition', 'Community Edition')
148 148
149 149 # As long as our Pylons application does expect "unprepared" settings, make
150 150 # sure that we keep an unmodified copy. This avoids unintentional change of
151 151 # behavior in the old application.
152 152 settings_pylons = settings.copy()
153 153
154 154 sanitize_settings_and_apply_defaults(settings)
155 155 config = Configurator(settings=settings)
156 156 add_pylons_compat_data(config.registry, global_config, settings_pylons)
157 157
158 158 load_pyramid_environment(global_config, settings)
159 159
160 160 includeme_first(config)
161 161 includeme(config)
162 162 pyramid_app = config.make_wsgi_app()
163 163 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
164 164 pyramid_app.config = config
165 165
166 166 # creating the app uses a connection - return it after we are done
167 167 meta.Session.remove()
168 168
169 169 return pyramid_app
170 170
171 171
172 172 def make_not_found_view(config):
173 173 """
174 174 This creates the view which should be registered as not-found-view to
175 175 pyramid. Basically it contains of the old pylons app, converted to a view.
176 176 Additionally it is wrapped by some other middlewares.
177 177 """
178 178 settings = config.registry.settings
179 179 vcs_server_enabled = settings['vcs.server.enable']
180 180
181 181 # Make pylons app from unprepared settings.
182 182 pylons_app = make_app(
183 183 config.registry._pylons_compat_global_config,
184 184 **config.registry._pylons_compat_settings)
185 185 config.registry._pylons_compat_config = pylons_app.config
186 186
187 187 # Appenlight monitoring.
188 188 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
189 189 pylons_app, settings)
190 190
191 191 # The pylons app is executed inside of the pyramid 404 exception handler.
192 192 # Exceptions which are raised inside of it are not handled by pyramid
193 193 # again. Therefore we add a middleware that invokes the error handler in
194 194 # case of an exception or error response. This way we return proper error
195 195 # HTML pages in case of an error.
196 196 reraise = (settings.get('debugtoolbar.enabled', False) or
197 197 rhodecode.disable_error_handler)
198 198 pylons_app = PylonsErrorHandlingMiddleware(
199 199 pylons_app, error_handler, reraise)
200 200
201 201 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
202 202 # view to handle the request. Therefore it is wrapped around the pylons
203 203 # app. It has to be outside of the error handling otherwise error responses
204 204 # from the vcsserver are converted to HTML error pages. This confuses the
205 205 # command line tools and the user won't get a meaningful error message.
206 206 if vcs_server_enabled:
207 207 pylons_app = VCSMiddleware(
208 208 pylons_app, settings, appenlight_client, registry=config.registry)
209 209
210 210 # Convert WSGI app to pyramid view and return it.
211 211 return wsgiapp(pylons_app)
212 212
213 213
214 214 def add_pylons_compat_data(registry, global_config, settings):
215 215 """
216 216 Attach data to the registry to support the Pylons integration.
217 217 """
218 218 registry._pylons_compat_global_config = global_config
219 219 registry._pylons_compat_settings = settings
220 220
221 221
222 222 def error_handler(exception, request):
223 223 import rhodecode
224 224 from rhodecode.lib.utils2 import AttributeDict
225 225
226 226 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
227 227
228 228 base_response = HTTPInternalServerError()
229 229 # prefer original exception for the response since it may have headers set
230 230 if isinstance(exception, HTTPException):
231 231 base_response = exception
232 232
233 233 def is_http_error(response):
234 234 # error which should have traceback
235 235 return response.status_code > 499
236 236
237 237 if is_http_error(base_response):
238 238 log.exception(
239 239 'error occurred handling this request for path: %s', request.path)
240 240
241 241 c = AttributeDict()
242 242 c.error_message = base_response.status
243 243 c.error_explanation = base_response.explanation or str(base_response)
244 244 c.visual = AttributeDict()
245 245
246 246 c.visual.rhodecode_support_url = (
247 247 request.registry.settings.get('rhodecode_support_url') or
248 248 request.route_url('rhodecode_support')
249 249 )
250 250 c.redirect_time = 0
251 251 c.rhodecode_name = rhodecode_title
252 252 if not c.rhodecode_name:
253 253 c.rhodecode_name = 'Rhodecode'
254 254
255 255 c.causes = []
256 256 if hasattr(base_response, 'causes'):
257 257 c.causes = base_response.causes
258 258
259 259 response = render_to_response(
260 260 '/errors/error_document.mako', {'c': c}, request=request,
261 261 response=base_response)
262 262
263 263 return response
264 264
265 265
266 266 def includeme(config):
267 267 settings = config.registry.settings
268 268
269 269 # plugin information
270 270 config.registry.rhodecode_plugins = OrderedDict()
271 271
272 272 config.add_directive(
273 273 'register_rhodecode_plugin', register_rhodecode_plugin)
274 274
275 275 if asbool(settings.get('appenlight', 'false')):
276 276 config.include('appenlight_client.ext.pyramid_tween')
277 277
278 278 # Includes which are required. The application would fail without them.
279 279 config.include('pyramid_mako')
280 280 config.include('pyramid_beaker')
281 281
282 282 config.include('rhodecode.authentication')
283 283 config.include('rhodecode.integrations')
284 284
285 285 # apps
286 286 config.include('rhodecode.apps._base')
287 287
288 288 config.include('rhodecode.apps.admin')
289 289 config.include('rhodecode.apps.channelstream')
290 290 config.include('rhodecode.apps.login')
291 291 config.include('rhodecode.apps.repository')
292 292 config.include('rhodecode.apps.user_profile')
293 293 config.include('rhodecode.apps.my_account')
294 294 config.include('rhodecode.apps.svn_support')
295 295
296 296 config.include('rhodecode.tweens')
297 297 config.include('rhodecode.api')
298 298
299 299 config.add_route(
300 300 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
301 301
302 302 config.add_translation_dirs('rhodecode:i18n/')
303 303 settings['default_locale_name'] = settings.get('lang', 'en')
304 304
305 305 # Add subscribers.
306 306 config.add_subscriber(create_largeobjects_dirs_if_needed, ApplicationCreated)
307 307 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
308 308 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
309 309 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
310 310
311 311 # Set the authorization policy.
312 312 authz_policy = ACLAuthorizationPolicy()
313 313 config.set_authorization_policy(authz_policy)
314 314
315 315 # Set the default renderer for HTML templates to mako.
316 316 config.add_mako_renderer('.html')
317 317
318 config.add_renderer(
319 name='json_ext',
320 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
321
318 322 # include RhodeCode plugins
319 323 includes = aslist(settings.get('rhodecode.includes', []))
320 324 for inc in includes:
321 325 config.include(inc)
322 326
323 327 # This is the glue which allows us to migrate in chunks. By registering the
324 328 # pylons based application as the "Not Found" view in Pyramid, we will
325 329 # fallback to the old application each time the new one does not yet know
326 330 # how to handle a request.
327 331 config.add_notfound_view(make_not_found_view(config))
328 332
329 333 if not settings.get('debugtoolbar.enabled', False):
330 334 # if no toolbar, then any exception gets caught and rendered
331 335 config.add_view(error_handler, context=Exception)
332 336
333 337 config.add_view(error_handler, context=HTTPError)
334 338
335 339
336 340 def includeme_first(config):
337 341 # redirect automatic browser favicon.ico requests to correct place
338 342 def favicon_redirect(context, request):
339 343 return HTTPFound(
340 344 request.static_path('rhodecode:public/images/favicon.ico'))
341 345
342 346 config.add_view(favicon_redirect, route_name='favicon')
343 347 config.add_route('favicon', '/favicon.ico')
344 348
345 349 def robots_redirect(context, request):
346 350 return HTTPFound(
347 351 request.static_path('rhodecode:public/robots.txt'))
348 352
349 353 config.add_view(robots_redirect, route_name='robots')
350 354 config.add_route('robots', '/robots.txt')
351 355
352 356 config.add_static_view(
353 357 '_static/deform', 'deform:static')
354 358 config.add_static_view(
355 359 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
356 360
357 361
358 362 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
359 363 """
360 364 Apply outer WSGI middlewares around the application.
361 365
362 366 Part of this has been moved up from the Pylons layer, so that the
363 367 data is also available if old Pylons code is hit through an already ported
364 368 view.
365 369 """
366 370 settings = config.registry.settings
367 371
368 372 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
369 373 pyramid_app = HttpsFixup(pyramid_app, settings)
370 374
371 375 # Add RoutesMiddleware to support the pylons compatibility tween during
372 376 # migration to pyramid.
373 377 pyramid_app = SkippableRoutesMiddleware(
374 378 pyramid_app, config.registry._pylons_compat_config['routes.map'],
375 379 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
376 380
377 381 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
378 382
379 383 if settings['gzip_responses']:
380 384 pyramid_app = make_gzip_middleware(
381 385 pyramid_app, settings, compress_level=1)
382 386
383 387 # this should be the outer most middleware in the wsgi stack since
384 388 # middleware like Routes make database calls
385 389 def pyramid_app_with_cleanup(environ, start_response):
386 390 try:
387 391 return pyramid_app(environ, start_response)
388 392 finally:
389 393 # Dispose current database session and rollback uncommitted
390 394 # transactions.
391 395 meta.Session.remove()
392 396
393 397 # In a single threaded mode server, on non sqlite db we should have
394 398 # '0 Current Checked out connections' at the end of a request,
395 399 # if not, then something, somewhere is leaving a connection open
396 400 pool = meta.Base.metadata.bind.engine.pool
397 401 log.debug('sa pool status: %s', pool.status())
398 402
399 403
400 404 return pyramid_app_with_cleanup
401 405
402 406
403 407 def sanitize_settings_and_apply_defaults(settings):
404 408 """
405 409 Applies settings defaults and does all type conversion.
406 410
407 411 We would move all settings parsing and preparation into this place, so that
408 412 we have only one place left which deals with this part. The remaining parts
409 413 of the application would start to rely fully on well prepared settings.
410 414
411 415 This piece would later be split up per topic to avoid a big fat monster
412 416 function.
413 417 """
414 418
415 419 # Pyramid's mako renderer has to search in the templates folder so that the
416 420 # old templates still work. Ported and new templates are expected to use
417 421 # real asset specifications for the includes.
418 422 mako_directories = settings.setdefault('mako.directories', [
419 423 # Base templates of the original Pylons application
420 424 'rhodecode:templates',
421 425 ])
422 426 log.debug(
423 427 "Using the following Mako template directories: %s",
424 428 mako_directories)
425 429
426 430 # Default includes, possible to change as a user
427 431 pyramid_includes = settings.setdefault('pyramid.includes', [
428 432 'rhodecode.lib.middleware.request_wrapper',
429 433 ])
430 434 log.debug(
431 435 "Using the following pyramid.includes: %s",
432 436 pyramid_includes)
433 437
434 438 # TODO: johbo: Re-think this, usually the call to config.include
435 439 # should allow to pass in a prefix.
436 440 settings.setdefault('rhodecode.api.url', '/_admin/api')
437 441
438 442 # Sanitize generic settings.
439 443 _list_setting(settings, 'default_encoding', 'UTF-8')
440 444 _bool_setting(settings, 'is_test', 'false')
441 445 _bool_setting(settings, 'gzip_responses', 'false')
442 446
443 447 # Call split out functions that sanitize settings for each topic.
444 448 _sanitize_appenlight_settings(settings)
445 449 _sanitize_vcs_settings(settings)
446 450
447 451 return settings
448 452
449 453
450 454 def _sanitize_appenlight_settings(settings):
451 455 _bool_setting(settings, 'appenlight', 'false')
452 456
453 457
454 458 def _sanitize_vcs_settings(settings):
455 459 """
456 460 Applies settings defaults and does type conversion for all VCS related
457 461 settings.
458 462 """
459 463 _string_setting(settings, 'vcs.svn.compatible_version', '')
460 464 _string_setting(settings, 'git_rev_filter', '--all')
461 465 _string_setting(settings, 'vcs.hooks.protocol', 'http')
462 466 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
463 467 _string_setting(settings, 'vcs.server', '')
464 468 _string_setting(settings, 'vcs.server.log_level', 'debug')
465 469 _string_setting(settings, 'vcs.server.protocol', 'http')
466 470 _bool_setting(settings, 'startup.import_repos', 'false')
467 471 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
468 472 _bool_setting(settings, 'vcs.server.enable', 'true')
469 473 _bool_setting(settings, 'vcs.start_server', 'false')
470 474 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
471 475 _int_setting(settings, 'vcs.connection_timeout', 3600)
472 476
473 477 # Support legacy values of vcs.scm_app_implementation. Legacy
474 478 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
475 479 # which is now mapped to 'http'.
476 480 scm_app_impl = settings['vcs.scm_app_implementation']
477 481 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
478 482 settings['vcs.scm_app_implementation'] = 'http'
479 483
480 484
481 485 def _int_setting(settings, name, default):
482 486 settings[name] = int(settings.get(name, default))
483 487
484 488
485 489 def _bool_setting(settings, name, default):
486 490 input = settings.get(name, default)
487 491 if isinstance(input, unicode):
488 492 input = input.encode('utf8')
489 493 settings[name] = asbool(input)
490 494
491 495
492 496 def _list_setting(settings, name, default):
493 497 raw_value = settings.get(name, default)
494 498
495 499 old_separator = ','
496 500 if old_separator in raw_value:
497 501 # If we get a comma separated list, pass it to our own function.
498 502 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
499 503 else:
500 504 # Otherwise we assume it uses pyramids space/newline separation.
501 505 settings[name] = aslist(raw_value)
502 506
503 507
504 508 def _string_setting(settings, name, default, lower=True):
505 509 value = settings.get(name, default)
506 510 if lower:
507 511 value = value.lower()
508 512 settings[name] = value
General Comments 0
You need to be logged in to leave comments. Login now