##// END OF EJS Templates
events: make sure we propagate our dummy request with proper application_url....
marcink -
r1960:c9da1578 default
parent child Browse files
Show More
@@ -1,702 +1,706 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 from pylons import config, tmpl_context as c, request, url
37 from pylons import config, tmpl_context as c, request, url
38 from pylons.controllers import WSGIController
38 from pylons.controllers import WSGIController
39 from pylons.controllers.util import redirect
39 from pylons.controllers.util import redirect
40 from pylons.i18n import translation
40 from pylons.i18n import translation
41 # marcink: don't remove this import
41 # marcink: don't remove this import
42 from pylons.templating import render_mako, pylons_globals, literal, cached_template
42 from pylons.templating import render_mako, pylons_globals, literal, cached_template
43 from pylons.i18n.translation import _
43 from pylons.i18n.translation import _
44 from webob.exc import HTTPFound
44 from webob.exc import HTTPFound
45
45
46
46
47 import rhodecode
47 import rhodecode
48 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.authentication.base import VCS_TYPE
49 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import auth, utils2
50 from rhodecode.lib import helpers as h
50 from rhodecode.lib import helpers as h
51 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
52 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.exceptions import UserCreationError
53 from rhodecode.lib.utils import (
53 from rhodecode.lib.utils import (
54 get_repo_slug, set_rhodecode_config, password_changed,
54 get_repo_slug, set_rhodecode_config, password_changed,
55 get_enabled_hook_classes)
55 get_enabled_hook_classes)
56 from rhodecode.lib.utils2 import (
56 from rhodecode.lib.utils2 import (
57 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
58 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
59 from rhodecode.model import meta
59 from rhodecode.model import meta
60 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.db import Repository, User, ChangesetComment
61 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.notification import NotificationModel
62 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
64
64
65
65
66 log = logging.getLogger(__name__)
66 log = logging.getLogger(__name__)
67
67
68
68
69 # hack to make the migration to pyramid easier
69 # hack to make the migration to pyramid easier
70 def render(template_name, extra_vars=None, cache_key=None,
70 def render(template_name, extra_vars=None, cache_key=None,
71 cache_type=None, cache_expire=None):
71 cache_type=None, cache_expire=None):
72 """Render a template with Mako
72 """Render a template with Mako
73
73
74 Accepts the cache options ``cache_key``, ``cache_type``, and
74 Accepts the cache options ``cache_key``, ``cache_type``, and
75 ``cache_expire``.
75 ``cache_expire``.
76
76
77 """
77 """
78 # Create a render callable for the cache function
78 # Create a render callable for the cache function
79 def render_template():
79 def render_template():
80 # Pull in extra vars if needed
80 # Pull in extra vars if needed
81 globs = extra_vars or {}
81 globs = extra_vars or {}
82
82
83 # Second, get the globals
83 # Second, get the globals
84 globs.update(pylons_globals())
84 globs.update(pylons_globals())
85
85
86 globs['_ungettext'] = globs['ungettext']
86 globs['_ungettext'] = globs['ungettext']
87 # Grab a template reference
87 # Grab a template reference
88 template = globs['app_globals'].mako_lookup.get_template(template_name)
88 template = globs['app_globals'].mako_lookup.get_template(template_name)
89
89
90 return literal(template.render_unicode(**globs))
90 return literal(template.render_unicode(**globs))
91
91
92 return cached_template(template_name, render_template, cache_key=cache_key,
92 return cached_template(template_name, render_template, cache_key=cache_key,
93 cache_type=cache_type, cache_expire=cache_expire)
93 cache_type=cache_type, cache_expire=cache_expire)
94
94
95 def _filter_proxy(ip):
95 def _filter_proxy(ip):
96 """
96 """
97 Passed in IP addresses in HEADERS can be in a special format of multiple
97 Passed in IP addresses in HEADERS can be in a special format of multiple
98 ips. Those comma separated IPs are passed from various proxies in the
98 ips. Those comma separated IPs are passed from various proxies in the
99 chain of request processing. The left-most being the original client.
99 chain of request processing. The left-most being the original client.
100 We only care about the first IP which came from the org. client.
100 We only care about the first IP which came from the org. client.
101
101
102 :param ip: ip string from headers
102 :param ip: ip string from headers
103 """
103 """
104 if ',' in ip:
104 if ',' in ip:
105 _ips = ip.split(',')
105 _ips = ip.split(',')
106 _first_ip = _ips[0].strip()
106 _first_ip = _ips[0].strip()
107 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
107 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
108 return _first_ip
108 return _first_ip
109 return ip
109 return ip
110
110
111
111
112 def _filter_port(ip):
112 def _filter_port(ip):
113 """
113 """
114 Removes a port from ip, there are 4 main cases to handle here.
114 Removes a port from ip, there are 4 main cases to handle here.
115 - ipv4 eg. 127.0.0.1
115 - ipv4 eg. 127.0.0.1
116 - ipv6 eg. ::1
116 - ipv6 eg. ::1
117 - ipv4+port eg. 127.0.0.1:8080
117 - ipv4+port eg. 127.0.0.1:8080
118 - ipv6+port eg. [::1]:8080
118 - ipv6+port eg. [::1]:8080
119
119
120 :param ip:
120 :param ip:
121 """
121 """
122 def is_ipv6(ip_addr):
122 def is_ipv6(ip_addr):
123 if hasattr(socket, 'inet_pton'):
123 if hasattr(socket, 'inet_pton'):
124 try:
124 try:
125 socket.inet_pton(socket.AF_INET6, ip_addr)
125 socket.inet_pton(socket.AF_INET6, ip_addr)
126 except socket.error:
126 except socket.error:
127 return False
127 return False
128 else:
128 else:
129 # fallback to ipaddress
129 # fallback to ipaddress
130 try:
130 try:
131 ipaddress.IPv6Address(safe_unicode(ip_addr))
131 ipaddress.IPv6Address(safe_unicode(ip_addr))
132 except Exception:
132 except Exception:
133 return False
133 return False
134 return True
134 return True
135
135
136 if ':' not in ip: # must be ipv4 pure ip
136 if ':' not in ip: # must be ipv4 pure ip
137 return ip
137 return ip
138
138
139 if '[' in ip and ']' in ip: # ipv6 with port
139 if '[' in ip and ']' in ip: # ipv6 with port
140 return ip.split(']')[0][1:].lower()
140 return ip.split(']')[0][1:].lower()
141
141
142 # must be ipv6 or ipv4 with port
142 # must be ipv6 or ipv4 with port
143 if is_ipv6(ip):
143 if is_ipv6(ip):
144 return ip
144 return ip
145 else:
145 else:
146 ip, _port = ip.split(':')[:2] # means ipv4+port
146 ip, _port = ip.split(':')[:2] # means ipv4+port
147 return ip
147 return ip
148
148
149
149
150 def get_ip_addr(environ):
150 def get_ip_addr(environ):
151 proxy_key = 'HTTP_X_REAL_IP'
151 proxy_key = 'HTTP_X_REAL_IP'
152 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
152 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
153 def_key = 'REMOTE_ADDR'
153 def_key = 'REMOTE_ADDR'
154 _filters = lambda x: _filter_port(_filter_proxy(x))
154 _filters = lambda x: _filter_port(_filter_proxy(x))
155
155
156 ip = environ.get(proxy_key)
156 ip = environ.get(proxy_key)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(proxy_key2)
160 ip = environ.get(proxy_key2)
161 if ip:
161 if ip:
162 return _filters(ip)
162 return _filters(ip)
163
163
164 ip = environ.get(def_key, '0.0.0.0')
164 ip = environ.get(def_key, '0.0.0.0')
165 return _filters(ip)
165 return _filters(ip)
166
166
167
167
168 def get_server_ip_addr(environ, log_errors=True):
168 def get_server_ip_addr(environ, log_errors=True):
169 hostname = environ.get('SERVER_NAME')
169 hostname = environ.get('SERVER_NAME')
170 try:
170 try:
171 return socket.gethostbyname(hostname)
171 return socket.gethostbyname(hostname)
172 except Exception as e:
172 except Exception as e:
173 if log_errors:
173 if log_errors:
174 # in some cases this lookup is not possible, and we don't want to
174 # in some cases this lookup is not possible, and we don't want to
175 # make it an exception in logs
175 # make it an exception in logs
176 log.exception('Could not retrieve server ip address: %s', e)
176 log.exception('Could not retrieve server ip address: %s', e)
177 return hostname
177 return hostname
178
178
179
179
180 def get_server_port(environ):
180 def get_server_port(environ):
181 return environ.get('SERVER_PORT')
181 return environ.get('SERVER_PORT')
182
182
183
183
184 def get_access_path(environ):
184 def get_access_path(environ):
185 path = environ.get('PATH_INFO')
185 path = environ.get('PATH_INFO')
186 org_req = environ.get('pylons.original_request')
186 org_req = environ.get('pylons.original_request')
187 if org_req:
187 if org_req:
188 path = org_req.environ.get('PATH_INFO')
188 path = org_req.environ.get('PATH_INFO')
189 return path
189 return path
190
190
191
191
192 def get_user_agent(environ):
192 def get_user_agent(environ):
193 return environ.get('HTTP_USER_AGENT')
193 return environ.get('HTTP_USER_AGENT')
194
194
195
195
196 def vcs_operation_context(
196 def vcs_operation_context(
197 environ, repo_name, username, action, scm, check_locking=True,
197 environ, repo_name, username, action, scm, check_locking=True,
198 is_shadow_repo=False):
198 is_shadow_repo=False):
199 """
199 """
200 Generate the context for a vcs operation, e.g. push or pull.
200 Generate the context for a vcs operation, e.g. push or pull.
201
201
202 This context is passed over the layers so that hooks triggered by the
202 This context is passed over the layers so that hooks triggered by the
203 vcs operation know details like the user, the user's IP address etc.
203 vcs operation know details like the user, the user's IP address etc.
204
204
205 :param check_locking: Allows to switch of the computation of the locking
205 :param check_locking: Allows to switch of the computation of the locking
206 data. This serves mainly the need of the simplevcs middleware to be
206 data. This serves mainly the need of the simplevcs middleware to be
207 able to disable this for certain operations.
207 able to disable this for certain operations.
208
208
209 """
209 """
210 # Tri-state value: False: unlock, None: nothing, True: lock
210 # Tri-state value: False: unlock, None: nothing, True: lock
211 make_lock = None
211 make_lock = None
212 locked_by = [None, None, None]
212 locked_by = [None, None, None]
213 is_anonymous = username == User.DEFAULT_USER
213 is_anonymous = username == User.DEFAULT_USER
214 if not is_anonymous and check_locking:
214 if not is_anonymous and check_locking:
215 log.debug('Checking locking on repository "%s"', repo_name)
215 log.debug('Checking locking on repository "%s"', repo_name)
216 user = User.get_by_username(username)
216 user = User.get_by_username(username)
217 repo = Repository.get_by_repo_name(repo_name)
217 repo = Repository.get_by_repo_name(repo_name)
218 make_lock, __, locked_by = repo.get_locking_state(
218 make_lock, __, locked_by = repo.get_locking_state(
219 action, user.user_id)
219 action, user.user_id)
220
220
221 settings_model = VcsSettingsModel(repo=repo_name)
221 settings_model = VcsSettingsModel(repo=repo_name)
222 ui_settings = settings_model.get_ui_settings()
222 ui_settings = settings_model.get_ui_settings()
223
223
224 extras = {
224 extras = {
225 'ip': get_ip_addr(environ),
225 'ip': get_ip_addr(environ),
226 'username': username,
226 'username': username,
227 'action': action,
227 'action': action,
228 'repository': repo_name,
228 'repository': repo_name,
229 'scm': scm,
229 'scm': scm,
230 'config': rhodecode.CONFIG['__file__'],
230 'config': rhodecode.CONFIG['__file__'],
231 'make_lock': make_lock,
231 'make_lock': make_lock,
232 'locked_by': locked_by,
232 'locked_by': locked_by,
233 'server_url': utils2.get_server_url(environ),
233 'server_url': utils2.get_server_url(environ),
234 'user_agent': get_user_agent(environ),
234 'user_agent': get_user_agent(environ),
235 'hooks': get_enabled_hook_classes(ui_settings),
235 'hooks': get_enabled_hook_classes(ui_settings),
236 'is_shadow_repo': is_shadow_repo,
236 'is_shadow_repo': is_shadow_repo,
237 }
237 }
238 return extras
238 return extras
239
239
240
240
241 class BasicAuth(AuthBasicAuthenticator):
241 class BasicAuth(AuthBasicAuthenticator):
242
242
243 def __init__(self, realm, authfunc, registry, auth_http_code=None,
243 def __init__(self, realm, authfunc, registry, auth_http_code=None,
244 initial_call_detection=False, acl_repo_name=None):
244 initial_call_detection=False, acl_repo_name=None):
245 self.realm = realm
245 self.realm = realm
246 self.initial_call = initial_call_detection
246 self.initial_call = initial_call_detection
247 self.authfunc = authfunc
247 self.authfunc = authfunc
248 self.registry = registry
248 self.registry = registry
249 self.acl_repo_name = acl_repo_name
249 self.acl_repo_name = acl_repo_name
250 self._rc_auth_http_code = auth_http_code
250 self._rc_auth_http_code = auth_http_code
251
251
252 def _get_response_from_code(self, http_code):
252 def _get_response_from_code(self, http_code):
253 try:
253 try:
254 return get_exception(safe_int(http_code))
254 return get_exception(safe_int(http_code))
255 except Exception:
255 except Exception:
256 log.exception('Failed to fetch response for code %s' % http_code)
256 log.exception('Failed to fetch response for code %s' % http_code)
257 return HTTPForbidden
257 return HTTPForbidden
258
258
259 def build_authentication(self):
259 def build_authentication(self):
260 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
261 if self._rc_auth_http_code and not self.initial_call:
261 if self._rc_auth_http_code and not self.initial_call:
262 # return alternative HTTP code if alternative http return code
262 # return alternative HTTP code if alternative http return code
263 # is specified in RhodeCode config, but ONLY if it's not the
263 # is specified in RhodeCode config, but ONLY if it's not the
264 # FIRST call
264 # FIRST call
265 custom_response_klass = self._get_response_from_code(
265 custom_response_klass = self._get_response_from_code(
266 self._rc_auth_http_code)
266 self._rc_auth_http_code)
267 return custom_response_klass(headers=head)
267 return custom_response_klass(headers=head)
268 return HTTPUnauthorized(headers=head)
268 return HTTPUnauthorized(headers=head)
269
269
270 def authenticate(self, environ):
270 def authenticate(self, environ):
271 authorization = AUTHORIZATION(environ)
271 authorization = AUTHORIZATION(environ)
272 if not authorization:
272 if not authorization:
273 return self.build_authentication()
273 return self.build_authentication()
274 (authmeth, auth) = authorization.split(' ', 1)
274 (authmeth, auth) = authorization.split(' ', 1)
275 if 'basic' != authmeth.lower():
275 if 'basic' != authmeth.lower():
276 return self.build_authentication()
276 return self.build_authentication()
277 auth = auth.strip().decode('base64')
277 auth = auth.strip().decode('base64')
278 _parts = auth.split(':', 1)
278 _parts = auth.split(':', 1)
279 if len(_parts) == 2:
279 if len(_parts) == 2:
280 username, password = _parts
280 username, password = _parts
281 if self.authfunc(
281 if self.authfunc(
282 username, password, environ, VCS_TYPE,
282 username, password, environ, VCS_TYPE,
283 registry=self.registry, acl_repo_name=self.acl_repo_name):
283 registry=self.registry, acl_repo_name=self.acl_repo_name):
284 return username
284 return username
285 if username and password:
285 if username and password:
286 # we mark that we actually executed authentication once, at
286 # we mark that we actually executed authentication once, at
287 # that point we can use the alternative auth code
287 # that point we can use the alternative auth code
288 self.initial_call = False
288 self.initial_call = False
289
289
290 return self.build_authentication()
290 return self.build_authentication()
291
291
292 __call__ = authenticate
292 __call__ = authenticate
293
293
294
294
295 def calculate_version_hash():
295 def calculate_version_hash():
296 return md5(
296 return md5(
297 config.get('beaker.session.secret', '') +
297 config.get('beaker.session.secret', '') +
298 rhodecode.__version__)[:8]
298 rhodecode.__version__)[:8]
299
299
300
300
301 def get_current_lang(request):
301 def get_current_lang(request):
302 # NOTE(marcink): remove after pyramid move
302 # NOTE(marcink): remove after pyramid move
303 try:
303 try:
304 return translation.get_lang()[0]
304 return translation.get_lang()[0]
305 except:
305 except:
306 pass
306 pass
307
307
308 return getattr(request, '_LOCALE_', request.locale_name)
308 return getattr(request, '_LOCALE_', request.locale_name)
309
309
310
310
311 def attach_context_attributes(context, request, user_id):
311 def attach_context_attributes(context, request, user_id):
312 """
312 """
313 Attach variables into template context called `c`, please note that
313 Attach variables into template context called `c`, please note that
314 request could be pylons or pyramid request in here.
314 request could be pylons or pyramid request in here.
315 """
315 """
316
316
317 rc_config = SettingsModel().get_all_settings(cache=True)
317 rc_config = SettingsModel().get_all_settings(cache=True)
318
318
319 context.rhodecode_version = rhodecode.__version__
319 context.rhodecode_version = rhodecode.__version__
320 context.rhodecode_edition = config.get('rhodecode.edition')
320 context.rhodecode_edition = config.get('rhodecode.edition')
321 # unique secret + version does not leak the version but keep consistency
321 # unique secret + version does not leak the version but keep consistency
322 context.rhodecode_version_hash = calculate_version_hash()
322 context.rhodecode_version_hash = calculate_version_hash()
323
323
324 # Default language set for the incoming request
324 # Default language set for the incoming request
325 context.language = get_current_lang(request)
325 context.language = get_current_lang(request)
326
326
327 # Visual options
327 # Visual options
328 context.visual = AttributeDict({})
328 context.visual = AttributeDict({})
329
329
330 # DB stored Visual Items
330 # DB stored Visual Items
331 context.visual.show_public_icon = str2bool(
331 context.visual.show_public_icon = str2bool(
332 rc_config.get('rhodecode_show_public_icon'))
332 rc_config.get('rhodecode_show_public_icon'))
333 context.visual.show_private_icon = str2bool(
333 context.visual.show_private_icon = str2bool(
334 rc_config.get('rhodecode_show_private_icon'))
334 rc_config.get('rhodecode_show_private_icon'))
335 context.visual.stylify_metatags = str2bool(
335 context.visual.stylify_metatags = str2bool(
336 rc_config.get('rhodecode_stylify_metatags'))
336 rc_config.get('rhodecode_stylify_metatags'))
337 context.visual.dashboard_items = safe_int(
337 context.visual.dashboard_items = safe_int(
338 rc_config.get('rhodecode_dashboard_items', 100))
338 rc_config.get('rhodecode_dashboard_items', 100))
339 context.visual.admin_grid_items = safe_int(
339 context.visual.admin_grid_items = safe_int(
340 rc_config.get('rhodecode_admin_grid_items', 100))
340 rc_config.get('rhodecode_admin_grid_items', 100))
341 context.visual.repository_fields = str2bool(
341 context.visual.repository_fields = str2bool(
342 rc_config.get('rhodecode_repository_fields'))
342 rc_config.get('rhodecode_repository_fields'))
343 context.visual.show_version = str2bool(
343 context.visual.show_version = str2bool(
344 rc_config.get('rhodecode_show_version'))
344 rc_config.get('rhodecode_show_version'))
345 context.visual.use_gravatar = str2bool(
345 context.visual.use_gravatar = str2bool(
346 rc_config.get('rhodecode_use_gravatar'))
346 rc_config.get('rhodecode_use_gravatar'))
347 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
347 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
348 context.visual.default_renderer = rc_config.get(
348 context.visual.default_renderer = rc_config.get(
349 'rhodecode_markup_renderer', 'rst')
349 'rhodecode_markup_renderer', 'rst')
350 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
350 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
351 context.visual.rhodecode_support_url = \
351 context.visual.rhodecode_support_url = \
352 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
352 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
353
353
354 context.visual.affected_files_cut_off = 60
354 context.visual.affected_files_cut_off = 60
355
355
356 context.pre_code = rc_config.get('rhodecode_pre_code')
356 context.pre_code = rc_config.get('rhodecode_pre_code')
357 context.post_code = rc_config.get('rhodecode_post_code')
357 context.post_code = rc_config.get('rhodecode_post_code')
358 context.rhodecode_name = rc_config.get('rhodecode_title')
358 context.rhodecode_name = rc_config.get('rhodecode_title')
359 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
359 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
360 # if we have specified default_encoding in the request, it has more
360 # if we have specified default_encoding in the request, it has more
361 # priority
361 # priority
362 if request.GET.get('default_encoding'):
362 if request.GET.get('default_encoding'):
363 context.default_encodings.insert(0, request.GET.get('default_encoding'))
363 context.default_encodings.insert(0, request.GET.get('default_encoding'))
364 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
364 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
365
365
366 # INI stored
366 # INI stored
367 context.labs_active = str2bool(
367 context.labs_active = str2bool(
368 config.get('labs_settings_active', 'false'))
368 config.get('labs_settings_active', 'false'))
369 context.visual.allow_repo_location_change = str2bool(
369 context.visual.allow_repo_location_change = str2bool(
370 config.get('allow_repo_location_change', True))
370 config.get('allow_repo_location_change', True))
371 context.visual.allow_custom_hooks_settings = str2bool(
371 context.visual.allow_custom_hooks_settings = str2bool(
372 config.get('allow_custom_hooks_settings', True))
372 config.get('allow_custom_hooks_settings', True))
373 context.debug_style = str2bool(config.get('debug_style', False))
373 context.debug_style = str2bool(config.get('debug_style', False))
374
374
375 context.rhodecode_instanceid = config.get('instance_id')
375 context.rhodecode_instanceid = config.get('instance_id')
376
376
377 context.visual.cut_off_limit_diff = safe_int(
377 context.visual.cut_off_limit_diff = safe_int(
378 config.get('cut_off_limit_diff'))
378 config.get('cut_off_limit_diff'))
379 context.visual.cut_off_limit_file = safe_int(
379 context.visual.cut_off_limit_file = safe_int(
380 config.get('cut_off_limit_file'))
380 config.get('cut_off_limit_file'))
381
381
382 # AppEnlight
382 # AppEnlight
383 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
383 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
384 context.appenlight_api_public_key = config.get(
384 context.appenlight_api_public_key = config.get(
385 'appenlight.api_public_key', '')
385 'appenlight.api_public_key', '')
386 context.appenlight_server_url = config.get('appenlight.server_url', '')
386 context.appenlight_server_url = config.get('appenlight.server_url', '')
387
387
388 # JS template context
388 # JS template context
389 context.template_context = {
389 context.template_context = {
390 'repo_name': None,
390 'repo_name': None,
391 'repo_type': None,
391 'repo_type': None,
392 'repo_landing_commit': None,
392 'repo_landing_commit': None,
393 'rhodecode_user': {
393 'rhodecode_user': {
394 'username': None,
394 'username': None,
395 'email': None,
395 'email': None,
396 'notification_status': False
396 'notification_status': False
397 },
397 },
398 'visual': {
398 'visual': {
399 'default_renderer': None
399 'default_renderer': None
400 },
400 },
401 'commit_data': {
401 'commit_data': {
402 'commit_id': None
402 'commit_id': None
403 },
403 },
404 'pull_request_data': {'pull_request_id': None},
404 'pull_request_data': {'pull_request_id': None},
405 'timeago': {
405 'timeago': {
406 'refresh_time': 120 * 1000,
406 'refresh_time': 120 * 1000,
407 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
407 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
408 },
408 },
409 'pylons_dispatch': {
409 'pylons_dispatch': {
410 # 'controller': request.environ['pylons.routes_dict']['controller'],
410 # 'controller': request.environ['pylons.routes_dict']['controller'],
411 # 'action': request.environ['pylons.routes_dict']['action'],
411 # 'action': request.environ['pylons.routes_dict']['action'],
412 },
412 },
413 'pyramid_dispatch': {
413 'pyramid_dispatch': {
414
414
415 },
415 },
416 'extra': {'plugins': {}}
416 'extra': {'plugins': {}}
417 }
417 }
418 # END CONFIG VARS
418 # END CONFIG VARS
419
419
420 # TODO: This dosn't work when called from pylons compatibility tween.
420 # TODO: This dosn't work when called from pylons compatibility tween.
421 # Fix this and remove it from base controller.
421 # Fix this and remove it from base controller.
422 # context.repo_name = get_repo_slug(request) # can be empty
422 # context.repo_name = get_repo_slug(request) # can be empty
423
423
424 diffmode = 'sideside'
424 diffmode = 'sideside'
425 if request.GET.get('diffmode'):
425 if request.GET.get('diffmode'):
426 if request.GET['diffmode'] == 'unified':
426 if request.GET['diffmode'] == 'unified':
427 diffmode = 'unified'
427 diffmode = 'unified'
428 elif request.session.get('diffmode'):
428 elif request.session.get('diffmode'):
429 diffmode = request.session['diffmode']
429 diffmode = request.session['diffmode']
430
430
431 context.diffmode = diffmode
431 context.diffmode = diffmode
432
432
433 if request.session.get('diffmode') != diffmode:
433 if request.session.get('diffmode') != diffmode:
434 request.session['diffmode'] = diffmode
434 request.session['diffmode'] = diffmode
435
435
436 context.csrf_token = auth.get_csrf_token(session=request.session)
436 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.backends = rhodecode.BACKENDS.keys()
437 context.backends = rhodecode.BACKENDS.keys()
438 context.backends.sort()
438 context.backends.sort()
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440
440
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # given request will ALWAYS be pyramid one
442 # given request will ALWAYS be pyramid one
443 pyramid_request = pyramid.threadlocal.get_current_request()
443 pyramid_request = pyramid.threadlocal.get_current_request()
444 context.pyramid_request = pyramid_request
444 context.pyramid_request = pyramid_request
445
445
446 # web case
446 # web case
447 if hasattr(pyramid_request, 'user'):
447 if hasattr(pyramid_request, 'user'):
448 context.auth_user = pyramid_request.user
448 context.auth_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
450
450
451 # api case
451 # api case
452 if hasattr(pyramid_request, 'rpc_user'):
452 if hasattr(pyramid_request, 'rpc_user'):
453 context.auth_user = pyramid_request.rpc_user
453 context.auth_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
455
455
456 # attach the whole call context to the request
456 # attach the whole call context to the request
457 request.call_context = context
457 request.call_context = context
458
458
459
459
460 def get_auth_user(request):
460 def get_auth_user(request):
461 environ = request.environ
461 environ = request.environ
462 session = request.session
462 session = request.session
463
463
464 ip_addr = get_ip_addr(environ)
464 ip_addr = get_ip_addr(environ)
465 # make sure that we update permissions each time we call controller
465 # make sure that we update permissions each time we call controller
466 _auth_token = (request.GET.get('auth_token', '') or
466 _auth_token = (request.GET.get('auth_token', '') or
467 request.GET.get('api_key', ''))
467 request.GET.get('api_key', ''))
468
468
469 if _auth_token:
469 if _auth_token:
470 # when using API_KEY we assume user exists, and
470 # when using API_KEY we assume user exists, and
471 # doesn't need auth based on cookies.
471 # doesn't need auth based on cookies.
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 authenticated = False
473 authenticated = False
474 else:
474 else:
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 try:
476 try:
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 ip_addr=ip_addr)
478 ip_addr=ip_addr)
479 except UserCreationError as e:
479 except UserCreationError as e:
480 h.flash(e, 'error')
480 h.flash(e, 'error')
481 # container auth or other auth functions that create users
481 # container auth or other auth functions that create users
482 # on the fly can throw this exception signaling that there's
482 # on the fly can throw this exception signaling that there's
483 # issue with user creation, explanation should be provided
483 # issue with user creation, explanation should be provided
484 # in Exception itself. We then create a simple blank
484 # in Exception itself. We then create a simple blank
485 # AuthUser
485 # AuthUser
486 auth_user = AuthUser(ip_addr=ip_addr)
486 auth_user = AuthUser(ip_addr=ip_addr)
487
487
488 if password_changed(auth_user, session):
488 if password_changed(auth_user, session):
489 session.invalidate()
489 session.invalidate()
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 auth_user = AuthUser(ip_addr=ip_addr)
491 auth_user = AuthUser(ip_addr=ip_addr)
492
492
493 authenticated = cookie_store.get('is_authenticated')
493 authenticated = cookie_store.get('is_authenticated')
494
494
495 if not auth_user.is_authenticated and auth_user.is_user_object:
495 if not auth_user.is_authenticated and auth_user.is_user_object:
496 # user is not authenticated and not empty
496 # user is not authenticated and not empty
497 auth_user.set_authenticated(authenticated)
497 auth_user.set_authenticated(authenticated)
498
498
499 return auth_user
499 return auth_user
500
500
501
501
502 class BaseController(WSGIController):
502 class BaseController(WSGIController):
503
503
504 def __before__(self):
504 def __before__(self):
505 """
505 """
506 __before__ is called before controller methods and after __call__
506 __before__ is called before controller methods and after __call__
507 """
507 """
508 # on each call propagate settings calls into global settings.
508 # on each call propagate settings calls into global settings.
509 set_rhodecode_config(config)
509 set_rhodecode_config(config)
510 attach_context_attributes(c, request, self._rhodecode_user.user_id)
510 attach_context_attributes(c, request, self._rhodecode_user.user_id)
511
511
512 # TODO: Remove this when fixed in attach_context_attributes()
512 # TODO: Remove this when fixed in attach_context_attributes()
513 c.repo_name = get_repo_slug(request) # can be empty
513 c.repo_name = get_repo_slug(request) # can be empty
514
514
515 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
515 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
516 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
516 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
517 self.sa = meta.Session
517 self.sa = meta.Session
518 self.scm_model = ScmModel(self.sa)
518 self.scm_model = ScmModel(self.sa)
519
519
520 # set user language
520 # set user language
521 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
521 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
522 if user_lang:
522 if user_lang:
523 translation.set_lang(user_lang)
523 translation.set_lang(user_lang)
524 log.debug('set language to %s for user %s',
524 log.debug('set language to %s for user %s',
525 user_lang, self._rhodecode_user)
525 user_lang, self._rhodecode_user)
526
526
527 def _dispatch_redirect(self, with_url, environ, start_response):
527 def _dispatch_redirect(self, with_url, environ, start_response):
528 resp = HTTPFound(with_url)
528 resp = HTTPFound(with_url)
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
530 environ['PATH_INFO'] = with_url
530 environ['PATH_INFO'] = with_url
531 return resp(environ, start_response)
531 return resp(environ, start_response)
532
532
533 def __call__(self, environ, start_response):
533 def __call__(self, environ, start_response):
534 """Invoke the Controller"""
534 """Invoke the Controller"""
535 # WSGIController.__call__ dispatches to the Controller method
535 # WSGIController.__call__ dispatches to the Controller method
536 # the request is routed to. This routing information is
536 # the request is routed to. This routing information is
537 # available in environ['pylons.routes_dict']
537 # available in environ['pylons.routes_dict']
538 from rhodecode.lib import helpers as h
538 from rhodecode.lib import helpers as h
539
539
540 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
540 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
541 if environ.get('debugtoolbar.wants_pylons_context', False):
541 if environ.get('debugtoolbar.wants_pylons_context', False):
542 environ['debugtoolbar.pylons_context'] = c._current_obj()
542 environ['debugtoolbar.pylons_context'] = c._current_obj()
543
543
544 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
544 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
545 environ['pylons.routes_dict']['action']])
545 environ['pylons.routes_dict']['action']])
546
546
547 self.rc_config = SettingsModel().get_all_settings(cache=True)
547 self.rc_config = SettingsModel().get_all_settings(cache=True)
548 self.ip_addr = get_ip_addr(environ)
548 self.ip_addr = get_ip_addr(environ)
549
549
550 # The rhodecode auth user is looked up and passed through the
550 # The rhodecode auth user is looked up and passed through the
551 # environ by the pylons compatibility tween in pyramid.
551 # environ by the pylons compatibility tween in pyramid.
552 # So we can just grab it from there.
552 # So we can just grab it from there.
553 auth_user = environ['rc_auth_user']
553 auth_user = environ['rc_auth_user']
554
554
555 # set globals for auth user
555 # set globals for auth user
556 request.user = auth_user
556 request.user = auth_user
557 self._rhodecode_user = auth_user
557 self._rhodecode_user = auth_user
558
558
559 log.info('IP: %s User: %s accessed %s [%s]' % (
559 log.info('IP: %s User: %s accessed %s [%s]' % (
560 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
560 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
561 _route_name)
561 _route_name)
562 )
562 )
563
563
564 user_obj = auth_user.get_instance()
564 user_obj = auth_user.get_instance()
565 if user_obj and user_obj.user_data.get('force_password_change'):
565 if user_obj and user_obj.user_data.get('force_password_change'):
566 h.flash('You are required to change your password', 'warning',
566 h.flash('You are required to change your password', 'warning',
567 ignore_duplicate=True)
567 ignore_duplicate=True)
568 return self._dispatch_redirect(
568 return self._dispatch_redirect(
569 url('my_account_password'), environ, start_response)
569 url('my_account_password'), environ, start_response)
570
570
571 return WSGIController.__call__(self, environ, start_response)
571 return WSGIController.__call__(self, environ, start_response)
572
572
573
573
574 def h_filter(s):
574 def h_filter(s):
575 """
575 """
576 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
576 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
577 we wrap this with additional functionality that converts None to empty
577 we wrap this with additional functionality that converts None to empty
578 strings
578 strings
579 """
579 """
580 if s is None:
580 if s is None:
581 return markupsafe.Markup()
581 return markupsafe.Markup()
582 return markupsafe.escape(s)
582 return markupsafe.escape(s)
583
583
584
584
585 def add_events_routes(config):
585 def add_events_routes(config):
586 """
586 """
587 Adds routing that can be used in events. Because some events are triggered
587 Adds routing that can be used in events. Because some events are triggered
588 outside of pyramid context, we need to bootstrap request with some
588 outside of pyramid context, we need to bootstrap request with some
589 routing registered
589 routing registered
590 """
590 """
591 config.add_route(name='home', pattern='/')
591 config.add_route(name='home', pattern='/')
592
592
593 config.add_route(name='repo_summary', pattern='/{repo_name}')
593 config.add_route(name='repo_summary', pattern='/{repo_name}')
594 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
594 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
595 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
595 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
596
596
597 config.add_route(name='pullrequest_show',
597 config.add_route(name='pullrequest_show',
598 pattern='/{repo_name}/pull-request/{pull_request_id}')
598 pattern='/{repo_name}/pull-request/{pull_request_id}')
599 config.add_route(name='pull_requests_global',
599 config.add_route(name='pull_requests_global',
600 pattern='/pull-request/{pull_request_id}')
600 pattern='/pull-request/{pull_request_id}')
601
601
602 config.add_route(name='repo_commit',
602 config.add_route(name='repo_commit',
603 pattern='/{repo_name}/changeset/{commit_id}')
603 pattern='/{repo_name}/changeset/{commit_id}')
604 config.add_route(name='repo_files',
604 config.add_route(name='repo_files',
605 pattern='/{repo_name}/files/{commit_id}/{f_path}')
605 pattern='/{repo_name}/files/{commit_id}/{f_path}')
606
606
607
607
608 def bootstrap_request():
608 def bootstrap_request(**kwargs):
609 import pyramid.testing
609 import pyramid.testing
610 request = pyramid.testing.DummyRequest()
610 request = pyramid.testing.DummyRequest(**kwargs)
611 request.application_url = kwargs.pop('application_url', 'http://example.com')
612 request.host = kwargs.pop('host', 'example.com:80')
613 request.domain = kwargs.pop('domain', 'example.com')
614
611 config = pyramid.testing.setUp(request=request)
615 config = pyramid.testing.setUp(request=request)
612 add_events_routes(config)
616 add_events_routes(config)
613
617
614
618
615 class BaseRepoController(BaseController):
619 class BaseRepoController(BaseController):
616 """
620 """
617 Base class for controllers responsible for loading all needed data for
621 Base class for controllers responsible for loading all needed data for
618 repository loaded items are
622 repository loaded items are
619
623
620 c.rhodecode_repo: instance of scm repository
624 c.rhodecode_repo: instance of scm repository
621 c.rhodecode_db_repo: instance of db
625 c.rhodecode_db_repo: instance of db
622 c.repository_requirements_missing: shows that repository specific data
626 c.repository_requirements_missing: shows that repository specific data
623 could not be displayed due to the missing requirements
627 could not be displayed due to the missing requirements
624 c.repository_pull_requests: show number of open pull requests
628 c.repository_pull_requests: show number of open pull requests
625 """
629 """
626
630
627 def __before__(self):
631 def __before__(self):
628 super(BaseRepoController, self).__before__()
632 super(BaseRepoController, self).__before__()
629 if c.repo_name: # extracted from routes
633 if c.repo_name: # extracted from routes
630 db_repo = Repository.get_by_repo_name(c.repo_name)
634 db_repo = Repository.get_by_repo_name(c.repo_name)
631 if not db_repo:
635 if not db_repo:
632 return
636 return
633
637
634 log.debug(
638 log.debug(
635 'Found repository in database %s with state `%s`',
639 'Found repository in database %s with state `%s`',
636 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
640 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
637 route = getattr(request.environ.get('routes.route'), 'name', '')
641 route = getattr(request.environ.get('routes.route'), 'name', '')
638
642
639 # allow to delete repos that are somehow damages in filesystem
643 # allow to delete repos that are somehow damages in filesystem
640 if route in ['delete_repo']:
644 if route in ['delete_repo']:
641 return
645 return
642
646
643 if db_repo.repo_state in [Repository.STATE_PENDING]:
647 if db_repo.repo_state in [Repository.STATE_PENDING]:
644 if route in ['repo_creating_home']:
648 if route in ['repo_creating_home']:
645 return
649 return
646 check_url = url('repo_creating_home', repo_name=c.repo_name)
650 check_url = url('repo_creating_home', repo_name=c.repo_name)
647 return redirect(check_url)
651 return redirect(check_url)
648
652
649 self.rhodecode_db_repo = db_repo
653 self.rhodecode_db_repo = db_repo
650
654
651 missing_requirements = False
655 missing_requirements = False
652 try:
656 try:
653 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
657 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
654 except RepositoryRequirementError as e:
658 except RepositoryRequirementError as e:
655 missing_requirements = True
659 missing_requirements = True
656 self._handle_missing_requirements(e)
660 self._handle_missing_requirements(e)
657
661
658 if self.rhodecode_repo is None and not missing_requirements:
662 if self.rhodecode_repo is None and not missing_requirements:
659 log.error('%s this repository is present in database but it '
663 log.error('%s this repository is present in database but it '
660 'cannot be created as an scm instance', c.repo_name)
664 'cannot be created as an scm instance', c.repo_name)
661
665
662 h.flash(_(
666 h.flash(_(
663 "The repository at %(repo_name)s cannot be located.") %
667 "The repository at %(repo_name)s cannot be located.") %
664 {'repo_name': c.repo_name},
668 {'repo_name': c.repo_name},
665 category='error', ignore_duplicate=True)
669 category='error', ignore_duplicate=True)
666 redirect(h.route_path('home'))
670 redirect(h.route_path('home'))
667
671
668 # update last change according to VCS data
672 # update last change according to VCS data
669 if not missing_requirements:
673 if not missing_requirements:
670 commit = db_repo.get_commit(
674 commit = db_repo.get_commit(
671 pre_load=["author", "date", "message", "parents"])
675 pre_load=["author", "date", "message", "parents"])
672 db_repo.update_commit_cache(commit)
676 db_repo.update_commit_cache(commit)
673
677
674 # Prepare context
678 # Prepare context
675 c.rhodecode_db_repo = db_repo
679 c.rhodecode_db_repo = db_repo
676 c.rhodecode_repo = self.rhodecode_repo
680 c.rhodecode_repo = self.rhodecode_repo
677 c.repository_requirements_missing = missing_requirements
681 c.repository_requirements_missing = missing_requirements
678
682
679 self._update_global_counters(self.scm_model, db_repo)
683 self._update_global_counters(self.scm_model, db_repo)
680
684
681 def _update_global_counters(self, scm_model, db_repo):
685 def _update_global_counters(self, scm_model, db_repo):
682 """
686 """
683 Base variables that are exposed to every page of repository
687 Base variables that are exposed to every page of repository
684 """
688 """
685 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
689 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
686
690
687 def _handle_missing_requirements(self, error):
691 def _handle_missing_requirements(self, error):
688 self.rhodecode_repo = None
692 self.rhodecode_repo = None
689 log.error(
693 log.error(
690 'Requirements are missing for repository %s: %s',
694 'Requirements are missing for repository %s: %s',
691 c.repo_name, error.message)
695 c.repo_name, error.message)
692
696
693 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
697 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
694 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
698 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
695 settings_update_url = url('repo', repo_name=c.repo_name)
699 settings_update_url = url('repo', repo_name=c.repo_name)
696 path = request.path
700 path = request.path
697 should_redirect = (
701 should_redirect = (
698 path not in (summary_url, settings_update_url)
702 path not in (summary_url, settings_update_url)
699 and '/settings' not in path or path == statistics_url
703 and '/settings' not in path or path == statistics_url
700 )
704 )
701 if should_redirect:
705 if should_redirect:
702 redirect(summary_url)
706 redirect(summary_url)
@@ -1,237 +1,239 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 import json
21 import json
22 import logging
22 import logging
23 import traceback
23 import traceback
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode.model import meta
29 from rhodecode.model import meta
30 from rhodecode.lib.base import bootstrap_request
30 from rhodecode.lib.base import bootstrap_request
31 from rhodecode.lib import hooks_base
31 from rhodecode.lib import hooks_base
32 from rhodecode.lib.utils2 import AttributeDict
32 from rhodecode.lib.utils2 import AttributeDict
33
33
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class HooksHttpHandler(BaseHTTPRequestHandler):
38 class HooksHttpHandler(BaseHTTPRequestHandler):
39 def do_POST(self):
39 def do_POST(self):
40 method, extras = self._read_request()
40 method, extras = self._read_request()
41 try:
41 try:
42 result = self._call_hook(method, extras)
42 result = self._call_hook(method, extras)
43 except Exception as e:
43 except Exception as e:
44 exc_tb = traceback.format_exc()
44 exc_tb = traceback.format_exc()
45 result = {
45 result = {
46 'exception': e.__class__.__name__,
46 'exception': e.__class__.__name__,
47 'exception_traceback': exc_tb,
47 'exception_traceback': exc_tb,
48 'exception_args': e.args
48 'exception_args': e.args
49 }
49 }
50 self._write_response(result)
50 self._write_response(result)
51
51
52 def _read_request(self):
52 def _read_request(self):
53 length = int(self.headers['Content-Length'])
53 length = int(self.headers['Content-Length'])
54 body = self.rfile.read(length).decode('utf-8')
54 body = self.rfile.read(length).decode('utf-8')
55 data = json.loads(body)
55 data = json.loads(body)
56 return data['method'], data['extras']
56 return data['method'], data['extras']
57
57
58 def _write_response(self, result):
58 def _write_response(self, result):
59 self.send_response(200)
59 self.send_response(200)
60 self.send_header("Content-type", "text/json")
60 self.send_header("Content-type", "text/json")
61 self.end_headers()
61 self.end_headers()
62 self.wfile.write(json.dumps(result))
62 self.wfile.write(json.dumps(result))
63
63
64 def _call_hook(self, method, extras):
64 def _call_hook(self, method, extras):
65 hooks = Hooks()
65 hooks = Hooks()
66 try:
66 try:
67 result = getattr(hooks, method)(extras)
67 result = getattr(hooks, method)(extras)
68 finally:
68 finally:
69 meta.Session.remove()
69 meta.Session.remove()
70 return result
70 return result
71
71
72 def log_message(self, format, *args):
72 def log_message(self, format, *args):
73 """
73 """
74 This is an overridden method of BaseHTTPRequestHandler which logs using
74 This is an overridden method of BaseHTTPRequestHandler which logs using
75 logging library instead of writing directly to stderr.
75 logging library instead of writing directly to stderr.
76 """
76 """
77
77
78 message = format % args
78 message = format % args
79
79
80 # TODO: mikhail: add different log levels support
80 # TODO: mikhail: add different log levels support
81 log.debug(
81 log.debug(
82 "%s - - [%s] %s", self.client_address[0],
82 "%s - - [%s] %s", self.client_address[0],
83 self.log_date_time_string(), message)
83 self.log_date_time_string(), message)
84
84
85
85
86 class DummyHooksCallbackDaemon(object):
86 class DummyHooksCallbackDaemon(object):
87 def __init__(self):
87 def __init__(self):
88 self.hooks_module = Hooks.__module__
88 self.hooks_module = Hooks.__module__
89
89
90 def __enter__(self):
90 def __enter__(self):
91 log.debug('Running dummy hooks callback daemon')
91 log.debug('Running dummy hooks callback daemon')
92 return self
92 return self
93
93
94 def __exit__(self, exc_type, exc_val, exc_tb):
94 def __exit__(self, exc_type, exc_val, exc_tb):
95 log.debug('Exiting dummy hooks callback daemon')
95 log.debug('Exiting dummy hooks callback daemon')
96
96
97
97
98 class ThreadedHookCallbackDaemon(object):
98 class ThreadedHookCallbackDaemon(object):
99
99
100 _callback_thread = None
100 _callback_thread = None
101 _daemon = None
101 _daemon = None
102 _done = False
102 _done = False
103
103
104 def __init__(self):
104 def __init__(self):
105 self._prepare()
105 self._prepare()
106
106
107 def __enter__(self):
107 def __enter__(self):
108 self._run()
108 self._run()
109 return self
109 return self
110
110
111 def __exit__(self, exc_type, exc_val, exc_tb):
111 def __exit__(self, exc_type, exc_val, exc_tb):
112 self._stop()
112 self._stop()
113
113
114 def _prepare(self):
114 def _prepare(self):
115 raise NotImplementedError()
115 raise NotImplementedError()
116
116
117 def _run(self):
117 def _run(self):
118 raise NotImplementedError()
118 raise NotImplementedError()
119
119
120 def _stop(self):
120 def _stop(self):
121 raise NotImplementedError()
121 raise NotImplementedError()
122
122
123
123
124 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
124 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
125 """
125 """
126 Context manager which will run a callback daemon in a background thread.
126 Context manager which will run a callback daemon in a background thread.
127 """
127 """
128
128
129 hooks_uri = None
129 hooks_uri = None
130
130
131 IP_ADDRESS = '127.0.0.1'
131 IP_ADDRESS = '127.0.0.1'
132
132
133 # From Python docs: Polling reduces our responsiveness to a shutdown
133 # From Python docs: Polling reduces our responsiveness to a shutdown
134 # request and wastes cpu at all other times.
134 # request and wastes cpu at all other times.
135 POLL_INTERVAL = 0.1
135 POLL_INTERVAL = 0.1
136
136
137 def _prepare(self):
137 def _prepare(self):
138 log.debug("Preparing callback daemon and registering hook object")
138 log.debug("Preparing callback daemon and registering hook object")
139
139
140 self._done = False
140 self._done = False
141 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
141 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
142 _, port = self._daemon.server_address
142 _, port = self._daemon.server_address
143 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
143 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
144
144
145 log.debug("Hooks uri is: %s", self.hooks_uri)
145 log.debug("Hooks uri is: %s", self.hooks_uri)
146
146
147 def _run(self):
147 def _run(self):
148 log.debug("Running event loop of callback daemon in background thread")
148 log.debug("Running event loop of callback daemon in background thread")
149 callback_thread = threading.Thread(
149 callback_thread = threading.Thread(
150 target=self._daemon.serve_forever,
150 target=self._daemon.serve_forever,
151 kwargs={'poll_interval': self.POLL_INTERVAL})
151 kwargs={'poll_interval': self.POLL_INTERVAL})
152 callback_thread.daemon = True
152 callback_thread.daemon = True
153 callback_thread.start()
153 callback_thread.start()
154 self._callback_thread = callback_thread
154 self._callback_thread = callback_thread
155
155
156 def _stop(self):
156 def _stop(self):
157 log.debug("Waiting for background thread to finish.")
157 log.debug("Waiting for background thread to finish.")
158 self._daemon.shutdown()
158 self._daemon.shutdown()
159 self._callback_thread.join()
159 self._callback_thread.join()
160 self._daemon = None
160 self._daemon = None
161 self._callback_thread = None
161 self._callback_thread = None
162
162
163
163
164 def prepare_callback_daemon(extras, protocol, use_direct_calls):
164 def prepare_callback_daemon(extras, protocol, use_direct_calls):
165 callback_daemon = None
165 callback_daemon = None
166
166
167 if use_direct_calls:
167 if use_direct_calls:
168 callback_daemon = DummyHooksCallbackDaemon()
168 callback_daemon = DummyHooksCallbackDaemon()
169 extras['hooks_module'] = callback_daemon.hooks_module
169 extras['hooks_module'] = callback_daemon.hooks_module
170 else:
170 else:
171 if protocol == 'http':
171 if protocol == 'http':
172 callback_daemon = HttpHooksCallbackDaemon()
172 callback_daemon = HttpHooksCallbackDaemon()
173 else:
173 else:
174 log.error('Unsupported callback daemon protocol "%s"', protocol)
174 log.error('Unsupported callback daemon protocol "%s"', protocol)
175 raise Exception('Unsupported callback daemon protocol.')
175 raise Exception('Unsupported callback daemon protocol.')
176
176
177 extras['hooks_uri'] = callback_daemon.hooks_uri
177 extras['hooks_uri'] = callback_daemon.hooks_uri
178 extras['hooks_protocol'] = protocol
178 extras['hooks_protocol'] = protocol
179
179
180 return callback_daemon, extras
180 return callback_daemon, extras
181
181
182
182
183 class Hooks(object):
183 class Hooks(object):
184 """
184 """
185 Exposes the hooks for remote call backs
185 Exposes the hooks for remote call backs
186 """
186 """
187
187
188 def repo_size(self, extras):
188 def repo_size(self, extras):
189 log.debug("Called repo_size of %s object", self)
189 log.debug("Called repo_size of %s object", self)
190 return self._call_hook(hooks_base.repo_size, extras)
190 return self._call_hook(hooks_base.repo_size, extras)
191
191
192 def pre_pull(self, extras):
192 def pre_pull(self, extras):
193 log.debug("Called pre_pull of %s object", self)
193 log.debug("Called pre_pull of %s object", self)
194 return self._call_hook(hooks_base.pre_pull, extras)
194 return self._call_hook(hooks_base.pre_pull, extras)
195
195
196 def post_pull(self, extras):
196 def post_pull(self, extras):
197 log.debug("Called post_pull of %s object", self)
197 log.debug("Called post_pull of %s object", self)
198 return self._call_hook(hooks_base.post_pull, extras)
198 return self._call_hook(hooks_base.post_pull, extras)
199
199
200 def pre_push(self, extras):
200 def pre_push(self, extras):
201 log.debug("Called pre_push of %s object", self)
201 log.debug("Called pre_push of %s object", self)
202 return self._call_hook(hooks_base.pre_push, extras)
202 return self._call_hook(hooks_base.pre_push, extras)
203
203
204 def post_push(self, extras):
204 def post_push(self, extras):
205 log.debug("Called post_push of %s object", self)
205 log.debug("Called post_push of %s object", self)
206 return self._call_hook(hooks_base.post_push, extras)
206 return self._call_hook(hooks_base.post_push, extras)
207
207
208 def _call_hook(self, hook, extras):
208 def _call_hook(self, hook, extras):
209 extras = AttributeDict(extras)
209 extras = AttributeDict(extras)
210 extras.request = bootstrap_request()
210
211 extras.request = bootstrap_request(
212 application_url=extras['server_url'])
211
213
212 try:
214 try:
213 result = hook(extras)
215 result = hook(extras)
214 except Exception as error:
216 except Exception as error:
215 exc_tb = traceback.format_exc()
217 exc_tb = traceback.format_exc()
216 log.exception('Exception when handling hook %s', hook)
218 log.exception('Exception when handling hook %s', hook)
217 error_args = error.args
219 error_args = error.args
218 return {
220 return {
219 'status': 128,
221 'status': 128,
220 'output': '',
222 'output': '',
221 'exception': type(error).__name__,
223 'exception': type(error).__name__,
222 'exception_traceback': exc_tb,
224 'exception_traceback': exc_tb,
223 'exception_args': error_args,
225 'exception_args': error_args,
224 }
226 }
225 finally:
227 finally:
226 meta.Session.remove()
228 meta.Session.remove()
227
229
228 return {
230 return {
229 'status': result.status,
231 'status': result.status,
230 'output': result.output,
232 'output': result.output,
231 }
233 }
232
234
233 def __enter__(self):
235 def __enter__(self):
234 return self
236 return self
235
237
236 def __exit__(self, exc_type, exc_val, exc_tb):
238 def __exit__(self, exc_type, exc_val, exc_tb):
237 pass
239 pass
General Comments 0
You need to be logged in to leave comments. Login now