##// END OF EJS Templates
errorpages: use original http status for rendered error page
dan -
r190:299c0020 default
parent child Browse files
Show More
@@ -1,351 +1,358 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25
25
26 from paste.registry import RegistryManager
26 from paste.registry import RegistryManager
27 from paste.gzipper import make_gzip_middleware
27 from paste.gzipper import make_gzip_middleware
28 from pylons.wsgiapp import PylonsApp
28 from pylons.wsgiapp import PylonsApp
29 from pyramid.authorization import ACLAuthorizationPolicy
29 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.config import Configurator
30 from pyramid.config import Configurator
31 from pyramid.static import static_view
31 from pyramid.static import static_view
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import HTTPError
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
35 import pyramid.httpexceptions as httpexceptions
35 import pyramid.httpexceptions as httpexceptions
36 from pyramid.renderers import render_to_response
36 from pyramid.renderers import render_to_response, render
37 from routes.middleware import RoutesMiddleware
37 from routes.middleware import RoutesMiddleware
38 import routes.util
38 import routes.util
39
39
40 import rhodecode
40 import rhodecode
41 from rhodecode.config import patches
41 from rhodecode.config import patches
42 from rhodecode.config.environment import (
42 from rhodecode.config.environment import (
43 load_environment, load_pyramid_environment)
43 load_environment, load_pyramid_environment)
44 from rhodecode.lib.middleware import csrf
44 from rhodecode.lib.middleware import csrf
45 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
45 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
46 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 from rhodecode.lib.middleware.vcs import VCSMiddleware
48 from rhodecode.lib.middleware.vcs import VCSMiddleware
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50
50
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
55 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
56 """Create a Pylons WSGI application and return it
56 """Create a Pylons WSGI application and return it
57
57
58 ``global_conf``
58 ``global_conf``
59 The inherited configuration for this application. Normally from
59 The inherited configuration for this application. Normally from
60 the [DEFAULT] section of the Paste ini file.
60 the [DEFAULT] section of the Paste ini file.
61
61
62 ``full_stack``
62 ``full_stack``
63 Whether or not this application provides a full WSGI stack (by
63 Whether or not this application provides a full WSGI stack (by
64 default, meaning it handles its own exceptions and errors).
64 default, meaning it handles its own exceptions and errors).
65 Disable full_stack when this application is "managed" by
65 Disable full_stack when this application is "managed" by
66 another WSGI middleware.
66 another WSGI middleware.
67
67
68 ``app_conf``
68 ``app_conf``
69 The application's local configuration. Normally specified in
69 The application's local configuration. Normally specified in
70 the [app:<name>] section of the Paste ini file (where <name>
70 the [app:<name>] section of the Paste ini file (where <name>
71 defaults to main).
71 defaults to main).
72
72
73 """
73 """
74 # Apply compatibility patches
74 # Apply compatibility patches
75 patches.kombu_1_5_1_python_2_7_11()
75 patches.kombu_1_5_1_python_2_7_11()
76 patches.inspect_getargspec()
76 patches.inspect_getargspec()
77
77
78 # Configure the Pylons environment
78 # Configure the Pylons environment
79 config = load_environment(global_conf, app_conf)
79 config = load_environment(global_conf, app_conf)
80
80
81 # The Pylons WSGI app
81 # The Pylons WSGI app
82 app = PylonsApp(config=config)
82 app = PylonsApp(config=config)
83 if rhodecode.is_test:
83 if rhodecode.is_test:
84 app = csrf.CSRFDetector(app)
84 app = csrf.CSRFDetector(app)
85
85
86 expected_origin = config.get('expected_origin')
86 expected_origin = config.get('expected_origin')
87 if expected_origin:
87 if expected_origin:
88 # The API can be accessed from other Origins.
88 # The API can be accessed from other Origins.
89 app = csrf.OriginChecker(app, expected_origin,
89 app = csrf.OriginChecker(app, expected_origin,
90 skip_urls=[routes.util.url_for('api')])
90 skip_urls=[routes.util.url_for('api')])
91
91
92
92
93 if asbool(full_stack):
93 if asbool(full_stack):
94
94
95 # Appenlight monitoring and error handler
95 # Appenlight monitoring and error handler
96 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
96 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
97
97
98 # we want our low level middleware to get to the request ASAP. We don't
98 # we want our low level middleware to get to the request ASAP. We don't
99 # need any pylons stack middleware in them
99 # need any pylons stack middleware in them
100 app = VCSMiddleware(app, config, appenlight_client)
100 app = VCSMiddleware(app, config, appenlight_client)
101
101
102 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
102 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
103 app = HttpsFixup(app, config)
103 app = HttpsFixup(app, config)
104
104
105 # Establish the Registry for this application
105 # Establish the Registry for this application
106 app = RegistryManager(app)
106 app = RegistryManager(app)
107
107
108 app.config = config
108 app.config = config
109
109
110 return app
110 return app
111
111
112
112
113 def make_pyramid_app(global_config, **settings):
113 def make_pyramid_app(global_config, **settings):
114 """
114 """
115 Constructs the WSGI application based on Pyramid and wraps the Pylons based
115 Constructs the WSGI application based on Pyramid and wraps the Pylons based
116 application.
116 application.
117
117
118 Specials:
118 Specials:
119
119
120 * We migrate from Pylons to Pyramid. While doing this, we keep both
120 * We migrate from Pylons to Pyramid. While doing this, we keep both
121 frameworks functional. This involves moving some WSGI middlewares around
121 frameworks functional. This involves moving some WSGI middlewares around
122 and providing access to some data internals, so that the old code is
122 and providing access to some data internals, so that the old code is
123 still functional.
123 still functional.
124
124
125 * The application can also be integrated like a plugin via the call to
125 * The application can also be integrated like a plugin via the call to
126 `includeme`. This is accompanied with the other utility functions which
126 `includeme`. This is accompanied with the other utility functions which
127 are called. Changing this should be done with great care to not break
127 are called. Changing this should be done with great care to not break
128 cases when these fragments are assembled from another place.
128 cases when these fragments are assembled from another place.
129
129
130 """
130 """
131 # The edition string should be available in pylons too, so we add it here
131 # The edition string should be available in pylons too, so we add it here
132 # before copying the settings.
132 # before copying the settings.
133 settings.setdefault('rhodecode.edition', 'Community Edition')
133 settings.setdefault('rhodecode.edition', 'Community Edition')
134
134
135 # As long as our Pylons application does expect "unprepared" settings, make
135 # As long as our Pylons application does expect "unprepared" settings, make
136 # sure that we keep an unmodified copy. This avoids unintentional change of
136 # sure that we keep an unmodified copy. This avoids unintentional change of
137 # behavior in the old application.
137 # behavior in the old application.
138 settings_pylons = settings.copy()
138 settings_pylons = settings.copy()
139
139
140 sanitize_settings_and_apply_defaults(settings)
140 sanitize_settings_and_apply_defaults(settings)
141 config = Configurator(settings=settings)
141 config = Configurator(settings=settings)
142 add_pylons_compat_data(config.registry, global_config, settings_pylons)
142 add_pylons_compat_data(config.registry, global_config, settings_pylons)
143
143
144 load_pyramid_environment(global_config, settings)
144 load_pyramid_environment(global_config, settings)
145
145
146 includeme(config)
146 includeme(config)
147 includeme_last(config)
147 includeme_last(config)
148 pyramid_app = config.make_wsgi_app()
148 pyramid_app = config.make_wsgi_app()
149 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
149 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
150 return pyramid_app
150 return pyramid_app
151
151
152
152
153 def add_pylons_compat_data(registry, global_config, settings):
153 def add_pylons_compat_data(registry, global_config, settings):
154 """
154 """
155 Attach data to the registry to support the Pylons integration.
155 Attach data to the registry to support the Pylons integration.
156 """
156 """
157 registry._pylons_compat_global_config = global_config
157 registry._pylons_compat_global_config = global_config
158 registry._pylons_compat_settings = settings
158 registry._pylons_compat_settings = settings
159
159
160
160
161 def error_handler(exc, request):
161 def error_handler(exc, request):
162 # TODO: dan: replace the old pylons error controller with this
162 # TODO: dan: replace the old pylons error controller with this
163 from rhodecode.model.settings import SettingsModel
163 from rhodecode.model.settings import SettingsModel
164 from rhodecode.lib.utils2 import AttributeDict
164 from rhodecode.lib.utils2 import AttributeDict
165
165
166 try:
166 try:
167 rc_config = SettingsModel().get_all_settings()
167 rc_config = SettingsModel().get_all_settings()
168 except Exception:
168 except Exception:
169 log.exception('failed to fetch settings')
169 log.exception('failed to fetch settings')
170 rc_config = {}
170 rc_config = {}
171
171
172 c = AttributeDict()
172 c = AttributeDict()
173 c.error_message = exc.status
173 c.error_message = exc.status
174 c.error_explanation = exc.explanation or str(exc)
174 c.error_explanation = exc.explanation or str(exc)
175 c.visual = AttributeDict()
175 c.visual = AttributeDict()
176
176
177 c.visual.rhodecode_support_url = (
177 c.visual.rhodecode_support_url = (
178 request.registry.settings.get('rhodecode_support_url') or
178 request.registry.settings.get('rhodecode_support_url') or
179 request.route_url('rhodecode_support')
179 request.route_url('rhodecode_support')
180 )
180 )
181 c.redirect_time = 0
181 c.redirect_time = 0
182 c.rhodecode_name = rc_config.get('rhodecode_title')
182 c.rhodecode_name = rc_config.get('rhodecode_title')
183 if not c.rhodecode_name:
183 if not c.rhodecode_name:
184 c.rhodecode_name = 'Rhodecode'
184 c.rhodecode_name = 'Rhodecode'
185
185
186 base_response = HTTPInternalServerError()
187 # prefer original exception for the response since it may have headers set
188 if isinstance(exc, HTTPError):
189 base_response = exc
190
186 response = render_to_response(
191 response = render_to_response(
187 '/errors/error_document.html', {'c': c}, request=request)
192 '/errors/error_document.html', {'c': c}, request=request,
193 response=base_response)
194
188 return response
195 return response
189
196
190
197
191 def includeme(config):
198 def includeme(config):
192 settings = config.registry.settings
199 settings = config.registry.settings
193
200
194 # Includes which are required. The application would fail without them.
201 # Includes which are required. The application would fail without them.
195 config.include('pyramid_mako')
202 config.include('pyramid_mako')
196 config.include('pyramid_beaker')
203 config.include('pyramid_beaker')
197 config.include('rhodecode.authentication')
204 config.include('rhodecode.authentication')
198 config.include('rhodecode.login')
205 config.include('rhodecode.login')
199 config.include('rhodecode.tweens')
206 config.include('rhodecode.tweens')
200 config.include('rhodecode.api')
207 config.include('rhodecode.api')
201 config.add_route(
208 config.add_route(
202 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
209 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
203
210
204 # Set the authorization policy.
211 # Set the authorization policy.
205 authz_policy = ACLAuthorizationPolicy()
212 authz_policy = ACLAuthorizationPolicy()
206 config.set_authorization_policy(authz_policy)
213 config.set_authorization_policy(authz_policy)
207
214
208 # Set the default renderer for HTML templates to mako.
215 # Set the default renderer for HTML templates to mako.
209 config.add_mako_renderer('.html')
216 config.add_mako_renderer('.html')
210
217
211 # plugin information
218 # plugin information
212 config.registry.rhodecode_plugins = {}
219 config.registry.rhodecode_plugins = {}
213
220
214 config.add_directive(
221 config.add_directive(
215 'register_rhodecode_plugin', register_rhodecode_plugin)
222 'register_rhodecode_plugin', register_rhodecode_plugin)
216 # include RhodeCode plugins
223 # include RhodeCode plugins
217 includes = aslist(settings.get('rhodecode.includes', []))
224 includes = aslist(settings.get('rhodecode.includes', []))
218 for inc in includes:
225 for inc in includes:
219 config.include(inc)
226 config.include(inc)
220
227
221 pylons_app = make_app(
228 pylons_app = make_app(
222 config.registry._pylons_compat_global_config,
229 config.registry._pylons_compat_global_config,
223 **config.registry._pylons_compat_settings)
230 **config.registry._pylons_compat_settings)
224 config.registry._pylons_compat_config = pylons_app.config
231 config.registry._pylons_compat_config = pylons_app.config
225
232
226 pylons_app_as_view = wsgiapp(pylons_app)
233 pylons_app_as_view = wsgiapp(pylons_app)
227
234
228 # Protect from VCS Server error related pages when server is not available
235 # Protect from VCS Server error related pages when server is not available
229 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
236 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
230 if not vcs_server_enabled:
237 if not vcs_server_enabled:
231 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
238 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
232
239
233
240
234 def pylons_app_with_error_handler(context, request):
241 def pylons_app_with_error_handler(context, request):
235 """
242 """
236 Handle exceptions from rc pylons app:
243 Handle exceptions from rc pylons app:
237
244
238 - old webob type exceptions get converted to pyramid exceptions
245 - old webob type exceptions get converted to pyramid exceptions
239 - pyramid exceptions are passed to the error handler view
246 - pyramid exceptions are passed to the error handler view
240 """
247 """
241 try:
248 try:
242 response = pylons_app_as_view(context, request)
249 response = pylons_app_as_view(context, request)
243 if 400 <= response.status_int <= 599: # webob type error responses
250 if 400 <= response.status_int <= 599: # webob type error responses
244 ExcClass = httpexceptions.status_map[response.status_int]
251 ExcClass = httpexceptions.status_map[response.status_int]
245 return error_handler(ExcClass(response.status), request)
252 return error_handler(ExcClass(response.status), request)
246 except HTTPError as e: # pyramid type exceptions
253 except HTTPError as e: # pyramid type exceptions
247 return error_handler(e, request)
254 return error_handler(e, request)
248
255
249 return response
256 return response
250
257
251 # This is the glue which allows us to migrate in chunks. By registering the
258 # This is the glue which allows us to migrate in chunks. By registering the
252 # pylons based application as the "Not Found" view in Pyramid, we will
259 # pylons based application as the "Not Found" view in Pyramid, we will
253 # fallback to the old application each time the new one does not yet know
260 # fallback to the old application each time the new one does not yet know
254 # how to handle a request.
261 # how to handle a request.
255 config.add_notfound_view(pylons_app_with_error_handler)
262 config.add_notfound_view(pylons_app_with_error_handler)
256
263
257 config.add_view(error_handler, context=HTTPError) # exceptions in rc pyramid
264 config.add_view(error_handler, context=HTTPError) # exceptions in rc pyramid
258
265
259 def includeme_last(config):
266 def includeme_last(config):
260 """
267 """
261 The static file catchall needs to be last in the view configuration.
268 The static file catchall needs to be last in the view configuration.
262 """
269 """
263 settings = config.registry.settings
270 settings = config.registry.settings
264
271
265 # Note: johbo: I would prefer to register a prefix for static files at some
272 # Note: johbo: I would prefer to register a prefix for static files at some
266 # point, e.g. move them under '_static/'. This would fully avoid that we
273 # point, e.g. move them under '_static/'. This would fully avoid that we
267 # can have name clashes with a repository name. Imaging someone calling his
274 # can have name clashes with a repository name. Imaging someone calling his
268 # repo "css" ;-) Also having an external web server to serve out the static
275 # repo "css" ;-) Also having an external web server to serve out the static
269 # files seems to be easier to set up if they have a common prefix.
276 # files seems to be easier to set up if they have a common prefix.
270 #
277 #
271 # Example: config.add_static_view('_static', path='rhodecode:public')
278 # Example: config.add_static_view('_static', path='rhodecode:public')
272 #
279 #
273 # It might be an option to register both paths for a while and then migrate
280 # It might be an option to register both paths for a while and then migrate
274 # over to the new location.
281 # over to the new location.
275
282
276 # Serving static files with a catchall.
283 # Serving static files with a catchall.
277 if settings['static_files']:
284 if settings['static_files']:
278 config.add_route('catchall_static', '/*subpath')
285 config.add_route('catchall_static', '/*subpath')
279 config.add_view(
286 config.add_view(
280 static_view('rhodecode:public'), route_name='catchall_static')
287 static_view('rhodecode:public'), route_name='catchall_static')
281
288
282
289
283 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
290 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
284 """
291 """
285 Apply outer WSGI middlewares around the application.
292 Apply outer WSGI middlewares around the application.
286
293
287 Part of this has been moved up from the Pylons layer, so that the
294 Part of this has been moved up from the Pylons layer, so that the
288 data is also available if old Pylons code is hit through an already ported
295 data is also available if old Pylons code is hit through an already ported
289 view.
296 view.
290 """
297 """
291 settings = config.registry.settings
298 settings = config.registry.settings
292
299
293 # Add RoutesMiddleware to support the pylons compatibility tween during
300 # Add RoutesMiddleware to support the pylons compatibility tween during
294 # migration to pyramid.
301 # migration to pyramid.
295 pyramid_app = RoutesMiddleware(
302 pyramid_app = RoutesMiddleware(
296 pyramid_app, config.registry._pylons_compat_config['routes.map'])
303 pyramid_app, config.registry._pylons_compat_config['routes.map'])
297
304
298 # TODO: johbo: Don't really see why we enable the gzip middleware when
305 # TODO: johbo: Don't really see why we enable the gzip middleware when
299 # serving static files, might be something that should have its own setting
306 # serving static files, might be something that should have its own setting
300 # as well?
307 # as well?
301 if settings['static_files']:
308 if settings['static_files']:
302 pyramid_app = make_gzip_middleware(
309 pyramid_app = make_gzip_middleware(
303 pyramid_app, settings, compress_level=1)
310 pyramid_app, settings, compress_level=1)
304
311
305 return pyramid_app
312 return pyramid_app
306
313
307
314
308 def sanitize_settings_and_apply_defaults(settings):
315 def sanitize_settings_and_apply_defaults(settings):
309 """
316 """
310 Applies settings defaults and does all type conversion.
317 Applies settings defaults and does all type conversion.
311
318
312 We would move all settings parsing and preparation into this place, so that
319 We would move all settings parsing and preparation into this place, so that
313 we have only one place left which deals with this part. The remaining parts
320 we have only one place left which deals with this part. The remaining parts
314 of the application would start to rely fully on well prepared settings.
321 of the application would start to rely fully on well prepared settings.
315
322
316 This piece would later be split up per topic to avoid a big fat monster
323 This piece would later be split up per topic to avoid a big fat monster
317 function.
324 function.
318 """
325 """
319
326
320 # Pyramid's mako renderer has to search in the templates folder so that the
327 # Pyramid's mako renderer has to search in the templates folder so that the
321 # old templates still work. Ported and new templates are expected to use
328 # old templates still work. Ported and new templates are expected to use
322 # real asset specifications for the includes.
329 # real asset specifications for the includes.
323 mako_directories = settings.setdefault('mako.directories', [
330 mako_directories = settings.setdefault('mako.directories', [
324 # Base templates of the original Pylons application
331 # Base templates of the original Pylons application
325 'rhodecode:templates',
332 'rhodecode:templates',
326 ])
333 ])
327 log.debug(
334 log.debug(
328 "Using the following Mako template directories: %s",
335 "Using the following Mako template directories: %s",
329 mako_directories)
336 mako_directories)
330
337
331 # Default includes, possible to change as a user
338 # Default includes, possible to change as a user
332 pyramid_includes = settings.setdefault('pyramid.includes', [
339 pyramid_includes = settings.setdefault('pyramid.includes', [
333 'rhodecode.lib.middleware.request_wrapper',
340 'rhodecode.lib.middleware.request_wrapper',
334 ])
341 ])
335 log.debug(
342 log.debug(
336 "Using the following pyramid.includes: %s",
343 "Using the following pyramid.includes: %s",
337 pyramid_includes)
344 pyramid_includes)
338
345
339 # TODO: johbo: Re-think this, usually the call to config.include
346 # TODO: johbo: Re-think this, usually the call to config.include
340 # should allow to pass in a prefix.
347 # should allow to pass in a prefix.
341 settings.setdefault('rhodecode.api.url', '/_admin/api')
348 settings.setdefault('rhodecode.api.url', '/_admin/api')
342
349
343 _bool_setting(settings, 'vcs.server.enable', 'true')
350 _bool_setting(settings, 'vcs.server.enable', 'true')
344 _bool_setting(settings, 'static_files', 'true')
351 _bool_setting(settings, 'static_files', 'true')
345 _bool_setting(settings, 'is_test', 'false')
352 _bool_setting(settings, 'is_test', 'false')
346
353
347 return settings
354 return settings
348
355
349
356
350 def _bool_setting(settings, name, default):
357 def _bool_setting(settings, name, default):
351 settings[name] = asbool(settings.get(name, default))
358 settings[name] = asbool(settings.get(name, default))
General Comments 0
You need to be logged in to leave comments. Login now