##// END OF EJS Templates
core: multiple fixes to unicode vs str usage...
super-admin -
r5065:bfe9513d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,479 +1,479 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 re
21 import re
22 import logging
22 import logging
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25 import datetime
25 import datetime
26 from pyramid.interfaces import IRoutesMapper
26 from pyramid.interfaces import IRoutesMapper
27
27
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode import events
34 from rhodecode import events
35
35
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib.utils2 import aslist, safe_unicode
39 from rhodecode.lib.utils2 import aslist, safe_str
40 from rhodecode.model.db import (
40 from rhodecode.model.db import (
41 or_, coalesce, User, UserIpMap, UserSshKeys)
41 or_, coalesce, User, UserIpMap, UserSshKeys)
42 from rhodecode.model.forms import (
42 from rhodecode.model.forms import (
43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.permission import PermissionModel
45 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class AdminPermissionsView(BaseAppView, DataGridAppView):
52 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 PermissionModel().set_global_permission_choices(
55 PermissionModel().set_global_permission_choices(
56 c, gettext_translator=self.request.translate)
56 c, gettext_translator=self.request.translate)
57 return c
57 return c
58
58
59 @LoginRequired()
59 @LoginRequired()
60 @HasPermissionAllDecorator('hg.admin')
60 @HasPermissionAllDecorator('hg.admin')
61 def permissions_application(self):
61 def permissions_application(self):
62 c = self.load_default_context()
62 c = self.load_default_context()
63 c.active = 'application'
63 c.active = 'application'
64
64
65 c.user = User.get_default_user(refresh=True)
65 c.user = User.get_default_user(refresh=True)
66
66
67 app_settings = c.rc_config
67 app_settings = c.rc_config
68
68
69 defaults = {
69 defaults = {
70 'anonymous': c.user.active,
70 'anonymous': c.user.active,
71 'default_register_message': app_settings.get(
71 'default_register_message': app_settings.get(
72 'rhodecode_register_message')
72 'rhodecode_register_message')
73 }
73 }
74 defaults.update(c.user.get_default_perms())
74 defaults.update(c.user.get_default_perms())
75
75
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 self._get_template_context(c), self.request)
77 self._get_template_context(c), self.request)
78 html = formencode.htmlfill.render(
78 html = formencode.htmlfill.render(
79 data,
79 data,
80 defaults=defaults,
80 defaults=defaults,
81 encoding="UTF-8",
81 encoding="UTF-8",
82 force_defaults=False
82 force_defaults=False
83 )
83 )
84 return Response(html)
84 return Response(html)
85
85
86 @LoginRequired()
86 @LoginRequired()
87 @HasPermissionAllDecorator('hg.admin')
87 @HasPermissionAllDecorator('hg.admin')
88 @CSRFRequired()
88 @CSRFRequired()
89 def permissions_application_update(self):
89 def permissions_application_update(self):
90 _ = self.request.translate
90 _ = self.request.translate
91 c = self.load_default_context()
91 c = self.load_default_context()
92 c.active = 'application'
92 c.active = 'application'
93
93
94 _form = ApplicationPermissionsForm(
94 _form = ApplicationPermissionsForm(
95 self.request.translate,
95 self.request.translate,
96 [x[0] for x in c.register_choices],
96 [x[0] for x in c.register_choices],
97 [x[0] for x in c.password_reset_choices],
97 [x[0] for x in c.password_reset_choices],
98 [x[0] for x in c.extern_activate_choices])()
98 [x[0] for x in c.extern_activate_choices])()
99
99
100 try:
100 try:
101 form_result = _form.to_python(dict(self.request.POST))
101 form_result = _form.to_python(dict(self.request.POST))
102 form_result.update({'perm_user_name': User.DEFAULT_USER})
102 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 PermissionModel().update_application_permissions(form_result)
103 PermissionModel().update_application_permissions(form_result)
104
104
105 settings = [
105 settings = [
106 ('register_message', 'default_register_message'),
106 ('register_message', 'default_register_message'),
107 ]
107 ]
108 for setting, form_key in settings:
108 for setting, form_key in settings:
109 sett = SettingsModel().create_or_update_setting(
109 sett = SettingsModel().create_or_update_setting(
110 setting, form_result[form_key])
110 setting, form_result[form_key])
111 Session().add(sett)
111 Session().add(sett)
112
112
113 Session().commit()
113 Session().commit()
114 h.flash(_('Application permissions updated successfully'),
114 h.flash(_('Application permissions updated successfully'),
115 category='success')
115 category='success')
116
116
117 except formencode.Invalid as errors:
117 except formencode.Invalid as errors:
118 defaults = errors.value
118 defaults = errors.value
119
119
120 data = render(
120 data = render(
121 'rhodecode:templates/admin/permissions/permissions.mako',
121 'rhodecode:templates/admin/permissions/permissions.mako',
122 self._get_template_context(c), self.request)
122 self._get_template_context(c), self.request)
123 html = formencode.htmlfill.render(
123 html = formencode.htmlfill.render(
124 data,
124 data,
125 defaults=defaults,
125 defaults=defaults,
126 errors=errors.unpack_errors() or {},
126 errors=errors.unpack_errors() or {},
127 prefix_error=False,
127 prefix_error=False,
128 encoding="UTF-8",
128 encoding="UTF-8",
129 force_defaults=False
129 force_defaults=False
130 )
130 )
131 return Response(html)
131 return Response(html)
132
132
133 except Exception:
133 except Exception:
134 log.exception("Exception during update of permissions")
134 log.exception("Exception during update of permissions")
135 h.flash(_('Error occurred during update of permissions'),
135 h.flash(_('Error occurred during update of permissions'),
136 category='error')
136 category='error')
137
137
138 affected_user_ids = [User.get_default_user_id()]
138 affected_user_ids = [User.get_default_user_id()]
139 PermissionModel().trigger_permission_flush(affected_user_ids)
139 PermissionModel().trigger_permission_flush(affected_user_ids)
140
140
141 raise HTTPFound(h.route_path('admin_permissions_application'))
141 raise HTTPFound(h.route_path('admin_permissions_application'))
142
142
143 @LoginRequired()
143 @LoginRequired()
144 @HasPermissionAllDecorator('hg.admin')
144 @HasPermissionAllDecorator('hg.admin')
145 def permissions_objects(self):
145 def permissions_objects(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147 c.active = 'objects'
147 c.active = 'objects'
148
148
149 c.user = User.get_default_user(refresh=True)
149 c.user = User.get_default_user(refresh=True)
150 defaults = {}
150 defaults = {}
151 defaults.update(c.user.get_default_perms())
151 defaults.update(c.user.get_default_perms())
152
152
153 data = render(
153 data = render(
154 'rhodecode:templates/admin/permissions/permissions.mako',
154 'rhodecode:templates/admin/permissions/permissions.mako',
155 self._get_template_context(c), self.request)
155 self._get_template_context(c), self.request)
156 html = formencode.htmlfill.render(
156 html = formencode.htmlfill.render(
157 data,
157 data,
158 defaults=defaults,
158 defaults=defaults,
159 encoding="UTF-8",
159 encoding="UTF-8",
160 force_defaults=False
160 force_defaults=False
161 )
161 )
162 return Response(html)
162 return Response(html)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @HasPermissionAllDecorator('hg.admin')
165 @HasPermissionAllDecorator('hg.admin')
166 @CSRFRequired()
166 @CSRFRequired()
167 def permissions_objects_update(self):
167 def permissions_objects_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'objects'
170 c.active = 'objects'
171
171
172 _form = ObjectPermissionsForm(
172 _form = ObjectPermissionsForm(
173 self.request.translate,
173 self.request.translate,
174 [x[0] for x in c.repo_perms_choices],
174 [x[0] for x in c.repo_perms_choices],
175 [x[0] for x in c.group_perms_choices],
175 [x[0] for x in c.group_perms_choices],
176 [x[0] for x in c.user_group_perms_choices],
176 [x[0] for x in c.user_group_perms_choices],
177 )()
177 )()
178
178
179 try:
179 try:
180 form_result = _form.to_python(dict(self.request.POST))
180 form_result = _form.to_python(dict(self.request.POST))
181 form_result.update({'perm_user_name': User.DEFAULT_USER})
181 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 PermissionModel().update_object_permissions(form_result)
182 PermissionModel().update_object_permissions(form_result)
183
183
184 Session().commit()
184 Session().commit()
185 h.flash(_('Object permissions updated successfully'),
185 h.flash(_('Object permissions updated successfully'),
186 category='success')
186 category='success')
187
187
188 except formencode.Invalid as errors:
188 except formencode.Invalid as errors:
189 defaults = errors.value
189 defaults = errors.value
190
190
191 data = render(
191 data = render(
192 'rhodecode:templates/admin/permissions/permissions.mako',
192 'rhodecode:templates/admin/permissions/permissions.mako',
193 self._get_template_context(c), self.request)
193 self._get_template_context(c), self.request)
194 html = formencode.htmlfill.render(
194 html = formencode.htmlfill.render(
195 data,
195 data,
196 defaults=defaults,
196 defaults=defaults,
197 errors=errors.unpack_errors() or {},
197 errors=errors.unpack_errors() or {},
198 prefix_error=False,
198 prefix_error=False,
199 encoding="UTF-8",
199 encoding="UTF-8",
200 force_defaults=False
200 force_defaults=False
201 )
201 )
202 return Response(html)
202 return Response(html)
203 except Exception:
203 except Exception:
204 log.exception("Exception during update of permissions")
204 log.exception("Exception during update of permissions")
205 h.flash(_('Error occurred during update of permissions'),
205 h.flash(_('Error occurred during update of permissions'),
206 category='error')
206 category='error')
207
207
208 affected_user_ids = [User.get_default_user_id()]
208 affected_user_ids = [User.get_default_user_id()]
209 PermissionModel().trigger_permission_flush(affected_user_ids)
209 PermissionModel().trigger_permission_flush(affected_user_ids)
210
210
211 raise HTTPFound(h.route_path('admin_permissions_object'))
211 raise HTTPFound(h.route_path('admin_permissions_object'))
212
212
213 @LoginRequired()
213 @LoginRequired()
214 @HasPermissionAllDecorator('hg.admin')
214 @HasPermissionAllDecorator('hg.admin')
215 def permissions_branch(self):
215 def permissions_branch(self):
216 c = self.load_default_context()
216 c = self.load_default_context()
217 c.active = 'branch'
217 c.active = 'branch'
218
218
219 c.user = User.get_default_user(refresh=True)
219 c.user = User.get_default_user(refresh=True)
220 defaults = {}
220 defaults = {}
221 defaults.update(c.user.get_default_perms())
221 defaults.update(c.user.get_default_perms())
222
222
223 data = render(
223 data = render(
224 'rhodecode:templates/admin/permissions/permissions.mako',
224 'rhodecode:templates/admin/permissions/permissions.mako',
225 self._get_template_context(c), self.request)
225 self._get_template_context(c), self.request)
226 html = formencode.htmlfill.render(
226 html = formencode.htmlfill.render(
227 data,
227 data,
228 defaults=defaults,
228 defaults=defaults,
229 encoding="UTF-8",
229 encoding="UTF-8",
230 force_defaults=False
230 force_defaults=False
231 )
231 )
232 return Response(html)
232 return Response(html)
233
233
234 @LoginRequired()
234 @LoginRequired()
235 @HasPermissionAllDecorator('hg.admin')
235 @HasPermissionAllDecorator('hg.admin')
236 def permissions_global(self):
236 def permissions_global(self):
237 c = self.load_default_context()
237 c = self.load_default_context()
238 c.active = 'global'
238 c.active = 'global'
239
239
240 c.user = User.get_default_user(refresh=True)
240 c.user = User.get_default_user(refresh=True)
241 defaults = {}
241 defaults = {}
242 defaults.update(c.user.get_default_perms())
242 defaults.update(c.user.get_default_perms())
243
243
244 data = render(
244 data = render(
245 'rhodecode:templates/admin/permissions/permissions.mako',
245 'rhodecode:templates/admin/permissions/permissions.mako',
246 self._get_template_context(c), self.request)
246 self._get_template_context(c), self.request)
247 html = formencode.htmlfill.render(
247 html = formencode.htmlfill.render(
248 data,
248 data,
249 defaults=defaults,
249 defaults=defaults,
250 encoding="UTF-8",
250 encoding="UTF-8",
251 force_defaults=False
251 force_defaults=False
252 )
252 )
253 return Response(html)
253 return Response(html)
254
254
255 @LoginRequired()
255 @LoginRequired()
256 @HasPermissionAllDecorator('hg.admin')
256 @HasPermissionAllDecorator('hg.admin')
257 @CSRFRequired()
257 @CSRFRequired()
258 def permissions_global_update(self):
258 def permissions_global_update(self):
259 _ = self.request.translate
259 _ = self.request.translate
260 c = self.load_default_context()
260 c = self.load_default_context()
261 c.active = 'global'
261 c.active = 'global'
262
262
263 _form = UserPermissionsForm(
263 _form = UserPermissionsForm(
264 self.request.translate,
264 self.request.translate,
265 [x[0] for x in c.repo_create_choices],
265 [x[0] for x in c.repo_create_choices],
266 [x[0] for x in c.repo_create_on_write_choices],
266 [x[0] for x in c.repo_create_on_write_choices],
267 [x[0] for x in c.repo_group_create_choices],
267 [x[0] for x in c.repo_group_create_choices],
268 [x[0] for x in c.user_group_create_choices],
268 [x[0] for x in c.user_group_create_choices],
269 [x[0] for x in c.fork_choices],
269 [x[0] for x in c.fork_choices],
270 [x[0] for x in c.inherit_default_permission_choices])()
270 [x[0] for x in c.inherit_default_permission_choices])()
271
271
272 try:
272 try:
273 form_result = _form.to_python(dict(self.request.POST))
273 form_result = _form.to_python(dict(self.request.POST))
274 form_result.update({'perm_user_name': User.DEFAULT_USER})
274 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 PermissionModel().update_user_permissions(form_result)
275 PermissionModel().update_user_permissions(form_result)
276
276
277 Session().commit()
277 Session().commit()
278 h.flash(_('Global permissions updated successfully'),
278 h.flash(_('Global permissions updated successfully'),
279 category='success')
279 category='success')
280
280
281 except formencode.Invalid as errors:
281 except formencode.Invalid as errors:
282 defaults = errors.value
282 defaults = errors.value
283
283
284 data = render(
284 data = render(
285 'rhodecode:templates/admin/permissions/permissions.mako',
285 'rhodecode:templates/admin/permissions/permissions.mako',
286 self._get_template_context(c), self.request)
286 self._get_template_context(c), self.request)
287 html = formencode.htmlfill.render(
287 html = formencode.htmlfill.render(
288 data,
288 data,
289 defaults=defaults,
289 defaults=defaults,
290 errors=errors.unpack_errors() or {},
290 errors=errors.unpack_errors() or {},
291 prefix_error=False,
291 prefix_error=False,
292 encoding="UTF-8",
292 encoding="UTF-8",
293 force_defaults=False
293 force_defaults=False
294 )
294 )
295 return Response(html)
295 return Response(html)
296 except Exception:
296 except Exception:
297 log.exception("Exception during update of permissions")
297 log.exception("Exception during update of permissions")
298 h.flash(_('Error occurred during update of permissions'),
298 h.flash(_('Error occurred during update of permissions'),
299 category='error')
299 category='error')
300
300
301 affected_user_ids = [User.get_default_user_id()]
301 affected_user_ids = [User.get_default_user_id()]
302 PermissionModel().trigger_permission_flush(affected_user_ids)
302 PermissionModel().trigger_permission_flush(affected_user_ids)
303
303
304 raise HTTPFound(h.route_path('admin_permissions_global'))
304 raise HTTPFound(h.route_path('admin_permissions_global'))
305
305
306 @LoginRequired()
306 @LoginRequired()
307 @HasPermissionAllDecorator('hg.admin')
307 @HasPermissionAllDecorator('hg.admin')
308 def permissions_ips(self):
308 def permissions_ips(self):
309 c = self.load_default_context()
309 c = self.load_default_context()
310 c.active = 'ips'
310 c.active = 'ips'
311
311
312 c.user = User.get_default_user(refresh=True)
312 c.user = User.get_default_user(refresh=True)
313 c.user_ip_map = (
313 c.user_ip_map = (
314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315
315
316 return self._get_template_context(c)
316 return self._get_template_context(c)
317
317
318 @LoginRequired()
318 @LoginRequired()
319 @HasPermissionAllDecorator('hg.admin')
319 @HasPermissionAllDecorator('hg.admin')
320 def permissions_overview(self):
320 def permissions_overview(self):
321 c = self.load_default_context()
321 c = self.load_default_context()
322 c.active = 'perms'
322 c.active = 'perms'
323
323
324 c.user = User.get_default_user(refresh=True)
324 c.user = User.get_default_user(refresh=True)
325 c.perm_user = c.user.AuthUser()
325 c.perm_user = c.user.AuthUser()
326 return self._get_template_context(c)
326 return self._get_template_context(c)
327
327
328 @LoginRequired()
328 @LoginRequired()
329 @HasPermissionAllDecorator('hg.admin')
329 @HasPermissionAllDecorator('hg.admin')
330 def auth_token_access(self):
330 def auth_token_access(self):
331 from rhodecode import CONFIG
331 from rhodecode import CONFIG
332
332
333 c = self.load_default_context()
333 c = self.load_default_context()
334 c.active = 'auth_token_access'
334 c.active = 'auth_token_access'
335
335
336 c.user = User.get_default_user(refresh=True)
336 c.user = User.get_default_user(refresh=True)
337 c.perm_user = c.user.AuthUser()
337 c.perm_user = c.user.AuthUser()
338
338
339 mapper = self.request.registry.queryUtility(IRoutesMapper)
339 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 c.view_data = []
340 c.view_data = []
341
341
342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 introspector = self.request.registry.introspector
343 introspector = self.request.registry.introspector
344
344
345 view_intr = {}
345 view_intr = {}
346 for view_data in introspector.get_category('views'):
346 for view_data in introspector.get_category('views'):
347 intr = view_data['introspectable']
347 intr = view_data['introspectable']
348
348
349 if 'route_name' in intr and intr['attr']:
349 if 'route_name' in intr and intr['attr']:
350 view_intr[intr['route_name']] = '{}:{}'.format(
350 view_intr[intr['route_name']] = '{}:{}'.format(
351 str(intr['derived_callable'].__name__), intr['attr']
351 str(intr['derived_callable'].__name__), intr['attr']
352 )
352 )
353
353
354 c.whitelist_key = 'api_access_controllers_whitelist'
354 c.whitelist_key = 'api_access_controllers_whitelist'
355 c.whitelist_file = CONFIG.get('__file__')
355 c.whitelist_file = CONFIG.get('__file__')
356 whitelist_views = aslist(
356 whitelist_views = aslist(
357 CONFIG.get(c.whitelist_key), sep=',')
357 CONFIG.get(c.whitelist_key), sep=',')
358
358
359 for route_info in mapper.get_routes():
359 for route_info in mapper.get_routes():
360 if not route_info.name.startswith('__'):
360 if not route_info.name.startswith('__'):
361 routepath = route_info.pattern
361 routepath = route_info.pattern
362
362
363 def replace(matchobj):
363 def replace(matchobj):
364 if matchobj.group(1):
364 if matchobj.group(1):
365 return "{%s}" % matchobj.group(1).split(':')[0]
365 return "{%s}" % matchobj.group(1).split(':')[0]
366 else:
366 else:
367 return "{%s}" % matchobj.group(2)
367 return "{%s}" % matchobj.group(2)
368
368
369 routepath = _argument_prog.sub(replace, routepath)
369 routepath = _argument_prog.sub(replace, routepath)
370
370
371 if not routepath.startswith('/'):
371 if not routepath.startswith('/'):
372 routepath = '/' + routepath
372 routepath = '/' + routepath
373
373
374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 active = view_fqn in whitelist_views
375 active = view_fqn in whitelist_views
376 c.view_data.append((route_info.name, view_fqn, routepath, active))
376 c.view_data.append((route_info.name, view_fqn, routepath, active))
377
377
378 c.whitelist_views = whitelist_views
378 c.whitelist_views = whitelist_views
379 return self._get_template_context(c)
379 return self._get_template_context(c)
380
380
381 def ssh_enabled(self):
381 def ssh_enabled(self):
382 return self.request.registry.settings.get(
382 return self.request.registry.settings.get(
383 'ssh.generate_authorized_keyfile')
383 'ssh.generate_authorized_keyfile')
384
384
385 @LoginRequired()
385 @LoginRequired()
386 @HasPermissionAllDecorator('hg.admin')
386 @HasPermissionAllDecorator('hg.admin')
387 def ssh_keys(self):
387 def ssh_keys(self):
388 c = self.load_default_context()
388 c = self.load_default_context()
389 c.active = 'ssh_keys'
389 c.active = 'ssh_keys'
390 c.ssh_enabled = self.ssh_enabled()
390 c.ssh_enabled = self.ssh_enabled()
391 return self._get_template_context(c)
391 return self._get_template_context(c)
392
392
393 @LoginRequired()
393 @LoginRequired()
394 @HasPermissionAllDecorator('hg.admin')
394 @HasPermissionAllDecorator('hg.admin')
395 def ssh_keys_data(self):
395 def ssh_keys_data(self):
396 _ = self.request.translate
396 _ = self.request.translate
397 self.load_default_context()
397 self.load_default_context()
398 column_map = {
398 column_map = {
399 'fingerprint': 'ssh_key_fingerprint',
399 'fingerprint': 'ssh_key_fingerprint',
400 'username': User.username
400 'username': User.username
401 }
401 }
402 draw, start, limit = self._extract_chunk(self.request)
402 draw, start, limit = self._extract_chunk(self.request)
403 search_q, order_by, order_dir = self._extract_ordering(
403 search_q, order_by, order_dir = self._extract_ordering(
404 self.request, column_map=column_map)
404 self.request, column_map=column_map)
405
405
406 ssh_keys_data_total_count = UserSshKeys.query()\
406 ssh_keys_data_total_count = UserSshKeys.query()\
407 .count()
407 .count()
408
408
409 # json generate
409 # json generate
410 base_q = UserSshKeys.query().join(UserSshKeys.user)
410 base_q = UserSshKeys.query().join(UserSshKeys.user)
411
411
412 if search_q:
412 if search_q:
413 like_expression = u'%{}%'.format(safe_unicode(search_q))
413 like_expression = u'%{}%'.format(safe_str(search_q))
414 base_q = base_q.filter(or_(
414 base_q = base_q.filter(or_(
415 User.username.ilike(like_expression),
415 User.username.ilike(like_expression),
416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 ))
417 ))
418
418
419 users_data_total_filtered_count = base_q.count()
419 users_data_total_filtered_count = base_q.count()
420
420
421 sort_col = self._get_order_col(order_by, UserSshKeys)
421 sort_col = self._get_order_col(order_by, UserSshKeys)
422 if sort_col:
422 if sort_col:
423 if order_dir == 'asc':
423 if order_dir == 'asc':
424 # handle null values properly to order by NULL last
424 # handle null values properly to order by NULL last
425 if order_by in ['created_on']:
425 if order_by in ['created_on']:
426 sort_col = coalesce(sort_col, datetime.date.max)
426 sort_col = coalesce(sort_col, datetime.date.max)
427 sort_col = sort_col.asc()
427 sort_col = sort_col.asc()
428 else:
428 else:
429 # handle null values properly to order by NULL last
429 # handle null values properly to order by NULL last
430 if order_by in ['created_on']:
430 if order_by in ['created_on']:
431 sort_col = coalesce(sort_col, datetime.date.min)
431 sort_col = coalesce(sort_col, datetime.date.min)
432 sort_col = sort_col.desc()
432 sort_col = sort_col.desc()
433
433
434 base_q = base_q.order_by(sort_col)
434 base_q = base_q.order_by(sort_col)
435 base_q = base_q.offset(start).limit(limit)
435 base_q = base_q.offset(start).limit(limit)
436
436
437 ssh_keys = base_q.all()
437 ssh_keys = base_q.all()
438
438
439 ssh_keys_data = []
439 ssh_keys_data = []
440 for ssh_key in ssh_keys:
440 for ssh_key in ssh_keys:
441 ssh_keys_data.append({
441 ssh_keys_data.append({
442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 "fingerprint": ssh_key.ssh_key_fingerprint,
443 "fingerprint": ssh_key.ssh_key_fingerprint,
444 "description": ssh_key.description,
444 "description": ssh_key.description,
445 "created_on": h.format_date(ssh_key.created_on),
445 "created_on": h.format_date(ssh_key.created_on),
446 "accessed_on": h.format_date(ssh_key.accessed_on),
446 "accessed_on": h.format_date(ssh_key.accessed_on),
447 "action": h.link_to(
447 "action": h.link_to(
448 _('Edit'), h.route_path('edit_user_ssh_keys',
448 _('Edit'), h.route_path('edit_user_ssh_keys',
449 user_id=ssh_key.user.user_id))
449 user_id=ssh_key.user.user_id))
450 })
450 })
451
451
452 data = ({
452 data = ({
453 'draw': draw,
453 'draw': draw,
454 'data': ssh_keys_data,
454 'data': ssh_keys_data,
455 'recordsTotal': ssh_keys_data_total_count,
455 'recordsTotal': ssh_keys_data_total_count,
456 'recordsFiltered': users_data_total_filtered_count,
456 'recordsFiltered': users_data_total_filtered_count,
457 })
457 })
458
458
459 return data
459 return data
460
460
461 @LoginRequired()
461 @LoginRequired()
462 @HasPermissionAllDecorator('hg.admin')
462 @HasPermissionAllDecorator('hg.admin')
463 @CSRFRequired()
463 @CSRFRequired()
464 def ssh_keys_update(self):
464 def ssh_keys_update(self):
465 _ = self.request.translate
465 _ = self.request.translate
466 self.load_default_context()
466 self.load_default_context()
467
467
468 ssh_enabled = self.ssh_enabled()
468 ssh_enabled = self.ssh_enabled()
469 key_file = self.request.registry.settings.get(
469 key_file = self.request.registry.settings.get(
470 'ssh.authorized_keys_file_path')
470 'ssh.authorized_keys_file_path')
471 if ssh_enabled:
471 if ssh_enabled:
472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 h.flash(_('Updated SSH keys file: {}').format(key_file),
473 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 category='success')
474 category='success')
475 else:
475 else:
476 h.flash(_('SSH key support is disabled in .ini file'),
476 h.flash(_('SSH key support is disabled in .ini file'),
477 category='warning')
477 category='warning')
478
478
479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,356 +1,356 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import datetime
20 import datetime
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26
26
27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28
28
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34
34
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, CSRFRequired, NotAnonymous,
36 LoginRequired, CSRFRequired, NotAnonymous,
37 HasPermissionAny, HasRepoGroupPermissionAny)
37 HasPermissionAny, HasRepoGroupPermissionAny)
38 from rhodecode.lib import helpers as h, audit_logger
38 from rhodecode.lib import helpers as h, audit_logger
39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
39 from rhodecode.lib.str_utils import safe_int, safe_str
40 from rhodecode.model.forms import RepoGroupForm
40 from rhodecode.model.forms import RepoGroupForm
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.repo_group import RepoGroupModel
42 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.scm import RepoGroupList
43 from rhodecode.model.scm import RepoGroupList
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51
51
52 def load_default_context(self):
52 def load_default_context(self):
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54
54
55 return c
55 return c
56
56
57 def _load_form_data(self, c):
57 def _load_form_data(self, c):
58 allow_empty_group = False
58 allow_empty_group = False
59
59
60 if self._can_create_repo_group():
60 if self._can_create_repo_group():
61 # we're global admin, we're ok and we can create TOP level groups
61 # we're global admin, we're ok and we can create TOP level groups
62 allow_empty_group = True
62 allow_empty_group = True
63
63
64 # override the choices for this form, we need to filter choices
64 # override the choices for this form, we need to filter choices
65 # and display only those we have ADMIN right
65 # and display only those we have ADMIN right
66 groups_with_admin_rights = RepoGroupList(
66 groups_with_admin_rights = RepoGroupList(
67 RepoGroup.query().all(),
67 RepoGroup.query().all(),
68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 c.repo_groups = RepoGroup.groups_choices(
69 c.repo_groups = RepoGroup.groups_choices(
70 groups=groups_with_admin_rights,
70 groups=groups_with_admin_rights,
71 show_empty_group=allow_empty_group)
71 show_empty_group=allow_empty_group)
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73
73
74 def _can_create_repo_group(self, parent_group_id=None):
74 def _can_create_repo_group(self, parent_group_id=None):
75 is_admin = HasPermissionAny('hg.admin')('group create controller')
75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 create_repo_group = HasPermissionAny(
76 create_repo_group = HasPermissionAny(
77 'hg.repogroup.create.true')('group create controller')
77 'hg.repogroup.create.true')('group create controller')
78 if is_admin or (create_repo_group and not parent_group_id):
78 if is_admin or (create_repo_group and not parent_group_id):
79 # we're global admin, or we have global repo group create
79 # we're global admin, or we have global repo group create
80 # permission
80 # permission
81 # we're ok and we can create TOP level groups
81 # we're ok and we can create TOP level groups
82 return True
82 return True
83 elif parent_group_id:
83 elif parent_group_id:
84 # we check the permission if we can write to parent group
84 # we check the permission if we can write to parent group
85 group = RepoGroup.get(parent_group_id)
85 group = RepoGroup.get(parent_group_id)
86 group_name = group.group_name if group else None
86 group_name = group.group_name if group else None
87 if HasRepoGroupPermissionAny('group.admin')(
87 if HasRepoGroupPermissionAny('group.admin')(
88 group_name, 'check if user is an admin of group'):
88 group_name, 'check if user is an admin of group'):
89 # we're an admin of passed in group, we're ok.
89 # we're an admin of passed in group, we're ok.
90 return True
90 return True
91 else:
91 else:
92 return False
92 return False
93 return False
93 return False
94
94
95 # permission check in data loading of
95 # permission check in data loading of
96 # `repo_group_list_data` via RepoGroupList
96 # `repo_group_list_data` via RepoGroupList
97 @LoginRequired()
97 @LoginRequired()
98 @NotAnonymous()
98 @NotAnonymous()
99 def repo_group_list(self):
99 def repo_group_list(self):
100 c = self.load_default_context()
100 c = self.load_default_context()
101 return self._get_template_context(c)
101 return self._get_template_context(c)
102
102
103 # permission check inside
103 # permission check inside
104 @LoginRequired()
104 @LoginRequired()
105 @NotAnonymous()
105 @NotAnonymous()
106 def repo_group_list_data(self):
106 def repo_group_list_data(self):
107 self.load_default_context()
107 self.load_default_context()
108 column_map = {
108 column_map = {
109 'name': 'group_name_hash',
109 'name': 'group_name_hash',
110 'desc': 'group_description',
110 'desc': 'group_description',
111 'last_change': 'updated_on',
111 'last_change': 'updated_on',
112 'top_level_repos': 'repos_total',
112 'top_level_repos': 'repos_total',
113 'owner': 'user_username',
113 'owner': 'user_username',
114 }
114 }
115 draw, start, limit = self._extract_chunk(self.request)
115 draw, start, limit = self._extract_chunk(self.request)
116 search_q, order_by, order_dir = self._extract_ordering(
116 search_q, order_by, order_dir = self._extract_ordering(
117 self.request, column_map=column_map)
117 self.request, column_map=column_map)
118
118
119 _render = self.request.get_partial_renderer(
119 _render = self.request.get_partial_renderer(
120 'rhodecode:templates/data_table/_dt_elements.mako')
120 'rhodecode:templates/data_table/_dt_elements.mako')
121 c = _render.get_call_context()
121 c = _render.get_call_context()
122
122
123 def quick_menu(repo_group_name):
123 def quick_menu(repo_group_name):
124 return _render('quick_repo_group_menu', repo_group_name)
124 return _render('quick_repo_group_menu', repo_group_name)
125
125
126 def repo_group_lnk(repo_group_name):
126 def repo_group_lnk(repo_group_name):
127 return _render('repo_group_name', repo_group_name)
127 return _render('repo_group_name', repo_group_name)
128
128
129 def last_change(last_change):
129 def last_change(last_change):
130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 ts = time.time()
131 ts = time.time()
132 utc_offset = (datetime.datetime.fromtimestamp(ts)
132 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 return _render("last_change", last_change)
135 return _render("last_change", last_change)
136
136
137 def desc(desc, personal):
137 def desc(desc, personal):
138 return _render(
138 return _render(
139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140
140
141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 return _render(
142 return _render(
143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144
144
145 def user_profile(username):
145 def user_profile(username):
146 return _render('user_profile', username)
146 return _render('user_profile', username)
147
147
148 _perms = ['group.admin']
148 _perms = ['group.admin']
149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150
150
151 repo_groups_data_total_count = RepoGroup.query()\
151 repo_groups_data_total_count = RepoGroup.query()\
152 .filter(or_(
152 .filter(or_(
153 # generate multiple IN to fix limitation problems
153 # generate multiple IN to fix limitation problems
154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 )) \
155 )) \
156 .count()
156 .count()
157
157
158 repo_groups_data_total_inactive_count = RepoGroup.query()\
158 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 .filter(RepoGroup.group_id.in_(allowed_ids))\
159 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 .count()
160 .count()
161
161
162 repo_count = count(Repository.repo_id)
162 repo_count = count(Repository.repo_id)
163 base_q = Session.query(
163 base_q = Session.query(
164 RepoGroup.group_name,
164 RepoGroup.group_name,
165 RepoGroup.group_name_hash,
165 RepoGroup.group_name_hash,
166 RepoGroup.group_description,
166 RepoGroup.group_description,
167 RepoGroup.group_id,
167 RepoGroup.group_id,
168 RepoGroup.personal,
168 RepoGroup.personal,
169 RepoGroup.updated_on,
169 RepoGroup.updated_on,
170 User,
170 User,
171 repo_count.label('repos_count')
171 repo_count.label('repos_count')
172 ) \
172 ) \
173 .filter(or_(
173 .filter(or_(
174 # generate multiple IN to fix limitation problems
174 # generate multiple IN to fix limitation problems
175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 )) \
176 )) \
177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 .join(User, User.user_id == RepoGroup.user_id) \
178 .join(User, User.user_id == RepoGroup.user_id) \
179 .group_by(RepoGroup, User)
179 .group_by(RepoGroup, User)
180
180
181 if search_q:
181 if search_q:
182 like_expression = u'%{}%'.format(safe_unicode(search_q))
182 like_expression = u'%{}%'.format(safe_unicode(search_q))
183 base_q = base_q.filter(or_(
183 base_q = base_q.filter(or_(
184 RepoGroup.group_name.ilike(like_expression),
184 RepoGroup.group_name.ilike(like_expression),
185 ))
185 ))
186
186
187 repo_groups_data_total_filtered_count = base_q.count()
187 repo_groups_data_total_filtered_count = base_q.count()
188 # the inactive isn't really used, but we still make it same as other data grids
188 # the inactive isn't really used, but we still make it same as other data grids
189 # which use inactive (users,user groups)
189 # which use inactive (users,user groups)
190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191
191
192 sort_defined = False
192 sort_defined = False
193 if order_by == 'group_name':
193 if order_by == 'group_name':
194 sort_col = func.lower(RepoGroup.group_name)
194 sort_col = func.lower(RepoGroup.group_name)
195 sort_defined = True
195 sort_defined = True
196 elif order_by == 'repos_total':
196 elif order_by == 'repos_total':
197 sort_col = repo_count
197 sort_col = repo_count
198 sort_defined = True
198 sort_defined = True
199 elif order_by == 'user_username':
199 elif order_by == 'user_username':
200 sort_col = User.username
200 sort_col = User.username
201 else:
201 else:
202 sort_col = getattr(RepoGroup, order_by, None)
202 sort_col = getattr(RepoGroup, order_by, None)
203
203
204 if sort_defined or sort_col:
204 if sort_defined or sort_col:
205 if order_dir == 'asc':
205 if order_dir == 'asc':
206 sort_col = sort_col.asc()
206 sort_col = sort_col.asc()
207 else:
207 else:
208 sort_col = sort_col.desc()
208 sort_col = sort_col.desc()
209
209
210 base_q = base_q.order_by(sort_col)
210 base_q = base_q.order_by(sort_col)
211 base_q = base_q.offset(start).limit(limit)
211 base_q = base_q.offset(start).limit(limit)
212
212
213 # authenticated access to user groups
213 # authenticated access to user groups
214 auth_repo_group_list = base_q.all()
214 auth_repo_group_list = base_q.all()
215
215
216 repo_groups_data = []
216 repo_groups_data = []
217 for repo_gr in auth_repo_group_list:
217 for repo_gr in auth_repo_group_list:
218 row = {
218 row = {
219 "menu": quick_menu(repo_gr.group_name),
219 "menu": quick_menu(repo_gr.group_name),
220 "name": repo_group_lnk(repo_gr.group_name),
220 "name": repo_group_lnk(repo_gr.group_name),
221
221
222 "last_change": last_change(repo_gr.updated_on),
222 "last_change": last_change(repo_gr.updated_on),
223
223
224 "last_changeset": "",
224 "last_changeset": "",
225 "last_changeset_raw": "",
225 "last_changeset_raw": "",
226
226
227 "desc": desc(repo_gr.group_description, repo_gr.personal),
227 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 "owner": user_profile(repo_gr.User.username),
228 "owner": user_profile(repo_gr.User.username),
229 "top_level_repos": repo_gr.repos_count,
229 "top_level_repos": repo_gr.repos_count,
230 "action": repo_group_actions(
230 "action": repo_group_actions(
231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232
232
233 }
233 }
234
234
235 repo_groups_data.append(row)
235 repo_groups_data.append(row)
236
236
237 data = ({
237 data = ({
238 'draw': draw,
238 'draw': draw,
239 'data': repo_groups_data,
239 'data': repo_groups_data,
240 'recordsTotal': repo_groups_data_total_count,
240 'recordsTotal': repo_groups_data_total_count,
241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 'recordsFiltered': repo_groups_data_total_filtered_count,
242 'recordsFiltered': repo_groups_data_total_filtered_count,
243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 })
244 })
245
245
246 return data
246 return data
247
247
248 @LoginRequired()
248 @LoginRequired()
249 @NotAnonymous()
249 @NotAnonymous()
250 # perm checks inside
250 # perm checks inside
251 def repo_group_new(self):
251 def repo_group_new(self):
252 c = self.load_default_context()
252 c = self.load_default_context()
253
253
254 # perm check for admin, create_group perm or admin of parent_group
254 # perm check for admin, create_group perm or admin of parent_group
255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 _gr = RepoGroup.get(parent_group_id)
256 _gr = RepoGroup.get(parent_group_id)
257 if not self._can_create_repo_group(parent_group_id):
257 if not self._can_create_repo_group(parent_group_id):
258 raise HTTPForbidden()
258 raise HTTPForbidden()
259
259
260 self._load_form_data(c)
260 self._load_form_data(c)
261
261
262 defaults = {} # Future proof for default of repo group
262 defaults = {} # Future proof for default of repo group
263
263
264 parent_group_choice = '-1'
264 parent_group_choice = '-1'
265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 parent_group_choice = self._rhodecode_user.personal_repo_group
266 parent_group_choice = self._rhodecode_user.personal_repo_group
267
267
268 if parent_group_id and _gr:
268 if parent_group_id and _gr:
269 if parent_group_id in [x[0] for x in c.repo_groups]:
269 if parent_group_id in [x[0] for x in c.repo_groups]:
270 parent_group_choice = safe_unicode(parent_group_id)
270 parent_group_choice = safe_unicode(parent_group_id)
271
271
272 defaults.update({'group_parent_id': parent_group_choice})
272 defaults.update({'group_parent_id': parent_group_choice})
273
273
274 data = render(
274 data = render(
275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 self._get_template_context(c), self.request)
276 self._get_template_context(c), self.request)
277
277
278 html = formencode.htmlfill.render(
278 html = formencode.htmlfill.render(
279 data,
279 data,
280 defaults=defaults,
280 defaults=defaults,
281 encoding="UTF-8",
281 encoding="UTF-8",
282 force_defaults=False
282 force_defaults=False
283 )
283 )
284 return Response(html)
284 return Response(html)
285
285
286 @LoginRequired()
286 @LoginRequired()
287 @NotAnonymous()
287 @NotAnonymous()
288 @CSRFRequired()
288 @CSRFRequired()
289 # perm checks inside
289 # perm checks inside
290 def repo_group_create(self):
290 def repo_group_create(self):
291 c = self.load_default_context()
291 c = self.load_default_context()
292 _ = self.request.translate
292 _ = self.request.translate
293
293
294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 can_create = self._can_create_repo_group(parent_group_id)
295 can_create = self._can_create_repo_group(parent_group_id)
296
296
297 self._load_form_data(c)
297 self._load_form_data(c)
298 # permissions for can create group based on parent_id are checked
298 # permissions for can create group based on parent_id are checked
299 # here in the Form
299 # here in the Form
300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 repo_group_form = RepoGroupForm(
301 repo_group_form = RepoGroupForm(
302 self.request.translate, available_groups=available_groups,
302 self.request.translate, available_groups=available_groups,
303 can_create_in_root=can_create)()
303 can_create_in_root=can_create)()
304
304
305 repo_group_name = self.request.POST.get('group_name')
305 repo_group_name = self.request.POST.get('group_name')
306 try:
306 try:
307 owner = self._rhodecode_user
307 owner = self._rhodecode_user
308 form_result = repo_group_form.to_python(dict(self.request.POST))
308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 copy_permissions = form_result.get('group_copy_permissions')
309 copy_permissions = form_result.get('group_copy_permissions')
310 repo_group = RepoGroupModel().create(
310 repo_group = RepoGroupModel().create(
311 group_name=form_result['group_name_full'],
311 group_name=form_result['group_name_full'],
312 group_description=form_result['group_description'],
312 group_description=form_result['group_description'],
313 owner=owner.user_id,
313 owner=owner.user_id,
314 copy_permissions=form_result['group_copy_permissions']
314 copy_permissions=form_result['group_copy_permissions']
315 )
315 )
316 Session().flush()
316 Session().flush()
317
317
318 repo_group_data = repo_group.get_api_data()
318 repo_group_data = repo_group.get_api_data()
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'repo_group.create', action_data={'data': repo_group_data},
320 'repo_group.create', action_data={'data': repo_group_data},
321 user=self._rhodecode_user)
321 user=self._rhodecode_user)
322
322
323 Session().commit()
323 Session().commit()
324
324
325 _new_group_name = form_result['group_name_full']
325 _new_group_name = form_result['group_name_full']
326
326
327 repo_group_url = h.link_to(
327 repo_group_url = h.link_to(
328 _new_group_name,
328 _new_group_name,
329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 h.flash(h.literal(_('Created repository group %s')
330 h.flash(h.literal(_('Created repository group %s')
331 % repo_group_url), category='success')
331 % repo_group_url), category='success')
332
332
333 except formencode.Invalid as errors:
333 except formencode.Invalid as errors:
334 data = render(
334 data = render(
335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 self._get_template_context(c), self.request)
336 self._get_template_context(c), self.request)
337 html = formencode.htmlfill.render(
337 html = formencode.htmlfill.render(
338 data,
338 data,
339 defaults=errors.value,
339 defaults=errors.value,
340 errors=errors.unpack_errors() or {},
340 errors=errors.unpack_errors() or {},
341 prefix_error=False,
341 prefix_error=False,
342 encoding="UTF-8",
342 encoding="UTF-8",
343 force_defaults=False
343 force_defaults=False
344 )
344 )
345 return Response(html)
345 return Response(html)
346 except Exception:
346 except Exception:
347 log.exception("Exception during creation of repository group")
347 log.exception("Exception during creation of repository group")
348 h.flash(_('Error occurred during creation of repository group %s')
348 h.flash(_('Error occurred during creation of repository group %s')
349 % repo_group_name, category='error')
349 % repo_group_name, category='error')
350 raise HTTPFound(h.route_path('home'))
350 raise HTTPFound(h.route_path('home'))
351
351
352 PermissionModel().trigger_permission_flush()
352 PermissionModel().trigger_permission_flush()
353
353
354 raise HTTPFound(
354 raise HTTPFound(
355 h.route_path('repo_group_home',
355 h.route_path('repo_group_home',
356 repo_group_name=form_result['group_name_full']))
356 repo_group_name=form_result['group_name_full']))
@@ -1,250 +1,250 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22 import formencode
22 import formencode
23 import formencode.htmlfill
23 import formencode.htmlfill
24
24
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26
26
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.lib.celerylib.utils import get_task_id
32 from rhodecode.lib.celerylib.utils import get_task_id
33
33
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, CSRFRequired, NotAnonymous,
35 LoginRequired, CSRFRequired, NotAnonymous,
36 HasPermissionAny, HasRepoGroupPermissionAny)
36 HasPermissionAny, HasRepoGroupPermissionAny)
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.utils import repo_name_slug
38 from rhodecode.lib.utils import repo_name_slug
39 from rhodecode.lib.utils2 import safe_int, safe_unicode
39 from rhodecode.lib.utils2 import safe_int, safe_str
40 from rhodecode.model.forms import RepoForm
40 from rhodecode.model.forms import RepoForm
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.db import (
45 from rhodecode.model.db import (
46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class AdminReposView(BaseAppView, DataGridAppView):
51 class AdminReposView(BaseAppView, DataGridAppView):
52
52
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 return c
55 return c
56
56
57 def _load_form_data(self, c):
57 def _load_form_data(self, c):
58 acl_groups = RepoGroupList(RepoGroup.query().all(),
58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 perm_set=['group.write', 'group.admin'])
59 perm_set=['group.write', 'group.admin'])
60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
61 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @NotAnonymous()
65 @NotAnonymous()
66 # perms check inside
66 # perms check inside
67 def repository_list(self):
67 def repository_list(self):
68 c = self.load_default_context()
68 c = self.load_default_context()
69 return self._get_template_context(c)
69 return self._get_template_context(c)
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 # perms check inside
73 # perms check inside
74 def repository_list_data(self):
74 def repository_list_data(self):
75 self.load_default_context()
75 self.load_default_context()
76 column_map = {
76 column_map = {
77 'name': 'repo_name',
77 'name': 'repo_name',
78 'desc': 'description',
78 'desc': 'description',
79 'last_change': 'updated_on',
79 'last_change': 'updated_on',
80 'owner': 'user_username',
80 'owner': 'user_username',
81 }
81 }
82 draw, start, limit = self._extract_chunk(self.request)
82 draw, start, limit = self._extract_chunk(self.request)
83 search_q, order_by, order_dir = self._extract_ordering(
83 search_q, order_by, order_dir = self._extract_ordering(
84 self.request, column_map=column_map)
84 self.request, column_map=column_map)
85
85
86 _perms = ['repository.admin']
86 _perms = ['repository.admin']
87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88
88
89 repos_data_total_count = Repository.query() \
89 repos_data_total_count = Repository.query() \
90 .filter(or_(
90 .filter(or_(
91 # generate multiple IN to fix limitation problems
91 # generate multiple IN to fix limitation problems
92 *in_filter_generator(Repository.repo_id, allowed_ids))
92 *in_filter_generator(Repository.repo_id, allowed_ids))
93 ) \
93 ) \
94 .count()
94 .count()
95
95
96 base_q = Session.query(
96 base_q = Session.query(
97 Repository.repo_id,
97 Repository.repo_id,
98 Repository.repo_name,
98 Repository.repo_name,
99 Repository.description,
99 Repository.description,
100 Repository.repo_type,
100 Repository.repo_type,
101 Repository.repo_state,
101 Repository.repo_state,
102 Repository.private,
102 Repository.private,
103 Repository.archived,
103 Repository.archived,
104 Repository.fork,
104 Repository.fork,
105 Repository.updated_on,
105 Repository.updated_on,
106 Repository._changeset_cache,
106 Repository._changeset_cache,
107 User,
107 User,
108 ) \
108 ) \
109 .filter(or_(
109 .filter(or_(
110 # generate multiple IN to fix limitation problems
110 # generate multiple IN to fix limitation problems
111 *in_filter_generator(Repository.repo_id, allowed_ids))
111 *in_filter_generator(Repository.repo_id, allowed_ids))
112 ) \
112 ) \
113 .join(User, User.user_id == Repository.user_id) \
113 .join(User, User.user_id == Repository.user_id) \
114 .group_by(Repository, User)
114 .group_by(Repository, User)
115
115
116 if search_q:
116 if search_q:
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 like_expression = u'%{}%'.format(safe_str(search_q))
118 base_q = base_q.filter(or_(
118 base_q = base_q.filter(or_(
119 Repository.repo_name.ilike(like_expression),
119 Repository.repo_name.ilike(like_expression),
120 ))
120 ))
121
121
122 repos_data_total_filtered_count = base_q.count()
122 repos_data_total_filtered_count = base_q.count()
123
123
124 sort_defined = False
124 sort_defined = False
125 if order_by == 'repo_name':
125 if order_by == 'repo_name':
126 sort_col = func.lower(Repository.repo_name)
126 sort_col = func.lower(Repository.repo_name)
127 sort_defined = True
127 sort_defined = True
128 elif order_by == 'user_username':
128 elif order_by == 'user_username':
129 sort_col = User.username
129 sort_col = User.username
130 else:
130 else:
131 sort_col = getattr(Repository, order_by, None)
131 sort_col = getattr(Repository, order_by, None)
132
132
133 if sort_defined or sort_col:
133 if sort_defined or sort_col:
134 if order_dir == 'asc':
134 if order_dir == 'asc':
135 sort_col = sort_col.asc()
135 sort_col = sort_col.asc()
136 else:
136 else:
137 sort_col = sort_col.desc()
137 sort_col = sort_col.desc()
138
138
139 base_q = base_q.order_by(sort_col)
139 base_q = base_q.order_by(sort_col)
140 base_q = base_q.offset(start).limit(limit)
140 base_q = base_q.offset(start).limit(limit)
141
141
142 repos_list = base_q.all()
142 repos_list = base_q.all()
143
143
144 repos_data = RepoModel().get_repos_as_dict(
144 repos_data = RepoModel().get_repos_as_dict(
145 repo_list=repos_list, admin=True, super_user_actions=True)
145 repo_list=repos_list, admin=True, super_user_actions=True)
146
146
147 data = ({
147 data = ({
148 'draw': draw,
148 'draw': draw,
149 'data': repos_data,
149 'data': repos_data,
150 'recordsTotal': repos_data_total_count,
150 'recordsTotal': repos_data_total_count,
151 'recordsFiltered': repos_data_total_filtered_count,
151 'recordsFiltered': repos_data_total_filtered_count,
152 })
152 })
153 return data
153 return data
154
154
155 @LoginRequired()
155 @LoginRequired()
156 @NotAnonymous()
156 @NotAnonymous()
157 # perms check inside
157 # perms check inside
158 def repository_new(self):
158 def repository_new(self):
159 c = self.load_default_context()
159 c = self.load_default_context()
160
160
161 new_repo = self.request.GET.get('repo', '')
161 new_repo = self.request.GET.get('repo', '')
162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 _gr = RepoGroup.get(parent_group_id)
163 _gr = RepoGroup.get(parent_group_id)
164
164
165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 # you're not super admin nor have global create permissions,
166 # you're not super admin nor have global create permissions,
167 # but maybe you have at least write permission to a parent group ?
167 # but maybe you have at least write permission to a parent group ?
168
168
169 gr_name = _gr.group_name if _gr else None
169 gr_name = _gr.group_name if _gr else None
170 # create repositories with write permission on group is set to true
170 # create repositories with write permission on group is set to true
171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 if not (group_admin or (group_write and create_on_write)):
174 if not (group_admin or (group_write and create_on_write)):
175 raise HTTPForbidden()
175 raise HTTPForbidden()
176
176
177 self._load_form_data(c)
177 self._load_form_data(c)
178 c.new_repo = repo_name_slug(new_repo)
178 c.new_repo = repo_name_slug(new_repo)
179
179
180 # apply the defaults from defaults page
180 # apply the defaults from defaults page
181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 # set checkbox to autochecked
182 # set checkbox to autochecked
183 defaults['repo_copy_permissions'] = True
183 defaults['repo_copy_permissions'] = True
184
184
185 parent_group_choice = '-1'
185 parent_group_choice = '-1'
186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 parent_group_choice = self._rhodecode_user.personal_repo_group
187 parent_group_choice = self._rhodecode_user.personal_repo_group
188
188
189 if parent_group_id and _gr:
189 if parent_group_id and _gr:
190 if parent_group_id in [x[0] for x in c.repo_groups]:
190 if parent_group_id in [x[0] for x in c.repo_groups]:
191 parent_group_choice = safe_unicode(parent_group_id)
191 parent_group_choice = safe_unicode(parent_group_id)
192
192
193 defaults.update({'repo_group': parent_group_choice})
193 defaults.update({'repo_group': parent_group_choice})
194
194
195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 self._get_template_context(c), self.request)
196 self._get_template_context(c), self.request)
197 html = formencode.htmlfill.render(
197 html = formencode.htmlfill.render(
198 data,
198 data,
199 defaults=defaults,
199 defaults=defaults,
200 encoding="UTF-8",
200 encoding="UTF-8",
201 force_defaults=False
201 force_defaults=False
202 )
202 )
203 return Response(html)
203 return Response(html)
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 @CSRFRequired()
207 @CSRFRequired()
208 # perms check inside
208 # perms check inside
209 def repository_create(self):
209 def repository_create(self):
210 c = self.load_default_context()
210 c = self.load_default_context()
211
211
212 form_result = {}
212 form_result = {}
213 self._load_form_data(c)
213 self._load_form_data(c)
214
214
215 try:
215 try:
216 # CanWriteToGroup validators checks permissions of this POST
216 # CanWriteToGroup validators checks permissions of this POST
217 form = RepoForm(
217 form = RepoForm(
218 self.request.translate, repo_groups=c.repo_groups_choices)()
218 self.request.translate, repo_groups=c.repo_groups_choices)()
219 form_result = form.to_python(dict(self.request.POST))
219 form_result = form.to_python(dict(self.request.POST))
220 copy_permissions = form_result.get('repo_copy_permissions')
220 copy_permissions = form_result.get('repo_copy_permissions')
221 # create is done sometimes async on celery, db transaction
221 # create is done sometimes async on celery, db transaction
222 # management is handled there.
222 # management is handled there.
223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 task_id = get_task_id(task)
224 task_id = get_task_id(task)
225 except formencode.Invalid as errors:
225 except formencode.Invalid as errors:
226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 self._get_template_context(c), self.request)
227 self._get_template_context(c), self.request)
228 html = formencode.htmlfill.render(
228 html = formencode.htmlfill.render(
229 data,
229 data,
230 defaults=errors.value,
230 defaults=errors.value,
231 errors=errors.unpack_errors() or {},
231 errors=errors.unpack_errors() or {},
232 prefix_error=False,
232 prefix_error=False,
233 encoding="UTF-8",
233 encoding="UTF-8",
234 force_defaults=False
234 force_defaults=False
235 )
235 )
236 return Response(html)
236 return Response(html)
237
237
238 except Exception as e:
238 except Exception as e:
239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 h.flash(msg, category='error')
240 h.flash(msg, category='error')
241 raise HTTPFound(h.route_path('home'))
241 raise HTTPFound(h.route_path('home'))
242
242
243 repo_name = form_result.get('repo_name_full')
243 repo_name = form_result.get('repo_name_full')
244
244
245 affected_user_ids = [self._rhodecode_user.user_id]
245 affected_user_ids = [self._rhodecode_user.user_id]
246 PermissionModel().trigger_permission_flush(affected_user_ids)
246 PermissionModel().trigger_permission_flush(affected_user_ids)
247
247
248 raise HTTPFound(
248 raise HTTPFound(
249 h.route_path('repo_creating', repo_name=repo_name,
249 h.route_path('repo_creating', repo_name=repo_name,
250 _query=dict(task_id=task_id)))
250 _query=dict(task_id=task_id)))
@@ -1,719 +1,722 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import datetime
24 import datetime
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27
27
28 import rhodecode
28 import rhodecode
29
29
30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.renderers import render
31 from pyramid.renderers import render
32 from pyramid.response import Response
32 from pyramid.response import Response
33
33
34 from rhodecode.apps._base import BaseAppView
34 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base.navigation import navigation_list
35 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps.svn_support.config_keys import generate_config
36 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 from rhodecode.lib.celerylib import tasks, run_task
40 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.str_utils import safe_str
41 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
43 from rhodecode.lib.utils2 import str2bool, AttributeDict
43 from rhodecode.lib.index import searcher_from_config
44 from rhodecode.lib.index import searcher_from_config
44
45
45 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.forms import (ApplicationSettingsForm,
47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 LabsSettingsForm, IssueTrackerPatternsForm)
49 LabsSettingsForm, IssueTrackerPatternsForm)
49 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.repo_group import RepoGroupModel
51
52
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
55 from rhodecode.model.settings import (
56 from rhodecode.model.settings import (
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 SettingsModel)
58 SettingsModel)
58
59
59
60
60 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
61
62
62
63
63 class AdminSettingsView(BaseAppView):
64 class AdminSettingsView(BaseAppView):
64
65
65 def load_default_context(self):
66 def load_default_context(self):
66 c = self._get_local_tmpl_context()
67 c = self._get_local_tmpl_context()
67 c.labs_active = str2bool(
68 c.labs_active = str2bool(
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 c.navlist = navigation_list(self.request)
70 c.navlist = navigation_list(self.request)
70 return c
71 return c
71
72
72 @classmethod
73 @classmethod
73 def _get_ui_settings(cls):
74 def _get_ui_settings(cls):
74 ret = RhodeCodeUi.query().all()
75 ret = RhodeCodeUi.query().all()
75
76
76 if not ret:
77 if not ret:
77 raise Exception('Could not get application ui settings !')
78 raise Exception('Could not get application ui settings !')
78 settings = {}
79 settings = {}
79 for each in ret:
80 for each in ret:
80 k = each.ui_key
81 k = each.ui_key
81 v = each.ui_value
82 v = each.ui_value
82 if k == '/':
83 if k == '/':
83 k = 'root_path'
84 k = 'root_path'
84
85
85 if k in ['push_ssl', 'publish', 'enabled']:
86 if k in ['push_ssl', 'publish', 'enabled']:
86 v = str2bool(v)
87 v = str2bool(v)
87
88
88 if k.find('.') != -1:
89 if k.find('.') != -1:
89 k = k.replace('.', '_')
90 k = k.replace('.', '_')
90
91
91 if each.ui_section in ['hooks', 'extensions']:
92 if each.ui_section in ['hooks', 'extensions']:
92 v = each.ui_active
93 v = each.ui_active
93
94
94 settings[each.ui_section + '_' + k] = v
95 settings[each.ui_section + '_' + k] = v
95 return settings
96 return settings
96
97
97 @classmethod
98 @classmethod
98 def _form_defaults(cls):
99 def _form_defaults(cls):
99 defaults = SettingsModel().get_all_settings()
100 defaults = SettingsModel().get_all_settings()
100 defaults.update(cls._get_ui_settings())
101 defaults.update(cls._get_ui_settings())
101
102
102 defaults.update({
103 defaults.update({
103 'new_svn_branch': '',
104 'new_svn_branch': '',
104 'new_svn_tag': '',
105 'new_svn_tag': '',
105 })
106 })
106 return defaults
107 return defaults
107
108
108 @LoginRequired()
109 @LoginRequired()
109 @HasPermissionAllDecorator('hg.admin')
110 @HasPermissionAllDecorator('hg.admin')
110 def settings_vcs(self):
111 def settings_vcs(self):
111 c = self.load_default_context()
112 c = self.load_default_context()
112 c.active = 'vcs'
113 c.active = 'vcs'
113 model = VcsSettingsModel()
114 model = VcsSettingsModel()
114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116
117
117 settings = self.request.registry.settings
118 settings = self.request.registry.settings
118 c.svn_proxy_generate_config = settings[generate_config]
119 c.svn_proxy_generate_config = settings[generate_config]
119
120
120 defaults = self._form_defaults()
121 defaults = self._form_defaults()
121
122
122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
123
124
124 data = render('rhodecode:templates/admin/settings/settings.mako',
125 data = render('rhodecode:templates/admin/settings/settings.mako',
125 self._get_template_context(c), self.request)
126 self._get_template_context(c), self.request)
126 html = formencode.htmlfill.render(
127 html = formencode.htmlfill.render(
127 data,
128 data,
128 defaults=defaults,
129 defaults=defaults,
129 encoding="UTF-8",
130 encoding="UTF-8",
130 force_defaults=False
131 force_defaults=False
131 )
132 )
132 return Response(html)
133 return Response(html)
133
134
134 @LoginRequired()
135 @LoginRequired()
135 @HasPermissionAllDecorator('hg.admin')
136 @HasPermissionAllDecorator('hg.admin')
136 @CSRFRequired()
137 @CSRFRequired()
137 def settings_vcs_update(self):
138 def settings_vcs_update(self):
138 _ = self.request.translate
139 _ = self.request.translate
139 c = self.load_default_context()
140 c = self.load_default_context()
140 c.active = 'vcs'
141 c.active = 'vcs'
141
142
142 model = VcsSettingsModel()
143 model = VcsSettingsModel()
143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
145
146
146 settings = self.request.registry.settings
147 settings = self.request.registry.settings
147 c.svn_proxy_generate_config = settings[generate_config]
148 c.svn_proxy_generate_config = settings[generate_config]
148
149
149 application_form = ApplicationUiSettingsForm(self.request.translate)()
150 application_form = ApplicationUiSettingsForm(self.request.translate)()
150
151
151 try:
152 try:
152 form_result = application_form.to_python(dict(self.request.POST))
153 form_result = application_form.to_python(dict(self.request.POST))
153 except formencode.Invalid as errors:
154 except formencode.Invalid as errors:
154 h.flash(
155 h.flash(
155 _("Some form inputs contain invalid data."),
156 _("Some form inputs contain invalid data."),
156 category='error')
157 category='error')
157 data = render('rhodecode:templates/admin/settings/settings.mako',
158 data = render('rhodecode:templates/admin/settings/settings.mako',
158 self._get_template_context(c), self.request)
159 self._get_template_context(c), self.request)
159 html = formencode.htmlfill.render(
160 html = formencode.htmlfill.render(
160 data,
161 data,
161 defaults=errors.value,
162 defaults=errors.value,
162 errors=errors.unpack_errors() or {},
163 errors=errors.unpack_errors() or {},
163 prefix_error=False,
164 prefix_error=False,
164 encoding="UTF-8",
165 encoding="UTF-8",
165 force_defaults=False
166 force_defaults=False
166 )
167 )
167 return Response(html)
168 return Response(html)
168
169
169 try:
170 try:
170 if c.visual.allow_repo_location_change:
171 if c.visual.allow_repo_location_change:
171 model.update_global_path_setting(form_result['paths_root_path'])
172 model.update_global_path_setting(form_result['paths_root_path'])
172
173
173 model.update_global_ssl_setting(form_result['web_push_ssl'])
174 model.update_global_ssl_setting(form_result['web_push_ssl'])
174 model.update_global_hook_settings(form_result)
175 model.update_global_hook_settings(form_result)
175
176
176 model.create_or_update_global_svn_settings(form_result)
177 model.create_or_update_global_svn_settings(form_result)
177 model.create_or_update_global_hg_settings(form_result)
178 model.create_or_update_global_hg_settings(form_result)
178 model.create_or_update_global_git_settings(form_result)
179 model.create_or_update_global_git_settings(form_result)
179 model.create_or_update_global_pr_settings(form_result)
180 model.create_or_update_global_pr_settings(form_result)
180 except Exception:
181 except Exception:
181 log.exception("Exception while updating settings")
182 log.exception("Exception while updating settings")
182 h.flash(_('Error occurred during updating '
183 h.flash(_('Error occurred during updating '
183 'application settings'), category='error')
184 'application settings'), category='error')
184 else:
185 else:
185 Session().commit()
186 Session().commit()
186 h.flash(_('Updated VCS settings'), category='success')
187 h.flash(_('Updated VCS settings'), category='success')
187 raise HTTPFound(h.route_path('admin_settings_vcs'))
188 raise HTTPFound(h.route_path('admin_settings_vcs'))
188
189
189 data = render('rhodecode:templates/admin/settings/settings.mako',
190 data = render('rhodecode:templates/admin/settings/settings.mako',
190 self._get_template_context(c), self.request)
191 self._get_template_context(c), self.request)
191 html = formencode.htmlfill.render(
192 html = formencode.htmlfill.render(
192 data,
193 data,
193 defaults=self._form_defaults(),
194 defaults=self._form_defaults(),
194 encoding="UTF-8",
195 encoding="UTF-8",
195 force_defaults=False
196 force_defaults=False
196 )
197 )
197 return Response(html)
198 return Response(html)
198
199
199 @LoginRequired()
200 @LoginRequired()
200 @HasPermissionAllDecorator('hg.admin')
201 @HasPermissionAllDecorator('hg.admin')
201 @CSRFRequired()
202 @CSRFRequired()
202 def settings_vcs_delete_svn_pattern(self):
203 def settings_vcs_delete_svn_pattern(self):
203 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
204 model = VcsSettingsModel()
205 model = VcsSettingsModel()
205 try:
206 try:
206 model.delete_global_svn_pattern(delete_pattern_id)
207 model.delete_global_svn_pattern(delete_pattern_id)
207 except SettingNotFound:
208 except SettingNotFound:
208 log.exception(
209 log.exception(
209 'Failed to delete svn_pattern with id %s', delete_pattern_id)
210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
210 raise HTTPNotFound()
211 raise HTTPNotFound()
211
212
212 Session().commit()
213 Session().commit()
213 return True
214 return True
214
215
215 @LoginRequired()
216 @LoginRequired()
216 @HasPermissionAllDecorator('hg.admin')
217 @HasPermissionAllDecorator('hg.admin')
217 def settings_mapping(self):
218 def settings_mapping(self):
218 c = self.load_default_context()
219 c = self.load_default_context()
219 c.active = 'mapping'
220 c.active = 'mapping'
220
221
221 data = render('rhodecode:templates/admin/settings/settings.mako',
222 data = render('rhodecode:templates/admin/settings/settings.mako',
222 self._get_template_context(c), self.request)
223 self._get_template_context(c), self.request)
223 html = formencode.htmlfill.render(
224 html = formencode.htmlfill.render(
224 data,
225 data,
225 defaults=self._form_defaults(),
226 defaults=self._form_defaults(),
226 encoding="UTF-8",
227 encoding="UTF-8",
227 force_defaults=False
228 force_defaults=False
228 )
229 )
229 return Response(html)
230 return Response(html)
230
231
231 @LoginRequired()
232 @LoginRequired()
232 @HasPermissionAllDecorator('hg.admin')
233 @HasPermissionAllDecorator('hg.admin')
233 @CSRFRequired()
234 @CSRFRequired()
234 def settings_mapping_update(self):
235 def settings_mapping_update(self):
235 _ = self.request.translate
236 _ = self.request.translate
236 c = self.load_default_context()
237 c = self.load_default_context()
237 c.active = 'mapping'
238 c.active = 'mapping'
238 rm_obsolete = self.request.POST.get('destroy', False)
239 rm_obsolete = self.request.POST.get('destroy', False)
239 invalidate_cache = self.request.POST.get('invalidate', False)
240 invalidate_cache = self.request.POST.get('invalidate', False)
240 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
241
242
242 if invalidate_cache:
243 if invalidate_cache:
243 log.debug('invalidating all repositories cache')
244 log.debug('invalidating all repositories cache')
244 for repo in Repository.get_all():
245 for repo in Repository.get_all():
245 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
246
247
247 filesystem_repos = ScmModel().repo_scan()
248 filesystem_repos = ScmModel().repo_scan()
248 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
249 PermissionModel().trigger_permission_flush()
250 PermissionModel().trigger_permission_flush()
250
251
251 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
252 def _repr(l):
253 return ', '.join(map(safe_str, l)) or '-'
252 h.flash(_('Repositories successfully '
254 h.flash(_('Repositories successfully '
253 'rescanned added: %s ; removed: %s') %
255 'rescanned added: %s ; removed: %s') %
254 (_repr(added), _repr(removed)),
256 (_repr(added), _repr(removed)),
255 category='success')
257 category='success')
256 raise HTTPFound(h.route_path('admin_settings_mapping'))
258 raise HTTPFound(h.route_path('admin_settings_mapping'))
257
259
258 @LoginRequired()
260 @LoginRequired()
259 @HasPermissionAllDecorator('hg.admin')
261 @HasPermissionAllDecorator('hg.admin')
260 def settings_global(self):
262 def settings_global(self):
261 c = self.load_default_context()
263 c = self.load_default_context()
262 c.active = 'global'
264 c.active = 'global'
263 c.personal_repo_group_default_pattern = RepoGroupModel()\
265 c.personal_repo_group_default_pattern = RepoGroupModel()\
264 .get_personal_group_name_pattern()
266 .get_personal_group_name_pattern()
265
267
266 data = render('rhodecode:templates/admin/settings/settings.mako',
268 data = render('rhodecode:templates/admin/settings/settings.mako',
267 self._get_template_context(c), self.request)
269 self._get_template_context(c), self.request)
268 html = formencode.htmlfill.render(
270 html = formencode.htmlfill.render(
269 data,
271 data,
270 defaults=self._form_defaults(),
272 defaults=self._form_defaults(),
271 encoding="UTF-8",
273 encoding="UTF-8",
272 force_defaults=False
274 force_defaults=False
273 )
275 )
274 return Response(html)
276 return Response(html)
275
277
276 @LoginRequired()
278 @LoginRequired()
277 @HasPermissionAllDecorator('hg.admin')
279 @HasPermissionAllDecorator('hg.admin')
278 @CSRFRequired()
280 @CSRFRequired()
279 def settings_global_update(self):
281 def settings_global_update(self):
280 _ = self.request.translate
282 _ = self.request.translate
281 c = self.load_default_context()
283 c = self.load_default_context()
282 c.active = 'global'
284 c.active = 'global'
283 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 .get_personal_group_name_pattern()
286 .get_personal_group_name_pattern()
285 application_form = ApplicationSettingsForm(self.request.translate)()
287 application_form = ApplicationSettingsForm(self.request.translate)()
286 try:
288 try:
287 form_result = application_form.to_python(dict(self.request.POST))
289 form_result = application_form.to_python(dict(self.request.POST))
288 except formencode.Invalid as errors:
290 except formencode.Invalid as errors:
289 h.flash(
291 h.flash(
290 _("Some form inputs contain invalid data."),
292 _("Some form inputs contain invalid data."),
291 category='error')
293 category='error')
292 data = render('rhodecode:templates/admin/settings/settings.mako',
294 data = render('rhodecode:templates/admin/settings/settings.mako',
293 self._get_template_context(c), self.request)
295 self._get_template_context(c), self.request)
294 html = formencode.htmlfill.render(
296 html = formencode.htmlfill.render(
295 data,
297 data,
296 defaults=errors.value,
298 defaults=errors.value,
297 errors=errors.unpack_errors() or {},
299 errors=errors.unpack_errors() or {},
298 prefix_error=False,
300 prefix_error=False,
299 encoding="UTF-8",
301 encoding="UTF-8",
300 force_defaults=False
302 force_defaults=False
301 )
303 )
302 return Response(html)
304 return Response(html)
303
305
304 settings = [
306 settings = [
305 ('title', 'rhodecode_title', 'unicode'),
307 ('title', 'rhodecode_title', 'unicode'),
306 ('realm', 'rhodecode_realm', 'unicode'),
308 ('realm', 'rhodecode_realm', 'unicode'),
307 ('pre_code', 'rhodecode_pre_code', 'unicode'),
309 ('pre_code', 'rhodecode_pre_code', 'unicode'),
308 ('post_code', 'rhodecode_post_code', 'unicode'),
310 ('post_code', 'rhodecode_post_code', 'unicode'),
309 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
311 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
310 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
312 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
311 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
313 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
312 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
314 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
313 ]
315 ]
316
314 try:
317 try:
315 for setting, form_key, type_ in settings:
318 for setting, form_key, type_ in settings:
316 sett = SettingsModel().create_or_update_setting(
319 sett = SettingsModel().create_or_update_setting(
317 setting, form_result[form_key], type_)
320 setting, form_result[form_key], type_)
318 Session().add(sett)
321 Session().add(sett)
319
322
320 Session().commit()
323 Session().commit()
321 SettingsModel().invalidate_settings_cache()
324 SettingsModel().invalidate_settings_cache()
322 h.flash(_('Updated application settings'), category='success')
325 h.flash(_('Updated application settings'), category='success')
323 except Exception:
326 except Exception:
324 log.exception("Exception while updating application settings")
327 log.exception("Exception while updating application settings")
325 h.flash(
328 h.flash(
326 _('Error occurred during updating application settings'),
329 _('Error occurred during updating application settings'),
327 category='error')
330 category='error')
328
331
329 raise HTTPFound(h.route_path('admin_settings_global'))
332 raise HTTPFound(h.route_path('admin_settings_global'))
330
333
331 @LoginRequired()
334 @LoginRequired()
332 @HasPermissionAllDecorator('hg.admin')
335 @HasPermissionAllDecorator('hg.admin')
333 def settings_visual(self):
336 def settings_visual(self):
334 c = self.load_default_context()
337 c = self.load_default_context()
335 c.active = 'visual'
338 c.active = 'visual'
336
339
337 data = render('rhodecode:templates/admin/settings/settings.mako',
340 data = render('rhodecode:templates/admin/settings/settings.mako',
338 self._get_template_context(c), self.request)
341 self._get_template_context(c), self.request)
339 html = formencode.htmlfill.render(
342 html = formencode.htmlfill.render(
340 data,
343 data,
341 defaults=self._form_defaults(),
344 defaults=self._form_defaults(),
342 encoding="UTF-8",
345 encoding="UTF-8",
343 force_defaults=False
346 force_defaults=False
344 )
347 )
345 return Response(html)
348 return Response(html)
346
349
347 @LoginRequired()
350 @LoginRequired()
348 @HasPermissionAllDecorator('hg.admin')
351 @HasPermissionAllDecorator('hg.admin')
349 @CSRFRequired()
352 @CSRFRequired()
350 def settings_visual_update(self):
353 def settings_visual_update(self):
351 _ = self.request.translate
354 _ = self.request.translate
352 c = self.load_default_context()
355 c = self.load_default_context()
353 c.active = 'visual'
356 c.active = 'visual'
354 application_form = ApplicationVisualisationForm(self.request.translate)()
357 application_form = ApplicationVisualisationForm(self.request.translate)()
355 try:
358 try:
356 form_result = application_form.to_python(dict(self.request.POST))
359 form_result = application_form.to_python(dict(self.request.POST))
357 except formencode.Invalid as errors:
360 except formencode.Invalid as errors:
358 h.flash(
361 h.flash(
359 _("Some form inputs contain invalid data."),
362 _("Some form inputs contain invalid data."),
360 category='error')
363 category='error')
361 data = render('rhodecode:templates/admin/settings/settings.mako',
364 data = render('rhodecode:templates/admin/settings/settings.mako',
362 self._get_template_context(c), self.request)
365 self._get_template_context(c), self.request)
363 html = formencode.htmlfill.render(
366 html = formencode.htmlfill.render(
364 data,
367 data,
365 defaults=errors.value,
368 defaults=errors.value,
366 errors=errors.unpack_errors() or {},
369 errors=errors.unpack_errors() or {},
367 prefix_error=False,
370 prefix_error=False,
368 encoding="UTF-8",
371 encoding="UTF-8",
369 force_defaults=False
372 force_defaults=False
370 )
373 )
371 return Response(html)
374 return Response(html)
372
375
373 try:
376 try:
374 settings = [
377 settings = [
375 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
378 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
376 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
379 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
377 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
380 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
378 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
381 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
379 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
382 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
380 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
383 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
381 ('show_version', 'rhodecode_show_version', 'bool'),
384 ('show_version', 'rhodecode_show_version', 'bool'),
382 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
385 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
383 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
386 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
384 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
387 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
385 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
388 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
386 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
389 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
387 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
390 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
388 ('support_url', 'rhodecode_support_url', 'unicode'),
391 ('support_url', 'rhodecode_support_url', 'unicode'),
389 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
392 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
390 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
393 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
391 ]
394 ]
392 for setting, form_key, type_ in settings:
395 for setting, form_key, type_ in settings:
393 sett = SettingsModel().create_or_update_setting(
396 sett = SettingsModel().create_or_update_setting(
394 setting, form_result[form_key], type_)
397 setting, form_result[form_key], type_)
395 Session().add(sett)
398 Session().add(sett)
396
399
397 Session().commit()
400 Session().commit()
398 SettingsModel().invalidate_settings_cache()
401 SettingsModel().invalidate_settings_cache()
399 h.flash(_('Updated visualisation settings'), category='success')
402 h.flash(_('Updated visualisation settings'), category='success')
400 except Exception:
403 except Exception:
401 log.exception("Exception updating visualization settings")
404 log.exception("Exception updating visualization settings")
402 h.flash(_('Error occurred during updating '
405 h.flash(_('Error occurred during updating '
403 'visualisation settings'),
406 'visualisation settings'),
404 category='error')
407 category='error')
405
408
406 raise HTTPFound(h.route_path('admin_settings_visual'))
409 raise HTTPFound(h.route_path('admin_settings_visual'))
407
410
408 @LoginRequired()
411 @LoginRequired()
409 @HasPermissionAllDecorator('hg.admin')
412 @HasPermissionAllDecorator('hg.admin')
410 def settings_issuetracker(self):
413 def settings_issuetracker(self):
411 c = self.load_default_context()
414 c = self.load_default_context()
412 c.active = 'issuetracker'
415 c.active = 'issuetracker'
413 defaults = c.rc_config
416 defaults = c.rc_config
414
417
415 entry_key = 'rhodecode_issuetracker_pat_'
418 entry_key = 'rhodecode_issuetracker_pat_'
416
419
417 c.issuetracker_entries = {}
420 c.issuetracker_entries = {}
418 for k, v in defaults.items():
421 for k, v in defaults.items():
419 if k.startswith(entry_key):
422 if k.startswith(entry_key):
420 uid = k[len(entry_key):]
423 uid = k[len(entry_key):]
421 c.issuetracker_entries[uid] = None
424 c.issuetracker_entries[uid] = None
422
425
423 for uid in c.issuetracker_entries:
426 for uid in c.issuetracker_entries:
424 c.issuetracker_entries[uid] = AttributeDict({
427 c.issuetracker_entries[uid] = AttributeDict({
425 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
428 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
426 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
429 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
427 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
430 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
428 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
431 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
429 })
432 })
430
433
431 return self._get_template_context(c)
434 return self._get_template_context(c)
432
435
433 @LoginRequired()
436 @LoginRequired()
434 @HasPermissionAllDecorator('hg.admin')
437 @HasPermissionAllDecorator('hg.admin')
435 @CSRFRequired()
438 @CSRFRequired()
436 def settings_issuetracker_test(self):
439 def settings_issuetracker_test(self):
437 error_container = []
440 error_container = []
438
441
439 urlified_commit = h.urlify_commit_message(
442 urlified_commit = h.urlify_commit_message(
440 self.request.POST.get('test_text', ''),
443 self.request.POST.get('test_text', ''),
441 'repo_group/test_repo1', error_container=error_container)
444 'repo_group/test_repo1', error_container=error_container)
442 if error_container:
445 if error_container:
443 def converter(inp):
446 def converter(inp):
444 return h.html_escape(inp)
447 return h.html_escape(inp)
445
448
446 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
449 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
447
450
448 return urlified_commit
451 return urlified_commit
449
452
450 @LoginRequired()
453 @LoginRequired()
451 @HasPermissionAllDecorator('hg.admin')
454 @HasPermissionAllDecorator('hg.admin')
452 @CSRFRequired()
455 @CSRFRequired()
453 def settings_issuetracker_update(self):
456 def settings_issuetracker_update(self):
454 _ = self.request.translate
457 _ = self.request.translate
455 self.load_default_context()
458 self.load_default_context()
456 settings_model = IssueTrackerSettingsModel()
459 settings_model = IssueTrackerSettingsModel()
457
460
458 try:
461 try:
459 form = IssueTrackerPatternsForm(self.request.translate)()
462 form = IssueTrackerPatternsForm(self.request.translate)()
460 data = form.to_python(self.request.POST)
463 data = form.to_python(self.request.POST)
461 except formencode.Invalid as errors:
464 except formencode.Invalid as errors:
462 log.exception('Failed to add new pattern')
465 log.exception('Failed to add new pattern')
463 error = errors
466 error = errors
464 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
467 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
465 category='error')
468 category='error')
466 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
469 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
467
470
468 if data:
471 if data:
469 for uid in data.get('delete_patterns', []):
472 for uid in data.get('delete_patterns', []):
470 settings_model.delete_entries(uid)
473 settings_model.delete_entries(uid)
471
474
472 for pattern in data.get('patterns', []):
475 for pattern in data.get('patterns', []):
473 for setting, value, type_ in pattern:
476 for setting, value, type_ in pattern:
474 sett = settings_model.create_or_update_setting(
477 sett = settings_model.create_or_update_setting(
475 setting, value, type_)
478 setting, value, type_)
476 Session().add(sett)
479 Session().add(sett)
477
480
478 Session().commit()
481 Session().commit()
479
482
480 SettingsModel().invalidate_settings_cache()
483 SettingsModel().invalidate_settings_cache()
481 h.flash(_('Updated issue tracker entries'), category='success')
484 h.flash(_('Updated issue tracker entries'), category='success')
482 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
485 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
483
486
484 @LoginRequired()
487 @LoginRequired()
485 @HasPermissionAllDecorator('hg.admin')
488 @HasPermissionAllDecorator('hg.admin')
486 @CSRFRequired()
489 @CSRFRequired()
487 def settings_issuetracker_delete(self):
490 def settings_issuetracker_delete(self):
488 _ = self.request.translate
491 _ = self.request.translate
489 self.load_default_context()
492 self.load_default_context()
490 uid = self.request.POST.get('uid')
493 uid = self.request.POST.get('uid')
491 try:
494 try:
492 IssueTrackerSettingsModel().delete_entries(uid)
495 IssueTrackerSettingsModel().delete_entries(uid)
493 except Exception:
496 except Exception:
494 log.exception('Failed to delete issue tracker setting %s', uid)
497 log.exception('Failed to delete issue tracker setting %s', uid)
495 raise HTTPNotFound()
498 raise HTTPNotFound()
496
499
497 SettingsModel().invalidate_settings_cache()
500 SettingsModel().invalidate_settings_cache()
498 h.flash(_('Removed issue tracker entry.'), category='success')
501 h.flash(_('Removed issue tracker entry.'), category='success')
499
502
500 return {'deleted': uid}
503 return {'deleted': uid}
501
504
502 @LoginRequired()
505 @LoginRequired()
503 @HasPermissionAllDecorator('hg.admin')
506 @HasPermissionAllDecorator('hg.admin')
504 def settings_email(self):
507 def settings_email(self):
505 c = self.load_default_context()
508 c = self.load_default_context()
506 c.active = 'email'
509 c.active = 'email'
507 c.rhodecode_ini = rhodecode.CONFIG
510 c.rhodecode_ini = rhodecode.CONFIG
508
511
509 data = render('rhodecode:templates/admin/settings/settings.mako',
512 data = render('rhodecode:templates/admin/settings/settings.mako',
510 self._get_template_context(c), self.request)
513 self._get_template_context(c), self.request)
511 html = formencode.htmlfill.render(
514 html = formencode.htmlfill.render(
512 data,
515 data,
513 defaults=self._form_defaults(),
516 defaults=self._form_defaults(),
514 encoding="UTF-8",
517 encoding="UTF-8",
515 force_defaults=False
518 force_defaults=False
516 )
519 )
517 return Response(html)
520 return Response(html)
518
521
519 @LoginRequired()
522 @LoginRequired()
520 @HasPermissionAllDecorator('hg.admin')
523 @HasPermissionAllDecorator('hg.admin')
521 @CSRFRequired()
524 @CSRFRequired()
522 def settings_email_update(self):
525 def settings_email_update(self):
523 _ = self.request.translate
526 _ = self.request.translate
524 c = self.load_default_context()
527 c = self.load_default_context()
525 c.active = 'email'
528 c.active = 'email'
526
529
527 test_email = self.request.POST.get('test_email')
530 test_email = self.request.POST.get('test_email')
528
531
529 if not test_email:
532 if not test_email:
530 h.flash(_('Please enter email address'), category='error')
533 h.flash(_('Please enter email address'), category='error')
531 raise HTTPFound(h.route_path('admin_settings_email'))
534 raise HTTPFound(h.route_path('admin_settings_email'))
532
535
533 email_kwargs = {
536 email_kwargs = {
534 'date': datetime.datetime.now(),
537 'date': datetime.datetime.now(),
535 'user': self._rhodecode_db_user
538 'user': self._rhodecode_db_user
536 }
539 }
537
540
538 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
541 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
539 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
542 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
540
543
541 recipients = [test_email] if test_email else None
544 recipients = [test_email] if test_email else None
542
545
543 run_task(tasks.send_email, recipients, subject,
546 run_task(tasks.send_email, recipients, subject,
544 email_body_plaintext, email_body)
547 email_body_plaintext, email_body)
545
548
546 h.flash(_('Send email task created'), category='success')
549 h.flash(_('Send email task created'), category='success')
547 raise HTTPFound(h.route_path('admin_settings_email'))
550 raise HTTPFound(h.route_path('admin_settings_email'))
548
551
549 @LoginRequired()
552 @LoginRequired()
550 @HasPermissionAllDecorator('hg.admin')
553 @HasPermissionAllDecorator('hg.admin')
551 def settings_hooks(self):
554 def settings_hooks(self):
552 c = self.load_default_context()
555 c = self.load_default_context()
553 c.active = 'hooks'
556 c.active = 'hooks'
554
557
555 model = SettingsModel()
558 model = SettingsModel()
556 c.hooks = model.get_builtin_hooks()
559 c.hooks = model.get_builtin_hooks()
557 c.custom_hooks = model.get_custom_hooks()
560 c.custom_hooks = model.get_custom_hooks()
558
561
559 data = render('rhodecode:templates/admin/settings/settings.mako',
562 data = render('rhodecode:templates/admin/settings/settings.mako',
560 self._get_template_context(c), self.request)
563 self._get_template_context(c), self.request)
561 html = formencode.htmlfill.render(
564 html = formencode.htmlfill.render(
562 data,
565 data,
563 defaults=self._form_defaults(),
566 defaults=self._form_defaults(),
564 encoding="UTF-8",
567 encoding="UTF-8",
565 force_defaults=False
568 force_defaults=False
566 )
569 )
567 return Response(html)
570 return Response(html)
568
571
569 @LoginRequired()
572 @LoginRequired()
570 @HasPermissionAllDecorator('hg.admin')
573 @HasPermissionAllDecorator('hg.admin')
571 @CSRFRequired()
574 @CSRFRequired()
572 def settings_hooks_update(self):
575 def settings_hooks_update(self):
573 _ = self.request.translate
576 _ = self.request.translate
574 c = self.load_default_context()
577 c = self.load_default_context()
575 c.active = 'hooks'
578 c.active = 'hooks'
576 if c.visual.allow_custom_hooks_settings:
579 if c.visual.allow_custom_hooks_settings:
577 ui_key = self.request.POST.get('new_hook_ui_key')
580 ui_key = self.request.POST.get('new_hook_ui_key')
578 ui_value = self.request.POST.get('new_hook_ui_value')
581 ui_value = self.request.POST.get('new_hook_ui_value')
579
582
580 hook_id = self.request.POST.get('hook_id')
583 hook_id = self.request.POST.get('hook_id')
581 new_hook = False
584 new_hook = False
582
585
583 model = SettingsModel()
586 model = SettingsModel()
584 try:
587 try:
585 if ui_value and ui_key:
588 if ui_value and ui_key:
586 model.create_or_update_hook(ui_key, ui_value)
589 model.create_or_update_hook(ui_key, ui_value)
587 h.flash(_('Added new hook'), category='success')
590 h.flash(_('Added new hook'), category='success')
588 new_hook = True
591 new_hook = True
589 elif hook_id:
592 elif hook_id:
590 RhodeCodeUi.delete(hook_id)
593 RhodeCodeUi.delete(hook_id)
591 Session().commit()
594 Session().commit()
592
595
593 # check for edits
596 # check for edits
594 update = False
597 update = False
595 _d = self.request.POST.dict_of_lists()
598 _d = self.request.POST.dict_of_lists()
596 for k, v in zip(_d.get('hook_ui_key', []),
599 for k, v in zip(_d.get('hook_ui_key', []),
597 _d.get('hook_ui_value_new', [])):
600 _d.get('hook_ui_value_new', [])):
598 model.create_or_update_hook(k, v)
601 model.create_or_update_hook(k, v)
599 update = True
602 update = True
600
603
601 if update and not new_hook:
604 if update and not new_hook:
602 h.flash(_('Updated hooks'), category='success')
605 h.flash(_('Updated hooks'), category='success')
603 Session().commit()
606 Session().commit()
604 except Exception:
607 except Exception:
605 log.exception("Exception during hook creation")
608 log.exception("Exception during hook creation")
606 h.flash(_('Error occurred during hook creation'),
609 h.flash(_('Error occurred during hook creation'),
607 category='error')
610 category='error')
608
611
609 raise HTTPFound(h.route_path('admin_settings_hooks'))
612 raise HTTPFound(h.route_path('admin_settings_hooks'))
610
613
611 @LoginRequired()
614 @LoginRequired()
612 @HasPermissionAllDecorator('hg.admin')
615 @HasPermissionAllDecorator('hg.admin')
613 def settings_search(self):
616 def settings_search(self):
614 c = self.load_default_context()
617 c = self.load_default_context()
615 c.active = 'search'
618 c.active = 'search'
616
619
617 c.searcher = searcher_from_config(self.request.registry.settings)
620 c.searcher = searcher_from_config(self.request.registry.settings)
618 c.statistics = c.searcher.statistics(self.request.translate)
621 c.statistics = c.searcher.statistics(self.request.translate)
619
622
620 return self._get_template_context(c)
623 return self._get_template_context(c)
621
624
622 @LoginRequired()
625 @LoginRequired()
623 @HasPermissionAllDecorator('hg.admin')
626 @HasPermissionAllDecorator('hg.admin')
624 def settings_automation(self):
627 def settings_automation(self):
625 c = self.load_default_context()
628 c = self.load_default_context()
626 c.active = 'automation'
629 c.active = 'automation'
627
630
628 return self._get_template_context(c)
631 return self._get_template_context(c)
629
632
630 @LoginRequired()
633 @LoginRequired()
631 @HasPermissionAllDecorator('hg.admin')
634 @HasPermissionAllDecorator('hg.admin')
632 def settings_labs(self):
635 def settings_labs(self):
633 c = self.load_default_context()
636 c = self.load_default_context()
634 if not c.labs_active:
637 if not c.labs_active:
635 raise HTTPFound(h.route_path('admin_settings'))
638 raise HTTPFound(h.route_path('admin_settings'))
636
639
637 c.active = 'labs'
640 c.active = 'labs'
638 c.lab_settings = _LAB_SETTINGS
641 c.lab_settings = _LAB_SETTINGS
639
642
640 data = render('rhodecode:templates/admin/settings/settings.mako',
643 data = render('rhodecode:templates/admin/settings/settings.mako',
641 self._get_template_context(c), self.request)
644 self._get_template_context(c), self.request)
642 html = formencode.htmlfill.render(
645 html = formencode.htmlfill.render(
643 data,
646 data,
644 defaults=self._form_defaults(),
647 defaults=self._form_defaults(),
645 encoding="UTF-8",
648 encoding="UTF-8",
646 force_defaults=False
649 force_defaults=False
647 )
650 )
648 return Response(html)
651 return Response(html)
649
652
650 @LoginRequired()
653 @LoginRequired()
651 @HasPermissionAllDecorator('hg.admin')
654 @HasPermissionAllDecorator('hg.admin')
652 @CSRFRequired()
655 @CSRFRequired()
653 def settings_labs_update(self):
656 def settings_labs_update(self):
654 _ = self.request.translate
657 _ = self.request.translate
655 c = self.load_default_context()
658 c = self.load_default_context()
656 c.active = 'labs'
659 c.active = 'labs'
657
660
658 application_form = LabsSettingsForm(self.request.translate)()
661 application_form = LabsSettingsForm(self.request.translate)()
659 try:
662 try:
660 form_result = application_form.to_python(dict(self.request.POST))
663 form_result = application_form.to_python(dict(self.request.POST))
661 except formencode.Invalid as errors:
664 except formencode.Invalid as errors:
662 h.flash(
665 h.flash(
663 _("Some form inputs contain invalid data."),
666 _("Some form inputs contain invalid data."),
664 category='error')
667 category='error')
665 data = render('rhodecode:templates/admin/settings/settings.mako',
668 data = render('rhodecode:templates/admin/settings/settings.mako',
666 self._get_template_context(c), self.request)
669 self._get_template_context(c), self.request)
667 html = formencode.htmlfill.render(
670 html = formencode.htmlfill.render(
668 data,
671 data,
669 defaults=errors.value,
672 defaults=errors.value,
670 errors=errors.unpack_errors() or {},
673 errors=errors.unpack_errors() or {},
671 prefix_error=False,
674 prefix_error=False,
672 encoding="UTF-8",
675 encoding="UTF-8",
673 force_defaults=False
676 force_defaults=False
674 )
677 )
675 return Response(html)
678 return Response(html)
676
679
677 try:
680 try:
678 session = Session()
681 session = Session()
679 for setting in _LAB_SETTINGS:
682 for setting in _LAB_SETTINGS:
680 setting_name = setting.key[len('rhodecode_'):]
683 setting_name = setting.key[len('rhodecode_'):]
681 sett = SettingsModel().create_or_update_setting(
684 sett = SettingsModel().create_or_update_setting(
682 setting_name, form_result[setting.key], setting.type)
685 setting_name, form_result[setting.key], setting.type)
683 session.add(sett)
686 session.add(sett)
684
687
685 except Exception:
688 except Exception:
686 log.exception('Exception while updating lab settings')
689 log.exception('Exception while updating lab settings')
687 h.flash(_('Error occurred during updating labs settings'),
690 h.flash(_('Error occurred during updating labs settings'),
688 category='error')
691 category='error')
689 else:
692 else:
690 Session().commit()
693 Session().commit()
691 SettingsModel().invalidate_settings_cache()
694 SettingsModel().invalidate_settings_cache()
692 h.flash(_('Updated Labs settings'), category='success')
695 h.flash(_('Updated Labs settings'), category='success')
693 raise HTTPFound(h.route_path('admin_settings_labs'))
696 raise HTTPFound(h.route_path('admin_settings_labs'))
694
697
695 data = render('rhodecode:templates/admin/settings/settings.mako',
698 data = render('rhodecode:templates/admin/settings/settings.mako',
696 self._get_template_context(c), self.request)
699 self._get_template_context(c), self.request)
697 html = formencode.htmlfill.render(
700 html = formencode.htmlfill.render(
698 data,
701 data,
699 defaults=self._form_defaults(),
702 defaults=self._form_defaults(),
700 encoding="UTF-8",
703 encoding="UTF-8",
701 force_defaults=False
704 force_defaults=False
702 )
705 )
703 return Response(html)
706 return Response(html)
704
707
705
708
706 # :param key: name of the setting including the 'rhodecode_' prefix
709 # :param key: name of the setting including the 'rhodecode_' prefix
707 # :param type: the RhodeCodeSetting type to use.
710 # :param type: the RhodeCodeSetting type to use.
708 # :param group: the i18ned group in which we should dispaly this setting
711 # :param group: the i18ned group in which we should dispaly this setting
709 # :param label: the i18ned label we should display for this setting
712 # :param label: the i18ned label we should display for this setting
710 # :param help: the i18ned help we should dispaly for this setting
713 # :param help: the i18ned help we should dispaly for this setting
711 LabSetting = collections.namedtuple(
714 LabSetting = collections.namedtuple(
712 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
715 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
713
716
714
717
715 # This list has to be kept in sync with the form
718 # This list has to be kept in sync with the form
716 # rhodecode.model.forms.LabsSettingsForm.
719 # rhodecode.model.forms.LabsSettingsForm.
717 _LAB_SETTINGS = [
720 _LAB_SETTINGS = [
718
721
719 ]
722 ]
@@ -1,234 +1,237 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22 import urllib.request, urllib.error, urllib.parse
22 import urllib.request
23 import urllib.error
24 import urllib.parse
23 import os
25 import os
24
26
25 import rhodecode
27 import rhodecode
26 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base.navigation import navigation_list
29 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
31 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib import system_info
33 from rhodecode.lib import system_info
32 from rhodecode.model.update import UpdateModel
34 from rhodecode.model.update import UpdateModel
33
35
34 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
35
37
36
38
37 class AdminSystemInfoSettingsView(BaseAppView):
39 class AdminSystemInfoSettingsView(BaseAppView):
38 def load_default_context(self):
40 def load_default_context(self):
39 c = self._get_local_tmpl_context()
41 c = self._get_local_tmpl_context()
40 return c
42 return c
41
43
42 def get_env_data(self):
44 def get_env_data(self):
43 black_list = [
45 black_list = [
44 'NIX_LDFLAGS',
46 'NIX_LDFLAGS',
45 'NIX_CFLAGS_COMPILE',
47 'NIX_CFLAGS_COMPILE',
46 'propagatedBuildInputs',
48 'propagatedBuildInputs',
47 'propagatedNativeBuildInputs',
49 'propagatedNativeBuildInputs',
48 'postInstall',
50 'postInstall',
49 'buildInputs',
51 'buildInputs',
50 'buildPhase',
52 'buildPhase',
51 'preShellHook',
53 'preShellHook',
52 'preShellHook',
54 'preShellHook',
53 'preCheck',
55 'preCheck',
54 'preBuild',
56 'preBuild',
55 'postShellHook',
57 'postShellHook',
56 'postFixup',
58 'postFixup',
57 'postCheck',
59 'postCheck',
58 'nativeBuildInputs',
60 'nativeBuildInputs',
59 'installPhase',
61 'installPhase',
60 'installCheckPhase',
62 'installCheckPhase',
61 'checkPhase',
63 'checkPhase',
62 'configurePhase',
64 'configurePhase',
63 'shellHook'
65 'shellHook'
64 ]
66 ]
65 secret_list = [
67 secret_list = [
66 'RHODECODE_USER_PASS'
68 'RHODECODE_USER_PASS'
67 ]
69 ]
68
70
69 for k, v in sorted(os.environ.items()):
71 for k, v in sorted(os.environ.items()):
70 if k in black_list:
72 if k in black_list:
71 continue
73 continue
72 if k in secret_list:
74 if k in secret_list:
73 v = '*****'
75 v = '*****'
74 yield k, v
76 yield k, v
75
77
76 @LoginRequired()
78 @LoginRequired()
77 @HasPermissionAllDecorator('hg.admin')
79 @HasPermissionAllDecorator('hg.admin')
78 def settings_system_info(self):
80 def settings_system_info(self):
79 _ = self.request.translate
81 _ = self.request.translate
80 c = self.load_default_context()
82 c = self.load_default_context()
81
83
82 c.active = 'system'
84 c.active = 'system'
83 c.navlist = navigation_list(self.request)
85 c.navlist = navigation_list(self.request)
84
86
85 # TODO(marcink), figure out how to allow only selected users to do this
87 # TODO(marcink), figure out how to allow only selected users to do this
86 c.allowed_to_snapshot = self._rhodecode_user.admin
88 c.allowed_to_snapshot = self._rhodecode_user.admin
87
89
88 snapshot = str2bool(self.request.params.get('snapshot'))
90 snapshot = str2bool(self.request.params.get('snapshot'))
89
91
90 c.rhodecode_update_url = UpdateModel().get_update_url()
92 c.rhodecode_update_url = UpdateModel().get_update_url()
91 c.env_data = self.get_env_data()
93 c.env_data = self.get_env_data()
92 server_info = system_info.get_system_info(self.request.environ)
94 server_info = system_info.get_system_info(self.request.environ)
93
95
94 for key, val in server_info.items():
96 for key, val in server_info.items():
95 setattr(c, key, val)
97 setattr(c, key, val)
96
98
97 def val(name, subkey='human_value'):
99 def val(name, subkey='human_value'):
98 return server_info[name][subkey]
100 return server_info[name][subkey]
99
101
100 def state(name):
102 def state(name):
101 return server_info[name]['state']
103 return server_info[name]['state']
102
104
103 def val2(name):
105 def val2(name):
104 val = server_info[name]['human_value']
106 val = server_info[name]['human_value']
105 state = server_info[name]['state']
107 state = server_info[name]['state']
106 return val, state
108 return val, state
107
109
108 update_info_msg = _('Note: please make sure this server can '
110 update_info_msg = _('Note: please make sure this server can '
109 'access `${url}` for the update link to work',
111 'access `${url}` for the update link to work',
110 mapping=dict(url=c.rhodecode_update_url))
112 mapping=dict(url=c.rhodecode_update_url))
111 version = UpdateModel().get_stored_version()
113 version = UpdateModel().get_stored_version()
112 is_outdated = UpdateModel().is_outdated(
114 is_outdated = UpdateModel().is_outdated(
113 rhodecode.__version__, version)
115 rhodecode.__version__, version)
114 update_state = {
116 update_state = {
115 'type': 'warning',
117 'type': 'warning',
116 'message': 'New version available: {}'.format(version)
118 'message': 'New version available: {}'.format(version)
117 } \
119 } \
118 if is_outdated else {}
120 if is_outdated else {}
119 c.data_items = [
121 c.data_items = [
120 # update info
122 # update info
121 (_('Update info'), h.literal(
123 (_('Update info'), h.literal(
122 '<span class="link" id="check_for_update" >%s.</span>' % (
124 '<span class="link" id="check_for_update" >%s.</span>' % (
123 _('Check for updates')) +
125 _('Check for updates')) +
124 '<br/> <span >%s.</span>' % (update_info_msg)
126 '<br/> <span >%s.</span>' % (update_info_msg)
125 ), ''),
127 ), ''),
126
128
127 # RhodeCode specific
129 # RhodeCode specific
128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
130 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 (_('Latest version'), version, update_state),
131 (_('Latest version'), version, update_state),
130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
132 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
133 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
134 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
135 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
136 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
137 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
138 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 ('', '', ''), # spacer
139 ('', '', ''), # spacer
138
140
139 # Database
141 # Database
140 (_('Database'), val('database')['url'], state('database')),
142 (_('Database'), val('database')['url'], state('database')),
141 (_('Database version'), val('database')['version'], state('database')),
143 (_('Database version'), val('database')['version'], state('database')),
142 ('', '', ''), # spacer
144 ('', '', ''), # spacer
143
145
144 # Platform/Python
146 # Platform/Python
145 (_('Platform'), val('platform')['name'], state('platform')),
147 (_('Platform'), val('platform')['name'], state('platform')),
146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
148 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 (_('Lang'), val('locale'), state('locale')),
149 (_('Lang'), val('locale'), state('locale')),
148 (_('Python version'), val('python')['version'], state('python')),
150 (_('Python version'), val('python')['version'], state('python')),
149 (_('Python path'), val('python')['executable'], state('python')),
151 (_('Python path'), val('python')['executable'], state('python')),
150 ('', '', ''), # spacer
152 ('', '', ''), # spacer
151
153
152 # Systems stats
154 # Systems stats
153 (_('CPU'), val('cpu')['text'], state('cpu')),
155 (_('CPU'), val('cpu')['text'], state('cpu')),
154 (_('Load'), val('load')['text'], state('load')),
156 (_('Load'), val('load')['text'], state('load')),
155 (_('Memory'), val('memory')['text'], state('memory')),
157 (_('Memory'), val('memory')['text'], state('memory')),
156 (_('Uptime'), val('uptime')['text'], state('uptime')),
158 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 ('', '', ''), # spacer
159 ('', '', ''), # spacer
158
160
159 # ulimit
161 # ulimit
160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
162 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161
163
162 # Repo storage
164 # Repo storage
163 (_('Storage location'), val('storage')['path'], state('storage')),
165 (_('Storage location'), val('storage')['path'], state('storage')),
164 (_('Storage info'), val('storage')['text'], state('storage')),
166 (_('Storage info'), val('storage')['text'], state('storage')),
165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166
168
167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
169 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
170 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169
171
170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
172 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
173 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172
174
173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
175 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
176 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175
177
176 (_('Search info'), val('search')['text'], state('search')),
178 (_('Search info'), val('search')['text'], state('search')),
177 (_('Search location'), val('search')['location'], state('search')),
179 (_('Search location'), val('search')['location'], state('search')),
178 ('', '', ''), # spacer
180 ('', '', ''), # spacer
179
181
180 # VCS specific
182 # VCS specific
181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
183 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
184 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 (_('GIT'), val('git'), state('git')),
185 (_('GIT'), val('git'), state('git')),
184 (_('HG'), val('hg'), state('hg')),
186 (_('HG'), val('hg'), state('hg')),
185 (_('SVN'), val('svn'), state('svn')),
187 (_('SVN'), val('svn'), state('svn')),
186
188
187 ]
189 ]
188
190
189 c.vcsserver_data_items = [
191 c.vcsserver_data_items = [
190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
192 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 ]
193 ]
192
194
193 if snapshot:
195 if snapshot:
194 if c.allowed_to_snapshot:
196 if c.allowed_to_snapshot:
195 c.data_items.pop(0) # remove server info
197 c.data_items.pop(0) # remove server info
196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
198 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 else:
199 else:
198 h.flash('You are not allowed to do this', category='warning')
200 h.flash('You are not allowed to do this', category='warning')
199 return self._get_template_context(c)
201 return self._get_template_context(c)
200
202
201 @LoginRequired()
203 @LoginRequired()
202 @HasPermissionAllDecorator('hg.admin')
204 @HasPermissionAllDecorator('hg.admin')
203 def settings_system_info_check_update(self):
205 def settings_system_info_check_update(self):
204 _ = self.request.translate
206 _ = self.request.translate
205 c = self.load_default_context()
207 c = self.load_default_context()
206
208
207 update_url = UpdateModel().get_update_url()
209 update_url = UpdateModel().get_update_url()
208
210
209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
211 def _err(s):
212 return '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
210 try:
213 try:
211 data = UpdateModel().get_update_data(update_url)
214 data = UpdateModel().get_update_data(update_url)
212 except urllib.error.URLError as e:
215 except urllib.error.URLError as e:
213 log.exception("Exception contacting upgrade server")
216 log.exception("Exception contacting upgrade server")
214 self.request.override_renderer = 'string'
217 self.request.override_renderer = 'string'
215 return _err('Failed to contact upgrade server: %r' % e)
218 return _err('Failed to contact upgrade server: %r' % e)
216 except ValueError as e:
219 except ValueError as e:
217 log.exception("Bad data sent from update server")
220 log.exception("Bad data sent from update server")
218 self.request.override_renderer = 'string'
221 self.request.override_renderer = 'string'
219 return _err('Bad data sent from update server')
222 return _err('Bad data sent from update server')
220
223
221 latest = data['versions'][0]
224 latest = data['versions'][0]
222
225
223 c.update_url = update_url
226 c.update_url = update_url
224 c.latest_data = latest
227 c.latest_data = latest
225 c.latest_ver = latest['version']
228 c.latest_ver = latest['version']
226 c.cur_ver = rhodecode.__version__
229 c.cur_ver = rhodecode.__version__
227 c.should_upgrade = False
230 c.should_upgrade = False
228
231
229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
232 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 if is_oudated:
233 if is_oudated:
231 c.should_upgrade = True
234 c.should_upgrade = True
232 c.important_notices = latest['general']
235 c.important_notices = latest['general']
233 UpdateModel().store_version(latest['version'])
236 UpdateModel().store_version(latest['version'])
234 return self._get_template_context(c)
237 return self._get_template_context(c)
@@ -1,253 +1,252 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27
27
28 from pyramid.response import Response
28 from pyramid.response import Response
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 from rhodecode.lib import helpers as h, audit_logger
35 from rhodecode.lib import helpers as h, audit_logger
36 from rhodecode.lib.utils2 import safe_unicode
36 from rhodecode.lib.str_utils import safe_str
37
37
38 from rhodecode.model.forms import UserGroupForm
38 from rhodecode.model.forms import UserGroupForm
39 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.scm import UserGroupList
41 from rhodecode.model.db import (
40 from rhodecode.model.db import (
42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
41 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
44 from rhodecode.model.user_group import UserGroupModel
43 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.db import true
44 from rhodecode.model.db import true
46
45
47 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
48
47
49
48
50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
49 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51
50
52 def load_default_context(self):
51 def load_default_context(self):
53 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
54 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
55 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
56 return c
55 return c
57
56
58 # permission check in data loading of
57 # permission check in data loading of
59 # `user_groups_list_data` via UserGroupList
58 # `user_groups_list_data` via UserGroupList
60 @LoginRequired()
59 @LoginRequired()
61 @NotAnonymous()
60 @NotAnonymous()
62 def user_groups_list(self):
61 def user_groups_list(self):
63 c = self.load_default_context()
62 c = self.load_default_context()
64 return self._get_template_context(c)
63 return self._get_template_context(c)
65
64
66 # permission check inside
65 # permission check inside
67 @LoginRequired()
66 @LoginRequired()
68 @NotAnonymous()
67 @NotAnonymous()
69 def user_groups_list_data(self):
68 def user_groups_list_data(self):
70 self.load_default_context()
69 self.load_default_context()
71 column_map = {
70 column_map = {
72 'active': 'users_group_active',
71 'active': 'users_group_active',
73 'description': 'user_group_description',
72 'description': 'user_group_description',
74 'members': 'members_total',
73 'members': 'members_total',
75 'owner': 'user_username',
74 'owner': 'user_username',
76 'sync': 'group_data'
75 'sync': 'group_data'
77 }
76 }
78 draw, start, limit = self._extract_chunk(self.request)
77 draw, start, limit = self._extract_chunk(self.request)
79 search_q, order_by, order_dir = self._extract_ordering(
78 search_q, order_by, order_dir = self._extract_ordering(
80 self.request, column_map=column_map)
79 self.request, column_map=column_map)
81
80
82 _render = self.request.get_partial_renderer(
81 _render = self.request.get_partial_renderer(
83 'rhodecode:templates/data_table/_dt_elements.mako')
82 'rhodecode:templates/data_table/_dt_elements.mako')
84
83
85 def user_group_name(user_group_name):
84 def user_group_name(user_group_name):
86 return _render("user_group_name", user_group_name)
85 return _render("user_group_name", user_group_name)
87
86
88 def user_group_actions(user_group_id, user_group_name):
87 def user_group_actions(user_group_id, user_group_name):
89 return _render("user_group_actions", user_group_id, user_group_name)
88 return _render("user_group_actions", user_group_id, user_group_name)
90
89
91 def user_profile(username):
90 def user_profile(username):
92 return _render('user_profile', username)
91 return _render('user_profile', username)
93
92
94 _perms = ['usergroup.admin']
93 _perms = ['usergroup.admin']
95 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
94 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
96
95
97 user_groups_data_total_count = UserGroup.query()\
96 user_groups_data_total_count = UserGroup.query()\
98 .filter(or_(
97 .filter(or_(
99 # generate multiple IN to fix limitation problems
98 # generate multiple IN to fix limitation problems
100 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
99 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
101 ))\
100 ))\
102 .count()
101 .count()
103
102
104 user_groups_data_total_inactive_count = UserGroup.query()\
103 user_groups_data_total_inactive_count = UserGroup.query()\
105 .filter(or_(
104 .filter(or_(
106 # generate multiple IN to fix limitation problems
105 # generate multiple IN to fix limitation problems
107 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
106 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
108 ))\
107 ))\
109 .filter(UserGroup.users_group_active != true()).count()
108 .filter(UserGroup.users_group_active != true()).count()
110
109
111 member_count = count(UserGroupMember.user_id)
110 member_count = count(UserGroupMember.user_id)
112 base_q = Session.query(
111 base_q = Session.query(
113 UserGroup.users_group_name,
112 UserGroup.users_group_name,
114 UserGroup.user_group_description,
113 UserGroup.user_group_description,
115 UserGroup.users_group_active,
114 UserGroup.users_group_active,
116 UserGroup.users_group_id,
115 UserGroup.users_group_id,
117 UserGroup.group_data,
116 UserGroup.group_data,
118 User,
117 User,
119 member_count.label('member_count')
118 member_count.label('member_count')
120 ) \
119 ) \
121 .filter(or_(
120 .filter(or_(
122 # generate multiple IN to fix limitation problems
121 # generate multiple IN to fix limitation problems
123 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
122 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
124 )) \
123 )) \
125 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
124 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
126 .join(User, User.user_id == UserGroup.user_id) \
125 .join(User, User.user_id == UserGroup.user_id) \
127 .group_by(UserGroup, User)
126 .group_by(UserGroup, User)
128
127
129 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
128 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
130
129
131 if search_q:
130 if search_q:
132 like_expression = u'%{}%'.format(safe_unicode(search_q))
131 like_expression = u'%{}%'.format(safe_str(search_q))
133 base_q = base_q.filter(or_(
132 base_q = base_q.filter(or_(
134 UserGroup.users_group_name.ilike(like_expression),
133 UserGroup.users_group_name.ilike(like_expression),
135 ))
134 ))
136 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
135 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
137
136
138 user_groups_data_total_filtered_count = base_q.count()
137 user_groups_data_total_filtered_count = base_q.count()
139 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
138 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
140
139
141 sort_defined = False
140 sort_defined = False
142 if order_by == 'members_total':
141 if order_by == 'members_total':
143 sort_col = member_count
142 sort_col = member_count
144 sort_defined = True
143 sort_defined = True
145 elif order_by == 'user_username':
144 elif order_by == 'user_username':
146 sort_col = User.username
145 sort_col = User.username
147 else:
146 else:
148 sort_col = getattr(UserGroup, order_by, None)
147 sort_col = getattr(UserGroup, order_by, None)
149
148
150 if sort_defined or sort_col:
149 if sort_defined or sort_col:
151 if order_dir == 'asc':
150 if order_dir == 'asc':
152 sort_col = sort_col.asc()
151 sort_col = sort_col.asc()
153 else:
152 else:
154 sort_col = sort_col.desc()
153 sort_col = sort_col.desc()
155
154
156 base_q = base_q.order_by(sort_col)
155 base_q = base_q.order_by(sort_col)
157 base_q = base_q.offset(start).limit(limit)
156 base_q = base_q.offset(start).limit(limit)
158
157
159 # authenticated access to user groups
158 # authenticated access to user groups
160 auth_user_group_list = base_q.all()
159 auth_user_group_list = base_q.all()
161
160
162 user_groups_data = []
161 user_groups_data = []
163 for user_gr in auth_user_group_list:
162 for user_gr in auth_user_group_list:
164 row = {
163 row = {
165 "users_group_name": user_group_name(user_gr.users_group_name),
164 "users_group_name": user_group_name(user_gr.users_group_name),
166 "description": h.escape(user_gr.user_group_description),
165 "description": h.escape(user_gr.user_group_description),
167 "members": user_gr.member_count,
166 "members": user_gr.member_count,
168 # NOTE(marcink): because of advanced query we
167 # NOTE(marcink): because of advanced query we
169 # need to load it like that
168 # need to load it like that
170 "sync": UserGroup._load_sync(
169 "sync": UserGroup._load_sync(
171 UserGroup._load_group_data(user_gr.group_data)),
170 UserGroup._load_group_data(user_gr.group_data)),
172 "active": h.bool2icon(user_gr.users_group_active),
171 "active": h.bool2icon(user_gr.users_group_active),
173 "owner": user_profile(user_gr.User.username),
172 "owner": user_profile(user_gr.User.username),
174 "action": user_group_actions(
173 "action": user_group_actions(
175 user_gr.users_group_id, user_gr.users_group_name)
174 user_gr.users_group_id, user_gr.users_group_name)
176 }
175 }
177 user_groups_data.append(row)
176 user_groups_data.append(row)
178
177
179 data = ({
178 data = ({
180 'draw': draw,
179 'draw': draw,
181 'data': user_groups_data,
180 'data': user_groups_data,
182 'recordsTotal': user_groups_data_total_count,
181 'recordsTotal': user_groups_data_total_count,
183 'recordsTotalInactive': user_groups_data_total_inactive_count,
182 'recordsTotalInactive': user_groups_data_total_inactive_count,
184 'recordsFiltered': user_groups_data_total_filtered_count,
183 'recordsFiltered': user_groups_data_total_filtered_count,
185 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
184 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
186 })
185 })
187
186
188 return data
187 return data
189
188
190 @LoginRequired()
189 @LoginRequired()
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
190 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 def user_groups_new(self):
191 def user_groups_new(self):
193 c = self.load_default_context()
192 c = self.load_default_context()
194 return self._get_template_context(c)
193 return self._get_template_context(c)
195
194
196 @LoginRequired()
195 @LoginRequired()
197 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
196 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
198 @CSRFRequired()
197 @CSRFRequired()
199 def user_groups_create(self):
198 def user_groups_create(self):
200 _ = self.request.translate
199 _ = self.request.translate
201 c = self.load_default_context()
200 c = self.load_default_context()
202 users_group_form = UserGroupForm(self.request.translate)()
201 users_group_form = UserGroupForm(self.request.translate)()
203
202
204 user_group_name = self.request.POST.get('users_group_name')
203 user_group_name = self.request.POST.get('users_group_name')
205 try:
204 try:
206 form_result = users_group_form.to_python(dict(self.request.POST))
205 form_result = users_group_form.to_python(dict(self.request.POST))
207 user_group = UserGroupModel().create(
206 user_group = UserGroupModel().create(
208 name=form_result['users_group_name'],
207 name=form_result['users_group_name'],
209 description=form_result['user_group_description'],
208 description=form_result['user_group_description'],
210 owner=self._rhodecode_user.user_id,
209 owner=self._rhodecode_user.user_id,
211 active=form_result['users_group_active'])
210 active=form_result['users_group_active'])
212 Session().flush()
211 Session().flush()
213 creation_data = user_group.get_api_data()
212 creation_data = user_group.get_api_data()
214 user_group_name = form_result['users_group_name']
213 user_group_name = form_result['users_group_name']
215
214
216 audit_logger.store_web(
215 audit_logger.store_web(
217 'user_group.create', action_data={'data': creation_data},
216 'user_group.create', action_data={'data': creation_data},
218 user=self._rhodecode_user)
217 user=self._rhodecode_user)
219
218
220 user_group_link = h.link_to(
219 user_group_link = h.link_to(
221 h.escape(user_group_name),
220 h.escape(user_group_name),
222 h.route_path(
221 h.route_path(
223 'edit_user_group', user_group_id=user_group.users_group_id))
222 'edit_user_group', user_group_id=user_group.users_group_id))
224 h.flash(h.literal(_('Created user group %(user_group_link)s')
223 h.flash(h.literal(_('Created user group %(user_group_link)s')
225 % {'user_group_link': user_group_link}),
224 % {'user_group_link': user_group_link}),
226 category='success')
225 category='success')
227 Session().commit()
226 Session().commit()
228 user_group_id = user_group.users_group_id
227 user_group_id = user_group.users_group_id
229 except formencode.Invalid as errors:
228 except formencode.Invalid as errors:
230
229
231 data = render(
230 data = render(
232 'rhodecode:templates/admin/user_groups/user_group_add.mako',
231 'rhodecode:templates/admin/user_groups/user_group_add.mako',
233 self._get_template_context(c), self.request)
232 self._get_template_context(c), self.request)
234 html = formencode.htmlfill.render(
233 html = formencode.htmlfill.render(
235 data,
234 data,
236 defaults=errors.value,
235 defaults=errors.value,
237 errors=errors.unpack_errors() or {},
236 errors=errors.unpack_errors() or {},
238 prefix_error=False,
237 prefix_error=False,
239 encoding="UTF-8",
238 encoding="UTF-8",
240 force_defaults=False
239 force_defaults=False
241 )
240 )
242 return Response(html)
241 return Response(html)
243
242
244 except Exception:
243 except Exception:
245 log.exception("Exception creating user group")
244 log.exception("Exception creating user group")
246 h.flash(_('Error occurred during creation of user group %s') \
245 h.flash(_('Error occurred during creation of user group %s') \
247 % user_group_name, category='error')
246 % user_group_name, category='error')
248 raise HTTPFound(h.route_path('user_groups_new'))
247 raise HTTPFound(h.route_path('user_groups_new'))
249
248
250 PermissionModel().trigger_permission_flush()
249 PermissionModel().trigger_permission_flush()
251
250
252 raise HTTPFound(
251 raise HTTPFound(
253 h.route_path('edit_user_group', user_group_id=user_group_id))
252 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1322 +1,1323 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.events import trigger
35 from rhodecode.events import trigger
36 from rhodecode.model.db import true, UserNotice
36 from rhodecode.model.db import true, UserNotice
37
37
38 from rhodecode.lib import audit_logger, rc_cache, auth
38 from rhodecode.lib import audit_logger, rc_cache, auth
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 UserOwnsArtifactsException, DefaultUserException)
42 UserOwnsArtifactsException, DefaultUserException)
43 from rhodecode.lib import ext_json
43 from rhodecode.lib import ext_json
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 from rhodecode.lib import helpers as h
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib.helpers import SqlPage
47 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
48 from rhodecode.lib.utils2 import safe_int, safe_str, AttributeDict
49 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserExtraEmailForm, UserExtraIpForm)
52 UserExtraEmailForm, UserExtraIpForm)
53 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.repo_group import RepoGroupModel
54 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.ssh_key import SshKeyModel
55 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.user import UserModel
56 from rhodecode.model.user import UserModel
57 from rhodecode.model.user_group import UserGroupModel
57 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 UserApiKeys, UserSshKeys, RepoGroup)
60 UserApiKeys, UserSshKeys, RepoGroup)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 class AdminUsersView(BaseAppView, DataGridAppView):
66 class AdminUsersView(BaseAppView, DataGridAppView):
67
67
68 def load_default_context(self):
68 def load_default_context(self):
69 c = self._get_local_tmpl_context()
69 c = self._get_local_tmpl_context()
70 return c
70 return c
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @HasPermissionAllDecorator('hg.admin')
73 @HasPermissionAllDecorator('hg.admin')
74 def users_list(self):
74 def users_list(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 return self._get_template_context(c)
76 return self._get_template_context(c)
77
77
78 @LoginRequired()
78 @LoginRequired()
79 @HasPermissionAllDecorator('hg.admin')
79 @HasPermissionAllDecorator('hg.admin')
80 def users_list_data(self):
80 def users_list_data(self):
81 self.load_default_context()
81 self.load_default_context()
82 column_map = {
82 column_map = {
83 'first_name': 'name',
83 'first_name': 'name',
84 'last_name': 'lastname',
84 'last_name': 'lastname',
85 }
85 }
86 draw, start, limit = self._extract_chunk(self.request)
86 draw, start, limit = self._extract_chunk(self.request)
87 search_q, order_by, order_dir = self._extract_ordering(
87 search_q, order_by, order_dir = self._extract_ordering(
88 self.request, column_map=column_map)
88 self.request, column_map=column_map)
89 _render = self.request.get_partial_renderer(
89 _render = self.request.get_partial_renderer(
90 'rhodecode:templates/data_table/_dt_elements.mako')
90 'rhodecode:templates/data_table/_dt_elements.mako')
91
91
92 def user_actions(user_id, username):
92 def user_actions(user_id, username):
93 return _render("user_actions", user_id, username)
93 return _render("user_actions", user_id, username)
94
94
95 users_data_total_count = User.query()\
95 users_data_total_count = User.query()\
96 .filter(User.username != User.DEFAULT_USER) \
96 .filter(User.username != User.DEFAULT_USER) \
97 .count()
97 .count()
98
98
99 users_data_total_inactive_count = User.query()\
99 users_data_total_inactive_count = User.query()\
100 .filter(User.username != User.DEFAULT_USER) \
100 .filter(User.username != User.DEFAULT_USER) \
101 .filter(User.active != true())\
101 .filter(User.active != true())\
102 .count()
102 .count()
103
103
104 # json generate
104 # json generate
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 base_inactive_q = base_q.filter(User.active != true())
106 base_inactive_q = base_q.filter(User.active != true())
107
107
108 if search_q:
108 if search_q:
109 like_expression = '%{}%'.format(safe_unicode(search_q))
109 like_expression = '%{}%'.format(safe_str(search_q))
110 base_q = base_q.filter(or_(
110 base_q = base_q.filter(or_(
111 User.username.ilike(like_expression),
111 User.username.ilike(like_expression),
112 User._email.ilike(like_expression),
112 User._email.ilike(like_expression),
113 User.name.ilike(like_expression),
113 User.name.ilike(like_expression),
114 User.lastname.ilike(like_expression),
114 User.lastname.ilike(like_expression),
115 ))
115 ))
116 base_inactive_q = base_q.filter(User.active != true())
116 base_inactive_q = base_q.filter(User.active != true())
117
117
118 users_data_total_filtered_count = base_q.count()
118 users_data_total_filtered_count = base_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120
120
121 sort_col = getattr(User, order_by, None)
121 sort_col = getattr(User, order_by, None)
122 if sort_col:
122 if sort_col:
123 if order_dir == 'asc':
123 if order_dir == 'asc':
124 # handle null values properly to order by NULL last
124 # handle null values properly to order by NULL last
125 if order_by in ['last_activity']:
125 if order_by in ['last_activity']:
126 sort_col = coalesce(sort_col, datetime.date.max)
126 sort_col = coalesce(sort_col, datetime.date.max)
127 sort_col = sort_col.asc()
127 sort_col = sort_col.asc()
128 else:
128 else:
129 # handle null values properly to order by NULL last
129 # handle null values properly to order by NULL last
130 if order_by in ['last_activity']:
130 if order_by in ['last_activity']:
131 sort_col = coalesce(sort_col, datetime.date.min)
131 sort_col = coalesce(sort_col, datetime.date.min)
132 sort_col = sort_col.desc()
132 sort_col = sort_col.desc()
133
133
134 base_q = base_q.order_by(sort_col)
134 base_q = base_q.order_by(sort_col)
135 base_q = base_q.offset(start).limit(limit)
135 base_q = base_q.offset(start).limit(limit)
136
136
137 users_list = base_q.all()
137 users_list = base_q.all()
138
138
139 users_data = []
139 users_data = []
140 for user in users_list:
140 for user in users_list:
141 users_data.append({
141 users_data.append({
142 "username": h.gravatar_with_user(self.request, user.username),
142 "username": h.gravatar_with_user(self.request, user.username),
143 "email": user.email,
143 "email": user.email,
144 "first_name": user.first_name,
144 "first_name": user.first_name,
145 "last_name": user.last_name,
145 "last_name": user.last_name,
146 "last_login": h.format_date(user.last_login),
146 "last_login": h.format_date(user.last_login),
147 "last_activity": h.format_date(user.last_activity),
147 "last_activity": h.format_date(user.last_activity),
148 "active": h.bool2icon(user.active),
148 "active": h.bool2icon(user.active),
149 "active_raw": user.active,
149 "active_raw": user.active,
150 "admin": h.bool2icon(user.admin),
150 "admin": h.bool2icon(user.admin),
151 "extern_type": user.extern_type,
151 "extern_type": user.extern_type,
152 "extern_name": user.extern_name,
152 "extern_name": user.extern_name,
153 "action": user_actions(user.user_id, user.username),
153 "action": user_actions(user.user_id, user.username),
154 })
154 })
155 data = ({
155 data = ({
156 'draw': draw,
156 'draw': draw,
157 'data': users_data,
157 'data': users_data,
158 'recordsTotal': users_data_total_count,
158 'recordsTotal': users_data_total_count,
159 'recordsFiltered': users_data_total_filtered_count,
159 'recordsFiltered': users_data_total_filtered_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 })
162 })
163
163
164 return data
164 return data
165
165
166 def _set_personal_repo_group_template_vars(self, c_obj):
166 def _set_personal_repo_group_template_vars(self, c_obj):
167 DummyUser = AttributeDict({
167 DummyUser = AttributeDict({
168 'username': '${username}',
168 'username': '${username}',
169 'user_id': '${user_id}',
169 'user_id': '${user_id}',
170 })
170 })
171 c_obj.default_create_repo_group = RepoGroupModel() \
171 c_obj.default_create_repo_group = RepoGroupModel() \
172 .get_default_create_personal_repo_group()
172 .get_default_create_personal_repo_group()
173 c_obj.personal_repo_group_name = RepoGroupModel() \
173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 .get_personal_group_name(DummyUser)
174 .get_personal_group_name(DummyUser)
175
175
176 @LoginRequired()
176 @LoginRequired()
177 @HasPermissionAllDecorator('hg.admin')
177 @HasPermissionAllDecorator('hg.admin')
178 def users_new(self):
178 def users_new(self):
179 _ = self.request.translate
179 _ = self.request.translate
180 c = self.load_default_context()
180 c = self.load_default_context()
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 self._set_personal_repo_group_template_vars(c)
182 self._set_personal_repo_group_template_vars(c)
183 return self._get_template_context(c)
183 return self._get_template_context(c)
184
184
185 @LoginRequired()
185 @LoginRequired()
186 @HasPermissionAllDecorator('hg.admin')
186 @HasPermissionAllDecorator('hg.admin')
187 @CSRFRequired()
187 @CSRFRequired()
188 def users_create(self):
188 def users_create(self):
189 _ = self.request.translate
189 _ = self.request.translate
190 c = self.load_default_context()
190 c = self.load_default_context()
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 user_model = UserModel()
192 user_model = UserModel()
193 user_form = UserForm(self.request.translate)()
193 user_form = UserForm(self.request.translate)()
194 try:
194 try:
195 form_result = user_form.to_python(dict(self.request.POST))
195 form_result = user_form.to_python(dict(self.request.POST))
196 user = user_model.create(form_result)
196 user = user_model.create(form_result)
197 Session().flush()
197 Session().flush()
198 creation_data = user.get_api_data()
198 creation_data = user.get_api_data()
199 username = form_result['username']
199 username = form_result['username']
200
200
201 audit_logger.store_web(
201 audit_logger.store_web(
202 'user.create', action_data={'data': creation_data},
202 'user.create', action_data={'data': creation_data},
203 user=c.rhodecode_user)
203 user=c.rhodecode_user)
204
204
205 user_link = h.link_to(
205 user_link = h.link_to(
206 h.escape(username),
206 h.escape(username),
207 h.route_path('user_edit', user_id=user.user_id))
207 h.route_path('user_edit', user_id=user.user_id))
208 h.flash(h.literal(_('Created user %(user_link)s')
208 h.flash(h.literal(_('Created user %(user_link)s')
209 % {'user_link': user_link}), category='success')
209 % {'user_link': user_link}), category='success')
210 Session().commit()
210 Session().commit()
211 except formencode.Invalid as errors:
211 except formencode.Invalid as errors:
212 self._set_personal_repo_group_template_vars(c)
212 self._set_personal_repo_group_template_vars(c)
213 data = render(
213 data = render(
214 'rhodecode:templates/admin/users/user_add.mako',
214 'rhodecode:templates/admin/users/user_add.mako',
215 self._get_template_context(c), self.request)
215 self._get_template_context(c), self.request)
216 html = formencode.htmlfill.render(
216 html = formencode.htmlfill.render(
217 data,
217 data,
218 defaults=errors.value,
218 defaults=errors.value,
219 errors=errors.unpack_errors() or {},
219 errors=errors.unpack_errors() or {},
220 prefix_error=False,
220 prefix_error=False,
221 encoding="UTF-8",
221 encoding="UTF-8",
222 force_defaults=False
222 force_defaults=False
223 )
223 )
224 return Response(html)
224 return Response(html)
225 except UserCreationError as e:
225 except UserCreationError as e:
226 h.flash(safe_unicode(e), 'error')
226 h.flash(safe_str(e), 'error')
227 except Exception:
227 except Exception:
228 log.exception("Exception creation of user")
228 log.exception("Exception creation of user")
229 h.flash(_('Error occurred during creation of user %s')
229 h.flash(_('Error occurred during creation of user %s')
230 % self.request.POST.get('username'), category='error')
230 % self.request.POST.get('username'), category='error')
231 raise HTTPFound(h.route_path('users'))
231 raise HTTPFound(h.route_path('users'))
232
232
233
233
234 class UsersView(UserAppView):
234 class UsersView(UserAppView):
235 ALLOW_SCOPED_TOKENS = False
235 ALLOW_SCOPED_TOKENS = False
236 """
236 """
237 This view has alternative version inside EE, if modified please take a look
237 This view has alternative version inside EE, if modified please take a look
238 in there as well.
238 in there as well.
239 """
239 """
240
240
241 def get_auth_plugins(self):
241 def get_auth_plugins(self):
242 valid_plugins = []
242 valid_plugins = []
243 authn_registry = get_authn_registry(self.request.registry)
243 authn_registry = get_authn_registry(self.request.registry)
244 for plugin in authn_registry.get_plugins_for_authentication():
244 for plugin in authn_registry.get_plugins_for_authentication():
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 valid_plugins.append(plugin)
246 valid_plugins.append(plugin)
247 elif plugin.name == 'rhodecode':
247 elif plugin.name == 'rhodecode':
248 valid_plugins.append(plugin)
248 valid_plugins.append(plugin)
249
249
250 # extend our choices if user has set a bound plugin which isn't enabled at the
250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 # moment
251 # moment
252 extern_type = self.db_user.extern_type
252 extern_type = self.db_user.extern_type
253 if extern_type not in [x.uid for x in valid_plugins]:
253 if extern_type not in [x.uid for x in valid_plugins]:
254 try:
254 try:
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 if plugin:
256 if plugin:
257 valid_plugins.append(plugin)
257 valid_plugins.append(plugin)
258
258
259 except Exception:
259 except Exception:
260 log.exception(
260 log.exception(
261 'Could not extend user plugins with `{}`'.format(extern_type))
261 'Could not extend user plugins with `{}`'.format(extern_type))
262 return valid_plugins
262 return valid_plugins
263
263
264 def load_default_context(self):
264 def load_default_context(self):
265 req = self.request
265 req = self.request
266
266
267 c = self._get_local_tmpl_context()
267 c = self._get_local_tmpl_context()
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 c.allowed_languages = [
269 c.allowed_languages = [
270 ('en', 'English (en)'),
270 ('en', 'English (en)'),
271 ('de', 'German (de)'),
271 ('de', 'German (de)'),
272 ('fr', 'French (fr)'),
272 ('fr', 'French (fr)'),
273 ('it', 'Italian (it)'),
273 ('it', 'Italian (it)'),
274 ('ja', 'Japanese (ja)'),
274 ('ja', 'Japanese (ja)'),
275 ('pl', 'Polish (pl)'),
275 ('pl', 'Polish (pl)'),
276 ('pt', 'Portuguese (pt)'),
276 ('pt', 'Portuguese (pt)'),
277 ('ru', 'Russian (ru)'),
277 ('ru', 'Russian (ru)'),
278 ('zh', 'Chinese (zh)'),
278 ('zh', 'Chinese (zh)'),
279 ]
279 ]
280
280
281 c.allowed_extern_types = [
281 c.allowed_extern_types = [
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 ]
283 ]
284 perms = req.registry.settings.get('available_permissions')
284 perms = req.registry.settings.get('available_permissions')
285 if not perms:
285 if not perms:
286 # inject info about available permissions
286 # inject info about available permissions
287 auth.set_available_permissions(req.registry.settings)
287 auth.set_available_permissions(req.registry.settings)
288
288
289 c.available_permissions = req.registry.settings['available_permissions']
289 c.available_permissions = req.registry.settings['available_permissions']
290 PermissionModel().set_global_permission_choices(
290 PermissionModel().set_global_permission_choices(
291 c, gettext_translator=req.translate)
291 c, gettext_translator=req.translate)
292
292
293 return c
293 return c
294
294
295 @LoginRequired()
295 @LoginRequired()
296 @HasPermissionAllDecorator('hg.admin')
296 @HasPermissionAllDecorator('hg.admin')
297 @CSRFRequired()
297 @CSRFRequired()
298 def user_update(self):
298 def user_update(self):
299 _ = self.request.translate
299 _ = self.request.translate
300 c = self.load_default_context()
300 c = self.load_default_context()
301
301
302 user_id = self.db_user_id
302 user_id = self.db_user_id
303 c.user = self.db_user
303 c.user = self.db_user
304
304
305 c.active = 'profile'
305 c.active = 'profile'
306 c.extern_type = c.user.extern_type
306 c.extern_type = c.user.extern_type
307 c.extern_name = c.user.extern_name
307 c.extern_name = c.user.extern_name
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 available_languages = [x[0] for x in c.allowed_languages]
309 available_languages = [x[0] for x in c.allowed_languages]
310 _form = UserForm(self.request.translate, edit=True,
310 _form = UserForm(self.request.translate, edit=True,
311 available_languages=available_languages,
311 available_languages=available_languages,
312 old_data={'user_id': user_id,
312 old_data={'user_id': user_id,
313 'email': c.user.email})()
313 'email': c.user.email})()
314
314
315 c.edit_mode = self.request.POST.get('edit') == '1'
315 c.edit_mode = self.request.POST.get('edit') == '1'
316 form_result = {}
316 form_result = {}
317 old_values = c.user.get_api_data()
317 old_values = c.user.get_api_data()
318 try:
318 try:
319 form_result = _form.to_python(dict(self.request.POST))
319 form_result = _form.to_python(dict(self.request.POST))
320 skip_attrs = ['extern_name']
320 skip_attrs = ['extern_name']
321 # TODO: plugin should define if username can be updated
321 # TODO: plugin should define if username can be updated
322
322
323 if c.extern_type != "rhodecode" and not c.edit_mode:
323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 # forbid updating username for external accounts
324 # forbid updating username for external accounts
325 skip_attrs.append('username')
325 skip_attrs.append('username')
326
326
327 UserModel().update_user(
327 UserModel().update_user(
328 user_id, skip_attrs=skip_attrs, **form_result)
328 user_id, skip_attrs=skip_attrs, **form_result)
329
329
330 audit_logger.store_web(
330 audit_logger.store_web(
331 'user.edit', action_data={'old_data': old_values},
331 'user.edit', action_data={'old_data': old_values},
332 user=c.rhodecode_user)
332 user=c.rhodecode_user)
333
333
334 Session().commit()
334 Session().commit()
335 h.flash(_('User updated successfully'), category='success')
335 h.flash(_('User updated successfully'), category='success')
336 except formencode.Invalid as errors:
336 except formencode.Invalid as errors:
337 data = render(
337 data = render(
338 'rhodecode:templates/admin/users/user_edit.mako',
338 'rhodecode:templates/admin/users/user_edit.mako',
339 self._get_template_context(c), self.request)
339 self._get_template_context(c), self.request)
340 html = formencode.htmlfill.render(
340 html = formencode.htmlfill.render(
341 data,
341 data,
342 defaults=errors.value,
342 defaults=errors.value,
343 errors=errors.unpack_errors() or {},
343 errors=errors.unpack_errors() or {},
344 prefix_error=False,
344 prefix_error=False,
345 encoding="UTF-8",
345 encoding="UTF-8",
346 force_defaults=False
346 force_defaults=False
347 )
347 )
348 return Response(html)
348 return Response(html)
349 except UserCreationError as e:
349 except UserCreationError as e:
350 h.flash(safe_unicode(e), 'error')
350 h.flash(safe_str(e), 'error')
351 except Exception:
351 except Exception:
352 log.exception("Exception updating user")
352 log.exception("Exception updating user")
353 h.flash(_('Error occurred during update of user %s')
353 h.flash(_('Error occurred during update of user %s')
354 % form_result.get('username'), category='error')
354 % form_result.get('username'), category='error')
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356
356
357 @LoginRequired()
357 @LoginRequired()
358 @HasPermissionAllDecorator('hg.admin')
358 @HasPermissionAllDecorator('hg.admin')
359 @CSRFRequired()
359 @CSRFRequired()
360 def user_delete(self):
360 def user_delete(self):
361 _ = self.request.translate
361 _ = self.request.translate
362 c = self.load_default_context()
362 c = self.load_default_context()
363 c.user = self.db_user
363 c.user = self.db_user
364
364
365 _repos = c.user.repositories
365 _repos = c.user.repositories
366 _repo_groups = c.user.repository_groups
366 _repo_groups = c.user.repository_groups
367 _user_groups = c.user.user_groups
367 _user_groups = c.user.user_groups
368 _pull_requests = c.user.user_pull_requests
368 _pull_requests = c.user.user_pull_requests
369 _artifacts = c.user.artifacts
369 _artifacts = c.user.artifacts
370
370
371 handle_repos = None
371 handle_repos = None
372 handle_repo_groups = None
372 handle_repo_groups = None
373 handle_user_groups = None
373 handle_user_groups = None
374 handle_pull_requests = None
374 handle_pull_requests = None
375 handle_artifacts = None
375 handle_artifacts = None
376
376
377 # calls for flash of handle based on handle case detach or delete
377 # calls for flash of handle based on handle case detach or delete
378 def set_handle_flash_repos():
378 def set_handle_flash_repos():
379 handle = handle_repos
379 handle = handle_repos
380 if handle == 'detach':
380 if handle == 'detach':
381 h.flash(_('Detached %s repositories') % len(_repos),
381 h.flash(_('Detached %s repositories') % len(_repos),
382 category='success')
382 category='success')
383 elif handle == 'delete':
383 elif handle == 'delete':
384 h.flash(_('Deleted %s repositories') % len(_repos),
384 h.flash(_('Deleted %s repositories') % len(_repos),
385 category='success')
385 category='success')
386
386
387 def set_handle_flash_repo_groups():
387 def set_handle_flash_repo_groups():
388 handle = handle_repo_groups
388 handle = handle_repo_groups
389 if handle == 'detach':
389 if handle == 'detach':
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
391 category='success')
391 category='success')
392 elif handle == 'delete':
392 elif handle == 'delete':
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
394 category='success')
394 category='success')
395
395
396 def set_handle_flash_user_groups():
396 def set_handle_flash_user_groups():
397 handle = handle_user_groups
397 handle = handle_user_groups
398 if handle == 'detach':
398 if handle == 'detach':
399 h.flash(_('Detached %s user groups') % len(_user_groups),
399 h.flash(_('Detached %s user groups') % len(_user_groups),
400 category='success')
400 category='success')
401 elif handle == 'delete':
401 elif handle == 'delete':
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
403 category='success')
403 category='success')
404
404
405 def set_handle_flash_pull_requests():
405 def set_handle_flash_pull_requests():
406 handle = handle_pull_requests
406 handle = handle_pull_requests
407 if handle == 'detach':
407 if handle == 'detach':
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
409 category='success')
409 category='success')
410 elif handle == 'delete':
410 elif handle == 'delete':
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
412 category='success')
412 category='success')
413
413
414 def set_handle_flash_artifacts():
414 def set_handle_flash_artifacts():
415 handle = handle_artifacts
415 handle = handle_artifacts
416 if handle == 'detach':
416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 category='success')
418 category='success')
419 elif handle == 'delete':
419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 category='success')
421 category='success')
422
422
423 handle_user = User.get_first_super_admin()
423 handle_user = User.get_first_super_admin()
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 if handle_user_id:
425 if handle_user_id:
426 # NOTE(marcink): we get new owner for objects...
426 # NOTE(marcink): we get new owner for objects...
427 handle_user = User.get_or_404(handle_user_id)
427 handle_user = User.get_or_404(handle_user_id)
428
428
429 if _repos and self.request.POST.get('user_repos'):
429 if _repos and self.request.POST.get('user_repos'):
430 handle_repos = self.request.POST['user_repos']
430 handle_repos = self.request.POST['user_repos']
431
431
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 handle_repo_groups = self.request.POST['user_repo_groups']
433 handle_repo_groups = self.request.POST['user_repo_groups']
434
434
435 if _user_groups and self.request.POST.get('user_user_groups'):
435 if _user_groups and self.request.POST.get('user_user_groups'):
436 handle_user_groups = self.request.POST['user_user_groups']
436 handle_user_groups = self.request.POST['user_user_groups']
437
437
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 handle_pull_requests = self.request.POST['user_pull_requests']
439 handle_pull_requests = self.request.POST['user_pull_requests']
440
440
441 if _artifacts and self.request.POST.get('user_artifacts'):
441 if _artifacts and self.request.POST.get('user_artifacts'):
442 handle_artifacts = self.request.POST['user_artifacts']
442 handle_artifacts = self.request.POST['user_artifacts']
443
443
444 old_values = c.user.get_api_data()
444 old_values = c.user.get_api_data()
445
445
446 try:
446 try:
447
447
448 UserModel().delete(
448 UserModel().delete(
449 c.user,
449 c.user,
450 handle_repos=handle_repos,
450 handle_repos=handle_repos,
451 handle_repo_groups=handle_repo_groups,
451 handle_repo_groups=handle_repo_groups,
452 handle_user_groups=handle_user_groups,
452 handle_user_groups=handle_user_groups,
453 handle_pull_requests=handle_pull_requests,
453 handle_pull_requests=handle_pull_requests,
454 handle_artifacts=handle_artifacts,
454 handle_artifacts=handle_artifacts,
455 handle_new_owner=handle_user
455 handle_new_owner=handle_user
456 )
456 )
457
457
458 audit_logger.store_web(
458 audit_logger.store_web(
459 'user.delete', action_data={'old_data': old_values},
459 'user.delete', action_data={'old_data': old_values},
460 user=c.rhodecode_user)
460 user=c.rhodecode_user)
461
461
462 Session().commit()
462 Session().commit()
463 set_handle_flash_repos()
463 set_handle_flash_repos()
464 set_handle_flash_repo_groups()
464 set_handle_flash_repo_groups()
465 set_handle_flash_user_groups()
465 set_handle_flash_user_groups()
466 set_handle_flash_pull_requests()
466 set_handle_flash_pull_requests()
467 set_handle_flash_artifacts()
467 set_handle_flash_artifacts()
468 username = h.escape(old_values['username'])
468 username = h.escape(old_values['username'])
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 UserOwnsArtifactsException, DefaultUserException) as e:
472 UserOwnsArtifactsException, DefaultUserException) as e:
473 h.flash(e, category='warning')
473
474 h.flash(safe_str(e), category='warning')
474 except Exception:
475 except Exception:
475 log.exception("Exception during deletion of user")
476 log.exception("Exception during deletion of user")
476 h.flash(_('An error occurred during deletion of user'),
477 h.flash(_('An error occurred during deletion of user'),
477 category='error')
478 category='error')
478 raise HTTPFound(h.route_path('users'))
479 raise HTTPFound(h.route_path('users'))
479
480
480 @LoginRequired()
481 @LoginRequired()
481 @HasPermissionAllDecorator('hg.admin')
482 @HasPermissionAllDecorator('hg.admin')
482 def user_edit(self):
483 def user_edit(self):
483 _ = self.request.translate
484 _ = self.request.translate
484 c = self.load_default_context()
485 c = self.load_default_context()
485 c.user = self.db_user
486 c.user = self.db_user
486
487
487 c.active = 'profile'
488 c.active = 'profile'
488 c.extern_type = c.user.extern_type
489 c.extern_type = c.user.extern_type
489 c.extern_name = c.user.extern_name
490 c.extern_name = c.user.extern_name
490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 c.edit_mode = self.request.GET.get('edit') == '1'
492 c.edit_mode = self.request.GET.get('edit') == '1'
492
493
493 defaults = c.user.get_dict()
494 defaults = c.user.get_dict()
494 defaults.update({'language': c.user.user_data.get('language')})
495 defaults.update({'language': c.user.user_data.get('language')})
495
496
496 data = render(
497 data = render(
497 'rhodecode:templates/admin/users/user_edit.mako',
498 'rhodecode:templates/admin/users/user_edit.mako',
498 self._get_template_context(c), self.request)
499 self._get_template_context(c), self.request)
499 html = formencode.htmlfill.render(
500 html = formencode.htmlfill.render(
500 data,
501 data,
501 defaults=defaults,
502 defaults=defaults,
502 encoding="UTF-8",
503 encoding="UTF-8",
503 force_defaults=False
504 force_defaults=False
504 )
505 )
505 return Response(html)
506 return Response(html)
506
507
507 @LoginRequired()
508 @LoginRequired()
508 @HasPermissionAllDecorator('hg.admin')
509 @HasPermissionAllDecorator('hg.admin')
509 def user_edit_advanced(self):
510 def user_edit_advanced(self):
510 _ = self.request.translate
511 _ = self.request.translate
511 c = self.load_default_context()
512 c = self.load_default_context()
512
513
513 user_id = self.db_user_id
514 user_id = self.db_user_id
514 c.user = self.db_user
515 c.user = self.db_user
515
516
516 c.detach_user = User.get_first_super_admin()
517 c.detach_user = User.get_first_super_admin()
517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 if detach_user_id:
519 if detach_user_id:
519 c.detach_user = User.get_or_404(detach_user_id)
520 c.detach_user = User.get_or_404(detach_user_id)
520
521
521 c.active = 'advanced'
522 c.active = 'advanced'
522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 c.personal_repo_group_name = RepoGroupModel()\
524 c.personal_repo_group_name = RepoGroupModel()\
524 .get_personal_group_name(c.user)
525 .get_personal_group_name(c.user)
525
526
526 c.user_to_review_rules = sorted(
527 c.user_to_review_rules = sorted(
527 (x.user for x in c.user.user_review_rules),
528 (x.user for x in c.user.user_review_rules),
528 key=lambda u: u.username.lower())
529 key=lambda u: u.username.lower())
529
530
530 defaults = c.user.get_dict()
531 defaults = c.user.get_dict()
531
532
532 # Interim workaround if the user participated on any pull requests as a
533 # Interim workaround if the user participated on any pull requests as a
533 # reviewer.
534 # reviewer.
534 has_review = len(c.user.reviewer_pull_requests)
535 has_review = len(c.user.reviewer_pull_requests)
535 c.can_delete_user = not has_review
536 c.can_delete_user = not has_review
536 c.can_delete_user_message = ''
537 c.can_delete_user_message = ''
537 inactive_link = h.link_to(
538 inactive_link = h.link_to(
538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 if has_review == 1:
540 if has_review == 1:
540 c.can_delete_user_message = h.literal(_(
541 c.can_delete_user_message = h.literal(_(
541 'The user participates as reviewer in {} pull request and '
542 'The user participates as reviewer in {} pull request and '
542 'cannot be deleted. \nYou can set the user to '
543 'cannot be deleted. \nYou can set the user to '
543 '"{}" instead of deleting it.').format(
544 '"{}" instead of deleting it.').format(
544 has_review, inactive_link))
545 has_review, inactive_link))
545 elif has_review:
546 elif has_review:
546 c.can_delete_user_message = h.literal(_(
547 c.can_delete_user_message = h.literal(_(
547 'The user participates as reviewer in {} pull requests and '
548 'The user participates as reviewer in {} pull requests and '
548 'cannot be deleted. \nYou can set the user to '
549 'cannot be deleted. \nYou can set the user to '
549 '"{}" instead of deleting it.').format(
550 '"{}" instead of deleting it.').format(
550 has_review, inactive_link))
551 has_review, inactive_link))
551
552
552 data = render(
553 data = render(
553 'rhodecode:templates/admin/users/user_edit.mako',
554 'rhodecode:templates/admin/users/user_edit.mako',
554 self._get_template_context(c), self.request)
555 self._get_template_context(c), self.request)
555 html = formencode.htmlfill.render(
556 html = formencode.htmlfill.render(
556 data,
557 data,
557 defaults=defaults,
558 defaults=defaults,
558 encoding="UTF-8",
559 encoding="UTF-8",
559 force_defaults=False
560 force_defaults=False
560 )
561 )
561 return Response(html)
562 return Response(html)
562
563
563 @LoginRequired()
564 @LoginRequired()
564 @HasPermissionAllDecorator('hg.admin')
565 @HasPermissionAllDecorator('hg.admin')
565 def user_edit_global_perms(self):
566 def user_edit_global_perms(self):
566 _ = self.request.translate
567 _ = self.request.translate
567 c = self.load_default_context()
568 c = self.load_default_context()
568 c.user = self.db_user
569 c.user = self.db_user
569
570
570 c.active = 'global_perms'
571 c.active = 'global_perms'
571
572
572 c.default_user = User.get_default_user()
573 c.default_user = User.get_default_user()
573 defaults = c.user.get_dict()
574 defaults = c.user.get_dict()
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 defaults.update(c.default_user.get_default_perms())
576 defaults.update(c.default_user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
577 defaults.update(c.user.get_default_perms())
577
578
578 data = render(
579 data = render(
579 'rhodecode:templates/admin/users/user_edit.mako',
580 'rhodecode:templates/admin/users/user_edit.mako',
580 self._get_template_context(c), self.request)
581 self._get_template_context(c), self.request)
581 html = formencode.htmlfill.render(
582 html = formencode.htmlfill.render(
582 data,
583 data,
583 defaults=defaults,
584 defaults=defaults,
584 encoding="UTF-8",
585 encoding="UTF-8",
585 force_defaults=False
586 force_defaults=False
586 )
587 )
587 return Response(html)
588 return Response(html)
588
589
589 @LoginRequired()
590 @LoginRequired()
590 @HasPermissionAllDecorator('hg.admin')
591 @HasPermissionAllDecorator('hg.admin')
591 @CSRFRequired()
592 @CSRFRequired()
592 def user_edit_global_perms_update(self):
593 def user_edit_global_perms_update(self):
593 _ = self.request.translate
594 _ = self.request.translate
594 c = self.load_default_context()
595 c = self.load_default_context()
595
596
596 user_id = self.db_user_id
597 user_id = self.db_user_id
597 c.user = self.db_user
598 c.user = self.db_user
598
599
599 c.active = 'global_perms'
600 c.active = 'global_perms'
600 try:
601 try:
601 # first stage that verifies the checkbox
602 # first stage that verifies the checkbox
602 _form = UserIndividualPermissionsForm(self.request.translate)
603 _form = UserIndividualPermissionsForm(self.request.translate)
603 form_result = _form.to_python(dict(self.request.POST))
604 form_result = _form.to_python(dict(self.request.POST))
604 inherit_perms = form_result['inherit_default_permissions']
605 inherit_perms = form_result['inherit_default_permissions']
605 c.user.inherit_default_permissions = inherit_perms
606 c.user.inherit_default_permissions = inherit_perms
606 Session().add(c.user)
607 Session().add(c.user)
607
608
608 if not inherit_perms:
609 if not inherit_perms:
609 # only update the individual ones if we un check the flag
610 # only update the individual ones if we un check the flag
610 _form = UserPermissionsForm(
611 _form = UserPermissionsForm(
611 self.request.translate,
612 self.request.translate,
612 [x[0] for x in c.repo_create_choices],
613 [x[0] for x in c.repo_create_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
614 [x[0] for x in c.repo_create_on_write_choices],
614 [x[0] for x in c.repo_group_create_choices],
615 [x[0] for x in c.repo_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
616 [x[0] for x in c.user_group_create_choices],
616 [x[0] for x in c.fork_choices],
617 [x[0] for x in c.fork_choices],
617 [x[0] for x in c.inherit_default_permission_choices])()
618 [x[0] for x in c.inherit_default_permission_choices])()
618
619
619 form_result = _form.to_python(dict(self.request.POST))
620 form_result = _form.to_python(dict(self.request.POST))
620 form_result.update({'perm_user_id': c.user.user_id})
621 form_result.update({'perm_user_id': c.user.user_id})
621
622
622 PermissionModel().update_user_permissions(form_result)
623 PermissionModel().update_user_permissions(form_result)
623
624
624 # TODO(marcink): implement global permissions
625 # TODO(marcink): implement global permissions
625 # audit_log.store_web('user.edit.permissions')
626 # audit_log.store_web('user.edit.permissions')
626
627
627 Session().commit()
628 Session().commit()
628
629
629 h.flash(_('User global permissions updated successfully'),
630 h.flash(_('User global permissions updated successfully'),
630 category='success')
631 category='success')
631
632
632 except formencode.Invalid as errors:
633 except formencode.Invalid as errors:
633 data = render(
634 data = render(
634 'rhodecode:templates/admin/users/user_edit.mako',
635 'rhodecode:templates/admin/users/user_edit.mako',
635 self._get_template_context(c), self.request)
636 self._get_template_context(c), self.request)
636 html = formencode.htmlfill.render(
637 html = formencode.htmlfill.render(
637 data,
638 data,
638 defaults=errors.value,
639 defaults=errors.value,
639 errors=errors.unpack_errors() or {},
640 errors=errors.unpack_errors() or {},
640 prefix_error=False,
641 prefix_error=False,
641 encoding="UTF-8",
642 encoding="UTF-8",
642 force_defaults=False
643 force_defaults=False
643 )
644 )
644 return Response(html)
645 return Response(html)
645 except Exception:
646 except Exception:
646 log.exception("Exception during permissions saving")
647 log.exception("Exception during permissions saving")
647 h.flash(_('An error occurred during permissions saving'),
648 h.flash(_('An error occurred during permissions saving'),
648 category='error')
649 category='error')
649
650
650 affected_user_ids = [user_id]
651 affected_user_ids = [user_id]
651 PermissionModel().trigger_permission_flush(affected_user_ids)
652 PermissionModel().trigger_permission_flush(affected_user_ids)
652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653
654
654 @LoginRequired()
655 @LoginRequired()
655 @HasPermissionAllDecorator('hg.admin')
656 @HasPermissionAllDecorator('hg.admin')
656 @CSRFRequired()
657 @CSRFRequired()
657 def user_enable_force_password_reset(self):
658 def user_enable_force_password_reset(self):
658 _ = self.request.translate
659 _ = self.request.translate
659 c = self.load_default_context()
660 c = self.load_default_context()
660
661
661 user_id = self.db_user_id
662 user_id = self.db_user_id
662 c.user = self.db_user
663 c.user = self.db_user
663
664
664 try:
665 try:
665 c.user.update_userdata(force_password_change=True)
666 c.user.update_userdata(force_password_change=True)
666
667
667 msg = _('Force password change enabled for user')
668 msg = _('Force password change enabled for user')
668 audit_logger.store_web('user.edit.password_reset.enabled',
669 audit_logger.store_web('user.edit.password_reset.enabled',
669 user=c.rhodecode_user)
670 user=c.rhodecode_user)
670
671
671 Session().commit()
672 Session().commit()
672 h.flash(msg, category='success')
673 h.flash(msg, category='success')
673 except Exception:
674 except Exception:
674 log.exception("Exception during password reset for user")
675 log.exception("Exception during password reset for user")
675 h.flash(_('An error occurred during password reset for user'),
676 h.flash(_('An error occurred during password reset for user'),
676 category='error')
677 category='error')
677
678
678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679
680
680 @LoginRequired()
681 @LoginRequired()
681 @HasPermissionAllDecorator('hg.admin')
682 @HasPermissionAllDecorator('hg.admin')
682 @CSRFRequired()
683 @CSRFRequired()
683 def user_disable_force_password_reset(self):
684 def user_disable_force_password_reset(self):
684 _ = self.request.translate
685 _ = self.request.translate
685 c = self.load_default_context()
686 c = self.load_default_context()
686
687
687 user_id = self.db_user_id
688 user_id = self.db_user_id
688 c.user = self.db_user
689 c.user = self.db_user
689
690
690 try:
691 try:
691 c.user.update_userdata(force_password_change=False)
692 c.user.update_userdata(force_password_change=False)
692
693
693 msg = _('Force password change disabled for user')
694 msg = _('Force password change disabled for user')
694 audit_logger.store_web(
695 audit_logger.store_web(
695 'user.edit.password_reset.disabled',
696 'user.edit.password_reset.disabled',
696 user=c.rhodecode_user)
697 user=c.rhodecode_user)
697
698
698 Session().commit()
699 Session().commit()
699 h.flash(msg, category='success')
700 h.flash(msg, category='success')
700 except Exception:
701 except Exception:
701 log.exception("Exception during password reset for user")
702 log.exception("Exception during password reset for user")
702 h.flash(_('An error occurred during password reset for user'),
703 h.flash(_('An error occurred during password reset for user'),
703 category='error')
704 category='error')
704
705
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706
707
707 @LoginRequired()
708 @LoginRequired()
708 @HasPermissionAllDecorator('hg.admin')
709 @HasPermissionAllDecorator('hg.admin')
709 @CSRFRequired()
710 @CSRFRequired()
710 def user_notice_dismiss(self):
711 def user_notice_dismiss(self):
711 _ = self.request.translate
712 _ = self.request.translate
712 c = self.load_default_context()
713 c = self.load_default_context()
713
714
714 user_id = self.db_user_id
715 user_id = self.db_user_id
715 c.user = self.db_user
716 c.user = self.db_user
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 notice = UserNotice().query()\
718 notice = UserNotice().query()\
718 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .scalar()
721 .scalar()
721 read = False
722 read = False
722 if notice:
723 if notice:
723 notice.notice_read = True
724 notice.notice_read = True
724 Session().add(notice)
725 Session().add(notice)
725 Session().commit()
726 Session().commit()
726 read = True
727 read = True
727
728
728 return {'notice': user_notice_id, 'read': read}
729 return {'notice': user_notice_id, 'read': read}
729
730
730 @LoginRequired()
731 @LoginRequired()
731 @HasPermissionAllDecorator('hg.admin')
732 @HasPermissionAllDecorator('hg.admin')
732 @CSRFRequired()
733 @CSRFRequired()
733 def user_create_personal_repo_group(self):
734 def user_create_personal_repo_group(self):
734 """
735 """
735 Create personal repository group for this user
736 Create personal repository group for this user
736 """
737 """
737 from rhodecode.model.repo_group import RepoGroupModel
738 from rhodecode.model.repo_group import RepoGroupModel
738
739
739 _ = self.request.translate
740 _ = self.request.translate
740 c = self.load_default_context()
741 c = self.load_default_context()
741
742
742 user_id = self.db_user_id
743 user_id = self.db_user_id
743 c.user = self.db_user
744 c.user = self.db_user
744
745
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 c.user.user_id)
747 c.user.user_id)
747 if personal_repo_group:
748 if personal_repo_group:
748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749
750
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 named_personal_group = RepoGroup.get_by_group_name(
752 named_personal_group = RepoGroup.get_by_group_name(
752 personal_repo_group_name)
753 personal_repo_group_name)
753 try:
754 try:
754
755
755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 # migrate the same named group, and mark it as personal
757 # migrate the same named group, and mark it as personal
757 named_personal_group.personal = True
758 named_personal_group.personal = True
758 Session().add(named_personal_group)
759 Session().add(named_personal_group)
759 Session().commit()
760 Session().commit()
760 msg = _('Linked repository group `%s` as personal' % (
761 msg = _('Linked repository group `%s` as personal' % (
761 personal_repo_group_name,))
762 personal_repo_group_name,))
762 h.flash(msg, category='success')
763 h.flash(msg, category='success')
763 elif not named_personal_group:
764 elif not named_personal_group:
764 RepoGroupModel().create_personal_repo_group(c.user)
765 RepoGroupModel().create_personal_repo_group(c.user)
765
766
766 msg = _('Created repository group `%s`' % (
767 msg = _('Created repository group `%s`' % (
767 personal_repo_group_name,))
768 personal_repo_group_name,))
768 h.flash(msg, category='success')
769 h.flash(msg, category='success')
769 else:
770 else:
770 msg = _('Repository group `%s` is already taken' % (
771 msg = _('Repository group `%s` is already taken' % (
771 personal_repo_group_name,))
772 personal_repo_group_name,))
772 h.flash(msg, category='warning')
773 h.flash(msg, category='warning')
773 except Exception:
774 except Exception:
774 log.exception("Exception during repository group creation")
775 log.exception("Exception during repository group creation")
775 msg = _(
776 msg = _(
776 'An error occurred during repository group creation for user')
777 'An error occurred during repository group creation for user')
777 h.flash(msg, category='error')
778 h.flash(msg, category='error')
778 Session().rollback()
779 Session().rollback()
779
780
780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781
782
782 @LoginRequired()
783 @LoginRequired()
783 @HasPermissionAllDecorator('hg.admin')
784 @HasPermissionAllDecorator('hg.admin')
784 def auth_tokens(self):
785 def auth_tokens(self):
785 _ = self.request.translate
786 _ = self.request.translate
786 c = self.load_default_context()
787 c = self.load_default_context()
787 c.user = self.db_user
788 c.user = self.db_user
788
789
789 c.active = 'auth_tokens'
790 c.active = 'auth_tokens'
790
791
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 c.role_values = [
793 c.role_values = [
793 (x, AuthTokenModel.cls._get_role_name(x))
794 (x, AuthTokenModel.cls._get_role_name(x))
794 for x in AuthTokenModel.cls.ROLES]
795 for x in AuthTokenModel.cls.ROLES]
795 c.role_options = [(c.role_values, _("Role"))]
796 c.role_options = [(c.role_values, _("Role"))]
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 c.user.user_id, show_expired=True)
798 c.user.user_id, show_expired=True)
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 return self._get_template_context(c)
800 return self._get_template_context(c)
800
801
801 @LoginRequired()
802 @LoginRequired()
802 @HasPermissionAllDecorator('hg.admin')
803 @HasPermissionAllDecorator('hg.admin')
803 def auth_tokens_view(self):
804 def auth_tokens_view(self):
804 _ = self.request.translate
805 _ = self.request.translate
805 c = self.load_default_context()
806 c = self.load_default_context()
806 c.user = self.db_user
807 c.user = self.db_user
807
808
808 auth_token_id = self.request.POST.get('auth_token_id')
809 auth_token_id = self.request.POST.get('auth_token_id')
809
810
810 if auth_token_id:
811 if auth_token_id:
811 token = UserApiKeys.get_or_404(auth_token_id)
812 token = UserApiKeys.get_or_404(auth_token_id)
812
813
813 return {
814 return {
814 'auth_token': token.api_key
815 'auth_token': token.api_key
815 }
816 }
816
817
817 def maybe_attach_token_scope(self, token):
818 def maybe_attach_token_scope(self, token):
818 # implemented in EE edition
819 # implemented in EE edition
819 pass
820 pass
820
821
821 @LoginRequired()
822 @LoginRequired()
822 @HasPermissionAllDecorator('hg.admin')
823 @HasPermissionAllDecorator('hg.admin')
823 @CSRFRequired()
824 @CSRFRequired()
824 def auth_tokens_add(self):
825 def auth_tokens_add(self):
825 _ = self.request.translate
826 _ = self.request.translate
826 c = self.load_default_context()
827 c = self.load_default_context()
827
828
828 user_id = self.db_user_id
829 user_id = self.db_user_id
829 c.user = self.db_user
830 c.user = self.db_user
830
831
831 user_data = c.user.get_api_data()
832 user_data = c.user.get_api_data()
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 description = self.request.POST.get('description')
834 description = self.request.POST.get('description')
834 role = self.request.POST.get('role')
835 role = self.request.POST.get('role')
835
836
836 token = UserModel().add_auth_token(
837 token = UserModel().add_auth_token(
837 user=c.user.user_id,
838 user=c.user.user_id,
838 lifetime_minutes=lifetime, role=role, description=description,
839 lifetime_minutes=lifetime, role=role, description=description,
839 scope_callback=self.maybe_attach_token_scope)
840 scope_callback=self.maybe_attach_token_scope)
840 token_data = token.get_api_data()
841 token_data = token.get_api_data()
841
842
842 audit_logger.store_web(
843 audit_logger.store_web(
843 'user.edit.token.add', action_data={
844 'user.edit.token.add', action_data={
844 'data': {'token': token_data, 'user': user_data}},
845 'data': {'token': token_data, 'user': user_data}},
845 user=self._rhodecode_user, )
846 user=self._rhodecode_user, )
846 Session().commit()
847 Session().commit()
847
848
848 h.flash(_("Auth token successfully created"), category='success')
849 h.flash(_("Auth token successfully created"), category='success')
849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850
851
851 @LoginRequired()
852 @LoginRequired()
852 @HasPermissionAllDecorator('hg.admin')
853 @HasPermissionAllDecorator('hg.admin')
853 @CSRFRequired()
854 @CSRFRequired()
854 def auth_tokens_delete(self):
855 def auth_tokens_delete(self):
855 _ = self.request.translate
856 _ = self.request.translate
856 c = self.load_default_context()
857 c = self.load_default_context()
857
858
858 user_id = self.db_user_id
859 user_id = self.db_user_id
859 c.user = self.db_user
860 c.user = self.db_user
860
861
861 user_data = c.user.get_api_data()
862 user_data = c.user.get_api_data()
862
863
863 del_auth_token = self.request.POST.get('del_auth_token')
864 del_auth_token = self.request.POST.get('del_auth_token')
864
865
865 if del_auth_token:
866 if del_auth_token:
866 token = UserApiKeys.get_or_404(del_auth_token)
867 token = UserApiKeys.get_or_404(del_auth_token)
867 token_data = token.get_api_data()
868 token_data = token.get_api_data()
868
869
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 audit_logger.store_web(
871 audit_logger.store_web(
871 'user.edit.token.delete', action_data={
872 'user.edit.token.delete', action_data={
872 'data': {'token': token_data, 'user': user_data}},
873 'data': {'token': token_data, 'user': user_data}},
873 user=self._rhodecode_user,)
874 user=self._rhodecode_user,)
874 Session().commit()
875 Session().commit()
875 h.flash(_("Auth token successfully deleted"), category='success')
876 h.flash(_("Auth token successfully deleted"), category='success')
876
877
877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878
879
879 @LoginRequired()
880 @LoginRequired()
880 @HasPermissionAllDecorator('hg.admin')
881 @HasPermissionAllDecorator('hg.admin')
881 def ssh_keys(self):
882 def ssh_keys(self):
882 _ = self.request.translate
883 _ = self.request.translate
883 c = self.load_default_context()
884 c = self.load_default_context()
884 c.user = self.db_user
885 c.user = self.db_user
885
886
886 c.active = 'ssh_keys'
887 c.active = 'ssh_keys'
887 c.default_key = self.request.GET.get('default_key')
888 c.default_key = self.request.GET.get('default_key')
888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 return self._get_template_context(c)
890 return self._get_template_context(c)
890
891
891 @LoginRequired()
892 @LoginRequired()
892 @HasPermissionAllDecorator('hg.admin')
893 @HasPermissionAllDecorator('hg.admin')
893 def ssh_keys_generate_keypair(self):
894 def ssh_keys_generate_keypair(self):
894 _ = self.request.translate
895 _ = self.request.translate
895 c = self.load_default_context()
896 c = self.load_default_context()
896
897
897 c.user = self.db_user
898 c.user = self.db_user
898
899
899 c.active = 'ssh_keys_generate'
900 c.active = 'ssh_keys_generate'
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 private_format = self.request.GET.get('private_format') \
902 private_format = self.request.GET.get('private_format') \
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 c.private, c.public = SshKeyModel().generate_keypair(
904 c.private, c.public = SshKeyModel().generate_keypair(
904 comment=comment, private_format=private_format)
905 comment=comment, private_format=private_format)
905
906
906 return self._get_template_context(c)
907 return self._get_template_context(c)
907
908
908 @LoginRequired()
909 @LoginRequired()
909 @HasPermissionAllDecorator('hg.admin')
910 @HasPermissionAllDecorator('hg.admin')
910 @CSRFRequired()
911 @CSRFRequired()
911 def ssh_keys_add(self):
912 def ssh_keys_add(self):
912 _ = self.request.translate
913 _ = self.request.translate
913 c = self.load_default_context()
914 c = self.load_default_context()
914
915
915 user_id = self.db_user_id
916 user_id = self.db_user_id
916 c.user = self.db_user
917 c.user = self.db_user
917
918
918 user_data = c.user.get_api_data()
919 user_data = c.user.get_api_data()
919 key_data = self.request.POST.get('key_data')
920 key_data = self.request.POST.get('key_data')
920 description = self.request.POST.get('description')
921 description = self.request.POST.get('description')
921
922
922 fingerprint = 'unknown'
923 fingerprint = 'unknown'
923 try:
924 try:
924 if not key_data:
925 if not key_data:
925 raise ValueError('Please add a valid public key')
926 raise ValueError('Please add a valid public key')
926
927
927 key = SshKeyModel().parse_key(key_data.strip())
928 key = SshKeyModel().parse_key(key_data.strip())
928 fingerprint = key.hash_md5()
929 fingerprint = key.hash_md5()
929
930
930 ssh_key = SshKeyModel().create(
931 ssh_key = SshKeyModel().create(
931 c.user.user_id, fingerprint, key.keydata, description)
932 c.user.user_id, fingerprint, key.keydata, description)
932 ssh_key_data = ssh_key.get_api_data()
933 ssh_key_data = ssh_key.get_api_data()
933
934
934 audit_logger.store_web(
935 audit_logger.store_web(
935 'user.edit.ssh_key.add', action_data={
936 'user.edit.ssh_key.add', action_data={
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 user=self._rhodecode_user, )
938 user=self._rhodecode_user, )
938 Session().commit()
939 Session().commit()
939
940
940 # Trigger an event on change of keys.
941 # Trigger an event on change of keys.
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
942 trigger(SshKeyFileChangeEvent(), self.request.registry)
942
943
943 h.flash(_("Ssh Key successfully created"), category='success')
944 h.flash(_("Ssh Key successfully created"), category='success')
944
945
945 except IntegrityError:
946 except IntegrityError:
946 log.exception("Exception during ssh key saving")
947 log.exception("Exception during ssh key saving")
947 err = 'Such key with fingerprint `{}` already exists, ' \
948 err = 'Such key with fingerprint `{}` already exists, ' \
948 'please use a different one'.format(fingerprint)
949 'please use a different one'.format(fingerprint)
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 category='error')
951 category='error')
951 except Exception as e:
952 except Exception as e:
952 log.exception("Exception during ssh key saving")
953 log.exception("Exception during ssh key saving")
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 category='error')
955 category='error')
955
956
956 return HTTPFound(
957 return HTTPFound(
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
958 h.route_path('edit_user_ssh_keys', user_id=user_id))
958
959
959 @LoginRequired()
960 @LoginRequired()
960 @HasPermissionAllDecorator('hg.admin')
961 @HasPermissionAllDecorator('hg.admin')
961 @CSRFRequired()
962 @CSRFRequired()
962 def ssh_keys_delete(self):
963 def ssh_keys_delete(self):
963 _ = self.request.translate
964 _ = self.request.translate
964 c = self.load_default_context()
965 c = self.load_default_context()
965
966
966 user_id = self.db_user_id
967 user_id = self.db_user_id
967 c.user = self.db_user
968 c.user = self.db_user
968
969
969 user_data = c.user.get_api_data()
970 user_data = c.user.get_api_data()
970
971
971 del_ssh_key = self.request.POST.get('del_ssh_key')
972 del_ssh_key = self.request.POST.get('del_ssh_key')
972
973
973 if del_ssh_key:
974 if del_ssh_key:
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 ssh_key_data = ssh_key.get_api_data()
976 ssh_key_data = ssh_key.get_api_data()
976
977
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 audit_logger.store_web(
979 audit_logger.store_web(
979 'user.edit.ssh_key.delete', action_data={
980 'user.edit.ssh_key.delete', action_data={
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 user=self._rhodecode_user,)
982 user=self._rhodecode_user,)
982 Session().commit()
983 Session().commit()
983 # Trigger an event on change of keys.
984 # Trigger an event on change of keys.
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 h.flash(_("Ssh key successfully deleted"), category='success')
986 h.flash(_("Ssh key successfully deleted"), category='success')
986
987
987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988
989
989 @LoginRequired()
990 @LoginRequired()
990 @HasPermissionAllDecorator('hg.admin')
991 @HasPermissionAllDecorator('hg.admin')
991 def emails(self):
992 def emails(self):
992 _ = self.request.translate
993 _ = self.request.translate
993 c = self.load_default_context()
994 c = self.load_default_context()
994 c.user = self.db_user
995 c.user = self.db_user
995
996
996 c.active = 'emails'
997 c.active = 'emails'
997 c.user_email_map = UserEmailMap.query() \
998 c.user_email_map = UserEmailMap.query() \
998 .filter(UserEmailMap.user == c.user).all()
999 .filter(UserEmailMap.user == c.user).all()
999
1000
1000 return self._get_template_context(c)
1001 return self._get_template_context(c)
1001
1002
1002 @LoginRequired()
1003 @LoginRequired()
1003 @HasPermissionAllDecorator('hg.admin')
1004 @HasPermissionAllDecorator('hg.admin')
1004 @CSRFRequired()
1005 @CSRFRequired()
1005 def emails_add(self):
1006 def emails_add(self):
1006 _ = self.request.translate
1007 _ = self.request.translate
1007 c = self.load_default_context()
1008 c = self.load_default_context()
1008
1009
1009 user_id = self.db_user_id
1010 user_id = self.db_user_id
1010 c.user = self.db_user
1011 c.user = self.db_user
1011
1012
1012 email = self.request.POST.get('new_email')
1013 email = self.request.POST.get('new_email')
1013 user_data = c.user.get_api_data()
1014 user_data = c.user.get_api_data()
1014 try:
1015 try:
1015
1016
1016 form = UserExtraEmailForm(self.request.translate)()
1017 form = UserExtraEmailForm(self.request.translate)()
1017 data = form.to_python({'email': email})
1018 data = form.to_python({'email': email})
1018 email = data['email']
1019 email = data['email']
1019
1020
1020 UserModel().add_extra_email(c.user.user_id, email)
1021 UserModel().add_extra_email(c.user.user_id, email)
1021 audit_logger.store_web(
1022 audit_logger.store_web(
1022 'user.edit.email.add',
1023 'user.edit.email.add',
1023 action_data={'email': email, 'user': user_data},
1024 action_data={'email': email, 'user': user_data},
1024 user=self._rhodecode_user)
1025 user=self._rhodecode_user)
1025 Session().commit()
1026 Session().commit()
1026 h.flash(_("Added new email address `%s` for user account") % email,
1027 h.flash(_("Added new email address `%s` for user account") % email,
1027 category='success')
1028 category='success')
1028 except formencode.Invalid as error:
1029 except formencode.Invalid as error:
1029 msg = error.unpack_errors()['email']
1030 msg = error.unpack_errors()['email']
1030 h.flash(h.escape(msg), category='error')
1031 h.flash(h.escape(msg), category='error')
1031 except IntegrityError:
1032 except IntegrityError:
1032 log.warning("Email %s already exists", email)
1033 log.warning("Email %s already exists", email)
1033 h.flash(_('Email `{}` is already registered for another user.').format(email),
1034 h.flash(_('Email `{}` is already registered for another user.').format(email),
1034 category='error')
1035 category='error')
1035 except Exception:
1036 except Exception:
1036 log.exception("Exception during email saving")
1037 log.exception("Exception during email saving")
1037 h.flash(_('An error occurred during email saving'),
1038 h.flash(_('An error occurred during email saving'),
1038 category='error')
1039 category='error')
1039 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1040 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1040
1041
1041 @LoginRequired()
1042 @LoginRequired()
1042 @HasPermissionAllDecorator('hg.admin')
1043 @HasPermissionAllDecorator('hg.admin')
1043 @CSRFRequired()
1044 @CSRFRequired()
1044 def emails_delete(self):
1045 def emails_delete(self):
1045 _ = self.request.translate
1046 _ = self.request.translate
1046 c = self.load_default_context()
1047 c = self.load_default_context()
1047
1048
1048 user_id = self.db_user_id
1049 user_id = self.db_user_id
1049 c.user = self.db_user
1050 c.user = self.db_user
1050
1051
1051 email_id = self.request.POST.get('del_email_id')
1052 email_id = self.request.POST.get('del_email_id')
1052 user_model = UserModel()
1053 user_model = UserModel()
1053
1054
1054 email = UserEmailMap.query().get(email_id).email
1055 email = UserEmailMap.query().get(email_id).email
1055 user_data = c.user.get_api_data()
1056 user_data = c.user.get_api_data()
1056 user_model.delete_extra_email(c.user.user_id, email_id)
1057 user_model.delete_extra_email(c.user.user_id, email_id)
1057 audit_logger.store_web(
1058 audit_logger.store_web(
1058 'user.edit.email.delete',
1059 'user.edit.email.delete',
1059 action_data={'email': email, 'user': user_data},
1060 action_data={'email': email, 'user': user_data},
1060 user=self._rhodecode_user)
1061 user=self._rhodecode_user)
1061 Session().commit()
1062 Session().commit()
1062 h.flash(_("Removed email address from user account"),
1063 h.flash(_("Removed email address from user account"),
1063 category='success')
1064 category='success')
1064 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1065 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1065
1066
1066 @LoginRequired()
1067 @LoginRequired()
1067 @HasPermissionAllDecorator('hg.admin')
1068 @HasPermissionAllDecorator('hg.admin')
1068 def ips(self):
1069 def ips(self):
1069 _ = self.request.translate
1070 _ = self.request.translate
1070 c = self.load_default_context()
1071 c = self.load_default_context()
1071 c.user = self.db_user
1072 c.user = self.db_user
1072
1073
1073 c.active = 'ips'
1074 c.active = 'ips'
1074 c.user_ip_map = UserIpMap.query() \
1075 c.user_ip_map = UserIpMap.query() \
1075 .filter(UserIpMap.user == c.user).all()
1076 .filter(UserIpMap.user == c.user).all()
1076
1077
1077 c.inherit_default_ips = c.user.inherit_default_permissions
1078 c.inherit_default_ips = c.user.inherit_default_permissions
1078 c.default_user_ip_map = UserIpMap.query() \
1079 c.default_user_ip_map = UserIpMap.query() \
1079 .filter(UserIpMap.user == User.get_default_user()).all()
1080 .filter(UserIpMap.user == User.get_default_user()).all()
1080
1081
1081 return self._get_template_context(c)
1082 return self._get_template_context(c)
1082
1083
1083 @LoginRequired()
1084 @LoginRequired()
1084 @HasPermissionAllDecorator('hg.admin')
1085 @HasPermissionAllDecorator('hg.admin')
1085 @CSRFRequired()
1086 @CSRFRequired()
1086 # NOTE(marcink): this view is allowed for default users, as we can
1087 # NOTE(marcink): this view is allowed for default users, as we can
1087 # edit their IP white list
1088 # edit their IP white list
1088 def ips_add(self):
1089 def ips_add(self):
1089 _ = self.request.translate
1090 _ = self.request.translate
1090 c = self.load_default_context()
1091 c = self.load_default_context()
1091
1092
1092 user_id = self.db_user_id
1093 user_id = self.db_user_id
1093 c.user = self.db_user
1094 c.user = self.db_user
1094
1095
1095 user_model = UserModel()
1096 user_model = UserModel()
1096 desc = self.request.POST.get('description')
1097 desc = self.request.POST.get('description')
1097 try:
1098 try:
1098 ip_list = user_model.parse_ip_range(
1099 ip_list = user_model.parse_ip_range(
1099 self.request.POST.get('new_ip'))
1100 self.request.POST.get('new_ip'))
1100 except Exception as e:
1101 except Exception as e:
1101 ip_list = []
1102 ip_list = []
1102 log.exception("Exception during ip saving")
1103 log.exception("Exception during ip saving")
1103 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1104 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1104 category='error')
1105 category='error')
1105 added = []
1106 added = []
1106 user_data = c.user.get_api_data()
1107 user_data = c.user.get_api_data()
1107 for ip in ip_list:
1108 for ip in ip_list:
1108 try:
1109 try:
1109 form = UserExtraIpForm(self.request.translate)()
1110 form = UserExtraIpForm(self.request.translate)()
1110 data = form.to_python({'ip': ip})
1111 data = form.to_python({'ip': ip})
1111 ip = data['ip']
1112 ip = data['ip']
1112
1113
1113 user_model.add_extra_ip(c.user.user_id, ip, desc)
1114 user_model.add_extra_ip(c.user.user_id, ip, desc)
1114 audit_logger.store_web(
1115 audit_logger.store_web(
1115 'user.edit.ip.add',
1116 'user.edit.ip.add',
1116 action_data={'ip': ip, 'user': user_data},
1117 action_data={'ip': ip, 'user': user_data},
1117 user=self._rhodecode_user)
1118 user=self._rhodecode_user)
1118 Session().commit()
1119 Session().commit()
1119 added.append(ip)
1120 added.append(ip)
1120 except formencode.Invalid as error:
1121 except formencode.Invalid as error:
1121 msg = error.unpack_errors()['ip']
1122 msg = error.unpack_errors()['ip']
1122 h.flash(msg, category='error')
1123 h.flash(msg, category='error')
1123 except Exception:
1124 except Exception:
1124 log.exception("Exception during ip saving")
1125 log.exception("Exception during ip saving")
1125 h.flash(_('An error occurred during ip saving'),
1126 h.flash(_('An error occurred during ip saving'),
1126 category='error')
1127 category='error')
1127 if added:
1128 if added:
1128 h.flash(
1129 h.flash(
1129 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1130 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1130 category='success')
1131 category='success')
1131 if 'default_user' in self.request.POST:
1132 if 'default_user' in self.request.POST:
1132 # case for editing global IP list we do it for 'DEFAULT' user
1133 # case for editing global IP list we do it for 'DEFAULT' user
1133 raise HTTPFound(h.route_path('admin_permissions_ips'))
1134 raise HTTPFound(h.route_path('admin_permissions_ips'))
1134 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1135 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1135
1136
1136 @LoginRequired()
1137 @LoginRequired()
1137 @HasPermissionAllDecorator('hg.admin')
1138 @HasPermissionAllDecorator('hg.admin')
1138 @CSRFRequired()
1139 @CSRFRequired()
1139 # NOTE(marcink): this view is allowed for default users, as we can
1140 # NOTE(marcink): this view is allowed for default users, as we can
1140 # edit their IP white list
1141 # edit their IP white list
1141 def ips_delete(self):
1142 def ips_delete(self):
1142 _ = self.request.translate
1143 _ = self.request.translate
1143 c = self.load_default_context()
1144 c = self.load_default_context()
1144
1145
1145 user_id = self.db_user_id
1146 user_id = self.db_user_id
1146 c.user = self.db_user
1147 c.user = self.db_user
1147
1148
1148 ip_id = self.request.POST.get('del_ip_id')
1149 ip_id = self.request.POST.get('del_ip_id')
1149 user_model = UserModel()
1150 user_model = UserModel()
1150 user_data = c.user.get_api_data()
1151 user_data = c.user.get_api_data()
1151 ip = UserIpMap.query().get(ip_id).ip_addr
1152 ip = UserIpMap.query().get(ip_id).ip_addr
1152 user_model.delete_extra_ip(c.user.user_id, ip_id)
1153 user_model.delete_extra_ip(c.user.user_id, ip_id)
1153 audit_logger.store_web(
1154 audit_logger.store_web(
1154 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1155 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1155 user=self._rhodecode_user)
1156 user=self._rhodecode_user)
1156 Session().commit()
1157 Session().commit()
1157 h.flash(_("Removed ip address from user whitelist"), category='success')
1158 h.flash(_("Removed ip address from user whitelist"), category='success')
1158
1159
1159 if 'default_user' in self.request.POST:
1160 if 'default_user' in self.request.POST:
1160 # case for editing global IP list we do it for 'DEFAULT' user
1161 # case for editing global IP list we do it for 'DEFAULT' user
1161 raise HTTPFound(h.route_path('admin_permissions_ips'))
1162 raise HTTPFound(h.route_path('admin_permissions_ips'))
1162 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1163 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1163
1164
1164 @LoginRequired()
1165 @LoginRequired()
1165 @HasPermissionAllDecorator('hg.admin')
1166 @HasPermissionAllDecorator('hg.admin')
1166 def groups_management(self):
1167 def groups_management(self):
1167 c = self.load_default_context()
1168 c = self.load_default_context()
1168 c.user = self.db_user
1169 c.user = self.db_user
1169 c.data = c.user.group_member
1170 c.data = c.user.group_member
1170
1171
1171 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1172 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1172 for group in c.user.group_member]
1173 for group in c.user.group_member]
1173 c.groups = ext_json.str_json(groups)
1174 c.groups = ext_json.str_json(groups)
1174 c.active = 'groups'
1175 c.active = 'groups'
1175
1176
1176 return self._get_template_context(c)
1177 return self._get_template_context(c)
1177
1178
1178 @LoginRequired()
1179 @LoginRequired()
1179 @HasPermissionAllDecorator('hg.admin')
1180 @HasPermissionAllDecorator('hg.admin')
1180 @CSRFRequired()
1181 @CSRFRequired()
1181 def groups_management_updates(self):
1182 def groups_management_updates(self):
1182 _ = self.request.translate
1183 _ = self.request.translate
1183 c = self.load_default_context()
1184 c = self.load_default_context()
1184
1185
1185 user_id = self.db_user_id
1186 user_id = self.db_user_id
1186 c.user = self.db_user
1187 c.user = self.db_user
1187
1188
1188 user_groups = set(self.request.POST.getall('users_group_id'))
1189 user_groups = set(self.request.POST.getall('users_group_id'))
1189 user_groups_objects = []
1190 user_groups_objects = []
1190
1191
1191 for ugid in user_groups:
1192 for ugid in user_groups:
1192 user_groups_objects.append(
1193 user_groups_objects.append(
1193 UserGroupModel().get_group(safe_int(ugid)))
1194 UserGroupModel().get_group(safe_int(ugid)))
1194 user_group_model = UserGroupModel()
1195 user_group_model = UserGroupModel()
1195 added_to_groups, removed_from_groups = \
1196 added_to_groups, removed_from_groups = \
1196 user_group_model.change_groups(c.user, user_groups_objects)
1197 user_group_model.change_groups(c.user, user_groups_objects)
1197
1198
1198 user_data = c.user.get_api_data()
1199 user_data = c.user.get_api_data()
1199 for user_group_id in added_to_groups:
1200 for user_group_id in added_to_groups:
1200 user_group = UserGroup.get(user_group_id)
1201 user_group = UserGroup.get(user_group_id)
1201 old_values = user_group.get_api_data()
1202 old_values = user_group.get_api_data()
1202 audit_logger.store_web(
1203 audit_logger.store_web(
1203 'user_group.edit.member.add',
1204 'user_group.edit.member.add',
1204 action_data={'user': user_data, 'old_data': old_values},
1205 action_data={'user': user_data, 'old_data': old_values},
1205 user=self._rhodecode_user)
1206 user=self._rhodecode_user)
1206
1207
1207 for user_group_id in removed_from_groups:
1208 for user_group_id in removed_from_groups:
1208 user_group = UserGroup.get(user_group_id)
1209 user_group = UserGroup.get(user_group_id)
1209 old_values = user_group.get_api_data()
1210 old_values = user_group.get_api_data()
1210 audit_logger.store_web(
1211 audit_logger.store_web(
1211 'user_group.edit.member.delete',
1212 'user_group.edit.member.delete',
1212 action_data={'user': user_data, 'old_data': old_values},
1213 action_data={'user': user_data, 'old_data': old_values},
1213 user=self._rhodecode_user)
1214 user=self._rhodecode_user)
1214
1215
1215 Session().commit()
1216 Session().commit()
1216 c.active = 'user_groups_management'
1217 c.active = 'user_groups_management'
1217 h.flash(_("Groups successfully changed"), category='success')
1218 h.flash(_("Groups successfully changed"), category='success')
1218
1219
1219 return HTTPFound(h.route_path(
1220 return HTTPFound(h.route_path(
1220 'edit_user_groups_management', user_id=user_id))
1221 'edit_user_groups_management', user_id=user_id))
1221
1222
1222 @LoginRequired()
1223 @LoginRequired()
1223 @HasPermissionAllDecorator('hg.admin')
1224 @HasPermissionAllDecorator('hg.admin')
1224 def user_audit_logs(self):
1225 def user_audit_logs(self):
1225 _ = self.request.translate
1226 _ = self.request.translate
1226 c = self.load_default_context()
1227 c = self.load_default_context()
1227 c.user = self.db_user
1228 c.user = self.db_user
1228
1229
1229 c.active = 'audit'
1230 c.active = 'audit'
1230
1231
1231 p = safe_int(self.request.GET.get('page', 1), 1)
1232 p = safe_int(self.request.GET.get('page', 1), 1)
1232
1233
1233 filter_term = self.request.GET.get('filter')
1234 filter_term = self.request.GET.get('filter')
1234 user_log = UserModel().get_user_log(c.user, filter_term)
1235 user_log = UserModel().get_user_log(c.user, filter_term)
1235
1236
1236 def url_generator(page_num):
1237 def url_generator(page_num):
1237 query_params = {
1238 query_params = {
1238 'page': page_num
1239 'page': page_num
1239 }
1240 }
1240 if filter_term:
1241 if filter_term:
1241 query_params['filter'] = filter_term
1242 query_params['filter'] = filter_term
1242 return self.request.current_route_path(_query=query_params)
1243 return self.request.current_route_path(_query=query_params)
1243
1244
1244 c.audit_logs = SqlPage(
1245 c.audit_logs = SqlPage(
1245 user_log, page=p, items_per_page=10, url_maker=url_generator)
1246 user_log, page=p, items_per_page=10, url_maker=url_generator)
1246 c.filter_term = filter_term
1247 c.filter_term = filter_term
1247 return self._get_template_context(c)
1248 return self._get_template_context(c)
1248
1249
1249 @LoginRequired()
1250 @LoginRequired()
1250 @HasPermissionAllDecorator('hg.admin')
1251 @HasPermissionAllDecorator('hg.admin')
1251 def user_audit_logs_download(self):
1252 def user_audit_logs_download(self):
1252 _ = self.request.translate
1253 _ = self.request.translate
1253 c = self.load_default_context()
1254 c = self.load_default_context()
1254 c.user = self.db_user
1255 c.user = self.db_user
1255
1256
1256 user_log = UserModel().get_user_log(c.user, filter_term=None)
1257 user_log = UserModel().get_user_log(c.user, filter_term=None)
1257
1258
1258 audit_log_data = {}
1259 audit_log_data = {}
1259 for entry in user_log:
1260 for entry in user_log:
1260 audit_log_data[entry.user_log_id] = entry.get_dict()
1261 audit_log_data[entry.user_log_id] = entry.get_dict()
1261
1262
1262 response = Response(ext_json.formatted_str_json(audit_log_data))
1263 response = Response(ext_json.formatted_str_json(audit_log_data))
1263 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1264 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1264 response.content_type = 'application/json'
1265 response.content_type = 'application/json'
1265
1266
1266 return response
1267 return response
1267
1268
1268 @LoginRequired()
1269 @LoginRequired()
1269 @HasPermissionAllDecorator('hg.admin')
1270 @HasPermissionAllDecorator('hg.admin')
1270 def user_perms_summary(self):
1271 def user_perms_summary(self):
1271 _ = self.request.translate
1272 _ = self.request.translate
1272 c = self.load_default_context()
1273 c = self.load_default_context()
1273 c.user = self.db_user
1274 c.user = self.db_user
1274
1275
1275 c.active = 'perms_summary'
1276 c.active = 'perms_summary'
1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277
1278
1278 return self._get_template_context(c)
1279 return self._get_template_context(c)
1279
1280
1280 @LoginRequired()
1281 @LoginRequired()
1281 @HasPermissionAllDecorator('hg.admin')
1282 @HasPermissionAllDecorator('hg.admin')
1282 def user_perms_summary_json(self):
1283 def user_perms_summary_json(self):
1283 self.load_default_context()
1284 self.load_default_context()
1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285
1286
1286 return perm_user.permissions
1287 return perm_user.permissions
1287
1288
1288 @LoginRequired()
1289 @LoginRequired()
1289 @HasPermissionAllDecorator('hg.admin')
1290 @HasPermissionAllDecorator('hg.admin')
1290 def user_caches(self):
1291 def user_caches(self):
1291 _ = self.request.translate
1292 _ = self.request.translate
1292 c = self.load_default_context()
1293 c = self.load_default_context()
1293 c.user = self.db_user
1294 c.user = self.db_user
1294
1295
1295 c.active = 'caches'
1296 c.active = 'caches'
1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297
1298
1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 c.backend = c.region.backend
1301 c.backend = c.region.backend
1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302
1303
1303 return self._get_template_context(c)
1304 return self._get_template_context(c)
1304
1305
1305 @LoginRequired()
1306 @LoginRequired()
1306 @HasPermissionAllDecorator('hg.admin')
1307 @HasPermissionAllDecorator('hg.admin')
1307 @CSRFRequired()
1308 @CSRFRequired()
1308 def user_caches_update(self):
1309 def user_caches_update(self):
1309 _ = self.request.translate
1310 _ = self.request.translate
1310 c = self.load_default_context()
1311 c = self.load_default_context()
1311 c.user = self.db_user
1312 c.user = self.db_user
1312
1313
1313 c.active = 'caches'
1314 c.active = 'caches'
1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315
1316
1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318
1319
1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320
1321
1321 return HTTPFound(h.route_path(
1322 return HTTPFound(h.route_path(
1322 'edit_user_caches', user_id=c.user.user_id))
1323 'edit_user_caches', user_id=c.user.user_id))
@@ -1,106 +1,106 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
20 import os
21
21
22 from pyramid.events import ApplicationCreated
22 from pyramid.events import ApplicationCreated
23 from pyramid.settings import asbool
23 from pyramid.settings import asbool
24
24
25 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import str_json
27 from rhodecode.lib.str_utils import safe_str
27
28
28
29
29
30 def url_gen(request):
30 def url_gen(request):
31 registry = request.registry
31 registry = request.registry
32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
33 ws_url = registry.settings.get('channelstream.ws_url', '')
33 ws_url = registry.settings.get('channelstream.ws_url', '')
34 proxy_url = request.route_url('channelstream_proxy')
34 proxy_url = request.route_url('channelstream_proxy')
35 urls = {
35 urls = {
36 'connect': request.route_path('channelstream_connect'),
36 'connect': request.route_path('channelstream_connect'),
37 'subscribe': request.route_path('channelstream_subscribe'),
37 'subscribe': request.route_path('channelstream_subscribe'),
38 'longpoll': longpoll_url or proxy_url,
38 'longpoll': longpoll_url or proxy_url,
39 'ws': ws_url or proxy_url.replace('http', 'ws')
39 'ws': ws_url or proxy_url.replace('http', 'ws')
40 }
40 }
41 return safe_str(json.dumps(urls))
41 return str_json(urls)
42
42
43
43
44 PLUGIN_DEFINITION = {
44 PLUGIN_DEFINITION = {
45 'name': 'channelstream',
45 'name': 'channelstream',
46 'config': {
46 'config': {
47 'javascript': [],
47 'javascript': [],
48 'css': [],
48 'css': [],
49 'template_hooks': {
49 'template_hooks': {
50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
51 },
51 },
52 'url_gen': url_gen,
52 'url_gen': url_gen,
53 'static': None,
53 'static': None,
54 'enabled': False,
54 'enabled': False,
55 'server': '',
55 'server': '',
56 'secret': ''
56 'secret': ''
57 }
57 }
58 }
58 }
59
59
60
60
61 def maybe_create_history_store(event):
61 def maybe_create_history_store(event):
62 # create plugin history location
62 # create plugin history location
63 settings = event.app.registry.settings
63 settings = event.app.registry.settings
64 history_dir = settings.get('channelstream.history.location', '')
64 history_dir = settings.get('channelstream.history.location', '')
65 if history_dir and not os.path.exists(history_dir):
65 if history_dir and not os.path.exists(history_dir):
66 os.makedirs(history_dir, 0o750)
66 os.makedirs(history_dir, 0o750)
67
67
68
68
69 def includeme(config):
69 def includeme(config):
70 from rhodecode.apps.channelstream.views import ChannelstreamView
70 from rhodecode.apps.channelstream.views import ChannelstreamView
71
71
72 settings = config.registry.settings
72 settings = config.registry.settings
73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
74 settings.get('channelstream.enabled'))
74 settings.get('channelstream.enabled'))
75 PLUGIN_DEFINITION['config']['server'] = settings.get(
75 PLUGIN_DEFINITION['config']['server'] = settings.get(
76 'channelstream.server', '')
76 'channelstream.server', '')
77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
78 'channelstream.secret', '')
78 'channelstream.secret', '')
79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
80 'channelstream.history.location', '')
80 'channelstream.history.location', '')
81 config.register_rhodecode_plugin(
81 config.register_rhodecode_plugin(
82 PLUGIN_DEFINITION['name'],
82 PLUGIN_DEFINITION['name'],
83 PLUGIN_DEFINITION['config']
83 PLUGIN_DEFINITION['config']
84 )
84 )
85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
86
86
87 config.add_route(
87 config.add_route(
88 name='channelstream_connect',
88 name='channelstream_connect',
89 pattern=ADMIN_PREFIX + '/channelstream/connect')
89 pattern=ADMIN_PREFIX + '/channelstream/connect')
90 config.add_view(
90 config.add_view(
91 ChannelstreamView,
91 ChannelstreamView,
92 attr='channelstream_connect',
92 attr='channelstream_connect',
93 route_name='channelstream_connect', renderer='json_ext')
93 route_name='channelstream_connect', renderer='json_ext')
94
94
95 config.add_route(
95 config.add_route(
96 name='channelstream_subscribe',
96 name='channelstream_subscribe',
97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
98 config.add_view(
98 config.add_view(
99 ChannelstreamView,
99 ChannelstreamView,
100 attr='channelstream_subscribe',
100 attr='channelstream_subscribe',
101 route_name='channelstream_subscribe', renderer='json_ext')
101 route_name='channelstream_subscribe', renderer='json_ext')
102
102
103 config.add_route(
103 config.add_route(
104 name='channelstream_proxy',
104 name='channelstream_proxy',
105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
106
106
@@ -1,147 +1,147 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 from rhodecode.config import routing_links
20 from rhodecode.config import routing_links
21
21
22
22
23 class VCSCallPredicate(object):
23 class VCSCallPredicate(object):
24 def __init__(self, val, config):
24 def __init__(self, val, config):
25 self.val = val
25 self.val = val
26
26
27 def text(self):
27 def text(self):
28 return 'vcs_call route = %s' % self.val
28 return f'vcs_call route = {self.val}'
29
29
30 phash = text
30 phash = text
31
31
32 def __call__(self, info, request):
32 def __call__(self, info, request):
33 if hasattr(request, 'vcs_call'):
33 if hasattr(request, 'vcs_call'):
34 # skip vcs calls
34 # skip vcs calls
35 return False
35 return False
36
36
37 return True
37 return True
38
38
39
39
40 def includeme(config):
40 def includeme(config):
41 from rhodecode.apps.home.views import HomeView
41 from rhodecode.apps.home.views import HomeView
42
42
43 config.add_route_predicate(
43 config.add_route_predicate(
44 'skip_vcs_call', VCSCallPredicate)
44 'skip_vcs_call', VCSCallPredicate)
45
45
46 config.add_route(
46 config.add_route(
47 name='home',
47 name='home',
48 pattern='/')
48 pattern='/')
49 config.add_view(
49 config.add_view(
50 HomeView,
50 HomeView,
51 attr='main_page',
51 attr='main_page',
52 route_name='home', request_method='GET',
52 route_name='home', request_method='GET',
53 renderer='rhodecode:templates/index.mako')
53 renderer='rhodecode:templates/index.mako')
54
54
55 config.add_route(
55 config.add_route(
56 name='main_page_repos_data',
56 name='main_page_repos_data',
57 pattern='/_home_repos')
57 pattern='/_home_repos')
58 config.add_view(
58 config.add_view(
59 HomeView,
59 HomeView,
60 attr='main_page_repos_data',
60 attr='main_page_repos_data',
61 route_name='main_page_repos_data',
61 route_name='main_page_repos_data',
62 request_method='GET', renderer='json_ext', xhr=True)
62 request_method='GET', renderer='json_ext', xhr=True)
63
63
64 config.add_route(
64 config.add_route(
65 name='main_page_repo_groups_data',
65 name='main_page_repo_groups_data',
66 pattern='/_home_repo_groups')
66 pattern='/_home_repo_groups')
67 config.add_view(
67 config.add_view(
68 HomeView,
68 HomeView,
69 attr='main_page_repo_groups_data',
69 attr='main_page_repo_groups_data',
70 route_name='main_page_repo_groups_data',
70 route_name='main_page_repo_groups_data',
71 request_method='GET', renderer='json_ext', xhr=True)
71 request_method='GET', renderer='json_ext', xhr=True)
72
72
73 config.add_route(
73 config.add_route(
74 name='user_autocomplete_data',
74 name='user_autocomplete_data',
75 pattern='/_users')
75 pattern='/_users')
76 config.add_view(
76 config.add_view(
77 HomeView,
77 HomeView,
78 attr='user_autocomplete_data',
78 attr='user_autocomplete_data',
79 route_name='user_autocomplete_data', request_method='GET',
79 route_name='user_autocomplete_data', request_method='GET',
80 renderer='json_ext', xhr=True)
80 renderer='json_ext', xhr=True)
81
81
82 config.add_route(
82 config.add_route(
83 name='user_group_autocomplete_data',
83 name='user_group_autocomplete_data',
84 pattern='/_user_groups')
84 pattern='/_user_groups')
85 config.add_view(
85 config.add_view(
86 HomeView,
86 HomeView,
87 attr='user_group_autocomplete_data',
87 attr='user_group_autocomplete_data',
88 route_name='user_group_autocomplete_data', request_method='GET',
88 route_name='user_group_autocomplete_data', request_method='GET',
89 renderer='json_ext', xhr=True)
89 renderer='json_ext', xhr=True)
90
90
91 config.add_route(
91 config.add_route(
92 name='repo_list_data',
92 name='repo_list_data',
93 pattern='/_repos')
93 pattern='/_repos')
94 config.add_view(
94 config.add_view(
95 HomeView,
95 HomeView,
96 attr='repo_list_data',
96 attr='repo_list_data',
97 route_name='repo_list_data', request_method='GET',
97 route_name='repo_list_data', request_method='GET',
98 renderer='json_ext', xhr=True)
98 renderer='json_ext', xhr=True)
99
99
100 config.add_route(
100 config.add_route(
101 name='repo_group_list_data',
101 name='repo_group_list_data',
102 pattern='/_repo_groups')
102 pattern='/_repo_groups')
103 config.add_view(
103 config.add_view(
104 HomeView,
104 HomeView,
105 attr='repo_group_list_data',
105 attr='repo_group_list_data',
106 route_name='repo_group_list_data', request_method='GET',
106 route_name='repo_group_list_data', request_method='GET',
107 renderer='json_ext', xhr=True)
107 renderer='json_ext', xhr=True)
108
108
109 config.add_route(
109 config.add_route(
110 name='goto_switcher_data',
110 name='goto_switcher_data',
111 pattern='/_goto_data')
111 pattern='/_goto_data')
112 config.add_view(
112 config.add_view(
113 HomeView,
113 HomeView,
114 attr='goto_switcher_data',
114 attr='goto_switcher_data',
115 route_name='goto_switcher_data', request_method='GET',
115 route_name='goto_switcher_data', request_method='GET',
116 renderer='json_ext', xhr=True)
116 renderer='json_ext', xhr=True)
117
117
118 config.add_route(
118 config.add_route(
119 name='markup_preview',
119 name='markup_preview',
120 pattern='/_markup_preview')
120 pattern='/_markup_preview')
121 config.add_view(
121 config.add_view(
122 HomeView,
122 HomeView,
123 attr='markup_preview',
123 attr='markup_preview',
124 route_name='markup_preview', request_method='POST',
124 route_name='markup_preview', request_method='POST',
125 renderer='string', xhr=True)
125 renderer='string', xhr=True)
126
126
127 config.add_route(
127 config.add_route(
128 name='file_preview',
128 name='file_preview',
129 pattern='/_file_preview')
129 pattern='/_file_preview')
130 config.add_view(
130 config.add_view(
131 HomeView,
131 HomeView,
132 attr='file_preview',
132 attr='file_preview',
133 route_name='file_preview', request_method='POST',
133 route_name='file_preview', request_method='POST',
134 renderer='string', xhr=True)
134 renderer='string', xhr=True)
135
135
136 config.add_route(
136 config.add_route(
137 name='store_user_session_value',
137 name='store_user_session_value',
138 pattern='/_store_session_attr')
138 pattern='/_store_session_attr')
139 config.add_view(
139 config.add_view(
140 HomeView,
140 HomeView,
141 attr='store_user_session_attr',
141 attr='store_user_session_attr',
142 route_name='store_user_session_value', request_method='POST',
142 route_name='store_user_session_value', request_method='POST',
143 renderer='string', xhr=True)
143 renderer='string', xhr=True)
144
144
145 # register our static links via redirection mechanism
145 # register our static links via redirection mechanism
146 routing_links.connect_redirection_links(config)
146 routing_links.connect_redirection_links(config)
147
147
@@ -1,856 +1,856 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound
25 from pyramid.httpexceptions import HTTPNotFound
26
26
27 from rhodecode.apps._base import BaseAppView, DataGridAppView
27 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 HasRepoGroupPermissionAny, AuthUser)
31 HasRepoGroupPermissionAny, AuthUser)
32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int, safe_str
34 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
35 from rhodecode.lib.vcs.nodes import FileNode
35 from rhodecode.lib.vcs.nodes import FileNode
36 from rhodecode.model.db import (
36 from rhodecode.model.db import (
37 func, true, or_, case, cast, in_filter_generator, String, Session,
37 func, true, or_, case, cast, in_filter_generator, String, Session,
38 Repository, RepoGroup, User, UserGroup, PullRequest)
38 Repository, RepoGroup, User, UserGroup, PullRequest)
39 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo_group import RepoGroupModel
40 from rhodecode.model.repo_group import RepoGroupModel
41 from rhodecode.model.user import UserModel
41 from rhodecode.model.user import UserModel
42 from rhodecode.model.user_group import UserGroupModel
42 from rhodecode.model.user_group import UserGroupModel
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class HomeView(BaseAppView, DataGridAppView):
47 class HomeView(BaseAppView, DataGridAppView):
48
48
49 def load_default_context(self):
49 def load_default_context(self):
50 c = self._get_local_tmpl_context()
50 c = self._get_local_tmpl_context()
51 c.user = c.auth_user.get_instance()
51 c.user = c.auth_user.get_instance()
52 return c
52 return c
53
53
54 @LoginRequired()
54 @LoginRequired()
55 def user_autocomplete_data(self):
55 def user_autocomplete_data(self):
56 self.load_default_context()
56 self.load_default_context()
57 query = self.request.GET.get('query')
57 query = self.request.GET.get('query')
58 active = str2bool(self.request.GET.get('active') or True)
58 active = str2bool(self.request.GET.get('active') or True)
59 include_groups = str2bool(self.request.GET.get('user_groups'))
59 include_groups = str2bool(self.request.GET.get('user_groups'))
60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
62
62
63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
64 query, active, include_groups)
64 query, active, include_groups)
65
65
66 _users = UserModel().get_users(
66 _users = UserModel().get_users(
67 name_contains=query, only_active=active)
67 name_contains=query, only_active=active)
68
68
69 def maybe_skip_default_user(usr):
69 def maybe_skip_default_user(usr):
70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
71 return False
71 return False
72 return True
72 return True
73 _users = filter(maybe_skip_default_user, _users)
73 _users = filter(maybe_skip_default_user, _users)
74
74
75 if include_groups:
75 if include_groups:
76 # extend with user groups
76 # extend with user groups
77 _user_groups = UserGroupModel().get_user_groups(
77 _user_groups = UserGroupModel().get_user_groups(
78 name_contains=query, only_active=active,
78 name_contains=query, only_active=active,
79 expand_groups=expand_groups)
79 expand_groups=expand_groups)
80 _users = _users + _user_groups
80 _users = _users + _user_groups
81
81
82 return {'suggestions': _users}
82 return {'suggestions': _users}
83
83
84 @LoginRequired()
84 @LoginRequired()
85 @NotAnonymous()
85 @NotAnonymous()
86 def user_group_autocomplete_data(self):
86 def user_group_autocomplete_data(self):
87 self.load_default_context()
87 self.load_default_context()
88 query = self.request.GET.get('query')
88 query = self.request.GET.get('query')
89 active = str2bool(self.request.GET.get('active') or True)
89 active = str2bool(self.request.GET.get('active') or True)
90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
91
91
92 log.debug('generating user group list, query:%s, active:%s',
92 log.debug('generating user group list, query:%s, active:%s',
93 query, active)
93 query, active)
94
94
95 _user_groups = UserGroupModel().get_user_groups(
95 _user_groups = UserGroupModel().get_user_groups(
96 name_contains=query, only_active=active,
96 name_contains=query, only_active=active,
97 expand_groups=expand_groups)
97 expand_groups=expand_groups)
98 _user_groups = _user_groups
98 _user_groups = _user_groups
99
99
100 return {'suggestions': _user_groups}
100 return {'suggestions': _user_groups}
101
101
102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
103 org_query = name_contains
103 org_query = name_contains
104 allowed_ids = self._rhodecode_user.repo_acl_ids(
104 allowed_ids = self._rhodecode_user.repo_acl_ids(
105 ['repository.read', 'repository.write', 'repository.admin'],
105 ['repository.read', 'repository.write', 'repository.admin'],
106 cache=True, name_filter=name_contains) or [-1]
106 cache=True, name_filter=name_contains) or [-1]
107
107
108 query = Session().query(
108 query = Session().query(
109 Repository.repo_name,
109 Repository.repo_name,
110 Repository.repo_id,
110 Repository.repo_id,
111 Repository.repo_type,
111 Repository.repo_type,
112 Repository.private,
112 Repository.private,
113 )\
113 )\
114 .filter(Repository.archived.isnot(true()))\
114 .filter(Repository.archived.isnot(true()))\
115 .filter(or_(
115 .filter(or_(
116 # generate multiple IN to fix limitation problems
116 # generate multiple IN to fix limitation problems
117 *in_filter_generator(Repository.repo_id, allowed_ids)
117 *in_filter_generator(Repository.repo_id, allowed_ids)
118 ))
118 ))
119
119
120 query = query.order_by(case(
120 query = query.order_by(case(
121 [
121 [
122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
123 ],
123 ],
124 ))
124 ))
125 query = query.order_by(func.length(Repository.repo_name))
125 query = query.order_by(func.length(Repository.repo_name))
126 query = query.order_by(Repository.repo_name)
126 query = query.order_by(Repository.repo_name)
127
127
128 if repo_type:
128 if repo_type:
129 query = query.filter(Repository.repo_type == repo_type)
129 query = query.filter(Repository.repo_type == repo_type)
130
130
131 if name_contains:
131 if name_contains:
132 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
132 ilike_expression = '%{}%'.format(safe_str(name_contains))
133 query = query.filter(
133 query = query.filter(
134 Repository.repo_name.ilike(ilike_expression))
134 Repository.repo_name.ilike(ilike_expression))
135 query = query.limit(limit)
135 query = query.limit(limit)
136
136
137 acl_iter = query
137 acl_iter = query
138
138
139 return [
139 return [
140 {
140 {
141 'id': obj.repo_name,
141 'id': obj.repo_name,
142 'value': org_query,
142 'value': org_query,
143 'value_display': obj.repo_name,
143 'value_display': obj.repo_name,
144 'text': obj.repo_name,
144 'text': obj.repo_name,
145 'type': 'repo',
145 'type': 'repo',
146 'repo_id': obj.repo_id,
146 'repo_id': obj.repo_id,
147 'repo_type': obj.repo_type,
147 'repo_type': obj.repo_type,
148 'private': obj.private,
148 'private': obj.private,
149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
150 }
150 }
151 for obj in acl_iter]
151 for obj in acl_iter]
152
152
153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
154 org_query = name_contains
154 org_query = name_contains
155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
156 ['group.read', 'group.write', 'group.admin'],
156 ['group.read', 'group.write', 'group.admin'],
157 cache=True, name_filter=name_contains) or [-1]
157 cache=True, name_filter=name_contains) or [-1]
158
158
159 query = Session().query(
159 query = Session().query(
160 RepoGroup.group_id,
160 RepoGroup.group_id,
161 RepoGroup.group_name,
161 RepoGroup.group_name,
162 )\
162 )\
163 .filter(or_(
163 .filter(or_(
164 # generate multiple IN to fix limitation problems
164 # generate multiple IN to fix limitation problems
165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
166 ))
166 ))
167
167
168 query = query.order_by(case(
168 query = query.order_by(case(
169 [
169 [
170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
171 ],
171 ],
172 ))
172 ))
173 query = query.order_by(func.length(RepoGroup.group_name))
173 query = query.order_by(func.length(RepoGroup.group_name))
174 query = query.order_by(RepoGroup.group_name)
174 query = query.order_by(RepoGroup.group_name)
175
175
176 if name_contains:
176 if name_contains:
177 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
177 ilike_expression = u'%{}%'.format(safe_str(name_contains))
178 query = query.filter(
178 query = query.filter(
179 RepoGroup.group_name.ilike(ilike_expression))
179 RepoGroup.group_name.ilike(ilike_expression))
180 query = query.limit(limit)
180 query = query.limit(limit)
181
181
182 acl_iter = query
182 acl_iter = query
183
183
184 return [
184 return [
185 {
185 {
186 'id': obj.group_name,
186 'id': obj.group_name,
187 'value': org_query,
187 'value': org_query,
188 'value_display': obj.group_name,
188 'value_display': obj.group_name,
189 'text': obj.group_name,
189 'text': obj.group_name,
190 'type': 'repo_group',
190 'type': 'repo_group',
191 'repo_group_id': obj.group_id,
191 'repo_group_id': obj.group_id,
192 'url': h.route_path(
192 'url': h.route_path(
193 'repo_group_home', repo_group_name=obj.group_name)
193 'repo_group_home', repo_group_name=obj.group_name)
194 }
194 }
195 for obj in acl_iter]
195 for obj in acl_iter]
196
196
197 def _get_user_list(self, name_contains=None, limit=20):
197 def _get_user_list(self, name_contains=None, limit=20):
198 org_query = name_contains
198 org_query = name_contains
199 if not name_contains:
199 if not name_contains:
200 return [], False
200 return [], False
201
201
202 # TODO(marcink): should all logged in users be allowed to search others?
202 # TODO(marcink): should all logged in users be allowed to search others?
203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
204 if not allowed_user_search:
204 if not allowed_user_search:
205 return [], False
205 return [], False
206
206
207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
208 if len(name_contains) != 1:
208 if len(name_contains) != 1:
209 return [], False
209 return [], False
210
210
211 name_contains = name_contains[0]
211 name_contains = name_contains[0]
212
212
213 query = User.query()\
213 query = User.query()\
214 .order_by(func.length(User.username))\
214 .order_by(func.length(User.username))\
215 .order_by(User.username) \
215 .order_by(User.username) \
216 .filter(User.username != User.DEFAULT_USER)
216 .filter(User.username != User.DEFAULT_USER)
217
217
218 if name_contains:
218 if name_contains:
219 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
219 ilike_expression = u'%{}%'.format(safe_str(name_contains))
220 query = query.filter(
220 query = query.filter(
221 User.username.ilike(ilike_expression))
221 User.username.ilike(ilike_expression))
222 query = query.limit(limit)
222 query = query.limit(limit)
223
223
224 acl_iter = query
224 acl_iter = query
225
225
226 return [
226 return [
227 {
227 {
228 'id': obj.user_id,
228 'id': obj.user_id,
229 'value': org_query,
229 'value': org_query,
230 'value_display': 'user: `{}`'.format(obj.username),
230 'value_display': 'user: `{}`'.format(obj.username),
231 'type': 'user',
231 'type': 'user',
232 'icon_link': h.gravatar_url(obj.email, 30),
232 'icon_link': h.gravatar_url(obj.email, 30),
233 'url': h.route_path(
233 'url': h.route_path(
234 'user_profile', username=obj.username)
234 'user_profile', username=obj.username)
235 }
235 }
236 for obj in acl_iter], True
236 for obj in acl_iter], True
237
237
238 def _get_user_groups_list(self, name_contains=None, limit=20):
238 def _get_user_groups_list(self, name_contains=None, limit=20):
239 org_query = name_contains
239 org_query = name_contains
240 if not name_contains:
240 if not name_contains:
241 return [], False
241 return [], False
242
242
243 # TODO(marcink): should all logged in users be allowed to search others?
243 # TODO(marcink): should all logged in users be allowed to search others?
244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
245 if not allowed_user_search:
245 if not allowed_user_search:
246 return [], False
246 return [], False
247
247
248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
249 if len(name_contains) != 1:
249 if len(name_contains) != 1:
250 return [], False
250 return [], False
251
251
252 name_contains = name_contains[0]
252 name_contains = name_contains[0]
253
253
254 query = UserGroup.query()\
254 query = UserGroup.query()\
255 .order_by(func.length(UserGroup.users_group_name))\
255 .order_by(func.length(UserGroup.users_group_name))\
256 .order_by(UserGroup.users_group_name)
256 .order_by(UserGroup.users_group_name)
257
257
258 if name_contains:
258 if name_contains:
259 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
259 ilike_expression = u'%{}%'.format(safe_str(name_contains))
260 query = query.filter(
260 query = query.filter(
261 UserGroup.users_group_name.ilike(ilike_expression))
261 UserGroup.users_group_name.ilike(ilike_expression))
262 query = query.limit(limit)
262 query = query.limit(limit)
263
263
264 acl_iter = query
264 acl_iter = query
265
265
266 return [
266 return [
267 {
267 {
268 'id': obj.users_group_id,
268 'id': obj.users_group_id,
269 'value': org_query,
269 'value': org_query,
270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
271 'type': 'user_group',
271 'type': 'user_group',
272 'url': h.route_path(
272 'url': h.route_path(
273 'user_group_profile', user_group_name=obj.users_group_name)
273 'user_group_profile', user_group_name=obj.users_group_name)
274 }
274 }
275 for obj in acl_iter], True
275 for obj in acl_iter], True
276
276
277 def _get_pull_request_list(self, name_contains=None, limit=20):
277 def _get_pull_request_list(self, name_contains=None, limit=20):
278 org_query = name_contains
278 org_query = name_contains
279 if not name_contains:
279 if not name_contains:
280 return [], False
280 return [], False
281
281
282 # TODO(marcink): should all logged in users be allowed to search others?
282 # TODO(marcink): should all logged in users be allowed to search others?
283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
284 if not allowed_user_search:
284 if not allowed_user_search:
285 return [], False
285 return [], False
286
286
287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
288 if len(name_contains) != 1:
288 if len(name_contains) != 1:
289 return [], False
289 return [], False
290
290
291 name_contains = name_contains[0]
291 name_contains = name_contains[0]
292
292
293 allowed_ids = self._rhodecode_user.repo_acl_ids(
293 allowed_ids = self._rhodecode_user.repo_acl_ids(
294 ['repository.read', 'repository.write', 'repository.admin'],
294 ['repository.read', 'repository.write', 'repository.admin'],
295 cache=True) or [-1]
295 cache=True) or [-1]
296
296
297 query = Session().query(
297 query = Session().query(
298 PullRequest.pull_request_id,
298 PullRequest.pull_request_id,
299 PullRequest.title,
299 PullRequest.title,
300 )
300 )
301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
302
302
303 query = query.filter(or_(
303 query = query.filter(or_(
304 # generate multiple IN to fix limitation problems
304 # generate multiple IN to fix limitation problems
305 *in_filter_generator(Repository.repo_id, allowed_ids)
305 *in_filter_generator(Repository.repo_id, allowed_ids)
306 ))
306 ))
307
307
308 query = query.order_by(PullRequest.pull_request_id)
308 query = query.order_by(PullRequest.pull_request_id)
309
309
310 if name_contains:
310 if name_contains:
311 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
311 ilike_expression = u'%{}%'.format(safe_str(name_contains))
312 query = query.filter(or_(
312 query = query.filter(or_(
313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
314 PullRequest.title.ilike(ilike_expression),
314 PullRequest.title.ilike(ilike_expression),
315 PullRequest.description.ilike(ilike_expression),
315 PullRequest.description.ilike(ilike_expression),
316 ))
316 ))
317
317
318 query = query.limit(limit)
318 query = query.limit(limit)
319
319
320 acl_iter = query
320 acl_iter = query
321
321
322 return [
322 return [
323 {
323 {
324 'id': obj.pull_request_id,
324 'id': obj.pull_request_id,
325 'value': org_query,
325 'value': org_query,
326 'value_display': 'pull request: `!{} - {}`'.format(
326 'value_display': 'pull request: `!{} - {}`'.format(
327 obj.pull_request_id, safe_str(obj.title[:50])),
327 obj.pull_request_id, safe_str(obj.title[:50])),
328 'type': 'pull_request',
328 'type': 'pull_request',
329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
330 }
330 }
331 for obj in acl_iter], True
331 for obj in acl_iter], True
332
332
333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
334 repo_name = repo_group_name = None
334 repo_name = repo_group_name = None
335 if repo:
335 if repo:
336 repo_name = repo.repo_name
336 repo_name = repo.repo_name
337 if repo_group:
337 if repo_group:
338 repo_group_name = repo_group.group_name
338 repo_group_name = repo_group.group_name
339
339
340 org_query = query
340 org_query = query
341 if not query or len(query) < 3 or not searcher:
341 if not query or len(query) < 3 or not searcher:
342 return [], False
342 return [], False
343
343
344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
345
345
346 if len(commit_hashes) != 1:
346 if len(commit_hashes) != 1:
347 return [], False
347 return [], False
348
348
349 commit_hash = commit_hashes[0]
349 commit_hash = commit_hashes[0]
350
350
351 result = searcher.search(
351 result = searcher.search(
352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
353 repo_name, repo_group_name, raise_on_exc=False)
353 repo_name, repo_group_name, raise_on_exc=False)
354
354
355 commits = []
355 commits = []
356 for entry in result['results']:
356 for entry in result['results']:
357 repo_data = {
357 repo_data = {
358 'repository_id': entry.get('repository_id'),
358 'repository_id': entry.get('repository_id'),
359 'repository_type': entry.get('repo_type'),
359 'repository_type': entry.get('repo_type'),
360 'repository_name': entry.get('repository'),
360 'repository_name': entry.get('repository'),
361 }
361 }
362
362
363 commit_entry = {
363 commit_entry = {
364 'id': entry['commit_id'],
364 'id': entry['commit_id'],
365 'value': org_query,
365 'value': org_query,
366 'value_display': '`{}` commit: {}'.format(
366 'value_display': '`{}` commit: {}'.format(
367 entry['repository'], entry['commit_id']),
367 entry['repository'], entry['commit_id']),
368 'type': 'commit',
368 'type': 'commit',
369 'repo': entry['repository'],
369 'repo': entry['repository'],
370 'repo_data': repo_data,
370 'repo_data': repo_data,
371
371
372 'url': h.route_path(
372 'url': h.route_path(
373 'repo_commit',
373 'repo_commit',
374 repo_name=entry['repository'], commit_id=entry['commit_id'])
374 repo_name=entry['repository'], commit_id=entry['commit_id'])
375 }
375 }
376
376
377 commits.append(commit_entry)
377 commits.append(commit_entry)
378 return commits, True
378 return commits, True
379
379
380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
381 repo_name = repo_group_name = None
381 repo_name = repo_group_name = None
382 if repo:
382 if repo:
383 repo_name = repo.repo_name
383 repo_name = repo.repo_name
384 if repo_group:
384 if repo_group:
385 repo_group_name = repo_group.group_name
385 repo_group_name = repo_group.group_name
386
386
387 org_query = query
387 org_query = query
388 if not query or len(query) < 3 or not searcher:
388 if not query or len(query) < 3 or not searcher:
389 return [], False
389 return [], False
390
390
391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
392 if len(paths_re) != 1:
392 if len(paths_re) != 1:
393 return [], False
393 return [], False
394
394
395 file_path = paths_re[0]
395 file_path = paths_re[0]
396
396
397 search_path = searcher.escape_specials(file_path)
397 search_path = searcher.escape_specials(file_path)
398 result = searcher.search(
398 result = searcher.search(
399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
400 repo_name, repo_group_name, raise_on_exc=False)
400 repo_name, repo_group_name, raise_on_exc=False)
401
401
402 files = []
402 files = []
403 for entry in result['results']:
403 for entry in result['results']:
404 repo_data = {
404 repo_data = {
405 'repository_id': entry.get('repository_id'),
405 'repository_id': entry.get('repository_id'),
406 'repository_type': entry.get('repo_type'),
406 'repository_type': entry.get('repo_type'),
407 'repository_name': entry.get('repository'),
407 'repository_name': entry.get('repository'),
408 }
408 }
409
409
410 file_entry = {
410 file_entry = {
411 'id': entry['commit_id'],
411 'id': entry['commit_id'],
412 'value': org_query,
412 'value': org_query,
413 'value_display': '`{}` file: {}'.format(
413 'value_display': '`{}` file: {}'.format(
414 entry['repository'], entry['file']),
414 entry['repository'], entry['file']),
415 'type': 'file',
415 'type': 'file',
416 'repo': entry['repository'],
416 'repo': entry['repository'],
417 'repo_data': repo_data,
417 'repo_data': repo_data,
418
418
419 'url': h.route_path(
419 'url': h.route_path(
420 'repo_files',
420 'repo_files',
421 repo_name=entry['repository'], commit_id=entry['commit_id'],
421 repo_name=entry['repository'], commit_id=entry['commit_id'],
422 f_path=entry['file'])
422 f_path=entry['file'])
423 }
423 }
424
424
425 files.append(file_entry)
425 files.append(file_entry)
426 return files, True
426 return files, True
427
427
428 @LoginRequired()
428 @LoginRequired()
429 def repo_list_data(self):
429 def repo_list_data(self):
430 _ = self.request.translate
430 _ = self.request.translate
431 self.load_default_context()
431 self.load_default_context()
432
432
433 query = self.request.GET.get('query')
433 query = self.request.GET.get('query')
434 repo_type = self.request.GET.get('repo_type')
434 repo_type = self.request.GET.get('repo_type')
435 log.debug('generating repo list, query:%s, repo_type:%s',
435 log.debug('generating repo list, query:%s, repo_type:%s',
436 query, repo_type)
436 query, repo_type)
437
437
438 res = []
438 res = []
439 repos = self._get_repo_list(query, repo_type=repo_type)
439 repos = self._get_repo_list(query, repo_type=repo_type)
440 if repos:
440 if repos:
441 res.append({
441 res.append({
442 'text': _('Repositories'),
442 'text': _('Repositories'),
443 'children': repos
443 'children': repos
444 })
444 })
445
445
446 data = {
446 data = {
447 'more': False,
447 'more': False,
448 'results': res
448 'results': res
449 }
449 }
450 return data
450 return data
451
451
452 @LoginRequired()
452 @LoginRequired()
453 def repo_group_list_data(self):
453 def repo_group_list_data(self):
454 _ = self.request.translate
454 _ = self.request.translate
455 self.load_default_context()
455 self.load_default_context()
456
456
457 query = self.request.GET.get('query')
457 query = self.request.GET.get('query')
458
458
459 log.debug('generating repo group list, query:%s',
459 log.debug('generating repo group list, query:%s',
460 query)
460 query)
461
461
462 res = []
462 res = []
463 repo_groups = self._get_repo_group_list(query)
463 repo_groups = self._get_repo_group_list(query)
464 if repo_groups:
464 if repo_groups:
465 res.append({
465 res.append({
466 'text': _('Repository Groups'),
466 'text': _('Repository Groups'),
467 'children': repo_groups
467 'children': repo_groups
468 })
468 })
469
469
470 data = {
470 data = {
471 'more': False,
471 'more': False,
472 'results': res
472 'results': res
473 }
473 }
474 return data
474 return data
475
475
476 def _get_default_search_queries(self, search_context, searcher, query):
476 def _get_default_search_queries(self, search_context, searcher, query):
477 if not searcher:
477 if not searcher:
478 return []
478 return []
479
479
480 is_es_6 = searcher.is_es_6
480 is_es_6 = searcher.is_es_6
481
481
482 queries = []
482 queries = []
483 repo_group_name, repo_name, repo_context = None, None, None
483 repo_group_name, repo_name, repo_context = None, None, None
484
484
485 # repo group context
485 # repo group context
486 if search_context.get('search_context[repo_group_name]'):
486 if search_context.get('search_context[repo_group_name]'):
487 repo_group_name = search_context.get('search_context[repo_group_name]')
487 repo_group_name = search_context.get('search_context[repo_group_name]')
488 if search_context.get('search_context[repo_name]'):
488 if search_context.get('search_context[repo_name]'):
489 repo_name = search_context.get('search_context[repo_name]')
489 repo_name = search_context.get('search_context[repo_name]')
490 repo_context = search_context.get('search_context[repo_view_type]')
490 repo_context = search_context.get('search_context[repo_view_type]')
491
491
492 if is_es_6 and repo_name:
492 if is_es_6 and repo_name:
493 # files
493 # files
494 def query_modifier():
494 def query_modifier():
495 qry = query
495 qry = query
496 return {'q': qry, 'type': 'content'}
496 return {'q': qry, 'type': 'content'}
497
497
498 label = u'File content search for `{}`'.format(h.escape(query))
498 label = u'File content search for `{}`'.format(h.escape(query))
499 file_qry = {
499 file_qry = {
500 'id': -10,
500 'id': -10,
501 'value': query,
501 'value': query,
502 'value_display': label,
502 'value_display': label,
503 'value_icon': '<i class="icon-code"></i>',
503 'value_icon': '<i class="icon-code"></i>',
504 'type': 'search',
504 'type': 'search',
505 'subtype': 'repo',
505 'subtype': 'repo',
506 'url': h.route_path('search_repo',
506 'url': h.route_path('search_repo',
507 repo_name=repo_name,
507 repo_name=repo_name,
508 _query=query_modifier())
508 _query=query_modifier())
509 }
509 }
510
510
511 # commits
511 # commits
512 def query_modifier():
512 def query_modifier():
513 qry = query
513 qry = query
514 return {'q': qry, 'type': 'commit'}
514 return {'q': qry, 'type': 'commit'}
515
515
516 label = u'Commit search for `{}`'.format(h.escape(query))
516 label = u'Commit search for `{}`'.format(h.escape(query))
517 commit_qry = {
517 commit_qry = {
518 'id': -20,
518 'id': -20,
519 'value': query,
519 'value': query,
520 'value_display': label,
520 'value_display': label,
521 'value_icon': '<i class="icon-history"></i>',
521 'value_icon': '<i class="icon-history"></i>',
522 'type': 'search',
522 'type': 'search',
523 'subtype': 'repo',
523 'subtype': 'repo',
524 'url': h.route_path('search_repo',
524 'url': h.route_path('search_repo',
525 repo_name=repo_name,
525 repo_name=repo_name,
526 _query=query_modifier())
526 _query=query_modifier())
527 }
527 }
528
528
529 if repo_context in ['commit', 'commits']:
529 if repo_context in ['commit', 'commits']:
530 queries.extend([commit_qry, file_qry])
530 queries.extend([commit_qry, file_qry])
531 elif repo_context in ['files', 'summary']:
531 elif repo_context in ['files', 'summary']:
532 queries.extend([file_qry, commit_qry])
532 queries.extend([file_qry, commit_qry])
533 else:
533 else:
534 queries.extend([commit_qry, file_qry])
534 queries.extend([commit_qry, file_qry])
535
535
536 elif is_es_6 and repo_group_name:
536 elif is_es_6 and repo_group_name:
537 # files
537 # files
538 def query_modifier():
538 def query_modifier():
539 qry = query
539 qry = query
540 return {'q': qry, 'type': 'content'}
540 return {'q': qry, 'type': 'content'}
541
541
542 label = u'File content search for `{}`'.format(query)
542 label = u'File content search for `{}`'.format(query)
543 file_qry = {
543 file_qry = {
544 'id': -30,
544 'id': -30,
545 'value': query,
545 'value': query,
546 'value_display': label,
546 'value_display': label,
547 'value_icon': '<i class="icon-code"></i>',
547 'value_icon': '<i class="icon-code"></i>',
548 'type': 'search',
548 'type': 'search',
549 'subtype': 'repo_group',
549 'subtype': 'repo_group',
550 'url': h.route_path('search_repo_group',
550 'url': h.route_path('search_repo_group',
551 repo_group_name=repo_group_name,
551 repo_group_name=repo_group_name,
552 _query=query_modifier())
552 _query=query_modifier())
553 }
553 }
554
554
555 # commits
555 # commits
556 def query_modifier():
556 def query_modifier():
557 qry = query
557 qry = query
558 return {'q': qry, 'type': 'commit'}
558 return {'q': qry, 'type': 'commit'}
559
559
560 label = u'Commit search for `{}`'.format(query)
560 label = u'Commit search for `{}`'.format(query)
561 commit_qry = {
561 commit_qry = {
562 'id': -40,
562 'id': -40,
563 'value': query,
563 'value': query,
564 'value_display': label,
564 'value_display': label,
565 'value_icon': '<i class="icon-history"></i>',
565 'value_icon': '<i class="icon-history"></i>',
566 'type': 'search',
566 'type': 'search',
567 'subtype': 'repo_group',
567 'subtype': 'repo_group',
568 'url': h.route_path('search_repo_group',
568 'url': h.route_path('search_repo_group',
569 repo_group_name=repo_group_name,
569 repo_group_name=repo_group_name,
570 _query=query_modifier())
570 _query=query_modifier())
571 }
571 }
572
572
573 if repo_context in ['commit', 'commits']:
573 if repo_context in ['commit', 'commits']:
574 queries.extend([commit_qry, file_qry])
574 queries.extend([commit_qry, file_qry])
575 elif repo_context in ['files', 'summary']:
575 elif repo_context in ['files', 'summary']:
576 queries.extend([file_qry, commit_qry])
576 queries.extend([file_qry, commit_qry])
577 else:
577 else:
578 queries.extend([commit_qry, file_qry])
578 queries.extend([commit_qry, file_qry])
579
579
580 # Global, not scoped
580 # Global, not scoped
581 if not queries:
581 if not queries:
582 queries.append(
582 queries.append(
583 {
583 {
584 'id': -1,
584 'id': -1,
585 'value': query,
585 'value': query,
586 'value_display': u'File content search for: `{}`'.format(query),
586 'value_display': u'File content search for: `{}`'.format(query),
587 'value_icon': '<i class="icon-code"></i>',
587 'value_icon': '<i class="icon-code"></i>',
588 'type': 'search',
588 'type': 'search',
589 'subtype': 'global',
589 'subtype': 'global',
590 'url': h.route_path('search',
590 'url': h.route_path('search',
591 _query={'q': query, 'type': 'content'})
591 _query={'q': query, 'type': 'content'})
592 })
592 })
593 queries.append(
593 queries.append(
594 {
594 {
595 'id': -2,
595 'id': -2,
596 'value': query,
596 'value': query,
597 'value_display': u'Commit search for: `{}`'.format(query),
597 'value_display': u'Commit search for: `{}`'.format(query),
598 'value_icon': '<i class="icon-history"></i>',
598 'value_icon': '<i class="icon-history"></i>',
599 'type': 'search',
599 'type': 'search',
600 'subtype': 'global',
600 'subtype': 'global',
601 'url': h.route_path('search',
601 'url': h.route_path('search',
602 _query={'q': query, 'type': 'commit'})
602 _query={'q': query, 'type': 'commit'})
603 })
603 })
604
604
605 return queries
605 return queries
606
606
607 @LoginRequired()
607 @LoginRequired()
608 def goto_switcher_data(self):
608 def goto_switcher_data(self):
609 c = self.load_default_context()
609 c = self.load_default_context()
610
610
611 _ = self.request.translate
611 _ = self.request.translate
612
612
613 query = self.request.GET.get('query')
613 query = self.request.GET.get('query')
614 log.debug('generating main filter data, query %s', query)
614 log.debug('generating main filter data, query %s', query)
615
615
616 res = []
616 res = []
617 if not query:
617 if not query:
618 return {'suggestions': res}
618 return {'suggestions': res}
619
619
620 def no_match(name):
620 def no_match(name):
621 return {
621 return {
622 'id': -1,
622 'id': -1,
623 'value': "",
623 'value': "",
624 'value_display': name,
624 'value_display': name,
625 'type': 'text',
625 'type': 'text',
626 'url': ""
626 'url': ""
627 }
627 }
628 searcher = searcher_from_config(self.request.registry.settings)
628 searcher = searcher_from_config(self.request.registry.settings)
629 has_specialized_search = False
629 has_specialized_search = False
630
630
631 # set repo context
631 # set repo context
632 repo = None
632 repo = None
633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
634 if repo_id:
634 if repo_id:
635 repo = Repository.get(repo_id)
635 repo = Repository.get(repo_id)
636
636
637 # set group context
637 # set group context
638 repo_group = None
638 repo_group = None
639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
640 if repo_group_id:
640 if repo_group_id:
641 repo_group = RepoGroup.get(repo_group_id)
641 repo_group = RepoGroup.get(repo_group_id)
642 prefix_match = False
642 prefix_match = False
643
643
644 # user: type search
644 # user: type search
645 if not prefix_match:
645 if not prefix_match:
646 users, prefix_match = self._get_user_list(query)
646 users, prefix_match = self._get_user_list(query)
647 if users:
647 if users:
648 has_specialized_search = True
648 has_specialized_search = True
649 for serialized_user in users:
649 for serialized_user in users:
650 res.append(serialized_user)
650 res.append(serialized_user)
651 elif prefix_match:
651 elif prefix_match:
652 has_specialized_search = True
652 has_specialized_search = True
653 res.append(no_match('No matching users found'))
653 res.append(no_match('No matching users found'))
654
654
655 # user_group: type search
655 # user_group: type search
656 if not prefix_match:
656 if not prefix_match:
657 user_groups, prefix_match = self._get_user_groups_list(query)
657 user_groups, prefix_match = self._get_user_groups_list(query)
658 if user_groups:
658 if user_groups:
659 has_specialized_search = True
659 has_specialized_search = True
660 for serialized_user_group in user_groups:
660 for serialized_user_group in user_groups:
661 res.append(serialized_user_group)
661 res.append(serialized_user_group)
662 elif prefix_match:
662 elif prefix_match:
663 has_specialized_search = True
663 has_specialized_search = True
664 res.append(no_match('No matching user groups found'))
664 res.append(no_match('No matching user groups found'))
665
665
666 # pr: type search
666 # pr: type search
667 if not prefix_match:
667 if not prefix_match:
668 pull_requests, prefix_match = self._get_pull_request_list(query)
668 pull_requests, prefix_match = self._get_pull_request_list(query)
669 if pull_requests:
669 if pull_requests:
670 has_specialized_search = True
670 has_specialized_search = True
671 for serialized_pull_request in pull_requests:
671 for serialized_pull_request in pull_requests:
672 res.append(serialized_pull_request)
672 res.append(serialized_pull_request)
673 elif prefix_match:
673 elif prefix_match:
674 has_specialized_search = True
674 has_specialized_search = True
675 res.append(no_match('No matching pull requests found'))
675 res.append(no_match('No matching pull requests found'))
676
676
677 # FTS commit: type search
677 # FTS commit: type search
678 if not prefix_match:
678 if not prefix_match:
679 commits, prefix_match = self._get_hash_commit_list(
679 commits, prefix_match = self._get_hash_commit_list(
680 c.auth_user, searcher, query, repo, repo_group)
680 c.auth_user, searcher, query, repo, repo_group)
681 if commits:
681 if commits:
682 has_specialized_search = True
682 has_specialized_search = True
683 unique_repos = collections.OrderedDict()
683 unique_repos = collections.OrderedDict()
684 for commit in commits:
684 for commit in commits:
685 repo_name = commit['repo']
685 repo_name = commit['repo']
686 unique_repos.setdefault(repo_name, []).append(commit)
686 unique_repos.setdefault(repo_name, []).append(commit)
687
687
688 for _repo, commits in unique_repos.items():
688 for _repo, commits in unique_repos.items():
689 for commit in commits:
689 for commit in commits:
690 res.append(commit)
690 res.append(commit)
691 elif prefix_match:
691 elif prefix_match:
692 has_specialized_search = True
692 has_specialized_search = True
693 res.append(no_match('No matching commits found'))
693 res.append(no_match('No matching commits found'))
694
694
695 # FTS file: type search
695 # FTS file: type search
696 if not prefix_match:
696 if not prefix_match:
697 paths, prefix_match = self._get_path_list(
697 paths, prefix_match = self._get_path_list(
698 c.auth_user, searcher, query, repo, repo_group)
698 c.auth_user, searcher, query, repo, repo_group)
699 if paths:
699 if paths:
700 has_specialized_search = True
700 has_specialized_search = True
701 unique_repos = collections.OrderedDict()
701 unique_repos = collections.OrderedDict()
702 for path in paths:
702 for path in paths:
703 repo_name = path['repo']
703 repo_name = path['repo']
704 unique_repos.setdefault(repo_name, []).append(path)
704 unique_repos.setdefault(repo_name, []).append(path)
705
705
706 for repo, paths in unique_repos.items():
706 for repo, paths in unique_repos.items():
707 for path in paths:
707 for path in paths:
708 res.append(path)
708 res.append(path)
709 elif prefix_match:
709 elif prefix_match:
710 has_specialized_search = True
710 has_specialized_search = True
711 res.append(no_match('No matching files found'))
711 res.append(no_match('No matching files found'))
712
712
713 # main suggestions
713 # main suggestions
714 if not has_specialized_search:
714 if not has_specialized_search:
715 repo_group_name = ''
715 repo_group_name = ''
716 if repo_group:
716 if repo_group:
717 repo_group_name = repo_group.group_name
717 repo_group_name = repo_group.group_name
718
718
719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
720 res.append(_q)
720 res.append(_q)
721
721
722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
723 for serialized_repo_group in repo_groups:
723 for serialized_repo_group in repo_groups:
724 res.append(serialized_repo_group)
724 res.append(serialized_repo_group)
725
725
726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
727 for serialized_repo in repos:
727 for serialized_repo in repos:
728 res.append(serialized_repo)
728 res.append(serialized_repo)
729
729
730 if not repos and not repo_groups:
730 if not repos and not repo_groups:
731 res.append(no_match('No matches found'))
731 res.append(no_match('No matches found'))
732
732
733 return {'suggestions': res}
733 return {'suggestions': res}
734
734
735 @LoginRequired()
735 @LoginRequired()
736 def main_page(self):
736 def main_page(self):
737 c = self.load_default_context()
737 c = self.load_default_context()
738 c.repo_group = None
738 c.repo_group = None
739 return self._get_template_context(c)
739 return self._get_template_context(c)
740
740
741 def _main_page_repo_groups_data(self, repo_group_id):
741 def _main_page_repo_groups_data(self, repo_group_id):
742 column_map = {
742 column_map = {
743 'name': 'group_name_hash',
743 'name': 'group_name_hash',
744 'desc': 'group_description',
744 'desc': 'group_description',
745 'last_change': 'updated_on',
745 'last_change': 'updated_on',
746 'owner': 'user_username',
746 'owner': 'user_username',
747 }
747 }
748 draw, start, limit = self._extract_chunk(self.request)
748 draw, start, limit = self._extract_chunk(self.request)
749 search_q, order_by, order_dir = self._extract_ordering(
749 search_q, order_by, order_dir = self._extract_ordering(
750 self.request, column_map=column_map)
750 self.request, column_map=column_map)
751 return RepoGroupModel().get_repo_groups_data_table(
751 return RepoGroupModel().get_repo_groups_data_table(
752 draw, start, limit,
752 draw, start, limit,
753 search_q, order_by, order_dir,
753 search_q, order_by, order_dir,
754 self._rhodecode_user, repo_group_id)
754 self._rhodecode_user, repo_group_id)
755
755
756 def _main_page_repos_data(self, repo_group_id):
756 def _main_page_repos_data(self, repo_group_id):
757 column_map = {
757 column_map = {
758 'name': 'repo_name',
758 'name': 'repo_name',
759 'desc': 'description',
759 'desc': 'description',
760 'last_change': 'updated_on',
760 'last_change': 'updated_on',
761 'owner': 'user_username',
761 'owner': 'user_username',
762 }
762 }
763 draw, start, limit = self._extract_chunk(self.request)
763 draw, start, limit = self._extract_chunk(self.request)
764 search_q, order_by, order_dir = self._extract_ordering(
764 search_q, order_by, order_dir = self._extract_ordering(
765 self.request, column_map=column_map)
765 self.request, column_map=column_map)
766 return RepoModel().get_repos_data_table(
766 return RepoModel().get_repos_data_table(
767 draw, start, limit,
767 draw, start, limit,
768 search_q, order_by, order_dir,
768 search_q, order_by, order_dir,
769 self._rhodecode_user, repo_group_id)
769 self._rhodecode_user, repo_group_id)
770
770
771 @LoginRequired()
771 @LoginRequired()
772 def main_page_repo_groups_data(self):
772 def main_page_repo_groups_data(self):
773 self.load_default_context()
773 self.load_default_context()
774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
775
775
776 if repo_group_id:
776 if repo_group_id:
777 group = RepoGroup.get_or_404(repo_group_id)
777 group = RepoGroup.get_or_404(repo_group_id)
778 _perms = AuthUser.repo_group_read_perms
778 _perms = AuthUser.repo_group_read_perms
779 if not HasRepoGroupPermissionAny(*_perms)(
779 if not HasRepoGroupPermissionAny(*_perms)(
780 group.group_name, 'user is allowed to list repo group children'):
780 group.group_name, 'user is allowed to list repo group children'):
781 raise HTTPNotFound()
781 raise HTTPNotFound()
782
782
783 return self._main_page_repo_groups_data(repo_group_id)
783 return self._main_page_repo_groups_data(repo_group_id)
784
784
785 @LoginRequired()
785 @LoginRequired()
786 def main_page_repos_data(self):
786 def main_page_repos_data(self):
787 self.load_default_context()
787 self.load_default_context()
788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
789
789
790 if repo_group_id:
790 if repo_group_id:
791 group = RepoGroup.get_or_404(repo_group_id)
791 group = RepoGroup.get_or_404(repo_group_id)
792 _perms = AuthUser.repo_group_read_perms
792 _perms = AuthUser.repo_group_read_perms
793 if not HasRepoGroupPermissionAny(*_perms)(
793 if not HasRepoGroupPermissionAny(*_perms)(
794 group.group_name, 'user is allowed to list repo group children'):
794 group.group_name, 'user is allowed to list repo group children'):
795 raise HTTPNotFound()
795 raise HTTPNotFound()
796
796
797 return self._main_page_repos_data(repo_group_id)
797 return self._main_page_repos_data(repo_group_id)
798
798
799 @LoginRequired()
799 @LoginRequired()
800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
801 def repo_group_main_page(self):
801 def repo_group_main_page(self):
802 c = self.load_default_context()
802 c = self.load_default_context()
803 c.repo_group = self.request.db_repo_group
803 c.repo_group = self.request.db_repo_group
804 return self._get_template_context(c)
804 return self._get_template_context(c)
805
805
806 @LoginRequired()
806 @LoginRequired()
807 @CSRFRequired()
807 @CSRFRequired()
808 def markup_preview(self):
808 def markup_preview(self):
809 # Technically a CSRF token is not needed as no state changes with this
809 # Technically a CSRF token is not needed as no state changes with this
810 # call. However, as this is a POST is better to have it, so automated
810 # call. However, as this is a POST is better to have it, so automated
811 # tools don't flag it as potential CSRF.
811 # tools don't flag it as potential CSRF.
812 # Post is required because the payload could be bigger than the maximum
812 # Post is required because the payload could be bigger than the maximum
813 # allowed by GET.
813 # allowed by GET.
814
814
815 text = self.request.POST.get('text')
815 text = self.request.POST.get('text')
816 renderer = self.request.POST.get('renderer') or 'rst'
816 renderer = self.request.POST.get('renderer') or 'rst'
817 if text:
817 if text:
818 return h.render(text, renderer=renderer, mentions=True)
818 return h.render(text, renderer=renderer, mentions=True)
819 return ''
819 return ''
820
820
821 @LoginRequired()
821 @LoginRequired()
822 @CSRFRequired()
822 @CSRFRequired()
823 def file_preview(self):
823 def file_preview(self):
824 # Technically a CSRF token is not needed as no state changes with this
824 # Technically a CSRF token is not needed as no state changes with this
825 # call. However, as this is a POST is better to have it, so automated
825 # call. However, as this is a POST is better to have it, so automated
826 # tools don't flag it as potential CSRF.
826 # tools don't flag it as potential CSRF.
827 # Post is required because the payload could be bigger than the maximum
827 # Post is required because the payload could be bigger than the maximum
828 # allowed by GET.
828 # allowed by GET.
829
829
830 text = self.request.POST.get('text')
830 text = self.request.POST.get('text')
831 file_path = self.request.POST.get('file_path')
831 file_path = self.request.POST.get('file_path')
832
832
833 renderer = h.renderer_from_filename(file_path)
833 renderer = h.renderer_from_filename(file_path)
834
834
835 if renderer:
835 if renderer:
836 return h.render(text, renderer=renderer, mentions=True)
836 return h.render(text, renderer=renderer, mentions=True)
837 else:
837 else:
838 self.load_default_context()
838 self.load_default_context()
839 _render = self.request.get_partial_renderer(
839 _render = self.request.get_partial_renderer(
840 'rhodecode:templates/files/file_content.mako')
840 'rhodecode:templates/files/file_content.mako')
841
841
842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
843
843
844 return _render('render_lines', lines)
844 return _render('render_lines', lines)
845
845
846 @LoginRequired()
846 @LoginRequired()
847 @CSRFRequired()
847 @CSRFRequired()
848 def store_user_session_attr(self):
848 def store_user_session_attr(self):
849 key = self.request.POST.get('key')
849 key = self.request.POST.get('key')
850 val = self.request.POST.get('val')
850 val = self.request.POST.get('val')
851
851
852 existing_value = self.request.session.get(key)
852 existing_value = self.request.session.get(key)
853 if existing_value != val:
853 if existing_value != val:
854 self.request.session[key] = val
854 self.request.session[key] = val
855
855
856 return 'stored:{}:{}'.format(key, val)
856 return 'stored:{}:{}'.format(key, val)
@@ -1,108 +1,108 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.httpexceptions import HTTPNotFound
25 from pyramid.httpexceptions import HTTPNotFound
26
26
27
27
28 from rhodecode.apps._base import BaseAppView, RepoAppView
28 from rhodecode.apps._base import BaseAppView, RepoAppView
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 HasRepoPermissionAnyDecorator)
32 HasRepoPermissionAnyDecorator)
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 from rhodecode.lib.index import searcher_from_config
34 from rhodecode.lib.index import searcher_from_config
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 from rhodecode.lib.utils2 import str2bool, safe_int
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
41 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo_group import RepoGroupModel
42 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.scm import RepoGroupList, RepoList
43 from rhodecode.model.scm import RepoGroupList, RepoList
44 from rhodecode.model.user import UserModel
44 from rhodecode.model.user import UserModel
45 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.user_group import UserGroupModel
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class HoverCardsView(BaseAppView):
50 class HoverCardsView(BaseAppView):
51
51
52 def load_default_context(self):
52 def load_default_context(self):
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 return c
54 return c
55
55
56 @LoginRequired()
56 @LoginRequired()
57 def hovercard_user(self):
57 def hovercard_user(self):
58 c = self.load_default_context()
58 c = self.load_default_context()
59 user_id = self.request.matchdict['user_id']
59 user_id = self.request.matchdict['user_id']
60 c.user = User.get_or_404(user_id)
60 c.user = User.get_or_404(user_id)
61 return self._get_template_context(c)
61 return self._get_template_context(c)
62
62
63 @LoginRequired()
63 @LoginRequired()
64 def hovercard_username(self):
64 def hovercard_username(self):
65 c = self.load_default_context()
65 c = self.load_default_context()
66 username = self.request.matchdict['username']
66 username = self.request.matchdict['username']
67 c.user = User.get_by_username(username)
67 c.user = User.get_by_username(username)
68 if not c.user:
68 if not c.user:
69 raise HTTPNotFound()
69 raise HTTPNotFound()
70
70
71 return self._get_template_context(c)
71 return self._get_template_context(c)
72
72
73 @LoginRequired()
73 @LoginRequired()
74 def hovercard_user_group(self):
74 def hovercard_user_group(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 user_group_id = self.request.matchdict['user_group_id']
76 user_group_id = self.request.matchdict['user_group_id']
77 c.user_group = UserGroup.get_or_404(user_group_id)
77 c.user_group = UserGroup.get_or_404(user_group_id)
78 return self._get_template_context(c)
78 return self._get_template_context(c)
79
79
80 @LoginRequired()
80 @LoginRequired()
81 def hovercard_pull_request(self):
81 def hovercard_pull_request(self):
82 c = self.load_default_context()
82 c = self.load_default_context()
83 c.pull_request = PullRequest.get_or_404(
83 c.pull_request = PullRequest.get_or_404(
84 self.request.matchdict['pull_request_id'])
84 self.request.matchdict['pull_request_id'])
85 perms = ['repository.read', 'repository.write', 'repository.admin']
85 perms = ['repository.read', 'repository.write', 'repository.admin']
86 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
86 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
87 c.pull_request.target_repo.repo_name)
87 c.pull_request.target_repo.repo_name)
88 return self._get_template_context(c)
88 return self._get_template_context(c)
89
89
90
90
91 class HoverCardsRepoView(RepoAppView):
91 class HoverCardsRepoView(RepoAppView):
92 def load_default_context(self):
92 def load_default_context(self):
93 c = self._get_local_tmpl_context()
93 c = self._get_local_tmpl_context()
94 return c
94 return c
95
95
96 @LoginRequired()
96 @LoginRequired()
97 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
97 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
98 def hovercard_repo_commit(self):
98 def hovercard_repo_commit(self):
99 c = self.load_default_context()
99 c = self.load_default_context()
100 commit_id = self.request.matchdict['commit_id']
100 commit_id = self.request.matchdict['commit_id']
101 pre_load = ['author', 'branch', 'date', 'message']
101 pre_load = ['author', 'branch', 'date', 'message']
102 try:
102 try:
103 c.commit = self.rhodecode_vcs_repo.get_commit(
103 c.commit = self.rhodecode_vcs_repo.get_commit(
104 commit_id=commit_id, pre_load=pre_load)
104 commit_id=commit_id, pre_load=pre_load)
105 except (CommitDoesNotExistError, EmptyRepositoryError):
105 except (CommitDoesNotExistError, EmptyRepositoryError):
106 raise HTTPNotFound()
106 raise HTTPNotFound()
107
107
108 return self._get_template_context(c)
108 return self._get_template_context(c)
@@ -1,363 +1,363 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import logging
20 import logging
21 import itertools
21 import itertools
22
22
23 from pyramid.httpexceptions import HTTPBadRequest
23 from pyramid.httpexceptions import HTTPBadRequest
24 from pyramid.response import Response
24 from pyramid.response import Response
25 from pyramid.renderers import render
25 from pyramid.renderers import render
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 or_, joinedload, Repository, UserLog, UserFollowing, User, UserApiKeys)
29 or_, joinedload, Repository, UserLog, UserFollowing, User, UserApiKeys)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 import rhodecode.lib.helpers as h
31 import rhodecode.lib.helpers as h
32 from rhodecode.lib.helpers import SqlPage
32 from rhodecode.lib.helpers import SqlPage
33 from rhodecode.lib.user_log_filter import user_log_filter
33 from rhodecode.lib.user_log_filter import user_log_filter
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
35 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
35 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
36 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
36 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
37 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class JournalView(BaseAppView):
42 class JournalView(BaseAppView):
43
43
44 def load_default_context(self):
44 def load_default_context(self):
45 c = self._get_local_tmpl_context(include_app_defaults=True)
45 c = self._get_local_tmpl_context(include_app_defaults=True)
46
46
47 self._load_defaults(c.rhodecode_name)
47 self._load_defaults(c.rhodecode_name)
48
48
49 # TODO(marcink): what is this, why we need a global register ?
49 # TODO(marcink): what is this, why we need a global register ?
50 c.search_term = self.request.GET.get('filter') or ''
50 c.search_term = self.request.GET.get('filter') or ''
51 return c
51 return c
52
52
53 def _get_config(self, rhodecode_name):
53 def _get_config(self, rhodecode_name):
54 import rhodecode
54 import rhodecode
55 config = rhodecode.CONFIG
55 config = rhodecode.CONFIG
56
56
57 return {
57 return {
58 'language': 'en-us',
58 'language': 'en-us',
59 'feed_ttl': '5', # TTL of feed,
59 'feed_ttl': '5', # TTL of feed,
60 'feed_items_per_page':
60 'feed_items_per_page':
61 safe_int(config.get('rss_items_per_page', 20)),
61 safe_int(config.get('rss_items_per_page', 20)),
62 'rhodecode_name': rhodecode_name
62 'rhodecode_name': rhodecode_name
63 }
63 }
64
64
65 def _load_defaults(self, rhodecode_name):
65 def _load_defaults(self, rhodecode_name):
66 config = self._get_config(rhodecode_name)
66 config = self._get_config(rhodecode_name)
67 # common values for feeds
67 # common values for feeds
68 self.language = config["language"]
68 self.language = config["language"]
69 self.ttl = config["feed_ttl"]
69 self.ttl = config["feed_ttl"]
70 self.feed_items_per_page = config['feed_items_per_page']
70 self.feed_items_per_page = config['feed_items_per_page']
71 self.rhodecode_name = config['rhodecode_name']
71 self.rhodecode_name = config['rhodecode_name']
72
72
73 def _get_daily_aggregate(self, journal):
73 def _get_daily_aggregate(self, journal):
74 groups = []
74 groups = []
75 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
75 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
76 user_group = []
76 user_group = []
77 # groupby username if it's a present value, else
77 # groupby username if it's a present value, else
78 # fallback to journal username
78 # fallback to journal username
79 for _, g2 in itertools.groupby(
79 for _, g2 in itertools.groupby(
80 list(g), lambda x: x.user.username if x.user else x.username):
80 list(g), lambda x: x.user.username if x.user else x.username):
81 l = list(g2)
81 l = list(g2)
82 user_group.append((l[0].user, l))
82 user_group.append((l[0].user, l))
83
83
84 groups.append((k, user_group,))
84 groups.append((k, user_group,))
85
85
86 return groups
86 return groups
87
87
88 def _get_journal_data(self, following_repos, search_term):
88 def _get_journal_data(self, following_repos, search_term):
89 repo_ids = [x.follows_repository.repo_id for x in following_repos
89 repo_ids = [x.follows_repository.repo_id for x in following_repos
90 if x.follows_repository is not None]
90 if x.follows_repository is not None]
91 user_ids = [x.follows_user.user_id for x in following_repos
91 user_ids = [x.follows_user.user_id for x in following_repos
92 if x.follows_user is not None]
92 if x.follows_user is not None]
93
93
94 filtering_criterion = None
94 filtering_criterion = None
95
95
96 if repo_ids and user_ids:
96 if repo_ids and user_ids:
97 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
97 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
98 UserLog.user_id.in_(user_ids))
98 UserLog.user_id.in_(user_ids))
99 if repo_ids and not user_ids:
99 if repo_ids and not user_ids:
100 filtering_criterion = UserLog.repository_id.in_(repo_ids)
100 filtering_criterion = UserLog.repository_id.in_(repo_ids)
101 if not repo_ids and user_ids:
101 if not repo_ids and user_ids:
102 filtering_criterion = UserLog.user_id.in_(user_ids)
102 filtering_criterion = UserLog.user_id.in_(user_ids)
103 if filtering_criterion is not None:
103 if filtering_criterion is not None:
104 journal = Session().query(UserLog)\
104 journal = Session().query(UserLog)\
105 .options(joinedload(UserLog.user))\
105 .options(joinedload(UserLog.user))\
106 .options(joinedload(UserLog.repository))
106 .options(joinedload(UserLog.repository))
107 # filter
107 # filter
108 try:
108 try:
109 journal = user_log_filter(journal, search_term)
109 journal = user_log_filter(journal, search_term)
110 except Exception:
110 except Exception:
111 # we want this to crash for now
111 # we want this to crash for now
112 raise
112 raise
113 journal = journal.filter(filtering_criterion)\
113 journal = journal.filter(filtering_criterion)\
114 .order_by(UserLog.action_date.desc())
114 .order_by(UserLog.action_date.desc())
115 else:
115 else:
116 journal = []
116 journal = []
117
117
118 return journal
118 return journal
119
119
120 def feed_uid(self, entry_id):
120 def feed_uid(self, entry_id):
121 return '{}:{}'.format('journal', md5_safe(entry_id))
121 return '{}:{}'.format('journal', md5_safe(str(entry_id)))
122
122
123 def _atom_feed(self, repos, search_term, public=True):
123 def _atom_feed(self, repos, search_term, public=True):
124 _ = self.request.translate
124 _ = self.request.translate
125 journal = self._get_journal_data(repos, search_term)
125 journal = self._get_journal_data(repos, search_term)
126 if public:
126 if public:
127 _link = h.route_url('journal_public_atom')
127 _link = h.route_url('journal_public_atom')
128 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
128 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
129 'atom feed')
129 'atom feed')
130 else:
130 else:
131 _link = h.route_url('journal_atom')
131 _link = h.route_url('journal_atom')
132 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
132 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
133
133
134 feed = Atom1Feed(
134 feed = Atom1Feed(
135 title=_desc, link=_link, description=_desc,
135 title=_desc, link=_link, description=_desc,
136 language=self.language, ttl=self.ttl)
136 language=self.language, ttl=self.ttl)
137
137
138 for entry in journal[:self.feed_items_per_page]:
138 for entry in journal[:self.feed_items_per_page]:
139 user = entry.user
139 user = entry.user
140 if user is None:
140 if user is None:
141 # fix deleted users
141 # fix deleted users
142 user = AttributeDict({'short_contact': entry.username,
142 user = AttributeDict({'short_contact': entry.username,
143 'email': '',
143 'email': '',
144 'full_contact': ''})
144 'full_contact': ''})
145 action, action_extra, ico = h.action_parser(
145 action, action_extra, ico = h.action_parser(
146 self.request, entry, feed=True)
146 self.request, entry, feed=True)
147 title = "%s - %s %s" % (user.short_contact, action(),
147 title = "%s - %s %s" % (user.short_contact, action(),
148 entry.repository.repo_name)
148 entry.repository.repo_name)
149 desc = action_extra()
149 desc = action_extra()
150 _url = h.route_url('home')
150 _url = h.route_url('home')
151 if entry.repository is not None:
151 if entry.repository is not None:
152 _url = h.route_url('repo_commits',
152 _url = h.route_url('repo_commits',
153 repo_name=entry.repository.repo_name)
153 repo_name=entry.repository.repo_name)
154
154
155 feed.add_item(
155 feed.add_item(
156 unique_id=self.feed_uid(entry.user_log_id),
156 unique_id=self.feed_uid(entry.user_log_id),
157 title=title,
157 title=title,
158 pubdate=entry.action_date,
158 pubdate=entry.action_date,
159 link=_url,
159 link=_url,
160 author_email=user.email,
160 author_email=user.email,
161 author_name=user.full_contact,
161 author_name=user.full_contact,
162 description=desc)
162 description=desc)
163
163
164 response = Response(feed.writeString('utf-8'))
164 response = Response(feed.writeString('utf-8'))
165 response.content_type = feed.content_type
165 response.content_type = feed.content_type
166 return response
166 return response
167
167
168 def _rss_feed(self, repos, search_term, public=True):
168 def _rss_feed(self, repos, search_term, public=True):
169 _ = self.request.translate
169 _ = self.request.translate
170 journal = self._get_journal_data(repos, search_term)
170 journal = self._get_journal_data(repos, search_term)
171 if public:
171 if public:
172 _link = h.route_url('journal_public_atom')
172 _link = h.route_url('journal_public_atom')
173 _desc = '%s %s %s' % (
173 _desc = '%s %s %s' % (
174 self.rhodecode_name, _('public journal'), 'rss feed')
174 self.rhodecode_name, _('public journal'), 'rss feed')
175 else:
175 else:
176 _link = h.route_url('journal_atom')
176 _link = h.route_url('journal_atom')
177 _desc = '%s %s %s' % (
177 _desc = '%s %s %s' % (
178 self.rhodecode_name, _('journal'), 'rss feed')
178 self.rhodecode_name, _('journal'), 'rss feed')
179
179
180 feed = Rss201rev2Feed(
180 feed = Rss201rev2Feed(
181 title=_desc, link=_link, description=_desc,
181 title=_desc, link=_link, description=_desc,
182 language=self.language, ttl=self.ttl)
182 language=self.language, ttl=self.ttl)
183
183
184 for entry in journal[:self.feed_items_per_page]:
184 for entry in journal[:self.feed_items_per_page]:
185 user = entry.user
185 user = entry.user
186 if user is None:
186 if user is None:
187 # fix deleted users
187 # fix deleted users
188 user = AttributeDict({'short_contact': entry.username,
188 user = AttributeDict({'short_contact': entry.username,
189 'email': '',
189 'email': '',
190 'full_contact': ''})
190 'full_contact': ''})
191 action, action_extra, ico = h.action_parser(
191 action, action_extra, ico = h.action_parser(
192 self.request, entry, feed=True)
192 self.request, entry, feed=True)
193 title = "%s - %s %s" % (user.short_contact, action(),
193 title = "%s - %s %s" % (user.short_contact, action(),
194 entry.repository.repo_name)
194 entry.repository.repo_name)
195 desc = action_extra()
195 desc = action_extra()
196 _url = h.route_url('home')
196 _url = h.route_url('home')
197 if entry.repository is not None:
197 if entry.repository is not None:
198 _url = h.route_url('repo_commits',
198 _url = h.route_url('repo_commits',
199 repo_name=entry.repository.repo_name)
199 repo_name=entry.repository.repo_name)
200
200
201 feed.add_item(
201 feed.add_item(
202 unique_id=self.feed_uid(entry.user_log_id),
202 unique_id=self.feed_uid(entry.user_log_id),
203 title=title,
203 title=title,
204 pubdate=entry.action_date,
204 pubdate=entry.action_date,
205 link=_url,
205 link=_url,
206 author_email=user.email,
206 author_email=user.email,
207 author_name=user.full_contact,
207 author_name=user.full_contact,
208 description=desc)
208 description=desc)
209
209
210 response = Response(feed.writeString('utf-8'))
210 response = Response(feed.writeString('utf-8'))
211 response.content_type = feed.content_type
211 response.content_type = feed.content_type
212 return response
212 return response
213
213
214 @LoginRequired()
214 @LoginRequired()
215 @NotAnonymous()
215 @NotAnonymous()
216 def journal(self):
216 def journal(self):
217 c = self.load_default_context()
217 c = self.load_default_context()
218
218
219 p = safe_int(self.request.GET.get('page', 1), 1)
219 p = safe_int(self.request.GET.get('page', 1), 1)
220 c.user = User.get(self._rhodecode_user.user_id)
220 c.user = User.get(self._rhodecode_user.user_id)
221 following = Session().query(UserFollowing)\
221 following = Session().query(UserFollowing)\
222 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
222 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
223 .options(joinedload(UserFollowing.follows_repository))\
223 .options(joinedload(UserFollowing.follows_repository))\
224 .all()
224 .all()
225
225
226 journal = self._get_journal_data(following, c.search_term)
226 journal = self._get_journal_data(following, c.search_term)
227
227
228 def url_generator(page_num):
228 def url_generator(page_num):
229 query_params = {
229 query_params = {
230 'page': page_num,
230 'page': page_num,
231 'filter': c.search_term
231 'filter': c.search_term
232 }
232 }
233 return self.request.current_route_path(_query=query_params)
233 return self.request.current_route_path(_query=query_params)
234
234
235 c.journal_pager = SqlPage(
235 c.journal_pager = SqlPage(
236 journal, page=p, items_per_page=20, url_maker=url_generator)
236 journal, page=p, items_per_page=20, url_maker=url_generator)
237 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
237 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
238
238
239 c.journal_data = render(
239 c.journal_data = render(
240 'rhodecode:templates/journal/journal_data.mako',
240 'rhodecode:templates/journal/journal_data.mako',
241 self._get_template_context(c), self.request)
241 self._get_template_context(c), self.request)
242
242
243 if self.request.is_xhr:
243 if self.request.is_xhr:
244 return Response(c.journal_data)
244 return Response(c.journal_data)
245
245
246 html = render(
246 html = render(
247 'rhodecode:templates/journal/journal.mako',
247 'rhodecode:templates/journal/journal.mako',
248 self._get_template_context(c), self.request)
248 self._get_template_context(c), self.request)
249 return Response(html)
249 return Response(html)
250
250
251 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
251 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
252 @NotAnonymous()
252 @NotAnonymous()
253 def journal_atom(self):
253 def journal_atom(self):
254 """
254 """
255 Produce an atom-1.0 feed via feedgenerator module
255 Produce an atom-1.0 feed via feedgenerator module
256 """
256 """
257 c = self.load_default_context()
257 c = self.load_default_context()
258 following_repos = Session().query(UserFollowing)\
258 following_repos = Session().query(UserFollowing)\
259 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
259 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
260 .options(joinedload(UserFollowing.follows_repository))\
260 .options(joinedload(UserFollowing.follows_repository))\
261 .all()
261 .all()
262 return self._atom_feed(following_repos, c.search_term, public=False)
262 return self._atom_feed(following_repos, c.search_term, public=False)
263
263
264 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
264 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
265 @NotAnonymous()
265 @NotAnonymous()
266 def journal_rss(self):
266 def journal_rss(self):
267 """
267 """
268 Produce an rss feed via feedgenerator module
268 Produce an rss feed via feedgenerator module
269 """
269 """
270 c = self.load_default_context()
270 c = self.load_default_context()
271 following_repos = Session().query(UserFollowing)\
271 following_repos = Session().query(UserFollowing)\
272 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
272 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
273 .options(joinedload(UserFollowing.follows_repository))\
273 .options(joinedload(UserFollowing.follows_repository))\
274 .all()
274 .all()
275 return self._rss_feed(following_repos, c.search_term, public=False)
275 return self._rss_feed(following_repos, c.search_term, public=False)
276
276
277 @LoginRequired()
277 @LoginRequired()
278 def journal_public(self):
278 def journal_public(self):
279 c = self.load_default_context()
279 c = self.load_default_context()
280 # Return a rendered template
280 # Return a rendered template
281 p = safe_int(self.request.GET.get('page', 1), 1)
281 p = safe_int(self.request.GET.get('page', 1), 1)
282
282
283 c.following = Session().query(UserFollowing)\
283 c.following = Session().query(UserFollowing)\
284 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
284 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
285 .options(joinedload(UserFollowing.follows_repository))\
285 .options(joinedload(UserFollowing.follows_repository))\
286 .all()
286 .all()
287
287
288 journal = self._get_journal_data(c.following, c.search_term)
288 journal = self._get_journal_data(c.following, c.search_term)
289
289
290 def url_generator(page_num):
290 def url_generator(page_num):
291 query_params = {
291 query_params = {
292 'page': page_num
292 'page': page_num
293 }
293 }
294 return self.request.current_route_path(_query=query_params)
294 return self.request.current_route_path(_query=query_params)
295
295
296 c.journal_pager = SqlPage(
296 c.journal_pager = SqlPage(
297 journal, page=p, items_per_page=20, url_maker=url_generator)
297 journal, page=p, items_per_page=20, url_maker=url_generator)
298 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
298 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
299
299
300 c.journal_data = render(
300 c.journal_data = render(
301 'rhodecode:templates/journal/journal_data.mako',
301 'rhodecode:templates/journal/journal_data.mako',
302 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
303
303
304 if self.request.is_xhr:
304 if self.request.is_xhr:
305 return Response(c.journal_data)
305 return Response(c.journal_data)
306
306
307 html = render(
307 html = render(
308 'rhodecode:templates/journal/public_journal.mako',
308 'rhodecode:templates/journal/public_journal.mako',
309 self._get_template_context(c), self.request)
309 self._get_template_context(c), self.request)
310 return Response(html)
310 return Response(html)
311
311
312 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
312 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
313 def journal_public_atom(self):
313 def journal_public_atom(self):
314 """
314 """
315 Produce an atom-1.0 feed via feedgenerator module
315 Produce an atom-1.0 feed via feedgenerator module
316 """
316 """
317 c = self.load_default_context()
317 c = self.load_default_context()
318 following_repos = Session().query(UserFollowing)\
318 following_repos = Session().query(UserFollowing)\
319 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
319 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
320 .options(joinedload(UserFollowing.follows_repository))\
320 .options(joinedload(UserFollowing.follows_repository))\
321 .all()
321 .all()
322
322
323 return self._atom_feed(following_repos, c.search_term)
323 return self._atom_feed(following_repos, c.search_term)
324
324
325 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
325 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
326 def journal_public_rss(self):
326 def journal_public_rss(self):
327 """
327 """
328 Produce an rss2 feed via feedgenerator module
328 Produce an rss2 feed via feedgenerator module
329 """
329 """
330 c = self.load_default_context()
330 c = self.load_default_context()
331 following_repos = Session().query(UserFollowing)\
331 following_repos = Session().query(UserFollowing)\
332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
333 .options(joinedload(UserFollowing.follows_repository))\
333 .options(joinedload(UserFollowing.follows_repository))\
334 .all()
334 .all()
335
335
336 return self._rss_feed(following_repos, c.search_term)
336 return self._rss_feed(following_repos, c.search_term)
337
337
338 @LoginRequired()
338 @LoginRequired()
339 @NotAnonymous()
339 @NotAnonymous()
340 @CSRFRequired()
340 @CSRFRequired()
341 def toggle_following(self):
341 def toggle_following(self):
342 user_id = self.request.POST.get('follows_user_id')
342 user_id = self.request.POST.get('follows_user_id')
343 if user_id:
343 if user_id:
344 try:
344 try:
345 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
345 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
346 Session().commit()
346 Session().commit()
347 return 'ok'
347 return 'ok'
348 except Exception:
348 except Exception:
349 raise HTTPBadRequest()
349 raise HTTPBadRequest()
350
350
351 repo_id = self.request.POST.get('follows_repo_id')
351 repo_id = self.request.POST.get('follows_repo_id')
352 repo = Repository.get_or_404(repo_id)
352 repo = Repository.get_or_404(repo_id)
353 perm_set = ['repository.read', 'repository.write', 'repository.admin']
353 perm_set = ['repository.read', 'repository.write', 'repository.admin']
354 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
354 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
355 if repo and has_perm:
355 if repo and has_perm:
356 try:
356 try:
357 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
357 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
358 Session().commit()
358 Session().commit()
359 return 'ok'
359 return 'ok'
360 except Exception:
360 except Exception:
361 raise HTTPBadRequest()
361 raise HTTPBadRequest()
362
362
363 raise HTTPBadRequest()
363 raise HTTPBadRequest()
@@ -1,783 +1,784 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib import ext_json
34 from rhodecode.lib import ext_json
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, NotAnonymous, CSRFRequired,
36 LoginRequired, NotAnonymous, CSRFRequired,
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 from rhodecode.lib.channelstream import (
38 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.hash_utils import md5_safe
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
44 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator,
45 IntegrityError, or_, in_filter_generator,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.validation_schema.schemas import user_schema
52 from rhodecode.model.validation_schema.schemas import user_schema
52
53
53 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
54
55
55
56
56 class MyAccountView(BaseAppView, DataGridAppView):
57 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
58 ALLOW_SCOPED_TOKENS = False
58 """
59 """
59 This view has alternative version inside EE, if modified please take a look
60 This view has alternative version inside EE, if modified please take a look
60 in there as well.
61 in there as well.
61 """
62 """
62
63
63 def load_default_context(self):
64 def load_default_context(self):
64 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
66 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 return c
68 return c
68
69
69 @LoginRequired()
70 @LoginRequired()
70 @NotAnonymous()
71 @NotAnonymous()
71 def my_account_profile(self):
72 def my_account_profile(self):
72 c = self.load_default_context()
73 c = self.load_default_context()
73 c.active = 'profile'
74 c.active = 'profile'
74 c.extern_type = c.user.extern_type
75 c.extern_type = c.user.extern_type
75 return self._get_template_context(c)
76 return self._get_template_context(c)
76
77
77 @LoginRequired()
78 @LoginRequired()
78 @NotAnonymous()
79 @NotAnonymous()
79 def my_account_edit(self):
80 def my_account_edit(self):
80 c = self.load_default_context()
81 c = self.load_default_context()
81 c.active = 'profile_edit'
82 c.active = 'profile_edit'
82 c.extern_type = c.user.extern_type
83 c.extern_type = c.user.extern_type
83 c.extern_name = c.user.extern_name
84 c.extern_name = c.user.extern_name
84
85
85 schema = user_schema.UserProfileSchema().bind(
86 schema = user_schema.UserProfileSchema().bind(
86 username=c.user.username, user_emails=c.user.emails)
87 username=c.user.username, user_emails=c.user.emails)
87 appstruct = {
88 appstruct = {
88 'username': c.user.username,
89 'username': c.user.username,
89 'email': c.user.email,
90 'email': c.user.email,
90 'firstname': c.user.firstname,
91 'firstname': c.user.firstname,
91 'lastname': c.user.lastname,
92 'lastname': c.user.lastname,
92 'description': c.user.description,
93 'description': c.user.description,
93 }
94 }
94 c.form = forms.RcForm(
95 c.form = forms.RcForm(
95 schema, appstruct=appstruct,
96 schema, appstruct=appstruct,
96 action=h.route_path('my_account_update'),
97 action=h.route_path('my_account_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
98
99
99 return self._get_template_context(c)
100 return self._get_template_context(c)
100
101
101 @LoginRequired()
102 @LoginRequired()
102 @NotAnonymous()
103 @NotAnonymous()
103 @CSRFRequired()
104 @CSRFRequired()
104 def my_account_update(self):
105 def my_account_update(self):
105 _ = self.request.translate
106 _ = self.request.translate
106 c = self.load_default_context()
107 c = self.load_default_context()
107 c.active = 'profile_edit'
108 c.active = 'profile_edit'
108 c.perm_user = c.auth_user
109 c.perm_user = c.auth_user
109 c.extern_type = c.user.extern_type
110 c.extern_type = c.user.extern_type
110 c.extern_name = c.user.extern_name
111 c.extern_name = c.user.extern_name
111
112
112 schema = user_schema.UserProfileSchema().bind(
113 schema = user_schema.UserProfileSchema().bind(
113 username=c.user.username, user_emails=c.user.emails)
114 username=c.user.username, user_emails=c.user.emails)
114 form = forms.RcForm(
115 form = forms.RcForm(
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116
117
117 controls = self.request.POST.items()
118 controls = list(self.request.POST.items())
118 try:
119 try:
119 valid_data = form.validate(controls)
120 valid_data = form.validate(controls)
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 'new_password', 'password_confirmation']
122 'new_password', 'password_confirmation']
122 if c.extern_type != "rhodecode":
123 if c.extern_type != "rhodecode":
123 # forbid updating username for external accounts
124 # forbid updating username for external accounts
124 skip_attrs.append('username')
125 skip_attrs.append('username')
125 old_email = c.user.email
126 old_email = c.user.email
126 UserModel().update_user(
127 UserModel().update_user(
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 **valid_data)
129 **valid_data)
129 if old_email != valid_data['email']:
130 if old_email != valid_data['email']:
130 old = UserEmailMap.query() \
131 old = UserEmailMap.query() \
131 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.email == valid_data['email'])\
133 .filter(UserEmailMap.email == valid_data['email'])\
133 .first()
134 .first()
134 old.email = old_email
135 old.email = old_email
135 h.flash(_('Your account was updated successfully'), category='success')
136 h.flash(_('Your account was updated successfully'), category='success')
136 Session().commit()
137 Session().commit()
137 except forms.ValidationFailure as e:
138 except forms.ValidationFailure as e:
138 c.form = e
139 c.form = e
139 return self._get_template_context(c)
140 return self._get_template_context(c)
140 except Exception:
141 except Exception:
141 log.exception("Exception updating user")
142 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
143 h.flash(_('Error occurred during update of user'),
143 category='error')
144 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
145 raise HTTPFound(h.route_path('my_account_profile'))
145
146
146 @LoginRequired()
147 @LoginRequired()
147 @NotAnonymous()
148 @NotAnonymous()
148 def my_account_password(self):
149 def my_account_password(self):
149 c = self.load_default_context()
150 c = self.load_default_context()
150 c.active = 'password'
151 c.active = 'password'
151 c.extern_type = c.user.extern_type
152 c.extern_type = c.user.extern_type
152
153
153 schema = user_schema.ChangePasswordSchema().bind(
154 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
155 username=c.user.username)
155
156
156 form = forms.Form(
157 form = forms.Form(
157 schema,
158 schema,
158 action=h.route_path('my_account_password_update'),
159 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
160 buttons=(forms.buttons.save, forms.buttons.reset))
160
161
161 c.form = form
162 c.form = form
162 return self._get_template_context(c)
163 return self._get_template_context(c)
163
164
164 @LoginRequired()
165 @LoginRequired()
165 @NotAnonymous()
166 @NotAnonymous()
166 @CSRFRequired()
167 @CSRFRequired()
167 def my_account_password_update(self):
168 def my_account_password_update(self):
168 _ = self.request.translate
169 _ = self.request.translate
169 c = self.load_default_context()
170 c = self.load_default_context()
170 c.active = 'password'
171 c.active = 'password'
171 c.extern_type = c.user.extern_type
172 c.extern_type = c.user.extern_type
172
173
173 schema = user_schema.ChangePasswordSchema().bind(
174 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
175 username=c.user.username)
175
176
176 form = forms.Form(
177 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
179
179 if c.extern_type != 'rhodecode':
180 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
181 raise HTTPFound(self.request.route_path('my_account_password'))
181
182
182 controls = self.request.POST.items()
183 controls = list(self.request.POST.items())
183 try:
184 try:
184 valid_data = form.validate(controls)
185 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
187 c.user.update_userdata(force_password_change=False)
187 Session().commit()
188 Session().commit()
188 except forms.ValidationFailure as e:
189 except forms.ValidationFailure as e:
189 c.form = e
190 c.form = e
190 return self._get_template_context(c)
191 return self._get_template_context(c)
191
192
192 except Exception:
193 except Exception:
193 log.exception("Exception updating password")
194 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
195 h.flash(_('Error occurred during update of user password'),
195 category='error')
196 category='error')
196 else:
197 else:
197 instance = c.auth_user.get_instance()
198 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
199 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5(instance.password)})
200 {'password': md5_safe(instance.password)})
200 self.session.save()
201 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
202 h.flash(_("Successfully updated password"), category='success')
202
203
203 raise HTTPFound(self.request.route_path('my_account_password'))
204 raise HTTPFound(self.request.route_path('my_account_password'))
204
205
205 @LoginRequired()
206 @LoginRequired()
206 @NotAnonymous()
207 @NotAnonymous()
207 def my_account_auth_tokens(self):
208 def my_account_auth_tokens(self):
208 _ = self.request.translate
209 _ = self.request.translate
209
210
210 c = self.load_default_context()
211 c = self.load_default_context()
211 c.active = 'auth_tokens'
212 c.active = 'auth_tokens'
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.role_values = [
214 c.role_values = [
214 (x, AuthTokenModel.cls._get_role_name(x))
215 (x, AuthTokenModel.cls._get_role_name(x))
215 for x in AuthTokenModel.cls.ROLES]
216 for x in AuthTokenModel.cls.ROLES]
216 c.role_options = [(c.role_values, _("Role"))]
217 c.role_options = [(c.role_values, _("Role"))]
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user.user_id, show_expired=True)
219 c.user.user_id, show_expired=True)
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 return self._get_template_context(c)
221 return self._get_template_context(c)
221
222
222 @LoginRequired()
223 @LoginRequired()
223 @NotAnonymous()
224 @NotAnonymous()
224 @CSRFRequired()
225 @CSRFRequired()
225 def my_account_auth_tokens_view(self):
226 def my_account_auth_tokens_view(self):
226 _ = self.request.translate
227 _ = self.request.translate
227 c = self.load_default_context()
228 c = self.load_default_context()
228
229
229 auth_token_id = self.request.POST.get('auth_token_id')
230 auth_token_id = self.request.POST.get('auth_token_id')
230
231
231 if auth_token_id:
232 if auth_token_id:
232 token = UserApiKeys.get_or_404(auth_token_id)
233 token = UserApiKeys.get_or_404(auth_token_id)
233 if token.user.user_id != c.user.user_id:
234 if token.user.user_id != c.user.user_id:
234 raise HTTPNotFound()
235 raise HTTPNotFound()
235
236
236 return {
237 return {
237 'auth_token': token.api_key
238 'auth_token': token.api_key
238 }
239 }
239
240
240 def maybe_attach_token_scope(self, token):
241 def maybe_attach_token_scope(self, token):
241 # implemented in EE edition
242 # implemented in EE edition
242 pass
243 pass
243
244
244 @LoginRequired()
245 @LoginRequired()
245 @NotAnonymous()
246 @NotAnonymous()
246 @CSRFRequired()
247 @CSRFRequired()
247 def my_account_auth_tokens_add(self):
248 def my_account_auth_tokens_add(self):
248 _ = self.request.translate
249 _ = self.request.translate
249 c = self.load_default_context()
250 c = self.load_default_context()
250
251
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 description = self.request.POST.get('description')
253 description = self.request.POST.get('description')
253 role = self.request.POST.get('role')
254 role = self.request.POST.get('role')
254
255
255 token = UserModel().add_auth_token(
256 token = UserModel().add_auth_token(
256 user=c.user.user_id,
257 user=c.user.user_id,
257 lifetime_minutes=lifetime, role=role, description=description,
258 lifetime_minutes=lifetime, role=role, description=description,
258 scope_callback=self.maybe_attach_token_scope)
259 scope_callback=self.maybe_attach_token_scope)
259 token_data = token.get_api_data()
260 token_data = token.get_api_data()
260
261
261 audit_logger.store_web(
262 audit_logger.store_web(
262 'user.edit.token.add', action_data={
263 'user.edit.token.add', action_data={
263 'data': {'token': token_data, 'user': 'self'}},
264 'data': {'token': token_data, 'user': 'self'}},
264 user=self._rhodecode_user, )
265 user=self._rhodecode_user, )
265 Session().commit()
266 Session().commit()
266
267
267 h.flash(_("Auth token successfully created"), category='success')
268 h.flash(_("Auth token successfully created"), category='success')
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269 return HTTPFound(h.route_path('my_account_auth_tokens'))
269
270
270 @LoginRequired()
271 @LoginRequired()
271 @NotAnonymous()
272 @NotAnonymous()
272 @CSRFRequired()
273 @CSRFRequired()
273 def my_account_auth_tokens_delete(self):
274 def my_account_auth_tokens_delete(self):
274 _ = self.request.translate
275 _ = self.request.translate
275 c = self.load_default_context()
276 c = self.load_default_context()
276
277
277 del_auth_token = self.request.POST.get('del_auth_token')
278 del_auth_token = self.request.POST.get('del_auth_token')
278
279
279 if del_auth_token:
280 if del_auth_token:
280 token = UserApiKeys.get_or_404(del_auth_token)
281 token = UserApiKeys.get_or_404(del_auth_token)
281 token_data = token.get_api_data()
282 token_data = token.get_api_data()
282
283
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 audit_logger.store_web(
285 audit_logger.store_web(
285 'user.edit.token.delete', action_data={
286 'user.edit.token.delete', action_data={
286 'data': {'token': token_data, 'user': 'self'}},
287 'data': {'token': token_data, 'user': 'self'}},
287 user=self._rhodecode_user,)
288 user=self._rhodecode_user,)
288 Session().commit()
289 Session().commit()
289 h.flash(_("Auth token successfully deleted"), category='success')
290 h.flash(_("Auth token successfully deleted"), category='success')
290
291
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292 return HTTPFound(h.route_path('my_account_auth_tokens'))
292
293
293 @LoginRequired()
294 @LoginRequired()
294 @NotAnonymous()
295 @NotAnonymous()
295 def my_account_emails(self):
296 def my_account_emails(self):
296 _ = self.request.translate
297 _ = self.request.translate
297
298
298 c = self.load_default_context()
299 c = self.load_default_context()
299 c.active = 'emails'
300 c.active = 'emails'
300
301
301 c.user_email_map = UserEmailMap.query()\
302 c.user_email_map = UserEmailMap.query()\
302 .filter(UserEmailMap.user == c.user).all()
303 .filter(UserEmailMap.user == c.user).all()
303
304
304 schema = user_schema.AddEmailSchema().bind(
305 schema = user_schema.AddEmailSchema().bind(
305 username=c.user.username, user_emails=c.user.emails)
306 username=c.user.username, user_emails=c.user.emails)
306
307
307 form = forms.RcForm(schema,
308 form = forms.RcForm(schema,
308 action=h.route_path('my_account_emails_add'),
309 action=h.route_path('my_account_emails_add'),
309 buttons=(forms.buttons.save, forms.buttons.reset))
310 buttons=(forms.buttons.save, forms.buttons.reset))
310
311
311 c.form = form
312 c.form = form
312 return self._get_template_context(c)
313 return self._get_template_context(c)
313
314
314 @LoginRequired()
315 @LoginRequired()
315 @NotAnonymous()
316 @NotAnonymous()
316 @CSRFRequired()
317 @CSRFRequired()
317 def my_account_emails_add(self):
318 def my_account_emails_add(self):
318 _ = self.request.translate
319 _ = self.request.translate
319 c = self.load_default_context()
320 c = self.load_default_context()
320 c.active = 'emails'
321 c.active = 'emails'
321
322
322 schema = user_schema.AddEmailSchema().bind(
323 schema = user_schema.AddEmailSchema().bind(
323 username=c.user.username, user_emails=c.user.emails)
324 username=c.user.username, user_emails=c.user.emails)
324
325
325 form = forms.RcForm(
326 form = forms.RcForm(
326 schema, action=h.route_path('my_account_emails_add'),
327 schema, action=h.route_path('my_account_emails_add'),
327 buttons=(forms.buttons.save, forms.buttons.reset))
328 buttons=(forms.buttons.save, forms.buttons.reset))
328
329
329 controls = self.request.POST.items()
330 controls = list(self.request.POST.items())
330 try:
331 try:
331 valid_data = form.validate(controls)
332 valid_data = form.validate(controls)
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 audit_logger.store_web(
334 audit_logger.store_web(
334 'user.edit.email.add', action_data={
335 'user.edit.email.add', action_data={
335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 'data': {'email': valid_data['email'], 'user': 'self'}},
336 user=self._rhodecode_user,)
337 user=self._rhodecode_user,)
337 Session().commit()
338 Session().commit()
338 except formencode.Invalid as error:
339 except formencode.Invalid as error:
339 h.flash(h.escape(error.error_dict['email']), category='error')
340 h.flash(h.escape(error.error_dict['email']), category='error')
340 except forms.ValidationFailure as e:
341 except forms.ValidationFailure as e:
341 c.user_email_map = UserEmailMap.query() \
342 c.user_email_map = UserEmailMap.query() \
342 .filter(UserEmailMap.user == c.user).all()
343 .filter(UserEmailMap.user == c.user).all()
343 c.form = e
344 c.form = e
344 return self._get_template_context(c)
345 return self._get_template_context(c)
345 except Exception:
346 except Exception:
346 log.exception("Exception adding email")
347 log.exception("Exception adding email")
347 h.flash(_('Error occurred during adding email'),
348 h.flash(_('Error occurred during adding email'),
348 category='error')
349 category='error')
349 else:
350 else:
350 h.flash(_("Successfully added email"), category='success')
351 h.flash(_("Successfully added email"), category='success')
351
352
352 raise HTTPFound(self.request.route_path('my_account_emails'))
353 raise HTTPFound(self.request.route_path('my_account_emails'))
353
354
354 @LoginRequired()
355 @LoginRequired()
355 @NotAnonymous()
356 @NotAnonymous()
356 @CSRFRequired()
357 @CSRFRequired()
357 def my_account_emails_delete(self):
358 def my_account_emails_delete(self):
358 _ = self.request.translate
359 _ = self.request.translate
359 c = self.load_default_context()
360 c = self.load_default_context()
360
361
361 del_email_id = self.request.POST.get('del_email_id')
362 del_email_id = self.request.POST.get('del_email_id')
362 if del_email_id:
363 if del_email_id:
363 email = UserEmailMap.get_or_404(del_email_id).email
364 email = UserEmailMap.get_or_404(del_email_id).email
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 audit_logger.store_web(
366 audit_logger.store_web(
366 'user.edit.email.delete', action_data={
367 'user.edit.email.delete', action_data={
367 'data': {'email': email, 'user': 'self'}},
368 'data': {'email': email, 'user': 'self'}},
368 user=self._rhodecode_user,)
369 user=self._rhodecode_user,)
369 Session().commit()
370 Session().commit()
370 h.flash(_("Email successfully deleted"),
371 h.flash(_("Email successfully deleted"),
371 category='success')
372 category='success')
372 return HTTPFound(h.route_path('my_account_emails'))
373 return HTTPFound(h.route_path('my_account_emails'))
373
374
374 @LoginRequired()
375 @LoginRequired()
375 @NotAnonymous()
376 @NotAnonymous()
376 @CSRFRequired()
377 @CSRFRequired()
377 def my_account_notifications_test_channelstream(self):
378 def my_account_notifications_test_channelstream(self):
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 self._rhodecode_user.username, datetime.datetime.now())
380 self._rhodecode_user.username, datetime.datetime.now())
380 payload = {
381 payload = {
381 # 'channel': 'broadcast',
382 # 'channel': 'broadcast',
382 'type': 'message',
383 'type': 'message',
383 'timestamp': datetime.datetime.utcnow(),
384 'timestamp': datetime.datetime.utcnow(),
384 'user': 'system',
385 'user': 'system',
385 'pm_users': [self._rhodecode_user.username],
386 'pm_users': [self._rhodecode_user.username],
386 'message': {
387 'message': {
387 'message': message,
388 'message': message,
388 'level': 'info',
389 'level': 'info',
389 'topic': '/notifications'
390 'topic': '/notifications'
390 }
391 }
391 }
392 }
392
393
393 registry = self.request.registry
394 registry = self.request.registry
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396 channelstream_config = rhodecode_plugins.get('channelstream', {})
396
397
397 try:
398 try:
398 channelstream_request(channelstream_config, [payload], '/message')
399 channelstream_request(channelstream_config, [payload], '/message')
399 except ChannelstreamException as e:
400 except ChannelstreamException as e:
400 log.exception('Failed to send channelstream data')
401 log.exception('Failed to send channelstream data')
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 return {"response": 'Channelstream data sent. '
403 return {"response": 'Channelstream data sent. '
403 'You should see a new live message now.'}
404 'You should see a new live message now.'}
404
405
405 def _load_my_repos_data(self, watched=False):
406 def _load_my_repos_data(self, watched=False):
406
407
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408
409
409 if watched:
410 if watched:
410 # repos user watch
411 # repos user watch
411 repo_list = Session().query(
412 repo_list = Session().query(
412 Repository
413 Repository
413 ) \
414 ) \
414 .join(
415 .join(
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 ) \
417 ) \
417 .filter(
418 .filter(
418 UserFollowing.user_id == self._rhodecode_user.user_id
419 UserFollowing.user_id == self._rhodecode_user.user_id
419 ) \
420 ) \
420 .filter(or_(
421 .filter(or_(
421 # generate multiple IN to fix limitation problems
422 # generate multiple IN to fix limitation problems
422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 *in_filter_generator(Repository.repo_id, allowed_ids))
423 ) \
424 ) \
424 .order_by(Repository.repo_name) \
425 .order_by(Repository.repo_name) \
425 .all()
426 .all()
426
427
427 else:
428 else:
428 # repos user is owner of
429 # repos user is owner of
429 repo_list = Session().query(
430 repo_list = Session().query(
430 Repository
431 Repository
431 ) \
432 ) \
432 .filter(
433 .filter(
433 Repository.user_id == self._rhodecode_user.user_id
434 Repository.user_id == self._rhodecode_user.user_id
434 ) \
435 ) \
435 .filter(or_(
436 .filter(or_(
436 # generate multiple IN to fix limitation problems
437 # generate multiple IN to fix limitation problems
437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 *in_filter_generator(Repository.repo_id, allowed_ids))
438 ) \
439 ) \
439 .order_by(Repository.repo_name) \
440 .order_by(Repository.repo_name) \
440 .all()
441 .all()
441
442
442 _render = self.request.get_partial_renderer(
443 _render = self.request.get_partial_renderer(
443 'rhodecode:templates/data_table/_dt_elements.mako')
444 'rhodecode:templates/data_table/_dt_elements.mako')
444
445
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 short_name=False, admin=False)
448 short_name=False, admin=False)
448
449
449 repos_data = []
450 repos_data = []
450 for repo in repo_list:
451 for repo in repo_list:
451 row = {
452 row = {
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 repo.private, repo.archived, repo.fork),
454 repo.private, repo.archived, repo.fork),
454 "name_raw": repo.repo_name.lower(),
455 "name_raw": repo.repo_name.lower(),
455 }
456 }
456
457
457 repos_data.append(row)
458 repos_data.append(row)
458
459
459 # json used to render the grid
460 # json used to render the grid
460 return ext_json.str_json(repos_data)
461 return ext_json.str_json(repos_data)
461
462
462 @LoginRequired()
463 @LoginRequired()
463 @NotAnonymous()
464 @NotAnonymous()
464 def my_account_repos(self):
465 def my_account_repos(self):
465 c = self.load_default_context()
466 c = self.load_default_context()
466 c.active = 'repos'
467 c.active = 'repos'
467
468
468 # json used to render the grid
469 # json used to render the grid
469 c.data = self._load_my_repos_data()
470 c.data = self._load_my_repos_data()
470 return self._get_template_context(c)
471 return self._get_template_context(c)
471
472
472 @LoginRequired()
473 @LoginRequired()
473 @NotAnonymous()
474 @NotAnonymous()
474 def my_account_watched(self):
475 def my_account_watched(self):
475 c = self.load_default_context()
476 c = self.load_default_context()
476 c.active = 'watched'
477 c.active = 'watched'
477
478
478 # json used to render the grid
479 # json used to render the grid
479 c.data = self._load_my_repos_data(watched=True)
480 c.data = self._load_my_repos_data(watched=True)
480 return self._get_template_context(c)
481 return self._get_template_context(c)
481
482
482 @LoginRequired()
483 @LoginRequired()
483 @NotAnonymous()
484 @NotAnonymous()
484 def my_account_bookmarks(self):
485 def my_account_bookmarks(self):
485 c = self.load_default_context()
486 c = self.load_default_context()
486 c.active = 'bookmarks'
487 c.active = 'bookmarks'
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 self._rhodecode_db_user.user_id, cache=False)
489 self._rhodecode_db_user.user_id, cache=False)
489 return self._get_template_context(c)
490 return self._get_template_context(c)
490
491
491 def _process_bookmark_entry(self, entry, user_id):
492 def _process_bookmark_entry(self, entry, user_id):
492 position = safe_int(entry.get('position'))
493 position = safe_int(entry.get('position'))
493 cur_position = safe_int(entry.get('cur_position'))
494 cur_position = safe_int(entry.get('cur_position'))
494 if position is None:
495 if position is None:
495 return
496 return
496
497
497 # check if this is an existing entry
498 # check if this is an existing entry
498 is_new = False
499 is_new = False
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500
501
501 if db_entry and str2bool(entry.get('remove')):
502 if db_entry and str2bool(entry.get('remove')):
502 log.debug('Marked bookmark %s for deletion', db_entry)
503 log.debug('Marked bookmark %s for deletion', db_entry)
503 Session().delete(db_entry)
504 Session().delete(db_entry)
504 return
505 return
505
506
506 if not db_entry:
507 if not db_entry:
507 # new
508 # new
508 db_entry = UserBookmark()
509 db_entry = UserBookmark()
509 is_new = True
510 is_new = True
510
511
511 should_save = False
512 should_save = False
512 default_redirect_url = ''
513 default_redirect_url = ''
513
514
514 # save repo
515 # save repo
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 repo = Repository.get(entry['bookmark_repo'])
517 repo = Repository.get(entry['bookmark_repo'])
517 perm_check = HasRepoPermissionAny(
518 perm_check = HasRepoPermissionAny(
518 'repository.read', 'repository.write', 'repository.admin')
519 'repository.read', 'repository.write', 'repository.admin')
519 if repo and perm_check(repo_name=repo.repo_name):
520 if repo and perm_check(repo_name=repo.repo_name):
520 db_entry.repository = repo
521 db_entry.repository = repo
521 should_save = True
522 should_save = True
522 default_redirect_url = '${repo_url}'
523 default_redirect_url = '${repo_url}'
523 # save repo group
524 # save repo group
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 perm_check = HasRepoGroupPermissionAny(
527 perm_check = HasRepoGroupPermissionAny(
527 'group.read', 'group.write', 'group.admin')
528 'group.read', 'group.write', 'group.admin')
528
529
529 if repo_group and perm_check(group_name=repo_group.group_name):
530 if repo_group and perm_check(group_name=repo_group.group_name):
530 db_entry.repository_group = repo_group
531 db_entry.repository_group = repo_group
531 should_save = True
532 should_save = True
532 default_redirect_url = '${repo_group_url}'
533 default_redirect_url = '${repo_group_url}'
533 # save generic info
534 # save generic info
534 elif entry.get('title') and entry.get('redirect_url'):
535 elif entry.get('title') and entry.get('redirect_url'):
535 should_save = True
536 should_save = True
536
537
537 if should_save:
538 if should_save:
538 # mark user and position
539 # mark user and position
539 db_entry.user_id = user_id
540 db_entry.user_id = user_id
540 db_entry.position = position
541 db_entry.position = position
541 db_entry.title = entry.get('title')
542 db_entry.title = entry.get('title')
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544
545
545 Session().add(db_entry)
546 Session().add(db_entry)
546
547
547 @LoginRequired()
548 @LoginRequired()
548 @NotAnonymous()
549 @NotAnonymous()
549 @CSRFRequired()
550 @CSRFRequired()
550 def my_account_bookmarks_update(self):
551 def my_account_bookmarks_update(self):
551 _ = self.request.translate
552 _ = self.request.translate
552 c = self.load_default_context()
553 c = self.load_default_context()
553 c.active = 'bookmarks'
554 c.active = 'bookmarks'
554
555
555 controls = peppercorn.parse(self.request.POST.items())
556 controls = peppercorn.parse(self.request.POST.items())
556 user_id = c.user.user_id
557 user_id = c.user.user_id
557
558
558 # validate positions
559 # validate positions
559 positions = {}
560 positions = {}
560 for entry in controls.get('bookmarks', []):
561 for entry in controls.get('bookmarks', []):
561 position = safe_int(entry['position'])
562 position = safe_int(entry['position'])
562 if position is None:
563 if position is None:
563 continue
564 continue
564
565
565 if position in positions:
566 if position in positions:
566 h.flash(_("Position {} is defined twice. "
567 h.flash(_("Position {} is defined twice. "
567 "Please correct this error.").format(position), category='error')
568 "Please correct this error.").format(position), category='error')
568 return HTTPFound(h.route_path('my_account_bookmarks'))
569 return HTTPFound(h.route_path('my_account_bookmarks'))
569
570
570 entry['position'] = position
571 entry['position'] = position
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 positions[position] = entry
573 positions[position] = entry
573
574
574 try:
575 try:
575 for entry in positions.values():
576 for entry in positions.values():
576 self._process_bookmark_entry(entry, user_id)
577 self._process_bookmark_entry(entry, user_id)
577
578
578 Session().commit()
579 Session().commit()
579 h.flash(_("Update Bookmarks"), category='success')
580 h.flash(_("Update Bookmarks"), category='success')
580 except IntegrityError:
581 except IntegrityError:
581 h.flash(_("Failed to update bookmarks. "
582 h.flash(_("Failed to update bookmarks. "
582 "Make sure an unique position is used."), category='error')
583 "Make sure an unique position is used."), category='error')
583
584
584 return HTTPFound(h.route_path('my_account_bookmarks'))
585 return HTTPFound(h.route_path('my_account_bookmarks'))
585
586
586 @LoginRequired()
587 @LoginRequired()
587 @NotAnonymous()
588 @NotAnonymous()
588 def my_account_goto_bookmark(self):
589 def my_account_goto_bookmark(self):
589
590
590 bookmark_id = self.request.matchdict['bookmark_id']
591 bookmark_id = self.request.matchdict['bookmark_id']
591 user_bookmark = UserBookmark().query()\
592 user_bookmark = UserBookmark().query()\
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.position == bookmark_id).scalar()
594 .filter(UserBookmark.position == bookmark_id).scalar()
594
595
595 redirect_url = h.route_path('my_account_bookmarks')
596 redirect_url = h.route_path('my_account_bookmarks')
596 if not user_bookmark:
597 if not user_bookmark:
597 raise HTTPFound(redirect_url)
598 raise HTTPFound(redirect_url)
598
599
599 # repository set
600 # repository set
600 if user_bookmark.repository:
601 if user_bookmark.repository:
601 repo_name = user_bookmark.repository.repo_name
602 repo_name = user_bookmark.repository.repo_name
602 base_redirect_url = h.route_path(
603 base_redirect_url = h.route_path(
603 'repo_summary', repo_name=repo_name)
604 'repo_summary', repo_name=repo_name)
604 if user_bookmark.redirect_url and \
605 if user_bookmark.redirect_url and \
605 '${repo_url}' in user_bookmark.redirect_url:
606 '${repo_url}' in user_bookmark.redirect_url:
606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 redirect_url = string.Template(user_bookmark.redirect_url)\
607 .safe_substitute({'repo_url': base_redirect_url})
608 .safe_substitute({'repo_url': base_redirect_url})
608 else:
609 else:
609 redirect_url = base_redirect_url
610 redirect_url = base_redirect_url
610 # repository group set
611 # repository group set
611 elif user_bookmark.repository_group:
612 elif user_bookmark.repository_group:
612 repo_group_name = user_bookmark.repository_group.group_name
613 repo_group_name = user_bookmark.repository_group.group_name
613 base_redirect_url = h.route_path(
614 base_redirect_url = h.route_path(
614 'repo_group_home', repo_group_name=repo_group_name)
615 'repo_group_home', repo_group_name=repo_group_name)
615 if user_bookmark.redirect_url and \
616 if user_bookmark.redirect_url and \
616 '${repo_group_url}' in user_bookmark.redirect_url:
617 '${repo_group_url}' in user_bookmark.redirect_url:
617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 redirect_url = string.Template(user_bookmark.redirect_url)\
618 .safe_substitute({'repo_group_url': base_redirect_url})
619 .safe_substitute({'repo_group_url': base_redirect_url})
619 else:
620 else:
620 redirect_url = base_redirect_url
621 redirect_url = base_redirect_url
621 # custom URL set
622 # custom URL set
622 elif user_bookmark.redirect_url:
623 elif user_bookmark.redirect_url:
623 server_url = h.route_url('home').rstrip('/')
624 server_url = h.route_url('home').rstrip('/')
624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 redirect_url = string.Template(user_bookmark.redirect_url) \
625 .safe_substitute({'server_url': server_url})
626 .safe_substitute({'server_url': server_url})
626
627
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 raise HTTPFound(redirect_url)
629 raise HTTPFound(redirect_url)
629
630
630 @LoginRequired()
631 @LoginRequired()
631 @NotAnonymous()
632 @NotAnonymous()
632 def my_account_perms(self):
633 def my_account_perms(self):
633 c = self.load_default_context()
634 c = self.load_default_context()
634 c.active = 'perms'
635 c.active = 'perms'
635
636
636 c.perm_user = c.auth_user
637 c.perm_user = c.auth_user
637 return self._get_template_context(c)
638 return self._get_template_context(c)
638
639
639 @LoginRequired()
640 @LoginRequired()
640 @NotAnonymous()
641 @NotAnonymous()
641 def my_notifications(self):
642 def my_notifications(self):
642 c = self.load_default_context()
643 c = self.load_default_context()
643 c.active = 'notifications'
644 c.active = 'notifications'
644
645
645 return self._get_template_context(c)
646 return self._get_template_context(c)
646
647
647 @LoginRequired()
648 @LoginRequired()
648 @NotAnonymous()
649 @NotAnonymous()
649 @CSRFRequired()
650 @CSRFRequired()
650 def my_notifications_toggle_visibility(self):
651 def my_notifications_toggle_visibility(self):
651 user = self._rhodecode_db_user
652 user = self._rhodecode_db_user
652 new_status = not user.user_data.get('notification_status', True)
653 new_status = not user.user_data.get('notification_status', True)
653 user.update_userdata(notification_status=new_status)
654 user.update_userdata(notification_status=new_status)
654 Session().commit()
655 Session().commit()
655 return user.user_data['notification_status']
656 return user.user_data['notification_status']
656
657
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
659 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
661
661 _render = self.request.get_partial_renderer(
662 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
663 'rhodecode:templates/data_table/_dt_elements.mako')
663
664
664 if filter_type == 'awaiting_my_review':
665 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
667 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
668 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
669 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
670 order_dir=order_dir)
670
671
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
674 else:
674 pull_requests = PullRequestModel().get_im_participating_in(
675 pull_requests = PullRequestModel().get_im_participating_in(
675 user_id=self._rhodecode_user.user_id,
676 user_id=self._rhodecode_user.user_id,
676 statuses=statuses, query=search_q,
677 statuses=statuses, query=search_q,
677 offset=start, length=limit, order_by=order_by,
678 offset=start, length=limit, order_by=order_by,
678 order_dir=order_dir)
679 order_dir=order_dir)
679
680
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682
683
683 data = []
684 data = []
684 comments_model = CommentsModel()
685 comments_model = CommentsModel()
685 for pr in pull_requests:
686 for pr in pull_requests:
686 repo_id = pr.target_repo_id
687 repo_id = pr.target_repo_id
687 comments_count = comments_model.get_all_comments(
688 comments_count = comments_model.get_all_comments(
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 owned = pr.user_id == self._rhodecode_user.user_id
690 owned = pr.user_id == self._rhodecode_user.user_id
690
691
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
694 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
696 my_review_status = statuses[0][1].status
696
697
697 data.append({
698 data.append({
698 'target_repo': _render('pullrequest_target_repo',
699 'target_repo': _render('pullrequest_target_repo',
699 pr.target_repo.repo_name),
700 pr.target_repo.repo_name),
700 'name': _render('pullrequest_name',
701 'name': _render('pullrequest_name',
701 pr.pull_request_id, pr.pull_request_state,
702 pr.pull_request_id, pr.pull_request_state,
702 pr.work_in_progress, pr.target_repo.repo_name,
703 pr.work_in_progress, pr.target_repo.repo_name,
703 short=True),
704 short=True),
704 'name_raw': pr.pull_request_id,
705 'name_raw': pr.pull_request_id,
705 'status': _render('pullrequest_status',
706 'status': _render('pullrequest_status',
706 pr.calculated_review_status()),
707 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
708 'my_status': _render('pullrequest_status',
708 my_review_status),
709 my_review_status),
709 'title': _render('pullrequest_title', pr.title, pr.description),
710 'title': _render('pullrequest_title', pr.title, pr.description),
710 'description': h.escape(pr.description),
711 'description': h.escape(pr.description),
711 'updated_on': _render('pullrequest_updated_on',
712 'updated_on': _render('pullrequest_updated_on',
712 h.datetime_to_time(pr.updated_on),
713 h.datetime_to_time(pr.updated_on),
713 pr.versions_count),
714 pr.versions_count),
714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 'created_on': _render('pullrequest_updated_on',
716 'created_on': _render('pullrequest_updated_on',
716 h.datetime_to_time(pr.created_on)),
717 h.datetime_to_time(pr.created_on)),
717 'created_on_raw': h.datetime_to_time(pr.created_on),
718 'created_on_raw': h.datetime_to_time(pr.created_on),
718 'state': pr.pull_request_state,
719 'state': pr.pull_request_state,
719 'author': _render('pullrequest_author',
720 'author': _render('pullrequest_author',
720 pr.author.full_contact, ),
721 pr.author.full_contact, ),
721 'author_raw': pr.author.full_name,
722 'author_raw': pr.author.full_name,
722 'comments': _render('pullrequest_comments', comments_count),
723 'comments': _render('pullrequest_comments', comments_count),
723 'comments_raw': comments_count,
724 'comments_raw': comments_count,
724 'closed': pr.is_closed(),
725 'closed': pr.is_closed(),
725 'owned': owned
726 'owned': owned
726 })
727 })
727
728
728 # json used to render the grid
729 # json used to render the grid
729 data = ({
730 data = ({
730 'draw': draw,
731 'draw': draw,
731 'data': data,
732 'data': data,
732 'recordsTotal': pull_requests_total_count,
733 'recordsTotal': pull_requests_total_count,
733 'recordsFiltered': pull_requests_total_count,
734 'recordsFiltered': pull_requests_total_count,
734 })
735 })
735 return data
736 return data
736
737
737 @LoginRequired()
738 @LoginRequired()
738 @NotAnonymous()
739 @NotAnonymous()
739 def my_account_pullrequests(self):
740 def my_account_pullrequests(self):
740 c = self.load_default_context()
741 c = self.load_default_context()
741 c.active = 'pullrequests'
742 c.active = 'pullrequests'
742 req_get = self.request.GET
743 req_get = self.request.GET
743
744
744 c.closed = str2bool(req_get.get('closed'))
745 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
747
747 c.selected_filter = 'all'
748 c.selected_filter = 'all'
748 if c.closed:
749 if c.closed:
749 c.selected_filter = 'all_closed'
750 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
751 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
752 c.selected_filter = 'awaiting_my_review'
752
753
753 return self._get_template_context(c)
754 return self._get_template_context(c)
754
755
755 @LoginRequired()
756 @LoginRequired()
756 @NotAnonymous()
757 @NotAnonymous()
757 def my_account_pullrequests_data(self):
758 def my_account_pullrequests_data(self):
758 self.load_default_context()
759 self.load_default_context()
759 req_get = self.request.GET
760 req_get = self.request.GET
760
761
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 closed = str2bool(req_get.get('closed'))
763 closed = str2bool(req_get.get('closed'))
763
764
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 if closed:
766 if closed:
766 statuses += [PullRequest.STATUS_CLOSED]
767 statuses += [PullRequest.STATUS_CLOSED]
767
768
768 filter_type = \
769 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
770 'awaiting_my_review' if awaiting_my_review \
770 else None
771 else None
771
772
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 return data
774 return data
774
775
775 @LoginRequired()
776 @LoginRequired()
776 @NotAnonymous()
777 @NotAnonymous()
777 def my_account_user_group_membership(self):
778 def my_account_user_group_membership(self):
778 c = self.load_default_context()
779 c = self.load_default_context()
779 c.active = 'user_group_membership'
780 c.active = 'user_group_membership'
780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 for group in self._rhodecode_db_user.group_member]
782 for group in self._rhodecode_db_user.group_member]
782 c.user_groups = ext_json.str_json(groups)
783 c.user_groups = ext_json.str_json(groups)
783 return self._get_template_context(c)
784 return self._get_template_context(c)
@@ -1,93 +1,93 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 os
21 import os
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26
26
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.lib.auth import (
28 from rhodecode.lib.auth import (
29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 from rhodecode.lib import helpers as h, rc_cache
30 from rhodecode.lib import helpers as h, rc_cache
31 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
32 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
33 from rhodecode.model.scm import ScmModel
33 from rhodecode.model.scm import ScmModel
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class RepoCachesView(RepoAppView):
38 class RepoCachesView(RepoAppView):
39 def load_default_context(self):
39 def load_default_context(self):
40 c = self._get_local_tmpl_context()
40 c = self._get_local_tmpl_context()
41 return c
41 return c
42
42
43 @LoginRequired()
43 @LoginRequired()
44 @HasRepoPermissionAnyDecorator('repository.admin')
44 @HasRepoPermissionAnyDecorator('repository.admin')
45 def repo_caches(self):
45 def repo_caches(self):
46 c = self.load_default_context()
46 c = self.load_default_context()
47 c.active = 'caches'
47 c.active = 'caches'
48 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
48 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
49 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
49 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
50 c.cached_diff_size = 0
50 c.cached_diff_size = 0
51 if os.path.isdir(cached_diffs_dir):
51 if os.path.isdir(cached_diffs_dir):
52 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
52 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
53 c.shadow_repos = c.rhodecode_db_repo.shadow_repos()
53 c.shadow_repos = c.rhodecode_db_repo.shadow_repos()
54
54
55 cache_namespace_uid = 'cache_repo.{}'.format(self.db_repo.repo_id)
55 cache_namespace_uid = 'repo.{}'.format(self.db_repo.repo_id)
56 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
56 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
57 c.backend = c.region.backend
57 c.backend = c.region.backend
58 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
58 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
59
59
60 return self._get_template_context(c)
60 return self._get_template_context(c)
61
61
62 @LoginRequired()
62 @LoginRequired()
63 @HasRepoPermissionAnyDecorator('repository.admin')
63 @HasRepoPermissionAnyDecorator('repository.admin')
64 @CSRFRequired()
64 @CSRFRequired()
65 def repo_caches_purge(self):
65 def repo_caches_purge(self):
66 _ = self.request.translate
66 _ = self.request.translate
67 c = self.load_default_context()
67 c = self.load_default_context()
68 c.active = 'caches'
68 c.active = 'caches'
69 invalidated = 0
69 invalidated = 0
70
70
71 try:
71 try:
72 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
72 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
73 Session().commit()
73 Session().commit()
74 invalidated +=1
74 invalidated +=1
75 except Exception:
75 except Exception:
76 log.exception("Exception during cache invalidation")
76 log.exception("Exception during cache invalidation")
77 h.flash(_('An error occurred during cache invalidation'),
77 h.flash(_('An error occurred during cache invalidation'),
78 category='error')
78 category='error')
79
79
80 try:
80 try:
81 invalidated += 1
81 invalidated += 1
82 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
82 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
83 except Exception:
83 except Exception:
84 log.exception("Exception during vcsserver cache invalidation")
84 log.exception("Exception during vcsserver cache invalidation")
85 h.flash(_('An error occurred during vcsserver cache invalidation'),
85 h.flash(_('An error occurred during vcsserver cache invalidation'),
86 category='error')
86 category='error')
87
87
88 if invalidated:
88 if invalidated:
89 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
89 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
90 category='success')
90 category='success')
91
91
92 raise HTTPFound(h.route_path(
92 raise HTTPFound(h.route_path(
93 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
93 'edit_repo_caches', repo_name=self.db_repo_name))
@@ -1,355 +1,356 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
23 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24
24
25 from pyramid.renderers import render
25 from pyramid.renderers import render
26 from pyramid.response import Response
26 from pyramid.response import Response
27
27
28 from rhodecode.apps._base import RepoAppView
28 from rhodecode.apps._base import RepoAppView
29 import rhodecode.lib.helpers as h
29 import rhodecode.lib.helpers as h
30 from rhodecode.lib import ext_json
30 from rhodecode.lib import ext_json
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
32 LoginRequired, HasRepoPermissionAnyDecorator)
33
33
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.graphmod import _colored, _dagwalker
34 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.helpers import RepoPage
35 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
36 from rhodecode.lib.utils2 import str2bool
37 from rhodecode.lib.str_utils import safe_int, safe_str
38 from rhodecode.lib.vcs.exceptions import (
38 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
39 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 DEFAULT_CHANGELOG_SIZE = 20
44 DEFAULT_CHANGELOG_SIZE = 20
45
45
46
46
47 class RepoChangelogView(RepoAppView):
47 class RepoChangelogView(RepoAppView):
48
48
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
50 """
51 This is a safe way to get commit. If an error occurs it redirects to
51 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
52 tip with proper message
53
53
54 :param commit_id: id of commit to fetch
54 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
55 :param redirect_after: toggle redirection
56 """
56 """
57 _ = self.request.translate
57 _ = self.request.translate
58
58
59 try:
59 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return None
63 return None
64
64
65 h.flash(h.literal(
65 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
66 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
69
70 except (CommitDoesNotExistError, LookupError):
70 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
71 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
72 h.flash(msg, category='error')
73 raise HTTPNotFound()
73 raise HTTPNotFound()
74 except RepositoryError as e:
74 except RepositoryError as e:
75 h.flash(h.escape(safe_str(e)), category='error')
75 h.flash(h.escape(safe_str(e)), category='error')
76 raise HTTPNotFound()
76 raise HTTPNotFound()
77
77
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
79 """
80 Generates a DAG graph for repo
80 Generates a DAG graph for repo
81
81
82 :param repo: repo instance
82 :param repo: repo instance
83 :param commits: list of commits
83 :param commits: list of commits
84 """
84 """
85 if not commits:
85 if not commits:
86 return json.dumps([]), json.dumps([])
86 return ext_json.str_json([]), ext_json.str_json([])
87
87
88 def serialize(commit, parents=True):
88 def serialize(commit, parents=True):
89 data = dict(
89 data = dict(
90 raw_id=commit.raw_id,
90 raw_id=commit.raw_id,
91 idx=commit.idx,
91 idx=commit.idx,
92 branch=None,
92 branch=None,
93 )
93 )
94 if parents:
94 if parents:
95 data['parents'] = [
95 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
96 serialize(x, parents=False) for x in commit.parents]
97 return data
97 return data
98
98
99 prev_data = prev_data or []
99 prev_data = prev_data or []
100 next_data = next_data or []
100 next_data = next_data or []
101
101
102 current = [serialize(x) for x in commits]
102 current = [serialize(x) for x in commits]
103
103 commits = prev_data + current + next_data
104 commits = prev_data + current + next_data
104
105
105 dag = _dagwalker(repo, commits)
106 dag = _dagwalker(repo, commits)
106
107
107 data = [[commit_id, vtx, edges, branch]
108 data = [[commit_id, vtx, edges, branch]
108 for commit_id, vtx, edges, branch in _colored(dag)]
109 for commit_id, vtx, edges, branch in _colored(dag)]
109 return ext_json.str_json(data), ext_json.str_json(current)
110 return ext_json.str_json(data), ext_json.str_json(current)
110
111
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
114 h.flash(u'Branch {} is not found.'.format(h.escape(safe_str(branch_name))),
114 category='warning')
115 category='warning')
115 redirect_url = h.route_path(
116 redirect_url = h.route_path(
116 'repo_commits_file', repo_name=repo_name,
117 'repo_commits_file', repo_name=repo_name,
117 commit_id=branch_name, f_path=f_path or '')
118 commit_id=branch_name, f_path=f_path or '')
118 raise HTTPFound(redirect_url)
119 raise HTTPFound(redirect_url)
119
120
120 def _load_changelog_data(
121 def _load_changelog_data(
121 self, c, collection, page, chunk_size, branch_name=None,
122 self, c, collection, page, chunk_size, branch_name=None,
122 dynamic=False, f_path=None, commit_id=None):
123 dynamic=False, f_path=None, commit_id=None):
123
124
124 def url_generator(page_num):
125 def url_generator(page_num):
125 query_params = {
126 query_params = {
126 'page': page_num
127 'page': page_num
127 }
128 }
128
129
129 if branch_name:
130 if branch_name:
130 query_params.update({
131 query_params.update({
131 'branch': branch_name
132 'branch': branch_name
132 })
133 })
133
134
134 if f_path:
135 if f_path:
135 # changelog for file
136 # changelog for file
136 return h.route_path(
137 return h.route_path(
137 'repo_commits_file',
138 'repo_commits_file',
138 repo_name=c.rhodecode_db_repo.repo_name,
139 repo_name=c.rhodecode_db_repo.repo_name,
139 commit_id=commit_id, f_path=f_path,
140 commit_id=commit_id, f_path=f_path,
140 _query=query_params)
141 _query=query_params)
141 else:
142 else:
142 return h.route_path(
143 return h.route_path(
143 'repo_commits',
144 'repo_commits',
144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145
146
146 c.total_cs = len(collection)
147 c.total_cs = len(collection)
147 c.showing_commits = min(chunk_size, c.total_cs)
148 c.showing_commits = min(chunk_size, c.total_cs)
148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 items_per_page=chunk_size, url_maker=url_generator)
150 items_per_page=chunk_size, url_maker=url_generator)
150
151
151 c.next_page = c.pagination.next_page
152 c.next_page = c.pagination.next_page
152 c.prev_page = c.pagination.previous_page
153 c.prev_page = c.pagination.previous_page
153
154
154 if dynamic:
155 if dynamic:
155 if self.request.GET.get('chunk') != 'next':
156 if self.request.GET.get('chunk') != 'next':
156 c.next_page = None
157 c.next_page = None
157 if self.request.GET.get('chunk') != 'prev':
158 if self.request.GET.get('chunk') != 'prev':
158 c.prev_page = None
159 c.prev_page = None
159
160
160 page_commit_ids = [x.raw_id for x in c.pagination]
161 page_commit_ids = [x.raw_id for x in c.pagination]
161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163
164
164 def load_default_context(self):
165 def load_default_context(self):
165 c = self._get_local_tmpl_context(include_app_defaults=True)
166 c = self._get_local_tmpl_context(include_app_defaults=True)
166
167
167 c.rhodecode_repo = self.rhodecode_vcs_repo
168 c.rhodecode_repo = self.rhodecode_vcs_repo
168
169
169 return c
170 return c
170
171
171 @LoginRequired()
172 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
174 def repo_changelog(self):
175 def repo_changelog(self):
175 c = self.load_default_context()
176 c = self.load_default_context()
176
177
177 commit_id = self.request.matchdict.get('commit_id')
178 commit_id = self.request.matchdict.get('commit_id')
178 f_path = self._get_f_path(self.request.matchdict)
179 f_path = self._get_f_path(self.request.matchdict)
179 show_hidden = str2bool(self.request.GET.get('evolve'))
180 show_hidden = str2bool(self.request.GET.get('evolve'))
180
181
181 chunk_size = 20
182 chunk_size = 20
182
183
183 c.branch_name = branch_name = self.request.GET.get('branch') or ''
184 c.branch_name = branch_name = self.request.GET.get('branch') or ''
184 c.book_name = book_name = self.request.GET.get('bookmark') or ''
185 c.book_name = book_name = self.request.GET.get('bookmark') or ''
185 c.f_path = f_path
186 c.f_path = f_path
186 c.commit_id = commit_id
187 c.commit_id = commit_id
187 c.show_hidden = show_hidden
188 c.show_hidden = show_hidden
188
189
189 hist_limit = safe_int(self.request.GET.get('limit')) or None
190 hist_limit = safe_int(self.request.GET.get('limit')) or None
190
191
191 p = safe_int(self.request.GET.get('page', 1), 1)
192 p = safe_int(self.request.GET.get('page', 1), 1)
192
193
193 c.selected_name = branch_name or book_name
194 c.selected_name = branch_name or book_name
194 if not commit_id and branch_name:
195 if not commit_id and branch_name:
195 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
196 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
196
197
197 c.changelog_for_path = f_path
198 c.changelog_for_path = f_path
198 pre_load = self.get_commit_preload_attrs()
199 pre_load = self.get_commit_preload_attrs()
199
200
200 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
201 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
201
202
202 try:
203 try:
203 if f_path:
204 if f_path:
204 log.debug('generating changelog for path %s', f_path)
205 log.debug('generating changelog for path %s', f_path)
205 # get the history for the file !
206 # get the history for the file !
206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207
208
208 try:
209 try:
209 collection = base_commit.get_path_history(
210 collection = base_commit.get_path_history(
210 f_path, limit=hist_limit, pre_load=pre_load)
211 f_path, limit=hist_limit, pre_load=pre_load)
211 if collection and partial_xhr:
212 if collection and partial_xhr:
212 # for ajax call we remove first one since we're looking
213 # for ajax call we remove first one since we're looking
213 # at it right now in the context of a file commit
214 # at it right now in the context of a file commit
214 collection.pop(0)
215 collection.pop(0)
215 except (NodeDoesNotExistError, CommitError):
216 except (NodeDoesNotExistError, CommitError):
216 # this node is not present at tip!
217 # this node is not present at tip!
217 try:
218 try:
218 commit = self._get_commit_or_redirect(commit_id)
219 commit = self._get_commit_or_redirect(commit_id)
219 collection = commit.get_path_history(f_path)
220 collection = commit.get_path_history(f_path)
220 except RepositoryError as e:
221 except RepositoryError as e:
221 h.flash(safe_str(e), category='warning')
222 h.flash(safe_str(e), category='warning')
222 redirect_url = h.route_path(
223 redirect_url = h.route_path(
223 'repo_commits', repo_name=self.db_repo_name)
224 'repo_commits', repo_name=self.db_repo_name)
224 raise HTTPFound(redirect_url)
225 raise HTTPFound(redirect_url)
225 collection = list(reversed(collection))
226 collection = list(reversed(collection))
226 else:
227 else:
227 collection = self.rhodecode_vcs_repo.get_commits(
228 collection = self.rhodecode_vcs_repo.get_commits(
228 branch_name=branch_name, show_hidden=show_hidden,
229 branch_name=branch_name, show_hidden=show_hidden,
229 pre_load=pre_load, translate_tags=False)
230 pre_load=pre_load, translate_tags=False)
230
231
231 self._load_changelog_data(
232 self._load_changelog_data(
232 c, collection, p, chunk_size, c.branch_name,
233 c, collection, p, chunk_size, c.branch_name,
233 f_path=f_path, commit_id=commit_id)
234 f_path=f_path, commit_id=commit_id)
234
235
235 except EmptyRepositoryError as e:
236 except EmptyRepositoryError as e:
236 h.flash(h.escape(safe_str(e)), category='warning')
237 h.flash(h.escape(safe_str(e)), category='warning')
237 raise HTTPFound(
238 raise HTTPFound(
238 h.route_path('repo_summary', repo_name=self.db_repo_name))
239 h.route_path('repo_summary', repo_name=self.db_repo_name))
239 except HTTPFound:
240 except HTTPFound:
240 raise
241 raise
241 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
242 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
242 log.exception(safe_str(e))
243 log.exception(safe_str(e))
243 h.flash(h.escape(safe_str(e)), category='error')
244 h.flash(h.escape(safe_str(e)), category='error')
244
245
245 if commit_id:
246 if commit_id:
246 # from single commit page, we redirect to main commits
247 # from single commit page, we redirect to main commits
247 raise HTTPFound(
248 raise HTTPFound(
248 h.route_path('repo_commits', repo_name=self.db_repo_name))
249 h.route_path('repo_commits', repo_name=self.db_repo_name))
249 else:
250 else:
250 # otherwise we redirect to summary
251 # otherwise we redirect to summary
251 raise HTTPFound(
252 raise HTTPFound(
252 h.route_path('repo_summary', repo_name=self.db_repo_name))
253 h.route_path('repo_summary', repo_name=self.db_repo_name))
253
254
254
255
255
256
256 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
257 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
257 # case when loading dynamic file history in file view
258 # case when loading dynamic file history in file view
258 # loading from ajax, we don't want the first result, it's popped
259 # loading from ajax, we don't want the first result, it's popped
259 # in the code above
260 # in the code above
260 html = render(
261 html = render(
261 'rhodecode:templates/commits/changelog_file_history.mako',
262 'rhodecode:templates/commits/changelog_file_history.mako',
262 self._get_template_context(c), self.request)
263 self._get_template_context(c), self.request)
263 return Response(html)
264 return Response(html)
264
265
265 commit_ids = []
266 commit_ids = []
266 if not f_path:
267 if not f_path:
267 # only load graph data when not in file history mode
268 # only load graph data when not in file history mode
268 commit_ids = c.pagination
269 commit_ids = c.pagination
269
270
270 c.graph_data, c.graph_commits = self._graph(
271 c.graph_data, c.graph_commits = self._graph(
271 self.rhodecode_vcs_repo, commit_ids)
272 self.rhodecode_vcs_repo, commit_ids)
272
273
273 return self._get_template_context(c)
274 return self._get_template_context(c)
274
275
275 @LoginRequired()
276 @LoginRequired()
276 @HasRepoPermissionAnyDecorator(
277 @HasRepoPermissionAnyDecorator(
277 'repository.read', 'repository.write', 'repository.admin')
278 'repository.read', 'repository.write', 'repository.admin')
278 def repo_commits_elements(self):
279 def repo_commits_elements(self):
279 c = self.load_default_context()
280 c = self.load_default_context()
280 commit_id = self.request.matchdict.get('commit_id')
281 commit_id = self.request.matchdict.get('commit_id')
281 f_path = self._get_f_path(self.request.matchdict)
282 f_path = self._get_f_path(self.request.matchdict)
282 show_hidden = str2bool(self.request.GET.get('evolve'))
283 show_hidden = str2bool(self.request.GET.get('evolve'))
283
284
284 chunk_size = 20
285 chunk_size = 20
285 hist_limit = safe_int(self.request.GET.get('limit')) or None
286 hist_limit = safe_int(self.request.GET.get('limit')) or None
286
287
287 def wrap_for_error(err):
288 def wrap_for_error(err):
288 html = '<tr>' \
289 html = '<tr>' \
289 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
290 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
290 '</tr>'.format(err)
291 '</tr>'.format(err)
291 return Response(html)
292 return Response(html)
292
293
293 c.branch_name = branch_name = self.request.GET.get('branch') or ''
294 c.branch_name = branch_name = self.request.GET.get('branch') or ''
294 c.book_name = book_name = self.request.GET.get('bookmark') or ''
295 c.book_name = book_name = self.request.GET.get('bookmark') or ''
295 c.f_path = f_path
296 c.f_path = f_path
296 c.commit_id = commit_id
297 c.commit_id = commit_id
297 c.show_hidden = show_hidden
298 c.show_hidden = show_hidden
298
299
299 c.selected_name = branch_name or book_name
300 c.selected_name = branch_name or book_name
300 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
301 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
301 return wrap_for_error(
302 return wrap_for_error(
302 safe_str('Branch: {} is not valid'.format(branch_name)))
303 safe_str('Branch: {} is not valid'.format(branch_name)))
303
304
304 pre_load = self.get_commit_preload_attrs()
305 pre_load = self.get_commit_preload_attrs()
305
306
306 if f_path:
307 if f_path:
307 try:
308 try:
308 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
309 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
309 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
310 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
310 log.exception(safe_str(e))
311 log.exception(safe_str(e))
311 raise HTTPFound(
312 raise HTTPFound(
312 h.route_path('repo_commits', repo_name=self.db_repo_name))
313 h.route_path('repo_commits', repo_name=self.db_repo_name))
313
314
314 collection = base_commit.get_path_history(
315 collection = base_commit.get_path_history(
315 f_path, limit=hist_limit, pre_load=pre_load)
316 f_path, limit=hist_limit, pre_load=pre_load)
316 collection = list(reversed(collection))
317 collection = list(reversed(collection))
317 else:
318 else:
318 collection = self.rhodecode_vcs_repo.get_commits(
319 collection = self.rhodecode_vcs_repo.get_commits(
319 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
320 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
320 translate_tags=False)
321 translate_tags=False)
321
322
322 p = safe_int(self.request.GET.get('page', 1), 1)
323 p = safe_int(self.request.GET.get('page', 1), 1)
323 try:
324 try:
324 self._load_changelog_data(
325 self._load_changelog_data(
325 c, collection, p, chunk_size, dynamic=True,
326 c, collection, p, chunk_size, dynamic=True,
326 f_path=f_path, commit_id=commit_id)
327 f_path=f_path, commit_id=commit_id)
327 except EmptyRepositoryError as e:
328 except EmptyRepositoryError as e:
328 return wrap_for_error(safe_str(e))
329 return wrap_for_error(safe_str(e))
329 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
330 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
330 log.exception('Failed to fetch commits')
331 log.exception('Failed to fetch commits')
331 return wrap_for_error(safe_str(e))
332 return wrap_for_error(safe_str(e))
332
333
333 prev_data = None
334 prev_data = None
334 next_data = None
335 next_data = None
335
336
336 try:
337 try:
337 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
338 prev_graph = ext_json.json.loads(self.request.POST.get('graph') or '{}')
338 except json.JSONDecodeError:
339 except ext_json.json.JSONDecodeError:
339 prev_graph = {}
340 prev_graph = {}
340
341
341 if self.request.GET.get('chunk') == 'prev':
342 if self.request.GET.get('chunk') == 'prev':
342 next_data = prev_graph
343 next_data = prev_graph
343 elif self.request.GET.get('chunk') == 'next':
344 elif self.request.GET.get('chunk') == 'next':
344 prev_data = prev_graph
345 prev_data = prev_graph
345
346
346 commit_ids = []
347 commit_ids = []
347 if not f_path:
348 if not f_path:
348 # only load graph data when not in file history mode
349 # only load graph data when not in file history mode
349 commit_ids = c.pagination
350 commit_ids = c.pagination
350
351
351 c.graph_data, c.graph_commits = self._graph(
352 c.graph_data, c.graph_commits = self._graph(
352 self.rhodecode_vcs_repo, commit_ids,
353 self.rhodecode_vcs_repo, commit_ids,
353 prev_data=prev_data, next_data=next_data)
354 prev_data=prev_data, next_data=next_data)
354
355
355 return self._get_template_context(c)
356 return self._get_template_context(c)
@@ -1,818 +1,819 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import logging
20 import logging
21 import collections
21 import collections
22
22
23 from pyramid.httpexceptions import (
23 from pyramid.httpexceptions import (
24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 from pyramid.renderers import render
25 from pyramid.renderers import render
26 from pyramid.response import Response
26 from pyramid.response import Response
27
27
28 from rhodecode.apps._base import RepoAppView
28 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps.file_store import utils as store_utils
29 from rhodecode.apps.file_store import utils as store_utils
30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31
31
32 from rhodecode.lib import diffs, codeblocks, channelstream
32 from rhodecode.lib import diffs, codeblocks, channelstream
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 from rhodecode.lib import ext_json
35 from rhodecode.lib import ext_json
36 from collections import OrderedDict
36 from collections import OrderedDict
37 from rhodecode.lib.diffs import (
37 from rhodecode.lib.diffs import (
38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 get_diff_whitespace_flag)
39 get_diff_whitespace_flag)
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
42 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 RepositoryError, CommitDoesNotExistError)
45 RepositoryError, CommitDoesNotExistError)
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 ChangesetCommentHistory
47 ChangesetCommentHistory
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.settings import VcsSettingsModel
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 def _update_with_GET(params, request):
56 def _update_with_GET(params, request):
57 for k in ['diff1', 'diff2', 'diff']:
57 for k in ['diff1', 'diff2', 'diff']:
58 params[k] += request.GET.getall(k)
58 params[k] += request.GET.getall(k)
59
59
60
60
61 class RepoCommitsView(RepoAppView):
61 class RepoCommitsView(RepoAppView):
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.rhodecode_repo = self.rhodecode_vcs_repo
64 c.rhodecode_repo = self.rhodecode_vcs_repo
65
65
66 return c
66 return c
67
67
68 def _is_diff_cache_enabled(self, target_repo):
68 def _is_diff_cache_enabled(self, target_repo):
69 caching_enabled = self._get_general_setting(
69 caching_enabled = self._get_general_setting(
70 target_repo, 'rhodecode_diff_cache')
70 target_repo, 'rhodecode_diff_cache')
71 log.debug('Diff caching enabled: %s', caching_enabled)
71 log.debug('Diff caching enabled: %s', caching_enabled)
72 return caching_enabled
72 return caching_enabled
73
73
74 def _commit(self, commit_id_range, method):
74 def _commit(self, commit_id_range, method):
75 _ = self.request.translate
75 _ = self.request.translate
76 c = self.load_default_context()
76 c = self.load_default_context()
77 c.fulldiff = self.request.GET.get('fulldiff')
77 c.fulldiff = self.request.GET.get('fulldiff')
78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79
79
80 # fetch global flags of ignore ws or context lines
80 # fetch global flags of ignore ws or context lines
81 diff_context = get_diff_context(self.request)
81 diff_context = get_diff_context(self.request)
82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83
83
84 # diff_limit will cut off the whole diff if the limit is applied
84 # diff_limit will cut off the whole diff if the limit is applied
85 # otherwise it will just hide the big files from the front-end
85 # otherwise it will just hide the big files from the front-end
86 diff_limit = c.visual.cut_off_limit_diff
86 diff_limit = c.visual.cut_off_limit_diff
87 file_limit = c.visual.cut_off_limit_file
87 file_limit = c.visual.cut_off_limit_file
88
88
89 # get ranges of commit ids if preset
89 # get ranges of commit ids if preset
90 commit_range = commit_id_range.split('...')[:2]
90 commit_range = commit_id_range.split('...')[:2]
91
91
92 try:
92 try:
93 pre_load = ['affected_files', 'author', 'branch', 'date',
93 pre_load = ['affected_files', 'author', 'branch', 'date',
94 'message', 'parents']
94 'message', 'parents']
95 if self.rhodecode_vcs_repo.alias == 'hg':
95 if self.rhodecode_vcs_repo.alias == 'hg':
96 pre_load += ['hidden', 'obsolete', 'phase']
96 pre_load += ['hidden', 'obsolete', 'phase']
97
97
98 if len(commit_range) == 2:
98 if len(commit_range) == 2:
99 commits = self.rhodecode_vcs_repo.get_commits(
99 commits = self.rhodecode_vcs_repo.get_commits(
100 start_id=commit_range[0], end_id=commit_range[1],
100 start_id=commit_range[0], end_id=commit_range[1],
101 pre_load=pre_load, translate_tags=False)
101 pre_load=pre_load, translate_tags=False)
102 commits = list(commits)
102 commits = list(commits)
103 else:
103 else:
104 commits = [self.rhodecode_vcs_repo.get_commit(
104 commits = [self.rhodecode_vcs_repo.get_commit(
105 commit_id=commit_id_range, pre_load=pre_load)]
105 commit_id=commit_id_range, pre_load=pre_load)]
106
106
107 c.commit_ranges = commits
107 c.commit_ranges = commits
108 if not c.commit_ranges:
108 if not c.commit_ranges:
109 raise RepositoryError('The commit range returned an empty result')
109 raise RepositoryError('The commit range returned an empty result')
110 except CommitDoesNotExistError as e:
110 except CommitDoesNotExistError as e:
111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 h.flash(msg, category='error')
112 h.flash(msg, category='error')
113 raise HTTPNotFound()
113 raise HTTPNotFound()
114 except Exception:
114 except Exception:
115 log.exception("General failure")
115 log.exception("General failure")
116 raise HTTPNotFound()
116 raise HTTPNotFound()
117 single_commit = len(c.commit_ranges) == 1
117 single_commit = len(c.commit_ranges) == 1
118
118
119 if redirect_to_combined and not single_commit:
119 if redirect_to_combined and not single_commit:
120 source_ref = getattr(c.commit_ranges[0].parents[0]
120 source_ref = getattr(c.commit_ranges[0].parents[0]
121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 target_ref = c.commit_ranges[-1].raw_id
122 target_ref = c.commit_ranges[-1].raw_id
123 next_url = h.route_path(
123 next_url = h.route_path(
124 'repo_compare',
124 'repo_compare',
125 repo_name=c.repo_name,
125 repo_name=c.repo_name,
126 source_ref_type='rev',
126 source_ref_type='rev',
127 source_ref=source_ref,
127 source_ref=source_ref,
128 target_ref_type='rev',
128 target_ref_type='rev',
129 target_ref=target_ref)
129 target_ref=target_ref)
130 raise HTTPFound(next_url)
130 raise HTTPFound(next_url)
131
131
132 c.changes = OrderedDict()
132 c.changes = OrderedDict()
133 c.lines_added = 0
133 c.lines_added = 0
134 c.lines_deleted = 0
134 c.lines_deleted = 0
135
135
136 # auto collapse if we have more than limit
136 # auto collapse if we have more than limit
137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139
139
140 c.commit_statuses = ChangesetStatus.STATUSES
140 c.commit_statuses = ChangesetStatus.STATUSES
141 c.inline_comments = []
141 c.inline_comments = []
142 c.files = []
142 c.files = []
143
143
144 c.comments = []
144 c.comments = []
145 c.unresolved_comments = []
145 c.unresolved_comments = []
146 c.resolved_comments = []
146 c.resolved_comments = []
147
147
148 # Single commit
148 # Single commit
149 if single_commit:
149 if single_commit:
150 commit = c.commit_ranges[0]
150 commit = c.commit_ranges[0]
151 c.comments = CommentsModel().get_comments(
151 c.comments = CommentsModel().get_comments(
152 self.db_repo.repo_id,
152 self.db_repo.repo_id,
153 revision=commit.raw_id)
153 revision=commit.raw_id)
154
154
155 # comments from PR
155 # comments from PR
156 statuses = ChangesetStatusModel().get_statuses(
156 statuses = ChangesetStatusModel().get_statuses(
157 self.db_repo.repo_id, commit.raw_id,
157 self.db_repo.repo_id, commit.raw_id,
158 with_revisions=True)
158 with_revisions=True)
159
159
160 prs = set()
160 prs = set()
161 reviewers = list()
161 reviewers = list()
162 reviewers_duplicates = set() # to not have duplicates from multiple votes
162 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 for c_status in statuses:
163 for c_status in statuses:
164
164
165 # extract associated pull-requests from votes
165 # extract associated pull-requests from votes
166 if c_status.pull_request:
166 if c_status.pull_request:
167 prs.add(c_status.pull_request)
167 prs.add(c_status.pull_request)
168
168
169 # extract reviewers
169 # extract reviewers
170 _user_id = c_status.author.user_id
170 _user_id = c_status.author.user_id
171 if _user_id not in reviewers_duplicates:
171 if _user_id not in reviewers_duplicates:
172 reviewers.append(
172 reviewers.append(
173 StrictAttributeDict({
173 StrictAttributeDict({
174 'user': c_status.author,
174 'user': c_status.author,
175
175
176 # fake attributed for commit, page that we don't have
176 # fake attributed for commit, page that we don't have
177 # but we share the display with PR page
177 # but we share the display with PR page
178 'mandatory': False,
178 'mandatory': False,
179 'reasons': [],
179 'reasons': [],
180 'rule_user_group_data': lambda: None
180 'rule_user_group_data': lambda: None
181 })
181 })
182 )
182 )
183 reviewers_duplicates.add(_user_id)
183 reviewers_duplicates.add(_user_id)
184
184
185 c.reviewers_count = len(reviewers)
185 c.reviewers_count = len(reviewers)
186 c.observers_count = 0
186 c.observers_count = 0
187
187
188 # from associated statuses, check the pull requests, and
188 # from associated statuses, check the pull requests, and
189 # show comments from them
189 # show comments from them
190 for pr in prs:
190 for pr in prs:
191 c.comments.extend(pr.comments)
191 c.comments.extend(pr.comments)
192
192
193 c.unresolved_comments = CommentsModel()\
193 c.unresolved_comments = CommentsModel()\
194 .get_commit_unresolved_todos(commit.raw_id)
194 .get_commit_unresolved_todos(commit.raw_id)
195 c.resolved_comments = CommentsModel()\
195 c.resolved_comments = CommentsModel()\
196 .get_commit_resolved_todos(commit.raw_id)
196 .get_commit_resolved_todos(commit.raw_id)
197
197
198 c.inline_comments_flat = CommentsModel()\
198 c.inline_comments_flat = CommentsModel()\
199 .get_commit_inline_comments(commit.raw_id)
199 .get_commit_inline_comments(commit.raw_id)
200
200
201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 statuses, reviewers)
202 statuses, reviewers)
203
203
204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205
205
206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207
207
208 for review_obj, member, reasons, mandatory, status in review_statuses:
208 for review_obj, member, reasons, mandatory, status in review_statuses:
209 member_reviewer = h.reviewer_as_json(
209 member_reviewer = h.reviewer_as_json(
210 member, reasons=reasons, mandatory=mandatory, role=None,
210 member, reasons=reasons, mandatory=mandatory, role=None,
211 user_group=None
211 user_group=None
212 )
212 )
213
213
214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 member_reviewer['review_status'] = current_review_status
215 member_reviewer['review_status'] = current_review_status
216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 member_reviewer['allowed_to_update'] = False
217 member_reviewer['allowed_to_update'] = False
218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219
219
220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
221
221
222 # NOTE(marcink): this uses the same voting logic as in pull-requests
222 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225
225
226 diff = None
226 diff = None
227 # Iterate over ranges (default commit view is always one commit)
227 # Iterate over ranges (default commit view is always one commit)
228 for commit in c.commit_ranges:
228 for commit in c.commit_ranges:
229 c.changes[commit.raw_id] = []
229 c.changes[commit.raw_id] = []
230
230
231 commit2 = commit
231 commit2 = commit
232 commit1 = commit.first_parent
232 commit1 = commit.first_parent
233
233
234 if method == 'show':
234 if method == 'show':
235 inline_comments = CommentsModel().get_inline_comments(
235 inline_comments = CommentsModel().get_inline_comments(
236 self.db_repo.repo_id, revision=commit.raw_id)
236 self.db_repo.repo_id, revision=commit.raw_id)
237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 inline_comments))
238 inline_comments))
239 c.inline_comments = inline_comments
239 c.inline_comments = inline_comments
240
240
241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 self.db_repo)
242 self.db_repo)
243 cache_file_path = diff_cache_exist(
243 cache_file_path = diff_cache_exist(
244 cache_path, 'diff', commit.raw_id,
244 cache_path, 'diff', commit.raw_id,
245 hide_whitespace_changes, diff_context, c.fulldiff)
245 hide_whitespace_changes, diff_context, c.fulldiff)
246
246
247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 force_recache = str2bool(self.request.GET.get('force_recache'))
248 force_recache = str2bool(self.request.GET.get('force_recache'))
249
249
250 cached_diff = None
250 cached_diff = None
251 if caching_enabled:
251 if caching_enabled:
252 cached_diff = load_cached_diff(cache_file_path)
252 cached_diff = load_cached_diff(cache_file_path)
253
253
254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 if not force_recache and has_proper_diff_cache:
255 if not force_recache and has_proper_diff_cache:
256 diffset = cached_diff['diff']
256 diffset = cached_diff['diff']
257 else:
257 else:
258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 commit1, commit2,
259 commit1, commit2,
260 ignore_whitespace=hide_whitespace_changes,
260 ignore_whitespace=hide_whitespace_changes,
261 context=diff_context)
261 context=diff_context)
262
262
263 diff_processor = diffs.DiffProcessor(
263 diff_processor = diffs.DiffProcessor(
264 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 vcs_diff, format='newdiff', diff_limit=diff_limit,
265 file_limit=file_limit, show_full_diff=c.fulldiff)
265 file_limit=file_limit, show_full_diff=c.fulldiff)
266
266
267 _parsed = diff_processor.prepare()
267 _parsed = diff_processor.prepare()
268
268
269 diffset = codeblocks.DiffSet(
269 diffset = codeblocks.DiffSet(
270 repo_name=self.db_repo_name,
270 repo_name=self.db_repo_name,
271 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 target_node_getter=codeblocks.diffset_node_getter(commit2))
273
273
274 diffset = self.path_filter.render_patchset_filtered(
274 diffset = self.path_filter.render_patchset_filtered(
275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276
276
277 # save cached diff
277 # save cached diff
278 if caching_enabled:
278 if caching_enabled:
279 cache_diff(cache_file_path, diffset, None)
279 cache_diff(cache_file_path, diffset, None)
280
280
281 c.limited_diff = diffset.limited_diff
281 c.limited_diff = diffset.limited_diff
282 c.changes[commit.raw_id] = diffset
282 c.changes[commit.raw_id] = diffset
283 else:
283 else:
284 # TODO(marcink): no cache usage here...
284 # TODO(marcink): no cache usage here...
285 _diff = self.rhodecode_vcs_repo.get_diff(
285 _diff = self.rhodecode_vcs_repo.get_diff(
286 commit1, commit2,
286 commit1, commit2,
287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 diff_processor = diffs.DiffProcessor(
288 diff_processor = diffs.DiffProcessor(
289 _diff, format='newdiff', diff_limit=diff_limit,
289 _diff, format='newdiff', diff_limit=diff_limit,
290 file_limit=file_limit, show_full_diff=c.fulldiff)
290 file_limit=file_limit, show_full_diff=c.fulldiff)
291 # downloads/raw we only need RAW diff nothing else
291 # downloads/raw we only need RAW diff nothing else
292 diff = self.path_filter.get_raw_patch(diff_processor)
292 diff = self.path_filter.get_raw_patch(diff_processor)
293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294
294
295 # sort comments by how they were generated
295 # sort comments by how they were generated
296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 c.at_version_num = None
297 c.at_version_num = None
298
298
299 if len(c.commit_ranges) == 1:
299 if len(c.commit_ranges) == 1:
300 c.commit = c.commit_ranges[0]
300 c.commit = c.commit_ranges[0]
301 c.parent_tmpl = ''.join(
301 c.parent_tmpl = ''.join(
302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303
303
304 if method == 'download':
304 if method == 'download':
305 response = Response(diff)
305 response = Response(diff)
306 response.content_type = 'text/plain'
306 response.content_type = 'text/plain'
307 response.content_disposition = (
307 response.content_disposition = (
308 'attachment; filename=%s.diff' % commit_id_range[:12])
308 'attachment; filename=%s.diff' % commit_id_range[:12])
309 return response
309 return response
310 elif method == 'patch':
310 elif method == 'patch':
311 c.diff = safe_unicode(diff)
311
312 c.diff = safe_str(diff)
312 patch = render(
313 patch = render(
313 'rhodecode:templates/changeset/patch_changeset.mako',
314 'rhodecode:templates/changeset/patch_changeset.mako',
314 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
315 response = Response(patch)
316 response = Response(patch)
316 response.content_type = 'text/plain'
317 response.content_type = 'text/plain'
317 return response
318 return response
318 elif method == 'raw':
319 elif method == 'raw':
319 response = Response(diff)
320 response = Response(diff)
320 response.content_type = 'text/plain'
321 response.content_type = 'text/plain'
321 return response
322 return response
322 elif method == 'show':
323 elif method == 'show':
323 if len(c.commit_ranges) == 1:
324 if len(c.commit_ranges) == 1:
324 html = render(
325 html = render(
325 'rhodecode:templates/changeset/changeset.mako',
326 'rhodecode:templates/changeset/changeset.mako',
326 self._get_template_context(c), self.request)
327 self._get_template_context(c), self.request)
327 return Response(html)
328 return Response(html)
328 else:
329 else:
329 c.ancestor = None
330 c.ancestor = None
330 c.target_repo = self.db_repo
331 c.target_repo = self.db_repo
331 html = render(
332 html = render(
332 'rhodecode:templates/changeset/changeset_range.mako',
333 'rhodecode:templates/changeset/changeset_range.mako',
333 self._get_template_context(c), self.request)
334 self._get_template_context(c), self.request)
334 return Response(html)
335 return Response(html)
335
336
336 raise HTTPBadRequest()
337 raise HTTPBadRequest()
337
338
338 @LoginRequired()
339 @LoginRequired()
339 @HasRepoPermissionAnyDecorator(
340 @HasRepoPermissionAnyDecorator(
340 'repository.read', 'repository.write', 'repository.admin')
341 'repository.read', 'repository.write', 'repository.admin')
341 def repo_commit_show(self):
342 def repo_commit_show(self):
342 commit_id = self.request.matchdict['commit_id']
343 commit_id = self.request.matchdict['commit_id']
343 return self._commit(commit_id, method='show')
344 return self._commit(commit_id, method='show')
344
345
345 @LoginRequired()
346 @LoginRequired()
346 @HasRepoPermissionAnyDecorator(
347 @HasRepoPermissionAnyDecorator(
347 'repository.read', 'repository.write', 'repository.admin')
348 'repository.read', 'repository.write', 'repository.admin')
348 def repo_commit_raw(self):
349 def repo_commit_raw(self):
349 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
350 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
351
352
352 @LoginRequired()
353 @LoginRequired()
353 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
354 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
355 def repo_commit_patch(self):
356 def repo_commit_patch(self):
356 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
357 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
358
359
359 @LoginRequired()
360 @LoginRequired()
360 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
361 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
362 def repo_commit_download(self):
363 def repo_commit_download(self):
363 commit_id = self.request.matchdict['commit_id']
364 commit_id = self.request.matchdict['commit_id']
364 return self._commit(commit_id, method='download')
365 return self._commit(commit_id, method='download')
365
366
366 def _commit_comments_create(self, commit_id, comments):
367 def _commit_comments_create(self, commit_id, comments):
367 _ = self.request.translate
368 _ = self.request.translate
368 data = {}
369 data = {}
369 if not comments:
370 if not comments:
370 return
371 return
371
372
372 commit = self.db_repo.get_commit(commit_id)
373 commit = self.db_repo.get_commit(commit_id)
373
374
374 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 for entry in comments:
376 for entry in comments:
376 c = self.load_default_context()
377 c = self.load_default_context()
377 comment_type = entry['comment_type']
378 comment_type = entry['comment_type']
378 text = entry['text']
379 text = entry['text']
379 status = entry['status']
380 status = entry['status']
380 is_draft = str2bool(entry['is_draft'])
381 is_draft = str2bool(entry['is_draft'])
381 resolves_comment_id = entry['resolves_comment_id']
382 resolves_comment_id = entry['resolves_comment_id']
382 f_path = entry['f_path']
383 f_path = entry['f_path']
383 line_no = entry['line']
384 line_no = entry['line']
384 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
385 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
385
386
386 if status:
387 if status:
387 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 % {'transition_icon': '>',
389 % {'transition_icon': '>',
389 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
390
391
391 comment = CommentsModel().create(
392 comment = CommentsModel().create(
392 text=text,
393 text=text,
393 repo=self.db_repo.repo_id,
394 repo=self.db_repo.repo_id,
394 user=self._rhodecode_db_user.user_id,
395 user=self._rhodecode_db_user.user_id,
395 commit_id=commit_id,
396 commit_id=commit_id,
396 f_path=f_path,
397 f_path=f_path,
397 line_no=line_no,
398 line_no=line_no,
398 status_change=(ChangesetStatus.get_status_lbl(status)
399 status_change=(ChangesetStatus.get_status_lbl(status)
399 if status else None),
400 if status else None),
400 status_change_type=status,
401 status_change_type=status,
401 comment_type=comment_type,
402 comment_type=comment_type,
402 is_draft=is_draft,
403 is_draft=is_draft,
403 resolves_comment_id=resolves_comment_id,
404 resolves_comment_id=resolves_comment_id,
404 auth_user=self._rhodecode_user,
405 auth_user=self._rhodecode_user,
405 send_email=not is_draft, # skip notification for draft comments
406 send_email=not is_draft, # skip notification for draft comments
406 )
407 )
407 is_inline = comment.is_inline
408 is_inline = comment.is_inline
408
409
409 # get status if set !
410 # get status if set !
410 if status:
411 if status:
411 # `dont_allow_on_closed_pull_request = True` means
412 # `dont_allow_on_closed_pull_request = True` means
412 # if latest status was from pull request and it's closed
413 # if latest status was from pull request and it's closed
413 # disallow changing status !
414 # disallow changing status !
414
415
415 try:
416 try:
416 ChangesetStatusModel().set_status(
417 ChangesetStatusModel().set_status(
417 self.db_repo.repo_id,
418 self.db_repo.repo_id,
418 status,
419 status,
419 self._rhodecode_db_user.user_id,
420 self._rhodecode_db_user.user_id,
420 comment,
421 comment,
421 revision=commit_id,
422 revision=commit_id,
422 dont_allow_on_closed_pull_request=True
423 dont_allow_on_closed_pull_request=True
423 )
424 )
424 except StatusChangeOnClosedPullRequestError:
425 except StatusChangeOnClosedPullRequestError:
425 msg = _('Changing the status of a commit associated with '
426 msg = _('Changing the status of a commit associated with '
426 'a closed pull request is not allowed')
427 'a closed pull request is not allowed')
427 log.exception(msg)
428 log.exception(msg)
428 h.flash(msg, category='warning')
429 h.flash(msg, category='warning')
429 raise HTTPFound(h.route_path(
430 raise HTTPFound(h.route_path(
430 'repo_commit', repo_name=self.db_repo_name,
431 'repo_commit', repo_name=self.db_repo_name,
431 commit_id=commit_id))
432 commit_id=commit_id))
432
433
433 Session().flush()
434 Session().flush()
434 # this is somehow required to get access to some relationship
435 # this is somehow required to get access to some relationship
435 # loaded on comment
436 # loaded on comment
436 Session().refresh(comment)
437 Session().refresh(comment)
437
438
438 # skip notifications for drafts
439 # skip notifications for drafts
439 if not is_draft:
440 if not is_draft:
440 CommentsModel().trigger_commit_comment_hook(
441 CommentsModel().trigger_commit_comment_hook(
441 self.db_repo, self._rhodecode_user, 'create',
442 self.db_repo, self._rhodecode_user, 'create',
442 data={'comment': comment, 'commit': commit})
443 data={'comment': comment, 'commit': commit})
443
444
444 comment_id = comment.comment_id
445 comment_id = comment.comment_id
445 data[comment_id] = {
446 data[comment_id] = {
446 'target_id': target_elem_id
447 'target_id': target_elem_id
447 }
448 }
448 Session().flush()
449 Session().flush()
449
450
450 c.co = comment
451 c.co = comment
451 c.at_version_num = 0
452 c.at_version_num = 0
452 c.is_new = True
453 c.is_new = True
453 rendered_comment = render(
454 rendered_comment = render(
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 self._get_template_context(c), self.request)
456 self._get_template_context(c), self.request)
456
457
457 data[comment_id].update(comment.get_dict())
458 data[comment_id].update(comment.get_dict())
458 data[comment_id].update({'rendered_text': rendered_comment})
459 data[comment_id].update({'rendered_text': rendered_comment})
459
460
460 # finalize, commit and redirect
461 # finalize, commit and redirect
461 Session().commit()
462 Session().commit()
462
463
463 # skip channelstream for draft comments
464 # skip channelstream for draft comments
464 if not all_drafts:
465 if not all_drafts:
465 comment_broadcast_channel = channelstream.comment_channel(
466 comment_broadcast_channel = channelstream.comment_channel(
466 self.db_repo_name, commit_obj=commit)
467 self.db_repo_name, commit_obj=commit)
467
468
468 comment_data = data
469 comment_data = data
469 posted_comment_type = 'inline' if is_inline else 'general'
470 posted_comment_type = 'inline' if is_inline else 'general'
470 if len(data) == 1:
471 if len(data) == 1:
471 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 else:
473 else:
473 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474
475
475 channelstream.comment_channelstream_push(
476 channelstream.comment_channelstream_push(
476 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 comment_data=comment_data)
478 comment_data=comment_data)
478
479
479 return data
480 return data
480
481
481 @LoginRequired()
482 @LoginRequired()
482 @NotAnonymous()
483 @NotAnonymous()
483 @HasRepoPermissionAnyDecorator(
484 @HasRepoPermissionAnyDecorator(
484 'repository.read', 'repository.write', 'repository.admin')
485 'repository.read', 'repository.write', 'repository.admin')
485 @CSRFRequired()
486 @CSRFRequired()
486 def repo_commit_comment_create(self):
487 def repo_commit_comment_create(self):
487 _ = self.request.translate
488 _ = self.request.translate
488 commit_id = self.request.matchdict['commit_id']
489 commit_id = self.request.matchdict['commit_id']
489
490
490 multi_commit_ids = []
491 multi_commit_ids = []
491 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 if _commit_id not in multi_commit_ids:
494 if _commit_id not in multi_commit_ids:
494 multi_commit_ids.append(_commit_id)
495 multi_commit_ids.append(_commit_id)
495
496
496 commit_ids = multi_commit_ids or [commit_id]
497 commit_ids = multi_commit_ids or [commit_id]
497
498
498 data = []
499 data = []
499 # Multiple comments for each passed commit id
500 # Multiple comments for each passed commit id
500 for current_id in filter(None, commit_ids):
501 for current_id in filter(None, commit_ids):
501 comment_data = {
502 comment_data = {
502 'comment_type': self.request.POST.get('comment_type'),
503 'comment_type': self.request.POST.get('comment_type'),
503 'text': self.request.POST.get('text'),
504 'text': self.request.POST.get('text'),
504 'status': self.request.POST.get('changeset_status', None),
505 'status': self.request.POST.get('changeset_status', None),
505 'is_draft': self.request.POST.get('draft'),
506 'is_draft': self.request.POST.get('draft'),
506 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 'close_pull_request': self.request.POST.get('close_pull_request'),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
508 'f_path': self.request.POST.get('f_path'),
509 'f_path': self.request.POST.get('f_path'),
509 'line': self.request.POST.get('line'),
510 'line': self.request.POST.get('line'),
510 }
511 }
511 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 data.append(comment)
513 data.append(comment)
513
514
514 return data if len(data) > 1 else data[0]
515 return data if len(data) > 1 else data[0]
515
516
516 @LoginRequired()
517 @LoginRequired()
517 @NotAnonymous()
518 @NotAnonymous()
518 @HasRepoPermissionAnyDecorator(
519 @HasRepoPermissionAnyDecorator(
519 'repository.read', 'repository.write', 'repository.admin')
520 'repository.read', 'repository.write', 'repository.admin')
520 @CSRFRequired()
521 @CSRFRequired()
521 def repo_commit_comment_preview(self):
522 def repo_commit_comment_preview(self):
522 # Technically a CSRF token is not needed as no state changes with this
523 # Technically a CSRF token is not needed as no state changes with this
523 # call. However, as this is a POST is better to have it, so automated
524 # call. However, as this is a POST is better to have it, so automated
524 # tools don't flag it as potential CSRF.
525 # tools don't flag it as potential CSRF.
525 # Post is required because the payload could be bigger than the maximum
526 # Post is required because the payload could be bigger than the maximum
526 # allowed by GET.
527 # allowed by GET.
527
528
528 text = self.request.POST.get('text')
529 text = self.request.POST.get('text')
529 renderer = self.request.POST.get('renderer') or 'rst'
530 renderer = self.request.POST.get('renderer') or 'rst'
530 if text:
531 if text:
531 return h.render(text, renderer=renderer, mentions=True,
532 return h.render(text, renderer=renderer, mentions=True,
532 repo_name=self.db_repo_name)
533 repo_name=self.db_repo_name)
533 return ''
534 return ''
534
535
535 @LoginRequired()
536 @LoginRequired()
536 @HasRepoPermissionAnyDecorator(
537 @HasRepoPermissionAnyDecorator(
537 'repository.read', 'repository.write', 'repository.admin')
538 'repository.read', 'repository.write', 'repository.admin')
538 @CSRFRequired()
539 @CSRFRequired()
539 def repo_commit_comment_history_view(self):
540 def repo_commit_comment_history_view(self):
540 c = self.load_default_context()
541 c = self.load_default_context()
541 comment_id = self.request.matchdict['comment_id']
542 comment_id = self.request.matchdict['comment_id']
542 comment_history_id = self.request.matchdict['comment_history_id']
543 comment_history_id = self.request.matchdict['comment_history_id']
543
544
544 comment = ChangesetComment.get_or_404(comment_id)
545 comment = ChangesetComment.get_or_404(comment_id)
545 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
546 if comment.draft and not comment_owner:
547 if comment.draft and not comment_owner:
547 # if we see draft comments history, we only allow this for owner
548 # if we see draft comments history, we only allow this for owner
548 raise HTTPNotFound()
549 raise HTTPNotFound()
549
550
550 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
551 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
552
553
553 if is_repo_comment:
554 if is_repo_comment:
554 c.comment_history = comment_history
555 c.comment_history = comment_history
555
556
556 rendered_comment = render(
557 rendered_comment = render(
557 'rhodecode:templates/changeset/comment_history.mako',
558 'rhodecode:templates/changeset/comment_history.mako',
558 self._get_template_context(c), self.request)
559 self._get_template_context(c), self.request)
559 return rendered_comment
560 return rendered_comment
560 else:
561 else:
561 log.warning('No permissions for user %s to show comment_history_id: %s',
562 log.warning('No permissions for user %s to show comment_history_id: %s',
562 self._rhodecode_db_user, comment_history_id)
563 self._rhodecode_db_user, comment_history_id)
563 raise HTTPNotFound()
564 raise HTTPNotFound()
564
565
565 @LoginRequired()
566 @LoginRequired()
566 @NotAnonymous()
567 @NotAnonymous()
567 @HasRepoPermissionAnyDecorator(
568 @HasRepoPermissionAnyDecorator(
568 'repository.read', 'repository.write', 'repository.admin')
569 'repository.read', 'repository.write', 'repository.admin')
569 @CSRFRequired()
570 @CSRFRequired()
570 def repo_commit_comment_attachment_upload(self):
571 def repo_commit_comment_attachment_upload(self):
571 c = self.load_default_context()
572 c = self.load_default_context()
572 upload_key = 'attachment'
573 upload_key = 'attachment'
573
574
574 file_obj = self.request.POST.get(upload_key)
575 file_obj = self.request.POST.get(upload_key)
575
576
576 if file_obj is None:
577 if file_obj is None:
577 self.request.response.status = 400
578 self.request.response.status = 400
578 return {'store_fid': None,
579 return {'store_fid': None,
579 'access_path': None,
580 'access_path': None,
580 'error': '{} data field is missing'.format(upload_key)}
581 'error': '{} data field is missing'.format(upload_key)}
581
582
582 if not hasattr(file_obj, 'filename'):
583 if not hasattr(file_obj, 'filename'):
583 self.request.response.status = 400
584 self.request.response.status = 400
584 return {'store_fid': None,
585 return {'store_fid': None,
585 'access_path': None,
586 'access_path': None,
586 'error': 'filename cannot be read from the data field'}
587 'error': 'filename cannot be read from the data field'}
587
588
588 filename = file_obj.filename
589 filename = file_obj.filename
589 file_display_name = filename
590 file_display_name = filename
590
591
591 metadata = {
592 metadata = {
592 'user_uploaded': {'username': self._rhodecode_user.username,
593 'user_uploaded': {'username': self._rhodecode_user.username,
593 'user_id': self._rhodecode_user.user_id,
594 'user_id': self._rhodecode_user.user_id,
594 'ip': self._rhodecode_user.ip_addr}}
595 'ip': self._rhodecode_user.ip_addr}}
595
596
596 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
597 allowed_extensions = [
598 allowed_extensions = [
598 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
599 '.pptx', '.txt', '.xlsx', '.zip']
600 '.pptx', '.txt', '.xlsx', '.zip']
600 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
601
602
602 try:
603 try:
603 storage = store_utils.get_file_storage(self.request.registry.settings)
604 storage = store_utils.get_file_storage(self.request.registry.settings)
604 store_uid, metadata = storage.save_file(
605 store_uid, metadata = storage.save_file(
605 file_obj.file, filename, extra_metadata=metadata,
606 file_obj.file, filename, extra_metadata=metadata,
606 extensions=allowed_extensions, max_filesize=max_file_size)
607 extensions=allowed_extensions, max_filesize=max_file_size)
607 except FileNotAllowedException:
608 except FileNotAllowedException:
608 self.request.response.status = 400
609 self.request.response.status = 400
609 permitted_extensions = ', '.join(allowed_extensions)
610 permitted_extensions = ', '.join(allowed_extensions)
610 error_msg = 'File `{}` is not allowed. ' \
611 error_msg = 'File `{}` is not allowed. ' \
611 'Only following extensions are permitted: {}'.format(
612 'Only following extensions are permitted: {}'.format(
612 filename, permitted_extensions)
613 filename, permitted_extensions)
613 return {'store_fid': None,
614 return {'store_fid': None,
614 'access_path': None,
615 'access_path': None,
615 'error': error_msg}
616 'error': error_msg}
616 except FileOverSizeException:
617 except FileOverSizeException:
617 self.request.response.status = 400
618 self.request.response.status = 400
618 limit_mb = h.format_byte_size_binary(max_file_size)
619 limit_mb = h.format_byte_size_binary(max_file_size)
619 return {'store_fid': None,
620 return {'store_fid': None,
620 'access_path': None,
621 'access_path': None,
621 'error': 'File {} is exceeding allowed limit of {}.'.format(
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
622 filename, limit_mb)}
623 filename, limit_mb)}
623
624
624 try:
625 try:
625 entry = FileStore.create(
626 entry = FileStore.create(
626 file_uid=store_uid, filename=metadata["filename"],
627 file_uid=store_uid, filename=metadata["filename"],
627 file_hash=metadata["sha256"], file_size=metadata["size"],
628 file_hash=metadata["sha256"], file_size=metadata["size"],
628 file_display_name=file_display_name,
629 file_display_name=file_display_name,
629 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
630 file_description=u'comment attachment `{}`'.format(safe_str(filename)),
630 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
631 scope_repo_id=self.db_repo.repo_id
632 scope_repo_id=self.db_repo.repo_id
632 )
633 )
633 Session().add(entry)
634 Session().add(entry)
634 Session().commit()
635 Session().commit()
635 log.debug('Stored upload in DB as %s', entry)
636 log.debug('Stored upload in DB as %s', entry)
636 except Exception:
637 except Exception:
637 log.exception('Failed to store file %s', filename)
638 log.exception('Failed to store file %s', filename)
638 self.request.response.status = 400
639 self.request.response.status = 400
639 return {'store_fid': None,
640 return {'store_fid': None,
640 'access_path': None,
641 'access_path': None,
641 'error': 'File {} failed to store in DB.'.format(filename)}
642 'error': 'File {} failed to store in DB.'.format(filename)}
642
643
643 Session().commit()
644 Session().commit()
644
645
645 return {
646 return {
646 'store_fid': store_uid,
647 'store_fid': store_uid,
647 'access_path': h.route_path(
648 'access_path': h.route_path(
648 'download_file', fid=store_uid),
649 'download_file', fid=store_uid),
649 'fqn_access_path': h.route_url(
650 'fqn_access_path': h.route_url(
650 'download_file', fid=store_uid),
651 'download_file', fid=store_uid),
651 'repo_access_path': h.route_path(
652 'repo_access_path': h.route_path(
652 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
653 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
653 'repo_fqn_access_path': h.route_url(
654 'repo_fqn_access_path': h.route_url(
654 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
655 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
655 }
656 }
656
657
657 @LoginRequired()
658 @LoginRequired()
658 @NotAnonymous()
659 @NotAnonymous()
659 @HasRepoPermissionAnyDecorator(
660 @HasRepoPermissionAnyDecorator(
660 'repository.read', 'repository.write', 'repository.admin')
661 'repository.read', 'repository.write', 'repository.admin')
661 @CSRFRequired()
662 @CSRFRequired()
662 def repo_commit_comment_delete(self):
663 def repo_commit_comment_delete(self):
663 commit_id = self.request.matchdict['commit_id']
664 commit_id = self.request.matchdict['commit_id']
664 comment_id = self.request.matchdict['comment_id']
665 comment_id = self.request.matchdict['comment_id']
665
666
666 comment = ChangesetComment.get_or_404(comment_id)
667 comment = ChangesetComment.get_or_404(comment_id)
667 if not comment:
668 if not comment:
668 log.debug('Comment with id:%s not found, skipping', comment_id)
669 log.debug('Comment with id:%s not found, skipping', comment_id)
669 # comment already deleted in another call probably
670 # comment already deleted in another call probably
670 return True
671 return True
671
672
672 if comment.immutable:
673 if comment.immutable:
673 # don't allow deleting comments that are immutable
674 # don't allow deleting comments that are immutable
674 raise HTTPForbidden()
675 raise HTTPForbidden()
675
676
676 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
677 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
677 super_admin = h.HasPermissionAny('hg.admin')()
678 super_admin = h.HasPermissionAny('hg.admin')()
678 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
679 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
679 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
680 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
680 comment_repo_admin = is_repo_admin and is_repo_comment
681 comment_repo_admin = is_repo_admin and is_repo_comment
681
682
682 if comment.draft and not comment_owner:
683 if comment.draft and not comment_owner:
683 # We never allow to delete draft comments for other than owners
684 # We never allow to delete draft comments for other than owners
684 raise HTTPNotFound()
685 raise HTTPNotFound()
685
686
686 if super_admin or comment_owner or comment_repo_admin:
687 if super_admin or comment_owner or comment_repo_admin:
687 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
688 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
688 Session().commit()
689 Session().commit()
689 return True
690 return True
690 else:
691 else:
691 log.warning('No permissions for user %s to delete comment_id: %s',
692 log.warning('No permissions for user %s to delete comment_id: %s',
692 self._rhodecode_db_user, comment_id)
693 self._rhodecode_db_user, comment_id)
693 raise HTTPNotFound()
694 raise HTTPNotFound()
694
695
695 @LoginRequired()
696 @LoginRequired()
696 @NotAnonymous()
697 @NotAnonymous()
697 @HasRepoPermissionAnyDecorator(
698 @HasRepoPermissionAnyDecorator(
698 'repository.read', 'repository.write', 'repository.admin')
699 'repository.read', 'repository.write', 'repository.admin')
699 @CSRFRequired()
700 @CSRFRequired()
700 def repo_commit_comment_edit(self):
701 def repo_commit_comment_edit(self):
701 self.load_default_context()
702 self.load_default_context()
702
703
703 commit_id = self.request.matchdict['commit_id']
704 commit_id = self.request.matchdict['commit_id']
704 comment_id = self.request.matchdict['comment_id']
705 comment_id = self.request.matchdict['comment_id']
705 comment = ChangesetComment.get_or_404(comment_id)
706 comment = ChangesetComment.get_or_404(comment_id)
706
707
707 if comment.immutable:
708 if comment.immutable:
708 # don't allow deleting comments that are immutable
709 # don't allow deleting comments that are immutable
709 raise HTTPForbidden()
710 raise HTTPForbidden()
710
711
711 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
712 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
712 super_admin = h.HasPermissionAny('hg.admin')()
713 super_admin = h.HasPermissionAny('hg.admin')()
713 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
714 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
714 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
715 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
715 comment_repo_admin = is_repo_admin and is_repo_comment
716 comment_repo_admin = is_repo_admin and is_repo_comment
716
717
717 if super_admin or comment_owner or comment_repo_admin:
718 if super_admin or comment_owner or comment_repo_admin:
718 text = self.request.POST.get('text')
719 text = self.request.POST.get('text')
719 version = self.request.POST.get('version')
720 version = self.request.POST.get('version')
720 if text == comment.text:
721 if text == comment.text:
721 log.warning(
722 log.warning(
722 'Comment(repo): '
723 'Comment(repo): '
723 'Trying to create new version '
724 'Trying to create new version '
724 'with the same comment body {}'.format(
725 'with the same comment body {}'.format(
725 comment_id,
726 comment_id,
726 )
727 )
727 )
728 )
728 raise HTTPNotFound()
729 raise HTTPNotFound()
729
730
730 if version.isdigit():
731 if version.isdigit():
731 version = int(version)
732 version = int(version)
732 else:
733 else:
733 log.warning(
734 log.warning(
734 'Comment(repo): Wrong version type {} {} '
735 'Comment(repo): Wrong version type {} {} '
735 'for comment {}'.format(
736 'for comment {}'.format(
736 version,
737 version,
737 type(version),
738 type(version),
738 comment_id,
739 comment_id,
739 )
740 )
740 )
741 )
741 raise HTTPNotFound()
742 raise HTTPNotFound()
742
743
743 try:
744 try:
744 comment_history = CommentsModel().edit(
745 comment_history = CommentsModel().edit(
745 comment_id=comment_id,
746 comment_id=comment_id,
746 text=text,
747 text=text,
747 auth_user=self._rhodecode_user,
748 auth_user=self._rhodecode_user,
748 version=version,
749 version=version,
749 )
750 )
750 except CommentVersionMismatch:
751 except CommentVersionMismatch:
751 raise HTTPConflict()
752 raise HTTPConflict()
752
753
753 if not comment_history:
754 if not comment_history:
754 raise HTTPNotFound()
755 raise HTTPNotFound()
755
756
756 if not comment.draft:
757 if not comment.draft:
757 commit = self.db_repo.get_commit(commit_id)
758 commit = self.db_repo.get_commit(commit_id)
758 CommentsModel().trigger_commit_comment_hook(
759 CommentsModel().trigger_commit_comment_hook(
759 self.db_repo, self._rhodecode_user, 'edit',
760 self.db_repo, self._rhodecode_user, 'edit',
760 data={'comment': comment, 'commit': commit})
761 data={'comment': comment, 'commit': commit})
761
762
762 Session().commit()
763 Session().commit()
763 return {
764 return {
764 'comment_history_id': comment_history.comment_history_id,
765 'comment_history_id': comment_history.comment_history_id,
765 'comment_id': comment.comment_id,
766 'comment_id': comment.comment_id,
766 'comment_version': comment_history.version,
767 'comment_version': comment_history.version,
767 'comment_author_username': comment_history.author.username,
768 'comment_author_username': comment_history.author.username,
768 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
769 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
769 'comment_created_on': h.age_component(comment_history.created_on,
770 'comment_created_on': h.age_component(comment_history.created_on,
770 time_is_local=True),
771 time_is_local=True),
771 }
772 }
772 else:
773 else:
773 log.warning('No permissions for user %s to edit comment_id: %s',
774 log.warning('No permissions for user %s to edit comment_id: %s',
774 self._rhodecode_db_user, comment_id)
775 self._rhodecode_db_user, comment_id)
775 raise HTTPNotFound()
776 raise HTTPNotFound()
776
777
777 @LoginRequired()
778 @LoginRequired()
778 @HasRepoPermissionAnyDecorator(
779 @HasRepoPermissionAnyDecorator(
779 'repository.read', 'repository.write', 'repository.admin')
780 'repository.read', 'repository.write', 'repository.admin')
780 def repo_commit_data(self):
781 def repo_commit_data(self):
781 commit_id = self.request.matchdict['commit_id']
782 commit_id = self.request.matchdict['commit_id']
782 self.load_default_context()
783 self.load_default_context()
783
784
784 try:
785 try:
785 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
786 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
786 except CommitDoesNotExistError as e:
787 except CommitDoesNotExistError as e:
787 return EmptyCommit(message=str(e))
788 return EmptyCommit(message=str(e))
788
789
789 @LoginRequired()
790 @LoginRequired()
790 @HasRepoPermissionAnyDecorator(
791 @HasRepoPermissionAnyDecorator(
791 'repository.read', 'repository.write', 'repository.admin')
792 'repository.read', 'repository.write', 'repository.admin')
792 def repo_commit_children(self):
793 def repo_commit_children(self):
793 commit_id = self.request.matchdict['commit_id']
794 commit_id = self.request.matchdict['commit_id']
794 self.load_default_context()
795 self.load_default_context()
795
796
796 try:
797 try:
797 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 children = commit.children
799 children = commit.children
799 except CommitDoesNotExistError:
800 except CommitDoesNotExistError:
800 children = []
801 children = []
801
802
802 result = {"results": children}
803 result = {"results": children}
803 return result
804 return result
804
805
805 @LoginRequired()
806 @LoginRequired()
806 @HasRepoPermissionAnyDecorator(
807 @HasRepoPermissionAnyDecorator(
807 'repository.read', 'repository.write', 'repository.admin')
808 'repository.read', 'repository.write', 'repository.admin')
808 def repo_commit_parents(self):
809 def repo_commit_parents(self):
809 commit_id = self.request.matchdict['commit_id']
810 commit_id = self.request.matchdict['commit_id']
810 self.load_default_context()
811 self.load_default_context()
811
812
812 try:
813 try:
813 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
814 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
814 parents = commit.parents
815 parents = commit.parents
815 except CommitDoesNotExistError:
816 except CommitDoesNotExistError:
816 parents = []
817 parents = []
817 result = {"results": parents}
818 result = {"results": parents}
818 return result
819 return result
@@ -1,305 +1,306 b''
1
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25
25
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import str2bool
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 NodeDoesNotExistError)
39 NodeDoesNotExistError)
40 from rhodecode.model.db import Repository, ChangesetStatus
40 from rhodecode.model.db import Repository, ChangesetStatus
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class RepoCompareView(RepoAppView):
45 class RepoCompareView(RepoAppView):
46 def load_default_context(self):
46 def load_default_context(self):
47 c = self._get_local_tmpl_context(include_app_defaults=True)
47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 c.rhodecode_repo = self.rhodecode_vcs_repo
48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 return c
49 return c
50
50
51 def _get_commit_or_redirect(
51 def _get_commit_or_redirect(
52 self, ref, ref_type, repo, redirect_after=True, partial=False):
52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 """
53 """
54 This is a safe way to get a commit. If an error occurs it
54 This is a safe way to get a commit. If an error occurs it
55 redirects to a commit with a proper message. If partial is set
55 redirects to a commit with a proper message. If partial is set
56 then it does not do redirect raise and throws an exception instead.
56 then it does not do redirect raise and throws an exception instead.
57 """
57 """
58 _ = self.request.translate
58 _ = self.request.translate
59 try:
59 try:
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return repo.scm_instance().EMPTY_COMMIT
63 return repo.scm_instance().EMPTY_COMMIT
64 h.flash(h.literal(_('There are no commits yet')),
64 h.flash(h.literal(_('There are no commits yet')),
65 category='warning')
65 category='warning')
66 if not partial:
66 if not partial:
67 raise HTTPFound(
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=repo.repo_name))
68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 raise HTTPBadRequest()
69 raise HTTPBadRequest()
70
70
71 except RepositoryError as e:
71 except RepositoryError as e:
72 log.exception(safe_str(e))
72 log.exception(safe_str(e))
73 h.flash(h.escape(safe_str(e)), category='warning')
73 h.flash(h.escape(safe_str(e)), category='warning')
74 if not partial:
74 if not partial:
75 raise HTTPFound(
75 raise HTTPFound(
76 h.route_path('repo_summary', repo_name=repo.repo_name))
76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 raise HTTPBadRequest()
77 raise HTTPBadRequest()
78
78
79 @LoginRequired()
79 @LoginRequired()
80 @HasRepoPermissionAnyDecorator(
80 @HasRepoPermissionAnyDecorator(
81 'repository.read', 'repository.write', 'repository.admin')
81 'repository.read', 'repository.write', 'repository.admin')
82 def compare_select(self):
82 def compare_select(self):
83 _ = self.request.translate
83 _ = self.request.translate
84 c = self.load_default_context()
84 c = self.load_default_context()
85
85
86 source_repo = self.db_repo_name
86 source_repo = self.db_repo_name
87 target_repo = self.request.GET.get('target_repo', source_repo)
87 target_repo = self.request.GET.get('target_repo', source_repo)
88 c.source_repo = Repository.get_by_repo_name(source_repo)
88 c.source_repo = Repository.get_by_repo_name(source_repo)
89 c.target_repo = Repository.get_by_repo_name(target_repo)
89 c.target_repo = Repository.get_by_repo_name(target_repo)
90
90
91 if c.source_repo is None or c.target_repo is None:
91 if c.source_repo is None or c.target_repo is None:
92 raise HTTPNotFound()
92 raise HTTPNotFound()
93
93
94 c.compare_home = True
94 c.compare_home = True
95 c.commit_ranges = []
95 c.commit_ranges = []
96 c.collapse_all_commits = False
96 c.collapse_all_commits = False
97 c.diffset = None
97 c.diffset = None
98 c.limited_diff = False
98 c.limited_diff = False
99 c.source_ref = c.target_ref = _('Select commit')
99 c.source_ref = c.target_ref = _('Select commit')
100 c.source_ref_type = ""
100 c.source_ref_type = ""
101 c.target_ref_type = ""
101 c.target_ref_type = ""
102 c.commit_statuses = ChangesetStatus.STATUSES
102 c.commit_statuses = ChangesetStatus.STATUSES
103 c.preview_mode = False
103 c.preview_mode = False
104 c.file_path = None
104 c.file_path = None
105
105
106 return self._get_template_context(c)
106 return self._get_template_context(c)
107
107
108 @LoginRequired()
108 @LoginRequired()
109 @HasRepoPermissionAnyDecorator(
109 @HasRepoPermissionAnyDecorator(
110 'repository.read', 'repository.write', 'repository.admin')
110 'repository.read', 'repository.write', 'repository.admin')
111 def compare(self):
111 def compare(self):
112 _ = self.request.translate
112 _ = self.request.translate
113 c = self.load_default_context()
113 c = self.load_default_context()
114
114
115 source_ref_type = self.request.matchdict['source_ref_type']
115 source_ref_type = self.request.matchdict['source_ref_type']
116 source_ref = self.request.matchdict['source_ref']
116 source_ref = self.request.matchdict['source_ref']
117 target_ref_type = self.request.matchdict['target_ref_type']
117 target_ref_type = self.request.matchdict['target_ref_type']
118 target_ref = self.request.matchdict['target_ref']
118 target_ref = self.request.matchdict['target_ref']
119
119
120 # source_ref will be evaluated in source_repo
120 # source_ref will be evaluated in source_repo
121 source_repo_name = self.db_repo_name
121 source_repo_name = self.db_repo_name
122 source_path, source_id = parse_path_ref(source_ref)
122 source_path, source_id = parse_path_ref(source_ref)
123
123
124 # target_ref will be evaluated in target_repo
124 # target_ref will be evaluated in target_repo
125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 target_path, target_id = parse_path_ref(
126 target_path, target_id = parse_path_ref(
127 target_ref, default_path=self.request.GET.get('f_path', ''))
127 target_ref, default_path=self.request.GET.get('f_path', ''))
128
128
129 # if merge is True
129 # if merge is True
130 # Show what changes since the shared ancestor commit of target/source
130 # Show what changes since the shared ancestor commit of target/source
131 # the source would get if it was merged with target. Only commits
131 # the source would get if it was merged with target. Only commits
132 # which are in target but not in source will be shown.
132 # which are in target but not in source will be shown.
133 merge = str2bool(self.request.GET.get('merge'))
133 merge = str2bool(self.request.GET.get('merge'))
134 # if merge is False
134 # if merge is False
135 # Show a raw diff of source/target refs even if no ancestor exists
135 # Show a raw diff of source/target refs even if no ancestor exists
136
136
137 # c.fulldiff disables cut_off_limit
137 # c.fulldiff disables cut_off_limit
138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139
139
140 # fetch global flags of ignore ws or context lines
140 # fetch global flags of ignore ws or context lines
141 diff_context = diffs.get_diff_context(self.request)
141 diff_context = diffs.get_diff_context(self.request)
142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143
143
144 c.file_path = target_path
144 c.file_path = target_path
145 c.commit_statuses = ChangesetStatus.STATUSES
145 c.commit_statuses = ChangesetStatus.STATUSES
146
146
147 # if partial, returns just compare_commits.html (commits log)
147 # if partial, returns just compare_commits.html (commits log)
148 partial = self.request.is_xhr
148 partial = self.request.is_xhr
149
149
150 # swap url for compare_diff page
150 # swap url for compare_diff page
151 c.swap_url = h.route_path(
151 c.swap_url = h.route_path(
152 'repo_compare',
152 'repo_compare',
153 repo_name=target_repo_name,
153 repo_name=target_repo_name,
154 source_ref_type=target_ref_type,
154 source_ref_type=target_ref_type,
155 source_ref=target_ref,
155 source_ref=target_ref,
156 target_repo=source_repo_name,
156 target_repo=source_repo_name,
157 target_ref_type=source_ref_type,
157 target_ref_type=source_ref_type,
158 target_ref=source_ref,
158 target_ref=source_ref,
159 _query=dict(merge=merge and '1' or '', f_path=target_path))
159 _query=dict(merge=merge and '1' or '', f_path=target_path))
160
160
161 source_repo = Repository.get_by_repo_name(source_repo_name)
161 source_repo = Repository.get_by_repo_name(source_repo_name)
162 target_repo = Repository.get_by_repo_name(target_repo_name)
162 target_repo = Repository.get_by_repo_name(target_repo_name)
163
163
164 if source_repo is None:
164 if source_repo is None:
165 log.error('Could not find the source repo: {}'
165 log.error('Could not find the source repo: {}'
166 .format(source_repo_name))
166 .format(source_repo_name))
167 h.flash(_('Could not find the source repo: `{}`')
167 h.flash(_('Could not find the source repo: `{}`')
168 .format(h.escape(source_repo_name)), category='error')
168 .format(h.escape(source_repo_name)), category='error')
169 raise HTTPFound(
169 raise HTTPFound(
170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171
171
172 if target_repo is None:
172 if target_repo is None:
173 log.error('Could not find the target repo: {}'
173 log.error('Could not find the target repo: {}'
174 .format(source_repo_name))
174 .format(source_repo_name))
175 h.flash(_('Could not find the target repo: `{}`')
175 h.flash(_('Could not find the target repo: `{}`')
176 .format(h.escape(target_repo_name)), category='error')
176 .format(h.escape(target_repo_name)), category='error')
177 raise HTTPFound(
177 raise HTTPFound(
178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179
179
180 source_scm = source_repo.scm_instance()
180 source_scm = source_repo.scm_instance()
181 target_scm = target_repo.scm_instance()
181 target_scm = target_repo.scm_instance()
182
182
183 source_alias = source_scm.alias
183 source_alias = source_scm.alias
184 target_alias = target_scm.alias
184 target_alias = target_scm.alias
185 if source_alias != target_alias:
185 if source_alias != target_alias:
186 msg = _('The comparison of two different kinds of remote repos '
186 msg = _('The comparison of two different kinds of remote repos '
187 'is not available')
187 'is not available')
188 log.error(msg)
188 log.error(msg)
189 h.flash(msg, category='error')
189 h.flash(msg, category='error')
190 raise HTTPFound(
190 raise HTTPFound(
191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192
192
193 source_commit = self._get_commit_or_redirect(
193 source_commit = self._get_commit_or_redirect(
194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 partial=partial)
195 partial=partial)
196 target_commit = self._get_commit_or_redirect(
196 target_commit = self._get_commit_or_redirect(
197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 partial=partial)
198 partial=partial)
199
199
200 c.compare_home = False
200 c.compare_home = False
201 c.source_repo = source_repo
201 c.source_repo = source_repo
202 c.target_repo = target_repo
202 c.target_repo = target_repo
203 c.source_ref = source_ref
203 c.source_ref = source_ref
204 c.target_ref = target_ref
204 c.target_ref = target_ref
205 c.source_ref_type = source_ref_type
205 c.source_ref_type = source_ref_type
206 c.target_ref_type = target_ref_type
206 c.target_ref_type = target_ref_type
207
207
208 pre_load = ["author", "date", "message", "branch"]
208 pre_load = ["author", "date", "message", "branch"]
209 c.ancestor = None
209 c.ancestor = None
210
210
211 try:
211 try:
212 c.commit_ranges = source_scm.compare(
212 c.commit_ranges = source_scm.compare(
213 source_commit.raw_id, target_commit.raw_id,
213 source_commit.raw_id, target_commit.raw_id,
214 target_scm, merge, pre_load=pre_load) or []
214 target_scm, merge, pre_load=pre_load) or []
215 if merge:
215 if merge:
216 c.ancestor = source_scm.get_common_ancestor(
216 c.ancestor = source_scm.get_common_ancestor(
217 source_commit.raw_id, target_commit.raw_id, target_scm)
217 source_commit.raw_id, target_commit.raw_id, target_scm)
218 except RepositoryRequirementError:
218 except RepositoryRequirementError:
219 msg = _('Could not compare repos with different '
219 msg = _('Could not compare repos with different '
220 'large file settings')
220 'large file settings')
221 log.error(msg)
221 log.error(msg)
222 if partial:
222 if partial:
223 return Response(msg)
223 return Response(msg)
224 h.flash(msg, category='error')
224 h.flash(msg, category='error')
225 raise HTTPFound(
225 raise HTTPFound(
226 h.route_path('repo_compare_select',
226 h.route_path('repo_compare_select',
227 repo_name=self.db_repo_name))
227 repo_name=self.db_repo_name))
228
228
229 c.statuses = self.db_repo.statuses(
229 c.statuses = self.db_repo.statuses(
230 [x.raw_id for x in c.commit_ranges])
230 [x.raw_id for x in c.commit_ranges])
231
231
232 # auto collapse if we have more than limit
232 # auto collapse if we have more than limit
233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235
235
236 if partial: # for PR ajax commits loader
236 if partial: # for PR ajax commits loader
237 if not c.ancestor:
237 if not c.ancestor:
238 return Response('') # cannot merge if there is no ancestor
238 return Response('') # cannot merge if there is no ancestor
239
239
240 html = render(
240 html = render(
241 'rhodecode:templates/compare/compare_commits.mako',
241 'rhodecode:templates/compare/compare_commits.mako',
242 self._get_template_context(c), self.request)
242 self._get_template_context(c), self.request)
243 return Response(html)
243 return Response(html)
244
244
245 if c.ancestor:
245 if c.ancestor:
246 # case we want a simple diff without incoming commits,
246 # case we want a simple diff without incoming commits,
247 # previewing what will be merged.
247 # previewing what will be merged.
248 # Make the diff on target repo (which is known to have target_ref)
248 # Make the diff on target repo (which is known to have target_ref)
249 log.debug('Using ancestor %s as source_ref instead of %s',
249 log.debug('Using ancestor %s as source_ref instead of %s',
250 c.ancestor, source_ref)
250 c.ancestor, source_ref)
251 source_repo = target_repo
251 source_repo = target_repo
252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253
253
254 # diff_limit will cut off the whole diff if the limit is applied
254 # diff_limit will cut off the whole diff if the limit is applied
255 # otherwise it will just hide the big files from the front-end
255 # otherwise it will just hide the big files from the front-end
256 diff_limit = c.visual.cut_off_limit_diff
256 diff_limit = c.visual.cut_off_limit_diff
257 file_limit = c.visual.cut_off_limit_file
257 file_limit = c.visual.cut_off_limit_file
258
258
259 log.debug('calculating diff between '
259 log.debug('calculating diff between '
260 'source_ref:%s and target_ref:%s for repo `%s`',
260 'source_ref:%s and target_ref:%s for repo `%s`',
261 source_commit, target_commit,
261 source_commit, target_commit,
262 safe_unicode(source_repo.scm_instance().path))
262 safe_str(source_repo.scm_instance().path))
263
263
264 if source_commit.repository != target_commit.repository:
264 if source_commit.repository != target_commit.repository:
265
265 msg = _(
266 msg = _(
266 "Repositories unrelated. "
267 "Repositories unrelated. "
267 "Cannot compare commit %(commit1)s from repository %(repo1)s "
268 "Cannot compare commit %(commit1)s from repository %(repo1)s "
268 "with commit %(commit2)s from repository %(repo2)s.") % {
269 "with commit %(commit2)s from repository %(repo2)s.") % {
269 'commit1': h.show_id(source_commit),
270 'commit1': h.show_id(source_commit),
270 'repo1': source_repo.repo_name,
271 'repo1': source_repo.repo_name,
271 'commit2': h.show_id(target_commit),
272 'commit2': h.show_id(target_commit),
272 'repo2': target_repo.repo_name,
273 'repo2': target_repo.repo_name,
273 }
274 }
274 h.flash(msg, category='error')
275 h.flash(msg, category='error')
275 raise HTTPFound(
276 raise HTTPFound(
276 h.route_path('repo_compare_select',
277 h.route_path('repo_compare_select',
277 repo_name=self.db_repo_name))
278 repo_name=self.db_repo_name))
278
279
279 txt_diff = source_repo.scm_instance().get_diff(
280 txt_diff = source_repo.scm_instance().get_diff(
280 commit1=source_commit, commit2=target_commit,
281 commit1=source_commit, commit2=target_commit,
281 path=target_path, path1=source_path,
282 path=target_path, path1=source_path,
282 ignore_whitespace=hide_whitespace_changes, context=diff_context)
283 ignore_whitespace=hide_whitespace_changes, context=diff_context)
283
284
284 diff_processor = diffs.DiffProcessor(
285 diff_processor = diffs.DiffProcessor(
285 txt_diff, format='newdiff', diff_limit=diff_limit,
286 txt_diff, format='newdiff', diff_limit=diff_limit,
286 file_limit=file_limit, show_full_diff=c.fulldiff)
287 file_limit=file_limit, show_full_diff=c.fulldiff)
287 _parsed = diff_processor.prepare()
288 _parsed = diff_processor.prepare()
288
289
289 diffset = codeblocks.DiffSet(
290 diffset = codeblocks.DiffSet(
290 repo_name=source_repo.repo_name,
291 repo_name=source_repo.repo_name,
291 source_node_getter=codeblocks.diffset_node_getter(source_commit),
292 source_node_getter=codeblocks.diffset_node_getter(source_commit),
292 target_repo_name=self.db_repo_name,
293 target_repo_name=self.db_repo_name,
293 target_node_getter=codeblocks.diffset_node_getter(target_commit),
294 target_node_getter=codeblocks.diffset_node_getter(target_commit),
294 )
295 )
295 c.diffset = self.path_filter.render_patchset_filtered(
296 c.diffset = self.path_filter.render_patchset_filtered(
296 diffset, _parsed, source_ref, target_ref)
297 diffset, _parsed, source_ref, target_ref)
297
298
298 c.preview_mode = merge
299 c.preview_mode = merge
299 c.source_commit = source_commit
300 c.source_commit = source_commit
300 c.target_commit = target_commit
301 c.target_commit = target_commit
301
302
302 html = render(
303 html = render(
303 'rhodecode:templates/compare/compare_diff.mako',
304 'rhodecode:templates/compare/compare_diff.mako',
304 self._get_template_context(c), self.request)
305 self._get_template_context(c), self.request)
305 return Response(html) No newline at end of file
306 return Response(html)
@@ -1,254 +1,254 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27
27
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 import rhodecode.lib.helpers as h
36 import rhodecode.lib.helpers as h
37 from rhodecode.lib.str_utils import safe_str
37 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.forms import RepoForkForm
42 from rhodecode.model.forms import RepoForkForm
42 from rhodecode.model.scm import ScmModel, RepoGroupList
43 from rhodecode.model.scm import ScmModel, RepoGroupList
43 from rhodecode.lib.utils2 import safe_int, safe_unicode
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class RepoForksView(RepoAppView, DataGridAppView):
48 class RepoForksView(RepoAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context(include_app_defaults=True)
51 c = self._get_local_tmpl_context(include_app_defaults=True)
52 c.rhodecode_repo = self.rhodecode_vcs_repo
52 c.rhodecode_repo = self.rhodecode_vcs_repo
53
53
54 acl_groups = RepoGroupList(
54 acl_groups = RepoGroupList(
55 RepoGroup.query().all(),
55 RepoGroup.query().all(),
56 perm_set=['group.write', 'group.admin'])
56 perm_set=['group.write', 'group.admin'])
57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
58 c.repo_groups_choices = list(map(lambda k: safe_str(k[0]), c.repo_groups))
59
59
60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61
61
62 return c
62 return c
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @HasRepoPermissionAnyDecorator(
65 @HasRepoPermissionAnyDecorator(
66 'repository.read', 'repository.write', 'repository.admin')
66 'repository.read', 'repository.write', 'repository.admin')
67 def repo_forks_show_all(self):
67 def repo_forks_show_all(self):
68 c = self.load_default_context()
68 c = self.load_default_context()
69 return self._get_template_context(c)
69 return self._get_template_context(c)
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @HasRepoPermissionAnyDecorator(
72 @HasRepoPermissionAnyDecorator(
73 'repository.read', 'repository.write', 'repository.admin')
73 'repository.read', 'repository.write', 'repository.admin')
74 def repo_forks_data(self):
74 def repo_forks_data(self):
75 _ = self.request.translate
75 _ = self.request.translate
76 self.load_default_context()
76 self.load_default_context()
77 column_map = {
77 column_map = {
78 'fork_name': 'repo_name',
78 'fork_name': 'repo_name',
79 'fork_date': 'created_on',
79 'fork_date': 'created_on',
80 'last_activity': 'updated_on'
80 'last_activity': 'updated_on'
81 }
81 }
82 draw, start, limit = self._extract_chunk(self.request)
82 draw, start, limit = self._extract_chunk(self.request)
83 search_q, order_by, order_dir = self._extract_ordering(
83 search_q, order_by, order_dir = self._extract_ordering(
84 self.request, column_map=column_map)
84 self.request, column_map=column_map)
85
85
86 acl_check = HasRepoPermissionAny(
86 acl_check = HasRepoPermissionAny(
87 'repository.read', 'repository.write', 'repository.admin')
87 'repository.read', 'repository.write', 'repository.admin')
88 repo_id = self.db_repo.repo_id
88 repo_id = self.db_repo.repo_id
89 allowed_ids = [-1]
89 allowed_ids = [-1]
90 for f in Repository.query().filter(Repository.fork_id == repo_id):
90 for f in Repository.query().filter(Repository.fork_id == repo_id):
91 if acl_check(f.repo_name, 'get forks check'):
91 if acl_check(f.repo_name, 'get forks check'):
92 allowed_ids.append(f.repo_id)
92 allowed_ids.append(f.repo_id)
93
93
94 forks_data_total_count = Repository.query()\
94 forks_data_total_count = Repository.query()\
95 .filter(Repository.fork_id == repo_id)\
95 .filter(Repository.fork_id == repo_id)\
96 .filter(Repository.repo_id.in_(allowed_ids))\
96 .filter(Repository.repo_id.in_(allowed_ids))\
97 .count()
97 .count()
98
98
99 # json generate
99 # json generate
100 base_q = Repository.query()\
100 base_q = Repository.query()\
101 .filter(Repository.fork_id == repo_id)\
101 .filter(Repository.fork_id == repo_id)\
102 .filter(Repository.repo_id.in_(allowed_ids))\
102 .filter(Repository.repo_id.in_(allowed_ids))\
103
103
104 if search_q:
104 if search_q:
105 like_expression = u'%{}%'.format(safe_unicode(search_q))
105 like_expression = u'%{}%'.format(safe_str(search_q))
106 base_q = base_q.filter(or_(
106 base_q = base_q.filter(or_(
107 Repository.repo_name.ilike(like_expression),
107 Repository.repo_name.ilike(like_expression),
108 Repository.description.ilike(like_expression),
108 Repository.description.ilike(like_expression),
109 ))
109 ))
110
110
111 forks_data_total_filtered_count = base_q.count()
111 forks_data_total_filtered_count = base_q.count()
112
112
113 sort_col = getattr(Repository, order_by, None)
113 sort_col = getattr(Repository, order_by, None)
114 if sort_col:
114 if sort_col:
115 if order_dir == 'asc':
115 if order_dir == 'asc':
116 # handle null values properly to order by NULL last
116 # handle null values properly to order by NULL last
117 if order_by in ['last_activity']:
117 if order_by in ['last_activity']:
118 sort_col = coalesce(sort_col, datetime.date.max)
118 sort_col = coalesce(sort_col, datetime.date.max)
119 sort_col = sort_col.asc()
119 sort_col = sort_col.asc()
120 else:
120 else:
121 # handle null values properly to order by NULL last
121 # handle null values properly to order by NULL last
122 if order_by in ['last_activity']:
122 if order_by in ['last_activity']:
123 sort_col = coalesce(sort_col, datetime.date.min)
123 sort_col = coalesce(sort_col, datetime.date.min)
124 sort_col = sort_col.desc()
124 sort_col = sort_col.desc()
125
125
126 base_q = base_q.order_by(sort_col)
126 base_q = base_q.order_by(sort_col)
127 base_q = base_q.offset(start).limit(limit)
127 base_q = base_q.offset(start).limit(limit)
128
128
129 fork_list = base_q.all()
129 fork_list = base_q.all()
130
130
131 def fork_actions(fork):
131 def fork_actions(fork):
132 url_link = h.route_path(
132 url_link = h.route_path(
133 'repo_compare',
133 'repo_compare',
134 repo_name=fork.repo_name,
134 repo_name=fork.repo_name,
135 source_ref_type=self.db_repo.landing_ref_type,
135 source_ref_type=self.db_repo.landing_ref_type,
136 source_ref=self.db_repo.landing_ref_name,
136 source_ref=self.db_repo.landing_ref_name,
137 target_ref_type=self.db_repo.landing_ref_type,
137 target_ref_type=self.db_repo.landing_ref_type,
138 target_ref=self.db_repo.landing_ref_name,
138 target_ref=self.db_repo.landing_ref_name,
139 _query=dict(merge=1, target_repo=f.repo_name))
139 _query=dict(merge=1, target_repo=f.repo_name))
140 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
140 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
141
141
142 def fork_name(fork):
142 def fork_name(fork):
143 return h.link_to(fork.repo_name,
143 return h.link_to(fork.repo_name,
144 h.route_path('repo_summary', repo_name=fork.repo_name))
144 h.route_path('repo_summary', repo_name=fork.repo_name))
145
145
146 forks_data = []
146 forks_data = []
147 for fork in fork_list:
147 for fork in fork_list:
148 forks_data.append({
148 forks_data.append({
149 "username": h.gravatar_with_user(self.request, fork.user.username),
149 "username": h.gravatar_with_user(self.request, fork.user.username),
150 "fork_name": fork_name(fork),
150 "fork_name": fork_name(fork),
151 "description": fork.description_safe,
151 "description": fork.description_safe,
152 "fork_date": h.age_component(fork.created_on, time_is_local=True),
152 "fork_date": h.age_component(fork.created_on, time_is_local=True),
153 "last_activity": h.format_date(fork.updated_on),
153 "last_activity": h.format_date(fork.updated_on),
154 "action": fork_actions(fork),
154 "action": fork_actions(fork),
155 })
155 })
156
156
157 data = ({
157 data = ({
158 'draw': draw,
158 'draw': draw,
159 'data': forks_data,
159 'data': forks_data,
160 'recordsTotal': forks_data_total_count,
160 'recordsTotal': forks_data_total_count,
161 'recordsFiltered': forks_data_total_filtered_count,
161 'recordsFiltered': forks_data_total_filtered_count,
162 })
162 })
163
163
164 return data
164 return data
165
165
166 @LoginRequired()
166 @LoginRequired()
167 @NotAnonymous()
167 @NotAnonymous()
168 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
168 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
169 @HasRepoPermissionAnyDecorator(
169 @HasRepoPermissionAnyDecorator(
170 'repository.read', 'repository.write', 'repository.admin')
170 'repository.read', 'repository.write', 'repository.admin')
171 def repo_fork_new(self):
171 def repo_fork_new(self):
172 c = self.load_default_context()
172 c = self.load_default_context()
173
173
174 defaults = RepoModel()._get_defaults(self.db_repo_name)
174 defaults = RepoModel()._get_defaults(self.db_repo_name)
175 # alter the description to indicate a fork
175 # alter the description to indicate a fork
176 defaults['description'] = (
176 defaults['description'] = (
177 'fork of repository: %s \n%s' % (
177 'fork of repository: %s \n%s' % (
178 defaults['repo_name'], defaults['description']))
178 defaults['repo_name'], defaults['description']))
179 # add suffix to fork
179 # add suffix to fork
180 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
180 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
181
181
182 data = render('rhodecode:templates/forks/fork.mako',
182 data = render('rhodecode:templates/forks/fork.mako',
183 self._get_template_context(c), self.request)
183 self._get_template_context(c), self.request)
184 html = formencode.htmlfill.render(
184 html = formencode.htmlfill.render(
185 data,
185 data,
186 defaults=defaults,
186 defaults=defaults,
187 encoding="UTF-8",
187 encoding="UTF-8",
188 force_defaults=False
188 force_defaults=False
189 )
189 )
190 return Response(html)
190 return Response(html)
191
191
192 @LoginRequired()
192 @LoginRequired()
193 @NotAnonymous()
193 @NotAnonymous()
194 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
194 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
195 @HasRepoPermissionAnyDecorator(
195 @HasRepoPermissionAnyDecorator(
196 'repository.read', 'repository.write', 'repository.admin')
196 'repository.read', 'repository.write', 'repository.admin')
197 @CSRFRequired()
197 @CSRFRequired()
198 def repo_fork_create(self):
198 def repo_fork_create(self):
199 _ = self.request.translate
199 _ = self.request.translate
200 c = self.load_default_context()
200 c = self.load_default_context()
201
201
202 _form = RepoForkForm(self.request.translate,
202 _form = RepoForkForm(self.request.translate,
203 old_data={'repo_type': self.db_repo.repo_type},
203 old_data={'repo_type': self.db_repo.repo_type},
204 repo_groups=c.repo_groups_choices)()
204 repo_groups=c.repo_groups_choices)()
205 post_data = dict(self.request.POST)
205 post_data = dict(self.request.POST)
206
206
207 # forbid injecting other repo by forging a request
207 # forbid injecting other repo by forging a request
208 post_data['fork_parent_id'] = self.db_repo.repo_id
208 post_data['fork_parent_id'] = self.db_repo.repo_id
209 post_data['landing_rev'] = self.db_repo._landing_revision
209 post_data['landing_rev'] = self.db_repo._landing_revision
210
210
211 form_result = {}
211 form_result = {}
212 task_id = None
212 task_id = None
213 try:
213 try:
214 form_result = _form.to_python(post_data)
214 form_result = _form.to_python(post_data)
215 copy_permissions = form_result.get('copy_permissions')
215 copy_permissions = form_result.get('copy_permissions')
216 # create fork is done sometimes async on celery, db transaction
216 # create fork is done sometimes async on celery, db transaction
217 # management is handled there.
217 # management is handled there.
218 task = RepoModel().create_fork(
218 task = RepoModel().create_fork(
219 form_result, c.rhodecode_user.user_id)
219 form_result, c.rhodecode_user.user_id)
220
220
221 task_id = get_task_id(task)
221 task_id = get_task_id(task)
222 except formencode.Invalid as errors:
222 except formencode.Invalid as errors:
223 c.rhodecode_db_repo = self.db_repo
223 c.rhodecode_db_repo = self.db_repo
224
224
225 data = render('rhodecode:templates/forks/fork.mako',
225 data = render('rhodecode:templates/forks/fork.mako',
226 self._get_template_context(c), self.request)
226 self._get_template_context(c), self.request)
227 html = formencode.htmlfill.render(
227 html = formencode.htmlfill.render(
228 data,
228 data,
229 defaults=errors.value,
229 defaults=errors.value,
230 errors=errors.error_dict or {},
230 errors=errors.error_dict or {},
231 prefix_error=False,
231 prefix_error=False,
232 encoding="UTF-8",
232 encoding="UTF-8",
233 force_defaults=False
233 force_defaults=False
234 )
234 )
235 return Response(html)
235 return Response(html)
236 except Exception:
236 except Exception:
237 log.exception(
237 log.exception(
238 u'Exception while trying to fork the repository %s', self.db_repo_name)
238 u'Exception while trying to fork the repository %s', self.db_repo_name)
239 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
239 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
240 h.flash(msg, category='error')
240 h.flash(msg, category='error')
241 raise HTTPFound(h.route_path('home'))
241 raise HTTPFound(h.route_path('home'))
242
242
243 repo_name = form_result.get('repo_name_full', self.db_repo_name)
243 repo_name = form_result.get('repo_name_full', self.db_repo_name)
244
244
245 affected_user_ids = [self._rhodecode_user.user_id]
245 affected_user_ids = [self._rhodecode_user.user_id]
246 if copy_permissions:
246 if copy_permissions:
247 # permission flush is done in repo creating
247 # permission flush is done in repo creating
248 pass
248 pass
249
249
250 PermissionModel().trigger_permission_flush(affected_user_ids)
250 PermissionModel().trigger_permission_flush(affected_user_ids)
251
251
252 raise HTTPFound(
252 raise HTTPFound(
253 h.route_path('repo_creating', repo_name=repo_name,
253 h.route_path('repo_creating', repo_name=repo_name,
254 _query=dict(task_id=task_id)))
254 _query=dict(task_id=task_id)))
@@ -1,1877 +1,1877 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib import ext_json
38 from rhodecode.lib import ext_json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name,
82 repo_name,
83 search_q=search_q, statuses=statuses,
83 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name,
86 repo_name,
87 search_q=search_q, statuses=statuses)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, self._rhodecode_user.user_id,
90 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
95 search_q=search_q, statuses=statuses)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
110 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
111
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
116 my_review_status = statuses[0][1].status
117
117
118 data.append({
118 data.append({
119 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
120 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
121 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
122 short=True),
122 short=True),
123 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
124 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
125 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
126 'my_status': _render('pullrequest_status',
127 my_review_status),
127 my_review_status),
128 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
129 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
130 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
131 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
132 pr.versions_count),
132 pr.versions_count),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
135 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
138 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
139 pr.author.full_contact, ),
139 pr.author.full_contact, ),
140 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
141 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
142 'comments_raw': comments_count,
142 'comments_raw': comments_count,
143 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
144 })
144 })
145
145
146 data = ({
146 data = ({
147 'draw': draw,
147 'draw': draw,
148 'data': data,
148 'data': data,
149 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
151 })
151 })
152 return data
152 return data
153
153
154 @LoginRequired()
154 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
157 def pull_request_list(self):
157 def pull_request_list(self):
158 c = self.load_default_context()
158 c = self.load_default_context()
159
159
160 req_get = self.request.GET
160 req_get = self.request.GET
161 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
162 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
163 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166
166
167 c.active = 'open'
167 c.active = 'open'
168 if c.my:
168 if c.my:
169 c.active = 'my'
169 c.active = 'my'
170 if c.closed:
170 if c.closed:
171 c.active = 'closed'
171 c.active = 'closed'
172 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
173 c.active = 'awaiting'
173 c.active = 'awaiting'
174 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
175 c.active = 'source'
175 c.active = 'source'
176 if c.awaiting_my_review:
176 if c.awaiting_my_review:
177 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
178
178
179 return self._get_template_context(c)
179 return self._get_template_context(c)
180
180
181 @LoginRequired()
181 @LoginRequired()
182 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
184 def pull_request_list_data(self):
184 def pull_request_list_data(self):
185 self.load_default_context()
185 self.load_default_context()
186
186
187 # additional filters
187 # additional filters
188 req_get = self.request.GET
188 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
194
195 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
197 else None
197 else None
198
198
199 opened_by = None
199 opened_by = None
200 if my:
200 if my:
201 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
202
202
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
204 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
210
211 return data
211 return data
212
212
213 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
217 return caching_enabled
218
218
219 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
220 ancestor_commit,
220 ancestor_commit,
221 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
222 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224
224
225 target_commit_final = target_commit
225 target_commit_final = target_commit
226 source_commit_final = source_commit
226 source_commit_final = source_commit
227
227
228 if use_ancestor:
228 if use_ancestor:
229 # we might want to not use it for versions
229 # we might want to not use it for versions
230 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
231 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
232
232
233 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
234 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
235 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
236
236
237 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
240
240
241 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
242
242
243 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
244 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
245 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 )
248 )
249 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
250 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
251
251
252 return diffset
252 return diffset
253
253
254 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
255 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
256 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
257 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
258 commit1, commit2,
258 commit1, commit2,
259 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
260 context=diff_context)
260 context=diff_context)
261
261
262 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
265
265
266 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
267
267
268 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
269 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272
272
273 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275
275
276 return diffset
276 return diffset
277
277
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 comments_model = CommentsModel()
279 comments_model = CommentsModel()
280
280
281 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 if not include_drafts:
284 if not include_drafts:
285 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
286 general_comments = q
286 general_comments = q
287
287
288 # pick comments we want to render at current version
288 # pick comments we want to render at current version
289 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
290 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
291
291
292 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
295 if not include_drafts:
295 if not include_drafts:
296 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
297 inline_comments = q
297 inline_comments = q
298
298
299 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
300 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
301
301
302 # Comments inline+general
302 # Comments inline+general
303 if c.at_version:
303 if c.at_version:
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
306 else:
306 else:
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
309
309
310 return general_comments, inline_comments
310 return general_comments, inline_comments
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
314 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
315 def pull_request_show(self):
315 def pull_request_show(self):
316 _ = self.request.translate
316 _ = self.request.translate
317 c = self.load_default_context()
317 c = self.load_default_context()
318
318
319 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
320 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
321 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
322
322
323 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325
325
326 _new_state = {
326 _new_state = {
327 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
328 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
330
330
331 if can_force_state and _new_state:
331 if can_force_state and _new_state:
332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
333 h.flash(
333 h.flash(
334 _('Pull Request state was force changed to `{}`').format(_new_state),
334 _('Pull Request state was force changed to `{}`').format(_new_state),
335 category='success')
335 category='success')
336 Session().commit()
336 Session().commit()
337
337
338 raise HTTPFound(h.route_path(
338 raise HTTPFound(h.route_path(
339 'pullrequest_show', repo_name=self.db_repo_name,
339 'pullrequest_show', repo_name=self.db_repo_name,
340 pull_request_id=pull_request_id))
340 pull_request_id=pull_request_id))
341
341
342 version = self.request.GET.get('version')
342 version = self.request.GET.get('version')
343 from_version = self.request.GET.get('from_version') or version
343 from_version = self.request.GET.get('from_version') or version
344 merge_checks = self.request.GET.get('merge_checks')
344 merge_checks = self.request.GET.get('merge_checks')
345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
348
348
349 # fetch global flags of ignore ws or context lines
349 # fetch global flags of ignore ws or context lines
350 diff_context = diffs.get_diff_context(self.request)
350 diff_context = diffs.get_diff_context(self.request)
351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
352
352
353 (pull_request_latest,
353 (pull_request_latest,
354 pull_request_at_ver,
354 pull_request_at_ver,
355 pull_request_display_obj,
355 pull_request_display_obj,
356 at_version) = PullRequestModel().get_pr_version(
356 at_version) = PullRequestModel().get_pr_version(
357 pull_request_id, version=version)
357 pull_request_id, version=version)
358
358
359 pr_closed = pull_request_latest.is_closed()
359 pr_closed = pull_request_latest.is_closed()
360
360
361 if pr_closed and (version or from_version):
361 if pr_closed and (version or from_version):
362 # not allow to browse versions for closed PR
362 # not allow to browse versions for closed PR
363 raise HTTPFound(h.route_path(
363 raise HTTPFound(h.route_path(
364 'pullrequest_show', repo_name=self.db_repo_name,
364 'pullrequest_show', repo_name=self.db_repo_name,
365 pull_request_id=pull_request_id))
365 pull_request_id=pull_request_id))
366
366
367 versions = pull_request_display_obj.versions()
367 versions = pull_request_display_obj.versions()
368
368
369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
370
370
371 # used to store per-commit range diffs
371 # used to store per-commit range diffs
372 c.changes = collections.OrderedDict()
372 c.changes = collections.OrderedDict()
373
373
374 c.at_version = at_version
374 c.at_version = at_version
375 c.at_version_num = (at_version
375 c.at_version_num = (at_version
376 if at_version and at_version != PullRequest.LATEST_VER
376 if at_version and at_version != PullRequest.LATEST_VER
377 else None)
377 else None)
378
378
379 c.at_version_index = ChangesetComment.get_index_from_version(
379 c.at_version_index = ChangesetComment.get_index_from_version(
380 c.at_version_num, versions)
380 c.at_version_num, versions)
381
381
382 (prev_pull_request_latest,
382 (prev_pull_request_latest,
383 prev_pull_request_at_ver,
383 prev_pull_request_at_ver,
384 prev_pull_request_display_obj,
384 prev_pull_request_display_obj,
385 prev_at_version) = PullRequestModel().get_pr_version(
385 prev_at_version) = PullRequestModel().get_pr_version(
386 pull_request_id, version=from_version)
386 pull_request_id, version=from_version)
387
387
388 c.from_version = prev_at_version
388 c.from_version = prev_at_version
389 c.from_version_num = (prev_at_version
389 c.from_version_num = (prev_at_version
390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
391 else None)
391 else None)
392 c.from_version_index = ChangesetComment.get_index_from_version(
392 c.from_version_index = ChangesetComment.get_index_from_version(
393 c.from_version_num, versions)
393 c.from_version_num, versions)
394
394
395 # define if we're in COMPARE mode or VIEW at version mode
395 # define if we're in COMPARE mode or VIEW at version mode
396 compare = at_version != prev_at_version
396 compare = at_version != prev_at_version
397
397
398 # pull_requests repo_name we opened it against
398 # pull_requests repo_name we opened it against
399 # ie. target_repo must match
399 # ie. target_repo must match
400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
401 log.warning('Mismatch between the current repo: %s, and target %s',
401 log.warning('Mismatch between the current repo: %s, and target %s',
402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
403 raise HTTPNotFound()
403 raise HTTPNotFound()
404
404
405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
406
406
407 c.pull_request = pull_request_display_obj
407 c.pull_request = pull_request_display_obj
408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
409 c.pull_request_latest = pull_request_latest
409 c.pull_request_latest = pull_request_latest
410
410
411 # inject latest version
411 # inject latest version
412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
413 c.versions = versions + [latest_ver]
413 c.versions = versions + [latest_ver]
414
414
415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
416 c.allowed_to_change_status = False
416 c.allowed_to_change_status = False
417 c.allowed_to_update = False
417 c.allowed_to_update = False
418 c.allowed_to_merge = False
418 c.allowed_to_merge = False
419 c.allowed_to_delete = False
419 c.allowed_to_delete = False
420 c.allowed_to_comment = False
420 c.allowed_to_comment = False
421 c.allowed_to_close = False
421 c.allowed_to_close = False
422 else:
422 else:
423 can_change_status = PullRequestModel().check_user_change_status(
423 can_change_status = PullRequestModel().check_user_change_status(
424 pull_request_at_ver, self._rhodecode_user)
424 pull_request_at_ver, self._rhodecode_user)
425 c.allowed_to_change_status = can_change_status and not pr_closed
425 c.allowed_to_change_status = can_change_status and not pr_closed
426
426
427 c.allowed_to_update = PullRequestModel().check_user_update(
427 c.allowed_to_update = PullRequestModel().check_user_update(
428 pull_request_latest, self._rhodecode_user) and not pr_closed
428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 c.allowed_to_merge = PullRequestModel().check_user_merge(
430 pull_request_latest, self._rhodecode_user) and not pr_closed
430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 c.allowed_to_delete = PullRequestModel().check_user_delete(
432 pull_request_latest, self._rhodecode_user) and not pr_closed
432 pull_request_latest, self._rhodecode_user) and not pr_closed
433 c.allowed_to_comment = not pr_closed
433 c.allowed_to_comment = not pr_closed
434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
435
435
436 c.forbid_adding_reviewers = False
436 c.forbid_adding_reviewers = False
437
437
438 if pull_request_latest.reviewer_data and \
438 if pull_request_latest.reviewer_data and \
439 'rules' in pull_request_latest.reviewer_data:
439 'rules' in pull_request_latest.reviewer_data:
440 rules = pull_request_latest.reviewer_data['rules'] or {}
440 rules = pull_request_latest.reviewer_data['rules'] or {}
441 try:
441 try:
442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
443 except Exception:
443 except Exception:
444 pass
444 pass
445
445
446 # check merge capabilities
446 # check merge capabilities
447 _merge_check = MergeCheck.validate(
447 _merge_check = MergeCheck.validate(
448 pull_request_latest, auth_user=self._rhodecode_user,
448 pull_request_latest, auth_user=self._rhodecode_user,
449 translator=self.request.translate,
449 translator=self.request.translate,
450 force_shadow_repo_refresh=force_refresh)
450 force_shadow_repo_refresh=force_refresh)
451
451
452 c.pr_merge_errors = _merge_check.error_details
452 c.pr_merge_errors = _merge_check.error_details
453 c.pr_merge_possible = not _merge_check.failed
453 c.pr_merge_possible = not _merge_check.failed
454 c.pr_merge_message = _merge_check.merge_msg
454 c.pr_merge_message = _merge_check.merge_msg
455 c.pr_merge_source_commit = _merge_check.source_commit
455 c.pr_merge_source_commit = _merge_check.source_commit
456 c.pr_merge_target_commit = _merge_check.target_commit
456 c.pr_merge_target_commit = _merge_check.target_commit
457
457
458 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 c.pr_merge_info = MergeCheck.get_merge_conditions(
459 pull_request_latest, translator=self.request.translate)
459 pull_request_latest, translator=self.request.translate)
460
460
461 c.pull_request_review_status = _merge_check.review_status
461 c.pull_request_review_status = _merge_check.review_status
462 if merge_checks:
462 if merge_checks:
463 self.request.override_renderer = \
463 self.request.override_renderer = \
464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
465 return self._get_template_context(c)
465 return self._get_template_context(c)
466
466
467 c.reviewers_count = pull_request.reviewers_count
467 c.reviewers_count = pull_request.reviewers_count
468 c.observers_count = pull_request.observers_count
468 c.observers_count = pull_request.observers_count
469
469
470 # reviewers and statuses
470 # reviewers and statuses
471 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
471 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
474
474
475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
476 member_reviewer = h.reviewer_as_json(
476 member_reviewer = h.reviewer_as_json(
477 member, reasons=reasons, mandatory=mandatory,
477 member, reasons=reasons, mandatory=mandatory,
478 role=review_obj.role,
478 role=review_obj.role,
479 user_group=review_obj.rule_user_group_data()
479 user_group=review_obj.rule_user_group_data()
480 )
480 )
481
481
482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
483 member_reviewer['review_status'] = current_review_status
483 member_reviewer['review_status'] = current_review_status
484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
485 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 member_reviewer['allowed_to_update'] = c.allowed_to_update
486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
487
487
488 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
488 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
489
489
490 for observer_obj, member in pull_request_at_ver.observers():
490 for observer_obj, member in pull_request_at_ver.observers():
491 member_observer = h.reviewer_as_json(
491 member_observer = h.reviewer_as_json(
492 member, reasons=[], mandatory=False,
492 member, reasons=[], mandatory=False,
493 role=observer_obj.role,
493 role=observer_obj.role,
494 user_group=observer_obj.rule_user_group_data()
494 user_group=observer_obj.rule_user_group_data()
495 )
495 )
496 member_observer['allowed_to_update'] = c.allowed_to_update
496 member_observer['allowed_to_update'] = c.allowed_to_update
497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
498
498
499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
500
500
501 general_comments, inline_comments = \
501 general_comments, inline_comments = \
502 self.register_comments_vars(c, pull_request_latest, versions)
502 self.register_comments_vars(c, pull_request_latest, versions)
503
503
504 # TODOs
504 # TODOs
505 c.unresolved_comments = CommentsModel() \
505 c.unresolved_comments = CommentsModel() \
506 .get_pull_request_unresolved_todos(pull_request_latest)
506 .get_pull_request_unresolved_todos(pull_request_latest)
507 c.resolved_comments = CommentsModel() \
507 c.resolved_comments = CommentsModel() \
508 .get_pull_request_resolved_todos(pull_request_latest)
508 .get_pull_request_resolved_todos(pull_request_latest)
509
509
510 # Drafts
510 # Drafts
511 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 c.draft_comments = CommentsModel().get_pull_request_drafts(
512 self._rhodecode_db_user.user_id,
512 self._rhodecode_db_user.user_id,
513 pull_request_latest)
513 pull_request_latest)
514
514
515 # if we use version, then do not show later comments
515 # if we use version, then do not show later comments
516 # than current version
516 # than current version
517 display_inline_comments = collections.defaultdict(
517 display_inline_comments = collections.defaultdict(
518 lambda: collections.defaultdict(list))
518 lambda: collections.defaultdict(list))
519 for co in inline_comments:
519 for co in inline_comments:
520 if c.at_version_num:
520 if c.at_version_num:
521 # pick comments that are at least UPTO given version, so we
521 # pick comments that are at least UPTO given version, so we
522 # don't render comments for higher version
522 # don't render comments for higher version
523 should_render = co.pull_request_version_id and \
523 should_render = co.pull_request_version_id and \
524 co.pull_request_version_id <= c.at_version_num
524 co.pull_request_version_id <= c.at_version_num
525 else:
525 else:
526 # showing all, for 'latest'
526 # showing all, for 'latest'
527 should_render = True
527 should_render = True
528
528
529 if should_render:
529 if should_render:
530 display_inline_comments[co.f_path][co.line_no].append(co)
530 display_inline_comments[co.f_path][co.line_no].append(co)
531
531
532 # load diff data into template context, if we use compare mode then
532 # load diff data into template context, if we use compare mode then
533 # diff is calculated based on changes between versions of PR
533 # diff is calculated based on changes between versions of PR
534
534
535 source_repo = pull_request_at_ver.source_repo
535 source_repo = pull_request_at_ver.source_repo
536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
537
537
538 target_repo = pull_request_at_ver.target_repo
538 target_repo = pull_request_at_ver.target_repo
539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
540
540
541 if compare:
541 if compare:
542 # in compare switch the diff base to latest commit from prev version
542 # in compare switch the diff base to latest commit from prev version
543 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 target_ref_id = prev_pull_request_display_obj.revisions[0]
544
544
545 # despite opening commits for bookmarks/branches/tags, we always
545 # despite opening commits for bookmarks/branches/tags, we always
546 # convert this to rev to prevent changes after bookmark or branch change
546 # convert this to rev to prevent changes after bookmark or branch change
547 c.source_ref_type = 'rev'
547 c.source_ref_type = 'rev'
548 c.source_ref = source_ref_id
548 c.source_ref = source_ref_id
549
549
550 c.target_ref_type = 'rev'
550 c.target_ref_type = 'rev'
551 c.target_ref = target_ref_id
551 c.target_ref = target_ref_id
552
552
553 c.source_repo = source_repo
553 c.source_repo = source_repo
554 c.target_repo = target_repo
554 c.target_repo = target_repo
555
555
556 c.commit_ranges = []
556 c.commit_ranges = []
557 source_commit = EmptyCommit()
557 source_commit = EmptyCommit()
558 target_commit = EmptyCommit()
558 target_commit = EmptyCommit()
559 c.missing_requirements = False
559 c.missing_requirements = False
560
560
561 source_scm = source_repo.scm_instance()
561 source_scm = source_repo.scm_instance()
562 target_scm = target_repo.scm_instance()
562 target_scm = target_repo.scm_instance()
563
563
564 shadow_scm = None
564 shadow_scm = None
565 try:
565 try:
566 shadow_scm = pull_request_latest.get_shadow_repo()
566 shadow_scm = pull_request_latest.get_shadow_repo()
567 except Exception:
567 except Exception:
568 log.debug('Failed to get shadow repo', exc_info=True)
568 log.debug('Failed to get shadow repo', exc_info=True)
569 # try first the existing source_repo, and then shadow
569 # try first the existing source_repo, and then shadow
570 # repo if we can obtain one
570 # repo if we can obtain one
571 commits_source_repo = source_scm
571 commits_source_repo = source_scm
572 if shadow_scm:
572 if shadow_scm:
573 commits_source_repo = shadow_scm
573 commits_source_repo = shadow_scm
574
574
575 c.commits_source_repo = commits_source_repo
575 c.commits_source_repo = commits_source_repo
576 c.ancestor = None # set it to None, to hide it from PR view
576 c.ancestor = None # set it to None, to hide it from PR view
577
577
578 # empty version means latest, so we keep this to prevent
578 # empty version means latest, so we keep this to prevent
579 # double caching
579 # double caching
580 version_normalized = version or PullRequest.LATEST_VER
580 version_normalized = version or PullRequest.LATEST_VER
581 from_version_normalized = from_version or PullRequest.LATEST_VER
581 from_version_normalized = from_version or PullRequest.LATEST_VER
582
582
583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
584 cache_file_path = diff_cache_exist(
584 cache_file_path = diff_cache_exist(
585 cache_path, 'pull_request', pull_request_id, version_normalized,
585 cache_path, 'pull_request', pull_request_id, version_normalized,
586 from_version_normalized, source_ref_id, target_ref_id,
586 from_version_normalized, source_ref_id, target_ref_id,
587 hide_whitespace_changes, diff_context, c.fulldiff)
587 hide_whitespace_changes, diff_context, c.fulldiff)
588
588
589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
590 force_recache = self.get_recache_flag()
590 force_recache = self.get_recache_flag()
591
591
592 cached_diff = None
592 cached_diff = None
593 if caching_enabled:
593 if caching_enabled:
594 cached_diff = load_cached_diff(cache_file_path)
594 cached_diff = load_cached_diff(cache_file_path)
595
595
596 has_proper_commit_cache = (
596 has_proper_commit_cache = (
597 cached_diff and cached_diff.get('commits')
597 cached_diff and cached_diff.get('commits')
598 and len(cached_diff.get('commits', [])) == 5
598 and len(cached_diff.get('commits', [])) == 5
599 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[0]
600 and cached_diff.get('commits')[3])
600 and cached_diff.get('commits')[3])
601
601
602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
603 diff_commit_cache = \
603 diff_commit_cache = \
604 (ancestor_commit, commit_cache, missing_requirements,
604 (ancestor_commit, commit_cache, missing_requirements,
605 source_commit, target_commit) = cached_diff['commits']
605 source_commit, target_commit) = cached_diff['commits']
606 else:
606 else:
607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
608 # merge errors resulting in potentially hidden commits in the shadow repo.
608 # merge errors resulting in potentially hidden commits in the shadow repo.
609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
610 and _merge_check.merge_response
610 and _merge_check.merge_response
611 maybe_unreachable = maybe_unreachable \
611 maybe_unreachable = maybe_unreachable \
612 and _merge_check.merge_response.metadata.get('unresolved_files')
612 and _merge_check.merge_response.metadata.get('unresolved_files')
613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
614 diff_commit_cache = \
614 diff_commit_cache = \
615 (ancestor_commit, commit_cache, missing_requirements,
615 (ancestor_commit, commit_cache, missing_requirements,
616 source_commit, target_commit) = self.get_commits(
616 source_commit, target_commit) = self.get_commits(
617 commits_source_repo,
617 commits_source_repo,
618 pull_request_at_ver,
618 pull_request_at_ver,
619 source_commit,
619 source_commit,
620 source_ref_id,
620 source_ref_id,
621 source_scm,
621 source_scm,
622 target_commit,
622 target_commit,
623 target_ref_id,
623 target_ref_id,
624 target_scm,
624 target_scm,
625 maybe_unreachable=maybe_unreachable)
625 maybe_unreachable=maybe_unreachable)
626
626
627 # register our commit range
627 # register our commit range
628 for comm in commit_cache.values():
628 for comm in commit_cache.values():
629 c.commit_ranges.append(comm)
629 c.commit_ranges.append(comm)
630
630
631 c.missing_requirements = missing_requirements
631 c.missing_requirements = missing_requirements
632 c.ancestor_commit = ancestor_commit
632 c.ancestor_commit = ancestor_commit
633 c.statuses = source_repo.statuses(
633 c.statuses = source_repo.statuses(
634 [x.raw_id for x in c.commit_ranges])
634 [x.raw_id for x in c.commit_ranges])
635
635
636 # auto collapse if we have more than limit
636 # auto collapse if we have more than limit
637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
639 c.compare_mode = compare
639 c.compare_mode = compare
640
640
641 # diff_limit is the old behavior, will cut off the whole diff
641 # diff_limit is the old behavior, will cut off the whole diff
642 # if the limit is applied otherwise will just hide the
642 # if the limit is applied otherwise will just hide the
643 # big files from the front-end
643 # big files from the front-end
644 diff_limit = c.visual.cut_off_limit_diff
644 diff_limit = c.visual.cut_off_limit_diff
645 file_limit = c.visual.cut_off_limit_file
645 file_limit = c.visual.cut_off_limit_file
646
646
647 c.missing_commits = False
647 c.missing_commits = False
648 if (c.missing_requirements
648 if (c.missing_requirements
649 or isinstance(source_commit, EmptyCommit)
649 or isinstance(source_commit, EmptyCommit)
650 or source_commit == target_commit):
650 or source_commit == target_commit):
651
651
652 c.missing_commits = True
652 c.missing_commits = True
653 else:
653 else:
654 c.inline_comments = display_inline_comments
654 c.inline_comments = display_inline_comments
655
655
656 use_ancestor = True
656 use_ancestor = True
657 if from_version_normalized != version_normalized:
657 if from_version_normalized != version_normalized:
658 use_ancestor = False
658 use_ancestor = False
659
659
660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
661 if not force_recache and has_proper_diff_cache:
661 if not force_recache and has_proper_diff_cache:
662 c.diffset = cached_diff['diff']
662 c.diffset = cached_diff['diff']
663 else:
663 else:
664 try:
664 try:
665 c.diffset = self._get_diffset(
665 c.diffset = self._get_diffset(
666 c.source_repo.repo_name, commits_source_repo,
666 c.source_repo.repo_name, commits_source_repo,
667 c.ancestor_commit,
667 c.ancestor_commit,
668 source_ref_id, target_ref_id,
668 source_ref_id, target_ref_id,
669 target_commit, source_commit,
669 target_commit, source_commit,
670 diff_limit, file_limit, c.fulldiff,
670 diff_limit, file_limit, c.fulldiff,
671 hide_whitespace_changes, diff_context,
671 hide_whitespace_changes, diff_context,
672 use_ancestor=use_ancestor
672 use_ancestor=use_ancestor
673 )
673 )
674
674
675 # save cached diff
675 # save cached diff
676 if caching_enabled:
676 if caching_enabled:
677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 log.exception('Failed to generate diffset')
679 log.exception('Failed to generate diffset')
680 c.missing_commits = True
680 c.missing_commits = True
681
681
682 if not c.missing_commits:
682 if not c.missing_commits:
683
683
684 c.limited_diff = c.diffset.limited_diff
684 c.limited_diff = c.diffset.limited_diff
685
685
686 # calculate removed files that are bound to comments
686 # calculate removed files that are bound to comments
687 comment_deleted_files = [
687 comment_deleted_files = [
688 fname for fname in display_inline_comments
688 fname for fname in display_inline_comments
689 if fname not in c.diffset.file_stats]
689 if fname not in c.diffset.file_stats]
690
690
691 c.deleted_files_comments = collections.defaultdict(dict)
691 c.deleted_files_comments = collections.defaultdict(dict)
692 for fname, per_line_comments in display_inline_comments.items():
692 for fname, per_line_comments in display_inline_comments.items():
693 if fname in comment_deleted_files:
693 if fname in comment_deleted_files:
694 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['stats'] = 0
695 c.deleted_files_comments[fname]['comments'] = list()
695 c.deleted_files_comments[fname]['comments'] = list()
696 for lno, comments in per_line_comments.items():
696 for lno, comments in per_line_comments.items():
697 c.deleted_files_comments[fname]['comments'].extend(comments)
697 c.deleted_files_comments[fname]['comments'].extend(comments)
698
698
699 # maybe calculate the range diff
699 # maybe calculate the range diff
700 if c.range_diff_on:
700 if c.range_diff_on:
701 # TODO(marcink): set whitespace/context
701 # TODO(marcink): set whitespace/context
702 context_lcl = 3
702 context_lcl = 3
703 ign_whitespace_lcl = False
703 ign_whitespace_lcl = False
704
704
705 for commit in c.commit_ranges:
705 for commit in c.commit_ranges:
706 commit2 = commit
706 commit2 = commit
707 commit1 = commit.first_parent
707 commit1 = commit.first_parent
708
708
709 range_diff_cache_file_path = diff_cache_exist(
709 range_diff_cache_file_path = diff_cache_exist(
710 cache_path, 'diff', commit.raw_id,
710 cache_path, 'diff', commit.raw_id,
711 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 ign_whitespace_lcl, context_lcl, c.fulldiff)
712
712
713 cached_diff = None
713 cached_diff = None
714 if caching_enabled:
714 if caching_enabled:
715 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 cached_diff = load_cached_diff(range_diff_cache_file_path)
716
716
717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
718 if not force_recache and has_proper_diff_cache:
718 if not force_recache and has_proper_diff_cache:
719 diffset = cached_diff['diff']
719 diffset = cached_diff['diff']
720 else:
720 else:
721 diffset = self._get_range_diffset(
721 diffset = self._get_range_diffset(
722 commits_source_repo, source_repo,
722 commits_source_repo, source_repo,
723 commit1, commit2, diff_limit, file_limit,
723 commit1, commit2, diff_limit, file_limit,
724 c.fulldiff, ign_whitespace_lcl, context_lcl
724 c.fulldiff, ign_whitespace_lcl, context_lcl
725 )
725 )
726
726
727 # save cached diff
727 # save cached diff
728 if caching_enabled:
728 if caching_enabled:
729 cache_diff(range_diff_cache_file_path, diffset, None)
729 cache_diff(range_diff_cache_file_path, diffset, None)
730
730
731 c.changes[commit.raw_id] = diffset
731 c.changes[commit.raw_id] = diffset
732
732
733 # this is a hack to properly display links, when creating PR, the
733 # this is a hack to properly display links, when creating PR, the
734 # compare view and others uses different notation, and
734 # compare view and others uses different notation, and
735 # compare_commits.mako renders links based on the target_repo.
735 # compare_commits.mako renders links based on the target_repo.
736 # We need to swap that here to generate it properly on the html side
736 # We need to swap that here to generate it properly on the html side
737 c.target_repo = c.source_repo
737 c.target_repo = c.source_repo
738
738
739 c.commit_statuses = ChangesetStatus.STATUSES
739 c.commit_statuses = ChangesetStatus.STATUSES
740
740
741 c.show_version_changes = not pr_closed
741 c.show_version_changes = not pr_closed
742 if c.show_version_changes:
742 if c.show_version_changes:
743 cur_obj = pull_request_at_ver
743 cur_obj = pull_request_at_ver
744 prev_obj = prev_pull_request_at_ver
744 prev_obj = prev_pull_request_at_ver
745
745
746 old_commit_ids = prev_obj.revisions
746 old_commit_ids = prev_obj.revisions
747 new_commit_ids = cur_obj.revisions
747 new_commit_ids = cur_obj.revisions
748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
749 old_commit_ids, new_commit_ids)
749 old_commit_ids, new_commit_ids)
750 c.commit_changes_summary = commit_changes
750 c.commit_changes_summary = commit_changes
751
751
752 # calculate the diff for commits between versions
752 # calculate the diff for commits between versions
753 c.commit_changes = []
753 c.commit_changes = []
754
754
755 def mark(cs, fw):
755 def mark(cs, fw):
756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
757
757
758 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 for c_type, raw_id in mark(commit_changes.added, 'a') \
759 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.removed, 'r') \
760 + mark(commit_changes.common, 'c'):
760 + mark(commit_changes.common, 'c'):
761
761
762 if raw_id in commit_cache:
762 if raw_id in commit_cache:
763 commit = commit_cache[raw_id]
763 commit = commit_cache[raw_id]
764 else:
764 else:
765 try:
765 try:
766 commit = commits_source_repo.get_commit(raw_id)
766 commit = commits_source_repo.get_commit(raw_id)
767 except CommitDoesNotExistError:
767 except CommitDoesNotExistError:
768 # in case we fail extracting still use "dummy" commit
768 # in case we fail extracting still use "dummy" commit
769 # for display in commit diff
769 # for display in commit diff
770 commit = h.AttributeDict(
770 commit = h.AttributeDict(
771 {'raw_id': raw_id,
771 {'raw_id': raw_id,
772 'message': 'EMPTY or MISSING COMMIT'})
772 'message': 'EMPTY or MISSING COMMIT'})
773 c.commit_changes.append([c_type, commit])
773 c.commit_changes.append([c_type, commit])
774
774
775 # current user review statuses for each version
775 # current user review statuses for each version
776 c.review_versions = {}
776 c.review_versions = {}
777 is_reviewer = PullRequestModel().is_user_reviewer(
777 is_reviewer = PullRequestModel().is_user_reviewer(
778 pull_request, self._rhodecode_user)
778 pull_request, self._rhodecode_user)
779 if is_reviewer:
779 if is_reviewer:
780 for co in general_comments:
780 for co in general_comments:
781 if co.author.user_id == self._rhodecode_user.user_id:
781 if co.author.user_id == self._rhodecode_user.user_id:
782 status = co.status_change
782 status = co.status_change
783 if status:
783 if status:
784 _ver_pr = status[0].comment.pull_request_version_id
784 _ver_pr = status[0].comment.pull_request_version_id
785 c.review_versions[_ver_pr] = status[0]
785 c.review_versions[_ver_pr] = status[0]
786
786
787 return self._get_template_context(c)
787 return self._get_template_context(c)
788
788
789 def get_commits(
789 def get_commits(
790 self, commits_source_repo, pull_request_at_ver, source_commit,
790 self, commits_source_repo, pull_request_at_ver, source_commit,
791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
792 maybe_unreachable=False):
792 maybe_unreachable=False):
793
793
794 commit_cache = collections.OrderedDict()
794 commit_cache = collections.OrderedDict()
795 missing_requirements = False
795 missing_requirements = False
796
796
797 try:
797 try:
798 pre_load = ["author", "date", "message", "branch", "parents"]
798 pre_load = ["author", "date", "message", "branch", "parents"]
799
799
800 pull_request_commits = pull_request_at_ver.revisions
800 pull_request_commits = pull_request_at_ver.revisions
801 log.debug('Loading %s commits from %s',
801 log.debug('Loading %s commits from %s',
802 len(pull_request_commits), commits_source_repo)
802 len(pull_request_commits), commits_source_repo)
803
803
804 for rev in pull_request_commits:
804 for rev in pull_request_commits:
805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
806 maybe_unreachable=maybe_unreachable)
806 maybe_unreachable=maybe_unreachable)
807 commit_cache[comm.raw_id] = comm
807 commit_cache[comm.raw_id] = comm
808
808
809 # Order here matters, we first need to get target, and then
809 # Order here matters, we first need to get target, and then
810 # the source
810 # the source
811 target_commit = commits_source_repo.get_commit(
811 target_commit = commits_source_repo.get_commit(
812 commit_id=safe_str(target_ref_id))
812 commit_id=safe_str(target_ref_id))
813
813
814 source_commit = commits_source_repo.get_commit(
814 source_commit = commits_source_repo.get_commit(
815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
816 except CommitDoesNotExistError:
816 except CommitDoesNotExistError:
817 log.warning('Failed to get commit from `{}` repo'.format(
817 log.warning('Failed to get commit from `{}` repo'.format(
818 commits_source_repo), exc_info=True)
818 commits_source_repo), exc_info=True)
819 except RepositoryRequirementError:
819 except RepositoryRequirementError:
820 log.warning('Failed to get all required data from repo', exc_info=True)
820 log.warning('Failed to get all required data from repo', exc_info=True)
821 missing_requirements = True
821 missing_requirements = True
822
822
823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
824
824
825 try:
825 try:
826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
827 except Exception:
827 except Exception:
828 ancestor_commit = None
828 ancestor_commit = None
829
829
830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
831
831
832 def assure_not_empty_repo(self):
832 def assure_not_empty_repo(self):
833 _ = self.request.translate
833 _ = self.request.translate
834
834
835 try:
835 try:
836 self.db_repo.scm_instance().get_commit()
836 self.db_repo.scm_instance().get_commit()
837 except EmptyRepositoryError:
837 except EmptyRepositoryError:
838 h.flash(h.literal(_('There are no commits yet')),
838 h.flash(h.literal(_('There are no commits yet')),
839 category='warning')
839 category='warning')
840 raise HTTPFound(
840 raise HTTPFound(
841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
842
842
843 @LoginRequired()
843 @LoginRequired()
844 @NotAnonymous()
844 @NotAnonymous()
845 @HasRepoPermissionAnyDecorator(
845 @HasRepoPermissionAnyDecorator(
846 'repository.read', 'repository.write', 'repository.admin')
846 'repository.read', 'repository.write', 'repository.admin')
847 def pull_request_new(self):
847 def pull_request_new(self):
848 _ = self.request.translate
848 _ = self.request.translate
849 c = self.load_default_context()
849 c = self.load_default_context()
850
850
851 self.assure_not_empty_repo()
851 self.assure_not_empty_repo()
852 source_repo = self.db_repo
852 source_repo = self.db_repo
853
853
854 commit_id = self.request.GET.get('commit')
854 commit_id = self.request.GET.get('commit')
855 branch_ref = self.request.GET.get('branch')
855 branch_ref = self.request.GET.get('branch')
856 bookmark_ref = self.request.GET.get('bookmark')
856 bookmark_ref = self.request.GET.get('bookmark')
857
857
858 try:
858 try:
859 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo_data = PullRequestModel().generate_repo_data(
860 source_repo, commit_id=commit_id,
860 source_repo, commit_id=commit_id,
861 branch=branch_ref, bookmark=bookmark_ref,
861 branch=branch_ref, bookmark=bookmark_ref,
862 translator=self.request.translate)
862 translator=self.request.translate)
863 except CommitDoesNotExistError as e:
863 except CommitDoesNotExistError as e:
864 log.exception(e)
864 log.exception(e)
865 h.flash(_('Commit does not exist'), 'error')
865 h.flash(_('Commit does not exist'), 'error')
866 raise HTTPFound(
866 raise HTTPFound(
867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
868
868
869 default_target_repo = source_repo
869 default_target_repo = source_repo
870
870
871 if source_repo.parent and c.has_origin_repo_read_perm:
871 if source_repo.parent and c.has_origin_repo_read_perm:
872 parent_vcs_obj = source_repo.parent.scm_instance()
872 parent_vcs_obj = source_repo.parent.scm_instance()
873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
874 # change default if we have a parent repo
874 # change default if we have a parent repo
875 default_target_repo = source_repo.parent
875 default_target_repo = source_repo.parent
876
876
877 target_repo_data = PullRequestModel().generate_repo_data(
877 target_repo_data = PullRequestModel().generate_repo_data(
878 default_target_repo, translator=self.request.translate)
878 default_target_repo, translator=self.request.translate)
879
879
880 selected_source_ref = source_repo_data['refs']['selected_ref']
880 selected_source_ref = source_repo_data['refs']['selected_ref']
881 title_source_ref = ''
881 title_source_ref = ''
882 if selected_source_ref:
882 if selected_source_ref:
883 title_source_ref = selected_source_ref.split(':', 2)[1]
883 title_source_ref = selected_source_ref.split(':', 2)[1]
884 c.default_title = PullRequestModel().generate_pullrequest_title(
884 c.default_title = PullRequestModel().generate_pullrequest_title(
885 source=source_repo.repo_name,
885 source=source_repo.repo_name,
886 source_ref=title_source_ref,
886 source_ref=title_source_ref,
887 target=default_target_repo.repo_name
887 target=default_target_repo.repo_name
888 )
888 )
889
889
890 c.default_repo_data = {
890 c.default_repo_data = {
891 'source_repo_name': source_repo.repo_name,
891 'source_repo_name': source_repo.repo_name,
892 'source_refs_json': ext_json.str_json(source_repo_data),
892 'source_refs_json': ext_json.str_json(source_repo_data),
893 'target_repo_name': default_target_repo.repo_name,
893 'target_repo_name': default_target_repo.repo_name,
894 'target_refs_json': ext_json.str_json(target_repo_data),
894 'target_refs_json': ext_json.str_json(target_repo_data),
895 }
895 }
896 c.default_source_ref = selected_source_ref
896 c.default_source_ref = selected_source_ref
897
897
898 return self._get_template_context(c)
898 return self._get_template_context(c)
899
899
900 @LoginRequired()
900 @LoginRequired()
901 @NotAnonymous()
901 @NotAnonymous()
902 @HasRepoPermissionAnyDecorator(
902 @HasRepoPermissionAnyDecorator(
903 'repository.read', 'repository.write', 'repository.admin')
903 'repository.read', 'repository.write', 'repository.admin')
904 def pull_request_repo_refs(self):
904 def pull_request_repo_refs(self):
905 self.load_default_context()
905 self.load_default_context()
906 target_repo_name = self.request.matchdict['target_repo_name']
906 target_repo_name = self.request.matchdict['target_repo_name']
907 repo = Repository.get_by_repo_name(target_repo_name)
907 repo = Repository.get_by_repo_name(target_repo_name)
908 if not repo:
908 if not repo:
909 raise HTTPNotFound()
909 raise HTTPNotFound()
910
910
911 target_perm = HasRepoPermissionAny(
911 target_perm = HasRepoPermissionAny(
912 'repository.read', 'repository.write', 'repository.admin')(
912 'repository.read', 'repository.write', 'repository.admin')(
913 target_repo_name)
913 target_repo_name)
914 if not target_perm:
914 if not target_perm:
915 raise HTTPNotFound()
915 raise HTTPNotFound()
916
916
917 return PullRequestModel().generate_repo_data(
917 return PullRequestModel().generate_repo_data(
918 repo, translator=self.request.translate)
918 repo, translator=self.request.translate)
919
919
920 @LoginRequired()
920 @LoginRequired()
921 @NotAnonymous()
921 @NotAnonymous()
922 @HasRepoPermissionAnyDecorator(
922 @HasRepoPermissionAnyDecorator(
923 'repository.read', 'repository.write', 'repository.admin')
923 'repository.read', 'repository.write', 'repository.admin')
924 def pullrequest_repo_targets(self):
924 def pullrequest_repo_targets(self):
925 _ = self.request.translate
925 _ = self.request.translate
926 filter_query = self.request.GET.get('query')
926 filter_query = self.request.GET.get('query')
927
927
928 # get the parents
928 # get the parents
929 parent_target_repos = []
929 parent_target_repos = []
930 if self.db_repo.parent:
930 if self.db_repo.parent:
931 parents_query = Repository.query() \
931 parents_query = Repository.query() \
932 .order_by(func.length(Repository.repo_name)) \
932 .order_by(func.length(Repository.repo_name)) \
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934
934
935 if filter_query:
935 if filter_query:
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 parents_query = parents_query.filter(
937 parents_query = parents_query.filter(
938 Repository.repo_name.ilike(ilike_expression))
938 Repository.repo_name.ilike(ilike_expression))
939 parents = parents_query.limit(20).all()
939 parents = parents_query.limit(20).all()
940
940
941 for parent in parents:
941 for parent in parents:
942 parent_vcs_obj = parent.scm_instance()
942 parent_vcs_obj = parent.scm_instance()
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 parent_target_repos.append(parent)
944 parent_target_repos.append(parent)
945
945
946 # get other forks, and repo itself
946 # get other forks, and repo itself
947 query = Repository.query() \
947 query = Repository.query() \
948 .order_by(func.length(Repository.repo_name)) \
948 .order_by(func.length(Repository.repo_name)) \
949 .filter(
949 .filter(
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 ) \
952 ) \
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954
954
955 if filter_query:
955 if filter_query:
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 ilike_expression = u'%{}%'.format(safe_str(filter_query))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958
958
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 target_repos = query.limit(limit).all()
960 target_repos = query.limit(limit).all()
961
961
962 all_target_repos = target_repos + parent_target_repos
962 all_target_repos = target_repos + parent_target_repos
963
963
964 repos = []
964 repos = []
965 # This checks permissions to the repositories
965 # This checks permissions to the repositories
966 for obj in ScmModel().get_repos(all_target_repos):
966 for obj in ScmModel().get_repos(all_target_repos):
967 repos.append({
967 repos.append({
968 'id': obj['name'],
968 'id': obj['name'],
969 'text': obj['name'],
969 'text': obj['name'],
970 'type': 'repo',
970 'type': 'repo',
971 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_id': obj['dbrepo']['repo_id'],
972 'repo_type': obj['dbrepo']['repo_type'],
972 'repo_type': obj['dbrepo']['repo_type'],
973 'private': obj['dbrepo']['private'],
973 'private': obj['dbrepo']['private'],
974
974
975 })
975 })
976
976
977 data = {
977 data = {
978 'more': False,
978 'more': False,
979 'results': [{
979 'results': [{
980 'text': _('Repositories'),
980 'text': _('Repositories'),
981 'children': repos
981 'children': repos
982 }] if repos else []
982 }] if repos else []
983 }
983 }
984 return data
984 return data
985
985
986 @classmethod
986 @classmethod
987 def get_comment_ids(cls, post_data):
987 def get_comment_ids(cls, post_data):
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989
989
990 @LoginRequired()
990 @LoginRequired()
991 @NotAnonymous()
991 @NotAnonymous()
992 @HasRepoPermissionAnyDecorator(
992 @HasRepoPermissionAnyDecorator(
993 'repository.read', 'repository.write', 'repository.admin')
993 'repository.read', 'repository.write', 'repository.admin')
994 def pullrequest_comments(self):
994 def pullrequest_comments(self):
995 self.load_default_context()
995 self.load_default_context()
996
996
997 pull_request = PullRequest.get_or_404(
997 pull_request = PullRequest.get_or_404(
998 self.request.matchdict['pull_request_id'])
998 self.request.matchdict['pull_request_id'])
999 pull_request_id = pull_request.pull_request_id
999 pull_request_id = pull_request.pull_request_id
1000 version = self.request.GET.get('version')
1000 version = self.request.GET.get('version')
1001
1001
1002 _render = self.request.get_partial_renderer(
1002 _render = self.request.get_partial_renderer(
1003 'rhodecode:templates/base/sidebar.mako')
1003 'rhodecode:templates/base/sidebar.mako')
1004 c = _render.get_call_context()
1004 c = _render.get_call_context()
1005
1005
1006 (pull_request_latest,
1006 (pull_request_latest,
1007 pull_request_at_ver,
1007 pull_request_at_ver,
1008 pull_request_display_obj,
1008 pull_request_display_obj,
1009 at_version) = PullRequestModel().get_pr_version(
1009 at_version) = PullRequestModel().get_pr_version(
1010 pull_request_id, version=version)
1010 pull_request_id, version=version)
1011 versions = pull_request_display_obj.versions()
1011 versions = pull_request_display_obj.versions()
1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1013 c.versions = versions + [latest_ver]
1013 c.versions = versions + [latest_ver]
1014
1014
1015 c.at_version = at_version
1015 c.at_version = at_version
1016 c.at_version_num = (at_version
1016 c.at_version_num = (at_version
1017 if at_version and at_version != PullRequest.LATEST_VER
1017 if at_version and at_version != PullRequest.LATEST_VER
1018 else None)
1018 else None)
1019
1019
1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1021 all_comments = c.inline_comments_flat + c.comments
1021 all_comments = c.inline_comments_flat + c.comments
1022
1022
1023 existing_ids = self.get_comment_ids(self.request.POST)
1023 existing_ids = self.get_comment_ids(self.request.POST)
1024 return _render('comments_table', all_comments, len(all_comments),
1024 return _render('comments_table', all_comments, len(all_comments),
1025 existing_ids=existing_ids)
1025 existing_ids=existing_ids)
1026
1026
1027 @LoginRequired()
1027 @LoginRequired()
1028 @NotAnonymous()
1028 @NotAnonymous()
1029 @HasRepoPermissionAnyDecorator(
1029 @HasRepoPermissionAnyDecorator(
1030 'repository.read', 'repository.write', 'repository.admin')
1030 'repository.read', 'repository.write', 'repository.admin')
1031 def pullrequest_todos(self):
1031 def pullrequest_todos(self):
1032 self.load_default_context()
1032 self.load_default_context()
1033
1033
1034 pull_request = PullRequest.get_or_404(
1034 pull_request = PullRequest.get_or_404(
1035 self.request.matchdict['pull_request_id'])
1035 self.request.matchdict['pull_request_id'])
1036 pull_request_id = pull_request.pull_request_id
1036 pull_request_id = pull_request.pull_request_id
1037 version = self.request.GET.get('version')
1037 version = self.request.GET.get('version')
1038
1038
1039 _render = self.request.get_partial_renderer(
1039 _render = self.request.get_partial_renderer(
1040 'rhodecode:templates/base/sidebar.mako')
1040 'rhodecode:templates/base/sidebar.mako')
1041 c = _render.get_call_context()
1041 c = _render.get_call_context()
1042 (pull_request_latest,
1042 (pull_request_latest,
1043 pull_request_at_ver,
1043 pull_request_at_ver,
1044 pull_request_display_obj,
1044 pull_request_display_obj,
1045 at_version) = PullRequestModel().get_pr_version(
1045 at_version) = PullRequestModel().get_pr_version(
1046 pull_request_id, version=version)
1046 pull_request_id, version=version)
1047 versions = pull_request_display_obj.versions()
1047 versions = pull_request_display_obj.versions()
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 c.versions = versions + [latest_ver]
1049 c.versions = versions + [latest_ver]
1050
1050
1051 c.at_version = at_version
1051 c.at_version = at_version
1052 c.at_version_num = (at_version
1052 c.at_version_num = (at_version
1053 if at_version and at_version != PullRequest.LATEST_VER
1053 if at_version and at_version != PullRequest.LATEST_VER
1054 else None)
1054 else None)
1055
1055
1056 c.unresolved_comments = CommentsModel() \
1056 c.unresolved_comments = CommentsModel() \
1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1058 c.resolved_comments = CommentsModel() \
1058 c.resolved_comments = CommentsModel() \
1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1060
1060
1061 all_comments = c.unresolved_comments + c.resolved_comments
1061 all_comments = c.unresolved_comments + c.resolved_comments
1062 existing_ids = self.get_comment_ids(self.request.POST)
1062 existing_ids = self.get_comment_ids(self.request.POST)
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 todo_comments=True, existing_ids=existing_ids)
1064 todo_comments=True, existing_ids=existing_ids)
1065
1065
1066 @LoginRequired()
1066 @LoginRequired()
1067 @NotAnonymous()
1067 @NotAnonymous()
1068 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1069 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1070 def pullrequest_drafts(self):
1070 def pullrequest_drafts(self):
1071 self.load_default_context()
1071 self.load_default_context()
1072
1072
1073 pull_request = PullRequest.get_or_404(
1073 pull_request = PullRequest.get_or_404(
1074 self.request.matchdict['pull_request_id'])
1074 self.request.matchdict['pull_request_id'])
1075 pull_request_id = pull_request.pull_request_id
1075 pull_request_id = pull_request.pull_request_id
1076 version = self.request.GET.get('version')
1076 version = self.request.GET.get('version')
1077
1077
1078 _render = self.request.get_partial_renderer(
1078 _render = self.request.get_partial_renderer(
1079 'rhodecode:templates/base/sidebar.mako')
1079 'rhodecode:templates/base/sidebar.mako')
1080 c = _render.get_call_context()
1080 c = _render.get_call_context()
1081
1081
1082 (pull_request_latest,
1082 (pull_request_latest,
1083 pull_request_at_ver,
1083 pull_request_at_ver,
1084 pull_request_display_obj,
1084 pull_request_display_obj,
1085 at_version) = PullRequestModel().get_pr_version(
1085 at_version) = PullRequestModel().get_pr_version(
1086 pull_request_id, version=version)
1086 pull_request_id, version=version)
1087 versions = pull_request_display_obj.versions()
1087 versions = pull_request_display_obj.versions()
1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1089 c.versions = versions + [latest_ver]
1089 c.versions = versions + [latest_ver]
1090
1090
1091 c.at_version = at_version
1091 c.at_version = at_version
1092 c.at_version_num = (at_version
1092 c.at_version_num = (at_version
1093 if at_version and at_version != PullRequest.LATEST_VER
1093 if at_version and at_version != PullRequest.LATEST_VER
1094 else None)
1094 else None)
1095
1095
1096 c.draft_comments = CommentsModel() \
1096 c.draft_comments = CommentsModel() \
1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1098
1098
1099 all_comments = c.draft_comments
1099 all_comments = c.draft_comments
1100
1100
1101 existing_ids = self.get_comment_ids(self.request.POST)
1101 existing_ids = self.get_comment_ids(self.request.POST)
1102 return _render('comments_table', all_comments, len(all_comments),
1102 return _render('comments_table', all_comments, len(all_comments),
1103 existing_ids=existing_ids, draft_comments=True)
1103 existing_ids=existing_ids, draft_comments=True)
1104
1104
1105 @LoginRequired()
1105 @LoginRequired()
1106 @NotAnonymous()
1106 @NotAnonymous()
1107 @HasRepoPermissionAnyDecorator(
1107 @HasRepoPermissionAnyDecorator(
1108 'repository.read', 'repository.write', 'repository.admin')
1108 'repository.read', 'repository.write', 'repository.admin')
1109 @CSRFRequired()
1109 @CSRFRequired()
1110 def pull_request_create(self):
1110 def pull_request_create(self):
1111 _ = self.request.translate
1111 _ = self.request.translate
1112 self.assure_not_empty_repo()
1112 self.assure_not_empty_repo()
1113 self.load_default_context()
1113 self.load_default_context()
1114
1114
1115 controls = peppercorn.parse(self.request.POST.items())
1115 controls = peppercorn.parse(self.request.POST.items())
1116
1116
1117 try:
1117 try:
1118 form = PullRequestForm(
1118 form = PullRequestForm(
1119 self.request.translate, self.db_repo.repo_id)()
1119 self.request.translate, self.db_repo.repo_id)()
1120 _form = form.to_python(controls)
1120 _form = form.to_python(controls)
1121 except formencode.Invalid as errors:
1121 except formencode.Invalid as errors:
1122 if errors.error_dict.get('revisions'):
1122 if errors.error_dict.get('revisions'):
1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1124 elif errors.error_dict.get('pullrequest_title'):
1124 elif errors.error_dict.get('pullrequest_title'):
1125 msg = errors.error_dict.get('pullrequest_title')
1125 msg = errors.error_dict.get('pullrequest_title')
1126 else:
1126 else:
1127 msg = _('Error creating pull request: {}').format(errors)
1127 msg = _('Error creating pull request: {}').format(errors)
1128 log.exception(msg)
1128 log.exception(msg)
1129 h.flash(msg, 'error')
1129 h.flash(msg, 'error')
1130
1130
1131 # would rather just go back to form ...
1131 # would rather just go back to form ...
1132 raise HTTPFound(
1132 raise HTTPFound(
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1134
1134
1135 source_repo = _form['source_repo']
1135 source_repo = _form['source_repo']
1136 source_ref = _form['source_ref']
1136 source_ref = _form['source_ref']
1137 target_repo = _form['target_repo']
1137 target_repo = _form['target_repo']
1138 target_ref = _form['target_ref']
1138 target_ref = _form['target_ref']
1139 commit_ids = _form['revisions'][::-1]
1139 commit_ids = _form['revisions'][::-1]
1140 common_ancestor_id = _form['common_ancestor']
1140 common_ancestor_id = _form['common_ancestor']
1141
1141
1142 # find the ancestor for this pr
1142 # find the ancestor for this pr
1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1145
1145
1146 if not (source_db_repo or target_db_repo):
1146 if not (source_db_repo or target_db_repo):
1147 h.flash(_('source_repo or target repo not found'), category='error')
1147 h.flash(_('source_repo or target repo not found'), category='error')
1148 raise HTTPFound(
1148 raise HTTPFound(
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1150
1150
1151 # re-check permissions again here
1151 # re-check permissions again here
1152 # source_repo we must have read permissions
1152 # source_repo we must have read permissions
1153
1153
1154 source_perm = HasRepoPermissionAny(
1154 source_perm = HasRepoPermissionAny(
1155 'repository.read', 'repository.write', 'repository.admin')(
1155 'repository.read', 'repository.write', 'repository.admin')(
1156 source_db_repo.repo_name)
1156 source_db_repo.repo_name)
1157 if not source_perm:
1157 if not source_perm:
1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1159 source_db_repo.repo_name))
1159 source_db_repo.repo_name))
1160 h.flash(msg, category='error')
1160 h.flash(msg, category='error')
1161 # copy the args back to redirect
1161 # copy the args back to redirect
1162 org_query = self.request.GET.mixed()
1162 org_query = self.request.GET.mixed()
1163 raise HTTPFound(
1163 raise HTTPFound(
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1165 _query=org_query))
1165 _query=org_query))
1166
1166
1167 # target repo we must have read permissions, and also later on
1167 # target repo we must have read permissions, and also later on
1168 # we want to check branch permissions here
1168 # we want to check branch permissions here
1169 target_perm = HasRepoPermissionAny(
1169 target_perm = HasRepoPermissionAny(
1170 'repository.read', 'repository.write', 'repository.admin')(
1170 'repository.read', 'repository.write', 'repository.admin')(
1171 target_db_repo.repo_name)
1171 target_db_repo.repo_name)
1172 if not target_perm:
1172 if not target_perm:
1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1174 target_db_repo.repo_name))
1174 target_db_repo.repo_name))
1175 h.flash(msg, category='error')
1175 h.flash(msg, category='error')
1176 # copy the args back to redirect
1176 # copy the args back to redirect
1177 org_query = self.request.GET.mixed()
1177 org_query = self.request.GET.mixed()
1178 raise HTTPFound(
1178 raise HTTPFound(
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1180 _query=org_query))
1180 _query=org_query))
1181
1181
1182 source_scm = source_db_repo.scm_instance()
1182 source_scm = source_db_repo.scm_instance()
1183 target_scm = target_db_repo.scm_instance()
1183 target_scm = target_db_repo.scm_instance()
1184
1184
1185 source_ref_obj = unicode_to_reference(source_ref)
1185 source_ref_obj = unicode_to_reference(source_ref)
1186 target_ref_obj = unicode_to_reference(target_ref)
1186 target_ref_obj = unicode_to_reference(target_ref)
1187
1187
1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1190
1190
1191 ancestor = source_scm.get_common_ancestor(
1191 ancestor = source_scm.get_common_ancestor(
1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1193
1193
1194 # recalculate target ref based on ancestor
1194 # recalculate target ref based on ancestor
1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1196
1196
1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1198 PullRequestModel().get_reviewer_functions()
1198 PullRequestModel().get_reviewer_functions()
1199
1199
1200 # recalculate reviewers logic, to make sure we can validate this
1200 # recalculate reviewers logic, to make sure we can validate this
1201 reviewer_rules = get_default_reviewers_data(
1201 reviewer_rules = get_default_reviewers_data(
1202 self._rhodecode_db_user,
1202 self._rhodecode_db_user,
1203 source_db_repo,
1203 source_db_repo,
1204 source_ref_obj,
1204 source_ref_obj,
1205 target_db_repo,
1205 target_db_repo,
1206 target_ref_obj,
1206 target_ref_obj,
1207 include_diff_info=False)
1207 include_diff_info=False)
1208
1208
1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1211
1211
1212 pullrequest_title = _form['pullrequest_title']
1212 pullrequest_title = _form['pullrequest_title']
1213 title_source_ref = source_ref_obj.name
1213 title_source_ref = source_ref_obj.name
1214 if not pullrequest_title:
1214 if not pullrequest_title:
1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1216 source=source_repo,
1216 source=source_repo,
1217 source_ref=title_source_ref,
1217 source_ref=title_source_ref,
1218 target=target_repo
1218 target=target_repo
1219 )
1219 )
1220
1220
1221 description = _form['pullrequest_desc']
1221 description = _form['pullrequest_desc']
1222 description_renderer = _form['description_renderer']
1222 description_renderer = _form['description_renderer']
1223
1223
1224 try:
1224 try:
1225 pull_request = PullRequestModel().create(
1225 pull_request = PullRequestModel().create(
1226 created_by=self._rhodecode_user.user_id,
1226 created_by=self._rhodecode_user.user_id,
1227 source_repo=source_repo,
1227 source_repo=source_repo,
1228 source_ref=source_ref,
1228 source_ref=source_ref,
1229 target_repo=target_repo,
1229 target_repo=target_repo,
1230 target_ref=target_ref,
1230 target_ref=target_ref,
1231 revisions=commit_ids,
1231 revisions=commit_ids,
1232 common_ancestor_id=common_ancestor_id,
1232 common_ancestor_id=common_ancestor_id,
1233 reviewers=reviewers,
1233 reviewers=reviewers,
1234 observers=observers,
1234 observers=observers,
1235 title=pullrequest_title,
1235 title=pullrequest_title,
1236 description=description,
1236 description=description,
1237 description_renderer=description_renderer,
1237 description_renderer=description_renderer,
1238 reviewer_data=reviewer_rules,
1238 reviewer_data=reviewer_rules,
1239 auth_user=self._rhodecode_user
1239 auth_user=self._rhodecode_user
1240 )
1240 )
1241 Session().commit()
1241 Session().commit()
1242
1242
1243 h.flash(_('Successfully opened new pull request'),
1243 h.flash(_('Successfully opened new pull request'),
1244 category='success')
1244 category='success')
1245 except Exception:
1245 except Exception:
1246 msg = _('Error occurred during creation of this pull request.')
1246 msg = _('Error occurred during creation of this pull request.')
1247 log.exception(msg)
1247 log.exception(msg)
1248 h.flash(msg, category='error')
1248 h.flash(msg, category='error')
1249
1249
1250 # copy the args back to redirect
1250 # copy the args back to redirect
1251 org_query = self.request.GET.mixed()
1251 org_query = self.request.GET.mixed()
1252 raise HTTPFound(
1252 raise HTTPFound(
1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1254 _query=org_query))
1254 _query=org_query))
1255
1255
1256 raise HTTPFound(
1256 raise HTTPFound(
1257 h.route_path('pullrequest_show', repo_name=target_repo,
1257 h.route_path('pullrequest_show', repo_name=target_repo,
1258 pull_request_id=pull_request.pull_request_id))
1258 pull_request_id=pull_request.pull_request_id))
1259
1259
1260 @LoginRequired()
1260 @LoginRequired()
1261 @NotAnonymous()
1261 @NotAnonymous()
1262 @HasRepoPermissionAnyDecorator(
1262 @HasRepoPermissionAnyDecorator(
1263 'repository.read', 'repository.write', 'repository.admin')
1263 'repository.read', 'repository.write', 'repository.admin')
1264 @CSRFRequired()
1264 @CSRFRequired()
1265 def pull_request_update(self):
1265 def pull_request_update(self):
1266 pull_request = PullRequest.get_or_404(
1266 pull_request = PullRequest.get_or_404(
1267 self.request.matchdict['pull_request_id'])
1267 self.request.matchdict['pull_request_id'])
1268 _ = self.request.translate
1268 _ = self.request.translate
1269
1269
1270 c = self.load_default_context()
1270 c = self.load_default_context()
1271 redirect_url = None
1271 redirect_url = None
1272 # we do this check as first, because we want to know ASAP in the flow that
1272 # we do this check as first, because we want to know ASAP in the flow that
1273 # pr is updating currently
1273 # pr is updating currently
1274 is_state_changing = pull_request.is_state_changing()
1274 is_state_changing = pull_request.is_state_changing()
1275
1275
1276 if pull_request.is_closed():
1276 if pull_request.is_closed():
1277 log.debug('update: forbidden because pull request is closed')
1277 log.debug('update: forbidden because pull request is closed')
1278 msg = _(u'Cannot update closed pull requests.')
1278 msg = _(u'Cannot update closed pull requests.')
1279 h.flash(msg, category='error')
1279 h.flash(msg, category='error')
1280 return {'response': True,
1280 return {'response': True,
1281 'redirect_url': redirect_url}
1281 'redirect_url': redirect_url}
1282
1282
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1284
1284
1285 # only owner or admin can update it
1285 # only owner or admin can update it
1286 allowed_to_update = PullRequestModel().check_user_update(
1286 allowed_to_update = PullRequestModel().check_user_update(
1287 pull_request, self._rhodecode_user)
1287 pull_request, self._rhodecode_user)
1288
1288
1289 if allowed_to_update:
1289 if allowed_to_update:
1290 controls = peppercorn.parse(self.request.POST.items())
1290 controls = peppercorn.parse(self.request.POST.items())
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1293
1293
1294 if 'review_members' in controls:
1294 if 'review_members' in controls:
1295 self._update_reviewers(
1295 self._update_reviewers(
1296 c,
1296 c,
1297 pull_request, controls['review_members'],
1297 pull_request, controls['review_members'],
1298 pull_request.reviewer_data,
1298 pull_request.reviewer_data,
1299 PullRequestReviewers.ROLE_REVIEWER)
1299 PullRequestReviewers.ROLE_REVIEWER)
1300 elif 'observer_members' in controls:
1300 elif 'observer_members' in controls:
1301 self._update_reviewers(
1301 self._update_reviewers(
1302 c,
1302 c,
1303 pull_request, controls['observer_members'],
1303 pull_request, controls['observer_members'],
1304 pull_request.reviewer_data,
1304 pull_request.reviewer_data,
1305 PullRequestReviewers.ROLE_OBSERVER)
1305 PullRequestReviewers.ROLE_OBSERVER)
1306 elif do_update_commits:
1306 elif do_update_commits:
1307 if is_state_changing:
1307 if is_state_changing:
1308 log.debug('commits update: forbidden because pull request is in state %s',
1308 log.debug('commits update: forbidden because pull request is in state %s',
1309 pull_request.pull_request_state)
1309 pull_request.pull_request_state)
1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1311 u'Current state is: `{}`').format(
1311 u'Current state is: `{}`').format(
1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1313 h.flash(msg, category='error')
1313 h.flash(msg, category='error')
1314 return {'response': True,
1314 return {'response': True,
1315 'redirect_url': redirect_url}
1315 'redirect_url': redirect_url}
1316
1316
1317 self._update_commits(c, pull_request)
1317 self._update_commits(c, pull_request)
1318 if force_refresh:
1318 if force_refresh:
1319 redirect_url = h.route_path(
1319 redirect_url = h.route_path(
1320 'pullrequest_show', repo_name=self.db_repo_name,
1320 'pullrequest_show', repo_name=self.db_repo_name,
1321 pull_request_id=pull_request.pull_request_id,
1321 pull_request_id=pull_request.pull_request_id,
1322 _query={"force_refresh": 1})
1322 _query={"force_refresh": 1})
1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1324 self._edit_pull_request(pull_request)
1324 self._edit_pull_request(pull_request)
1325 else:
1325 else:
1326 log.error('Unhandled update data.')
1326 log.error('Unhandled update data.')
1327 raise HTTPBadRequest()
1327 raise HTTPBadRequest()
1328
1328
1329 return {'response': True,
1329 return {'response': True,
1330 'redirect_url': redirect_url}
1330 'redirect_url': redirect_url}
1331 raise HTTPForbidden()
1331 raise HTTPForbidden()
1332
1332
1333 def _edit_pull_request(self, pull_request):
1333 def _edit_pull_request(self, pull_request):
1334 """
1334 """
1335 Edit title and description
1335 Edit title and description
1336 """
1336 """
1337 _ = self.request.translate
1337 _ = self.request.translate
1338
1338
1339 try:
1339 try:
1340 PullRequestModel().edit(
1340 PullRequestModel().edit(
1341 pull_request,
1341 pull_request,
1342 self.request.POST.get('title'),
1342 self.request.POST.get('title'),
1343 self.request.POST.get('description'),
1343 self.request.POST.get('description'),
1344 self.request.POST.get('description_renderer'),
1344 self.request.POST.get('description_renderer'),
1345 self._rhodecode_user)
1345 self._rhodecode_user)
1346 except ValueError:
1346 except ValueError:
1347 msg = _(u'Cannot update closed pull requests.')
1347 msg = _(u'Cannot update closed pull requests.')
1348 h.flash(msg, category='error')
1348 h.flash(msg, category='error')
1349 return
1349 return
1350 else:
1350 else:
1351 Session().commit()
1351 Session().commit()
1352
1352
1353 msg = _(u'Pull request title & description updated.')
1353 msg = _(u'Pull request title & description updated.')
1354 h.flash(msg, category='success')
1354 h.flash(msg, category='success')
1355 return
1355 return
1356
1356
1357 def _update_commits(self, c, pull_request):
1357 def _update_commits(self, c, pull_request):
1358 _ = self.request.translate
1358 _ = self.request.translate
1359 log.debug('pull-request: running update commits actions')
1359 log.debug('pull-request: running update commits actions')
1360
1360
1361 @retry(exception=Exception, n_tries=3, delay=2)
1361 @retry(exception=Exception, n_tries=3, delay=2)
1362 def commits_update():
1362 def commits_update():
1363 return PullRequestModel().update_commits(
1363 return PullRequestModel().update_commits(
1364 pull_request, self._rhodecode_db_user)
1364 pull_request, self._rhodecode_db_user)
1365
1365
1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1367 resp = commits_update() # retry x3
1367 resp = commits_update() # retry x3
1368
1368
1369 if resp.executed:
1369 if resp.executed:
1370
1370
1371 if resp.target_changed and resp.source_changed:
1371 if resp.target_changed and resp.source_changed:
1372 changed = 'target and source repositories'
1372 changed = 'target and source repositories'
1373 elif resp.target_changed and not resp.source_changed:
1373 elif resp.target_changed and not resp.source_changed:
1374 changed = 'target repository'
1374 changed = 'target repository'
1375 elif not resp.target_changed and resp.source_changed:
1375 elif not resp.target_changed and resp.source_changed:
1376 changed = 'source repository'
1376 changed = 'source repository'
1377 else:
1377 else:
1378 changed = 'nothing'
1378 changed = 'nothing'
1379
1379
1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1381 u'{count_added} added, {count_removed} removed commits. '
1381 u'{count_added} added, {count_removed} removed commits. '
1382 u'Source of changes: {change_source}.')
1382 u'Source of changes: {change_source}.')
1383 msg = msg.format(
1383 msg = msg.format(
1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1385 count_added=len(resp.changes.added),
1385 count_added=len(resp.changes.added),
1386 count_removed=len(resp.changes.removed),
1386 count_removed=len(resp.changes.removed),
1387 change_source=changed)
1387 change_source=changed)
1388 h.flash(msg, category='success')
1388 h.flash(msg, category='success')
1389 channelstream.pr_update_channelstream_push(
1389 channelstream.pr_update_channelstream_push(
1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 else:
1391 else:
1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1393 warning_reasons = [
1393 warning_reasons = [
1394 UpdateFailureReason.NO_CHANGE,
1394 UpdateFailureReason.NO_CHANGE,
1395 UpdateFailureReason.WRONG_REF_TYPE,
1395 UpdateFailureReason.WRONG_REF_TYPE,
1396 ]
1396 ]
1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1398 h.flash(msg, category=category)
1398 h.flash(msg, category=category)
1399
1399
1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1401 _ = self.request.translate
1401 _ = self.request.translate
1402
1402
1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1404 PullRequestModel().get_reviewer_functions()
1404 PullRequestModel().get_reviewer_functions()
1405
1405
1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1407 try:
1407 try:
1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1409 except ValueError as e:
1409 except ValueError as e:
1410 log.error('Reviewers Validation: {}'.format(e))
1410 log.error('Reviewers Validation: {}'.format(e))
1411 h.flash(e, category='error')
1411 h.flash(e, category='error')
1412 return
1412 return
1413
1413
1414 old_calculated_status = pull_request.calculated_review_status()
1414 old_calculated_status = pull_request.calculated_review_status()
1415 PullRequestModel().update_reviewers(
1415 PullRequestModel().update_reviewers(
1416 pull_request, reviewers, self._rhodecode_db_user)
1416 pull_request, reviewers, self._rhodecode_db_user)
1417
1417
1418 Session().commit()
1418 Session().commit()
1419
1419
1420 msg = _('Pull request reviewers updated.')
1420 msg = _('Pull request reviewers updated.')
1421 h.flash(msg, category='success')
1421 h.flash(msg, category='success')
1422 channelstream.pr_update_channelstream_push(
1422 channelstream.pr_update_channelstream_push(
1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1424
1424
1425 # trigger status changed if change in reviewers changes the status
1425 # trigger status changed if change in reviewers changes the status
1426 calculated_status = pull_request.calculated_review_status()
1426 calculated_status = pull_request.calculated_review_status()
1427 if old_calculated_status != calculated_status:
1427 if old_calculated_status != calculated_status:
1428 PullRequestModel().trigger_pull_request_hook(
1428 PullRequestModel().trigger_pull_request_hook(
1429 pull_request, self._rhodecode_user, 'review_status_change',
1429 pull_request, self._rhodecode_user, 'review_status_change',
1430 data={'status': calculated_status})
1430 data={'status': calculated_status})
1431
1431
1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1433 try:
1433 try:
1434 observers = validate_observers(review_members, reviewer_rules)
1434 observers = validate_observers(review_members, reviewer_rules)
1435 except ValueError as e:
1435 except ValueError as e:
1436 log.error('Observers Validation: {}'.format(e))
1436 log.error('Observers Validation: {}'.format(e))
1437 h.flash(e, category='error')
1437 h.flash(e, category='error')
1438 return
1438 return
1439
1439
1440 PullRequestModel().update_observers(
1440 PullRequestModel().update_observers(
1441 pull_request, observers, self._rhodecode_db_user)
1441 pull_request, observers, self._rhodecode_db_user)
1442
1442
1443 Session().commit()
1443 Session().commit()
1444 msg = _('Pull request observers updated.')
1444 msg = _('Pull request observers updated.')
1445 h.flash(msg, category='success')
1445 h.flash(msg, category='success')
1446 channelstream.pr_update_channelstream_push(
1446 channelstream.pr_update_channelstream_push(
1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1448
1448
1449 @LoginRequired()
1449 @LoginRequired()
1450 @NotAnonymous()
1450 @NotAnonymous()
1451 @HasRepoPermissionAnyDecorator(
1451 @HasRepoPermissionAnyDecorator(
1452 'repository.read', 'repository.write', 'repository.admin')
1452 'repository.read', 'repository.write', 'repository.admin')
1453 @CSRFRequired()
1453 @CSRFRequired()
1454 def pull_request_merge(self):
1454 def pull_request_merge(self):
1455 """
1455 """
1456 Merge will perform a server-side merge of the specified
1456 Merge will perform a server-side merge of the specified
1457 pull request, if the pull request is approved and mergeable.
1457 pull request, if the pull request is approved and mergeable.
1458 After successful merging, the pull request is automatically
1458 After successful merging, the pull request is automatically
1459 closed, with a relevant comment.
1459 closed, with a relevant comment.
1460 """
1460 """
1461 pull_request = PullRequest.get_or_404(
1461 pull_request = PullRequest.get_or_404(
1462 self.request.matchdict['pull_request_id'])
1462 self.request.matchdict['pull_request_id'])
1463 _ = self.request.translate
1463 _ = self.request.translate
1464
1464
1465 if pull_request.is_state_changing():
1465 if pull_request.is_state_changing():
1466 log.debug('show: forbidden because pull request is in state %s',
1466 log.debug('show: forbidden because pull request is in state %s',
1467 pull_request.pull_request_state)
1467 pull_request.pull_request_state)
1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1470 pull_request.pull_request_state)
1470 pull_request.pull_request_state)
1471 h.flash(msg, category='error')
1471 h.flash(msg, category='error')
1472 raise HTTPFound(
1472 raise HTTPFound(
1473 h.route_path('pullrequest_show',
1473 h.route_path('pullrequest_show',
1474 repo_name=pull_request.target_repo.repo_name,
1474 repo_name=pull_request.target_repo.repo_name,
1475 pull_request_id=pull_request.pull_request_id))
1475 pull_request_id=pull_request.pull_request_id))
1476
1476
1477 self.load_default_context()
1477 self.load_default_context()
1478
1478
1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1480 check = MergeCheck.validate(
1480 check = MergeCheck.validate(
1481 pull_request, auth_user=self._rhodecode_user,
1481 pull_request, auth_user=self._rhodecode_user,
1482 translator=self.request.translate)
1482 translator=self.request.translate)
1483 merge_possible = not check.failed
1483 merge_possible = not check.failed
1484
1484
1485 for err_type, error_msg in check.errors:
1485 for err_type, error_msg in check.errors:
1486 h.flash(error_msg, category=err_type)
1486 h.flash(error_msg, category=err_type)
1487
1487
1488 if merge_possible:
1488 if merge_possible:
1489 log.debug("Pre-conditions checked, trying to merge.")
1489 log.debug("Pre-conditions checked, trying to merge.")
1490 extras = vcs_operation_context(
1490 extras = vcs_operation_context(
1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1492 username=self._rhodecode_db_user.username, action='push',
1492 username=self._rhodecode_db_user.username, action='push',
1493 scm=pull_request.target_repo.repo_type)
1493 scm=pull_request.target_repo.repo_type)
1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1495 self._merge_pull_request(
1495 self._merge_pull_request(
1496 pull_request, self._rhodecode_db_user, extras)
1496 pull_request, self._rhodecode_db_user, extras)
1497 else:
1497 else:
1498 log.debug("Pre-conditions failed, NOT merging.")
1498 log.debug("Pre-conditions failed, NOT merging.")
1499
1499
1500 raise HTTPFound(
1500 raise HTTPFound(
1501 h.route_path('pullrequest_show',
1501 h.route_path('pullrequest_show',
1502 repo_name=pull_request.target_repo.repo_name,
1502 repo_name=pull_request.target_repo.repo_name,
1503 pull_request_id=pull_request.pull_request_id))
1503 pull_request_id=pull_request.pull_request_id))
1504
1504
1505 def _merge_pull_request(self, pull_request, user, extras):
1505 def _merge_pull_request(self, pull_request, user, extras):
1506 _ = self.request.translate
1506 _ = self.request.translate
1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1508
1508
1509 if merge_resp.executed:
1509 if merge_resp.executed:
1510 log.debug("The merge was successful, closing the pull request.")
1510 log.debug("The merge was successful, closing the pull request.")
1511 PullRequestModel().close_pull_request(
1511 PullRequestModel().close_pull_request(
1512 pull_request.pull_request_id, user)
1512 pull_request.pull_request_id, user)
1513 Session().commit()
1513 Session().commit()
1514 msg = _('Pull request was successfully merged and closed.')
1514 msg = _('Pull request was successfully merged and closed.')
1515 h.flash(msg, category='success')
1515 h.flash(msg, category='success')
1516 else:
1516 else:
1517 log.debug(
1517 log.debug(
1518 "The merge was not successful. Merge response: %s", merge_resp)
1518 "The merge was not successful. Merge response: %s", merge_resp)
1519 msg = merge_resp.merge_status_message
1519 msg = merge_resp.merge_status_message
1520 h.flash(msg, category='error')
1520 h.flash(msg, category='error')
1521
1521
1522 @LoginRequired()
1522 @LoginRequired()
1523 @NotAnonymous()
1523 @NotAnonymous()
1524 @HasRepoPermissionAnyDecorator(
1524 @HasRepoPermissionAnyDecorator(
1525 'repository.read', 'repository.write', 'repository.admin')
1525 'repository.read', 'repository.write', 'repository.admin')
1526 @CSRFRequired()
1526 @CSRFRequired()
1527 def pull_request_delete(self):
1527 def pull_request_delete(self):
1528 _ = self.request.translate
1528 _ = self.request.translate
1529
1529
1530 pull_request = PullRequest.get_or_404(
1530 pull_request = PullRequest.get_or_404(
1531 self.request.matchdict['pull_request_id'])
1531 self.request.matchdict['pull_request_id'])
1532 self.load_default_context()
1532 self.load_default_context()
1533
1533
1534 pr_closed = pull_request.is_closed()
1534 pr_closed = pull_request.is_closed()
1535 allowed_to_delete = PullRequestModel().check_user_delete(
1535 allowed_to_delete = PullRequestModel().check_user_delete(
1536 pull_request, self._rhodecode_user) and not pr_closed
1536 pull_request, self._rhodecode_user) and not pr_closed
1537
1537
1538 # only owner can delete it !
1538 # only owner can delete it !
1539 if allowed_to_delete:
1539 if allowed_to_delete:
1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1541 Session().commit()
1541 Session().commit()
1542 h.flash(_('Successfully deleted pull request'),
1542 h.flash(_('Successfully deleted pull request'),
1543 category='success')
1543 category='success')
1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1545 repo_name=self.db_repo_name))
1545 repo_name=self.db_repo_name))
1546
1546
1547 log.warning('user %s tried to delete pull request without access',
1547 log.warning('user %s tried to delete pull request without access',
1548 self._rhodecode_user)
1548 self._rhodecode_user)
1549 raise HTTPNotFound()
1549 raise HTTPNotFound()
1550
1550
1551 def _pull_request_comments_create(self, pull_request, comments):
1551 def _pull_request_comments_create(self, pull_request, comments):
1552 _ = self.request.translate
1552 _ = self.request.translate
1553 data = {}
1553 data = {}
1554 if not comments:
1554 if not comments:
1555 return
1555 return
1556 pull_request_id = pull_request.pull_request_id
1556 pull_request_id = pull_request.pull_request_id
1557
1557
1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1559
1559
1560 for entry in comments:
1560 for entry in comments:
1561 c = self.load_default_context()
1561 c = self.load_default_context()
1562 comment_type = entry['comment_type']
1562 comment_type = entry['comment_type']
1563 text = entry['text']
1563 text = entry['text']
1564 status = entry['status']
1564 status = entry['status']
1565 is_draft = str2bool(entry['is_draft'])
1565 is_draft = str2bool(entry['is_draft'])
1566 resolves_comment_id = entry['resolves_comment_id']
1566 resolves_comment_id = entry['resolves_comment_id']
1567 close_pull_request = entry['close_pull_request']
1567 close_pull_request = entry['close_pull_request']
1568 f_path = entry['f_path']
1568 f_path = entry['f_path']
1569 line_no = entry['line']
1569 line_no = entry['line']
1570 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1570 target_elem_id = 'file-{}'.format(h.safeid(h.safe_str(f_path)))
1571
1571
1572 # the logic here should work like following, if we submit close
1572 # the logic here should work like following, if we submit close
1573 # pr comment, use `close_pull_request_with_comment` function
1573 # pr comment, use `close_pull_request_with_comment` function
1574 # else handle regular comment logic
1574 # else handle regular comment logic
1575
1575
1576 if close_pull_request:
1576 if close_pull_request:
1577 # only owner or admin or person with write permissions
1577 # only owner or admin or person with write permissions
1578 allowed_to_close = PullRequestModel().check_user_update(
1578 allowed_to_close = PullRequestModel().check_user_update(
1579 pull_request, self._rhodecode_user)
1579 pull_request, self._rhodecode_user)
1580 if not allowed_to_close:
1580 if not allowed_to_close:
1581 log.debug('comment: forbidden because not allowed to close '
1581 log.debug('comment: forbidden because not allowed to close '
1582 'pull request %s', pull_request_id)
1582 'pull request %s', pull_request_id)
1583 raise HTTPForbidden()
1583 raise HTTPForbidden()
1584
1584
1585 # This also triggers `review_status_change`
1585 # This also triggers `review_status_change`
1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1588 auth_user=self._rhodecode_user)
1588 auth_user=self._rhodecode_user)
1589 Session().flush()
1589 Session().flush()
1590 is_inline = comment.is_inline
1590 is_inline = comment.is_inline
1591
1591
1592 PullRequestModel().trigger_pull_request_hook(
1592 PullRequestModel().trigger_pull_request_hook(
1593 pull_request, self._rhodecode_user, 'comment',
1593 pull_request, self._rhodecode_user, 'comment',
1594 data={'comment': comment})
1594 data={'comment': comment})
1595
1595
1596 else:
1596 else:
1597 # regular comment case, could be inline, or one with status.
1597 # regular comment case, could be inline, or one with status.
1598 # for that one we check also permissions
1598 # for that one we check also permissions
1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1601 pull_request, self._rhodecode_user) and not is_draft
1601 pull_request, self._rhodecode_user) and not is_draft
1602
1602
1603 if status and allowed_to_change_status:
1603 if status and allowed_to_change_status:
1604 message = (_('Status change %(transition_icon)s %(status)s')
1604 message = (_('Status change %(transition_icon)s %(status)s')
1605 % {'transition_icon': '>',
1605 % {'transition_icon': '>',
1606 'status': ChangesetStatus.get_status_lbl(status)})
1606 'status': ChangesetStatus.get_status_lbl(status)})
1607 text = text or message
1607 text = text or message
1608
1608
1609 comment = CommentsModel().create(
1609 comment = CommentsModel().create(
1610 text=text,
1610 text=text,
1611 repo=self.db_repo.repo_id,
1611 repo=self.db_repo.repo_id,
1612 user=self._rhodecode_user.user_id,
1612 user=self._rhodecode_user.user_id,
1613 pull_request=pull_request,
1613 pull_request=pull_request,
1614 f_path=f_path,
1614 f_path=f_path,
1615 line_no=line_no,
1615 line_no=line_no,
1616 status_change=(ChangesetStatus.get_status_lbl(status)
1616 status_change=(ChangesetStatus.get_status_lbl(status)
1617 if status and allowed_to_change_status else None),
1617 if status and allowed_to_change_status else None),
1618 status_change_type=(status
1618 status_change_type=(status
1619 if status and allowed_to_change_status else None),
1619 if status and allowed_to_change_status else None),
1620 comment_type=comment_type,
1620 comment_type=comment_type,
1621 is_draft=is_draft,
1621 is_draft=is_draft,
1622 resolves_comment_id=resolves_comment_id,
1622 resolves_comment_id=resolves_comment_id,
1623 auth_user=self._rhodecode_user,
1623 auth_user=self._rhodecode_user,
1624 send_email=not is_draft, # skip notification for draft comments
1624 send_email=not is_draft, # skip notification for draft comments
1625 )
1625 )
1626 is_inline = comment.is_inline
1626 is_inline = comment.is_inline
1627
1627
1628 if allowed_to_change_status:
1628 if allowed_to_change_status:
1629 # calculate old status before we change it
1629 # calculate old status before we change it
1630 old_calculated_status = pull_request.calculated_review_status()
1630 old_calculated_status = pull_request.calculated_review_status()
1631
1631
1632 # get status if set !
1632 # get status if set !
1633 if status:
1633 if status:
1634 ChangesetStatusModel().set_status(
1634 ChangesetStatusModel().set_status(
1635 self.db_repo.repo_id,
1635 self.db_repo.repo_id,
1636 status,
1636 status,
1637 self._rhodecode_user.user_id,
1637 self._rhodecode_user.user_id,
1638 comment,
1638 comment,
1639 pull_request=pull_request
1639 pull_request=pull_request
1640 )
1640 )
1641
1641
1642 Session().flush()
1642 Session().flush()
1643 # this is somehow required to get access to some relationship
1643 # this is somehow required to get access to some relationship
1644 # loaded on comment
1644 # loaded on comment
1645 Session().refresh(comment)
1645 Session().refresh(comment)
1646
1646
1647 # skip notifications for drafts
1647 # skip notifications for drafts
1648 if not is_draft:
1648 if not is_draft:
1649 PullRequestModel().trigger_pull_request_hook(
1649 PullRequestModel().trigger_pull_request_hook(
1650 pull_request, self._rhodecode_user, 'comment',
1650 pull_request, self._rhodecode_user, 'comment',
1651 data={'comment': comment})
1651 data={'comment': comment})
1652
1652
1653 # we now calculate the status of pull request, and based on that
1653 # we now calculate the status of pull request, and based on that
1654 # calculation we set the commits status
1654 # calculation we set the commits status
1655 calculated_status = pull_request.calculated_review_status()
1655 calculated_status = pull_request.calculated_review_status()
1656 if old_calculated_status != calculated_status:
1656 if old_calculated_status != calculated_status:
1657 PullRequestModel().trigger_pull_request_hook(
1657 PullRequestModel().trigger_pull_request_hook(
1658 pull_request, self._rhodecode_user, 'review_status_change',
1658 pull_request, self._rhodecode_user, 'review_status_change',
1659 data={'status': calculated_status})
1659 data={'status': calculated_status})
1660
1660
1661 comment_id = comment.comment_id
1661 comment_id = comment.comment_id
1662 data[comment_id] = {
1662 data[comment_id] = {
1663 'target_id': target_elem_id
1663 'target_id': target_elem_id
1664 }
1664 }
1665 Session().flush()
1665 Session().flush()
1666
1666
1667 c.co = comment
1667 c.co = comment
1668 c.at_version_num = None
1668 c.at_version_num = None
1669 c.is_new = True
1669 c.is_new = True
1670 rendered_comment = render(
1670 rendered_comment = render(
1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1672 self._get_template_context(c), self.request)
1672 self._get_template_context(c), self.request)
1673
1673
1674 data[comment_id].update(comment.get_dict())
1674 data[comment_id].update(comment.get_dict())
1675 data[comment_id].update({'rendered_text': rendered_comment})
1675 data[comment_id].update({'rendered_text': rendered_comment})
1676
1676
1677 Session().commit()
1677 Session().commit()
1678
1678
1679 # skip channelstream for draft comments
1679 # skip channelstream for draft comments
1680 if not all_drafts:
1680 if not all_drafts:
1681 comment_broadcast_channel = channelstream.comment_channel(
1681 comment_broadcast_channel = channelstream.comment_channel(
1682 self.db_repo_name, pull_request_obj=pull_request)
1682 self.db_repo_name, pull_request_obj=pull_request)
1683
1683
1684 comment_data = data
1684 comment_data = data
1685 posted_comment_type = 'inline' if is_inline else 'general'
1685 posted_comment_type = 'inline' if is_inline else 'general'
1686 if len(data) == 1:
1686 if len(data) == 1:
1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1688 else:
1688 else:
1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1690
1690
1691 channelstream.comment_channelstream_push(
1691 channelstream.comment_channelstream_push(
1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1693 comment_data=comment_data)
1693 comment_data=comment_data)
1694
1694
1695 return data
1695 return data
1696
1696
1697 @LoginRequired()
1697 @LoginRequired()
1698 @NotAnonymous()
1698 @NotAnonymous()
1699 @HasRepoPermissionAnyDecorator(
1699 @HasRepoPermissionAnyDecorator(
1700 'repository.read', 'repository.write', 'repository.admin')
1700 'repository.read', 'repository.write', 'repository.admin')
1701 @CSRFRequired()
1701 @CSRFRequired()
1702 def pull_request_comment_create(self):
1702 def pull_request_comment_create(self):
1703 _ = self.request.translate
1703 _ = self.request.translate
1704
1704
1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1706
1706
1707 if pull_request.is_closed():
1707 if pull_request.is_closed():
1708 log.debug('comment: forbidden because pull request is closed')
1708 log.debug('comment: forbidden because pull request is closed')
1709 raise HTTPForbidden()
1709 raise HTTPForbidden()
1710
1710
1711 allowed_to_comment = PullRequestModel().check_user_comment(
1711 allowed_to_comment = PullRequestModel().check_user_comment(
1712 pull_request, self._rhodecode_user)
1712 pull_request, self._rhodecode_user)
1713 if not allowed_to_comment:
1713 if not allowed_to_comment:
1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1715 raise HTTPForbidden()
1715 raise HTTPForbidden()
1716
1716
1717 comment_data = {
1717 comment_data = {
1718 'comment_type': self.request.POST.get('comment_type'),
1718 'comment_type': self.request.POST.get('comment_type'),
1719 'text': self.request.POST.get('text'),
1719 'text': self.request.POST.get('text'),
1720 'status': self.request.POST.get('changeset_status', None),
1720 'status': self.request.POST.get('changeset_status', None),
1721 'is_draft': self.request.POST.get('draft'),
1721 'is_draft': self.request.POST.get('draft'),
1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1724 'f_path': self.request.POST.get('f_path'),
1724 'f_path': self.request.POST.get('f_path'),
1725 'line': self.request.POST.get('line'),
1725 'line': self.request.POST.get('line'),
1726 }
1726 }
1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1728
1728
1729 return data
1729 return data
1730
1730
1731 @LoginRequired()
1731 @LoginRequired()
1732 @NotAnonymous()
1732 @NotAnonymous()
1733 @HasRepoPermissionAnyDecorator(
1733 @HasRepoPermissionAnyDecorator(
1734 'repository.read', 'repository.write', 'repository.admin')
1734 'repository.read', 'repository.write', 'repository.admin')
1735 @CSRFRequired()
1735 @CSRFRequired()
1736 def pull_request_comment_delete(self):
1736 def pull_request_comment_delete(self):
1737 pull_request = PullRequest.get_or_404(
1737 pull_request = PullRequest.get_or_404(
1738 self.request.matchdict['pull_request_id'])
1738 self.request.matchdict['pull_request_id'])
1739
1739
1740 comment = ChangesetComment.get_or_404(
1740 comment = ChangesetComment.get_or_404(
1741 self.request.matchdict['comment_id'])
1741 self.request.matchdict['comment_id'])
1742 comment_id = comment.comment_id
1742 comment_id = comment.comment_id
1743
1743
1744 if comment.immutable:
1744 if comment.immutable:
1745 # don't allow deleting comments that are immutable
1745 # don't allow deleting comments that are immutable
1746 raise HTTPForbidden()
1746 raise HTTPForbidden()
1747
1747
1748 if pull_request.is_closed():
1748 if pull_request.is_closed():
1749 log.debug('comment: forbidden because pull request is closed')
1749 log.debug('comment: forbidden because pull request is closed')
1750 raise HTTPForbidden()
1750 raise HTTPForbidden()
1751
1751
1752 if not comment:
1752 if not comment:
1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 # comment already deleted in another call probably
1754 # comment already deleted in another call probably
1755 return True
1755 return True
1756
1756
1757 if comment.pull_request.is_closed():
1757 if comment.pull_request.is_closed():
1758 # don't allow deleting comments on closed pull request
1758 # don't allow deleting comments on closed pull request
1759 raise HTTPForbidden()
1759 raise HTTPForbidden()
1760
1760
1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 super_admin = h.HasPermissionAny('hg.admin')()
1762 super_admin = h.HasPermissionAny('hg.admin')()
1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 comment_repo_admin = is_repo_admin and is_repo_comment
1765 comment_repo_admin = is_repo_admin and is_repo_comment
1766
1766
1767 if comment.draft and not comment_owner:
1767 if comment.draft and not comment_owner:
1768 # We never allow to delete draft comments for other than owners
1768 # We never allow to delete draft comments for other than owners
1769 raise HTTPNotFound()
1769 raise HTTPNotFound()
1770
1770
1771 if super_admin or comment_owner or comment_repo_admin:
1771 if super_admin or comment_owner or comment_repo_admin:
1772 old_calculated_status = comment.pull_request.calculated_review_status()
1772 old_calculated_status = comment.pull_request.calculated_review_status()
1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 Session().commit()
1774 Session().commit()
1775 calculated_status = comment.pull_request.calculated_review_status()
1775 calculated_status = comment.pull_request.calculated_review_status()
1776 if old_calculated_status != calculated_status:
1776 if old_calculated_status != calculated_status:
1777 PullRequestModel().trigger_pull_request_hook(
1777 PullRequestModel().trigger_pull_request_hook(
1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 data={'status': calculated_status})
1779 data={'status': calculated_status})
1780 return True
1780 return True
1781 else:
1781 else:
1782 log.warning('No permissions for user %s to delete comment_id: %s',
1782 log.warning('No permissions for user %s to delete comment_id: %s',
1783 self._rhodecode_db_user, comment_id)
1783 self._rhodecode_db_user, comment_id)
1784 raise HTTPNotFound()
1784 raise HTTPNotFound()
1785
1785
1786 @LoginRequired()
1786 @LoginRequired()
1787 @NotAnonymous()
1787 @NotAnonymous()
1788 @HasRepoPermissionAnyDecorator(
1788 @HasRepoPermissionAnyDecorator(
1789 'repository.read', 'repository.write', 'repository.admin')
1789 'repository.read', 'repository.write', 'repository.admin')
1790 @CSRFRequired()
1790 @CSRFRequired()
1791 def pull_request_comment_edit(self):
1791 def pull_request_comment_edit(self):
1792 self.load_default_context()
1792 self.load_default_context()
1793
1793
1794 pull_request = PullRequest.get_or_404(
1794 pull_request = PullRequest.get_or_404(
1795 self.request.matchdict['pull_request_id']
1795 self.request.matchdict['pull_request_id']
1796 )
1796 )
1797 comment = ChangesetComment.get_or_404(
1797 comment = ChangesetComment.get_or_404(
1798 self.request.matchdict['comment_id']
1798 self.request.matchdict['comment_id']
1799 )
1799 )
1800 comment_id = comment.comment_id
1800 comment_id = comment.comment_id
1801
1801
1802 if comment.immutable:
1802 if comment.immutable:
1803 # don't allow deleting comments that are immutable
1803 # don't allow deleting comments that are immutable
1804 raise HTTPForbidden()
1804 raise HTTPForbidden()
1805
1805
1806 if pull_request.is_closed():
1806 if pull_request.is_closed():
1807 log.debug('comment: forbidden because pull request is closed')
1807 log.debug('comment: forbidden because pull request is closed')
1808 raise HTTPForbidden()
1808 raise HTTPForbidden()
1809
1809
1810 if comment.pull_request.is_closed():
1810 if comment.pull_request.is_closed():
1811 # don't allow deleting comments on closed pull request
1811 # don't allow deleting comments on closed pull request
1812 raise HTTPForbidden()
1812 raise HTTPForbidden()
1813
1813
1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 super_admin = h.HasPermissionAny('hg.admin')()
1815 super_admin = h.HasPermissionAny('hg.admin')()
1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 comment_repo_admin = is_repo_admin and is_repo_comment
1818 comment_repo_admin = is_repo_admin and is_repo_comment
1819
1819
1820 if super_admin or comment_owner or comment_repo_admin:
1820 if super_admin or comment_owner or comment_repo_admin:
1821 text = self.request.POST.get('text')
1821 text = self.request.POST.get('text')
1822 version = self.request.POST.get('version')
1822 version = self.request.POST.get('version')
1823 if text == comment.text:
1823 if text == comment.text:
1824 log.warning(
1824 log.warning(
1825 'Comment(PR): '
1825 'Comment(PR): '
1826 'Trying to create new version '
1826 'Trying to create new version '
1827 'with the same comment body {}'.format(
1827 'with the same comment body {}'.format(
1828 comment_id,
1828 comment_id,
1829 )
1829 )
1830 )
1830 )
1831 raise HTTPNotFound()
1831 raise HTTPNotFound()
1832
1832
1833 if version.isdigit():
1833 if version.isdigit():
1834 version = int(version)
1834 version = int(version)
1835 else:
1835 else:
1836 log.warning(
1836 log.warning(
1837 'Comment(PR): Wrong version type {} {} '
1837 'Comment(PR): Wrong version type {} {} '
1838 'for comment {}'.format(
1838 'for comment {}'.format(
1839 version,
1839 version,
1840 type(version),
1840 type(version),
1841 comment_id,
1841 comment_id,
1842 )
1842 )
1843 )
1843 )
1844 raise HTTPNotFound()
1844 raise HTTPNotFound()
1845
1845
1846 try:
1846 try:
1847 comment_history = CommentsModel().edit(
1847 comment_history = CommentsModel().edit(
1848 comment_id=comment_id,
1848 comment_id=comment_id,
1849 text=text,
1849 text=text,
1850 auth_user=self._rhodecode_user,
1850 auth_user=self._rhodecode_user,
1851 version=version,
1851 version=version,
1852 )
1852 )
1853 except CommentVersionMismatch:
1853 except CommentVersionMismatch:
1854 raise HTTPConflict()
1854 raise HTTPConflict()
1855
1855
1856 if not comment_history:
1856 if not comment_history:
1857 raise HTTPNotFound()
1857 raise HTTPNotFound()
1858
1858
1859 Session().commit()
1859 Session().commit()
1860 if not comment.draft:
1860 if not comment.draft:
1861 PullRequestModel().trigger_pull_request_hook(
1861 PullRequestModel().trigger_pull_request_hook(
1862 pull_request, self._rhodecode_user, 'comment_edit',
1862 pull_request, self._rhodecode_user, 'comment_edit',
1863 data={'comment': comment})
1863 data={'comment': comment})
1864
1864
1865 return {
1865 return {
1866 'comment_history_id': comment_history.comment_history_id,
1866 'comment_history_id': comment_history.comment_history_id,
1867 'comment_id': comment.comment_id,
1867 'comment_id': comment.comment_id,
1868 'comment_version': comment_history.version,
1868 'comment_version': comment_history.version,
1869 'comment_author_username': comment_history.author.username,
1869 'comment_author_username': comment_history.author.username,
1870 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1870 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1871 'comment_created_on': h.age_component(comment_history.created_on,
1871 'comment_created_on': h.age_component(comment_history.created_on,
1872 time_is_local=True),
1872 time_is_local=True),
1873 }
1873 }
1874 else:
1874 else:
1875 log.warning('No permissions for user %s to edit comment_id: %s',
1875 log.warning('No permissions for user %s to edit comment_id: %s',
1876 self._rhodecode_db_user, comment_id)
1876 self._rhodecode_db_user, comment_id)
1877 raise HTTPNotFound()
1877 raise HTTPNotFound()
@@ -1,251 +1,251 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 logging
21 import logging
22
22
23 import deform
23 import deform
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode import events
26 from rhodecode import events
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.forms import RcForm
28 from rhodecode.forms import RcForm
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.permission import PermissionModel
36 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.scm import RepoGroupList, ScmModel
37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 from rhodecode.model.validation_schema.schemas import repo_schema
38 from rhodecode.model.validation_schema.schemas import repo_schema
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class RepoSettingsView(RepoAppView):
43 class RepoSettingsView(RepoAppView):
44
44
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
47
47
48 acl_groups = RepoGroupList(
48 acl_groups = RepoGroupList(
49 RepoGroup.query().all(),
49 RepoGroup.query().all(),
50 perm_set=['group.write', 'group.admin'])
50 perm_set=['group.write', 'group.admin'])
51 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
51 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
52 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
52 c.repo_groups_choices = list(map(lambda k: k[0], c.repo_groups))
53
53
54 # in case someone no longer have a group.write access to a repository
54 # in case someone no longer have a group.write access to a repository
55 # pre fill the list with this entry, we don't care if this is the same
55 # pre fill the list with this entry, we don't care if this is the same
56 # but it will allow saving repo data properly.
56 # but it will allow saving repo data properly.
57 repo_group = self.db_repo.group
57 repo_group = self.db_repo.group
58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 c.repo_groups_choices.append(repo_group.group_id)
59 c.repo_groups_choices.append(repo_group.group_id)
60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61
61
62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 # we might be in missing requirement state, so we load things
63 # we might be in missing requirement state, so we load things
64 # without touching scm_instance()
64 # without touching scm_instance()
65 c.landing_revs_choices, c.landing_revs = \
65 c.landing_revs_choices, c.landing_revs = \
66 ScmModel().get_repo_landing_revs(self.request.translate)
66 ScmModel().get_repo_landing_revs(self.request.translate)
67 else:
67 else:
68 c.landing_revs_choices, c.landing_revs = \
68 c.landing_revs_choices, c.landing_revs = \
69 ScmModel().get_repo_landing_revs(
69 ScmModel().get_repo_landing_revs(
70 self.request.translate, self.db_repo)
70 self.request.translate, self.db_repo)
71
71
72 c.personal_repo_group = c.auth_user.personal_repo_group
72 c.personal_repo_group = c.auth_user.personal_repo_group
73 c.repo_fields = RepositoryField.query()\
73 c.repo_fields = RepositoryField.query()\
74 .filter(RepositoryField.repository == self.db_repo).all()
74 .filter(RepositoryField.repository == self.db_repo).all()
75 return c
75 return c
76
76
77 def _get_schema(self, c, old_values=None):
77 def _get_schema(self, c, old_values=None):
78 return repo_schema.RepoSettingsSchema().bind(
78 return repo_schema.RepoSettingsSchema().bind(
79 repo_type=self.db_repo.repo_type,
79 repo_type=self.db_repo.repo_type,
80 repo_type_options=[self.db_repo.repo_type],
80 repo_type_options=[self.db_repo.repo_type],
81 repo_ref_options=c.landing_revs_choices,
81 repo_ref_options=c.landing_revs_choices,
82 repo_ref_items=c.landing_revs,
82 repo_ref_items=c.landing_revs,
83 repo_repo_group_options=c.repo_groups_choices,
83 repo_repo_group_options=c.repo_groups_choices,
84 repo_repo_group_items=c.repo_groups,
84 repo_repo_group_items=c.repo_groups,
85 # user caller
85 # user caller
86 user=self._rhodecode_user,
86 user=self._rhodecode_user,
87 old_values=old_values
87 old_values=old_values
88 )
88 )
89
89
90 @LoginRequired()
90 @LoginRequired()
91 @HasRepoPermissionAnyDecorator('repository.admin')
91 @HasRepoPermissionAnyDecorator('repository.admin')
92 def edit_settings(self):
92 def edit_settings(self):
93 c = self.load_default_context()
93 c = self.load_default_context()
94 c.active = 'settings'
94 c.active = 'settings'
95
95
96 defaults = RepoModel()._get_defaults(self.db_repo_name)
96 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 defaults['repo_owner'] = defaults['user']
97 defaults['repo_owner'] = defaults['user']
98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99
99
100 schema = self._get_schema(c)
100 schema = self._get_schema(c)
101 c.form = RcForm(schema, appstruct=defaults)
101 c.form = RcForm(schema, appstruct=defaults)
102 return self._get_template_context(c)
102 return self._get_template_context(c)
103
103
104 @LoginRequired()
104 @LoginRequired()
105 @HasRepoPermissionAnyDecorator('repository.admin')
105 @HasRepoPermissionAnyDecorator('repository.admin')
106 @CSRFRequired()
106 @CSRFRequired()
107 def edit_settings_update(self):
107 def edit_settings_update(self):
108 _ = self.request.translate
108 _ = self.request.translate
109 c = self.load_default_context()
109 c = self.load_default_context()
110 c.active = 'settings'
110 c.active = 'settings'
111 old_repo_name = self.db_repo_name
111 old_repo_name = self.db_repo_name
112
112
113 old_values = self.db_repo.get_api_data()
113 old_values = self.db_repo.get_api_data()
114 schema = self._get_schema(c, old_values=old_values)
114 schema = self._get_schema(c, old_values=old_values)
115
115
116 c.form = RcForm(schema)
116 c.form = RcForm(schema)
117 pstruct = self.request.POST.items()
117 pstruct = list(self.request.POST.items())
118 pstruct.append(('repo_type', self.db_repo.repo_type))
118 pstruct.append(('repo_type', self.db_repo.repo_type))
119 try:
119 try:
120 schema_data = c.form.validate(pstruct)
120 schema_data = c.form.validate(pstruct)
121 except deform.ValidationFailure as err_form:
121 except deform.ValidationFailure as err_form:
122 return self._get_template_context(c)
122 return self._get_template_context(c)
123
123
124 # data is now VALID, proceed with updates
124 # data is now VALID, proceed with updates
125 # save validated data back into the updates dict
125 # save validated data back into the updates dict
126 validated_updates = dict(
126 validated_updates = dict(
127 repo_name=schema_data['repo_group']['repo_name_without_group'],
127 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 repo_group=schema_data['repo_group']['repo_group_id'],
128 repo_group=schema_data['repo_group']['repo_group_id'],
129
129
130 user=schema_data['repo_owner'],
130 user=schema_data['repo_owner'],
131 repo_description=schema_data['repo_description'],
131 repo_description=schema_data['repo_description'],
132 repo_private=schema_data['repo_private'],
132 repo_private=schema_data['repo_private'],
133 clone_uri=schema_data['repo_clone_uri'],
133 clone_uri=schema_data['repo_clone_uri'],
134 push_uri=schema_data['repo_push_uri'],
134 push_uri=schema_data['repo_push_uri'],
135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 repo_enable_statistics=schema_data['repo_enable_statistics'],
136 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 repo_enable_locking=schema_data['repo_enable_locking'],
137 repo_enable_locking=schema_data['repo_enable_locking'],
138 repo_enable_downloads=schema_data['repo_enable_downloads'],
138 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 )
139 )
140 # detect if SYNC URI changed, if we get OLD means we keep old values
140 # detect if SYNC URI changed, if we get OLD means we keep old values
141 if schema_data['repo_clone_uri_change'] == 'OLD':
141 if schema_data['repo_clone_uri_change'] == 'OLD':
142 validated_updates['clone_uri'] = self.db_repo.clone_uri
142 validated_updates['clone_uri'] = self.db_repo.clone_uri
143
143
144 if schema_data['repo_push_uri_change'] == 'OLD':
144 if schema_data['repo_push_uri_change'] == 'OLD':
145 validated_updates['push_uri'] = self.db_repo.push_uri
145 validated_updates['push_uri'] = self.db_repo.push_uri
146
146
147 # use the new full name for redirect
147 # use the new full name for redirect
148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149
149
150 # save extra fields into our validated data
150 # save extra fields into our validated data
151 for key, value in pstruct:
151 for key, value in pstruct:
152 if key.startswith(RepositoryField.PREFIX):
152 if key.startswith(RepositoryField.PREFIX):
153 validated_updates[key] = value
153 validated_updates[key] = value
154
154
155 try:
155 try:
156 RepoModel().update(self.db_repo, **validated_updates)
156 RepoModel().update(self.db_repo, **validated_updates)
157 ScmModel().mark_for_invalidation(new_repo_name)
157 ScmModel().mark_for_invalidation(new_repo_name)
158
158
159 audit_logger.store_web(
159 audit_logger.store_web(
160 'repo.edit', action_data={'old_data': old_values},
160 'repo.edit', action_data={'old_data': old_values},
161 user=self._rhodecode_user, repo=self.db_repo)
161 user=self._rhodecode_user, repo=self.db_repo)
162
162
163 Session().commit()
163 Session().commit()
164
164
165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 category='success')
166 category='success')
167 except Exception:
167 except Exception:
168 log.exception("Exception during update of repository")
168 log.exception("Exception during update of repository")
169 h.flash(_('Error occurred during update of repository {}').format(
169 h.flash(_('Error occurred during update of repository {}').format(
170 old_repo_name), category='error')
170 old_repo_name), category='error')
171
171
172 name_changed = old_repo_name != new_repo_name
172 name_changed = old_repo_name != new_repo_name
173 if name_changed:
173 if name_changed:
174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 affected_user_ids = [perm['user_id'] for perm in current_perms]
175 affected_user_ids = [perm['user_id'] for perm in current_perms]
176
176
177 # NOTE(marcink): also add owner maybe it has changed
177 # NOTE(marcink): also add owner maybe it has changed
178 owner = User.get_by_username(schema_data['repo_owner'])
178 owner = User.get_by_username(schema_data['repo_owner'])
179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 PermissionModel().trigger_permission_flush(affected_user_ids)
181 PermissionModel().trigger_permission_flush(affected_user_ids)
182
182
183 raise HTTPFound(
183 raise HTTPFound(
184 h.route_path('edit_repo', repo_name=new_repo_name))
184 h.route_path('edit_repo', repo_name=new_repo_name))
185
185
186 @LoginRequired()
186 @LoginRequired()
187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 def toggle_locking(self):
188 def toggle_locking(self):
189 """
189 """
190 Toggle locking of repository by simple GET call to url
190 Toggle locking of repository by simple GET call to url
191 """
191 """
192 _ = self.request.translate
192 _ = self.request.translate
193 repo = self.db_repo
193 repo = self.db_repo
194
194
195 try:
195 try:
196 if repo.enable_locking:
196 if repo.enable_locking:
197 if repo.locked[0]:
197 if repo.locked[0]:
198 Repository.unlock(repo)
198 Repository.unlock(repo)
199 action = _('Unlocked')
199 action = _('Unlocked')
200 else:
200 else:
201 Repository.lock(
201 Repository.lock(
202 repo, self._rhodecode_user.user_id,
202 repo, self._rhodecode_user.user_id,
203 lock_reason=Repository.LOCK_WEB)
203 lock_reason=Repository.LOCK_WEB)
204 action = _('Locked')
204 action = _('Locked')
205
205
206 h.flash(_('Repository has been %s') % action,
206 h.flash(_('Repository has been %s') % action,
207 category='success')
207 category='success')
208 except Exception:
208 except Exception:
209 log.exception("Exception during unlocking")
209 log.exception("Exception during unlocking")
210 h.flash(_('An error occurred during unlocking'),
210 h.flash(_('An error occurred during unlocking'),
211 category='error')
211 category='error')
212 raise HTTPFound(
212 raise HTTPFound(
213 h.route_path('repo_summary', repo_name=self.db_repo_name))
213 h.route_path('repo_summary', repo_name=self.db_repo_name))
214
214
215 @LoginRequired()
215 @LoginRequired()
216 @HasRepoPermissionAnyDecorator('repository.admin')
216 @HasRepoPermissionAnyDecorator('repository.admin')
217 def edit_statistics_form(self):
217 def edit_statistics_form(self):
218 c = self.load_default_context()
218 c = self.load_default_context()
219
219
220 if self.db_repo.stats:
220 if self.db_repo.stats:
221 # this is on what revision we ended up so we add +1 for count
221 # this is on what revision we ended up so we add +1 for count
222 last_rev = self.db_repo.stats.stat_on_revision + 1
222 last_rev = self.db_repo.stats.stat_on_revision + 1
223 else:
223 else:
224 last_rev = 0
224 last_rev = 0
225
225
226 c.active = 'statistics'
226 c.active = 'statistics'
227 c.stats_revision = last_rev
227 c.stats_revision = last_rev
228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229
229
230 if last_rev == 0 or c.repo_last_rev == 0:
230 if last_rev == 0 or c.repo_last_rev == 0:
231 c.stats_percentage = 0
231 c.stats_percentage = 0
232 else:
232 else:
233 c.stats_percentage = '%.2f' % (
233 c.stats_percentage = '%.2f' % (
234 (float((last_rev)) / c.repo_last_rev) * 100)
234 (float((last_rev)) / c.repo_last_rev) * 100)
235 return self._get_template_context(c)
235 return self._get_template_context(c)
236
236
237 @LoginRequired()
237 @LoginRequired()
238 @HasRepoPermissionAnyDecorator('repository.admin')
238 @HasRepoPermissionAnyDecorator('repository.admin')
239 @CSRFRequired()
239 @CSRFRequired()
240 def repo_statistics_reset(self):
240 def repo_statistics_reset(self):
241 _ = self.request.translate
241 _ = self.request.translate
242
242
243 try:
243 try:
244 RepoModel().delete_stats(self.db_repo_name)
244 RepoModel().delete_stats(self.db_repo_name)
245 Session().commit()
245 Session().commit()
246 except Exception:
246 except Exception:
247 log.exception('Edit statistics failure')
247 log.exception('Edit statistics failure')
248 h.flash(_('An error occurred during deletion of repository stats'),
248 h.flash(_('An error occurred during deletion of repository stats'),
249 category='error')
249 category='error')
250 raise HTTPFound(
250 raise HTTPFound(
251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
@@ -1,274 +1,274 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 logging
21 import logging
22 import string
22 import string
23 import time
23 import time
24
24
25 import rhodecode
25 import rhodecode
26
26
27
27
28
28
29 from rhodecode.lib.view_utils import get_format_ref_id
29 from rhodecode.lib.view_utils import get_format_ref_id
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 from rhodecode.lib import helpers as h, rc_cache
32 from rhodecode.lib import helpers as h, rc_cache
33 from rhodecode.lib.utils2 import safe_str, safe_int
33 from rhodecode.lib.utils2 import safe_str, safe_int
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 from rhodecode.model.db import Statistics, CacheKey, User
39 from rhodecode.model.db import Statistics, CacheKey, User
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.scm import ScmModel
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class RepoSummaryView(RepoAppView):
46 class RepoSummaryView(RepoAppView):
47
47
48 def load_default_context(self):
48 def load_default_context(self):
49 c = self._get_local_tmpl_context(include_app_defaults=True)
49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 c.rhodecode_repo = None
50 c.rhodecode_repo = None
51 if not c.repository_requirements_missing:
51 if not c.repository_requirements_missing:
52 c.rhodecode_repo = self.rhodecode_vcs_repo
52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 return c
53 return c
54
54
55 def _load_commits_context(self, c):
55 def _load_commits_context(self, c):
56 p = safe_int(self.request.GET.get('page'), 1)
56 p = safe_int(self.request.GET.get('page'), 1)
57 size = safe_int(self.request.GET.get('size'), 10)
57 size = safe_int(self.request.GET.get('size'), 10)
58
58
59 def url_generator(page_num):
59 def url_generator(page_num):
60 query_params = {
60 query_params = {
61 'page': page_num,
61 'page': page_num,
62 'size': size
62 'size': size
63 }
63 }
64 return h.route_path(
64 return h.route_path(
65 'repo_summary_commits',
65 'repo_summary_commits',
66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67
67
68 pre_load = self.get_commit_preload_attrs()
68 pre_load = self.get_commit_preload_attrs()
69
69
70 try:
70 try:
71 collection = self.rhodecode_vcs_repo.get_commits(
71 collection = self.rhodecode_vcs_repo.get_commits(
72 pre_load=pre_load, translate_tags=False)
72 pre_load=pre_load, translate_tags=False)
73 except EmptyRepositoryError:
73 except EmptyRepositoryError:
74 collection = self.rhodecode_vcs_repo
74 collection = self.rhodecode_vcs_repo
75
75
76 c.repo_commits = h.RepoPage(
76 c.repo_commits = h.RepoPage(
77 collection, page=p, items_per_page=size, url_maker=url_generator)
77 collection, page=p, items_per_page=size, url_maker=url_generator)
78 page_ids = [x.raw_id for x in c.repo_commits]
78 page_ids = [x.raw_id for x in c.repo_commits]
79 c.comments = self.db_repo.get_comments(page_ids)
79 c.comments = self.db_repo.get_comments(page_ids)
80 c.statuses = self.db_repo.statuses(page_ids)
80 c.statuses = self.db_repo.statuses(page_ids)
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @HasRepoPermissionAnyDecorator(
83 @HasRepoPermissionAnyDecorator(
84 'repository.read', 'repository.write', 'repository.admin')
84 'repository.read', 'repository.write', 'repository.admin')
85 def summary_commits(self):
85 def summary_commits(self):
86 c = self.load_default_context()
86 c = self.load_default_context()
87 self._prepare_and_set_clone_url(c)
87 self._prepare_and_set_clone_url(c)
88 self._load_commits_context(c)
88 self._load_commits_context(c)
89 return self._get_template_context(c)
89 return self._get_template_context(c)
90
90
91 @LoginRequired()
91 @LoginRequired()
92 @HasRepoPermissionAnyDecorator(
92 @HasRepoPermissionAnyDecorator(
93 'repository.read', 'repository.write', 'repository.admin')
93 'repository.read', 'repository.write', 'repository.admin')
94 def summary(self):
94 def summary(self):
95 c = self.load_default_context()
95 c = self.load_default_context()
96
96
97 # Prepare the clone URL
97 # Prepare the clone URL
98 self._prepare_and_set_clone_url(c)
98 self._prepare_and_set_clone_url(c)
99
99
100 # If enabled, get statistics data
100 # If enabled, get statistics data
101 c.show_stats = bool(self.db_repo.enable_statistics)
101 c.show_stats = bool(self.db_repo.enable_statistics)
102
102
103 stats = Session().query(Statistics) \
103 stats = Session().query(Statistics) \
104 .filter(Statistics.repository == self.db_repo) \
104 .filter(Statistics.repository == self.db_repo) \
105 .scalar()
105 .scalar()
106
106
107 c.stats_percentage = 0
107 c.stats_percentage = 0
108
108
109 if stats and stats.languages:
109 if stats and stats.languages:
110 c.no_data = False is self.db_repo.enable_statistics
110 c.no_data = False is self.db_repo.enable_statistics
111 lang_stats_d = json.loads(stats.languages)
111 lang_stats_d = json.loads(stats.languages)
112
112
113 # Sort first by decreasing count and second by the file extension,
113 # Sort first by decreasing count and second by the file extension,
114 # so we have a consistent output.
114 # so we have a consistent output.
115 lang_stats_items = sorted(lang_stats_d.items(),
115 lang_stats_items = sorted(lang_stats_d.items(),
116 key=lambda k: (-k[1], k[0]))[:10]
116 key=lambda k: (-k[1], k[0]))[:10]
117 lang_stats = [(x, {"count": y,
117 lang_stats = [(x, {"count": y,
118 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
118 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
119 for x, y in lang_stats_items]
119 for x, y in lang_stats_items]
120
120
121 c.trending_languages = json.dumps(lang_stats)
121 c.trending_languages = json.dumps(lang_stats)
122 else:
122 else:
123 c.no_data = True
123 c.no_data = True
124 c.trending_languages = json.dumps({})
124 c.trending_languages = json.dumps({})
125
125
126 scm_model = ScmModel()
126 scm_model = ScmModel()
127 c.enable_downloads = self.db_repo.enable_downloads
127 c.enable_downloads = self.db_repo.enable_downloads
128 c.repository_followers = scm_model.get_followers(self.db_repo)
128 c.repository_followers = scm_model.get_followers(self.db_repo)
129 c.repository_forks = scm_model.get_forks(self.db_repo)
129 c.repository_forks = scm_model.get_forks(self.db_repo)
130
130
131 # first interaction with the VCS instance after here...
131 # first interaction with the VCS instance after here...
132 if c.repository_requirements_missing:
132 if c.repository_requirements_missing:
133 self.request.override_renderer = \
133 self.request.override_renderer = \
134 'rhodecode:templates/summary/missing_requirements.mako'
134 'rhodecode:templates/summary/missing_requirements.mako'
135 return self._get_template_context(c)
135 return self._get_template_context(c)
136
136
137 c.readme_data, c.readme_file = \
137 c.readme_data, c.readme_file = \
138 self._get_readme_data(self.db_repo, c.visual.default_renderer)
138 self._get_readme_data(self.db_repo, c.visual.default_renderer)
139
139
140 # loads the summary commits template context
140 # loads the summary commits template context
141 self._load_commits_context(c)
141 self._load_commits_context(c)
142
142
143 return self._get_template_context(c)
143 return self._get_template_context(c)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
146 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
147 'repository.read', 'repository.write', 'repository.admin')
148 def repo_stats(self):
148 def repo_stats(self):
149 show_stats = bool(self.db_repo.enable_statistics)
149 show_stats = bool(self.db_repo.enable_statistics)
150 repo_id = self.db_repo.repo_id
150 repo_id = self.db_repo.repo_id
151
151
152 landing_commit = self.db_repo.get_landing_commit()
152 landing_commit = self.db_repo.get_landing_commit()
153 if isinstance(landing_commit, EmptyCommit):
153 if isinstance(landing_commit, EmptyCommit):
154 return {'size': 0, 'code_stats': {}}
154 return {'size': 0, 'code_stats': {}}
155
155
156 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
156 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
157 cache_on = cache_seconds > 0
157 cache_on = cache_seconds > 0
158
158
159 log.debug(
159 log.debug(
160 'Computing REPO STATS for repo_id %s commit_id `%s` '
160 'Computing REPO STATS for repo_id %s commit_id `%s` '
161 'with caching: %s[TTL: %ss]' % (
161 'with caching: %s[TTL: %ss]' % (
162 repo_id, landing_commit, cache_on, cache_seconds or 0))
162 repo_id, landing_commit, cache_on, cache_seconds or 0))
163
163
164 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
164 cache_namespace_uid = 'repo.{}'.format(repo_id)
165 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
165 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166
166
167 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
167 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
168 condition=cache_on)
168 condition=cache_on)
169 def compute_stats(repo_id, commit_id, _show_stats):
169 def compute_stats(repo_id, commit_id, _show_stats):
170 code_stats = {}
170 code_stats = {}
171 size = 0
171 size = 0
172 try:
172 try:
173 commit = self.db_repo.get_commit(commit_id)
173 commit = self.db_repo.get_commit(commit_id)
174
174
175 for node in commit.get_filenodes_generator():
175 for node in commit.get_filenodes_generator():
176 size += node.size
176 size += node.size
177 if not _show_stats:
177 if not _show_stats:
178 continue
178 continue
179 ext = node.extension.lower()
179 ext = node.extension.lower()
180 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
180 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
181 if ext_info:
181 if ext_info:
182 if ext in code_stats:
182 if ext in code_stats:
183 code_stats[ext]['count'] += 1
183 code_stats[ext]['count'] += 1
184 else:
184 else:
185 code_stats[ext] = {"count": 1, "desc": ext_info}
185 code_stats[ext] = {"count": 1, "desc": ext_info}
186 except (EmptyRepositoryError, CommitDoesNotExistError):
186 except (EmptyRepositoryError, CommitDoesNotExistError):
187 pass
187 pass
188 return {'size': h.format_byte_size_binary(size),
188 return {'size': h.format_byte_size_binary(size),
189 'code_stats': code_stats}
189 'code_stats': code_stats}
190
190
191 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
191 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
192 return stats
192 return stats
193
193
194 @LoginRequired()
194 @LoginRequired()
195 @HasRepoPermissionAnyDecorator(
195 @HasRepoPermissionAnyDecorator(
196 'repository.read', 'repository.write', 'repository.admin')
196 'repository.read', 'repository.write', 'repository.admin')
197 def repo_refs_data(self):
197 def repo_refs_data(self):
198 _ = self.request.translate
198 _ = self.request.translate
199 self.load_default_context()
199 self.load_default_context()
200
200
201 repo = self.rhodecode_vcs_repo
201 repo = self.rhodecode_vcs_repo
202 refs_to_create = [
202 refs_to_create = [
203 (_("Branch"), repo.branches, 'branch'),
203 (_("Branch"), repo.branches, 'branch'),
204 (_("Tag"), repo.tags, 'tag'),
204 (_("Tag"), repo.tags, 'tag'),
205 (_("Bookmark"), repo.bookmarks, 'book'),
205 (_("Bookmark"), repo.bookmarks, 'book'),
206 ]
206 ]
207 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
207 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
208 data = {
208 data = {
209 'more': False,
209 'more': False,
210 'results': res
210 'results': res
211 }
211 }
212 return data
212 return data
213
213
214 @LoginRequired()
214 @LoginRequired()
215 @HasRepoPermissionAnyDecorator(
215 @HasRepoPermissionAnyDecorator(
216 'repository.read', 'repository.write', 'repository.admin')
216 'repository.read', 'repository.write', 'repository.admin')
217 def repo_refs_changelog_data(self):
217 def repo_refs_changelog_data(self):
218 _ = self.request.translate
218 _ = self.request.translate
219 self.load_default_context()
219 self.load_default_context()
220
220
221 repo = self.rhodecode_vcs_repo
221 repo = self.rhodecode_vcs_repo
222
222
223 refs_to_create = [
223 refs_to_create = [
224 (_("Branches"), repo.branches, 'branch'),
224 (_("Branches"), repo.branches, 'branch'),
225 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
225 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
226 # TODO: enable when vcs can handle bookmarks filters
226 # TODO: enable when vcs can handle bookmarks filters
227 # (_("Bookmarks"), repo.bookmarks, "book"),
227 # (_("Bookmarks"), repo.bookmarks, "book"),
228 ]
228 ]
229 res = self._create_reference_data(
229 res = self._create_reference_data(
230 repo, self.db_repo_name, refs_to_create)
230 repo, self.db_repo_name, refs_to_create)
231 data = {
231 data = {
232 'more': False,
232 'more': False,
233 'results': res
233 'results': res
234 }
234 }
235 return data
235 return data
236
236
237 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
237 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
238 format_ref_id = get_format_ref_id(repo)
238 format_ref_id = get_format_ref_id(repo)
239
239
240 result = []
240 result = []
241 for title, refs, ref_type in refs_to_create:
241 for title, refs, ref_type in refs_to_create:
242 if refs:
242 if refs:
243 result.append({
243 result.append({
244 'text': title,
244 'text': title,
245 'children': self._create_reference_items(
245 'children': self._create_reference_items(
246 repo, full_repo_name, refs, ref_type,
246 repo, full_repo_name, refs, ref_type,
247 format_ref_id),
247 format_ref_id),
248 })
248 })
249 return result
249 return result
250
250
251 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
251 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
252 result = []
252 result = []
253 is_svn = h.is_svn(repo)
253 is_svn = h.is_svn(repo)
254 for ref_name, raw_id in refs.items():
254 for ref_name, raw_id in refs.items():
255 files_url = self._create_files_url(
255 files_url = self._create_files_url(
256 repo, full_repo_name, ref_name, raw_id, is_svn)
256 repo, full_repo_name, ref_name, raw_id, is_svn)
257 result.append({
257 result.append({
258 'text': ref_name,
258 'text': ref_name,
259 'id': format_ref_id(ref_name, raw_id),
259 'id': format_ref_id(ref_name, raw_id),
260 'raw_id': raw_id,
260 'raw_id': raw_id,
261 'type': ref_type,
261 'type': ref_type,
262 'files_url': files_url,
262 'files_url': files_url,
263 'idx': 0,
263 'idx': 0,
264 })
264 })
265 return result
265 return result
266
266
267 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
267 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
268 use_commit_id = '/' in ref_name or is_svn
268 use_commit_id = '/' in ref_name or is_svn
269 return h.route_path(
269 return h.route_path(
270 'repo_files',
270 'repo_files',
271 repo_name=full_repo_name,
271 repo_name=full_repo_name,
272 f_path=ref_name if is_svn else '',
272 f_path=ref_name if is_svn else '',
273 commit_id=raw_id if use_commit_id else ref_name,
273 commit_id=raw_id if use_commit_id else ref_name,
274 _query=dict(at=ref_name))
274 _query=dict(at=ref_name))
@@ -1,166 +1,168 b''
1
1
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 logging
21 import logging
22 import urllib.request, urllib.parse, urllib.error
22 import urllib.request
23 import urllib.parse
24 import urllib.error
23
25
24 from webhelpers2.html.tools import update_params
26 from webhelpers2.html.tools import update_params
25
27
26 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
28 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
28 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
30 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 from rhodecode.lib.helpers import Page
31 from rhodecode.lib.helpers import Page
30 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.index import searcher_from_config
32 from rhodecode.model import validation_schema
34 from rhodecode.model import validation_schema
33 from rhodecode.model.validation_schema.schemas import search_schema
35 from rhodecode.model.validation_schema.schemas import search_schema
34
36
35 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
36
38
37
39
38 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
40 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 searcher = searcher_from_config(request.registry.settings)
41 searcher = searcher_from_config(request.registry.settings)
40 formatted_results = []
42 formatted_results = []
41 execution_time = ''
43 execution_time = ''
42
44
43 schema = search_schema.SearchParamsSchema()
45 schema = search_schema.SearchParamsSchema()
44 search_tags = []
46 search_tags = []
45 search_params = {}
47 search_params = {}
46 errors = []
48 errors = []
47
49
48 try:
50 try:
49 search_params = schema.deserialize(
51 search_params = schema.deserialize(
50 dict(
52 dict(
51 search_query=request.GET.get('q'),
53 search_query=request.GET.get('q'),
52 search_type=request.GET.get('type'),
54 search_type=request.GET.get('type'),
53 search_sort=request.GET.get('sort'),
55 search_sort=request.GET.get('sort'),
54 search_max_lines=request.GET.get('max_lines'),
56 search_max_lines=request.GET.get('max_lines'),
55 page_limit=request.GET.get('page_limit'),
57 page_limit=request.GET.get('page_limit'),
56 requested_page=request.GET.get('page'),
58 requested_page=request.GET.get('page'),
57 )
59 )
58 )
60 )
59 except validation_schema.Invalid as e:
61 except validation_schema.Invalid as e:
60 errors = e.children
62 errors = e.children
61
63
62 def url_generator(page_num):
64 def url_generator(page_num):
63
65
64 query_params = {
66 query_params = {
65 'page': page_num,
67 'page': page_num,
66 'q': safe_str(search_query),
68 'q': safe_str(search_query),
67 'type': safe_str(search_type),
69 'type': safe_str(search_type),
68 'max_lines': search_max_lines,
70 'max_lines': search_max_lines,
69 'sort': search_sort
71 'sort': search_sort
70 }
72 }
71
73
72 return '?' + urllib.parse.urlencode(query_params)
74 return '?' + urllib.parse.urlencode(query_params)
73
75
74 c = tmpl_context
76 c = tmpl_context
75 search_query = search_params.get('search_query')
77 search_query = search_params.get('search_query')
76 search_type = search_params.get('search_type')
78 search_type = search_params.get('search_type')
77 search_sort = search_params.get('search_sort')
79 search_sort = search_params.get('search_sort')
78 search_max_lines = search_params.get('search_max_lines')
80 search_max_lines = search_params.get('search_max_lines')
79 if search_params.get('search_query'):
81 if search_params.get('search_query'):
80 page_limit = search_params['page_limit']
82 page_limit = search_params['page_limit']
81 requested_page = search_params['requested_page']
83 requested_page = search_params['requested_page']
82
84
83 try:
85 try:
84 search_result = searcher.search(
86 search_result = searcher.search(
85 search_query, search_type, c.auth_user, repo_name, repo_group_name,
87 search_query, search_type, c.auth_user, repo_name, repo_group_name,
86 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
88 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
87
89
88 formatted_results = Page(
90 formatted_results = Page(
89 search_result['results'], page=requested_page,
91 search_result['results'], page=requested_page,
90 item_count=search_result['count'],
92 item_count=search_result['count'],
91 items_per_page=page_limit, url_maker=url_generator)
93 items_per_page=page_limit, url_maker=url_generator)
92 finally:
94 finally:
93 searcher.cleanup()
95 searcher.cleanup()
94
96
95 search_tags = searcher.extract_search_tags(search_query)
97 search_tags = searcher.extract_search_tags(search_query)
96
98
97 if not search_result['error']:
99 if not search_result['error']:
98 execution_time = '%s results (%.4f seconds)' % (
100 execution_time = '%s results (%.4f seconds)' % (
99 search_result['count'],
101 search_result['count'],
100 search_result['runtime'])
102 search_result['runtime'])
101 elif not errors:
103 elif not errors:
102 node = schema['search_query']
104 node = schema['search_query']
103 errors = [
105 errors = [
104 validation_schema.Invalid(node, search_result['error'])]
106 validation_schema.Invalid(node, search_result['error'])]
105
107
106 c.perm_user = c.auth_user
108 c.perm_user = c.auth_user
107 c.repo_name = repo_name
109 c.repo_name = repo_name
108 c.repo_group_name = repo_group_name
110 c.repo_group_name = repo_group_name
109 c.errors = errors
111 c.errors = errors
110 c.formatted_results = formatted_results
112 c.formatted_results = formatted_results
111 c.runtime = execution_time
113 c.runtime = execution_time
112 c.cur_query = search_query
114 c.cur_query = search_query
113 c.search_type = search_type
115 c.search_type = search_type
114 c.searcher = searcher
116 c.searcher = searcher
115 c.search_tags = search_tags
117 c.search_tags = search_tags
116
118
117 direction, sort_field = searcher.get_sort(search_type, search_sort)
119 direction, sort_field = searcher.get_sort(search_type, search_sort)
118 sort_definition = searcher.sort_def(search_type, direction, sort_field)
120 sort_definition = searcher.sort_def(search_type, direction, sort_field)
119 c.sort = ''
121 c.sort = ''
120 c.sort_tag = None
122 c.sort_tag = None
121 c.sort_tag_dir = direction
123 c.sort_tag_dir = direction
122 if sort_definition:
124 if sort_definition:
123 c.sort = '{}:{}'.format(direction, sort_field)
125 c.sort = '{}:{}'.format(direction, sort_field)
124 c.sort_tag = sort_field
126 c.sort_tag = sort_field
125
127
126
128
127 class SearchView(BaseAppView):
129 class SearchView(BaseAppView):
128 def load_default_context(self):
130 def load_default_context(self):
129 c = self._get_local_tmpl_context()
131 c = self._get_local_tmpl_context()
130 return c
132 return c
131
133
132 @LoginRequired()
134 @LoginRequired()
133 def search(self):
135 def search(self):
134 c = self.load_default_context()
136 c = self.load_default_context()
135 perform_search(self.request, c)
137 perform_search(self.request, c)
136 return self._get_template_context(c)
138 return self._get_template_context(c)
137
139
138
140
139 class SearchRepoView(RepoAppView):
141 class SearchRepoView(RepoAppView):
140 def load_default_context(self):
142 def load_default_context(self):
141 c = self._get_local_tmpl_context()
143 c = self._get_local_tmpl_context()
142 c.active = 'search'
144 c.active = 'search'
143 return c
145 return c
144
146
145 @LoginRequired()
147 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
148 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
149 'repository.read', 'repository.write', 'repository.admin')
148 def search_repo(self):
150 def search_repo(self):
149 c = self.load_default_context()
151 c = self.load_default_context()
150 perform_search(self.request, c, repo_name=self.db_repo_name)
152 perform_search(self.request, c, repo_name=self.db_repo_name)
151 return self._get_template_context(c)
153 return self._get_template_context(c)
152
154
153
155
154 class SearchRepoGroupView(RepoGroupAppView):
156 class SearchRepoGroupView(RepoGroupAppView):
155 def load_default_context(self):
157 def load_default_context(self):
156 c = self._get_local_tmpl_context()
158 c = self._get_local_tmpl_context()
157 c.active = 'search'
159 c.active = 'search'
158 return c
160 return c
159
161
160 @LoginRequired()
162 @LoginRequired()
161 @HasRepoGroupPermissionAnyDecorator(
163 @HasRepoGroupPermissionAnyDecorator(
162 'group.read', 'group.write', 'group.admin')
164 'group.read', 'group.write', 'group.admin')
163 def search_repo_group(self):
165 def search_repo_group(self):
164 c = self.load_default_context()
166 c = self.load_default_context()
165 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
167 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
166 return self._get_template_context(c)
168 return self._get_template_context(c)
@@ -1,163 +1,163 b''
1
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 os
21 import os
22 import sys
22 import sys
23 import json
24 import logging
23 import logging
25
24
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
25 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.ext_json import sjson as json
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class VcsServer(object):
33 class VcsServer(object):
34 repo_user_agent = None # set in child classes
34 repo_user_agent = None # set in child classes
35 _path = None # set executable path for hg/git/svn binary
35 _path = None # set executable path for hg/git/svn binary
36 backend = None # set in child classes
36 backend = None # set in child classes
37 tunnel = None # subprocess handling tunnel
37 tunnel = None # subprocess handling tunnel
38 write_perms = ['repository.admin', 'repository.write']
38 write_perms = ['repository.admin', 'repository.write']
39 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39 read_perms = ['repository.read', 'repository.admin', 'repository.write']
40
40
41 def __init__(self, user, user_permissions, config, env):
41 def __init__(self, user, user_permissions, config, env):
42 self.user = user
42 self.user = user
43 self.user_permissions = user_permissions
43 self.user_permissions = user_permissions
44 self.config = config
44 self.config = config
45 self.env = env
45 self.env = env
46 self.stdin = sys.stdin
46 self.stdin = sys.stdin
47
47
48 self.repo_name = None
48 self.repo_name = None
49 self.repo_mode = None
49 self.repo_mode = None
50 self.store = ''
50 self.store = ''
51 self.ini_path = ''
51 self.ini_path = ''
52
52
53 def _invalidate_cache(self, repo_name):
53 def _invalidate_cache(self, repo_name):
54 """
54 """
55 Set's cache for this repository for invalidation on next access
55 Set's cache for this repository for invalidation on next access
56
56
57 :param repo_name: full repo name, also a cache key
57 :param repo_name: full repo name, also a cache key
58 """
58 """
59 ScmModel().mark_for_invalidation(repo_name)
59 ScmModel().mark_for_invalidation(repo_name)
60
60
61 def has_write_perm(self):
61 def has_write_perm(self):
62 permission = self.user_permissions.get(self.repo_name)
62 permission = self.user_permissions.get(self.repo_name)
63 if permission in ['repository.write', 'repository.admin']:
63 if permission in ['repository.write', 'repository.admin']:
64 return True
64 return True
65
65
66 return False
66 return False
67
67
68 def _check_permissions(self, action):
68 def _check_permissions(self, action):
69 permission = self.user_permissions.get(self.repo_name)
69 permission = self.user_permissions.get(self.repo_name)
70 log.debug('permission for %s on %s are: %s',
70 log.debug('permission for %s on %s are: %s',
71 self.user, self.repo_name, permission)
71 self.user, self.repo_name, permission)
72
72
73 if not permission:
73 if not permission:
74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 self.user, self.repo_name)
75 self.user, self.repo_name)
76 return -2
76 return -2
77
77
78 if action == 'pull':
78 if action == 'pull':
79 if permission in self.read_perms:
79 if permission in self.read_perms:
80 log.info(
80 log.info(
81 'READ Permissions for User "%s" detected to repo "%s"!',
81 'READ Permissions for User "%s" detected to repo "%s"!',
82 self.user, self.repo_name)
82 self.user, self.repo_name)
83 return 0
83 return 0
84 else:
84 else:
85 if permission in self.write_perms:
85 if permission in self.write_perms:
86 log.info(
86 log.info(
87 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
87 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
88 self.user, self.repo_name)
88 self.user, self.repo_name)
89 return 0
89 return 0
90
90
91 log.error('Cannot properly fetch or verify user `%s` permissions. '
91 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 'Permissions: %s, vcs action: %s',
92 'Permissions: %s, vcs action: %s',
93 self.user, permission, action)
93 self.user, permission, action)
94 return -2
94 return -2
95
95
96 def update_environment(self, action, extras=None):
96 def update_environment(self, action, extras=None):
97
97
98 scm_data = {
98 scm_data = {
99 'ip': os.environ['SSH_CLIENT'].split()[0],
99 'ip': os.environ['SSH_CLIENT'].split()[0],
100 'username': self.user.username,
100 'username': self.user.username,
101 'user_id': self.user.user_id,
101 'user_id': self.user.user_id,
102 'action': action,
102 'action': action,
103 'repository': self.repo_name,
103 'repository': self.repo_name,
104 'scm': self.backend,
104 'scm': self.backend,
105 'config': self.ini_path,
105 'config': self.ini_path,
106 'repo_store': self.store,
106 'repo_store': self.store,
107 'make_lock': None,
107 'make_lock': None,
108 'locked_by': [None, None],
108 'locked_by': [None, None],
109 'server_url': None,
109 'server_url': None,
110 'user_agent': '{}/ssh-user-agent'.format(self.repo_user_agent),
110 'user_agent': '{}/ssh-user-agent'.format(self.repo_user_agent),
111 'hooks': ['push', 'pull'],
111 'hooks': ['push', 'pull'],
112 'hooks_module': 'rhodecode.lib.hooks_daemon',
112 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 'is_shadow_repo': False,
113 'is_shadow_repo': False,
114 'detect_force_push': False,
114 'detect_force_push': False,
115 'check_branch_perms': False,
115 'check_branch_perms': False,
116
116
117 'SSH': True,
117 'SSH': True,
118 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
118 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 }
119 }
120 if extras:
120 if extras:
121 scm_data.update(extras)
121 scm_data.update(extras)
122 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
122 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123
123
124 def get_root_store(self):
124 def get_root_store(self):
125 root_store = self.store
125 root_store = self.store
126 if not root_store.endswith('/'):
126 if not root_store.endswith('/'):
127 # always append trailing slash
127 # always append trailing slash
128 root_store = root_store + '/'
128 root_store = root_store + '/'
129 return root_store
129 return root_store
130
130
131 def _handle_tunnel(self, extras):
131 def _handle_tunnel(self, extras):
132 # pre-auth
132 # pre-auth
133 action = 'pull'
133 action = 'pull'
134 exit_code = self._check_permissions(action)
134 exit_code = self._check_permissions(action)
135 if exit_code:
135 if exit_code:
136 return exit_code, False
136 return exit_code, False
137
137
138 req = self.env['request']
138 req = self.env['request']
139 server_url = req.host_url + req.script_name
139 server_url = req.host_url + req.script_name
140 extras['server_url'] = server_url
140 extras['server_url'] = server_url
141
141
142 log.debug('Using %s binaries from path %s', self.backend, self._path)
142 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 exit_code = self.tunnel.run(extras)
143 exit_code = self.tunnel.run(extras)
144
144
145 return exit_code, action == "push"
145 return exit_code, action == "push"
146
146
147 def run(self, tunnel_extras=None):
147 def run(self, tunnel_extras=None):
148 tunnel_extras = tunnel_extras or {}
148 tunnel_extras = tunnel_extras or {}
149 extras = {}
149 extras = {}
150 extras.update(tunnel_extras)
150 extras.update(tunnel_extras)
151
151
152 callback_daemon, extras = prepare_callback_daemon(
152 callback_daemon, extras = prepare_callback_daemon(
153 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
153 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 host=vcs_settings.HOOKS_HOST,
154 host=vcs_settings.HOOKS_HOST,
155 use_direct_calls=False)
155 use_direct_calls=False)
156
156
157 with callback_daemon:
157 with callback_daemon:
158 try:
158 try:
159 return self._handle_tunnel(extras)
159 return self._handle_tunnel(extras)
160 finally:
160 finally:
161 log.debug('Running cleanup with cache invalidation')
161 log.debug('Running cleanup with cache invalidation')
162 if self.repo_name:
162 if self.repo_name:
163 self._invalidate_cache(self.repo_name)
163 self._invalidate_cache(self.repo_name)
@@ -1,258 +1,258 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 import logging
24 import logging
25 import signal
25 import signal
26 import tempfile
26 import tempfile
27 from subprocess import Popen, PIPE
27 from subprocess import Popen, PIPE
28 import urllib.parse
28 import urllib.parse
29
29
30 from .base import VcsServer
30 from .base import VcsServer
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class SubversionTunnelWrapper(object):
35 class SubversionTunnelWrapper(object):
36 process = None
36 process = None
37
37
38 def __init__(self, server):
38 def __init__(self, server):
39 self.server = server
39 self.server = server
40 self.timeout = 30
40 self.timeout = 30
41 self.stdin = sys.stdin
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
45
46 self.read_only = True # flag that we set to make the hooks readonly
46 self.read_only = True # flag that we set to make the hooks readonly
47
47
48 def create_svn_config(self):
48 def create_svn_config(self):
49 content = (
49 content = (
50 '[general]\n'
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
53 config_file.write(content)
54
54
55 def create_hooks_env(self):
55 def create_hooks_env(self):
56 content = (
56 content = (
57 '[default]\n'
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
62 hooks_env_file.write(content)
63
63
64 def remove_configs(self):
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
66 os.remove(self.hooks_env_path)
67
67
68 def command(self):
68 def command(self):
69 root = self.server.get_root_store()
69 root = self.server.get_root_store()
70 username = self.server.user.username
70 username = self.server.user.username
71
71
72 command = [
72 command = [
73 self.server.svn_path, '-t',
73 self.server.svn_path, '-t',
74 '--config-file', self.svn_conf_path,
74 '--config-file', self.svn_conf_path,
75 '--tunnel-user', username,
75 '--tunnel-user', username,
76 '-r', root]
76 '-r', root]
77 log.debug("Final CMD: %s", ' '.join(command))
77 log.debug("Final CMD: %s", ' '.join(command))
78 return command
78 return command
79
79
80 def start(self):
80 def start(self):
81 command = self.command()
81 command = self.command()
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
83
83
84 def sync(self):
84 def sync(self):
85 while self.process.poll() is None:
85 while self.process.poll() is None:
86 next_byte = self.stdin.read(1)
86 next_byte = self.stdin.read(1)
87 if not next_byte:
87 if not next_byte:
88 break
88 break
89 self.process.stdin.write(next_byte)
89 self.process.stdin.write(next_byte)
90 self.remove_configs()
90 self.remove_configs()
91
91
92 @property
92 @property
93 def return_code(self):
93 def return_code(self):
94 return self.process.returncode
94 return self.process.returncode
95
95
96 def get_first_client_response(self):
96 def get_first_client_response(self):
97 signal.signal(signal.SIGALRM, self.interrupt)
97 signal.signal(signal.SIGALRM, self.interrupt)
98 signal.alarm(self.timeout)
98 signal.alarm(self.timeout)
99 first_response = self._read_first_client_response()
99 first_response = self._read_first_client_response()
100 signal.alarm(0)
100 signal.alarm(0)
101 return (self._parse_first_client_response(first_response)
101 return (self._parse_first_client_response(first_response)
102 if first_response else None)
102 if first_response else None)
103
103
104 def patch_first_client_response(self, response, **kwargs):
104 def patch_first_client_response(self, response, **kwargs):
105 self.create_hooks_env()
105 self.create_hooks_env()
106 data = response.copy()
106 data = response.copy()
107 data.update(kwargs)
107 data.update(kwargs)
108 data['url'] = self._svn_string(data['url'])
108 data['url'] = self._svn_string(data['url'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
109 data['ra_client'] = self._svn_string(data['ra_client'])
110 data['client'] = data['client'] or ''
110 data['client'] = data['client'] or ''
111 buffer_ = (
111 buffer_ = (
112 "( {version} ( {capabilities} ) {url}{ra_client}"
112 "( {version} ( {capabilities} ) {url}{ra_client}"
113 "( {client}) ) ".format(**data))
113 "( {client}) ) ".format(**data))
114 self.process.stdin.write(buffer_)
114 self.process.stdin.write(buffer_)
115
115
116 def fail(self, message):
116 def fail(self, message):
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
118 message=self._svn_string(message)))
118 message=self._svn_string(message)))
119 self.remove_configs()
119 self.remove_configs()
120 self.process.kill()
120 self.process.kill()
121 return 1
121 return 1
122
122
123 def interrupt(self, signum, frame):
123 def interrupt(self, signum, frame):
124 self.fail("Exited by timeout")
124 self.fail("Exited by timeout")
125
125
126 def _svn_string(self, str_):
126 def _svn_string(self, str_):
127 if not str_:
127 if not str_:
128 return ''
128 return ''
129 return '{length}:{string} '.format(length=len(str_), string=str_)
129 return '{length}:{string} '.format(length=len(str_), string=str_)
130
130
131 def _read_first_client_response(self):
131 def _read_first_client_response(self):
132 buffer_ = ""
132 buffer_ = ""
133 brackets_stack = []
133 brackets_stack = []
134 while True:
134 while True:
135 next_byte = self.stdin.read(1)
135 next_byte = self.stdin.read(1)
136 buffer_ += next_byte
136 buffer_ += next_byte
137 if next_byte == "(":
137 if next_byte == "(":
138 brackets_stack.append(next_byte)
138 brackets_stack.append(next_byte)
139 elif next_byte == ")":
139 elif next_byte == ")":
140 brackets_stack.pop()
140 brackets_stack.pop()
141 elif next_byte == " " and not brackets_stack:
141 elif next_byte == " " and not brackets_stack:
142 break
142 break
143
143
144 return buffer_
144 return buffer_
145
145
146 def _parse_first_client_response(self, buffer_):
146 def _parse_first_client_response(self, buffer_):
147 """
147 """
148 According to the Subversion RA protocol, the first request
148 According to the Subversion RA protocol, the first request
149 should look like:
149 should look like:
150
150
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
151 ( version:number ( cap:word ... ) url:string ? ra-client:string
152 ( ? client:string ) )
152 ( ? client:string ) )
153
153
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
155 """
155 """
156 version_re = r'(?P<version>\d+)'
156 version_re = r'(?P<version>\d+)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
158 url_re = r'\d+\:(?P<url>[\W\w]+)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
161 regex = re.compile(
161 regex = re.compile(
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
163 r'\(\s{client}\)\s\)\s*$'.format(
163 r'\(\s{client}\)\s\)\s*$'.format(
164 version=version_re, capabilities=capabilities_re,
164 version=version_re, capabilities=capabilities_re,
165 url=url_re, ra_client=ra_client_re, client=client_re))
165 url=url_re, ra_client=ra_client_re, client=client_re))
166 matcher = regex.match(buffer_)
166 matcher = regex.match(buffer_)
167
167
168 return matcher.groupdict() if matcher else None
168 return matcher.groupdict() if matcher else None
169
169
170 def _match_repo_name(self, url):
170 def _match_repo_name(self, url):
171 """
171 """
172 Given an server url, try to match it against ALL known repository names.
172 Given an server url, try to match it against ALL known repository names.
173 This handles a tricky SVN case for SSH and subdir commits.
173 This handles a tricky SVN case for SSH and subdir commits.
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
175 result in the url with this subdir added.
175 result in the url with this subdir added.
176 """
176 """
177 # case 1 direct match, we don't do any "heavy" lookups
177 # case 1 direct match, we don't do any "heavy" lookups
178 if url in self.server.user_permissions:
178 if url in self.server.user_permissions:
179 return url
179 return url
180
180
181 log.debug('Extracting repository name from subdir path %s', url)
181 log.debug('Extracting repository name from subdir path %s', url)
182 # case 2 we check all permissions, and match closes possible case...
182 # case 2 we check all permissions, and match closes possible case...
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
184 # to assume that it will have the repo name as prefix, we ensure the prefix
184 # to assume that it will have the repo name as prefix, we ensure the prefix
185 # for similar repositories isn't matched by adding a /
185 # for similar repositories isn't matched by adding a /
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
187 for repo_name in self.server.user_permissions:
187 for repo_name in self.server.user_permissions:
188 repo_name_prefix = repo_name + '/'
188 repo_name_prefix = repo_name + '/'
189 if url.startswith(repo_name_prefix):
189 if url.startswith(repo_name_prefix):
190 log.debug('Found prefix %s match, returning proper repository name',
190 log.debug('Found prefix %s match, returning proper repository name',
191 repo_name_prefix)
191 repo_name_prefix)
192 return repo_name
192 return repo_name
193
193
194 return
194 return
195
195
196 def run(self, extras):
196 def run(self, extras):
197 action = 'pull'
197 action = 'pull'
198 self.create_svn_config()
198 self.create_svn_config()
199 self.start()
199 self.start()
200
200
201 first_response = self.get_first_client_response()
201 first_response = self.get_first_client_response()
202 if not first_response:
202 if not first_response:
203 return self.fail("Repository name cannot be extracted")
203 return self.fail("Repository name cannot be extracted")
204
204
205 url_parts = urllib.parse.urlparse(first_response['url'])
205 url_parts = urllib.parse.urlparse(first_response['url'])
206
206
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
208
208
209 exit_code = self.server._check_permissions(action)
209 exit_code = self.server._check_permissions(action)
210 if exit_code:
210 if exit_code:
211 return exit_code
211 return exit_code
212
212
213 # set the readonly flag to False if we have proper permissions
213 # set the readonly flag to False if we have proper permissions
214 if self.server.has_write_perm():
214 if self.server.has_write_perm():
215 self.read_only = False
215 self.read_only = False
216 self.server.update_environment(action=action, extras=extras)
216 self.server.update_environment(action=action, extras=extras)
217
217
218 self.patch_first_client_response(first_response)
218 self.patch_first_client_response(first_response)
219 self.sync()
219 self.sync()
220 return self.return_code
220 return self.return_code
221
221
222
222
223 class SubversionServer(VcsServer):
223 class SubversionServer(VcsServer):
224 backend = 'svn'
224 backend = 'svn'
225 repo_user_agent = 'svn'
225 repo_user_agent = 'svn'
226
226
227 def __init__(self, store, ini_path, repo_name,
227 def __init__(self, store, ini_path, repo_name,
228 user, user_permissions, config, env):
228 user, user_permissions, config, env):
229 super(SubversionServer, self)\
229 super(SubversionServer, self)\
230 .__init__(user, user_permissions, config, env)
230 .__init__(user, user_permissions, config, env)
231 self.store = store
231 self.store = store
232 self.ini_path = ini_path
232 self.ini_path = ini_path
233 # NOTE(dan): repo_name at this point is empty,
233 # NOTE(dan): repo_name at this point is empty,
234 # this is set later in .run() based from parsed input stream
234 # this is set later in .run() based from parsed input stream
235 self.repo_name = repo_name
235 self.repo_name = repo_name
236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
237
237
238 self.tunnel = SubversionTunnelWrapper(server=self)
238 self.tunnel = SubversionTunnelWrapper(server=self)
239
239
240 def _handle_tunnel(self, extras):
240 def _handle_tunnel(self, extras):
241
241
242 # pre-auth
242 # pre-auth
243 action = 'pull'
243 action = 'pull'
244 # Special case for SVN, we extract repo name at later stage
244 # Special case for SVN, we extract repo name at later stage
245 # exit_code = self._check_permissions(action)
245 # exit_code = self._check_permissions(action)
246 # if exit_code:
246 # if exit_code:
247 # return exit_code, False
247 # return exit_code, False
248
248
249 req = self.env['request']
249 req = self.env['request']
250 server_url = req.host_url + req.script_name
250 server_url = req.host_url + req.script_name
251 extras['server_url'] = server_url
251 extras['server_url'] = server_url
252
252
253 log.debug('Using %s binaries from path %s', self.backend, self._path)
253 log.debug('Using %s binaries from path %s', self.backend, self._path)
254 exit_code = self.tunnel.run(extras)
254 exit_code = self.tunnel.run(extras)
255
255
256 return exit_code, action == "push"
256 return exit_code, action == "push"
257
257
258
258
@@ -1,49 +1,48 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # 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
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # 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/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 """
20 """
22 Base module for form rendering / validation - currently just a wrapper for
21 Base module for form rendering / validation - currently just a wrapper for
23 deform - later can be replaced with something custom.
22 deform - later can be replaced with something custom.
24 """
23 """
25
24
26 from rhodecode.translation import _
25 from rhodecode.translation import _
27 from rhodecode.translation import TranslationString
26 from rhodecode.translation import TranslationString
28
27
29 from mako.template import Template
28 from mako.template import Template
30 from deform import Button, Form, widget, ValidationFailure
29 from deform import Button, Form, widget, ValidationFailure
31
30
32
31
33 class buttons:
32 class buttons:
34 save = Button(name='Save', type='submit')
33 save = Button(name='Save', type='submit')
35 reset = Button(name=_('Reset'), type='reset')
34 reset = Button(name=_('Reset'), type='reset')
36 delete = Button(name=_('Delete'), type='submit')
35 delete = Button(name=_('Delete'), type='submit')
37
36
38
37
39 class RcForm(Form):
38 class RcForm(Form):
40 def render_error(self, request, field):
39 def render_error(self, request, field):
41 html = ''
40 html = ''
42 if field.error:
41 if field.error:
43 for err in field.error.messages():
42 for err in field.error.messages():
44 if isinstance(err, TranslationString):
43 if isinstance(err, TranslationString):
45 err = request.translate(err)
44 err = request.translate(err)
46 html = Template(
45 html = Template(
47 '<span class="error-message">${err}</span>').render(err=err)
46 '<span class="error-message">${err}</span>').render(err=err)
48
47
49 return html
48 return html
@@ -1,305 +1,305 b''
1
1
2
2
3 # Copyright (C) 2017-2020 RhodeCode GmbH
3 # Copyright (C) 2017-2020 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 logging
21 import logging
22 import datetime
22 import datetime
23
23
24 from rhodecode.lib.jsonalchemy import JsonRaw
24 from rhodecode.lib.jsonalchemy import JsonRaw
25 from rhodecode.model import meta
25 from rhodecode.model import meta
26 from rhodecode.model.db import User, UserLog, Repository
26 from rhodecode.model.db import User, UserLog, Repository
27 from rhodecode.lib.str_utils import safe_str
27
28
28
29
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
31 # action as key, and expected action_data as value
32 # action as key, and expected action_data as value
32 ACTIONS_V1 = {
33 ACTIONS_V1 = {
33 'user.login.success': {'user_agent': ''},
34 'user.login.success': {'user_agent': ''},
34 'user.login.failure': {'user_agent': ''},
35 'user.login.failure': {'user_agent': ''},
35 'user.logout': {'user_agent': ''},
36 'user.logout': {'user_agent': ''},
36 'user.register': {},
37 'user.register': {},
37 'user.password.reset_request': {},
38 'user.password.reset_request': {},
38 'user.push': {'user_agent': '', 'commit_ids': []},
39 'user.push': {'user_agent': '', 'commit_ids': []},
39 'user.pull': {'user_agent': ''},
40 'user.pull': {'user_agent': ''},
40
41
41 'user.create': {'data': {}},
42 'user.create': {'data': {}},
42 'user.delete': {'old_data': {}},
43 'user.delete': {'old_data': {}},
43 'user.edit': {'old_data': {}},
44 'user.edit': {'old_data': {}},
44 'user.edit.permissions': {},
45 'user.edit.permissions': {},
45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 'user.edit.token.add': {'token': {}, 'user': {}},
48 'user.edit.token.add': {'token': {}, 'user': {}},
48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 'user.edit.token.delete': {'token': {}, 'user': {}},
49 'user.edit.email.add': {'email': ''},
50 'user.edit.email.add': {'email': ''},
50 'user.edit.email.delete': {'email': ''},
51 'user.edit.email.delete': {'email': ''},
51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 'user.edit.password_reset.enabled': {},
54 'user.edit.password_reset.enabled': {},
54 'user.edit.password_reset.disabled': {},
55 'user.edit.password_reset.disabled': {},
55
56
56 'user_group.create': {'data': {}},
57 'user_group.create': {'data': {}},
57 'user_group.delete': {'old_data': {}},
58 'user_group.delete': {'old_data': {}},
58 'user_group.edit': {'old_data': {}},
59 'user_group.edit': {'old_data': {}},
59 'user_group.edit.permissions': {},
60 'user_group.edit.permissions': {},
60 'user_group.edit.member.add': {'user': {}},
61 'user_group.edit.member.add': {'user': {}},
61 'user_group.edit.member.delete': {'user': {}},
62 'user_group.edit.member.delete': {'user': {}},
62
63
63 'repo.create': {'data': {}},
64 'repo.create': {'data': {}},
64 'repo.fork': {'data': {}},
65 'repo.fork': {'data': {}},
65 'repo.edit': {'old_data': {}},
66 'repo.edit': {'old_data': {}},
66 'repo.edit.permissions': {},
67 'repo.edit.permissions': {},
67 'repo.edit.permissions.branch': {},
68 'repo.edit.permissions.branch': {},
68 'repo.archive': {'old_data': {}},
69 'repo.archive': {'old_data': {}},
69 'repo.delete': {'old_data': {}},
70 'repo.delete': {'old_data': {}},
70
71
71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 'archive_spec': '', 'archive_cached': ''},
73 'archive_spec': '', 'archive_cached': ''},
73
74
74 'repo.permissions.branch_rule.create': {},
75 'repo.permissions.branch_rule.create': {},
75 'repo.permissions.branch_rule.edit': {},
76 'repo.permissions.branch_rule.edit': {},
76 'repo.permissions.branch_rule.delete': {},
77 'repo.permissions.branch_rule.delete': {},
77
78
78 'repo.pull_request.create': '',
79 'repo.pull_request.create': '',
79 'repo.pull_request.edit': '',
80 'repo.pull_request.edit': '',
80 'repo.pull_request.delete': '',
81 'repo.pull_request.delete': '',
81 'repo.pull_request.close': '',
82 'repo.pull_request.close': '',
82 'repo.pull_request.merge': '',
83 'repo.pull_request.merge': '',
83 'repo.pull_request.vote': '',
84 'repo.pull_request.vote': '',
84 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.edit': '',
86 'repo.pull_request.comment.edit': '',
86 'repo.pull_request.comment.delete': '',
87 'repo.pull_request.comment.delete': '',
87
88
88 'repo.pull_request.reviewer.add': '',
89 'repo.pull_request.reviewer.add': '',
89 'repo.pull_request.reviewer.delete': '',
90 'repo.pull_request.reviewer.delete': '',
90
91
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93 'repo.pull_request.observer.delete': '',
93
94
94 'repo.commit.strip': {'commit_id': ''},
95 'repo.commit.strip': {'commit_id': ''},
95 'repo.commit.comment.create': {'data': {}},
96 'repo.commit.comment.create': {'data': {}},
96 'repo.commit.comment.delete': {'data': {}},
97 'repo.commit.comment.delete': {'data': {}},
97 'repo.commit.comment.edit': {'data': {}},
98 'repo.commit.comment.edit': {'data': {}},
98 'repo.commit.vote': '',
99 'repo.commit.vote': '',
99
100
100 'repo.artifact.add': '',
101 'repo.artifact.add': '',
101 'repo.artifact.delete': '',
102 'repo.artifact.delete': '',
102
103
103 'repo_group.create': {'data': {}},
104 'repo_group.create': {'data': {}},
104 'repo_group.edit': {'old_data': {}},
105 'repo_group.edit': {'old_data': {}},
105 'repo_group.edit.permissions': {},
106 'repo_group.edit.permissions': {},
106 'repo_group.delete': {'old_data': {}},
107 'repo_group.delete': {'old_data': {}},
107 }
108 }
108
109
109 ACTIONS = ACTIONS_V1
110 ACTIONS = ACTIONS_V1
110
111
111 SOURCE_WEB = 'source_web'
112 SOURCE_WEB = 'source_web'
112 SOURCE_API = 'source_api'
113 SOURCE_API = 'source_api'
113
114
114
115
115 class UserWrap(object):
116 class UserWrap(object):
116 """
117 """
117 Fake object used to imitate AuthUser
118 Fake object used to imitate AuthUser
118 """
119 """
119
120
120 def __init__(self, user_id=None, username=None, ip_addr=None):
121 def __init__(self, user_id=None, username=None, ip_addr=None):
121 self.user_id = user_id
122 self.user_id = user_id
122 self.username = username
123 self.username = username
123 self.ip_addr = ip_addr
124 self.ip_addr = ip_addr
124
125
125
126
126 class RepoWrap(object):
127 class RepoWrap(object):
127 """
128 """
128 Fake object used to imitate RepoObject that audit logger requires
129 Fake object used to imitate RepoObject that audit logger requires
129 """
130 """
130
131
131 def __init__(self, repo_id=None, repo_name=None):
132 def __init__(self, repo_id=None, repo_name=None):
132 self.repo_id = repo_id
133 self.repo_id = repo_id
133 self.repo_name = repo_name
134 self.repo_name = repo_name
134
135
135
136
136 def _store_log(action_name, action_data, user_id, username, user_data,
137 def _store_log(action_name, action_data, user_id, username, user_data,
137 ip_address, repository_id, repository_name):
138 ip_address, repository_id, repository_name):
138 user_log = UserLog()
139 user_log = UserLog()
139 user_log.version = UserLog.VERSION_2
140 user_log.version = UserLog.VERSION_2
140
141
141 user_log.action = action_name
142 user_log.action = action_name
142 user_log.action_data = action_data or JsonRaw('{}')
143 user_log.action_data = action_data or JsonRaw('{}')
143
144
144 user_log.user_ip = ip_address
145 user_log.user_ip = ip_address
145
146
146 user_log.user_id = user_id
147 user_log.user_id = user_id
147 user_log.username = username
148 user_log.username = username
148 user_log.user_data = user_data or JsonRaw('{}')
149 user_log.user_data = user_data or JsonRaw('{}')
149
150
150 user_log.repository_id = repository_id
151 user_log.repository_id = repository_id
151 user_log.repository_name = repository_name
152 user_log.repository_name = repository_name
152
153
153 user_log.action_date = datetime.datetime.now()
154 user_log.action_date = datetime.datetime.now()
154
155
155 return user_log
156 return user_log
156
157
157
158
158 def store_web(*args, **kwargs):
159 def store_web(*args, **kwargs):
159 action_data = {}
160 action_data = {}
160 org_action_data = kwargs.pop('action_data', {})
161 org_action_data = kwargs.pop('action_data', {})
161 action_data.update(org_action_data)
162 action_data.update(org_action_data)
162 action_data['source'] = SOURCE_WEB
163 action_data['source'] = SOURCE_WEB
163 kwargs['action_data'] = action_data
164 kwargs['action_data'] = action_data
164
165
165 return store(*args, **kwargs)
166 return store(*args, **kwargs)
166
167
167
168
168 def store_api(*args, **kwargs):
169 def store_api(*args, **kwargs):
169 action_data = {}
170 action_data = {}
170 org_action_data = kwargs.pop('action_data', {})
171 org_action_data = kwargs.pop('action_data', {})
171 action_data.update(org_action_data)
172 action_data.update(org_action_data)
172 action_data['source'] = SOURCE_API
173 action_data['source'] = SOURCE_API
173 kwargs['action_data'] = action_data
174 kwargs['action_data'] = action_data
174
175
175 return store(*args, **kwargs)
176 return store(*args, **kwargs)
176
177
177
178
178 def store(action, user, action_data=None, user_data=None, ip_addr=None,
179 def store(action, user, action_data=None, user_data=None, ip_addr=None,
179 repo=None, sa_session=None, commit=False):
180 repo=None, sa_session=None, commit=False):
180 """
181 """
181 Audit logger for various actions made by users, typically this
182 Audit logger for various actions made by users, typically this
182 results in a call such::
183 results in a call such::
183
184
184 from rhodecode.lib import audit_logger
185 from rhodecode.lib import audit_logger
185
186
186 audit_logger.store(
187 audit_logger.store(
187 'repo.edit', user=self._rhodecode_user)
188 'repo.edit', user=self._rhodecode_user)
188 audit_logger.store(
189 audit_logger.store(
189 'repo.delete', action_data={'data': repo_data},
190 'repo.delete', action_data={'data': repo_data},
190 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
191 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
191
192
192 # repo action
193 # repo action
193 audit_logger.store(
194 audit_logger.store(
194 'repo.delete',
195 'repo.delete',
195 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
196 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
196 repo=audit_logger.RepoWrap(repo_name='some-repo'))
197 repo=audit_logger.RepoWrap(repo_name='some-repo'))
197
198
198 # repo action, when we know and have the repository object already
199 # repo action, when we know and have the repository object already
199 audit_logger.store(
200 audit_logger.store(
200 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
201 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
201 user=self._rhodecode_user,
202 user=self._rhodecode_user,
202 repo=repo_object)
203 repo=repo_object)
203
204
204 # alternative wrapper to the above
205 # alternative wrapper to the above
205 audit_logger.store_web(
206 audit_logger.store_web(
206 'repo.delete', action_data={},
207 'repo.delete', action_data={},
207 user=self._rhodecode_user,
208 user=self._rhodecode_user,
208 repo=repo_object)
209 repo=repo_object)
209
210
210 # without an user ?
211 # without an user ?
211 audit_logger.store(
212 audit_logger.store(
212 'user.login.failure',
213 'user.login.failure',
213 user=audit_logger.UserWrap(
214 user=audit_logger.UserWrap(
214 username=self.request.params.get('username'),
215 username=self.request.params.get('username'),
215 ip_addr=self.request.remote_addr))
216 ip_addr=self.request.remote_addr))
216
217
217 """
218 """
218 from rhodecode.lib.utils2 import safe_unicode
219 from rhodecode.lib.auth import AuthUser
219 from rhodecode.lib.auth import AuthUser
220
220
221 action_spec = ACTIONS.get(action, None)
221 action_spec = ACTIONS.get(action, None)
222 if action_spec is None:
222 if action_spec is None:
223 raise ValueError('Action `{}` is not supported'.format(action))
223 raise ValueError('Action `{}` is not supported'.format(action))
224
224
225 if not sa_session:
225 if not sa_session:
226 sa_session = meta.Session()
226 sa_session = meta.Session()
227
227
228 try:
228 try:
229 username = getattr(user, 'username', None)
229 username = getattr(user, 'username', None)
230 if not username:
230 if not username:
231 pass
231 pass
232
232
233 user_id = getattr(user, 'user_id', None)
233 user_id = getattr(user, 'user_id', None)
234 if not user_id:
234 if not user_id:
235 # maybe we have username ? Try to figure user_id from username
235 # maybe we have username ? Try to figure user_id from username
236 if username:
236 if username:
237 user_id = getattr(
237 user_id = getattr(
238 User.get_by_username(username), 'user_id', None)
238 User.get_by_username(username), 'user_id', None)
239
239
240 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
240 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
241 if not ip_addr:
241 if not ip_addr:
242 pass
242 pass
243
243
244 if not user_data:
244 if not user_data:
245 # try to get this from the auth user
245 # try to get this from the auth user
246 if isinstance(user, AuthUser):
246 if isinstance(user, AuthUser):
247 user_data = {
247 user_data = {
248 'username': user.username,
248 'username': user.username,
249 'email': user.email,
249 'email': user.email,
250 }
250 }
251
251
252 repository_name = getattr(repo, 'repo_name', None)
252 repository_name = getattr(repo, 'repo_name', None)
253 repository_id = getattr(repo, 'repo_id', None)
253 repository_id = getattr(repo, 'repo_id', None)
254 if not repository_id:
254 if not repository_id:
255 # maybe we have repo_name ? Try to figure repo_id from repo_name
255 # maybe we have repo_name ? Try to figure repo_id from repo_name
256 if repository_name:
256 if repository_name:
257 repository_id = getattr(
257 repository_id = getattr(
258 Repository.get_by_repo_name(repository_name), 'repo_id', None)
258 Repository.get_by_repo_name(repository_name), 'repo_id', None)
259
259
260 action_name = safe_unicode(action)
260 action_name = safe_str(action)
261 ip_address = safe_unicode(ip_addr)
261 ip_address = safe_str(ip_addr)
262
262
263 with sa_session.no_autoflush:
263 with sa_session.no_autoflush:
264
264
265 user_log = _store_log(
265 user_log = _store_log(
266 action_name=action_name,
266 action_name=action_name,
267 action_data=action_data or {},
267 action_data=action_data or {},
268 user_id=user_id,
268 user_id=user_id,
269 username=username,
269 username=username,
270 user_data=user_data or {},
270 user_data=user_data or {},
271 ip_address=ip_address,
271 ip_address=ip_address,
272 repository_id=repository_id,
272 repository_id=repository_id,
273 repository_name=repository_name
273 repository_name=repository_name
274 )
274 )
275
275
276 sa_session.add(user_log)
276 sa_session.add(user_log)
277 if commit:
277 if commit:
278 sa_session.commit()
278 sa_session.commit()
279 entry_id = user_log.entry_id or ''
279 entry_id = user_log.entry_id or ''
280
280
281 update_user_last_activity(sa_session, user_id)
281 update_user_last_activity(sa_session, user_id)
282
282
283 if commit:
283 if commit:
284 sa_session.commit()
284 sa_session.commit()
285
285
286 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
286 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
287 entry_id, action_name, user_id, username, ip_address,
287 entry_id, action_name, user_id, username, ip_address,
288 extra={"entry_id": entry_id, "action": action_name,
288 extra={"entry_id": entry_id, "action": action_name,
289 "user_id": user_id, "ip": ip_address})
289 "user_id": user_id, "ip": ip_address})
290
290
291 except Exception:
291 except Exception:
292 log.exception('AUDIT: failed to store audit log')
292 log.exception('AUDIT: failed to store audit log')
293
293
294
294
295 def update_user_last_activity(sa_session, user_id):
295 def update_user_last_activity(sa_session, user_id):
296 _last_activity = datetime.datetime.now()
296 _last_activity = datetime.datetime.now()
297 try:
297 try:
298 sa_session.query(User).filter(User.user_id == user_id).update(
298 sa_session.query(User).filter(User.user_id == user_id).update(
299 {"last_activity": _last_activity})
299 {"last_activity": _last_activity})
300 log.debug(
300 log.debug(
301 'updated user `%s` last activity to:%s', user_id, _last_activity)
301 'updated user `%s` last activity to:%s', user_id, _last_activity)
302 except Exception:
302 except Exception:
303 log.exception("Failed last activity update for user_id: %s", user_id)
303 log.exception("Failed last activity update for user_id: %s", user_id)
304 sa_session.rollback()
304 sa_session.rollback()
305
305
@@ -1,140 +1,140 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import os
21 import json
22 import logging
20 import logging
23 import datetime
21 import datetime
24 import time
22 import time
25
23
26 from functools import partial
24 from functools import partial
27
25
28 import configparser
26 import configparser
29 from celery.result import AsyncResult
27 from celery.result import AsyncResult
30 import celery.loaders.base
28 import celery.loaders.base
31 import celery.schedules
29 import celery.schedules
32
30
31 from rhodecode.lib.ext_json import sjson as json
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 def get_task_id(task):
36 def get_task_id(task):
37 task_id = None
37 task_id = None
38 if isinstance(task, AsyncResult):
38 if isinstance(task, AsyncResult):
39 task_id = task.task_id
39 task_id = task.task_id
40
40
41 return task_id
41 return task_id
42
42
43
43
44 def crontab(value):
44 def crontab(value):
45 return celery.schedules.crontab(**value)
45 return celery.schedules.crontab(**value)
46
46
47
47
48 def timedelta(value):
48 def timedelta(value):
49 return datetime.timedelta(**value)
49 return datetime.timedelta(**value)
50
50
51
51
52 def safe_json(get, section, key):
52 def safe_json(get, section, key):
53 value = ''
53 value = ''
54 try:
54 try:
55 value = get(key)
55 value = get(key)
56 json_value = json.loads(value)
56 json_value = json.loads(value)
57 except ValueError:
57 except ValueError:
58 msg = 'The %s=%s is not valid json in section %s' % (
58 msg = 'The %s=%s is not valid json in section %s' % (
59 key, value, section
59 key, value, section
60 )
60 )
61 raise ValueError(msg)
61 raise ValueError(msg)
62
62
63 return json_value
63 return json_value
64
64
65
65
66 def raw_2_schedule(schedule_value, schedule_type):
66 def raw_2_schedule(schedule_value, schedule_type):
67 schedule_type_map = {
67 schedule_type_map = {
68 'crontab': crontab,
68 'crontab': crontab,
69 'timedelta': timedelta,
69 'timedelta': timedelta,
70 'integer': int
70 'integer': int
71 }
71 }
72 scheduler_cls = schedule_type_map.get(schedule_type)
72 scheduler_cls = schedule_type_map.get(schedule_type)
73
73
74 if scheduler_cls is None:
74 if scheduler_cls is None:
75 raise ValueError(
75 raise ValueError(
76 'schedule type %s in section is invalid' % (
76 'schedule type %s in section is invalid' % (
77 schedule_type,
77 schedule_type,
78 )
78 )
79 )
79 )
80 try:
80 try:
81 schedule = scheduler_cls(schedule_value)
81 schedule = scheduler_cls(schedule_value)
82 except TypeError:
82 except TypeError:
83 log.exception('Failed to compose a schedule from value: %r', schedule_value)
83 log.exception('Failed to compose a schedule from value: %r', schedule_value)
84 schedule = None
84 schedule = None
85 return schedule
85 return schedule
86
86
87
87
88 def get_beat_config(parser, section):
88 def get_beat_config(parser, section):
89
89
90 get = partial(parser.get, section)
90 get = partial(parser.get, section)
91 has_option = partial(parser.has_option, section)
91 has_option = partial(parser.has_option, section)
92
92
93 schedule_type = get('type')
93 schedule_type = get('type')
94 schedule_value = safe_json(get, section, 'schedule')
94 schedule_value = safe_json(get, section, 'schedule')
95
95
96 config = {
96 config = {
97 'schedule_type': schedule_type,
97 'schedule_type': schedule_type,
98 'schedule_value': schedule_value,
98 'schedule_value': schedule_value,
99 'task': get('task'),
99 'task': get('task'),
100 }
100 }
101 schedule = raw_2_schedule(schedule_value, schedule_type)
101 schedule = raw_2_schedule(schedule_value, schedule_type)
102 if schedule:
102 if schedule:
103 config['schedule'] = schedule
103 config['schedule'] = schedule
104
104
105 if has_option('args'):
105 if has_option('args'):
106 config['args'] = safe_json(get, section, 'args')
106 config['args'] = safe_json(get, section, 'args')
107
107
108 if has_option('kwargs'):
108 if has_option('kwargs'):
109 config['kwargs'] = safe_json(get, section, 'kwargs')
109 config['kwargs'] = safe_json(get, section, 'kwargs')
110
110
111 if has_option('force_update'):
111 if has_option('force_update'):
112 config['force_update'] = get('force_update')
112 config['force_update'] = get('force_update')
113
113
114 return config
114 return config
115
115
116
116
117 def parse_ini_vars(ini_vars):
117 def parse_ini_vars(ini_vars):
118 options = {}
118 options = {}
119 for pairs in ini_vars.split(','):
119 for pairs in ini_vars.split(','):
120 key, value = pairs.split('=')
120 key, value = pairs.split('=')
121 options[key] = value
121 options[key] = value
122 return options
122 return options
123
123
124
124
125 def ping_db():
125 def ping_db():
126 from rhodecode.model import meta
126 from rhodecode.model import meta
127 from rhodecode.model.db import DbMigrateVersion
127 from rhodecode.model.db import DbMigrateVersion
128 log.info('Testing DB connection...')
128 log.info('Testing DB connection...')
129
129
130 for test in range(10):
130 for test in range(10):
131 try:
131 try:
132 scalar = DbMigrateVersion.query().scalar()
132 scalar = DbMigrateVersion.query().scalar()
133 log.debug('DB PING %s@%s', scalar, scalar.version)
133 log.debug('DB PING %s@%s', scalar, scalar.version)
134 break
134 break
135 except Exception:
135 except Exception:
136 retry = 1
136 retry = 1
137 log.debug('DB not ready, next try in %ss', retry)
137 log.debug('DB not ready, next try in %ss', retry)
138 time.sleep(retry)
138 time.sleep(retry)
139 finally:
139 finally:
140 meta.Session.remove()
140 meta.Session.remove()
@@ -1,78 +1,78 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 """
20 """
21 Provides utilities around date and time handling
21 Provides utilities around date and time handling
22 """
22 """
23
23
24 import datetime
24 import datetime
25 import time
25 import time
26
26
27
27
28 def makedate():
28 def makedate():
29 lt = time.localtime()
29 lt = time.localtime()
30 if lt[8] == 1 and time.daylight:
30 if lt[8] == 1 and time.daylight:
31 tz = time.altzone
31 tz = time.altzone
32 else:
32 else:
33 tz = time.timezone
33 tz = time.timezone
34 return time.mktime(lt), tz
34 return time.mktime(lt), tz
35
35
36
36
37 def utcdate_fromtimestamp(unixts, tzoffset=0):
37 def utcdate_fromtimestamp(unixts, tzoffset=0):
38 """
38 """
39 Makes a local datetime object out of unix timestamp
39 Makes a local datetime object out of unix timestamp
40
40
41 :param unixts:
41 :param unixts:
42 :param tzoffset:
42 :param tzoffset:
43 """
43 """
44
44
45 return datetime.datetime.utcfromtimestamp(float(unixts))
45 return datetime.datetime.utcfromtimestamp(float(unixts))
46
46
47
47
48 def date_astimestamp(value):
48 def date_astimestamp(value):
49 """
49 """
50 Convert a given `datetime.datetime` into a `float` like `time.time`
50 Convert a given `datetime.datetime` into a `float` like `time.time`
51 """
51 """
52 return time.mktime(value.timetuple()) + value.microsecond / 1E6
52 return time.mktime(value.timetuple()) + value.microsecond / 1E6
53
53
54
54
55 def date_to_timestamp_plus_offset(value):
55 def date_to_timestamp_plus_offset(value):
56 """
56 """
57 Convert a given `datetime.datetime` into a unix timestamp and offset.
57 Convert a given `datetime.datetime` into a unix timestamp and offset.
58 """
58 """
59 # TODO: johbo: The time handling looks quite fragile here since we mix
59 # TODO: johbo: The time handling looks quite fragile here since we mix
60 # system time zones with naive datetime instances.
60 # system time zones with naive datetime instances.
61 if value is None:
61 if value is None:
62 value = time.time()
62 value = time.time()
63 elif isinstance(value, datetime.datetime):
63 elif isinstance(value, datetime.datetime):
64 assert not is_aware(value), (
64 assert not is_aware(value), (
65 "This code is not prepared to handle aware datetime instances")
65 "This code is not prepared to handle aware datetime instances")
66 value = date_astimestamp(value)
66 value = date_astimestamp(value)
67 return (value, time.timezone)
67 return value, time.timezone
68
68
69
69
70 def is_aware(value):
70 def is_aware(value):
71 """
71 """
72 Determines if a given datetime.time is aware.
72 Determines if a given datetime.time is aware.
73
73
74 The logic is described in Python's docs:
74 The logic is described in Python's docs:
75 http://docs.python.org/library/datetime.html#datetime.tzinfo
75 http://docs.python.org/library/datetime.html#datetime.tzinfo
76 """
76 """
77 return (value.tzinfo is not None
77 return (value.tzinfo is not None
78 and value.tzinfo.utcoffset(value) is not None)
78 and value.tzinfo.utcoffset(value) is not None)
@@ -1,57 +1,57 b''
1 """
1 """
2 Utilities for XML generation/parsing.
2 Utilities for XML generation/parsing.
3 """
3 """
4
4
5 import six
5 import six
6
6
7 from xml.sax.saxutils import XMLGenerator, quoteattr
7 from xml.sax.saxutils import XMLGenerator, quoteattr
8 from urllib.parse import quote
8 from urllib.parse import quote
9 from rhodecode.lib.utils import safe_str, safe_unicode
9 from rhodecode.lib.str_utils import safe_str
10
10
11
11
12 class SimplerXMLGenerator(XMLGenerator):
12 class SimplerXMLGenerator(XMLGenerator):
13 def addQuickElement(self, name, contents=None, attrs=None):
13 def addQuickElement(self, name, contents=None, attrs=None):
14 "Convenience method for adding an element with no children"
14 "Convenience method for adding an element with no children"
15 if attrs is None:
15 if attrs is None:
16 attrs = {}
16 attrs = {}
17 self.startElement(name, attrs)
17 self.startElement(name, attrs)
18 if contents is not None:
18 if contents is not None:
19 self.characters(contents)
19 self.characters(contents)
20 self.endElement(name)
20 self.endElement(name)
21
21
22 def startElement(self, name, attrs):
22 def startElement(self, name, attrs):
23 self._write('<' + name)
23 self._write('<' + name)
24 # sort attributes for consistent output
24 # sort attributes for consistent output
25 for (name, value) in sorted(attrs.items()):
25 for (name, value) in sorted(attrs.items()):
26 self._write(' %s=%s' % (name, quoteattr(value)))
26 self._write(' %s=%s' % (name, quoteattr(value)))
27 self._write(six.u('>'))
27 self._write(six.u('>'))
28
28
29
29
30 def iri_to_uri(iri):
30 def iri_to_uri(iri):
31 """
31 """
32 Convert an Internationalized Resource Identifier (IRI) portion to a URI
32 Convert an Internationalized Resource Identifier (IRI) portion to a URI
33 portion that is suitable for inclusion in a URL.
33 portion that is suitable for inclusion in a URL.
34 This is the algorithm from section 3.1 of RFC 3987. However, since we are
34 This is the algorithm from section 3.1 of RFC 3987. However, since we are
35 assuming input is either UTF-8 or unicode already, we can simplify things a
35 assuming input is either UTF-8 or unicode already, we can simplify things a
36 little from the full method.
36 little from the full method.
37 Returns an ASCII string containing the encoded result.
37 Returns an ASCII string containing the encoded result.
38 """
38 """
39 # The list of safe characters here is constructed from the "reserved" and
39 # The list of safe characters here is constructed from the "reserved" and
40 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
40 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
41 # reserved = gen-delims / sub-delims
41 # reserved = gen-delims / sub-delims
42 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
42 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
43 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
43 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
44 # / "*" / "+" / "," / ";" / "="
44 # / "*" / "+" / "," / ";" / "="
45 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
45 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
46 # Of the unreserved characters, urllib.quote already considers all but
46 # Of the unreserved characters, urllib.quote already considers all but
47 # the ~ safe.
47 # the ~ safe.
48 # The % character is also added to the list of safe characters here, as the
48 # The % character is also added to the list of safe characters here, as the
49 # end of section 3.1 of RFC 3987 specifically mentions that % must not be
49 # end of section 3.1 of RFC 3987 specifically mentions that % must not be
50 # converted.
50 # converted.
51 if iri is None:
51 if iri is None:
52 return iri
52 return iri
53 return quote(safe_str(iri), safe=b"/#%[]=:;$&()+,!?*@'~")
53 return quote(safe_str(iri), safe=b"/#%[]=:;$&()+,!?*@'~")
54
54
55
55
56 def force_text(text, strings_only=False):
56 def force_text(text, strings_only=False):
57 return safe_unicode(text)
57 return safe_str(text)
@@ -1,25 +1,25 b''
1
1
2
2
3 # Copyright (C) 2020-2020 RhodeCode GmbH
3 # Copyright (C) 2020-2020 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 # base64 filter e.g ${ example | base64,n }
21 # base64 filter e.g ${ example | base64,n }
22 def base64(text):
22 def base64(text):
23 import base64
23 from rhodecode.lib.str_utils import base64_to_str
24 from rhodecode.lib.helpers import safe_str, safe_bytes
24 return base64_to_str(text)
25 return safe_str(base64.encodebytes(safe_bytes(text)))
25
@@ -1,311 +1,311 b''
1
1
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 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 Index schema for RhodeCode
22 Index schema for RhodeCode
23 """
23 """
24
24
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29
29
30 from whoosh import query as query_lib
30 from whoosh import query as query_lib
31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
33 from whoosh.qparser import QueryParser, QueryParserError
33 from whoosh.qparser import QueryParser, QueryParserError
34
34
35 import rhodecode.lib.helpers as h
35 import rhodecode.lib.helpers as h
36 from rhodecode.lib.index import BaseSearcher
36 from rhodecode.lib.index import BaseSearcher
37 from rhodecode.lib.utils2 import safe_unicode
37 from rhodecode.lib.str_utils import safe_str
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 try:
42 try:
43 # we first try to import from rhodecode tools, fallback to copies if
43 # we first try to import from rhodecode tools, fallback to copies if
44 # we're unable to
44 # we're unable to
45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 COMMIT_SCHEMA)
47 COMMIT_SCHEMA)
48 except ImportError:
48 except ImportError:
49 log.warning('rhodecode_tools schema not available, doing a fallback '
49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 from rhodecode.lib.index.whoosh_fallback_schema import (
51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 COMMIT_SCHEMA)
53 COMMIT_SCHEMA)
54
54
55
55
56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 FRAGMENTER = ContextFragmenter(200)
57 FRAGMENTER = ContextFragmenter(200)
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 class WhooshSearcher(BaseSearcher):
62 class WhooshSearcher(BaseSearcher):
63 # this also shows in UI
63 # this also shows in UI
64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 name = 'whoosh'
65 name = 'whoosh'
66
66
67 def __init__(self, config):
67 def __init__(self, config):
68 super(Searcher, self).__init__()
68 super(Searcher, self).__init__()
69 self.config = config
69 self.config = config
70 if not os.path.isdir(self.config['location']):
70 if not os.path.isdir(self.config['location']):
71 os.makedirs(self.config['location'])
71 os.makedirs(self.config['location'])
72
72
73 opener = create_in
73 opener = create_in
74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 opener = open_dir
75 opener = open_dir
76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 indexname=FILE_INDEX_NAME)
77 indexname=FILE_INDEX_NAME)
78
78
79 opener = create_in
79 opener = create_in
80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 opener = open_dir
81 opener = open_dir
82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 indexname=COMMIT_INDEX_NAME)
83 indexname=COMMIT_INDEX_NAME)
84
84
85 self.commit_schema = COMMIT_SCHEMA
85 self.commit_schema = COMMIT_SCHEMA
86 self.commit_index = changeset_index
86 self.commit_index = changeset_index
87 self.file_schema = FILE_SCHEMA
87 self.file_schema = FILE_SCHEMA
88 self.file_index = file_index
88 self.file_index = file_index
89 self.searcher = None
89 self.searcher = None
90
90
91 def cleanup(self):
91 def cleanup(self):
92 if self.searcher:
92 if self.searcher:
93 self.searcher.close()
93 self.searcher.close()
94
94
95 def _extend_query(self, query):
95 def _extend_query(self, query):
96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 if hashes:
97 if hashes:
98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 query = u'(%s) OR %s' % (query, hashes_or_query)
99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 return query
100 return query
101
101
102 def sort_def(self, search_type, direction, sort_field):
102 def sort_def(self, search_type, direction, sort_field):
103
103
104 if search_type == 'commit':
104 if search_type == 'commit':
105 field_defs = {
105 field_defs = {
106 'message': 'message',
106 'message': 'message',
107 'date': 'date',
107 'date': 'date',
108 'author_email': 'author',
108 'author_email': 'author',
109 }
109 }
110 elif search_type == 'path':
110 elif search_type == 'path':
111 field_defs = {
111 field_defs = {
112 'file': 'path',
112 'file': 'path',
113 'size': 'size',
113 'size': 'size',
114 'lines': 'lines',
114 'lines': 'lines',
115 }
115 }
116 elif search_type == 'content':
116 elif search_type == 'content':
117 # NOTE(dan): content doesn't support any sorting
117 # NOTE(dan): content doesn't support any sorting
118 field_defs = {}
118 field_defs = {}
119 else:
119 else:
120 return ''
120 return ''
121
121
122 if sort_field in field_defs:
122 if sort_field in field_defs:
123 return field_defs[sort_field]
123 return field_defs[sort_field]
124
124
125 def search(self, query, document_type, search_user,
125 def search(self, query, document_type, search_user,
126 repo_name=None, repo_group_name=None,
126 repo_name=None, repo_group_name=None,
127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
128
128
129 original_query = query
129 original_query = query
130 query = self._extend_query(query)
130 query = self._extend_query(query)
131
131
132 log.debug(u'QUERY: %s on %s', query, document_type)
132 log.debug(u'QUERY: %s on %s', query, document_type)
133 result = {
133 result = {
134 'results': [],
134 'results': [],
135 'count': 0,
135 'count': 0,
136 'error': None,
136 'error': None,
137 'runtime': 0
137 'runtime': 0
138 }
138 }
139 search_type, index_name, schema_defn = self._prepare_for_search(
139 search_type, index_name, schema_defn = self._prepare_for_search(
140 document_type)
140 document_type)
141 self._init_searcher(index_name)
141 self._init_searcher(index_name)
142 try:
142 try:
143 qp = QueryParser(search_type, schema=schema_defn)
143 qp = QueryParser(search_type, schema=schema_defn)
144 allowed_repos_filter = self._get_repo_filter(
144 allowed_repos_filter = self._get_repo_filter(
145 search_user, repo_name)
145 search_user, repo_name)
146 try:
146 try:
147 query = qp.parse(safe_unicode(query))
147 query = qp.parse(safe_str(query))
148 log.debug('query: %s (%s)', query, repr(query))
148 log.debug('query: %s (%s)', query, repr(query))
149
149
150 reverse, sorted_by = False, None
150 reverse, sorted_by = False, None
151 direction, sort_field = self.get_sort(search_type, sort)
151 direction, sort_field = self.get_sort(search_type, sort)
152 if sort_field:
152 if sort_field:
153 sort_definition = self.sort_def(search_type, direction, sort_field)
153 sort_definition = self.sort_def(search_type, direction, sort_field)
154 if sort_definition:
154 if sort_definition:
155 sorted_by = sort_definition
155 sorted_by = sort_definition
156 if direction == Searcher.DIRECTION_DESC:
156 if direction == Searcher.DIRECTION_DESC:
157 reverse = True
157 reverse = True
158 if direction == Searcher.DIRECTION_ASC:
158 if direction == Searcher.DIRECTION_ASC:
159 reverse = False
159 reverse = False
160
160
161 whoosh_results = self.searcher.search(
161 whoosh_results = self.searcher.search(
162 query, filter=allowed_repos_filter, limit=None,
162 query, filter=allowed_repos_filter, limit=None,
163 sortedby=sorted_by, reverse=reverse)
163 sortedby=sorted_by, reverse=reverse)
164
164
165 # fixes for 32k limit that whoosh uses for highlight
165 # fixes for 32k limit that whoosh uses for highlight
166 whoosh_results.fragmenter.charlimit = None
166 whoosh_results.fragmenter.charlimit = None
167 res_ln = whoosh_results.scored_length()
167 res_ln = whoosh_results.scored_length()
168 result['runtime'] = whoosh_results.runtime
168 result['runtime'] = whoosh_results.runtime
169 result['count'] = res_ln
169 result['count'] = res_ln
170 result['results'] = WhooshResultWrapper(
170 result['results'] = WhooshResultWrapper(
171 search_type, res_ln, whoosh_results)
171 search_type, res_ln, whoosh_results)
172
172
173 except QueryParserError:
173 except QueryParserError:
174 result['error'] = 'Invalid search query. Try quoting it.'
174 result['error'] = 'Invalid search query. Try quoting it.'
175 except (EmptyIndexError, IOError, OSError):
175 except (EmptyIndexError, IOError, OSError):
176 msg = 'There is no index to search in. Please run whoosh indexer'
176 msg = 'There is no index to search in. Please run whoosh indexer'
177 log.exception(msg)
177 log.exception(msg)
178 result['error'] = msg
178 result['error'] = msg
179 except Exception:
179 except Exception:
180 msg = 'An error occurred during this search operation'
180 msg = 'An error occurred during this search operation'
181 log.exception(msg)
181 log.exception(msg)
182 result['error'] = msg
182 result['error'] = msg
183
183
184 return result
184 return result
185
185
186 def statistics(self, translator):
186 def statistics(self, translator):
187 _ = translator
187 _ = translator
188 stats = [
188 stats = [
189 {'key': _('Index Type'), 'value': 'Whoosh'},
189 {'key': _('Index Type'), 'value': 'Whoosh'},
190 {'sep': True},
190 {'sep': True},
191
191
192 {'key': _('File Index'), 'value': str(self.file_index)},
192 {'key': _('File Index'), 'value': str(self.file_index)},
193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
195
195
196 {'sep': True},
196 {'sep': True},
197
197
198 {'key': _('Commit index'), 'value': str(self.commit_index)},
198 {'key': _('Commit index'), 'value': str(self.commit_index)},
199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
201 ]
201 ]
202 return stats
202 return stats
203
203
204 def _get_repo_filter(self, auth_user, repo_name):
204 def _get_repo_filter(self, auth_user, repo_name):
205
205
206 allowed_to_search = [
206 allowed_to_search = [
207 repo for repo, perm in
207 repo for repo, perm in
208 auth_user.permissions['repositories'].items()
208 auth_user.permissions['repositories'].items()
209 if perm != 'repository.none']
209 if perm != 'repository.none']
210
210
211 if repo_name:
211 if repo_name:
212 repo_filter = [query_lib.Term('repository', repo_name)]
212 repo_filter = [query_lib.Term('repository', repo_name)]
213
213
214 elif 'hg.admin' in auth_user.permissions.get('global', []):
214 elif 'hg.admin' in auth_user.permissions.get('global', []):
215 return None
215 return None
216
216
217 else:
217 else:
218 repo_filter = [query_lib.Term('repository', _rn)
218 repo_filter = [query_lib.Term('repository', _rn)
219 for _rn in allowed_to_search]
219 for _rn in allowed_to_search]
220 # in case we're not allowed to search anywhere, it's a trick
220 # in case we're not allowed to search anywhere, it's a trick
221 # to tell whoosh we're filtering, on ALL results
221 # to tell whoosh we're filtering, on ALL results
222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
223
223
224 return query_lib.Or(repo_filter)
224 return query_lib.Or(repo_filter)
225
225
226 def _prepare_for_search(self, cur_type):
226 def _prepare_for_search(self, cur_type):
227 search_type = {
227 search_type = {
228 'content': 'content',
228 'content': 'content',
229 'commit': 'message',
229 'commit': 'message',
230 'path': 'path',
230 'path': 'path',
231 'repository': 'repository'
231 'repository': 'repository'
232 }.get(cur_type, 'content')
232 }.get(cur_type, 'content')
233
233
234 index_name = {
234 index_name = {
235 'content': FILE_INDEX_NAME,
235 'content': FILE_INDEX_NAME,
236 'commit': COMMIT_INDEX_NAME,
236 'commit': COMMIT_INDEX_NAME,
237 'path': FILE_INDEX_NAME
237 'path': FILE_INDEX_NAME
238 }.get(cur_type, FILE_INDEX_NAME)
238 }.get(cur_type, FILE_INDEX_NAME)
239
239
240 schema_defn = {
240 schema_defn = {
241 'content': self.file_schema,
241 'content': self.file_schema,
242 'commit': self.commit_schema,
242 'commit': self.commit_schema,
243 'path': self.file_schema
243 'path': self.file_schema
244 }.get(cur_type, self.file_schema)
244 }.get(cur_type, self.file_schema)
245
245
246 log.debug('IDX: %s', index_name)
246 log.debug('IDX: %s', index_name)
247 log.debug('SCHEMA: %s', schema_defn)
247 log.debug('SCHEMA: %s', schema_defn)
248 return search_type, index_name, schema_defn
248 return search_type, index_name, schema_defn
249
249
250 def _init_searcher(self, index_name):
250 def _init_searcher(self, index_name):
251 idx = open_dir(self.config['location'], indexname=index_name)
251 idx = open_dir(self.config['location'], indexname=index_name)
252 self.searcher = idx.searcher()
252 self.searcher = idx.searcher()
253 return self.searcher
253 return self.searcher
254
254
255
255
256 Searcher = WhooshSearcher
256 Searcher = WhooshSearcher
257
257
258
258
259 class WhooshResultWrapper(object):
259 class WhooshResultWrapper(object):
260 def __init__(self, search_type, total_hits, results):
260 def __init__(self, search_type, total_hits, results):
261 self.search_type = search_type
261 self.search_type = search_type
262 self.results = results
262 self.results = results
263 self.total_hits = total_hits
263 self.total_hits = total_hits
264
264
265 def __str__(self):
265 def __str__(self):
266 return '<%s at %s>' % (self.__class__.__name__, len(self))
266 return '<%s at %s>' % (self.__class__.__name__, len(self))
267
267
268 def __repr__(self):
268 def __repr__(self):
269 return self.__str__()
269 return self.__str__()
270
270
271 def __len__(self):
271 def __len__(self):
272 return self.total_hits
272 return self.total_hits
273
273
274 def __iter__(self):
274 def __iter__(self):
275 """
275 """
276 Allows Iteration over results,and lazy generate content
276 Allows Iteration over results,and lazy generate content
277
277
278 *Requires* implementation of ``__getitem__`` method.
278 *Requires* implementation of ``__getitem__`` method.
279 """
279 """
280 for hit in self.results:
280 for hit in self.results:
281 yield self.get_full_content(hit)
281 yield self.get_full_content(hit)
282
282
283 def __getitem__(self, key):
283 def __getitem__(self, key):
284 """
284 """
285 Slicing of resultWrapper
285 Slicing of resultWrapper
286 """
286 """
287 i, j = key.start, key.stop
287 i, j = key.start, key.stop
288 for hit in self.results[i:j]:
288 for hit in self.results[i:j]:
289 yield self.get_full_content(hit)
289 yield self.get_full_content(hit)
290
290
291 def get_full_content(self, hit):
291 def get_full_content(self, hit):
292 # TODO: marcink: this feels like an overkill, there's a lot of data
292 # TODO: marcink: this feels like an overkill, there's a lot of data
293 # inside hit object, and we don't need all
293 # inside hit object, and we don't need all
294 res = dict(hit)
294 res = dict(hit)
295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
296 res['content_highlight'] = ''
296 res['content_highlight'] = ''
297
297
298 f_path = '' # pragma: no cover
298 f_path = '' # pragma: no cover
299 if self.search_type in ['content', 'path']:
299 if self.search_type in ['content', 'path']:
300 f_path = res['path'][len(res['repository']):]
300 f_path = res['path'][len(res['repository']):]
301 f_path = f_path.lstrip(os.sep)
301 f_path = f_path.lstrip(os.sep)
302
302
303 if self.search_type == 'content':
303 if self.search_type == 'content':
304 res.update({'content_short_hl': hit.highlights('content'),
304 res.update({'content_short_hl': hit.highlights('content'),
305 'f_path': f_path})
305 'f_path': f_path})
306 elif self.search_type == 'path':
306 elif self.search_type == 'path':
307 res.update({'f_path': f_path})
307 res.update({'f_path': f_path})
308 elif self.search_type == 'message':
308 elif self.search_type == 'message':
309 res.update({'message_hl': hit.highlights('message')})
309 res.update({'message_hl': hit.highlights('message')})
310
310
311 return res
311 return res
@@ -1,230 +1,232 b''
1
1
2 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import base64
20 import base64
21 import logging
21 import logging
22 import urllib.request, urllib.parse, urllib.error
22 import urllib.request
23 import urllib.parse
24 import urllib.error
23 import urllib.parse
25 import urllib.parse
24
26
25 import requests
27 import requests
26 from pyramid.httpexceptions import HTTPNotAcceptable
28 from pyramid.httpexceptions import HTTPNotAcceptable
27
29
28 from rhodecode.lib import rc_cache
30 from rhodecode.lib import rc_cache
29 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware.utils import get_path_info
32 from rhodecode.lib.middleware.utils import get_path_info
31 from rhodecode.lib.utils import is_valid_repo
33 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
34 from rhodecode.lib.str_utils import safe_str, safe_int
35 from rhodecode.lib.type_utils import str2bool
33 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
37 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
38
36
39
37 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
38
41
39
42
40 class SimpleSvnApp(object):
43 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
44 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
45 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
46 'transfer-encoding', 'content-length']
44 rc_extras = {}
47 rc_extras = {}
45
48
46 def __init__(self, config):
49 def __init__(self, config):
47 self.config = config
50 self.config = config
48
51
49 def __call__(self, environ, start_response):
52 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
53 request_headers = self._get_request_headers(environ)
51 data = environ['wsgi.input']
54 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
55 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
56 has_content_length = 'CONTENT_LENGTH' in environ
54
57
55 path_info = self._get_url(
58 path_info = self._get_url(
56 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
59 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
57 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
60 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
58 log.debug('Handling: %s method via `%s`', req_method, path_info)
61 log.debug('Handling: %s method via `%s`', req_method, path_info)
59
62
60 # stream control flag, based on request and content type...
63 # stream control flag, based on request and content type...
61 stream = False
64 stream = False
62
65
63 if req_method in ['MKCOL'] or has_content_length:
66 if req_method in ['MKCOL'] or has_content_length:
64 data_processed = False
67 data_processed = False
65 # read chunk to check if we have txn-with-props
68 # read chunk to check if we have txn-with-props
66 initial_data = data.read(1024)
69 initial_data = data.read(1024)
67 if initial_data.startswith('(create-txn-with-props'):
70 if initial_data.startswith('(create-txn-with-props'):
68 data = initial_data + data.read()
71 data = initial_data + data.read()
69 # store on-the-fly our rc_extra using svn revision properties
72 # store on-the-fly our rc_extra using svn revision properties
70 # those can be read later on in hooks executed so we have a way
73 # those can be read later on in hooks executed so we have a way
71 # to pass in the data into svn hooks
74 # to pass in the data into svn hooks
72 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
75 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
73 rc_data_len = len(rc_data)
76 rc_data_len = len(rc_data)
74 # header defines data length, and serialized data
77 # header defines data length, and serialized data
75 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
78 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
76 data = data[:-2] + skel + '))'
79 data = data[:-2] + skel + '))'
77 data_processed = True
80 data_processed = True
78
81
79 if not data_processed:
82 if not data_processed:
80 # NOTE(johbo): Avoid that we end up with sending the request in chunked
83 # NOTE(johbo): Avoid that we end up with sending the request in chunked
81 # transfer encoding (mainly on Gunicorn). If we know the content
84 # transfer encoding (mainly on Gunicorn). If we know the content
82 # length, then we should transfer the payload in one request.
85 # length, then we should transfer the payload in one request.
83 data = initial_data + data.read()
86 data = initial_data + data.read()
84
87
85 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
88 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
86 # NOTE(marcink): when getting/uploading files we want to STREAM content
89 # NOTE(marcink): when getting/uploading files we want to STREAM content
87 # back to the client/proxy instead of buffering it here...
90 # back to the client/proxy instead of buffering it here...
88 stream = True
91 stream = True
89
92
90 stream = stream
93 stream = stream
91 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
94 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
92 path_info, req_method, stream)
95 path_info, req_method, stream)
93 try:
96 try:
94 response = requests.request(
97 response = requests.request(
95 req_method, path_info,
98 req_method, path_info,
96 data=data, headers=request_headers, stream=stream)
99 data=data, headers=request_headers, stream=stream)
97 except requests.ConnectionError:
100 except requests.ConnectionError:
98 log.exception('ConnectionError occurred for endpoint %s', path_info)
101 log.exception('ConnectionError occurred for endpoint %s', path_info)
99 raise
102 raise
100
103
101 if response.status_code not in [200, 401]:
104 if response.status_code not in [200, 401]:
102 from rhodecode.lib.utils2 import safe_str
103 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
105 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
104 if response.status_code >= 500:
106 if response.status_code >= 500:
105 log.error('Got SVN response:%s with text:`%s`', response, text)
107 log.error('Got SVN response:%s with text:`%s`', response, text)
106 else:
108 else:
107 log.debug('Got SVN response:%s with text:`%s`', response, text)
109 log.debug('Got SVN response:%s with text:`%s`', response, text)
108 else:
110 else:
109 log.debug('got response code: %s', response.status_code)
111 log.debug('got response code: %s', response.status_code)
110
112
111 response_headers = self._get_response_headers(response.headers)
113 response_headers = self._get_response_headers(response.headers)
112
114
113 if response.headers.get('SVN-Txn-name'):
115 if response.headers.get('SVN-Txn-name'):
114 svn_tx_id = response.headers.get('SVN-Txn-name')
116 svn_tx_id = response.headers.get('SVN-Txn-name')
115 txn_id = rc_cache.utils.compute_key_from_params(
117 txn_id = rc_cache.utils.compute_key_from_params(
116 self.config['repository'], svn_tx_id)
118 self.config['repository'], svn_tx_id)
117 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
119 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
118 store_txn_id_data(txn_id, {'port': port})
120 store_txn_id_data(txn_id, {'port': port})
119
121
120 start_response(
122 start_response(
121 '{} {}'.format(response.status_code, response.reason),
123 '{} {}'.format(response.status_code, response.reason),
122 response_headers)
124 response_headers)
123 return response.iter_content(chunk_size=1024)
125 return response.iter_content(chunk_size=1024)
124
126
125 def _get_url(self, svn_http_server, path):
127 def _get_url(self, svn_http_server, path):
126 svn_http_server_url = (svn_http_server or '').rstrip('/')
128 svn_http_server_url = (svn_http_server or '').rstrip('/')
127 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
129 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
128 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
130 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
129 return url_path
131 return url_path
130
132
131 def _get_request_headers(self, environ):
133 def _get_request_headers(self, environ):
132 headers = {}
134 headers = {}
133
135
134 for key in environ:
136 for key in environ:
135 if not key.startswith('HTTP_'):
137 if not key.startswith('HTTP_'):
136 continue
138 continue
137 new_key = key.split('_')
139 new_key = key.split('_')
138 new_key = [k.capitalize() for k in new_key[1:]]
140 new_key = [k.capitalize() for k in new_key[1:]]
139 new_key = '-'.join(new_key)
141 new_key = '-'.join(new_key)
140 headers[new_key] = environ[key]
142 headers[new_key] = environ[key]
141
143
142 if 'CONTENT_TYPE' in environ:
144 if 'CONTENT_TYPE' in environ:
143 headers['Content-Type'] = environ['CONTENT_TYPE']
145 headers['Content-Type'] = environ['CONTENT_TYPE']
144
146
145 if 'CONTENT_LENGTH' in environ:
147 if 'CONTENT_LENGTH' in environ:
146 headers['Content-Length'] = environ['CONTENT_LENGTH']
148 headers['Content-Length'] = environ['CONTENT_LENGTH']
147
149
148 return headers
150 return headers
149
151
150 def _get_response_headers(self, headers):
152 def _get_response_headers(self, headers):
151 headers = [
153 headers = [
152 (h, headers[h])
154 (h, headers[h])
153 for h in headers
155 for h in headers
154 if h.lower() not in self.IGNORED_HEADERS
156 if h.lower() not in self.IGNORED_HEADERS
155 ]
157 ]
156
158
157 return headers
159 return headers
158
160
159
161
160 class DisabledSimpleSvnApp(object):
162 class DisabledSimpleSvnApp(object):
161 def __init__(self, config):
163 def __init__(self, config):
162 self.config = config
164 self.config = config
163
165
164 def __call__(self, environ, start_response):
166 def __call__(self, environ, start_response):
165 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
167 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
166 log.warning(reason)
168 log.warning(reason)
167 return HTTPNotAcceptable(reason)(environ, start_response)
169 return HTTPNotAcceptable(reason)(environ, start_response)
168
170
169
171
170 class SimpleSvn(simplevcs.SimpleVCS):
172 class SimpleSvn(simplevcs.SimpleVCS):
171
173
172 SCM = 'svn'
174 SCM = 'svn'
173 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
175 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
174 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
176 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
175
177
176 def _get_repository_name(self, environ):
178 def _get_repository_name(self, environ):
177 """
179 """
178 Gets repository name out of PATH_INFO header
180 Gets repository name out of PATH_INFO header
179
181
180 :param environ: environ where PATH_INFO is stored
182 :param environ: environ where PATH_INFO is stored
181 """
183 """
182 path = get_path_info(environ).split('!')
184 path = get_path_info(environ).split('!')
183 repo_name = path[0].strip('/')
185 repo_name = path[0].strip('/')
184
186
185 # SVN includes the whole path in it's requests, including
187 # SVN includes the whole path in it's requests, including
186 # subdirectories inside the repo. Therefore we have to search for
188 # subdirectories inside the repo. Therefore we have to search for
187 # the repo root directory.
189 # the repo root directory.
188 if not is_valid_repo(
190 if not is_valid_repo(
189 repo_name, self.base_path, explicit_scm=self.SCM):
191 repo_name, self.base_path, explicit_scm=self.SCM):
190 current_path = ''
192 current_path = ''
191 for component in repo_name.split('/'):
193 for component in repo_name.split('/'):
192 current_path += component
194 current_path += component
193 if is_valid_repo(
195 if is_valid_repo(
194 current_path, self.base_path, explicit_scm=self.SCM):
196 current_path, self.base_path, explicit_scm=self.SCM):
195 return current_path
197 return current_path
196 current_path += '/'
198 current_path += '/'
197
199
198 return repo_name
200 return repo_name
199
201
200 def _get_action(self, environ):
202 def _get_action(self, environ):
201 return (
203 return (
202 'pull'
204 'pull'
203 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
205 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
204 else 'push')
206 else 'push')
205
207
206 def _should_use_callback_daemon(self, extras, environ, action):
208 def _should_use_callback_daemon(self, extras, environ, action):
207 # only MERGE command triggers hooks, so we don't want to start
209 # only MERGE command triggers hooks, so we don't want to start
208 # hooks server too many times. POST however starts the svn transaction
210 # hooks server too many times. POST however starts the svn transaction
209 # so we also need to run the init of callback daemon of POST
211 # so we also need to run the init of callback daemon of POST
210 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
212 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
211 return True
213 return True
212 return False
214 return False
213
215
214 def _create_wsgi_app(self, repo_path, repo_name, config):
216 def _create_wsgi_app(self, repo_path, repo_name, config):
215 if self._is_svn_enabled():
217 if self._is_svn_enabled():
216 return SimpleSvnApp(config)
218 return SimpleSvnApp(config)
217 # we don't have http proxy enabled return dummy request handler
219 # we don't have http proxy enabled return dummy request handler
218 return DisabledSimpleSvnApp(config)
220 return DisabledSimpleSvnApp(config)
219
221
220 def _is_svn_enabled(self):
222 def _is_svn_enabled(self):
221 conf = self.repo_vcs_config
223 conf = self.repo_vcs_config
222 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
224 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
223
225
224 def _create_config(self, extras, repo_name, scheme='http'):
226 def _create_config(self, extras, repo_name, scheme='http'):
225 conf = self.repo_vcs_config
227 conf = self.repo_vcs_config
226 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
228 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
227 server_url = server_url or self.DEFAULT_HTTP_SERVER
229 server_url = server_url or self.DEFAULT_HTTP_SERVER
228
230
229 extras['subversion_http_server_url'] = server_url
231 extras['subversion_http_server_url'] = server_url
230 return extras
232 return extras
@@ -1,683 +1,689 b''
1
1
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import io
28 import io
29 import logging
29 import logging
30 import importlib
30 import importlib
31 from functools import wraps
31 from functools import wraps
32 from lxml import etree
32 from lxml import etree
33
33
34 import time
34 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
36
37 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import rc_cache
43 from rhodecode.lib import rc_cache
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
45 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.str_utils import safe_bytes
51 from rhodecode.lib.str_utils import safe_bytes
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 from rhodecode.lib.vcs.backends import base
55 from rhodecode.lib.vcs.backends import base
56
56
57 from rhodecode.model import meta
57 from rhodecode.model import meta
58 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.db import User, Repository, PullRequest
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.pull_request import PullRequestModel
61 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 def extract_svn_txn_id(acl_repo_name, data):
66 def extract_svn_txn_id(acl_repo_name, data):
67 """
67 """
68 Helper method for extraction of svn txn_id from submitted XML data during
68 Helper method for extraction of svn txn_id from submitted XML data during
69 POST operations
69 POST operations
70 """
70 """
71 try:
71 try:
72 root = etree.fromstring(data)
72 root = etree.fromstring(data)
73 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 for el in root:
74 for el in root:
75 if el.tag == '{DAV:}source':
75 if el.tag == '{DAV:}source':
76 for sub_el in el:
76 for sub_el in el:
77 if sub_el.tag == '{DAV:}href':
77 if sub_el.tag == '{DAV:}href':
78 match = pat.search(sub_el.text)
78 match = pat.search(sub_el.text)
79 if match:
79 if match:
80 svn_tx_id = match.groupdict()['txn_id']
80 svn_tx_id = match.groupdict()['txn_id']
81 txn_id = rc_cache.utils.compute_key_from_params(
81 txn_id = rc_cache.utils.compute_key_from_params(
82 acl_repo_name, svn_tx_id)
82 acl_repo_name, svn_tx_id)
83 return txn_id
83 return txn_id
84 except Exception:
84 except Exception:
85 log.exception('Failed to extract txn_id')
85 log.exception('Failed to extract txn_id')
86
86
87
87
88 def initialize_generator(factory):
88 def initialize_generator(factory):
89 """
89 """
90 Initializes the returned generator by draining its first element.
90 Initializes the returned generator by draining its first element.
91
91
92 This can be used to give a generator an initializer, which is the code
92 This can be used to give a generator an initializer, which is the code
93 up to the first yield statement. This decorator enforces that the first
93 up to the first yield statement. This decorator enforces that the first
94 produced element has the value ``"__init__"`` to make its special
94 produced element has the value ``"__init__"`` to make its special
95 purpose very explicit in the using code.
95 purpose very explicit in the using code.
96 """
96 """
97
97
98 @wraps(factory)
98 @wraps(factory)
99 def wrapper(*args, **kwargs):
99 def wrapper(*args, **kwargs):
100 gen = factory(*args, **kwargs)
100 gen = factory(*args, **kwargs)
101 try:
101 try:
102 init = next(gen)
102 init = next(gen)
103 except StopIteration:
103 except StopIteration:
104 raise ValueError('Generator must yield at least one element.')
104 raise ValueError('Generator must yield at least one element.')
105 if init != "__init__":
105 if init != "__init__":
106 raise ValueError('First yielded element must be "__init__".')
106 raise ValueError('First yielded element must be "__init__".')
107 return gen
107 return gen
108 return wrapper
108 return wrapper
109
109
110
110
111 class SimpleVCS(object):
111 class SimpleVCS(object):
112 """Common functionality for SCM HTTP handlers."""
112 """Common functionality for SCM HTTP handlers."""
113
113
114 SCM = 'unknown'
114 SCM = 'unknown'
115
115
116 acl_repo_name = None
116 acl_repo_name = None
117 url_repo_name = None
117 url_repo_name = None
118 vcs_repo_name = None
118 vcs_repo_name = None
119 rc_extras = {}
119 rc_extras = {}
120
120
121 # We have to handle requests to shadow repositories different than requests
121 # We have to handle requests to shadow repositories different than requests
122 # to normal repositories. Therefore we have to distinguish them. To do this
122 # to normal repositories. Therefore we have to distinguish them. To do this
123 # we use this regex which will match only on URLs pointing to shadow
123 # we use this regex which will match only on URLs pointing to shadow
124 # repositories.
124 # repositories.
125 shadow_repo_re = re.compile(
125 shadow_repo_re = re.compile(
126 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
127 '(?P<target>{slug_pat})/' # target repo
127 '(?P<target>{slug_pat})/' # target repo
128 'pull-request/(?P<pr_id>\\d+)/' # pull request
128 'pull-request/(?P<pr_id>\\d+)/' # pull request
129 'repository$' # shadow repo
129 'repository$' # shadow repo
130 .format(slug_pat=SLUG_RE.pattern))
130 .format(slug_pat=SLUG_RE.pattern))
131
131
132 def __init__(self, config, registry):
132 def __init__(self, config, registry):
133 self.registry = registry
133 self.registry = registry
134 self.config = config
134 self.config = config
135 # re-populated by specialized middleware
135 # re-populated by specialized middleware
136 self.repo_vcs_config = base.Config()
136 self.repo_vcs_config = base.Config()
137
137
138 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
138 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
139 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
139 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
140
140
141 # authenticate this VCS request using authfunc
141 # authenticate this VCS request using authfunc
142 auth_ret_code_detection = \
142 auth_ret_code_detection = \
143 str2bool(self.config.get('auth_ret_code_detection', False))
143 str2bool(self.config.get('auth_ret_code_detection', False))
144 self.authenticate = BasicAuth(
144 self.authenticate = BasicAuth(
145 '', authenticate, registry, config.get('auth_ret_code'),
145 '', authenticate, registry, config.get('auth_ret_code'),
146 auth_ret_code_detection, rc_realm=realm)
146 auth_ret_code_detection, rc_realm=realm)
147 self.ip_addr = '0.0.0.0'
147 self.ip_addr = '0.0.0.0'
148
148
149 @LazyProperty
149 @LazyProperty
150 def global_vcs_config(self):
150 def global_vcs_config(self):
151 try:
151 try:
152 return VcsSettingsModel().get_ui_settings_as_config_obj()
152 return VcsSettingsModel().get_ui_settings_as_config_obj()
153 except Exception:
153 except Exception:
154 return base.Config()
154 return base.Config()
155
155
156 @property
156 @property
157 def base_path(self):
157 def base_path(self):
158 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
158 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159
159
160 if not settings_path:
160 if not settings_path:
161 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
161 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162
162
163 if not settings_path:
163 if not settings_path:
164 # try, maybe we passed in explicitly as config option
164 # try, maybe we passed in explicitly as config option
165 settings_path = self.config.get('base_path')
165 settings_path = self.config.get('base_path')
166
166
167 if not settings_path:
167 if not settings_path:
168 raise ValueError('FATAL: base_path is empty')
168 raise ValueError('FATAL: base_path is empty')
169 return settings_path
169 return settings_path
170
170
171 def set_repo_names(self, environ):
171 def set_repo_names(self, environ):
172 """
172 """
173 This will populate the attributes acl_repo_name, url_repo_name,
173 This will populate the attributes acl_repo_name, url_repo_name,
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 shadow) repositories all names are equal. In case of requests to a
175 shadow) repositories all names are equal. In case of requests to a
176 shadow repository the acl-name points to the target repo of the pull
176 shadow repository the acl-name points to the target repo of the pull
177 request and the vcs-name points to the shadow repo file system path.
177 request and the vcs-name points to the shadow repo file system path.
178 The url-name is always the URL used by the vcs client program.
178 The url-name is always the URL used by the vcs client program.
179
179
180 Example in case of a shadow repo:
180 Example in case of a shadow repo:
181 acl_repo_name = RepoGroup/MyRepo
181 acl_repo_name = RepoGroup/MyRepo
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 """
184 """
185 # First we set the repo name from URL for all attributes. This is the
185 # First we set the repo name from URL for all attributes. This is the
186 # default if handling normal (non shadow) repo requests.
186 # default if handling normal (non shadow) repo requests.
187 self.url_repo_name = self._get_repository_name(environ)
187 self.url_repo_name = self._get_repository_name(environ)
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.is_shadow_repo = False
189 self.is_shadow_repo = False
190
190
191 # Check if this is a request to a shadow repository.
191 # Check if this is a request to a shadow repository.
192 match = self.shadow_repo_re.match(self.url_repo_name)
192 match = self.shadow_repo_re.match(self.url_repo_name)
193 if match:
193 if match:
194 match_dict = match.groupdict()
194 match_dict = match.groupdict()
195
195
196 # Build acl repo name from regex match.
196 # Build acl repo name from regex match.
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
197 acl_repo_name = safe_str('{groups}{target}'.format(
198 groups=match_dict['groups'] or '',
198 groups=match_dict['groups'] or '',
199 target=match_dict['target']))
199 target=match_dict['target']))
200
200
201 # Retrieve pull request instance by ID from regex match.
201 # Retrieve pull request instance by ID from regex match.
202 pull_request = PullRequest.get(match_dict['pr_id'])
202 pull_request = PullRequest.get(match_dict['pr_id'])
203
203
204 # Only proceed if we got a pull request and if acl repo name from
204 # Only proceed if we got a pull request and if acl repo name from
205 # URL equals the target repo name of the pull request.
205 # URL equals the target repo name of the pull request.
206 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
206 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
207
207
208 # Get file system path to shadow repository.
208 # Get file system path to shadow repository.
209 workspace_id = PullRequestModel()._workspace_id(pull_request)
209 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
210 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
211
211
212 # Store names for later usage.
212 # Store names for later usage.
213 self.vcs_repo_name = vcs_repo_name
213 self.vcs_repo_name = vcs_repo_name
214 self.acl_repo_name = acl_repo_name
214 self.acl_repo_name = acl_repo_name
215 self.is_shadow_repo = True
215 self.is_shadow_repo = True
216
216
217 log.debug('Setting all VCS repository names: %s', {
217 log.debug('Setting all VCS repository names: %s', {
218 'acl_repo_name': self.acl_repo_name,
218 'acl_repo_name': self.acl_repo_name,
219 'url_repo_name': self.url_repo_name,
219 'url_repo_name': self.url_repo_name,
220 'vcs_repo_name': self.vcs_repo_name,
220 'vcs_repo_name': self.vcs_repo_name,
221 })
221 })
222
222
223 @property
223 @property
224 def scm_app(self):
224 def scm_app(self):
225 custom_implementation = self.config['vcs.scm_app_implementation']
225 custom_implementation = self.config['vcs.scm_app_implementation']
226 if custom_implementation == 'http':
226 if custom_implementation == 'http':
227 log.debug('Using HTTP implementation of scm app.')
227 log.debug('Using HTTP implementation of scm app.')
228 scm_app_impl = scm_app_http
228 scm_app_impl = scm_app_http
229 else:
229 else:
230 log.debug('Using custom implementation of scm_app: "{}"'.format(
230 log.debug('Using custom implementation of scm_app: "{}"'.format(
231 custom_implementation))
231 custom_implementation))
232 scm_app_impl = importlib.import_module(custom_implementation)
232 scm_app_impl = importlib.import_module(custom_implementation)
233 return scm_app_impl
233 return scm_app_impl
234
234
235 def _get_by_id(self, repo_name):
235 def _get_by_id(self, repo_name):
236 """
236 """
237 Gets a special pattern _<ID> from clone url and tries to replace it
237 Gets a special pattern _<ID> from clone url and tries to replace it
238 with a repository_name for support of _<ID> non changeable urls
238 with a repository_name for support of _<ID> non changeable urls
239 """
239 """
240
240
241 data = repo_name.split('/')
241 data = repo_name.split('/')
242 if len(data) >= 2:
242 if len(data) >= 2:
243 from rhodecode.model.repo import RepoModel
243 from rhodecode.model.repo import RepoModel
244 by_id_match = RepoModel().get_repo_by_id(repo_name)
244 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 if by_id_match:
245 if by_id_match:
246 data[1] = by_id_match.repo_name
246 data[1] = by_id_match.repo_name
247
247
248 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
248 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
249 # and we use this data
249 # and we use this data
250 maybe_new_path = '/'.join(data)
250 maybe_new_path = '/'.join(data)
251 return safe_bytes(maybe_new_path).decode('latin1')
251 return safe_bytes(maybe_new_path).decode('latin1')
252
252
253 def _invalidate_cache(self, repo_name):
253 def _invalidate_cache(self, repo_name):
254 """
254 """
255 Set's cache for this repository for invalidation on next access
255 Set's cache for this repository for invalidation on next access
256
256
257 :param repo_name: full repo name, also a cache key
257 :param repo_name: full repo name, also a cache key
258 """
258 """
259 ScmModel().mark_for_invalidation(repo_name)
259 ScmModel().mark_for_invalidation(repo_name)
260
260
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 db_repo = Repository.get_by_repo_name(repo_name)
262 db_repo = Repository.get_by_repo_name(repo_name)
263 if not db_repo:
263 if not db_repo:
264 log.debug('Repository `%s` not found inside the database.',
264 log.debug('Repository `%s` not found inside the database.',
265 repo_name)
265 repo_name)
266 return False
266 return False
267
267
268 if db_repo.repo_type != scm_type:
268 if db_repo.repo_type != scm_type:
269 log.warning(
269 log.warning(
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 repo_name, db_repo.repo_type, scm_type)
271 repo_name, db_repo.repo_type, scm_type)
272 return False
272 return False
273
273
274 config = db_repo._config
274 config = db_repo._config
275 config.set('extensions', 'largefiles', '')
275 config.set('extensions', 'largefiles', '')
276 return is_valid_repo(
276 return is_valid_repo(
277 repo_name, base_path,
277 repo_name, base_path,
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279
279
280 def valid_and_active_user(self, user):
280 def valid_and_active_user(self, user):
281 """
281 """
282 Checks if that user is not empty, and if it's actually object it checks
282 Checks if that user is not empty, and if it's actually object it checks
283 if he's active.
283 if he's active.
284
284
285 :param user: user object or None
285 :param user: user object or None
286 :return: boolean
286 :return: boolean
287 """
287 """
288 if user is None:
288 if user is None:
289 return False
289 return False
290
290
291 elif user.active:
291 elif user.active:
292 return True
292 return True
293
293
294 return False
294 return False
295
295
296 @property
296 @property
297 def is_shadow_repo_dir(self):
297 def is_shadow_repo_dir(self):
298 return os.path.isdir(self.vcs_repo_name)
298 return os.path.isdir(self.vcs_repo_name)
299
299
300 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
300 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 """
302 """
303 Checks permissions using action (push/pull) user and repository
303 Checks permissions using action (push/pull) user and repository
304 name. If plugin_cache and ttl is set it will use the plugin which
304 name. If plugin_cache and ttl is set it will use the plugin which
305 authenticated the user to store the cached permissions result for N
305 authenticated the user to store the cached permissions result for N
306 amount of seconds as in cache_ttl
306 amount of seconds as in cache_ttl
307
307
308 :param action: push or pull action
308 :param action: push or pull action
309 :param user: user instance
309 :param user: user instance
310 :param repo_name: repository name
310 :param repo_name: repository name
311 """
311 """
312
312
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 plugin_id, plugin_cache_active, cache_ttl)
314 plugin_id, plugin_cache_active, cache_ttl)
315
315
316 user_id = user.user_id
316 user_id = user.user_id
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319
319
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 expiration_time=cache_ttl,
321 expiration_time=cache_ttl,
322 condition=plugin_cache_active)
322 condition=plugin_cache_active)
323 def compute_perm_vcs(
323 def compute_perm_vcs(
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325
325
326 log.debug('auth: calculating permission access now...')
326 log.debug('auth: calculating permission access now...')
327 # check IP
327 # check IP
328 inherit = user.inherit_default_permissions
328 inherit = user.inherit_default_permissions
329 ip_allowed = AuthUser.check_ip_allowed(
329 ip_allowed = AuthUser.check_ip_allowed(
330 user_id, ip_addr, inherit_from_default=inherit)
330 user_id, ip_addr, inherit_from_default=inherit)
331 if ip_allowed:
331 if ip_allowed:
332 log.info('Access for IP:%s allowed', ip_addr)
332 log.info('Access for IP:%s allowed', ip_addr)
333 else:
333 else:
334 return False
334 return False
335
335
336 if action == 'push':
336 if action == 'push':
337 perms = ('repository.write', 'repository.admin')
337 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
338 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 return False
339 return False
340
340
341 else:
341 else:
342 # any other action need at least read permission
342 # any other action need at least read permission
343 perms = (
343 perms = (
344 'repository.read', 'repository.write', 'repository.admin')
344 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
345 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 return False
346 return False
347
347
348 return True
348 return True
349
349
350 start = time.time()
350 start = time.time()
351 log.debug('Running plugin `%s` permissions check', plugin_id)
351 log.debug('Running plugin `%s` permissions check', plugin_id)
352
352
353 # for environ based auth, password can be empty, but then the validation is
353 # for environ based auth, password can be empty, but then the validation is
354 # on the server that fills in the env data needed for authentication
354 # on the server that fills in the env data needed for authentication
355 perm_result = compute_perm_vcs(
355 perm_result = compute_perm_vcs(
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357
357
358 auth_time = time.time() - start
358 auth_time = time.time() - start
359 log.debug('Permissions for plugin `%s` completed in %.4fs, '
359 log.debug('Permissions for plugin `%s` completed in %.4fs, '
360 'expiration time of fetched cache %.1fs.',
360 'expiration time of fetched cache %.1fs.',
361 plugin_id, auth_time, cache_ttl)
361 plugin_id, auth_time, cache_ttl)
362
362
363 return perm_result
363 return perm_result
364
364
365 def _get_http_scheme(self, environ):
365 def _get_http_scheme(self, environ):
366 try:
366 try:
367 return environ['wsgi.url_scheme']
367 return environ['wsgi.url_scheme']
368 except Exception:
368 except Exception:
369 log.exception('Failed to read http scheme')
369 log.exception('Failed to read http scheme')
370 return 'http'
370 return 'http'
371
371
372 def _check_ssl(self, environ, start_response):
372 def _check_ssl(self, environ, start_response):
373 """
373 """
374 Checks the SSL check flag and returns False if SSL is not present
374 Checks the SSL check flag and returns False if SSL is not present
375 and required True otherwise
375 and required True otherwise
376 """
376 """
377 org_proto = environ['wsgi._org_proto']
377 org_proto = environ['wsgi._org_proto']
378 # check if we have SSL required ! if not it's a bad request !
378 # check if we have SSL required ! if not it's a bad request !
379 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
379 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
380 if require_ssl and org_proto == 'http':
380 if require_ssl and org_proto == 'http':
381 log.debug(
381 log.debug(
382 'Bad request: detected protocol is `%s` and '
382 'Bad request: detected protocol is `%s` and '
383 'SSL/HTTPS is required.', org_proto)
383 'SSL/HTTPS is required.', org_proto)
384 return False
384 return False
385 return True
385 return True
386
386
387 def _get_default_cache_ttl(self):
387 def _get_default_cache_ttl(self):
388 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
388 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
389 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
389 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
390 plugin_settings = plugin.get_settings()
390 plugin_settings = plugin.get_settings()
391 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
391 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
392 plugin_settings) or (False, 0)
392 plugin_settings) or (False, 0)
393 return plugin_cache_active, cache_ttl
393 return plugin_cache_active, cache_ttl
394
394
395 def __call__(self, environ, start_response):
395 def __call__(self, environ, start_response):
396 try:
396 try:
397 return self._handle_request(environ, start_response)
397 return self._handle_request(environ, start_response)
398 except Exception:
398 except Exception:
399 log.exception("Exception while handling request")
399 log.exception("Exception while handling request")
400 appenlight.track_exception(environ)
400 appenlight.track_exception(environ)
401 return HTTPInternalServerError()(environ, start_response)
401 return HTTPInternalServerError()(environ, start_response)
402 finally:
402 finally:
403 meta.Session.remove()
403 meta.Session.remove()
404
404
405 def _handle_request(self, environ, start_response):
405 def _handle_request(self, environ, start_response):
406 if not self._check_ssl(environ, start_response):
406 if not self._check_ssl(environ, start_response):
407 reason = ('SSL required, while RhodeCode was unable '
407 reason = ('SSL required, while RhodeCode was unable '
408 'to detect this as SSL request')
408 'to detect this as SSL request')
409 log.debug('User not allowed to proceed, %s', reason)
409 log.debug('User not allowed to proceed, %s', reason)
410 return HTTPNotAcceptable(reason)(environ, start_response)
410 return HTTPNotAcceptable(reason)(environ, start_response)
411
411
412 if not self.url_repo_name:
412 if not self.url_repo_name:
413 log.warning('Repository name is empty: %s', self.url_repo_name)
413 log.warning('Repository name is empty: %s', self.url_repo_name)
414 # failed to get repo name, we fail now
414 # failed to get repo name, we fail now
415 return HTTPNotFound()(environ, start_response)
415 return HTTPNotFound()(environ, start_response)
416 log.debug('Extracted repo name is %s', self.url_repo_name)
416 log.debug('Extracted repo name is %s', self.url_repo_name)
417
417
418 ip_addr = get_ip_addr(environ)
418 ip_addr = get_ip_addr(environ)
419 user_agent = get_user_agent(environ)
419 user_agent = get_user_agent(environ)
420 username = None
420 username = None
421
421
422 # skip passing error to error controller
422 # skip passing error to error controller
423 environ['pylons.status_code_redirect'] = True
423 environ['pylons.status_code_redirect'] = True
424
424
425 # ======================================================================
425 # ======================================================================
426 # GET ACTION PULL or PUSH
426 # GET ACTION PULL or PUSH
427 # ======================================================================
427 # ======================================================================
428 action = self._get_action(environ)
428 action = self._get_action(environ)
429
429
430 # ======================================================================
430 # ======================================================================
431 # Check if this is a request to a shadow repository of a pull request.
431 # Check if this is a request to a shadow repository of a pull request.
432 # In this case only pull action is allowed.
432 # In this case only pull action is allowed.
433 # ======================================================================
433 # ======================================================================
434 if self.is_shadow_repo and action != 'pull':
434 if self.is_shadow_repo and action != 'pull':
435 reason = 'Only pull action is allowed for shadow repositories.'
435 reason = 'Only pull action is allowed for shadow repositories.'
436 log.debug('User not allowed to proceed, %s', reason)
436 log.debug('User not allowed to proceed, %s', reason)
437 return HTTPNotAcceptable(reason)(environ, start_response)
437 return HTTPNotAcceptable(reason)(environ, start_response)
438
438
439 # Check if the shadow repo actually exists, in case someone refers
439 # Check if the shadow repo actually exists, in case someone refers
440 # to it, and it has been deleted because of successful merge.
440 # to it, and it has been deleted because of successful merge.
441 if self.is_shadow_repo and not self.is_shadow_repo_dir:
441 if self.is_shadow_repo and not self.is_shadow_repo_dir:
442 log.debug(
442 log.debug(
443 'Shadow repo detected, and shadow repo dir `%s` is missing',
443 'Shadow repo detected, and shadow repo dir `%s` is missing',
444 self.is_shadow_repo_dir)
444 self.is_shadow_repo_dir)
445 return HTTPNotFound()(environ, start_response)
445 return HTTPNotFound()(environ, start_response)
446
446
447 # ======================================================================
447 # ======================================================================
448 # CHECK ANONYMOUS PERMISSION
448 # CHECK ANONYMOUS PERMISSION
449 # ======================================================================
449 # ======================================================================
450 detect_force_push = False
450 detect_force_push = False
451 check_branch_perms = False
451 check_branch_perms = False
452 if action in ['pull', 'push']:
452 if action in ['pull', 'push']:
453 user_obj = anonymous_user = User.get_default_user()
453 user_obj = anonymous_user = User.get_default_user()
454 auth_user = user_obj.AuthUser()
454 auth_user = user_obj.AuthUser()
455 username = anonymous_user.username
455 username = anonymous_user.username
456 if anonymous_user.active:
456 if anonymous_user.active:
457 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
457 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
458 # ONLY check permissions if the user is activated
458 # ONLY check permissions if the user is activated
459 anonymous_perm = self._check_permission(
459 anonymous_perm = self._check_permission(
460 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
460 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
461 plugin_id='anonymous_access',
461 plugin_id='anonymous_access',
462 plugin_cache_active=plugin_cache_active,
462 plugin_cache_active=plugin_cache_active,
463 cache_ttl=cache_ttl,
463 cache_ttl=cache_ttl,
464 )
464 )
465 else:
465 else:
466 anonymous_perm = False
466 anonymous_perm = False
467
467
468 if not anonymous_user.active or not anonymous_perm:
468 if not anonymous_user.active or not anonymous_perm:
469 if not anonymous_user.active:
469 if not anonymous_user.active:
470 log.debug('Anonymous access is disabled, running '
470 log.debug('Anonymous access is disabled, running '
471 'authentication')
471 'authentication')
472
472
473 if not anonymous_perm:
473 if not anonymous_perm:
474 log.debug('Not enough credentials to access this '
474 log.debug('Not enough credentials to access repo: `%s` '
475 'repository as anonymous user')
475 'repository as anonymous user', self.acl_repo_name)
476
476
477
477 username = None
478 username = None
478 # ==============================================================
479 # ==============================================================
479 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
480 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
480 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
481 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
481 # ==============================================================
482 # ==============================================================
482
483
483 # try to auth based on environ, container auth methods
484 # try to auth based on environ, container auth methods
484 log.debug('Running PRE-AUTH for container based authentication')
485 log.debug('Running PRE-AUTH for container|headers based authentication')
486
487 # headers auth, by just reading special headers and bypass the auth with user/passwd
485 pre_auth = authenticate(
488 pre_auth = authenticate(
486 '', '', environ, VCS_TYPE, registry=self.registry,
489 '', '', environ, VCS_TYPE, registry=self.registry,
487 acl_repo_name=self.acl_repo_name)
490 acl_repo_name=self.acl_repo_name)
491
488 if pre_auth and pre_auth.get('username'):
492 if pre_auth and pre_auth.get('username'):
489 username = pre_auth['username']
493 username = pre_auth['username']
490 log.debug('PRE-AUTH got %s as username', username)
494 log.debug('PRE-AUTH got `%s` as username', username)
491 if pre_auth:
495 if pre_auth:
492 log.debug('PRE-AUTH successful from %s',
496 log.debug('PRE-AUTH successful from %s',
493 pre_auth.get('auth_data', {}).get('_plugin'))
497 pre_auth.get('auth_data', {}).get('_plugin'))
494
498
495 # If not authenticated by the container, running basic auth
499 # If not authenticated by the container, running basic auth
496 # before inject the calling repo_name for special scope checks
500 # before inject the calling repo_name for special scope checks
497 self.authenticate.acl_repo_name = self.acl_repo_name
501 self.authenticate.acl_repo_name = self.acl_repo_name
498
502
499 plugin_cache_active, cache_ttl = False, 0
503 plugin_cache_active, cache_ttl = False, 0
500 plugin = None
504 plugin = None
505
506 # regular auth chain
501 if not username:
507 if not username:
502 self.authenticate.realm = self.authenticate.get_rc_realm()
508 self.authenticate.realm = self.authenticate.get_rc_realm()
503
509
504 try:
510 try:
505 auth_result = self.authenticate(environ)
511 auth_result = self.authenticate(environ)
506 except (UserCreationError, NotAllowedToCreateUserError) as e:
512 except (UserCreationError, NotAllowedToCreateUserError) as e:
507 log.error(e)
513 log.error(e)
508 reason = safe_str(e)
514 reason = safe_str(e)
509 return HTTPNotAcceptable(reason)(environ, start_response)
515 return HTTPNotAcceptable(reason)(environ, start_response)
510
516
511 if isinstance(auth_result, dict):
517 if isinstance(auth_result, dict):
512 AUTH_TYPE.update(environ, 'basic')
518 AUTH_TYPE.update(environ, 'basic')
513 REMOTE_USER.update(environ, auth_result['username'])
519 REMOTE_USER.update(environ, auth_result['username'])
514 username = auth_result['username']
520 username = auth_result['username']
515 plugin = auth_result.get('auth_data', {}).get('_plugin')
521 plugin = auth_result.get('auth_data', {}).get('_plugin')
516 log.info(
522 log.info(
517 'MAIN-AUTH successful for user `%s` from %s plugin',
523 'MAIN-AUTH successful for user `%s` from %s plugin',
518 username, plugin)
524 username, plugin)
519
525
520 plugin_cache_active, cache_ttl = auth_result.get(
526 plugin_cache_active, cache_ttl = auth_result.get(
521 'auth_data', {}).get('_ttl_cache') or (False, 0)
527 'auth_data', {}).get('_ttl_cache') or (False, 0)
522 else:
528 else:
523 return auth_result.wsgi_application(environ, start_response)
529 return auth_result.wsgi_application(environ, start_response)
524
530
525 # ==============================================================
531 # ==============================================================
526 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
532 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
527 # ==============================================================
533 # ==============================================================
528 user = User.get_by_username(username)
534 user = User.get_by_username(username)
529 if not self.valid_and_active_user(user):
535 if not self.valid_and_active_user(user):
530 return HTTPForbidden()(environ, start_response)
536 return HTTPForbidden()(environ, start_response)
531 username = user.username
537 username = user.username
532 user_id = user.user_id
538 user_id = user.user_id
533
539
534 # check user attributes for password change flag
540 # check user attributes for password change flag
535 user_obj = user
541 user_obj = user
536 auth_user = user_obj.AuthUser()
542 auth_user = user_obj.AuthUser()
537 if user_obj and user_obj.username != User.DEFAULT_USER and \
543 if user_obj and user_obj.username != User.DEFAULT_USER and \
538 user_obj.user_data.get('force_password_change'):
544 user_obj.user_data.get('force_password_change'):
539 reason = 'password change required'
545 reason = 'password change required'
540 log.debug('User not allowed to authenticate, %s', reason)
546 log.debug('User not allowed to authenticate, %s', reason)
541 return HTTPNotAcceptable(reason)(environ, start_response)
547 return HTTPNotAcceptable(reason)(environ, start_response)
542
548
543 # check permissions for this repository
549 # check permissions for this repository
544 perm = self._check_permission(
550 perm = self._check_permission(
545 action, user, auth_user, self.acl_repo_name, ip_addr,
551 action, user, auth_user, self.acl_repo_name, ip_addr,
546 plugin, plugin_cache_active, cache_ttl)
552 plugin, plugin_cache_active, cache_ttl)
547 if not perm:
553 if not perm:
548 return HTTPForbidden()(environ, start_response)
554 return HTTPForbidden()(environ, start_response)
549 environ['rc_auth_user_id'] = user_id
555 environ['rc_auth_user_id'] = str(user_id)
550
556
551 if action == 'push':
557 if action == 'push':
552 perms = auth_user.get_branch_permissions(self.acl_repo_name)
558 perms = auth_user.get_branch_permissions(self.acl_repo_name)
553 if perms:
559 if perms:
554 check_branch_perms = True
560 check_branch_perms = True
555 detect_force_push = True
561 detect_force_push = True
556
562
557 # extras are injected into UI object and later available
563 # extras are injected into UI object and later available
558 # in hooks executed by RhodeCode
564 # in hooks executed by RhodeCode
559 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
565 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
560
566
561 extras = vcs_operation_context(
567 extras = vcs_operation_context(
562 environ, repo_name=self.acl_repo_name, username=username,
568 environ, repo_name=self.acl_repo_name, username=username,
563 action=action, scm=self.SCM, check_locking=check_locking,
569 action=action, scm=self.SCM, check_locking=check_locking,
564 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
570 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
565 detect_force_push=detect_force_push
571 detect_force_push=detect_force_push
566 )
572 )
567
573
568 # ======================================================================
574 # ======================================================================
569 # REQUEST HANDLING
575 # REQUEST HANDLING
570 # ======================================================================
576 # ======================================================================
571 repo_path = os.path.join(
577 repo_path = os.path.join(
572 safe_str(self.base_path), safe_str(self.vcs_repo_name))
578 safe_str(self.base_path), safe_str(self.vcs_repo_name))
573 log.debug('Repository path is %s', repo_path)
579 log.debug('Repository path is %s', repo_path)
574
580
575 fix_PATH()
581 fix_PATH()
576
582
577 log.info(
583 log.info(
578 '%s action on %s repo "%s" by "%s" from %s %s',
584 '%s action on %s repo "%s" by "%s" from %s %s',
579 action, self.SCM, safe_str(self.url_repo_name),
585 action, self.SCM, safe_str(self.url_repo_name),
580 safe_str(username), ip_addr, user_agent)
586 safe_str(username), ip_addr, user_agent)
581
587
582 return self._generate_vcs_response(
588 return self._generate_vcs_response(
583 environ, start_response, repo_path, extras, action)
589 environ, start_response, repo_path, extras, action)
584
590
585 @initialize_generator
591 @initialize_generator
586 def _generate_vcs_response(
592 def _generate_vcs_response(
587 self, environ, start_response, repo_path, extras, action):
593 self, environ, start_response, repo_path, extras, action):
588 """
594 """
589 Returns a generator for the response content.
595 Returns a generator for the response content.
590
596
591 This method is implemented as a generator, so that it can trigger
597 This method is implemented as a generator, so that it can trigger
592 the cache validation after all content sent back to the client. It
598 the cache validation after all content sent back to the client. It
593 also handles the locking exceptions which will be triggered when
599 also handles the locking exceptions which will be triggered when
594 the first chunk is produced by the underlying WSGI application.
600 the first chunk is produced by the underlying WSGI application.
595 """
601 """
596 txn_id = ''
602 txn_id = ''
597 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
603 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
598 # case for SVN, we want to re-use the callback daemon port
604 # case for SVN, we want to re-use the callback daemon port
599 # so we use the txn_id, for this we peek the body, and still save
605 # so we use the txn_id, for this we peek the body, and still save
600 # it as wsgi.input
606 # it as wsgi.input
601 data = environ['wsgi.input'].read()
607 data = environ['wsgi.input'].read()
602 environ['wsgi.input'] = io.StringIO(data)
608 environ['wsgi.input'] = io.StringIO(data)
603 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
609 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
604
610
605 callback_daemon, extras = self._prepare_callback_daemon(
611 callback_daemon, extras = self._prepare_callback_daemon(
606 extras, environ, action, txn_id=txn_id)
612 extras, environ, action, txn_id=txn_id)
607 log.debug('HOOKS extras is %s', extras)
613 log.debug('HOOKS extras is %s', extras)
608
614
609 http_scheme = self._get_http_scheme(environ)
615 http_scheme = self._get_http_scheme(environ)
610
616
611 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
617 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
612 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
618 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
613 with callback_daemon:
619 with callback_daemon:
614 app.rc_extras = extras
620 app.rc_extras = extras
615
621
616 try:
622 try:
617 response = app(environ, start_response)
623 response = app(environ, start_response)
618 finally:
624 finally:
619 # This statement works together with the decorator
625 # This statement works together with the decorator
620 # "initialize_generator" above. The decorator ensures that
626 # "initialize_generator" above. The decorator ensures that
621 # we hit the first yield statement before the generator is
627 # we hit the first yield statement before the generator is
622 # returned back to the WSGI server. This is needed to
628 # returned back to the WSGI server. This is needed to
623 # ensure that the call to "app" above triggers the
629 # ensure that the call to "app" above triggers the
624 # needed callback to "start_response" before the
630 # needed callback to "start_response" before the
625 # generator is actually used.
631 # generator is actually used.
626 yield "__init__"
632 yield "__init__"
627
633
628 # iter content
634 # iter content
629 for chunk in response:
635 for chunk in response:
630 yield chunk
636 yield chunk
631
637
632 try:
638 try:
633 # invalidate cache on push
639 # invalidate cache on push
634 if action == 'push':
640 if action == 'push':
635 self._invalidate_cache(self.url_repo_name)
641 self._invalidate_cache(self.url_repo_name)
636 finally:
642 finally:
637 meta.Session.remove()
643 meta.Session.remove()
638
644
639 def _get_repository_name(self, environ):
645 def _get_repository_name(self, environ):
640 """Get repository name out of the environmnent
646 """Get repository name out of the environmnent
641
647
642 :param environ: WSGI environment
648 :param environ: WSGI environment
643 """
649 """
644 raise NotImplementedError()
650 raise NotImplementedError()
645
651
646 def _get_action(self, environ):
652 def _get_action(self, environ):
647 """Map request commands into a pull or push command.
653 """Map request commands into a pull or push command.
648
654
649 :param environ: WSGI environment
655 :param environ: WSGI environment
650 """
656 """
651 raise NotImplementedError()
657 raise NotImplementedError()
652
658
653 def _create_wsgi_app(self, repo_path, repo_name, config):
659 def _create_wsgi_app(self, repo_path, repo_name, config):
654 """Return the WSGI app that will finally handle the request."""
660 """Return the WSGI app that will finally handle the request."""
655 raise NotImplementedError()
661 raise NotImplementedError()
656
662
657 def _create_config(self, extras, repo_name, scheme='http'):
663 def _create_config(self, extras, repo_name, scheme='http'):
658 """Create a safe config representation."""
664 """Create a safe config representation."""
659 raise NotImplementedError()
665 raise NotImplementedError()
660
666
661 def _should_use_callback_daemon(self, extras, environ, action):
667 def _should_use_callback_daemon(self, extras, environ, action):
662 if extras.get('is_shadow_repo'):
668 if extras.get('is_shadow_repo'):
663 # we don't want to execute hooks, and callback daemon for shadow repos
669 # we don't want to execute hooks, and callback daemon for shadow repos
664 return False
670 return False
665 return True
671 return True
666
672
667 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
673 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
668 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
674 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
669 if not self._should_use_callback_daemon(extras, environ, action):
675 if not self._should_use_callback_daemon(extras, environ, action):
670 # disable callback daemon for actions that don't require it
676 # disable callback daemon for actions that don't require it
671 direct_calls = True
677 direct_calls = True
672
678
673 return prepare_callback_daemon(
679 return prepare_callback_daemon(
674 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
680 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
675 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
681 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
676
682
677
683
678 def _should_check_locking(query_string):
684 def _should_check_locking(query_string):
679 # this is kind of hacky, but due to how mercurial handles client-server
685 # this is kind of hacky, but due to how mercurial handles client-server
680 # server see all operation on commit; bookmarks, phases and
686 # server see all operation on commit; bookmarks, phases and
681 # obsolescence marker in different transaction, we don't want to check
687 # obsolescence marker in different transaction, we don't want to check
682 # locking on those
688 # locking on those
683 return query_string not in ['cmd=listkeys']
689 return query_string not in ['cmd=listkeys']
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now