##// 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 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import datetime
26 26 from pyramid.interfaces import IRoutesMapper
27 27
28 28 from pyramid.httpexceptions import HTTPFound
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 34 from rhodecode import events
35 35
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib.utils2 import aslist, safe_unicode
39 from rhodecode.lib.utils2 import aslist, safe_str
40 40 from rhodecode.model.db import (
41 41 or_, coalesce, User, UserIpMap, UserSshKeys)
42 42 from rhodecode.model.forms import (
43 43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.permission import PermissionModel
46 46 from rhodecode.model.settings import SettingsModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 PermissionModel().set_global_permission_choices(
56 56 c, gettext_translator=self.request.translate)
57 57 return c
58 58
59 59 @LoginRequired()
60 60 @HasPermissionAllDecorator('hg.admin')
61 61 def permissions_application(self):
62 62 c = self.load_default_context()
63 63 c.active = 'application'
64 64
65 65 c.user = User.get_default_user(refresh=True)
66 66
67 67 app_settings = c.rc_config
68 68
69 69 defaults = {
70 70 'anonymous': c.user.active,
71 71 'default_register_message': app_settings.get(
72 72 'rhodecode_register_message')
73 73 }
74 74 defaults.update(c.user.get_default_perms())
75 75
76 76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 77 self._get_template_context(c), self.request)
78 78 html = formencode.htmlfill.render(
79 79 data,
80 80 defaults=defaults,
81 81 encoding="UTF-8",
82 82 force_defaults=False
83 83 )
84 84 return Response(html)
85 85
86 86 @LoginRequired()
87 87 @HasPermissionAllDecorator('hg.admin')
88 88 @CSRFRequired()
89 89 def permissions_application_update(self):
90 90 _ = self.request.translate
91 91 c = self.load_default_context()
92 92 c.active = 'application'
93 93
94 94 _form = ApplicationPermissionsForm(
95 95 self.request.translate,
96 96 [x[0] for x in c.register_choices],
97 97 [x[0] for x in c.password_reset_choices],
98 98 [x[0] for x in c.extern_activate_choices])()
99 99
100 100 try:
101 101 form_result = _form.to_python(dict(self.request.POST))
102 102 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 103 PermissionModel().update_application_permissions(form_result)
104 104
105 105 settings = [
106 106 ('register_message', 'default_register_message'),
107 107 ]
108 108 for setting, form_key in settings:
109 109 sett = SettingsModel().create_or_update_setting(
110 110 setting, form_result[form_key])
111 111 Session().add(sett)
112 112
113 113 Session().commit()
114 114 h.flash(_('Application permissions updated successfully'),
115 115 category='success')
116 116
117 117 except formencode.Invalid as errors:
118 118 defaults = errors.value
119 119
120 120 data = render(
121 121 'rhodecode:templates/admin/permissions/permissions.mako',
122 122 self._get_template_context(c), self.request)
123 123 html = formencode.htmlfill.render(
124 124 data,
125 125 defaults=defaults,
126 126 errors=errors.unpack_errors() or {},
127 127 prefix_error=False,
128 128 encoding="UTF-8",
129 129 force_defaults=False
130 130 )
131 131 return Response(html)
132 132
133 133 except Exception:
134 134 log.exception("Exception during update of permissions")
135 135 h.flash(_('Error occurred during update of permissions'),
136 136 category='error')
137 137
138 138 affected_user_ids = [User.get_default_user_id()]
139 139 PermissionModel().trigger_permission_flush(affected_user_ids)
140 140
141 141 raise HTTPFound(h.route_path('admin_permissions_application'))
142 142
143 143 @LoginRequired()
144 144 @HasPermissionAllDecorator('hg.admin')
145 145 def permissions_objects(self):
146 146 c = self.load_default_context()
147 147 c.active = 'objects'
148 148
149 149 c.user = User.get_default_user(refresh=True)
150 150 defaults = {}
151 151 defaults.update(c.user.get_default_perms())
152 152
153 153 data = render(
154 154 'rhodecode:templates/admin/permissions/permissions.mako',
155 155 self._get_template_context(c), self.request)
156 156 html = formencode.htmlfill.render(
157 157 data,
158 158 defaults=defaults,
159 159 encoding="UTF-8",
160 160 force_defaults=False
161 161 )
162 162 return Response(html)
163 163
164 164 @LoginRequired()
165 165 @HasPermissionAllDecorator('hg.admin')
166 166 @CSRFRequired()
167 167 def permissions_objects_update(self):
168 168 _ = self.request.translate
169 169 c = self.load_default_context()
170 170 c.active = 'objects'
171 171
172 172 _form = ObjectPermissionsForm(
173 173 self.request.translate,
174 174 [x[0] for x in c.repo_perms_choices],
175 175 [x[0] for x in c.group_perms_choices],
176 176 [x[0] for x in c.user_group_perms_choices],
177 177 )()
178 178
179 179 try:
180 180 form_result = _form.to_python(dict(self.request.POST))
181 181 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 182 PermissionModel().update_object_permissions(form_result)
183 183
184 184 Session().commit()
185 185 h.flash(_('Object permissions updated successfully'),
186 186 category='success')
187 187
188 188 except formencode.Invalid as errors:
189 189 defaults = errors.value
190 190
191 191 data = render(
192 192 'rhodecode:templates/admin/permissions/permissions.mako',
193 193 self._get_template_context(c), self.request)
194 194 html = formencode.htmlfill.render(
195 195 data,
196 196 defaults=defaults,
197 197 errors=errors.unpack_errors() or {},
198 198 prefix_error=False,
199 199 encoding="UTF-8",
200 200 force_defaults=False
201 201 )
202 202 return Response(html)
203 203 except Exception:
204 204 log.exception("Exception during update of permissions")
205 205 h.flash(_('Error occurred during update of permissions'),
206 206 category='error')
207 207
208 208 affected_user_ids = [User.get_default_user_id()]
209 209 PermissionModel().trigger_permission_flush(affected_user_ids)
210 210
211 211 raise HTTPFound(h.route_path('admin_permissions_object'))
212 212
213 213 @LoginRequired()
214 214 @HasPermissionAllDecorator('hg.admin')
215 215 def permissions_branch(self):
216 216 c = self.load_default_context()
217 217 c.active = 'branch'
218 218
219 219 c.user = User.get_default_user(refresh=True)
220 220 defaults = {}
221 221 defaults.update(c.user.get_default_perms())
222 222
223 223 data = render(
224 224 'rhodecode:templates/admin/permissions/permissions.mako',
225 225 self._get_template_context(c), self.request)
226 226 html = formencode.htmlfill.render(
227 227 data,
228 228 defaults=defaults,
229 229 encoding="UTF-8",
230 230 force_defaults=False
231 231 )
232 232 return Response(html)
233 233
234 234 @LoginRequired()
235 235 @HasPermissionAllDecorator('hg.admin')
236 236 def permissions_global(self):
237 237 c = self.load_default_context()
238 238 c.active = 'global'
239 239
240 240 c.user = User.get_default_user(refresh=True)
241 241 defaults = {}
242 242 defaults.update(c.user.get_default_perms())
243 243
244 244 data = render(
245 245 'rhodecode:templates/admin/permissions/permissions.mako',
246 246 self._get_template_context(c), self.request)
247 247 html = formencode.htmlfill.render(
248 248 data,
249 249 defaults=defaults,
250 250 encoding="UTF-8",
251 251 force_defaults=False
252 252 )
253 253 return Response(html)
254 254
255 255 @LoginRequired()
256 256 @HasPermissionAllDecorator('hg.admin')
257 257 @CSRFRequired()
258 258 def permissions_global_update(self):
259 259 _ = self.request.translate
260 260 c = self.load_default_context()
261 261 c.active = 'global'
262 262
263 263 _form = UserPermissionsForm(
264 264 self.request.translate,
265 265 [x[0] for x in c.repo_create_choices],
266 266 [x[0] for x in c.repo_create_on_write_choices],
267 267 [x[0] for x in c.repo_group_create_choices],
268 268 [x[0] for x in c.user_group_create_choices],
269 269 [x[0] for x in c.fork_choices],
270 270 [x[0] for x in c.inherit_default_permission_choices])()
271 271
272 272 try:
273 273 form_result = _form.to_python(dict(self.request.POST))
274 274 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 275 PermissionModel().update_user_permissions(form_result)
276 276
277 277 Session().commit()
278 278 h.flash(_('Global permissions updated successfully'),
279 279 category='success')
280 280
281 281 except formencode.Invalid as errors:
282 282 defaults = errors.value
283 283
284 284 data = render(
285 285 'rhodecode:templates/admin/permissions/permissions.mako',
286 286 self._get_template_context(c), self.request)
287 287 html = formencode.htmlfill.render(
288 288 data,
289 289 defaults=defaults,
290 290 errors=errors.unpack_errors() or {},
291 291 prefix_error=False,
292 292 encoding="UTF-8",
293 293 force_defaults=False
294 294 )
295 295 return Response(html)
296 296 except Exception:
297 297 log.exception("Exception during update of permissions")
298 298 h.flash(_('Error occurred during update of permissions'),
299 299 category='error')
300 300
301 301 affected_user_ids = [User.get_default_user_id()]
302 302 PermissionModel().trigger_permission_flush(affected_user_ids)
303 303
304 304 raise HTTPFound(h.route_path('admin_permissions_global'))
305 305
306 306 @LoginRequired()
307 307 @HasPermissionAllDecorator('hg.admin')
308 308 def permissions_ips(self):
309 309 c = self.load_default_context()
310 310 c.active = 'ips'
311 311
312 312 c.user = User.get_default_user(refresh=True)
313 313 c.user_ip_map = (
314 314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315 315
316 316 return self._get_template_context(c)
317 317
318 318 @LoginRequired()
319 319 @HasPermissionAllDecorator('hg.admin')
320 320 def permissions_overview(self):
321 321 c = self.load_default_context()
322 322 c.active = 'perms'
323 323
324 324 c.user = User.get_default_user(refresh=True)
325 325 c.perm_user = c.user.AuthUser()
326 326 return self._get_template_context(c)
327 327
328 328 @LoginRequired()
329 329 @HasPermissionAllDecorator('hg.admin')
330 330 def auth_token_access(self):
331 331 from rhodecode import CONFIG
332 332
333 333 c = self.load_default_context()
334 334 c.active = 'auth_token_access'
335 335
336 336 c.user = User.get_default_user(refresh=True)
337 337 c.perm_user = c.user.AuthUser()
338 338
339 339 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 340 c.view_data = []
341 341
342 342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 343 introspector = self.request.registry.introspector
344 344
345 345 view_intr = {}
346 346 for view_data in introspector.get_category('views'):
347 347 intr = view_data['introspectable']
348 348
349 349 if 'route_name' in intr and intr['attr']:
350 350 view_intr[intr['route_name']] = '{}:{}'.format(
351 351 str(intr['derived_callable'].__name__), intr['attr']
352 352 )
353 353
354 354 c.whitelist_key = 'api_access_controllers_whitelist'
355 355 c.whitelist_file = CONFIG.get('__file__')
356 356 whitelist_views = aslist(
357 357 CONFIG.get(c.whitelist_key), sep=',')
358 358
359 359 for route_info in mapper.get_routes():
360 360 if not route_info.name.startswith('__'):
361 361 routepath = route_info.pattern
362 362
363 363 def replace(matchobj):
364 364 if matchobj.group(1):
365 365 return "{%s}" % matchobj.group(1).split(':')[0]
366 366 else:
367 367 return "{%s}" % matchobj.group(2)
368 368
369 369 routepath = _argument_prog.sub(replace, routepath)
370 370
371 371 if not routepath.startswith('/'):
372 372 routepath = '/' + routepath
373 373
374 374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 375 active = view_fqn in whitelist_views
376 376 c.view_data.append((route_info.name, view_fqn, routepath, active))
377 377
378 378 c.whitelist_views = whitelist_views
379 379 return self._get_template_context(c)
380 380
381 381 def ssh_enabled(self):
382 382 return self.request.registry.settings.get(
383 383 'ssh.generate_authorized_keyfile')
384 384
385 385 @LoginRequired()
386 386 @HasPermissionAllDecorator('hg.admin')
387 387 def ssh_keys(self):
388 388 c = self.load_default_context()
389 389 c.active = 'ssh_keys'
390 390 c.ssh_enabled = self.ssh_enabled()
391 391 return self._get_template_context(c)
392 392
393 393 @LoginRequired()
394 394 @HasPermissionAllDecorator('hg.admin')
395 395 def ssh_keys_data(self):
396 396 _ = self.request.translate
397 397 self.load_default_context()
398 398 column_map = {
399 399 'fingerprint': 'ssh_key_fingerprint',
400 400 'username': User.username
401 401 }
402 402 draw, start, limit = self._extract_chunk(self.request)
403 403 search_q, order_by, order_dir = self._extract_ordering(
404 404 self.request, column_map=column_map)
405 405
406 406 ssh_keys_data_total_count = UserSshKeys.query()\
407 407 .count()
408 408
409 409 # json generate
410 410 base_q = UserSshKeys.query().join(UserSshKeys.user)
411 411
412 412 if search_q:
413 like_expression = u'%{}%'.format(safe_unicode(search_q))
413 like_expression = u'%{}%'.format(safe_str(search_q))
414 414 base_q = base_q.filter(or_(
415 415 User.username.ilike(like_expression),
416 416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 417 ))
418 418
419 419 users_data_total_filtered_count = base_q.count()
420 420
421 421 sort_col = self._get_order_col(order_by, UserSshKeys)
422 422 if sort_col:
423 423 if order_dir == 'asc':
424 424 # handle null values properly to order by NULL last
425 425 if order_by in ['created_on']:
426 426 sort_col = coalesce(sort_col, datetime.date.max)
427 427 sort_col = sort_col.asc()
428 428 else:
429 429 # handle null values properly to order by NULL last
430 430 if order_by in ['created_on']:
431 431 sort_col = coalesce(sort_col, datetime.date.min)
432 432 sort_col = sort_col.desc()
433 433
434 434 base_q = base_q.order_by(sort_col)
435 435 base_q = base_q.offset(start).limit(limit)
436 436
437 437 ssh_keys = base_q.all()
438 438
439 439 ssh_keys_data = []
440 440 for ssh_key in ssh_keys:
441 441 ssh_keys_data.append({
442 442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 443 "fingerprint": ssh_key.ssh_key_fingerprint,
444 444 "description": ssh_key.description,
445 445 "created_on": h.format_date(ssh_key.created_on),
446 446 "accessed_on": h.format_date(ssh_key.accessed_on),
447 447 "action": h.link_to(
448 448 _('Edit'), h.route_path('edit_user_ssh_keys',
449 449 user_id=ssh_key.user.user_id))
450 450 })
451 451
452 452 data = ({
453 453 'draw': draw,
454 454 'data': ssh_keys_data,
455 455 'recordsTotal': ssh_keys_data_total_count,
456 456 'recordsFiltered': users_data_total_filtered_count,
457 457 })
458 458
459 459 return data
460 460
461 461 @LoginRequired()
462 462 @HasPermissionAllDecorator('hg.admin')
463 463 @CSRFRequired()
464 464 def ssh_keys_update(self):
465 465 _ = self.request.translate
466 466 self.load_default_context()
467 467
468 468 ssh_enabled = self.ssh_enabled()
469 469 key_file = self.request.registry.settings.get(
470 470 'ssh.authorized_keys_file_path')
471 471 if ssh_enabled:
472 472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 473 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 474 category='success')
475 475 else:
476 476 h.flash(_('SSH key support is disabled in .ini file'),
477 477 category='warning')
478 478
479 479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,356 +1,356 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import datetime
21 21 import logging
22 22 import time
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 28
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, CSRFRequired, NotAnonymous,
37 37 HasPermissionAny, HasRepoGroupPermissionAny)
38 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 40 from rhodecode.model.forms import RepoGroupForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.db import (
45 45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 allow_empty_group = False
59 59
60 60 if self._can_create_repo_group():
61 61 # we're global admin, we're ok and we can create TOP level groups
62 62 allow_empty_group = True
63 63
64 64 # override the choices for this form, we need to filter choices
65 65 # and display only those we have ADMIN right
66 66 groups_with_admin_rights = RepoGroupList(
67 67 RepoGroup.query().all(),
68 68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73 73
74 74 def _can_create_repo_group(self, parent_group_id=None):
75 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 76 create_repo_group = HasPermissionAny(
77 77 'hg.repogroup.create.true')('group create controller')
78 78 if is_admin or (create_repo_group and not parent_group_id):
79 79 # we're global admin, or we have global repo group create
80 80 # permission
81 81 # we're ok and we can create TOP level groups
82 82 return True
83 83 elif parent_group_id:
84 84 # we check the permission if we can write to parent group
85 85 group = RepoGroup.get(parent_group_id)
86 86 group_name = group.group_name if group else None
87 87 if HasRepoGroupPermissionAny('group.admin')(
88 88 group_name, 'check if user is an admin of group'):
89 89 # we're an admin of passed in group, we're ok.
90 90 return True
91 91 else:
92 92 return False
93 93 return False
94 94
95 95 # permission check in data loading of
96 96 # `repo_group_list_data` via RepoGroupList
97 97 @LoginRequired()
98 98 @NotAnonymous()
99 99 def repo_group_list(self):
100 100 c = self.load_default_context()
101 101 return self._get_template_context(c)
102 102
103 103 # permission check inside
104 104 @LoginRequired()
105 105 @NotAnonymous()
106 106 def repo_group_list_data(self):
107 107 self.load_default_context()
108 108 column_map = {
109 109 'name': 'group_name_hash',
110 110 'desc': 'group_description',
111 111 'last_change': 'updated_on',
112 112 'top_level_repos': 'repos_total',
113 113 'owner': 'user_username',
114 114 }
115 115 draw, start, limit = self._extract_chunk(self.request)
116 116 search_q, order_by, order_dir = self._extract_ordering(
117 117 self.request, column_map=column_map)
118 118
119 119 _render = self.request.get_partial_renderer(
120 120 'rhodecode:templates/data_table/_dt_elements.mako')
121 121 c = _render.get_call_context()
122 122
123 123 def quick_menu(repo_group_name):
124 124 return _render('quick_repo_group_menu', repo_group_name)
125 125
126 126 def repo_group_lnk(repo_group_name):
127 127 return _render('repo_group_name', repo_group_name)
128 128
129 129 def last_change(last_change):
130 130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 131 ts = time.time()
132 132 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 135 return _render("last_change", last_change)
136 136
137 137 def desc(desc, personal):
138 138 return _render(
139 139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140 140
141 141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 142 return _render(
143 143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144 144
145 145 def user_profile(username):
146 146 return _render('user_profile', username)
147 147
148 148 _perms = ['group.admin']
149 149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150 150
151 151 repo_groups_data_total_count = RepoGroup.query()\
152 152 .filter(or_(
153 153 # generate multiple IN to fix limitation problems
154 154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 155 )) \
156 156 .count()
157 157
158 158 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 159 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 160 .count()
161 161
162 162 repo_count = count(Repository.repo_id)
163 163 base_q = Session.query(
164 164 RepoGroup.group_name,
165 165 RepoGroup.group_name_hash,
166 166 RepoGroup.group_description,
167 167 RepoGroup.group_id,
168 168 RepoGroup.personal,
169 169 RepoGroup.updated_on,
170 170 User,
171 171 repo_count.label('repos_count')
172 172 ) \
173 173 .filter(or_(
174 174 # generate multiple IN to fix limitation problems
175 175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 176 )) \
177 177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 178 .join(User, User.user_id == RepoGroup.user_id) \
179 179 .group_by(RepoGroup, User)
180 180
181 181 if search_q:
182 182 like_expression = u'%{}%'.format(safe_unicode(search_q))
183 183 base_q = base_q.filter(or_(
184 184 RepoGroup.group_name.ilike(like_expression),
185 185 ))
186 186
187 187 repo_groups_data_total_filtered_count = base_q.count()
188 188 # the inactive isn't really used, but we still make it same as other data grids
189 189 # which use inactive (users,user groups)
190 190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191 191
192 192 sort_defined = False
193 193 if order_by == 'group_name':
194 194 sort_col = func.lower(RepoGroup.group_name)
195 195 sort_defined = True
196 196 elif order_by == 'repos_total':
197 197 sort_col = repo_count
198 198 sort_defined = True
199 199 elif order_by == 'user_username':
200 200 sort_col = User.username
201 201 else:
202 202 sort_col = getattr(RepoGroup, order_by, None)
203 203
204 204 if sort_defined or sort_col:
205 205 if order_dir == 'asc':
206 206 sort_col = sort_col.asc()
207 207 else:
208 208 sort_col = sort_col.desc()
209 209
210 210 base_q = base_q.order_by(sort_col)
211 211 base_q = base_q.offset(start).limit(limit)
212 212
213 213 # authenticated access to user groups
214 214 auth_repo_group_list = base_q.all()
215 215
216 216 repo_groups_data = []
217 217 for repo_gr in auth_repo_group_list:
218 218 row = {
219 219 "menu": quick_menu(repo_gr.group_name),
220 220 "name": repo_group_lnk(repo_gr.group_name),
221 221
222 222 "last_change": last_change(repo_gr.updated_on),
223 223
224 224 "last_changeset": "",
225 225 "last_changeset_raw": "",
226 226
227 227 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 228 "owner": user_profile(repo_gr.User.username),
229 229 "top_level_repos": repo_gr.repos_count,
230 230 "action": repo_group_actions(
231 231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232 232
233 233 }
234 234
235 235 repo_groups_data.append(row)
236 236
237 237 data = ({
238 238 'draw': draw,
239 239 'data': repo_groups_data,
240 240 'recordsTotal': repo_groups_data_total_count,
241 241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 242 'recordsFiltered': repo_groups_data_total_filtered_count,
243 243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 244 })
245 245
246 246 return data
247 247
248 248 @LoginRequired()
249 249 @NotAnonymous()
250 250 # perm checks inside
251 251 def repo_group_new(self):
252 252 c = self.load_default_context()
253 253
254 254 # perm check for admin, create_group perm or admin of parent_group
255 255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 256 _gr = RepoGroup.get(parent_group_id)
257 257 if not self._can_create_repo_group(parent_group_id):
258 258 raise HTTPForbidden()
259 259
260 260 self._load_form_data(c)
261 261
262 262 defaults = {} # Future proof for default of repo group
263 263
264 264 parent_group_choice = '-1'
265 265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 266 parent_group_choice = self._rhodecode_user.personal_repo_group
267 267
268 268 if parent_group_id and _gr:
269 269 if parent_group_id in [x[0] for x in c.repo_groups]:
270 270 parent_group_choice = safe_unicode(parent_group_id)
271 271
272 272 defaults.update({'group_parent_id': parent_group_choice})
273 273
274 274 data = render(
275 275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 276 self._get_template_context(c), self.request)
277 277
278 278 html = formencode.htmlfill.render(
279 279 data,
280 280 defaults=defaults,
281 281 encoding="UTF-8",
282 282 force_defaults=False
283 283 )
284 284 return Response(html)
285 285
286 286 @LoginRequired()
287 287 @NotAnonymous()
288 288 @CSRFRequired()
289 289 # perm checks inside
290 290 def repo_group_create(self):
291 291 c = self.load_default_context()
292 292 _ = self.request.translate
293 293
294 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 295 can_create = self._can_create_repo_group(parent_group_id)
296 296
297 297 self._load_form_data(c)
298 298 # permissions for can create group based on parent_id are checked
299 299 # here in the Form
300 300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 301 repo_group_form = RepoGroupForm(
302 302 self.request.translate, available_groups=available_groups,
303 303 can_create_in_root=can_create)()
304 304
305 305 repo_group_name = self.request.POST.get('group_name')
306 306 try:
307 307 owner = self._rhodecode_user
308 308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 309 copy_permissions = form_result.get('group_copy_permissions')
310 310 repo_group = RepoGroupModel().create(
311 311 group_name=form_result['group_name_full'],
312 312 group_description=form_result['group_description'],
313 313 owner=owner.user_id,
314 314 copy_permissions=form_result['group_copy_permissions']
315 315 )
316 316 Session().flush()
317 317
318 318 repo_group_data = repo_group.get_api_data()
319 319 audit_logger.store_web(
320 320 'repo_group.create', action_data={'data': repo_group_data},
321 321 user=self._rhodecode_user)
322 322
323 323 Session().commit()
324 324
325 325 _new_group_name = form_result['group_name_full']
326 326
327 327 repo_group_url = h.link_to(
328 328 _new_group_name,
329 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 330 h.flash(h.literal(_('Created repository group %s')
331 331 % repo_group_url), category='success')
332 332
333 333 except formencode.Invalid as errors:
334 334 data = render(
335 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 336 self._get_template_context(c), self.request)
337 337 html = formencode.htmlfill.render(
338 338 data,
339 339 defaults=errors.value,
340 340 errors=errors.unpack_errors() or {},
341 341 prefix_error=False,
342 342 encoding="UTF-8",
343 343 force_defaults=False
344 344 )
345 345 return Response(html)
346 346 except Exception:
347 347 log.exception("Exception during creation of repository group")
348 348 h.flash(_('Error occurred during creation of repository group %s')
349 349 % repo_group_name, category='error')
350 350 raise HTTPFound(h.route_path('home'))
351 351
352 352 PermissionModel().trigger_permission_flush()
353 353
354 354 raise HTTPFound(
355 355 h.route_path('repo_group_home',
356 356 repo_group_name=form_result['group_name_full']))
@@ -1,250 +1,250 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 26
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.celerylib.utils import get_task_id
33 33
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, CSRFRequired, NotAnonymous,
36 36 HasPermissionAny, HasRepoGroupPermissionAny)
37 37 from rhodecode.lib import helpers as h
38 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 40 from rhodecode.model.forms import RepoForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.db import (
46 46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class AdminReposView(BaseAppView, DataGridAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 59 perm_set=['group.write', 'group.admin'])
60 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 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63 63
64 64 @LoginRequired()
65 65 @NotAnonymous()
66 66 # perms check inside
67 67 def repository_list(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 # perms check inside
74 74 def repository_list_data(self):
75 75 self.load_default_context()
76 76 column_map = {
77 77 'name': 'repo_name',
78 78 'desc': 'description',
79 79 'last_change': 'updated_on',
80 80 'owner': 'user_username',
81 81 }
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(
84 84 self.request, column_map=column_map)
85 85
86 86 _perms = ['repository.admin']
87 87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88 88
89 89 repos_data_total_count = Repository.query() \
90 90 .filter(or_(
91 91 # generate multiple IN to fix limitation problems
92 92 *in_filter_generator(Repository.repo_id, allowed_ids))
93 93 ) \
94 94 .count()
95 95
96 96 base_q = Session.query(
97 97 Repository.repo_id,
98 98 Repository.repo_name,
99 99 Repository.description,
100 100 Repository.repo_type,
101 101 Repository.repo_state,
102 102 Repository.private,
103 103 Repository.archived,
104 104 Repository.fork,
105 105 Repository.updated_on,
106 106 Repository._changeset_cache,
107 107 User,
108 108 ) \
109 109 .filter(or_(
110 110 # generate multiple IN to fix limitation problems
111 111 *in_filter_generator(Repository.repo_id, allowed_ids))
112 112 ) \
113 113 .join(User, User.user_id == Repository.user_id) \
114 114 .group_by(Repository, User)
115 115
116 116 if search_q:
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 like_expression = u'%{}%'.format(safe_str(search_q))
118 118 base_q = base_q.filter(or_(
119 119 Repository.repo_name.ilike(like_expression),
120 120 ))
121 121
122 122 repos_data_total_filtered_count = base_q.count()
123 123
124 124 sort_defined = False
125 125 if order_by == 'repo_name':
126 126 sort_col = func.lower(Repository.repo_name)
127 127 sort_defined = True
128 128 elif order_by == 'user_username':
129 129 sort_col = User.username
130 130 else:
131 131 sort_col = getattr(Repository, order_by, None)
132 132
133 133 if sort_defined or sort_col:
134 134 if order_dir == 'asc':
135 135 sort_col = sort_col.asc()
136 136 else:
137 137 sort_col = sort_col.desc()
138 138
139 139 base_q = base_q.order_by(sort_col)
140 140 base_q = base_q.offset(start).limit(limit)
141 141
142 142 repos_list = base_q.all()
143 143
144 144 repos_data = RepoModel().get_repos_as_dict(
145 145 repo_list=repos_list, admin=True, super_user_actions=True)
146 146
147 147 data = ({
148 148 'draw': draw,
149 149 'data': repos_data,
150 150 'recordsTotal': repos_data_total_count,
151 151 'recordsFiltered': repos_data_total_filtered_count,
152 152 })
153 153 return data
154 154
155 155 @LoginRequired()
156 156 @NotAnonymous()
157 157 # perms check inside
158 158 def repository_new(self):
159 159 c = self.load_default_context()
160 160
161 161 new_repo = self.request.GET.get('repo', '')
162 162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 163 _gr = RepoGroup.get(parent_group_id)
164 164
165 165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 166 # you're not super admin nor have global create permissions,
167 167 # but maybe you have at least write permission to a parent group ?
168 168
169 169 gr_name = _gr.group_name if _gr else None
170 170 # create repositories with write permission on group is set to true
171 171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 174 if not (group_admin or (group_write and create_on_write)):
175 175 raise HTTPForbidden()
176 176
177 177 self._load_form_data(c)
178 178 c.new_repo = repo_name_slug(new_repo)
179 179
180 180 # apply the defaults from defaults page
181 181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 182 # set checkbox to autochecked
183 183 defaults['repo_copy_permissions'] = True
184 184
185 185 parent_group_choice = '-1'
186 186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 187 parent_group_choice = self._rhodecode_user.personal_repo_group
188 188
189 189 if parent_group_id and _gr:
190 190 if parent_group_id in [x[0] for x in c.repo_groups]:
191 191 parent_group_choice = safe_unicode(parent_group_id)
192 192
193 193 defaults.update({'repo_group': parent_group_choice})
194 194
195 195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 196 self._get_template_context(c), self.request)
197 197 html = formencode.htmlfill.render(
198 198 data,
199 199 defaults=defaults,
200 200 encoding="UTF-8",
201 201 force_defaults=False
202 202 )
203 203 return Response(html)
204 204
205 205 @LoginRequired()
206 206 @NotAnonymous()
207 207 @CSRFRequired()
208 208 # perms check inside
209 209 def repository_create(self):
210 210 c = self.load_default_context()
211 211
212 212 form_result = {}
213 213 self._load_form_data(c)
214 214
215 215 try:
216 216 # CanWriteToGroup validators checks permissions of this POST
217 217 form = RepoForm(
218 218 self.request.translate, repo_groups=c.repo_groups_choices)()
219 219 form_result = form.to_python(dict(self.request.POST))
220 220 copy_permissions = form_result.get('repo_copy_permissions')
221 221 # create is done sometimes async on celery, db transaction
222 222 # management is handled there.
223 223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 224 task_id = get_task_id(task)
225 225 except formencode.Invalid as errors:
226 226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 227 self._get_template_context(c), self.request)
228 228 html = formencode.htmlfill.render(
229 229 data,
230 230 defaults=errors.value,
231 231 errors=errors.unpack_errors() or {},
232 232 prefix_error=False,
233 233 encoding="UTF-8",
234 234 force_defaults=False
235 235 )
236 236 return Response(html)
237 237
238 238 except Exception as e:
239 239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 240 h.flash(msg, category='error')
241 241 raise HTTPFound(h.route_path('home'))
242 242
243 243 repo_name = form_result.get('repo_name_full')
244 244
245 245 affected_user_ids = [self._rhodecode_user.user_id]
246 246 PermissionModel().trigger_permission_flush(affected_user_ids)
247 247
248 248 raise HTTPFound(
249 249 h.route_path('repo_creating', repo_name=repo_name,
250 250 _query=dict(task_id=task_id)))
@@ -1,719 +1,722 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import datetime
25 25 import formencode
26 26 import formencode.htmlfill
27 27
28 28 import rhodecode
29 29
30 30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 31 from pyramid.renderers import render
32 32 from pyramid.response import Response
33 33
34 34 from rhodecode.apps._base import BaseAppView
35 35 from rhodecode.apps._base.navigation import navigation_list
36 36 from rhodecode.apps.svn_support.config_keys import generate_config
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 40 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.str_utils import safe_str
41 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 44 from rhodecode.lib.index import searcher_from_config
44 45
45 46 from rhodecode.model.db import RhodeCodeUi, Repository
46 47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 49 LabsSettingsForm, IssueTrackerPatternsForm)
49 50 from rhodecode.model.permission import PermissionModel
50 51 from rhodecode.model.repo_group import RepoGroupModel
51 52
52 53 from rhodecode.model.scm import ScmModel
53 54 from rhodecode.model.notification import EmailNotificationModel
54 55 from rhodecode.model.meta import Session
55 56 from rhodecode.model.settings import (
56 57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 58 SettingsModel)
58 59
59 60
60 61 log = logging.getLogger(__name__)
61 62
62 63
63 64 class AdminSettingsView(BaseAppView):
64 65
65 66 def load_default_context(self):
66 67 c = self._get_local_tmpl_context()
67 68 c.labs_active = str2bool(
68 69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 70 c.navlist = navigation_list(self.request)
70 71 return c
71 72
72 73 @classmethod
73 74 def _get_ui_settings(cls):
74 75 ret = RhodeCodeUi.query().all()
75 76
76 77 if not ret:
77 78 raise Exception('Could not get application ui settings !')
78 79 settings = {}
79 80 for each in ret:
80 81 k = each.ui_key
81 82 v = each.ui_value
82 83 if k == '/':
83 84 k = 'root_path'
84 85
85 86 if k in ['push_ssl', 'publish', 'enabled']:
86 87 v = str2bool(v)
87 88
88 89 if k.find('.') != -1:
89 90 k = k.replace('.', '_')
90 91
91 92 if each.ui_section in ['hooks', 'extensions']:
92 93 v = each.ui_active
93 94
94 95 settings[each.ui_section + '_' + k] = v
95 96 return settings
96 97
97 98 @classmethod
98 99 def _form_defaults(cls):
99 100 defaults = SettingsModel().get_all_settings()
100 101 defaults.update(cls._get_ui_settings())
101 102
102 103 defaults.update({
103 104 'new_svn_branch': '',
104 105 'new_svn_tag': '',
105 106 })
106 107 return defaults
107 108
108 109 @LoginRequired()
109 110 @HasPermissionAllDecorator('hg.admin')
110 111 def settings_vcs(self):
111 112 c = self.load_default_context()
112 113 c.active = 'vcs'
113 114 model = VcsSettingsModel()
114 115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 117
117 118 settings = self.request.registry.settings
118 119 c.svn_proxy_generate_config = settings[generate_config]
119 120
120 121 defaults = self._form_defaults()
121 122
122 123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
123 124
124 125 data = render('rhodecode:templates/admin/settings/settings.mako',
125 126 self._get_template_context(c), self.request)
126 127 html = formencode.htmlfill.render(
127 128 data,
128 129 defaults=defaults,
129 130 encoding="UTF-8",
130 131 force_defaults=False
131 132 )
132 133 return Response(html)
133 134
134 135 @LoginRequired()
135 136 @HasPermissionAllDecorator('hg.admin')
136 137 @CSRFRequired()
137 138 def settings_vcs_update(self):
138 139 _ = self.request.translate
139 140 c = self.load_default_context()
140 141 c.active = 'vcs'
141 142
142 143 model = VcsSettingsModel()
143 144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
144 145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
145 146
146 147 settings = self.request.registry.settings
147 148 c.svn_proxy_generate_config = settings[generate_config]
148 149
149 150 application_form = ApplicationUiSettingsForm(self.request.translate)()
150 151
151 152 try:
152 153 form_result = application_form.to_python(dict(self.request.POST))
153 154 except formencode.Invalid as errors:
154 155 h.flash(
155 156 _("Some form inputs contain invalid data."),
156 157 category='error')
157 158 data = render('rhodecode:templates/admin/settings/settings.mako',
158 159 self._get_template_context(c), self.request)
159 160 html = formencode.htmlfill.render(
160 161 data,
161 162 defaults=errors.value,
162 163 errors=errors.unpack_errors() or {},
163 164 prefix_error=False,
164 165 encoding="UTF-8",
165 166 force_defaults=False
166 167 )
167 168 return Response(html)
168 169
169 170 try:
170 171 if c.visual.allow_repo_location_change:
171 172 model.update_global_path_setting(form_result['paths_root_path'])
172 173
173 174 model.update_global_ssl_setting(form_result['web_push_ssl'])
174 175 model.update_global_hook_settings(form_result)
175 176
176 177 model.create_or_update_global_svn_settings(form_result)
177 178 model.create_or_update_global_hg_settings(form_result)
178 179 model.create_or_update_global_git_settings(form_result)
179 180 model.create_or_update_global_pr_settings(form_result)
180 181 except Exception:
181 182 log.exception("Exception while updating settings")
182 183 h.flash(_('Error occurred during updating '
183 184 'application settings'), category='error')
184 185 else:
185 186 Session().commit()
186 187 h.flash(_('Updated VCS settings'), category='success')
187 188 raise HTTPFound(h.route_path('admin_settings_vcs'))
188 189
189 190 data = render('rhodecode:templates/admin/settings/settings.mako',
190 191 self._get_template_context(c), self.request)
191 192 html = formencode.htmlfill.render(
192 193 data,
193 194 defaults=self._form_defaults(),
194 195 encoding="UTF-8",
195 196 force_defaults=False
196 197 )
197 198 return Response(html)
198 199
199 200 @LoginRequired()
200 201 @HasPermissionAllDecorator('hg.admin')
201 202 @CSRFRequired()
202 203 def settings_vcs_delete_svn_pattern(self):
203 204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
204 205 model = VcsSettingsModel()
205 206 try:
206 207 model.delete_global_svn_pattern(delete_pattern_id)
207 208 except SettingNotFound:
208 209 log.exception(
209 210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
210 211 raise HTTPNotFound()
211 212
212 213 Session().commit()
213 214 return True
214 215
215 216 @LoginRequired()
216 217 @HasPermissionAllDecorator('hg.admin')
217 218 def settings_mapping(self):
218 219 c = self.load_default_context()
219 220 c.active = 'mapping'
220 221
221 222 data = render('rhodecode:templates/admin/settings/settings.mako',
222 223 self._get_template_context(c), self.request)
223 224 html = formencode.htmlfill.render(
224 225 data,
225 226 defaults=self._form_defaults(),
226 227 encoding="UTF-8",
227 228 force_defaults=False
228 229 )
229 230 return Response(html)
230 231
231 232 @LoginRequired()
232 233 @HasPermissionAllDecorator('hg.admin')
233 234 @CSRFRequired()
234 235 def settings_mapping_update(self):
235 236 _ = self.request.translate
236 237 c = self.load_default_context()
237 238 c.active = 'mapping'
238 239 rm_obsolete = self.request.POST.get('destroy', False)
239 240 invalidate_cache = self.request.POST.get('invalidate', False)
240 241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
241 242
242 243 if invalidate_cache:
243 244 log.debug('invalidating all repositories cache')
244 245 for repo in Repository.get_all():
245 246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
246 247
247 248 filesystem_repos = ScmModel().repo_scan()
248 249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
249 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 254 h.flash(_('Repositories successfully '
253 255 'rescanned added: %s ; removed: %s') %
254 256 (_repr(added), _repr(removed)),
255 257 category='success')
256 258 raise HTTPFound(h.route_path('admin_settings_mapping'))
257 259
258 260 @LoginRequired()
259 261 @HasPermissionAllDecorator('hg.admin')
260 262 def settings_global(self):
261 263 c = self.load_default_context()
262 264 c.active = 'global'
263 265 c.personal_repo_group_default_pattern = RepoGroupModel()\
264 266 .get_personal_group_name_pattern()
265 267
266 268 data = render('rhodecode:templates/admin/settings/settings.mako',
267 269 self._get_template_context(c), self.request)
268 270 html = formencode.htmlfill.render(
269 271 data,
270 272 defaults=self._form_defaults(),
271 273 encoding="UTF-8",
272 274 force_defaults=False
273 275 )
274 276 return Response(html)
275 277
276 278 @LoginRequired()
277 279 @HasPermissionAllDecorator('hg.admin')
278 280 @CSRFRequired()
279 281 def settings_global_update(self):
280 282 _ = self.request.translate
281 283 c = self.load_default_context()
282 284 c.active = 'global'
283 285 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 286 .get_personal_group_name_pattern()
285 287 application_form = ApplicationSettingsForm(self.request.translate)()
286 288 try:
287 289 form_result = application_form.to_python(dict(self.request.POST))
288 290 except formencode.Invalid as errors:
289 291 h.flash(
290 292 _("Some form inputs contain invalid data."),
291 293 category='error')
292 294 data = render('rhodecode:templates/admin/settings/settings.mako',
293 295 self._get_template_context(c), self.request)
294 296 html = formencode.htmlfill.render(
295 297 data,
296 298 defaults=errors.value,
297 299 errors=errors.unpack_errors() or {},
298 300 prefix_error=False,
299 301 encoding="UTF-8",
300 302 force_defaults=False
301 303 )
302 304 return Response(html)
303 305
304 306 settings = [
305 307 ('title', 'rhodecode_title', 'unicode'),
306 308 ('realm', 'rhodecode_realm', 'unicode'),
307 309 ('pre_code', 'rhodecode_pre_code', 'unicode'),
308 310 ('post_code', 'rhodecode_post_code', 'unicode'),
309 311 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
310 312 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
311 313 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
312 314 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
313 315 ]
316
314 317 try:
315 318 for setting, form_key, type_ in settings:
316 319 sett = SettingsModel().create_or_update_setting(
317 320 setting, form_result[form_key], type_)
318 321 Session().add(sett)
319 322
320 323 Session().commit()
321 324 SettingsModel().invalidate_settings_cache()
322 325 h.flash(_('Updated application settings'), category='success')
323 326 except Exception:
324 327 log.exception("Exception while updating application settings")
325 328 h.flash(
326 329 _('Error occurred during updating application settings'),
327 330 category='error')
328 331
329 332 raise HTTPFound(h.route_path('admin_settings_global'))
330 333
331 334 @LoginRequired()
332 335 @HasPermissionAllDecorator('hg.admin')
333 336 def settings_visual(self):
334 337 c = self.load_default_context()
335 338 c.active = 'visual'
336 339
337 340 data = render('rhodecode:templates/admin/settings/settings.mako',
338 341 self._get_template_context(c), self.request)
339 342 html = formencode.htmlfill.render(
340 343 data,
341 344 defaults=self._form_defaults(),
342 345 encoding="UTF-8",
343 346 force_defaults=False
344 347 )
345 348 return Response(html)
346 349
347 350 @LoginRequired()
348 351 @HasPermissionAllDecorator('hg.admin')
349 352 @CSRFRequired()
350 353 def settings_visual_update(self):
351 354 _ = self.request.translate
352 355 c = self.load_default_context()
353 356 c.active = 'visual'
354 357 application_form = ApplicationVisualisationForm(self.request.translate)()
355 358 try:
356 359 form_result = application_form.to_python(dict(self.request.POST))
357 360 except formencode.Invalid as errors:
358 361 h.flash(
359 362 _("Some form inputs contain invalid data."),
360 363 category='error')
361 364 data = render('rhodecode:templates/admin/settings/settings.mako',
362 365 self._get_template_context(c), self.request)
363 366 html = formencode.htmlfill.render(
364 367 data,
365 368 defaults=errors.value,
366 369 errors=errors.unpack_errors() or {},
367 370 prefix_error=False,
368 371 encoding="UTF-8",
369 372 force_defaults=False
370 373 )
371 374 return Response(html)
372 375
373 376 try:
374 377 settings = [
375 378 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
376 379 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
377 380 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
378 381 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
379 382 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
380 383 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
381 384 ('show_version', 'rhodecode_show_version', 'bool'),
382 385 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
383 386 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
384 387 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
385 388 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
386 389 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
387 390 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
388 391 ('support_url', 'rhodecode_support_url', 'unicode'),
389 392 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
390 393 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
391 394 ]
392 395 for setting, form_key, type_ in settings:
393 396 sett = SettingsModel().create_or_update_setting(
394 397 setting, form_result[form_key], type_)
395 398 Session().add(sett)
396 399
397 400 Session().commit()
398 401 SettingsModel().invalidate_settings_cache()
399 402 h.flash(_('Updated visualisation settings'), category='success')
400 403 except Exception:
401 404 log.exception("Exception updating visualization settings")
402 405 h.flash(_('Error occurred during updating '
403 406 'visualisation settings'),
404 407 category='error')
405 408
406 409 raise HTTPFound(h.route_path('admin_settings_visual'))
407 410
408 411 @LoginRequired()
409 412 @HasPermissionAllDecorator('hg.admin')
410 413 def settings_issuetracker(self):
411 414 c = self.load_default_context()
412 415 c.active = 'issuetracker'
413 416 defaults = c.rc_config
414 417
415 418 entry_key = 'rhodecode_issuetracker_pat_'
416 419
417 420 c.issuetracker_entries = {}
418 421 for k, v in defaults.items():
419 422 if k.startswith(entry_key):
420 423 uid = k[len(entry_key):]
421 424 c.issuetracker_entries[uid] = None
422 425
423 426 for uid in c.issuetracker_entries:
424 427 c.issuetracker_entries[uid] = AttributeDict({
425 428 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
426 429 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
427 430 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
428 431 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
429 432 })
430 433
431 434 return self._get_template_context(c)
432 435
433 436 @LoginRequired()
434 437 @HasPermissionAllDecorator('hg.admin')
435 438 @CSRFRequired()
436 439 def settings_issuetracker_test(self):
437 440 error_container = []
438 441
439 442 urlified_commit = h.urlify_commit_message(
440 443 self.request.POST.get('test_text', ''),
441 444 'repo_group/test_repo1', error_container=error_container)
442 445 if error_container:
443 446 def converter(inp):
444 447 return h.html_escape(inp)
445 448
446 449 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
447 450
448 451 return urlified_commit
449 452
450 453 @LoginRequired()
451 454 @HasPermissionAllDecorator('hg.admin')
452 455 @CSRFRequired()
453 456 def settings_issuetracker_update(self):
454 457 _ = self.request.translate
455 458 self.load_default_context()
456 459 settings_model = IssueTrackerSettingsModel()
457 460
458 461 try:
459 462 form = IssueTrackerPatternsForm(self.request.translate)()
460 463 data = form.to_python(self.request.POST)
461 464 except formencode.Invalid as errors:
462 465 log.exception('Failed to add new pattern')
463 466 error = errors
464 467 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
465 468 category='error')
466 469 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
467 470
468 471 if data:
469 472 for uid in data.get('delete_patterns', []):
470 473 settings_model.delete_entries(uid)
471 474
472 475 for pattern in data.get('patterns', []):
473 476 for setting, value, type_ in pattern:
474 477 sett = settings_model.create_or_update_setting(
475 478 setting, value, type_)
476 479 Session().add(sett)
477 480
478 481 Session().commit()
479 482
480 483 SettingsModel().invalidate_settings_cache()
481 484 h.flash(_('Updated issue tracker entries'), category='success')
482 485 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
483 486
484 487 @LoginRequired()
485 488 @HasPermissionAllDecorator('hg.admin')
486 489 @CSRFRequired()
487 490 def settings_issuetracker_delete(self):
488 491 _ = self.request.translate
489 492 self.load_default_context()
490 493 uid = self.request.POST.get('uid')
491 494 try:
492 495 IssueTrackerSettingsModel().delete_entries(uid)
493 496 except Exception:
494 497 log.exception('Failed to delete issue tracker setting %s', uid)
495 498 raise HTTPNotFound()
496 499
497 500 SettingsModel().invalidate_settings_cache()
498 501 h.flash(_('Removed issue tracker entry.'), category='success')
499 502
500 503 return {'deleted': uid}
501 504
502 505 @LoginRequired()
503 506 @HasPermissionAllDecorator('hg.admin')
504 507 def settings_email(self):
505 508 c = self.load_default_context()
506 509 c.active = 'email'
507 510 c.rhodecode_ini = rhodecode.CONFIG
508 511
509 512 data = render('rhodecode:templates/admin/settings/settings.mako',
510 513 self._get_template_context(c), self.request)
511 514 html = formencode.htmlfill.render(
512 515 data,
513 516 defaults=self._form_defaults(),
514 517 encoding="UTF-8",
515 518 force_defaults=False
516 519 )
517 520 return Response(html)
518 521
519 522 @LoginRequired()
520 523 @HasPermissionAllDecorator('hg.admin')
521 524 @CSRFRequired()
522 525 def settings_email_update(self):
523 526 _ = self.request.translate
524 527 c = self.load_default_context()
525 528 c.active = 'email'
526 529
527 530 test_email = self.request.POST.get('test_email')
528 531
529 532 if not test_email:
530 533 h.flash(_('Please enter email address'), category='error')
531 534 raise HTTPFound(h.route_path('admin_settings_email'))
532 535
533 536 email_kwargs = {
534 537 'date': datetime.datetime.now(),
535 538 'user': self._rhodecode_db_user
536 539 }
537 540
538 541 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
539 542 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
540 543
541 544 recipients = [test_email] if test_email else None
542 545
543 546 run_task(tasks.send_email, recipients, subject,
544 547 email_body_plaintext, email_body)
545 548
546 549 h.flash(_('Send email task created'), category='success')
547 550 raise HTTPFound(h.route_path('admin_settings_email'))
548 551
549 552 @LoginRequired()
550 553 @HasPermissionAllDecorator('hg.admin')
551 554 def settings_hooks(self):
552 555 c = self.load_default_context()
553 556 c.active = 'hooks'
554 557
555 558 model = SettingsModel()
556 559 c.hooks = model.get_builtin_hooks()
557 560 c.custom_hooks = model.get_custom_hooks()
558 561
559 562 data = render('rhodecode:templates/admin/settings/settings.mako',
560 563 self._get_template_context(c), self.request)
561 564 html = formencode.htmlfill.render(
562 565 data,
563 566 defaults=self._form_defaults(),
564 567 encoding="UTF-8",
565 568 force_defaults=False
566 569 )
567 570 return Response(html)
568 571
569 572 @LoginRequired()
570 573 @HasPermissionAllDecorator('hg.admin')
571 574 @CSRFRequired()
572 575 def settings_hooks_update(self):
573 576 _ = self.request.translate
574 577 c = self.load_default_context()
575 578 c.active = 'hooks'
576 579 if c.visual.allow_custom_hooks_settings:
577 580 ui_key = self.request.POST.get('new_hook_ui_key')
578 581 ui_value = self.request.POST.get('new_hook_ui_value')
579 582
580 583 hook_id = self.request.POST.get('hook_id')
581 584 new_hook = False
582 585
583 586 model = SettingsModel()
584 587 try:
585 588 if ui_value and ui_key:
586 589 model.create_or_update_hook(ui_key, ui_value)
587 590 h.flash(_('Added new hook'), category='success')
588 591 new_hook = True
589 592 elif hook_id:
590 593 RhodeCodeUi.delete(hook_id)
591 594 Session().commit()
592 595
593 596 # check for edits
594 597 update = False
595 598 _d = self.request.POST.dict_of_lists()
596 599 for k, v in zip(_d.get('hook_ui_key', []),
597 600 _d.get('hook_ui_value_new', [])):
598 601 model.create_or_update_hook(k, v)
599 602 update = True
600 603
601 604 if update and not new_hook:
602 605 h.flash(_('Updated hooks'), category='success')
603 606 Session().commit()
604 607 except Exception:
605 608 log.exception("Exception during hook creation")
606 609 h.flash(_('Error occurred during hook creation'),
607 610 category='error')
608 611
609 612 raise HTTPFound(h.route_path('admin_settings_hooks'))
610 613
611 614 @LoginRequired()
612 615 @HasPermissionAllDecorator('hg.admin')
613 616 def settings_search(self):
614 617 c = self.load_default_context()
615 618 c.active = 'search'
616 619
617 620 c.searcher = searcher_from_config(self.request.registry.settings)
618 621 c.statistics = c.searcher.statistics(self.request.translate)
619 622
620 623 return self._get_template_context(c)
621 624
622 625 @LoginRequired()
623 626 @HasPermissionAllDecorator('hg.admin')
624 627 def settings_automation(self):
625 628 c = self.load_default_context()
626 629 c.active = 'automation'
627 630
628 631 return self._get_template_context(c)
629 632
630 633 @LoginRequired()
631 634 @HasPermissionAllDecorator('hg.admin')
632 635 def settings_labs(self):
633 636 c = self.load_default_context()
634 637 if not c.labs_active:
635 638 raise HTTPFound(h.route_path('admin_settings'))
636 639
637 640 c.active = 'labs'
638 641 c.lab_settings = _LAB_SETTINGS
639 642
640 643 data = render('rhodecode:templates/admin/settings/settings.mako',
641 644 self._get_template_context(c), self.request)
642 645 html = formencode.htmlfill.render(
643 646 data,
644 647 defaults=self._form_defaults(),
645 648 encoding="UTF-8",
646 649 force_defaults=False
647 650 )
648 651 return Response(html)
649 652
650 653 @LoginRequired()
651 654 @HasPermissionAllDecorator('hg.admin')
652 655 @CSRFRequired()
653 656 def settings_labs_update(self):
654 657 _ = self.request.translate
655 658 c = self.load_default_context()
656 659 c.active = 'labs'
657 660
658 661 application_form = LabsSettingsForm(self.request.translate)()
659 662 try:
660 663 form_result = application_form.to_python(dict(self.request.POST))
661 664 except formencode.Invalid as errors:
662 665 h.flash(
663 666 _("Some form inputs contain invalid data."),
664 667 category='error')
665 668 data = render('rhodecode:templates/admin/settings/settings.mako',
666 669 self._get_template_context(c), self.request)
667 670 html = formencode.htmlfill.render(
668 671 data,
669 672 defaults=errors.value,
670 673 errors=errors.unpack_errors() or {},
671 674 prefix_error=False,
672 675 encoding="UTF-8",
673 676 force_defaults=False
674 677 )
675 678 return Response(html)
676 679
677 680 try:
678 681 session = Session()
679 682 for setting in _LAB_SETTINGS:
680 683 setting_name = setting.key[len('rhodecode_'):]
681 684 sett = SettingsModel().create_or_update_setting(
682 685 setting_name, form_result[setting.key], setting.type)
683 686 session.add(sett)
684 687
685 688 except Exception:
686 689 log.exception('Exception while updating lab settings')
687 690 h.flash(_('Error occurred during updating labs settings'),
688 691 category='error')
689 692 else:
690 693 Session().commit()
691 694 SettingsModel().invalidate_settings_cache()
692 695 h.flash(_('Updated Labs settings'), category='success')
693 696 raise HTTPFound(h.route_path('admin_settings_labs'))
694 697
695 698 data = render('rhodecode:templates/admin/settings/settings.mako',
696 699 self._get_template_context(c), self.request)
697 700 html = formencode.htmlfill.render(
698 701 data,
699 702 defaults=self._form_defaults(),
700 703 encoding="UTF-8",
701 704 force_defaults=False
702 705 )
703 706 return Response(html)
704 707
705 708
706 709 # :param key: name of the setting including the 'rhodecode_' prefix
707 710 # :param type: the RhodeCodeSetting type to use.
708 711 # :param group: the i18ned group in which we should dispaly this setting
709 712 # :param label: the i18ned label we should display for this setting
710 713 # :param help: the i18ned help we should dispaly for this setting
711 714 LabSetting = collections.namedtuple(
712 715 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
713 716
714 717
715 718 # This list has to be kept in sync with the form
716 719 # rhodecode.model.forms.LabsSettingsForm.
717 720 _LAB_SETTINGS = [
718 721
719 722 ]
@@ -1,234 +1,237 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 import urllib.request, urllib.error, urllib.parse
22 import urllib.request
23 import urllib.error
24 import urllib.parse
23 25 import os
24 26
25 27 import rhodecode
26 28 from rhodecode.apps._base import BaseAppView
27 29 from rhodecode.apps._base.navigation import navigation_list
28 30 from rhodecode.lib import helpers as h
29 31 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 32 from rhodecode.lib.utils2 import str2bool
31 33 from rhodecode.lib import system_info
32 34 from rhodecode.model.update import UpdateModel
33 35
34 36 log = logging.getLogger(__name__)
35 37
36 38
37 39 class AdminSystemInfoSettingsView(BaseAppView):
38 40 def load_default_context(self):
39 41 c = self._get_local_tmpl_context()
40 42 return c
41 43
42 44 def get_env_data(self):
43 45 black_list = [
44 46 'NIX_LDFLAGS',
45 47 'NIX_CFLAGS_COMPILE',
46 48 'propagatedBuildInputs',
47 49 'propagatedNativeBuildInputs',
48 50 'postInstall',
49 51 'buildInputs',
50 52 'buildPhase',
51 53 'preShellHook',
52 54 'preShellHook',
53 55 'preCheck',
54 56 'preBuild',
55 57 'postShellHook',
56 58 'postFixup',
57 59 'postCheck',
58 60 'nativeBuildInputs',
59 61 'installPhase',
60 62 'installCheckPhase',
61 63 'checkPhase',
62 64 'configurePhase',
63 65 'shellHook'
64 66 ]
65 67 secret_list = [
66 68 'RHODECODE_USER_PASS'
67 69 ]
68 70
69 71 for k, v in sorted(os.environ.items()):
70 72 if k in black_list:
71 73 continue
72 74 if k in secret_list:
73 75 v = '*****'
74 76 yield k, v
75 77
76 78 @LoginRequired()
77 79 @HasPermissionAllDecorator('hg.admin')
78 80 def settings_system_info(self):
79 81 _ = self.request.translate
80 82 c = self.load_default_context()
81 83
82 84 c.active = 'system'
83 85 c.navlist = navigation_list(self.request)
84 86
85 87 # TODO(marcink), figure out how to allow only selected users to do this
86 88 c.allowed_to_snapshot = self._rhodecode_user.admin
87 89
88 90 snapshot = str2bool(self.request.params.get('snapshot'))
89 91
90 92 c.rhodecode_update_url = UpdateModel().get_update_url()
91 93 c.env_data = self.get_env_data()
92 94 server_info = system_info.get_system_info(self.request.environ)
93 95
94 96 for key, val in server_info.items():
95 97 setattr(c, key, val)
96 98
97 99 def val(name, subkey='human_value'):
98 100 return server_info[name][subkey]
99 101
100 102 def state(name):
101 103 return server_info[name]['state']
102 104
103 105 def val2(name):
104 106 val = server_info[name]['human_value']
105 107 state = server_info[name]['state']
106 108 return val, state
107 109
108 110 update_info_msg = _('Note: please make sure this server can '
109 111 'access `${url}` for the update link to work',
110 112 mapping=dict(url=c.rhodecode_update_url))
111 113 version = UpdateModel().get_stored_version()
112 114 is_outdated = UpdateModel().is_outdated(
113 115 rhodecode.__version__, version)
114 116 update_state = {
115 117 'type': 'warning',
116 118 'message': 'New version available: {}'.format(version)
117 119 } \
118 120 if is_outdated else {}
119 121 c.data_items = [
120 122 # update info
121 123 (_('Update info'), h.literal(
122 124 '<span class="link" id="check_for_update" >%s.</span>' % (
123 125 _('Check for updates')) +
124 126 '<br/> <span >%s.</span>' % (update_info_msg)
125 127 ), ''),
126 128
127 129 # RhodeCode specific
128 130 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 131 (_('Latest version'), version, update_state),
130 132 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 133 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 134 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 135 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 136 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 137 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 138 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 139 ('', '', ''), # spacer
138 140
139 141 # Database
140 142 (_('Database'), val('database')['url'], state('database')),
141 143 (_('Database version'), val('database')['version'], state('database')),
142 144 ('', '', ''), # spacer
143 145
144 146 # Platform/Python
145 147 (_('Platform'), val('platform')['name'], state('platform')),
146 148 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 149 (_('Lang'), val('locale'), state('locale')),
148 150 (_('Python version'), val('python')['version'], state('python')),
149 151 (_('Python path'), val('python')['executable'], state('python')),
150 152 ('', '', ''), # spacer
151 153
152 154 # Systems stats
153 155 (_('CPU'), val('cpu')['text'], state('cpu')),
154 156 (_('Load'), val('load')['text'], state('load')),
155 157 (_('Memory'), val('memory')['text'], state('memory')),
156 158 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 159 ('', '', ''), # spacer
158 160
159 161 # ulimit
160 162 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161 163
162 164 # Repo storage
163 165 (_('Storage location'), val('storage')['path'], state('storage')),
164 166 (_('Storage info'), val('storage')['text'], state('storage')),
165 167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166 168
167 169 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 170 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169 171
170 172 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 173 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172 174
173 175 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 176 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175 177
176 178 (_('Search info'), val('search')['text'], state('search')),
177 179 (_('Search location'), val('search')['location'], state('search')),
178 180 ('', '', ''), # spacer
179 181
180 182 # VCS specific
181 183 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 184 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 185 (_('GIT'), val('git'), state('git')),
184 186 (_('HG'), val('hg'), state('hg')),
185 187 (_('SVN'), val('svn'), state('svn')),
186 188
187 189 ]
188 190
189 191 c.vcsserver_data_items = [
190 192 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 193 ]
192 194
193 195 if snapshot:
194 196 if c.allowed_to_snapshot:
195 197 c.data_items.pop(0) # remove server info
196 198 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 199 else:
198 200 h.flash('You are not allowed to do this', category='warning')
199 201 return self._get_template_context(c)
200 202
201 203 @LoginRequired()
202 204 @HasPermissionAllDecorator('hg.admin')
203 205 def settings_system_info_check_update(self):
204 206 _ = self.request.translate
205 207 c = self.load_default_context()
206 208
207 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 213 try:
211 214 data = UpdateModel().get_update_data(update_url)
212 215 except urllib.error.URLError as e:
213 216 log.exception("Exception contacting upgrade server")
214 217 self.request.override_renderer = 'string'
215 218 return _err('Failed to contact upgrade server: %r' % e)
216 219 except ValueError as e:
217 220 log.exception("Bad data sent from update server")
218 221 self.request.override_renderer = 'string'
219 222 return _err('Bad data sent from update server')
220 223
221 224 latest = data['versions'][0]
222 225
223 226 c.update_url = update_url
224 227 c.latest_data = latest
225 228 c.latest_ver = latest['version']
226 229 c.cur_ver = rhodecode.__version__
227 230 c.should_upgrade = False
228 231
229 232 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 233 if is_oudated:
231 234 c.should_upgrade = True
232 235 c.important_notices = latest['general']
233 236 UpdateModel().store_version(latest['version'])
234 237 return self._get_template_context(c)
@@ -1,253 +1,252 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 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 38 from rhodecode.model.forms import UserGroupForm
39 39 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.scm import UserGroupList
41 40 from rhodecode.model.db import (
42 41 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 42 from rhodecode.model.meta import Session
44 43 from rhodecode.model.user_group import UserGroupModel
45 44 from rhodecode.model.db import true
46 45
47 46 log = logging.getLogger(__name__)
48 47
49 48
50 49 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51 50
52 51 def load_default_context(self):
53 52 c = self._get_local_tmpl_context()
54 53 PermissionModel().set_global_permission_choices(
55 54 c, gettext_translator=self.request.translate)
56 55 return c
57 56
58 57 # permission check in data loading of
59 58 # `user_groups_list_data` via UserGroupList
60 59 @LoginRequired()
61 60 @NotAnonymous()
62 61 def user_groups_list(self):
63 62 c = self.load_default_context()
64 63 return self._get_template_context(c)
65 64
66 65 # permission check inside
67 66 @LoginRequired()
68 67 @NotAnonymous()
69 68 def user_groups_list_data(self):
70 69 self.load_default_context()
71 70 column_map = {
72 71 'active': 'users_group_active',
73 72 'description': 'user_group_description',
74 73 'members': 'members_total',
75 74 'owner': 'user_username',
76 75 'sync': 'group_data'
77 76 }
78 77 draw, start, limit = self._extract_chunk(self.request)
79 78 search_q, order_by, order_dir = self._extract_ordering(
80 79 self.request, column_map=column_map)
81 80
82 81 _render = self.request.get_partial_renderer(
83 82 'rhodecode:templates/data_table/_dt_elements.mako')
84 83
85 84 def user_group_name(user_group_name):
86 85 return _render("user_group_name", user_group_name)
87 86
88 87 def user_group_actions(user_group_id, user_group_name):
89 88 return _render("user_group_actions", user_group_id, user_group_name)
90 89
91 90 def user_profile(username):
92 91 return _render('user_profile', username)
93 92
94 93 _perms = ['usergroup.admin']
95 94 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
96 95
97 96 user_groups_data_total_count = UserGroup.query()\
98 97 .filter(or_(
99 98 # generate multiple IN to fix limitation problems
100 99 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
101 100 ))\
102 101 .count()
103 102
104 103 user_groups_data_total_inactive_count = UserGroup.query()\
105 104 .filter(or_(
106 105 # generate multiple IN to fix limitation problems
107 106 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
108 107 ))\
109 108 .filter(UserGroup.users_group_active != true()).count()
110 109
111 110 member_count = count(UserGroupMember.user_id)
112 111 base_q = Session.query(
113 112 UserGroup.users_group_name,
114 113 UserGroup.user_group_description,
115 114 UserGroup.users_group_active,
116 115 UserGroup.users_group_id,
117 116 UserGroup.group_data,
118 117 User,
119 118 member_count.label('member_count')
120 119 ) \
121 120 .filter(or_(
122 121 # generate multiple IN to fix limitation problems
123 122 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
124 123 )) \
125 124 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
126 125 .join(User, User.user_id == UserGroup.user_id) \
127 126 .group_by(UserGroup, User)
128 127
129 128 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
130 129
131 130 if search_q:
132 like_expression = u'%{}%'.format(safe_unicode(search_q))
131 like_expression = u'%{}%'.format(safe_str(search_q))
133 132 base_q = base_q.filter(or_(
134 133 UserGroup.users_group_name.ilike(like_expression),
135 134 ))
136 135 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
137 136
138 137 user_groups_data_total_filtered_count = base_q.count()
139 138 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
140 139
141 140 sort_defined = False
142 141 if order_by == 'members_total':
143 142 sort_col = member_count
144 143 sort_defined = True
145 144 elif order_by == 'user_username':
146 145 sort_col = User.username
147 146 else:
148 147 sort_col = getattr(UserGroup, order_by, None)
149 148
150 149 if sort_defined or sort_col:
151 150 if order_dir == 'asc':
152 151 sort_col = sort_col.asc()
153 152 else:
154 153 sort_col = sort_col.desc()
155 154
156 155 base_q = base_q.order_by(sort_col)
157 156 base_q = base_q.offset(start).limit(limit)
158 157
159 158 # authenticated access to user groups
160 159 auth_user_group_list = base_q.all()
161 160
162 161 user_groups_data = []
163 162 for user_gr in auth_user_group_list:
164 163 row = {
165 164 "users_group_name": user_group_name(user_gr.users_group_name),
166 165 "description": h.escape(user_gr.user_group_description),
167 166 "members": user_gr.member_count,
168 167 # NOTE(marcink): because of advanced query we
169 168 # need to load it like that
170 169 "sync": UserGroup._load_sync(
171 170 UserGroup._load_group_data(user_gr.group_data)),
172 171 "active": h.bool2icon(user_gr.users_group_active),
173 172 "owner": user_profile(user_gr.User.username),
174 173 "action": user_group_actions(
175 174 user_gr.users_group_id, user_gr.users_group_name)
176 175 }
177 176 user_groups_data.append(row)
178 177
179 178 data = ({
180 179 'draw': draw,
181 180 'data': user_groups_data,
182 181 'recordsTotal': user_groups_data_total_count,
183 182 'recordsTotalInactive': user_groups_data_total_inactive_count,
184 183 'recordsFiltered': user_groups_data_total_filtered_count,
185 184 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
186 185 })
187 186
188 187 return data
189 188
190 189 @LoginRequired()
191 190 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 191 def user_groups_new(self):
193 192 c = self.load_default_context()
194 193 return self._get_template_context(c)
195 194
196 195 @LoginRequired()
197 196 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
198 197 @CSRFRequired()
199 198 def user_groups_create(self):
200 199 _ = self.request.translate
201 200 c = self.load_default_context()
202 201 users_group_form = UserGroupForm(self.request.translate)()
203 202
204 203 user_group_name = self.request.POST.get('users_group_name')
205 204 try:
206 205 form_result = users_group_form.to_python(dict(self.request.POST))
207 206 user_group = UserGroupModel().create(
208 207 name=form_result['users_group_name'],
209 208 description=form_result['user_group_description'],
210 209 owner=self._rhodecode_user.user_id,
211 210 active=form_result['users_group_active'])
212 211 Session().flush()
213 212 creation_data = user_group.get_api_data()
214 213 user_group_name = form_result['users_group_name']
215 214
216 215 audit_logger.store_web(
217 216 'user_group.create', action_data={'data': creation_data},
218 217 user=self._rhodecode_user)
219 218
220 219 user_group_link = h.link_to(
221 220 h.escape(user_group_name),
222 221 h.route_path(
223 222 'edit_user_group', user_group_id=user_group.users_group_id))
224 223 h.flash(h.literal(_('Created user group %(user_group_link)s')
225 224 % {'user_group_link': user_group_link}),
226 225 category='success')
227 226 Session().commit()
228 227 user_group_id = user_group.users_group_id
229 228 except formencode.Invalid as errors:
230 229
231 230 data = render(
232 231 'rhodecode:templates/admin/user_groups/user_group_add.mako',
233 232 self._get_template_context(c), self.request)
234 233 html = formencode.htmlfill.render(
235 234 data,
236 235 defaults=errors.value,
237 236 errors=errors.unpack_errors() or {},
238 237 prefix_error=False,
239 238 encoding="UTF-8",
240 239 force_defaults=False
241 240 )
242 241 return Response(html)
243 242
244 243 except Exception:
245 244 log.exception("Exception creating user group")
246 245 h.flash(_('Error occurred during creation of user group %s') \
247 246 % user_group_name, category='error')
248 247 raise HTTPFound(h.route_path('user_groups_new'))
249 248
250 249 PermissionModel().trigger_permission_flush()
251 250
252 251 raise HTTPFound(
253 252 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1322 +1,1323 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 34 from rhodecode.authentication.plugins import auth_rhodecode
35 35 from rhodecode.events import trigger
36 36 from rhodecode.model.db import true, UserNotice
37 37
38 38 from rhodecode.lib import audit_logger, rc_cache, auth
39 39 from rhodecode.lib.exceptions import (
40 40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 42 UserOwnsArtifactsException, DefaultUserException)
43 43 from rhodecode.lib import ext_json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 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 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.forms import (
51 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 52 UserExtraEmailForm, UserExtraIpForm)
53 53 from rhodecode.model.permission import PermissionModel
54 54 from rhodecode.model.repo_group import RepoGroupModel
55 55 from rhodecode.model.ssh_key import SshKeyModel
56 56 from rhodecode.model.user import UserModel
57 57 from rhodecode.model.user_group import UserGroupModel
58 58 from rhodecode.model.db import (
59 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 60 UserApiKeys, UserSshKeys, RepoGroup)
61 61 from rhodecode.model.meta import Session
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class AdminUsersView(BaseAppView, DataGridAppView):
67 67
68 68 def load_default_context(self):
69 69 c = self._get_local_tmpl_context()
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @HasPermissionAllDecorator('hg.admin')
74 74 def users_list(self):
75 75 c = self.load_default_context()
76 76 return self._get_template_context(c)
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def users_list_data(self):
81 81 self.load_default_context()
82 82 column_map = {
83 83 'first_name': 'name',
84 84 'last_name': 'lastname',
85 85 }
86 86 draw, start, limit = self._extract_chunk(self.request)
87 87 search_q, order_by, order_dir = self._extract_ordering(
88 88 self.request, column_map=column_map)
89 89 _render = self.request.get_partial_renderer(
90 90 'rhodecode:templates/data_table/_dt_elements.mako')
91 91
92 92 def user_actions(user_id, username):
93 93 return _render("user_actions", user_id, username)
94 94
95 95 users_data_total_count = User.query()\
96 96 .filter(User.username != User.DEFAULT_USER) \
97 97 .count()
98 98
99 99 users_data_total_inactive_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .filter(User.active != true())\
102 102 .count()
103 103
104 104 # json generate
105 105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 106 base_inactive_q = base_q.filter(User.active != true())
107 107
108 108 if search_q:
109 like_expression = '%{}%'.format(safe_unicode(search_q))
109 like_expression = '%{}%'.format(safe_str(search_q))
110 110 base_q = base_q.filter(or_(
111 111 User.username.ilike(like_expression),
112 112 User._email.ilike(like_expression),
113 113 User.name.ilike(like_expression),
114 114 User.lastname.ilike(like_expression),
115 115 ))
116 116 base_inactive_q = base_q.filter(User.active != true())
117 117
118 118 users_data_total_filtered_count = base_q.count()
119 119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120 120
121 121 sort_col = getattr(User, order_by, None)
122 122 if sort_col:
123 123 if order_dir == 'asc':
124 124 # handle null values properly to order by NULL last
125 125 if order_by in ['last_activity']:
126 126 sort_col = coalesce(sort_col, datetime.date.max)
127 127 sort_col = sort_col.asc()
128 128 else:
129 129 # handle null values properly to order by NULL last
130 130 if order_by in ['last_activity']:
131 131 sort_col = coalesce(sort_col, datetime.date.min)
132 132 sort_col = sort_col.desc()
133 133
134 134 base_q = base_q.order_by(sort_col)
135 135 base_q = base_q.offset(start).limit(limit)
136 136
137 137 users_list = base_q.all()
138 138
139 139 users_data = []
140 140 for user in users_list:
141 141 users_data.append({
142 142 "username": h.gravatar_with_user(self.request, user.username),
143 143 "email": user.email,
144 144 "first_name": user.first_name,
145 145 "last_name": user.last_name,
146 146 "last_login": h.format_date(user.last_login),
147 147 "last_activity": h.format_date(user.last_activity),
148 148 "active": h.bool2icon(user.active),
149 149 "active_raw": user.active,
150 150 "admin": h.bool2icon(user.admin),
151 151 "extern_type": user.extern_type,
152 152 "extern_name": user.extern_name,
153 153 "action": user_actions(user.user_id, user.username),
154 154 })
155 155 data = ({
156 156 'draw': draw,
157 157 'data': users_data,
158 158 'recordsTotal': users_data_total_count,
159 159 'recordsFiltered': users_data_total_filtered_count,
160 160 'recordsTotalInactive': users_data_total_inactive_count,
161 161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 162 })
163 163
164 164 return data
165 165
166 166 def _set_personal_repo_group_template_vars(self, c_obj):
167 167 DummyUser = AttributeDict({
168 168 'username': '${username}',
169 169 'user_id': '${user_id}',
170 170 })
171 171 c_obj.default_create_repo_group = RepoGroupModel() \
172 172 .get_default_create_personal_repo_group()
173 173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 174 .get_personal_group_name(DummyUser)
175 175
176 176 @LoginRequired()
177 177 @HasPermissionAllDecorator('hg.admin')
178 178 def users_new(self):
179 179 _ = self.request.translate
180 180 c = self.load_default_context()
181 181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 182 self._set_personal_repo_group_template_vars(c)
183 183 return self._get_template_context(c)
184 184
185 185 @LoginRequired()
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @CSRFRequired()
188 188 def users_create(self):
189 189 _ = self.request.translate
190 190 c = self.load_default_context()
191 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 192 user_model = UserModel()
193 193 user_form = UserForm(self.request.translate)()
194 194 try:
195 195 form_result = user_form.to_python(dict(self.request.POST))
196 196 user = user_model.create(form_result)
197 197 Session().flush()
198 198 creation_data = user.get_api_data()
199 199 username = form_result['username']
200 200
201 201 audit_logger.store_web(
202 202 'user.create', action_data={'data': creation_data},
203 203 user=c.rhodecode_user)
204 204
205 205 user_link = h.link_to(
206 206 h.escape(username),
207 207 h.route_path('user_edit', user_id=user.user_id))
208 208 h.flash(h.literal(_('Created user %(user_link)s')
209 209 % {'user_link': user_link}), category='success')
210 210 Session().commit()
211 211 except formencode.Invalid as errors:
212 212 self._set_personal_repo_group_template_vars(c)
213 213 data = render(
214 214 'rhodecode:templates/admin/users/user_add.mako',
215 215 self._get_template_context(c), self.request)
216 216 html = formencode.htmlfill.render(
217 217 data,
218 218 defaults=errors.value,
219 219 errors=errors.unpack_errors() or {},
220 220 prefix_error=False,
221 221 encoding="UTF-8",
222 222 force_defaults=False
223 223 )
224 224 return Response(html)
225 225 except UserCreationError as e:
226 h.flash(safe_unicode(e), 'error')
226 h.flash(safe_str(e), 'error')
227 227 except Exception:
228 228 log.exception("Exception creation of user")
229 229 h.flash(_('Error occurred during creation of user %s')
230 230 % self.request.POST.get('username'), category='error')
231 231 raise HTTPFound(h.route_path('users'))
232 232
233 233
234 234 class UsersView(UserAppView):
235 235 ALLOW_SCOPED_TOKENS = False
236 236 """
237 237 This view has alternative version inside EE, if modified please take a look
238 238 in there as well.
239 239 """
240 240
241 241 def get_auth_plugins(self):
242 242 valid_plugins = []
243 243 authn_registry = get_authn_registry(self.request.registry)
244 244 for plugin in authn_registry.get_plugins_for_authentication():
245 245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 246 valid_plugins.append(plugin)
247 247 elif plugin.name == 'rhodecode':
248 248 valid_plugins.append(plugin)
249 249
250 250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 251 # moment
252 252 extern_type = self.db_user.extern_type
253 253 if extern_type not in [x.uid for x in valid_plugins]:
254 254 try:
255 255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 256 if plugin:
257 257 valid_plugins.append(plugin)
258 258
259 259 except Exception:
260 260 log.exception(
261 261 'Could not extend user plugins with `{}`'.format(extern_type))
262 262 return valid_plugins
263 263
264 264 def load_default_context(self):
265 265 req = self.request
266 266
267 267 c = self._get_local_tmpl_context()
268 268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 269 c.allowed_languages = [
270 270 ('en', 'English (en)'),
271 271 ('de', 'German (de)'),
272 272 ('fr', 'French (fr)'),
273 273 ('it', 'Italian (it)'),
274 274 ('ja', 'Japanese (ja)'),
275 275 ('pl', 'Polish (pl)'),
276 276 ('pt', 'Portuguese (pt)'),
277 277 ('ru', 'Russian (ru)'),
278 278 ('zh', 'Chinese (zh)'),
279 279 ]
280 280
281 281 c.allowed_extern_types = [
282 282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 283 ]
284 284 perms = req.registry.settings.get('available_permissions')
285 285 if not perms:
286 286 # inject info about available permissions
287 287 auth.set_available_permissions(req.registry.settings)
288 288
289 289 c.available_permissions = req.registry.settings['available_permissions']
290 290 PermissionModel().set_global_permission_choices(
291 291 c, gettext_translator=req.translate)
292 292
293 293 return c
294 294
295 295 @LoginRequired()
296 296 @HasPermissionAllDecorator('hg.admin')
297 297 @CSRFRequired()
298 298 def user_update(self):
299 299 _ = self.request.translate
300 300 c = self.load_default_context()
301 301
302 302 user_id = self.db_user_id
303 303 c.user = self.db_user
304 304
305 305 c.active = 'profile'
306 306 c.extern_type = c.user.extern_type
307 307 c.extern_name = c.user.extern_name
308 308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 309 available_languages = [x[0] for x in c.allowed_languages]
310 310 _form = UserForm(self.request.translate, edit=True,
311 311 available_languages=available_languages,
312 312 old_data={'user_id': user_id,
313 313 'email': c.user.email})()
314 314
315 315 c.edit_mode = self.request.POST.get('edit') == '1'
316 316 form_result = {}
317 317 old_values = c.user.get_api_data()
318 318 try:
319 319 form_result = _form.to_python(dict(self.request.POST))
320 320 skip_attrs = ['extern_name']
321 321 # TODO: plugin should define if username can be updated
322 322
323 323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 324 # forbid updating username for external accounts
325 325 skip_attrs.append('username')
326 326
327 327 UserModel().update_user(
328 328 user_id, skip_attrs=skip_attrs, **form_result)
329 329
330 330 audit_logger.store_web(
331 331 'user.edit', action_data={'old_data': old_values},
332 332 user=c.rhodecode_user)
333 333
334 334 Session().commit()
335 335 h.flash(_('User updated successfully'), category='success')
336 336 except formencode.Invalid as errors:
337 337 data = render(
338 338 'rhodecode:templates/admin/users/user_edit.mako',
339 339 self._get_template_context(c), self.request)
340 340 html = formencode.htmlfill.render(
341 341 data,
342 342 defaults=errors.value,
343 343 errors=errors.unpack_errors() or {},
344 344 prefix_error=False,
345 345 encoding="UTF-8",
346 346 force_defaults=False
347 347 )
348 348 return Response(html)
349 349 except UserCreationError as e:
350 h.flash(safe_unicode(e), 'error')
350 h.flash(safe_str(e), 'error')
351 351 except Exception:
352 352 log.exception("Exception updating user")
353 353 h.flash(_('Error occurred during update of user %s')
354 354 % form_result.get('username'), category='error')
355 355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356 356
357 357 @LoginRequired()
358 358 @HasPermissionAllDecorator('hg.admin')
359 359 @CSRFRequired()
360 360 def user_delete(self):
361 361 _ = self.request.translate
362 362 c = self.load_default_context()
363 363 c.user = self.db_user
364 364
365 365 _repos = c.user.repositories
366 366 _repo_groups = c.user.repository_groups
367 367 _user_groups = c.user.user_groups
368 368 _pull_requests = c.user.user_pull_requests
369 369 _artifacts = c.user.artifacts
370 370
371 371 handle_repos = None
372 372 handle_repo_groups = None
373 373 handle_user_groups = None
374 374 handle_pull_requests = None
375 375 handle_artifacts = None
376 376
377 377 # calls for flash of handle based on handle case detach or delete
378 378 def set_handle_flash_repos():
379 379 handle = handle_repos
380 380 if handle == 'detach':
381 381 h.flash(_('Detached %s repositories') % len(_repos),
382 382 category='success')
383 383 elif handle == 'delete':
384 384 h.flash(_('Deleted %s repositories') % len(_repos),
385 385 category='success')
386 386
387 387 def set_handle_flash_repo_groups():
388 388 handle = handle_repo_groups
389 389 if handle == 'detach':
390 390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
391 391 category='success')
392 392 elif handle == 'delete':
393 393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
394 394 category='success')
395 395
396 396 def set_handle_flash_user_groups():
397 397 handle = handle_user_groups
398 398 if handle == 'detach':
399 399 h.flash(_('Detached %s user groups') % len(_user_groups),
400 400 category='success')
401 401 elif handle == 'delete':
402 402 h.flash(_('Deleted %s user groups') % len(_user_groups),
403 403 category='success')
404 404
405 405 def set_handle_flash_pull_requests():
406 406 handle = handle_pull_requests
407 407 if handle == 'detach':
408 408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
409 409 category='success')
410 410 elif handle == 'delete':
411 411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
412 412 category='success')
413 413
414 414 def set_handle_flash_artifacts():
415 415 handle = handle_artifacts
416 416 if handle == 'detach':
417 417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 418 category='success')
419 419 elif handle == 'delete':
420 420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 421 category='success')
422 422
423 423 handle_user = User.get_first_super_admin()
424 424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 425 if handle_user_id:
426 426 # NOTE(marcink): we get new owner for objects...
427 427 handle_user = User.get_or_404(handle_user_id)
428 428
429 429 if _repos and self.request.POST.get('user_repos'):
430 430 handle_repos = self.request.POST['user_repos']
431 431
432 432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 433 handle_repo_groups = self.request.POST['user_repo_groups']
434 434
435 435 if _user_groups and self.request.POST.get('user_user_groups'):
436 436 handle_user_groups = self.request.POST['user_user_groups']
437 437
438 438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 439 handle_pull_requests = self.request.POST['user_pull_requests']
440 440
441 441 if _artifacts and self.request.POST.get('user_artifacts'):
442 442 handle_artifacts = self.request.POST['user_artifacts']
443 443
444 444 old_values = c.user.get_api_data()
445 445
446 446 try:
447 447
448 448 UserModel().delete(
449 449 c.user,
450 450 handle_repos=handle_repos,
451 451 handle_repo_groups=handle_repo_groups,
452 452 handle_user_groups=handle_user_groups,
453 453 handle_pull_requests=handle_pull_requests,
454 454 handle_artifacts=handle_artifacts,
455 455 handle_new_owner=handle_user
456 456 )
457 457
458 458 audit_logger.store_web(
459 459 'user.delete', action_data={'old_data': old_values},
460 460 user=c.rhodecode_user)
461 461
462 462 Session().commit()
463 463 set_handle_flash_repos()
464 464 set_handle_flash_repo_groups()
465 465 set_handle_flash_user_groups()
466 466 set_handle_flash_pull_requests()
467 467 set_handle_flash_artifacts()
468 468 username = h.escape(old_values['username'])
469 469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 472 UserOwnsArtifactsException, DefaultUserException) as e:
473 h.flash(e, category='warning')
473
474 h.flash(safe_str(e), category='warning')
474 475 except Exception:
475 476 log.exception("Exception during deletion of user")
476 477 h.flash(_('An error occurred during deletion of user'),
477 478 category='error')
478 479 raise HTTPFound(h.route_path('users'))
479 480
480 481 @LoginRequired()
481 482 @HasPermissionAllDecorator('hg.admin')
482 483 def user_edit(self):
483 484 _ = self.request.translate
484 485 c = self.load_default_context()
485 486 c.user = self.db_user
486 487
487 488 c.active = 'profile'
488 489 c.extern_type = c.user.extern_type
489 490 c.extern_name = c.user.extern_name
490 491 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 492 c.edit_mode = self.request.GET.get('edit') == '1'
492 493
493 494 defaults = c.user.get_dict()
494 495 defaults.update({'language': c.user.user_data.get('language')})
495 496
496 497 data = render(
497 498 'rhodecode:templates/admin/users/user_edit.mako',
498 499 self._get_template_context(c), self.request)
499 500 html = formencode.htmlfill.render(
500 501 data,
501 502 defaults=defaults,
502 503 encoding="UTF-8",
503 504 force_defaults=False
504 505 )
505 506 return Response(html)
506 507
507 508 @LoginRequired()
508 509 @HasPermissionAllDecorator('hg.admin')
509 510 def user_edit_advanced(self):
510 511 _ = self.request.translate
511 512 c = self.load_default_context()
512 513
513 514 user_id = self.db_user_id
514 515 c.user = self.db_user
515 516
516 517 c.detach_user = User.get_first_super_admin()
517 518 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 519 if detach_user_id:
519 520 c.detach_user = User.get_or_404(detach_user_id)
520 521
521 522 c.active = 'advanced'
522 523 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 524 c.personal_repo_group_name = RepoGroupModel()\
524 525 .get_personal_group_name(c.user)
525 526
526 527 c.user_to_review_rules = sorted(
527 528 (x.user for x in c.user.user_review_rules),
528 529 key=lambda u: u.username.lower())
529 530
530 531 defaults = c.user.get_dict()
531 532
532 533 # Interim workaround if the user participated on any pull requests as a
533 534 # reviewer.
534 535 has_review = len(c.user.reviewer_pull_requests)
535 536 c.can_delete_user = not has_review
536 537 c.can_delete_user_message = ''
537 538 inactive_link = h.link_to(
538 539 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 540 if has_review == 1:
540 541 c.can_delete_user_message = h.literal(_(
541 542 'The user participates as reviewer in {} pull request and '
542 543 'cannot be deleted. \nYou can set the user to '
543 544 '"{}" instead of deleting it.').format(
544 545 has_review, inactive_link))
545 546 elif has_review:
546 547 c.can_delete_user_message = h.literal(_(
547 548 'The user participates as reviewer in {} pull requests and '
548 549 'cannot be deleted. \nYou can set the user to '
549 550 '"{}" instead of deleting it.').format(
550 551 has_review, inactive_link))
551 552
552 553 data = render(
553 554 'rhodecode:templates/admin/users/user_edit.mako',
554 555 self._get_template_context(c), self.request)
555 556 html = formencode.htmlfill.render(
556 557 data,
557 558 defaults=defaults,
558 559 encoding="UTF-8",
559 560 force_defaults=False
560 561 )
561 562 return Response(html)
562 563
563 564 @LoginRequired()
564 565 @HasPermissionAllDecorator('hg.admin')
565 566 def user_edit_global_perms(self):
566 567 _ = self.request.translate
567 568 c = self.load_default_context()
568 569 c.user = self.db_user
569 570
570 571 c.active = 'global_perms'
571 572
572 573 c.default_user = User.get_default_user()
573 574 defaults = c.user.get_dict()
574 575 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 576 defaults.update(c.default_user.get_default_perms())
576 577 defaults.update(c.user.get_default_perms())
577 578
578 579 data = render(
579 580 'rhodecode:templates/admin/users/user_edit.mako',
580 581 self._get_template_context(c), self.request)
581 582 html = formencode.htmlfill.render(
582 583 data,
583 584 defaults=defaults,
584 585 encoding="UTF-8",
585 586 force_defaults=False
586 587 )
587 588 return Response(html)
588 589
589 590 @LoginRequired()
590 591 @HasPermissionAllDecorator('hg.admin')
591 592 @CSRFRequired()
592 593 def user_edit_global_perms_update(self):
593 594 _ = self.request.translate
594 595 c = self.load_default_context()
595 596
596 597 user_id = self.db_user_id
597 598 c.user = self.db_user
598 599
599 600 c.active = 'global_perms'
600 601 try:
601 602 # first stage that verifies the checkbox
602 603 _form = UserIndividualPermissionsForm(self.request.translate)
603 604 form_result = _form.to_python(dict(self.request.POST))
604 605 inherit_perms = form_result['inherit_default_permissions']
605 606 c.user.inherit_default_permissions = inherit_perms
606 607 Session().add(c.user)
607 608
608 609 if not inherit_perms:
609 610 # only update the individual ones if we un check the flag
610 611 _form = UserPermissionsForm(
611 612 self.request.translate,
612 613 [x[0] for x in c.repo_create_choices],
613 614 [x[0] for x in c.repo_create_on_write_choices],
614 615 [x[0] for x in c.repo_group_create_choices],
615 616 [x[0] for x in c.user_group_create_choices],
616 617 [x[0] for x in c.fork_choices],
617 618 [x[0] for x in c.inherit_default_permission_choices])()
618 619
619 620 form_result = _form.to_python(dict(self.request.POST))
620 621 form_result.update({'perm_user_id': c.user.user_id})
621 622
622 623 PermissionModel().update_user_permissions(form_result)
623 624
624 625 # TODO(marcink): implement global permissions
625 626 # audit_log.store_web('user.edit.permissions')
626 627
627 628 Session().commit()
628 629
629 630 h.flash(_('User global permissions updated successfully'),
630 631 category='success')
631 632
632 633 except formencode.Invalid as errors:
633 634 data = render(
634 635 'rhodecode:templates/admin/users/user_edit.mako',
635 636 self._get_template_context(c), self.request)
636 637 html = formencode.htmlfill.render(
637 638 data,
638 639 defaults=errors.value,
639 640 errors=errors.unpack_errors() or {},
640 641 prefix_error=False,
641 642 encoding="UTF-8",
642 643 force_defaults=False
643 644 )
644 645 return Response(html)
645 646 except Exception:
646 647 log.exception("Exception during permissions saving")
647 648 h.flash(_('An error occurred during permissions saving'),
648 649 category='error')
649 650
650 651 affected_user_ids = [user_id]
651 652 PermissionModel().trigger_permission_flush(affected_user_ids)
652 653 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653 654
654 655 @LoginRequired()
655 656 @HasPermissionAllDecorator('hg.admin')
656 657 @CSRFRequired()
657 658 def user_enable_force_password_reset(self):
658 659 _ = self.request.translate
659 660 c = self.load_default_context()
660 661
661 662 user_id = self.db_user_id
662 663 c.user = self.db_user
663 664
664 665 try:
665 666 c.user.update_userdata(force_password_change=True)
666 667
667 668 msg = _('Force password change enabled for user')
668 669 audit_logger.store_web('user.edit.password_reset.enabled',
669 670 user=c.rhodecode_user)
670 671
671 672 Session().commit()
672 673 h.flash(msg, category='success')
673 674 except Exception:
674 675 log.exception("Exception during password reset for user")
675 676 h.flash(_('An error occurred during password reset for user'),
676 677 category='error')
677 678
678 679 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679 680
680 681 @LoginRequired()
681 682 @HasPermissionAllDecorator('hg.admin')
682 683 @CSRFRequired()
683 684 def user_disable_force_password_reset(self):
684 685 _ = self.request.translate
685 686 c = self.load_default_context()
686 687
687 688 user_id = self.db_user_id
688 689 c.user = self.db_user
689 690
690 691 try:
691 692 c.user.update_userdata(force_password_change=False)
692 693
693 694 msg = _('Force password change disabled for user')
694 695 audit_logger.store_web(
695 696 'user.edit.password_reset.disabled',
696 697 user=c.rhodecode_user)
697 698
698 699 Session().commit()
699 700 h.flash(msg, category='success')
700 701 except Exception:
701 702 log.exception("Exception during password reset for user")
702 703 h.flash(_('An error occurred during password reset for user'),
703 704 category='error')
704 705
705 706 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706 707
707 708 @LoginRequired()
708 709 @HasPermissionAllDecorator('hg.admin')
709 710 @CSRFRequired()
710 711 def user_notice_dismiss(self):
711 712 _ = self.request.translate
712 713 c = self.load_default_context()
713 714
714 715 user_id = self.db_user_id
715 716 c.user = self.db_user
716 717 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 718 notice = UserNotice().query()\
718 719 .filter(UserNotice.user_id == user_id)\
719 720 .filter(UserNotice.user_notice_id == user_notice_id)\
720 721 .scalar()
721 722 read = False
722 723 if notice:
723 724 notice.notice_read = True
724 725 Session().add(notice)
725 726 Session().commit()
726 727 read = True
727 728
728 729 return {'notice': user_notice_id, 'read': read}
729 730
730 731 @LoginRequired()
731 732 @HasPermissionAllDecorator('hg.admin')
732 733 @CSRFRequired()
733 734 def user_create_personal_repo_group(self):
734 735 """
735 736 Create personal repository group for this user
736 737 """
737 738 from rhodecode.model.repo_group import RepoGroupModel
738 739
739 740 _ = self.request.translate
740 741 c = self.load_default_context()
741 742
742 743 user_id = self.db_user_id
743 744 c.user = self.db_user
744 745
745 746 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 747 c.user.user_id)
747 748 if personal_repo_group:
748 749 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749 750
750 751 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 752 named_personal_group = RepoGroup.get_by_group_name(
752 753 personal_repo_group_name)
753 754 try:
754 755
755 756 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 757 # migrate the same named group, and mark it as personal
757 758 named_personal_group.personal = True
758 759 Session().add(named_personal_group)
759 760 Session().commit()
760 761 msg = _('Linked repository group `%s` as personal' % (
761 762 personal_repo_group_name,))
762 763 h.flash(msg, category='success')
763 764 elif not named_personal_group:
764 765 RepoGroupModel().create_personal_repo_group(c.user)
765 766
766 767 msg = _('Created repository group `%s`' % (
767 768 personal_repo_group_name,))
768 769 h.flash(msg, category='success')
769 770 else:
770 771 msg = _('Repository group `%s` is already taken' % (
771 772 personal_repo_group_name,))
772 773 h.flash(msg, category='warning')
773 774 except Exception:
774 775 log.exception("Exception during repository group creation")
775 776 msg = _(
776 777 'An error occurred during repository group creation for user')
777 778 h.flash(msg, category='error')
778 779 Session().rollback()
779 780
780 781 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781 782
782 783 @LoginRequired()
783 784 @HasPermissionAllDecorator('hg.admin')
784 785 def auth_tokens(self):
785 786 _ = self.request.translate
786 787 c = self.load_default_context()
787 788 c.user = self.db_user
788 789
789 790 c.active = 'auth_tokens'
790 791
791 792 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 793 c.role_values = [
793 794 (x, AuthTokenModel.cls._get_role_name(x))
794 795 for x in AuthTokenModel.cls.ROLES]
795 796 c.role_options = [(c.role_values, _("Role"))]
796 797 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 798 c.user.user_id, show_expired=True)
798 799 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 800 return self._get_template_context(c)
800 801
801 802 @LoginRequired()
802 803 @HasPermissionAllDecorator('hg.admin')
803 804 def auth_tokens_view(self):
804 805 _ = self.request.translate
805 806 c = self.load_default_context()
806 807 c.user = self.db_user
807 808
808 809 auth_token_id = self.request.POST.get('auth_token_id')
809 810
810 811 if auth_token_id:
811 812 token = UserApiKeys.get_or_404(auth_token_id)
812 813
813 814 return {
814 815 'auth_token': token.api_key
815 816 }
816 817
817 818 def maybe_attach_token_scope(self, token):
818 819 # implemented in EE edition
819 820 pass
820 821
821 822 @LoginRequired()
822 823 @HasPermissionAllDecorator('hg.admin')
823 824 @CSRFRequired()
824 825 def auth_tokens_add(self):
825 826 _ = self.request.translate
826 827 c = self.load_default_context()
827 828
828 829 user_id = self.db_user_id
829 830 c.user = self.db_user
830 831
831 832 user_data = c.user.get_api_data()
832 833 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 834 description = self.request.POST.get('description')
834 835 role = self.request.POST.get('role')
835 836
836 837 token = UserModel().add_auth_token(
837 838 user=c.user.user_id,
838 839 lifetime_minutes=lifetime, role=role, description=description,
839 840 scope_callback=self.maybe_attach_token_scope)
840 841 token_data = token.get_api_data()
841 842
842 843 audit_logger.store_web(
843 844 'user.edit.token.add', action_data={
844 845 'data': {'token': token_data, 'user': user_data}},
845 846 user=self._rhodecode_user, )
846 847 Session().commit()
847 848
848 849 h.flash(_("Auth token successfully created"), category='success')
849 850 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850 851
851 852 @LoginRequired()
852 853 @HasPermissionAllDecorator('hg.admin')
853 854 @CSRFRequired()
854 855 def auth_tokens_delete(self):
855 856 _ = self.request.translate
856 857 c = self.load_default_context()
857 858
858 859 user_id = self.db_user_id
859 860 c.user = self.db_user
860 861
861 862 user_data = c.user.get_api_data()
862 863
863 864 del_auth_token = self.request.POST.get('del_auth_token')
864 865
865 866 if del_auth_token:
866 867 token = UserApiKeys.get_or_404(del_auth_token)
867 868 token_data = token.get_api_data()
868 869
869 870 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 871 audit_logger.store_web(
871 872 'user.edit.token.delete', action_data={
872 873 'data': {'token': token_data, 'user': user_data}},
873 874 user=self._rhodecode_user,)
874 875 Session().commit()
875 876 h.flash(_("Auth token successfully deleted"), category='success')
876 877
877 878 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878 879
879 880 @LoginRequired()
880 881 @HasPermissionAllDecorator('hg.admin')
881 882 def ssh_keys(self):
882 883 _ = self.request.translate
883 884 c = self.load_default_context()
884 885 c.user = self.db_user
885 886
886 887 c.active = 'ssh_keys'
887 888 c.default_key = self.request.GET.get('default_key')
888 889 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 890 return self._get_template_context(c)
890 891
891 892 @LoginRequired()
892 893 @HasPermissionAllDecorator('hg.admin')
893 894 def ssh_keys_generate_keypair(self):
894 895 _ = self.request.translate
895 896 c = self.load_default_context()
896 897
897 898 c.user = self.db_user
898 899
899 900 c.active = 'ssh_keys_generate'
900 901 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 902 private_format = self.request.GET.get('private_format') \
902 903 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 904 c.private, c.public = SshKeyModel().generate_keypair(
904 905 comment=comment, private_format=private_format)
905 906
906 907 return self._get_template_context(c)
907 908
908 909 @LoginRequired()
909 910 @HasPermissionAllDecorator('hg.admin')
910 911 @CSRFRequired()
911 912 def ssh_keys_add(self):
912 913 _ = self.request.translate
913 914 c = self.load_default_context()
914 915
915 916 user_id = self.db_user_id
916 917 c.user = self.db_user
917 918
918 919 user_data = c.user.get_api_data()
919 920 key_data = self.request.POST.get('key_data')
920 921 description = self.request.POST.get('description')
921 922
922 923 fingerprint = 'unknown'
923 924 try:
924 925 if not key_data:
925 926 raise ValueError('Please add a valid public key')
926 927
927 928 key = SshKeyModel().parse_key(key_data.strip())
928 929 fingerprint = key.hash_md5()
929 930
930 931 ssh_key = SshKeyModel().create(
931 932 c.user.user_id, fingerprint, key.keydata, description)
932 933 ssh_key_data = ssh_key.get_api_data()
933 934
934 935 audit_logger.store_web(
935 936 'user.edit.ssh_key.add', action_data={
936 937 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 938 user=self._rhodecode_user, )
938 939 Session().commit()
939 940
940 941 # Trigger an event on change of keys.
941 942 trigger(SshKeyFileChangeEvent(), self.request.registry)
942 943
943 944 h.flash(_("Ssh Key successfully created"), category='success')
944 945
945 946 except IntegrityError:
946 947 log.exception("Exception during ssh key saving")
947 948 err = 'Such key with fingerprint `{}` already exists, ' \
948 949 'please use a different one'.format(fingerprint)
949 950 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 951 category='error')
951 952 except Exception as e:
952 953 log.exception("Exception during ssh key saving")
953 954 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 955 category='error')
955 956
956 957 return HTTPFound(
957 958 h.route_path('edit_user_ssh_keys', user_id=user_id))
958 959
959 960 @LoginRequired()
960 961 @HasPermissionAllDecorator('hg.admin')
961 962 @CSRFRequired()
962 963 def ssh_keys_delete(self):
963 964 _ = self.request.translate
964 965 c = self.load_default_context()
965 966
966 967 user_id = self.db_user_id
967 968 c.user = self.db_user
968 969
969 970 user_data = c.user.get_api_data()
970 971
971 972 del_ssh_key = self.request.POST.get('del_ssh_key')
972 973
973 974 if del_ssh_key:
974 975 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 976 ssh_key_data = ssh_key.get_api_data()
976 977
977 978 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 979 audit_logger.store_web(
979 980 'user.edit.ssh_key.delete', action_data={
980 981 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 982 user=self._rhodecode_user,)
982 983 Session().commit()
983 984 # Trigger an event on change of keys.
984 985 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 986 h.flash(_("Ssh key successfully deleted"), category='success')
986 987
987 988 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988 989
989 990 @LoginRequired()
990 991 @HasPermissionAllDecorator('hg.admin')
991 992 def emails(self):
992 993 _ = self.request.translate
993 994 c = self.load_default_context()
994 995 c.user = self.db_user
995 996
996 997 c.active = 'emails'
997 998 c.user_email_map = UserEmailMap.query() \
998 999 .filter(UserEmailMap.user == c.user).all()
999 1000
1000 1001 return self._get_template_context(c)
1001 1002
1002 1003 @LoginRequired()
1003 1004 @HasPermissionAllDecorator('hg.admin')
1004 1005 @CSRFRequired()
1005 1006 def emails_add(self):
1006 1007 _ = self.request.translate
1007 1008 c = self.load_default_context()
1008 1009
1009 1010 user_id = self.db_user_id
1010 1011 c.user = self.db_user
1011 1012
1012 1013 email = self.request.POST.get('new_email')
1013 1014 user_data = c.user.get_api_data()
1014 1015 try:
1015 1016
1016 1017 form = UserExtraEmailForm(self.request.translate)()
1017 1018 data = form.to_python({'email': email})
1018 1019 email = data['email']
1019 1020
1020 1021 UserModel().add_extra_email(c.user.user_id, email)
1021 1022 audit_logger.store_web(
1022 1023 'user.edit.email.add',
1023 1024 action_data={'email': email, 'user': user_data},
1024 1025 user=self._rhodecode_user)
1025 1026 Session().commit()
1026 1027 h.flash(_("Added new email address `%s` for user account") % email,
1027 1028 category='success')
1028 1029 except formencode.Invalid as error:
1029 1030 msg = error.unpack_errors()['email']
1030 1031 h.flash(h.escape(msg), category='error')
1031 1032 except IntegrityError:
1032 1033 log.warning("Email %s already exists", email)
1033 1034 h.flash(_('Email `{}` is already registered for another user.').format(email),
1034 1035 category='error')
1035 1036 except Exception:
1036 1037 log.exception("Exception during email saving")
1037 1038 h.flash(_('An error occurred during email saving'),
1038 1039 category='error')
1039 1040 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1040 1041
1041 1042 @LoginRequired()
1042 1043 @HasPermissionAllDecorator('hg.admin')
1043 1044 @CSRFRequired()
1044 1045 def emails_delete(self):
1045 1046 _ = self.request.translate
1046 1047 c = self.load_default_context()
1047 1048
1048 1049 user_id = self.db_user_id
1049 1050 c.user = self.db_user
1050 1051
1051 1052 email_id = self.request.POST.get('del_email_id')
1052 1053 user_model = UserModel()
1053 1054
1054 1055 email = UserEmailMap.query().get(email_id).email
1055 1056 user_data = c.user.get_api_data()
1056 1057 user_model.delete_extra_email(c.user.user_id, email_id)
1057 1058 audit_logger.store_web(
1058 1059 'user.edit.email.delete',
1059 1060 action_data={'email': email, 'user': user_data},
1060 1061 user=self._rhodecode_user)
1061 1062 Session().commit()
1062 1063 h.flash(_("Removed email address from user account"),
1063 1064 category='success')
1064 1065 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1065 1066
1066 1067 @LoginRequired()
1067 1068 @HasPermissionAllDecorator('hg.admin')
1068 1069 def ips(self):
1069 1070 _ = self.request.translate
1070 1071 c = self.load_default_context()
1071 1072 c.user = self.db_user
1072 1073
1073 1074 c.active = 'ips'
1074 1075 c.user_ip_map = UserIpMap.query() \
1075 1076 .filter(UserIpMap.user == c.user).all()
1076 1077
1077 1078 c.inherit_default_ips = c.user.inherit_default_permissions
1078 1079 c.default_user_ip_map = UserIpMap.query() \
1079 1080 .filter(UserIpMap.user == User.get_default_user()).all()
1080 1081
1081 1082 return self._get_template_context(c)
1082 1083
1083 1084 @LoginRequired()
1084 1085 @HasPermissionAllDecorator('hg.admin')
1085 1086 @CSRFRequired()
1086 1087 # NOTE(marcink): this view is allowed for default users, as we can
1087 1088 # edit their IP white list
1088 1089 def ips_add(self):
1089 1090 _ = self.request.translate
1090 1091 c = self.load_default_context()
1091 1092
1092 1093 user_id = self.db_user_id
1093 1094 c.user = self.db_user
1094 1095
1095 1096 user_model = UserModel()
1096 1097 desc = self.request.POST.get('description')
1097 1098 try:
1098 1099 ip_list = user_model.parse_ip_range(
1099 1100 self.request.POST.get('new_ip'))
1100 1101 except Exception as e:
1101 1102 ip_list = []
1102 1103 log.exception("Exception during ip saving")
1103 1104 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1104 1105 category='error')
1105 1106 added = []
1106 1107 user_data = c.user.get_api_data()
1107 1108 for ip in ip_list:
1108 1109 try:
1109 1110 form = UserExtraIpForm(self.request.translate)()
1110 1111 data = form.to_python({'ip': ip})
1111 1112 ip = data['ip']
1112 1113
1113 1114 user_model.add_extra_ip(c.user.user_id, ip, desc)
1114 1115 audit_logger.store_web(
1115 1116 'user.edit.ip.add',
1116 1117 action_data={'ip': ip, 'user': user_data},
1117 1118 user=self._rhodecode_user)
1118 1119 Session().commit()
1119 1120 added.append(ip)
1120 1121 except formencode.Invalid as error:
1121 1122 msg = error.unpack_errors()['ip']
1122 1123 h.flash(msg, category='error')
1123 1124 except Exception:
1124 1125 log.exception("Exception during ip saving")
1125 1126 h.flash(_('An error occurred during ip saving'),
1126 1127 category='error')
1127 1128 if added:
1128 1129 h.flash(
1129 1130 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1130 1131 category='success')
1131 1132 if 'default_user' in self.request.POST:
1132 1133 # case for editing global IP list we do it for 'DEFAULT' user
1133 1134 raise HTTPFound(h.route_path('admin_permissions_ips'))
1134 1135 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1135 1136
1136 1137 @LoginRequired()
1137 1138 @HasPermissionAllDecorator('hg.admin')
1138 1139 @CSRFRequired()
1139 1140 # NOTE(marcink): this view is allowed for default users, as we can
1140 1141 # edit their IP white list
1141 1142 def ips_delete(self):
1142 1143 _ = self.request.translate
1143 1144 c = self.load_default_context()
1144 1145
1145 1146 user_id = self.db_user_id
1146 1147 c.user = self.db_user
1147 1148
1148 1149 ip_id = self.request.POST.get('del_ip_id')
1149 1150 user_model = UserModel()
1150 1151 user_data = c.user.get_api_data()
1151 1152 ip = UserIpMap.query().get(ip_id).ip_addr
1152 1153 user_model.delete_extra_ip(c.user.user_id, ip_id)
1153 1154 audit_logger.store_web(
1154 1155 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1155 1156 user=self._rhodecode_user)
1156 1157 Session().commit()
1157 1158 h.flash(_("Removed ip address from user whitelist"), category='success')
1158 1159
1159 1160 if 'default_user' in self.request.POST:
1160 1161 # case for editing global IP list we do it for 'DEFAULT' user
1161 1162 raise HTTPFound(h.route_path('admin_permissions_ips'))
1162 1163 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1163 1164
1164 1165 @LoginRequired()
1165 1166 @HasPermissionAllDecorator('hg.admin')
1166 1167 def groups_management(self):
1167 1168 c = self.load_default_context()
1168 1169 c.user = self.db_user
1169 1170 c.data = c.user.group_member
1170 1171
1171 1172 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1172 1173 for group in c.user.group_member]
1173 1174 c.groups = ext_json.str_json(groups)
1174 1175 c.active = 'groups'
1175 1176
1176 1177 return self._get_template_context(c)
1177 1178
1178 1179 @LoginRequired()
1179 1180 @HasPermissionAllDecorator('hg.admin')
1180 1181 @CSRFRequired()
1181 1182 def groups_management_updates(self):
1182 1183 _ = self.request.translate
1183 1184 c = self.load_default_context()
1184 1185
1185 1186 user_id = self.db_user_id
1186 1187 c.user = self.db_user
1187 1188
1188 1189 user_groups = set(self.request.POST.getall('users_group_id'))
1189 1190 user_groups_objects = []
1190 1191
1191 1192 for ugid in user_groups:
1192 1193 user_groups_objects.append(
1193 1194 UserGroupModel().get_group(safe_int(ugid)))
1194 1195 user_group_model = UserGroupModel()
1195 1196 added_to_groups, removed_from_groups = \
1196 1197 user_group_model.change_groups(c.user, user_groups_objects)
1197 1198
1198 1199 user_data = c.user.get_api_data()
1199 1200 for user_group_id in added_to_groups:
1200 1201 user_group = UserGroup.get(user_group_id)
1201 1202 old_values = user_group.get_api_data()
1202 1203 audit_logger.store_web(
1203 1204 'user_group.edit.member.add',
1204 1205 action_data={'user': user_data, 'old_data': old_values},
1205 1206 user=self._rhodecode_user)
1206 1207
1207 1208 for user_group_id in removed_from_groups:
1208 1209 user_group = UserGroup.get(user_group_id)
1209 1210 old_values = user_group.get_api_data()
1210 1211 audit_logger.store_web(
1211 1212 'user_group.edit.member.delete',
1212 1213 action_data={'user': user_data, 'old_data': old_values},
1213 1214 user=self._rhodecode_user)
1214 1215
1215 1216 Session().commit()
1216 1217 c.active = 'user_groups_management'
1217 1218 h.flash(_("Groups successfully changed"), category='success')
1218 1219
1219 1220 return HTTPFound(h.route_path(
1220 1221 'edit_user_groups_management', user_id=user_id))
1221 1222
1222 1223 @LoginRequired()
1223 1224 @HasPermissionAllDecorator('hg.admin')
1224 1225 def user_audit_logs(self):
1225 1226 _ = self.request.translate
1226 1227 c = self.load_default_context()
1227 1228 c.user = self.db_user
1228 1229
1229 1230 c.active = 'audit'
1230 1231
1231 1232 p = safe_int(self.request.GET.get('page', 1), 1)
1232 1233
1233 1234 filter_term = self.request.GET.get('filter')
1234 1235 user_log = UserModel().get_user_log(c.user, filter_term)
1235 1236
1236 1237 def url_generator(page_num):
1237 1238 query_params = {
1238 1239 'page': page_num
1239 1240 }
1240 1241 if filter_term:
1241 1242 query_params['filter'] = filter_term
1242 1243 return self.request.current_route_path(_query=query_params)
1243 1244
1244 1245 c.audit_logs = SqlPage(
1245 1246 user_log, page=p, items_per_page=10, url_maker=url_generator)
1246 1247 c.filter_term = filter_term
1247 1248 return self._get_template_context(c)
1248 1249
1249 1250 @LoginRequired()
1250 1251 @HasPermissionAllDecorator('hg.admin')
1251 1252 def user_audit_logs_download(self):
1252 1253 _ = self.request.translate
1253 1254 c = self.load_default_context()
1254 1255 c.user = self.db_user
1255 1256
1256 1257 user_log = UserModel().get_user_log(c.user, filter_term=None)
1257 1258
1258 1259 audit_log_data = {}
1259 1260 for entry in user_log:
1260 1261 audit_log_data[entry.user_log_id] = entry.get_dict()
1261 1262
1262 1263 response = Response(ext_json.formatted_str_json(audit_log_data))
1263 1264 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1264 1265 response.content_type = 'application/json'
1265 1266
1266 1267 return response
1267 1268
1268 1269 @LoginRequired()
1269 1270 @HasPermissionAllDecorator('hg.admin')
1270 1271 def user_perms_summary(self):
1271 1272 _ = self.request.translate
1272 1273 c = self.load_default_context()
1273 1274 c.user = self.db_user
1274 1275
1275 1276 c.active = 'perms_summary'
1276 1277 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277 1278
1278 1279 return self._get_template_context(c)
1279 1280
1280 1281 @LoginRequired()
1281 1282 @HasPermissionAllDecorator('hg.admin')
1282 1283 def user_perms_summary_json(self):
1283 1284 self.load_default_context()
1284 1285 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285 1286
1286 1287 return perm_user.permissions
1287 1288
1288 1289 @LoginRequired()
1289 1290 @HasPermissionAllDecorator('hg.admin')
1290 1291 def user_caches(self):
1291 1292 _ = self.request.translate
1292 1293 c = self.load_default_context()
1293 1294 c.user = self.db_user
1294 1295
1295 1296 c.active = 'caches'
1296 1297 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297 1298
1298 1299 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 1300 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 1301 c.backend = c.region.backend
1301 1302 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302 1303
1303 1304 return self._get_template_context(c)
1304 1305
1305 1306 @LoginRequired()
1306 1307 @HasPermissionAllDecorator('hg.admin')
1307 1308 @CSRFRequired()
1308 1309 def user_caches_update(self):
1309 1310 _ = self.request.translate
1310 1311 c = self.load_default_context()
1311 1312 c.user = self.db_user
1312 1313
1313 1314 c.active = 'caches'
1314 1315 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315 1316
1316 1317 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 1318 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318 1319
1319 1320 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320 1321
1321 1322 return HTTPFound(h.route_path(
1322 1323 'edit_user_caches', user_id=c.user.user_id))
@@ -1,106 +1,106 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import os
21 21
22 22 from pyramid.events import ApplicationCreated
23 23 from pyramid.settings import asbool
24 24
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.str_utils import safe_str
26 from rhodecode.lib.ext_json import str_json
27
28 28
29 29
30 30 def url_gen(request):
31 31 registry = request.registry
32 32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
33 33 ws_url = registry.settings.get('channelstream.ws_url', '')
34 34 proxy_url = request.route_url('channelstream_proxy')
35 35 urls = {
36 36 'connect': request.route_path('channelstream_connect'),
37 37 'subscribe': request.route_path('channelstream_subscribe'),
38 38 'longpoll': longpoll_url or proxy_url,
39 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 44 PLUGIN_DEFINITION = {
45 45 'name': 'channelstream',
46 46 'config': {
47 47 'javascript': [],
48 48 'css': [],
49 49 'template_hooks': {
50 50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
51 51 },
52 52 'url_gen': url_gen,
53 53 'static': None,
54 54 'enabled': False,
55 55 'server': '',
56 56 'secret': ''
57 57 }
58 58 }
59 59
60 60
61 61 def maybe_create_history_store(event):
62 62 # create plugin history location
63 63 settings = event.app.registry.settings
64 64 history_dir = settings.get('channelstream.history.location', '')
65 65 if history_dir and not os.path.exists(history_dir):
66 66 os.makedirs(history_dir, 0o750)
67 67
68 68
69 69 def includeme(config):
70 70 from rhodecode.apps.channelstream.views import ChannelstreamView
71 71
72 72 settings = config.registry.settings
73 73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
74 74 settings.get('channelstream.enabled'))
75 75 PLUGIN_DEFINITION['config']['server'] = settings.get(
76 76 'channelstream.server', '')
77 77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
78 78 'channelstream.secret', '')
79 79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
80 80 'channelstream.history.location', '')
81 81 config.register_rhodecode_plugin(
82 82 PLUGIN_DEFINITION['name'],
83 83 PLUGIN_DEFINITION['config']
84 84 )
85 85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
86 86
87 87 config.add_route(
88 88 name='channelstream_connect',
89 89 pattern=ADMIN_PREFIX + '/channelstream/connect')
90 90 config.add_view(
91 91 ChannelstreamView,
92 92 attr='channelstream_connect',
93 93 route_name='channelstream_connect', renderer='json_ext')
94 94
95 95 config.add_route(
96 96 name='channelstream_subscribe',
97 97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
98 98 config.add_view(
99 99 ChannelstreamView,
100 100 attr='channelstream_subscribe',
101 101 route_name='channelstream_subscribe', renderer='json_ext')
102 102
103 103 config.add_route(
104 104 name='channelstream_proxy',
105 105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
106 106
@@ -1,147 +1,147 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.config import routing_links
21 21
22 22
23 23 class VCSCallPredicate(object):
24 24 def __init__(self, val, config):
25 25 self.val = val
26 26
27 27 def text(self):
28 return 'vcs_call route = %s' % self.val
28 return f'vcs_call route = {self.val}'
29 29
30 30 phash = text
31 31
32 32 def __call__(self, info, request):
33 33 if hasattr(request, 'vcs_call'):
34 34 # skip vcs calls
35 35 return False
36 36
37 37 return True
38 38
39 39
40 40 def includeme(config):
41 41 from rhodecode.apps.home.views import HomeView
42 42
43 43 config.add_route_predicate(
44 44 'skip_vcs_call', VCSCallPredicate)
45 45
46 46 config.add_route(
47 47 name='home',
48 48 pattern='/')
49 49 config.add_view(
50 50 HomeView,
51 51 attr='main_page',
52 52 route_name='home', request_method='GET',
53 53 renderer='rhodecode:templates/index.mako')
54 54
55 55 config.add_route(
56 56 name='main_page_repos_data',
57 57 pattern='/_home_repos')
58 58 config.add_view(
59 59 HomeView,
60 60 attr='main_page_repos_data',
61 61 route_name='main_page_repos_data',
62 62 request_method='GET', renderer='json_ext', xhr=True)
63 63
64 64 config.add_route(
65 65 name='main_page_repo_groups_data',
66 66 pattern='/_home_repo_groups')
67 67 config.add_view(
68 68 HomeView,
69 69 attr='main_page_repo_groups_data',
70 70 route_name='main_page_repo_groups_data',
71 71 request_method='GET', renderer='json_ext', xhr=True)
72 72
73 73 config.add_route(
74 74 name='user_autocomplete_data',
75 75 pattern='/_users')
76 76 config.add_view(
77 77 HomeView,
78 78 attr='user_autocomplete_data',
79 79 route_name='user_autocomplete_data', request_method='GET',
80 80 renderer='json_ext', xhr=True)
81 81
82 82 config.add_route(
83 83 name='user_group_autocomplete_data',
84 84 pattern='/_user_groups')
85 85 config.add_view(
86 86 HomeView,
87 87 attr='user_group_autocomplete_data',
88 88 route_name='user_group_autocomplete_data', request_method='GET',
89 89 renderer='json_ext', xhr=True)
90 90
91 91 config.add_route(
92 92 name='repo_list_data',
93 93 pattern='/_repos')
94 94 config.add_view(
95 95 HomeView,
96 96 attr='repo_list_data',
97 97 route_name='repo_list_data', request_method='GET',
98 98 renderer='json_ext', xhr=True)
99 99
100 100 config.add_route(
101 101 name='repo_group_list_data',
102 102 pattern='/_repo_groups')
103 103 config.add_view(
104 104 HomeView,
105 105 attr='repo_group_list_data',
106 106 route_name='repo_group_list_data', request_method='GET',
107 107 renderer='json_ext', xhr=True)
108 108
109 109 config.add_route(
110 110 name='goto_switcher_data',
111 111 pattern='/_goto_data')
112 112 config.add_view(
113 113 HomeView,
114 114 attr='goto_switcher_data',
115 115 route_name='goto_switcher_data', request_method='GET',
116 116 renderer='json_ext', xhr=True)
117 117
118 118 config.add_route(
119 119 name='markup_preview',
120 120 pattern='/_markup_preview')
121 121 config.add_view(
122 122 HomeView,
123 123 attr='markup_preview',
124 124 route_name='markup_preview', request_method='POST',
125 125 renderer='string', xhr=True)
126 126
127 127 config.add_route(
128 128 name='file_preview',
129 129 pattern='/_file_preview')
130 130 config.add_view(
131 131 HomeView,
132 132 attr='file_preview',
133 133 route_name='file_preview', request_method='POST',
134 134 renderer='string', xhr=True)
135 135
136 136 config.add_route(
137 137 name='store_user_session_value',
138 138 pattern='/_store_session_attr')
139 139 config.add_view(
140 140 HomeView,
141 141 attr='store_user_session_attr',
142 142 route_name='store_user_session_value', request_method='POST',
143 143 renderer='string', xhr=True)
144 144
145 145 # register our static links via redirection mechanism
146 146 routing_links.connect_redirection_links(config)
147 147
@@ -1,856 +1,856 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 26
27 27 from rhodecode.apps._base import BaseAppView, DataGridAppView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoGroupPermissionAny, AuthUser)
32 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
33 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 35 from rhodecode.lib.vcs.nodes import FileNode
36 36 from rhodecode.model.db import (
37 37 func, true, or_, case, cast, in_filter_generator, String, Session,
38 38 Repository, RepoGroup, User, UserGroup, PullRequest)
39 39 from rhodecode.model.repo import RepoModel
40 40 from rhodecode.model.repo_group import RepoGroupModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.user_group import UserGroupModel
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class HomeView(BaseAppView, DataGridAppView):
48 48
49 49 def load_default_context(self):
50 50 c = self._get_local_tmpl_context()
51 51 c.user = c.auth_user.get_instance()
52 52 return c
53 53
54 54 @LoginRequired()
55 55 def user_autocomplete_data(self):
56 56 self.load_default_context()
57 57 query = self.request.GET.get('query')
58 58 active = str2bool(self.request.GET.get('active') or True)
59 59 include_groups = str2bool(self.request.GET.get('user_groups'))
60 60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
61 61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
62 62
63 63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
64 64 query, active, include_groups)
65 65
66 66 _users = UserModel().get_users(
67 67 name_contains=query, only_active=active)
68 68
69 69 def maybe_skip_default_user(usr):
70 70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
71 71 return False
72 72 return True
73 73 _users = filter(maybe_skip_default_user, _users)
74 74
75 75 if include_groups:
76 76 # extend with user groups
77 77 _user_groups = UserGroupModel().get_user_groups(
78 78 name_contains=query, only_active=active,
79 79 expand_groups=expand_groups)
80 80 _users = _users + _user_groups
81 81
82 82 return {'suggestions': _users}
83 83
84 84 @LoginRequired()
85 85 @NotAnonymous()
86 86 def user_group_autocomplete_data(self):
87 87 self.load_default_context()
88 88 query = self.request.GET.get('query')
89 89 active = str2bool(self.request.GET.get('active') or True)
90 90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
91 91
92 92 log.debug('generating user group list, query:%s, active:%s',
93 93 query, active)
94 94
95 95 _user_groups = UserGroupModel().get_user_groups(
96 96 name_contains=query, only_active=active,
97 97 expand_groups=expand_groups)
98 98 _user_groups = _user_groups
99 99
100 100 return {'suggestions': _user_groups}
101 101
102 102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
103 103 org_query = name_contains
104 104 allowed_ids = self._rhodecode_user.repo_acl_ids(
105 105 ['repository.read', 'repository.write', 'repository.admin'],
106 106 cache=True, name_filter=name_contains) or [-1]
107 107
108 108 query = Session().query(
109 109 Repository.repo_name,
110 110 Repository.repo_id,
111 111 Repository.repo_type,
112 112 Repository.private,
113 113 )\
114 114 .filter(Repository.archived.isnot(true()))\
115 115 .filter(or_(
116 116 # generate multiple IN to fix limitation problems
117 117 *in_filter_generator(Repository.repo_id, allowed_ids)
118 118 ))
119 119
120 120 query = query.order_by(case(
121 121 [
122 122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
123 123 ],
124 124 ))
125 125 query = query.order_by(func.length(Repository.repo_name))
126 126 query = query.order_by(Repository.repo_name)
127 127
128 128 if repo_type:
129 129 query = query.filter(Repository.repo_type == repo_type)
130 130
131 131 if name_contains:
132 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
132 ilike_expression = '%{}%'.format(safe_str(name_contains))
133 133 query = query.filter(
134 134 Repository.repo_name.ilike(ilike_expression))
135 135 query = query.limit(limit)
136 136
137 137 acl_iter = query
138 138
139 139 return [
140 140 {
141 141 'id': obj.repo_name,
142 142 'value': org_query,
143 143 'value_display': obj.repo_name,
144 144 'text': obj.repo_name,
145 145 'type': 'repo',
146 146 'repo_id': obj.repo_id,
147 147 'repo_type': obj.repo_type,
148 148 'private': obj.private,
149 149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
150 150 }
151 151 for obj in acl_iter]
152 152
153 153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
154 154 org_query = name_contains
155 155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
156 156 ['group.read', 'group.write', 'group.admin'],
157 157 cache=True, name_filter=name_contains) or [-1]
158 158
159 159 query = Session().query(
160 160 RepoGroup.group_id,
161 161 RepoGroup.group_name,
162 162 )\
163 163 .filter(or_(
164 164 # generate multiple IN to fix limitation problems
165 165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
166 166 ))
167 167
168 168 query = query.order_by(case(
169 169 [
170 170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
171 171 ],
172 172 ))
173 173 query = query.order_by(func.length(RepoGroup.group_name))
174 174 query = query.order_by(RepoGroup.group_name)
175 175
176 176 if name_contains:
177 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
177 ilike_expression = u'%{}%'.format(safe_str(name_contains))
178 178 query = query.filter(
179 179 RepoGroup.group_name.ilike(ilike_expression))
180 180 query = query.limit(limit)
181 181
182 182 acl_iter = query
183 183
184 184 return [
185 185 {
186 186 'id': obj.group_name,
187 187 'value': org_query,
188 188 'value_display': obj.group_name,
189 189 'text': obj.group_name,
190 190 'type': 'repo_group',
191 191 'repo_group_id': obj.group_id,
192 192 'url': h.route_path(
193 193 'repo_group_home', repo_group_name=obj.group_name)
194 194 }
195 195 for obj in acl_iter]
196 196
197 197 def _get_user_list(self, name_contains=None, limit=20):
198 198 org_query = name_contains
199 199 if not name_contains:
200 200 return [], False
201 201
202 202 # TODO(marcink): should all logged in users be allowed to search others?
203 203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
204 204 if not allowed_user_search:
205 205 return [], False
206 206
207 207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
208 208 if len(name_contains) != 1:
209 209 return [], False
210 210
211 211 name_contains = name_contains[0]
212 212
213 213 query = User.query()\
214 214 .order_by(func.length(User.username))\
215 215 .order_by(User.username) \
216 216 .filter(User.username != User.DEFAULT_USER)
217 217
218 218 if name_contains:
219 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
219 ilike_expression = u'%{}%'.format(safe_str(name_contains))
220 220 query = query.filter(
221 221 User.username.ilike(ilike_expression))
222 222 query = query.limit(limit)
223 223
224 224 acl_iter = query
225 225
226 226 return [
227 227 {
228 228 'id': obj.user_id,
229 229 'value': org_query,
230 230 'value_display': 'user: `{}`'.format(obj.username),
231 231 'type': 'user',
232 232 'icon_link': h.gravatar_url(obj.email, 30),
233 233 'url': h.route_path(
234 234 'user_profile', username=obj.username)
235 235 }
236 236 for obj in acl_iter], True
237 237
238 238 def _get_user_groups_list(self, name_contains=None, limit=20):
239 239 org_query = name_contains
240 240 if not name_contains:
241 241 return [], False
242 242
243 243 # TODO(marcink): should all logged in users be allowed to search others?
244 244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
245 245 if not allowed_user_search:
246 246 return [], False
247 247
248 248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
249 249 if len(name_contains) != 1:
250 250 return [], False
251 251
252 252 name_contains = name_contains[0]
253 253
254 254 query = UserGroup.query()\
255 255 .order_by(func.length(UserGroup.users_group_name))\
256 256 .order_by(UserGroup.users_group_name)
257 257
258 258 if name_contains:
259 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
259 ilike_expression = u'%{}%'.format(safe_str(name_contains))
260 260 query = query.filter(
261 261 UserGroup.users_group_name.ilike(ilike_expression))
262 262 query = query.limit(limit)
263 263
264 264 acl_iter = query
265 265
266 266 return [
267 267 {
268 268 'id': obj.users_group_id,
269 269 'value': org_query,
270 270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
271 271 'type': 'user_group',
272 272 'url': h.route_path(
273 273 'user_group_profile', user_group_name=obj.users_group_name)
274 274 }
275 275 for obj in acl_iter], True
276 276
277 277 def _get_pull_request_list(self, name_contains=None, limit=20):
278 278 org_query = name_contains
279 279 if not name_contains:
280 280 return [], False
281 281
282 282 # TODO(marcink): should all logged in users be allowed to search others?
283 283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
284 284 if not allowed_user_search:
285 285 return [], False
286 286
287 287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
288 288 if len(name_contains) != 1:
289 289 return [], False
290 290
291 291 name_contains = name_contains[0]
292 292
293 293 allowed_ids = self._rhodecode_user.repo_acl_ids(
294 294 ['repository.read', 'repository.write', 'repository.admin'],
295 295 cache=True) or [-1]
296 296
297 297 query = Session().query(
298 298 PullRequest.pull_request_id,
299 299 PullRequest.title,
300 300 )
301 301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
302 302
303 303 query = query.filter(or_(
304 304 # generate multiple IN to fix limitation problems
305 305 *in_filter_generator(Repository.repo_id, allowed_ids)
306 306 ))
307 307
308 308 query = query.order_by(PullRequest.pull_request_id)
309 309
310 310 if name_contains:
311 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
311 ilike_expression = u'%{}%'.format(safe_str(name_contains))
312 312 query = query.filter(or_(
313 313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
314 314 PullRequest.title.ilike(ilike_expression),
315 315 PullRequest.description.ilike(ilike_expression),
316 316 ))
317 317
318 318 query = query.limit(limit)
319 319
320 320 acl_iter = query
321 321
322 322 return [
323 323 {
324 324 'id': obj.pull_request_id,
325 325 'value': org_query,
326 326 'value_display': 'pull request: `!{} - {}`'.format(
327 327 obj.pull_request_id, safe_str(obj.title[:50])),
328 328 'type': 'pull_request',
329 329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
330 330 }
331 331 for obj in acl_iter], True
332 332
333 333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
334 334 repo_name = repo_group_name = None
335 335 if repo:
336 336 repo_name = repo.repo_name
337 337 if repo_group:
338 338 repo_group_name = repo_group.group_name
339 339
340 340 org_query = query
341 341 if not query or len(query) < 3 or not searcher:
342 342 return [], False
343 343
344 344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
345 345
346 346 if len(commit_hashes) != 1:
347 347 return [], False
348 348
349 349 commit_hash = commit_hashes[0]
350 350
351 351 result = searcher.search(
352 352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
353 353 repo_name, repo_group_name, raise_on_exc=False)
354 354
355 355 commits = []
356 356 for entry in result['results']:
357 357 repo_data = {
358 358 'repository_id': entry.get('repository_id'),
359 359 'repository_type': entry.get('repo_type'),
360 360 'repository_name': entry.get('repository'),
361 361 }
362 362
363 363 commit_entry = {
364 364 'id': entry['commit_id'],
365 365 'value': org_query,
366 366 'value_display': '`{}` commit: {}'.format(
367 367 entry['repository'], entry['commit_id']),
368 368 'type': 'commit',
369 369 'repo': entry['repository'],
370 370 'repo_data': repo_data,
371 371
372 372 'url': h.route_path(
373 373 'repo_commit',
374 374 repo_name=entry['repository'], commit_id=entry['commit_id'])
375 375 }
376 376
377 377 commits.append(commit_entry)
378 378 return commits, True
379 379
380 380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
381 381 repo_name = repo_group_name = None
382 382 if repo:
383 383 repo_name = repo.repo_name
384 384 if repo_group:
385 385 repo_group_name = repo_group.group_name
386 386
387 387 org_query = query
388 388 if not query or len(query) < 3 or not searcher:
389 389 return [], False
390 390
391 391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
392 392 if len(paths_re) != 1:
393 393 return [], False
394 394
395 395 file_path = paths_re[0]
396 396
397 397 search_path = searcher.escape_specials(file_path)
398 398 result = searcher.search(
399 399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
400 400 repo_name, repo_group_name, raise_on_exc=False)
401 401
402 402 files = []
403 403 for entry in result['results']:
404 404 repo_data = {
405 405 'repository_id': entry.get('repository_id'),
406 406 'repository_type': entry.get('repo_type'),
407 407 'repository_name': entry.get('repository'),
408 408 }
409 409
410 410 file_entry = {
411 411 'id': entry['commit_id'],
412 412 'value': org_query,
413 413 'value_display': '`{}` file: {}'.format(
414 414 entry['repository'], entry['file']),
415 415 'type': 'file',
416 416 'repo': entry['repository'],
417 417 'repo_data': repo_data,
418 418
419 419 'url': h.route_path(
420 420 'repo_files',
421 421 repo_name=entry['repository'], commit_id=entry['commit_id'],
422 422 f_path=entry['file'])
423 423 }
424 424
425 425 files.append(file_entry)
426 426 return files, True
427 427
428 428 @LoginRequired()
429 429 def repo_list_data(self):
430 430 _ = self.request.translate
431 431 self.load_default_context()
432 432
433 433 query = self.request.GET.get('query')
434 434 repo_type = self.request.GET.get('repo_type')
435 435 log.debug('generating repo list, query:%s, repo_type:%s',
436 436 query, repo_type)
437 437
438 438 res = []
439 439 repos = self._get_repo_list(query, repo_type=repo_type)
440 440 if repos:
441 441 res.append({
442 442 'text': _('Repositories'),
443 443 'children': repos
444 444 })
445 445
446 446 data = {
447 447 'more': False,
448 448 'results': res
449 449 }
450 450 return data
451 451
452 452 @LoginRequired()
453 453 def repo_group_list_data(self):
454 454 _ = self.request.translate
455 455 self.load_default_context()
456 456
457 457 query = self.request.GET.get('query')
458 458
459 459 log.debug('generating repo group list, query:%s',
460 460 query)
461 461
462 462 res = []
463 463 repo_groups = self._get_repo_group_list(query)
464 464 if repo_groups:
465 465 res.append({
466 466 'text': _('Repository Groups'),
467 467 'children': repo_groups
468 468 })
469 469
470 470 data = {
471 471 'more': False,
472 472 'results': res
473 473 }
474 474 return data
475 475
476 476 def _get_default_search_queries(self, search_context, searcher, query):
477 477 if not searcher:
478 478 return []
479 479
480 480 is_es_6 = searcher.is_es_6
481 481
482 482 queries = []
483 483 repo_group_name, repo_name, repo_context = None, None, None
484 484
485 485 # repo group context
486 486 if search_context.get('search_context[repo_group_name]'):
487 487 repo_group_name = search_context.get('search_context[repo_group_name]')
488 488 if search_context.get('search_context[repo_name]'):
489 489 repo_name = search_context.get('search_context[repo_name]')
490 490 repo_context = search_context.get('search_context[repo_view_type]')
491 491
492 492 if is_es_6 and repo_name:
493 493 # files
494 494 def query_modifier():
495 495 qry = query
496 496 return {'q': qry, 'type': 'content'}
497 497
498 498 label = u'File content search for `{}`'.format(h.escape(query))
499 499 file_qry = {
500 500 'id': -10,
501 501 'value': query,
502 502 'value_display': label,
503 503 'value_icon': '<i class="icon-code"></i>',
504 504 'type': 'search',
505 505 'subtype': 'repo',
506 506 'url': h.route_path('search_repo',
507 507 repo_name=repo_name,
508 508 _query=query_modifier())
509 509 }
510 510
511 511 # commits
512 512 def query_modifier():
513 513 qry = query
514 514 return {'q': qry, 'type': 'commit'}
515 515
516 516 label = u'Commit search for `{}`'.format(h.escape(query))
517 517 commit_qry = {
518 518 'id': -20,
519 519 'value': query,
520 520 'value_display': label,
521 521 'value_icon': '<i class="icon-history"></i>',
522 522 'type': 'search',
523 523 'subtype': 'repo',
524 524 'url': h.route_path('search_repo',
525 525 repo_name=repo_name,
526 526 _query=query_modifier())
527 527 }
528 528
529 529 if repo_context in ['commit', 'commits']:
530 530 queries.extend([commit_qry, file_qry])
531 531 elif repo_context in ['files', 'summary']:
532 532 queries.extend([file_qry, commit_qry])
533 533 else:
534 534 queries.extend([commit_qry, file_qry])
535 535
536 536 elif is_es_6 and repo_group_name:
537 537 # files
538 538 def query_modifier():
539 539 qry = query
540 540 return {'q': qry, 'type': 'content'}
541 541
542 542 label = u'File content search for `{}`'.format(query)
543 543 file_qry = {
544 544 'id': -30,
545 545 'value': query,
546 546 'value_display': label,
547 547 'value_icon': '<i class="icon-code"></i>',
548 548 'type': 'search',
549 549 'subtype': 'repo_group',
550 550 'url': h.route_path('search_repo_group',
551 551 repo_group_name=repo_group_name,
552 552 _query=query_modifier())
553 553 }
554 554
555 555 # commits
556 556 def query_modifier():
557 557 qry = query
558 558 return {'q': qry, 'type': 'commit'}
559 559
560 560 label = u'Commit search for `{}`'.format(query)
561 561 commit_qry = {
562 562 'id': -40,
563 563 'value': query,
564 564 'value_display': label,
565 565 'value_icon': '<i class="icon-history"></i>',
566 566 'type': 'search',
567 567 'subtype': 'repo_group',
568 568 'url': h.route_path('search_repo_group',
569 569 repo_group_name=repo_group_name,
570 570 _query=query_modifier())
571 571 }
572 572
573 573 if repo_context in ['commit', 'commits']:
574 574 queries.extend([commit_qry, file_qry])
575 575 elif repo_context in ['files', 'summary']:
576 576 queries.extend([file_qry, commit_qry])
577 577 else:
578 578 queries.extend([commit_qry, file_qry])
579 579
580 580 # Global, not scoped
581 581 if not queries:
582 582 queries.append(
583 583 {
584 584 'id': -1,
585 585 'value': query,
586 586 'value_display': u'File content search for: `{}`'.format(query),
587 587 'value_icon': '<i class="icon-code"></i>',
588 588 'type': 'search',
589 589 'subtype': 'global',
590 590 'url': h.route_path('search',
591 591 _query={'q': query, 'type': 'content'})
592 592 })
593 593 queries.append(
594 594 {
595 595 'id': -2,
596 596 'value': query,
597 597 'value_display': u'Commit search for: `{}`'.format(query),
598 598 'value_icon': '<i class="icon-history"></i>',
599 599 'type': 'search',
600 600 'subtype': 'global',
601 601 'url': h.route_path('search',
602 602 _query={'q': query, 'type': 'commit'})
603 603 })
604 604
605 605 return queries
606 606
607 607 @LoginRequired()
608 608 def goto_switcher_data(self):
609 609 c = self.load_default_context()
610 610
611 611 _ = self.request.translate
612 612
613 613 query = self.request.GET.get('query')
614 614 log.debug('generating main filter data, query %s', query)
615 615
616 616 res = []
617 617 if not query:
618 618 return {'suggestions': res}
619 619
620 620 def no_match(name):
621 621 return {
622 622 'id': -1,
623 623 'value': "",
624 624 'value_display': name,
625 625 'type': 'text',
626 626 'url': ""
627 627 }
628 628 searcher = searcher_from_config(self.request.registry.settings)
629 629 has_specialized_search = False
630 630
631 631 # set repo context
632 632 repo = None
633 633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
634 634 if repo_id:
635 635 repo = Repository.get(repo_id)
636 636
637 637 # set group context
638 638 repo_group = None
639 639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
640 640 if repo_group_id:
641 641 repo_group = RepoGroup.get(repo_group_id)
642 642 prefix_match = False
643 643
644 644 # user: type search
645 645 if not prefix_match:
646 646 users, prefix_match = self._get_user_list(query)
647 647 if users:
648 648 has_specialized_search = True
649 649 for serialized_user in users:
650 650 res.append(serialized_user)
651 651 elif prefix_match:
652 652 has_specialized_search = True
653 653 res.append(no_match('No matching users found'))
654 654
655 655 # user_group: type search
656 656 if not prefix_match:
657 657 user_groups, prefix_match = self._get_user_groups_list(query)
658 658 if user_groups:
659 659 has_specialized_search = True
660 660 for serialized_user_group in user_groups:
661 661 res.append(serialized_user_group)
662 662 elif prefix_match:
663 663 has_specialized_search = True
664 664 res.append(no_match('No matching user groups found'))
665 665
666 666 # pr: type search
667 667 if not prefix_match:
668 668 pull_requests, prefix_match = self._get_pull_request_list(query)
669 669 if pull_requests:
670 670 has_specialized_search = True
671 671 for serialized_pull_request in pull_requests:
672 672 res.append(serialized_pull_request)
673 673 elif prefix_match:
674 674 has_specialized_search = True
675 675 res.append(no_match('No matching pull requests found'))
676 676
677 677 # FTS commit: type search
678 678 if not prefix_match:
679 679 commits, prefix_match = self._get_hash_commit_list(
680 680 c.auth_user, searcher, query, repo, repo_group)
681 681 if commits:
682 682 has_specialized_search = True
683 683 unique_repos = collections.OrderedDict()
684 684 for commit in commits:
685 685 repo_name = commit['repo']
686 686 unique_repos.setdefault(repo_name, []).append(commit)
687 687
688 688 for _repo, commits in unique_repos.items():
689 689 for commit in commits:
690 690 res.append(commit)
691 691 elif prefix_match:
692 692 has_specialized_search = True
693 693 res.append(no_match('No matching commits found'))
694 694
695 695 # FTS file: type search
696 696 if not prefix_match:
697 697 paths, prefix_match = self._get_path_list(
698 698 c.auth_user, searcher, query, repo, repo_group)
699 699 if paths:
700 700 has_specialized_search = True
701 701 unique_repos = collections.OrderedDict()
702 702 for path in paths:
703 703 repo_name = path['repo']
704 704 unique_repos.setdefault(repo_name, []).append(path)
705 705
706 706 for repo, paths in unique_repos.items():
707 707 for path in paths:
708 708 res.append(path)
709 709 elif prefix_match:
710 710 has_specialized_search = True
711 711 res.append(no_match('No matching files found'))
712 712
713 713 # main suggestions
714 714 if not has_specialized_search:
715 715 repo_group_name = ''
716 716 if repo_group:
717 717 repo_group_name = repo_group.group_name
718 718
719 719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
720 720 res.append(_q)
721 721
722 722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
723 723 for serialized_repo_group in repo_groups:
724 724 res.append(serialized_repo_group)
725 725
726 726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
727 727 for serialized_repo in repos:
728 728 res.append(serialized_repo)
729 729
730 730 if not repos and not repo_groups:
731 731 res.append(no_match('No matches found'))
732 732
733 733 return {'suggestions': res}
734 734
735 735 @LoginRequired()
736 736 def main_page(self):
737 737 c = self.load_default_context()
738 738 c.repo_group = None
739 739 return self._get_template_context(c)
740 740
741 741 def _main_page_repo_groups_data(self, repo_group_id):
742 742 column_map = {
743 743 'name': 'group_name_hash',
744 744 'desc': 'group_description',
745 745 'last_change': 'updated_on',
746 746 'owner': 'user_username',
747 747 }
748 748 draw, start, limit = self._extract_chunk(self.request)
749 749 search_q, order_by, order_dir = self._extract_ordering(
750 750 self.request, column_map=column_map)
751 751 return RepoGroupModel().get_repo_groups_data_table(
752 752 draw, start, limit,
753 753 search_q, order_by, order_dir,
754 754 self._rhodecode_user, repo_group_id)
755 755
756 756 def _main_page_repos_data(self, repo_group_id):
757 757 column_map = {
758 758 'name': 'repo_name',
759 759 'desc': 'description',
760 760 'last_change': 'updated_on',
761 761 'owner': 'user_username',
762 762 }
763 763 draw, start, limit = self._extract_chunk(self.request)
764 764 search_q, order_by, order_dir = self._extract_ordering(
765 765 self.request, column_map=column_map)
766 766 return RepoModel().get_repos_data_table(
767 767 draw, start, limit,
768 768 search_q, order_by, order_dir,
769 769 self._rhodecode_user, repo_group_id)
770 770
771 771 @LoginRequired()
772 772 def main_page_repo_groups_data(self):
773 773 self.load_default_context()
774 774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
775 775
776 776 if repo_group_id:
777 777 group = RepoGroup.get_or_404(repo_group_id)
778 778 _perms = AuthUser.repo_group_read_perms
779 779 if not HasRepoGroupPermissionAny(*_perms)(
780 780 group.group_name, 'user is allowed to list repo group children'):
781 781 raise HTTPNotFound()
782 782
783 783 return self._main_page_repo_groups_data(repo_group_id)
784 784
785 785 @LoginRequired()
786 786 def main_page_repos_data(self):
787 787 self.load_default_context()
788 788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
789 789
790 790 if repo_group_id:
791 791 group = RepoGroup.get_or_404(repo_group_id)
792 792 _perms = AuthUser.repo_group_read_perms
793 793 if not HasRepoGroupPermissionAny(*_perms)(
794 794 group.group_name, 'user is allowed to list repo group children'):
795 795 raise HTTPNotFound()
796 796
797 797 return self._main_page_repos_data(repo_group_id)
798 798
799 799 @LoginRequired()
800 800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
801 801 def repo_group_main_page(self):
802 802 c = self.load_default_context()
803 803 c.repo_group = self.request.db_repo_group
804 804 return self._get_template_context(c)
805 805
806 806 @LoginRequired()
807 807 @CSRFRequired()
808 808 def markup_preview(self):
809 809 # Technically a CSRF token is not needed as no state changes with this
810 810 # call. However, as this is a POST is better to have it, so automated
811 811 # tools don't flag it as potential CSRF.
812 812 # Post is required because the payload could be bigger than the maximum
813 813 # allowed by GET.
814 814
815 815 text = self.request.POST.get('text')
816 816 renderer = self.request.POST.get('renderer') or 'rst'
817 817 if text:
818 818 return h.render(text, renderer=renderer, mentions=True)
819 819 return ''
820 820
821 821 @LoginRequired()
822 822 @CSRFRequired()
823 823 def file_preview(self):
824 824 # Technically a CSRF token is not needed as no state changes with this
825 825 # call. However, as this is a POST is better to have it, so automated
826 826 # tools don't flag it as potential CSRF.
827 827 # Post is required because the payload could be bigger than the maximum
828 828 # allowed by GET.
829 829
830 830 text = self.request.POST.get('text')
831 831 file_path = self.request.POST.get('file_path')
832 832
833 833 renderer = h.renderer_from_filename(file_path)
834 834
835 835 if renderer:
836 836 return h.render(text, renderer=renderer, mentions=True)
837 837 else:
838 838 self.load_default_context()
839 839 _render = self.request.get_partial_renderer(
840 840 'rhodecode:templates/files/file_content.mako')
841 841
842 842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
843 843
844 844 return _render('render_lines', lines)
845 845
846 846 @LoginRequired()
847 847 @CSRFRequired()
848 848 def store_user_session_attr(self):
849 849 key = self.request.POST.get('key')
850 850 val = self.request.POST.get('val')
851 851
852 852 existing_value = self.request.session.get(key)
853 853 if existing_value != val:
854 854 self.request.session[key] = val
855 855
856 856 return 'stored:{}:{}'.format(key, val)
@@ -1,108 +1,108 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 26
27 27
28 28 from rhodecode.apps._base import BaseAppView, RepoAppView
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 32 HasRepoPermissionAnyDecorator)
33 33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 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 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.model.db import (
40 40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
41 41 from rhodecode.model.repo import RepoModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList, RepoList
44 44 from rhodecode.model.user import UserModel
45 45 from rhodecode.model.user_group import UserGroupModel
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HoverCardsView(BaseAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54 return c
55 55
56 56 @LoginRequired()
57 57 def hovercard_user(self):
58 58 c = self.load_default_context()
59 59 user_id = self.request.matchdict['user_id']
60 60 c.user = User.get_or_404(user_id)
61 61 return self._get_template_context(c)
62 62
63 63 @LoginRequired()
64 64 def hovercard_username(self):
65 65 c = self.load_default_context()
66 66 username = self.request.matchdict['username']
67 67 c.user = User.get_by_username(username)
68 68 if not c.user:
69 69 raise HTTPNotFound()
70 70
71 71 return self._get_template_context(c)
72 72
73 73 @LoginRequired()
74 74 def hovercard_user_group(self):
75 75 c = self.load_default_context()
76 76 user_group_id = self.request.matchdict['user_group_id']
77 77 c.user_group = UserGroup.get_or_404(user_group_id)
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 def hovercard_pull_request(self):
82 82 c = self.load_default_context()
83 83 c.pull_request = PullRequest.get_or_404(
84 84 self.request.matchdict['pull_request_id'])
85 85 perms = ['repository.read', 'repository.write', 'repository.admin']
86 86 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
87 87 c.pull_request.target_repo.repo_name)
88 88 return self._get_template_context(c)
89 89
90 90
91 91 class HoverCardsRepoView(RepoAppView):
92 92 def load_default_context(self):
93 93 c = self._get_local_tmpl_context()
94 94 return c
95 95
96 96 @LoginRequired()
97 97 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
98 98 def hovercard_repo_commit(self):
99 99 c = self.load_default_context()
100 100 commit_id = self.request.matchdict['commit_id']
101 101 pre_load = ['author', 'branch', 'date', 'message']
102 102 try:
103 103 c.commit = self.rhodecode_vcs_repo.get_commit(
104 104 commit_id=commit_id, pre_load=pre_load)
105 105 except (CommitDoesNotExistError, EmptyRepositoryError):
106 106 raise HTTPNotFound()
107 107
108 108 return self._get_template_context(c)
@@ -1,363 +1,363 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 21 import itertools
22 22
23 23 from pyramid.httpexceptions import HTTPBadRequest
24 24 from pyramid.response import Response
25 25 from pyramid.renderers import render
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.model.db import (
29 29 or_, joinedload, Repository, UserLog, UserFollowing, User, UserApiKeys)
30 30 from rhodecode.model.meta import Session
31 31 import rhodecode.lib.helpers as h
32 32 from rhodecode.lib.helpers import SqlPage
33 33 from rhodecode.lib.user_log_filter import user_log_filter
34 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
35 35 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
36 36 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
37 37 from rhodecode.model.scm import ScmModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class JournalView(BaseAppView):
43 43
44 44 def load_default_context(self):
45 45 c = self._get_local_tmpl_context(include_app_defaults=True)
46 46
47 47 self._load_defaults(c.rhodecode_name)
48 48
49 49 # TODO(marcink): what is this, why we need a global register ?
50 50 c.search_term = self.request.GET.get('filter') or ''
51 51 return c
52 52
53 53 def _get_config(self, rhodecode_name):
54 54 import rhodecode
55 55 config = rhodecode.CONFIG
56 56
57 57 return {
58 58 'language': 'en-us',
59 59 'feed_ttl': '5', # TTL of feed,
60 60 'feed_items_per_page':
61 61 safe_int(config.get('rss_items_per_page', 20)),
62 62 'rhodecode_name': rhodecode_name
63 63 }
64 64
65 65 def _load_defaults(self, rhodecode_name):
66 66 config = self._get_config(rhodecode_name)
67 67 # common values for feeds
68 68 self.language = config["language"]
69 69 self.ttl = config["feed_ttl"]
70 70 self.feed_items_per_page = config['feed_items_per_page']
71 71 self.rhodecode_name = config['rhodecode_name']
72 72
73 73 def _get_daily_aggregate(self, journal):
74 74 groups = []
75 75 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
76 76 user_group = []
77 77 # groupby username if it's a present value, else
78 78 # fallback to journal username
79 79 for _, g2 in itertools.groupby(
80 80 list(g), lambda x: x.user.username if x.user else x.username):
81 81 l = list(g2)
82 82 user_group.append((l[0].user, l))
83 83
84 84 groups.append((k, user_group,))
85 85
86 86 return groups
87 87
88 88 def _get_journal_data(self, following_repos, search_term):
89 89 repo_ids = [x.follows_repository.repo_id for x in following_repos
90 90 if x.follows_repository is not None]
91 91 user_ids = [x.follows_user.user_id for x in following_repos
92 92 if x.follows_user is not None]
93 93
94 94 filtering_criterion = None
95 95
96 96 if repo_ids and user_ids:
97 97 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
98 98 UserLog.user_id.in_(user_ids))
99 99 if repo_ids and not user_ids:
100 100 filtering_criterion = UserLog.repository_id.in_(repo_ids)
101 101 if not repo_ids and user_ids:
102 102 filtering_criterion = UserLog.user_id.in_(user_ids)
103 103 if filtering_criterion is not None:
104 104 journal = Session().query(UserLog)\
105 105 .options(joinedload(UserLog.user))\
106 106 .options(joinedload(UserLog.repository))
107 107 # filter
108 108 try:
109 109 journal = user_log_filter(journal, search_term)
110 110 except Exception:
111 111 # we want this to crash for now
112 112 raise
113 113 journal = journal.filter(filtering_criterion)\
114 114 .order_by(UserLog.action_date.desc())
115 115 else:
116 116 journal = []
117 117
118 118 return journal
119 119
120 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 123 def _atom_feed(self, repos, search_term, public=True):
124 124 _ = self.request.translate
125 125 journal = self._get_journal_data(repos, search_term)
126 126 if public:
127 127 _link = h.route_url('journal_public_atom')
128 128 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
129 129 'atom feed')
130 130 else:
131 131 _link = h.route_url('journal_atom')
132 132 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
133 133
134 134 feed = Atom1Feed(
135 135 title=_desc, link=_link, description=_desc,
136 136 language=self.language, ttl=self.ttl)
137 137
138 138 for entry in journal[:self.feed_items_per_page]:
139 139 user = entry.user
140 140 if user is None:
141 141 # fix deleted users
142 142 user = AttributeDict({'short_contact': entry.username,
143 143 'email': '',
144 144 'full_contact': ''})
145 145 action, action_extra, ico = h.action_parser(
146 146 self.request, entry, feed=True)
147 147 title = "%s - %s %s" % (user.short_contact, action(),
148 148 entry.repository.repo_name)
149 149 desc = action_extra()
150 150 _url = h.route_url('home')
151 151 if entry.repository is not None:
152 152 _url = h.route_url('repo_commits',
153 153 repo_name=entry.repository.repo_name)
154 154
155 155 feed.add_item(
156 156 unique_id=self.feed_uid(entry.user_log_id),
157 157 title=title,
158 158 pubdate=entry.action_date,
159 159 link=_url,
160 160 author_email=user.email,
161 161 author_name=user.full_contact,
162 162 description=desc)
163 163
164 164 response = Response(feed.writeString('utf-8'))
165 165 response.content_type = feed.content_type
166 166 return response
167 167
168 168 def _rss_feed(self, repos, search_term, public=True):
169 169 _ = self.request.translate
170 170 journal = self._get_journal_data(repos, search_term)
171 171 if public:
172 172 _link = h.route_url('journal_public_atom')
173 173 _desc = '%s %s %s' % (
174 174 self.rhodecode_name, _('public journal'), 'rss feed')
175 175 else:
176 176 _link = h.route_url('journal_atom')
177 177 _desc = '%s %s %s' % (
178 178 self.rhodecode_name, _('journal'), 'rss feed')
179 179
180 180 feed = Rss201rev2Feed(
181 181 title=_desc, link=_link, description=_desc,
182 182 language=self.language, ttl=self.ttl)
183 183
184 184 for entry in journal[:self.feed_items_per_page]:
185 185 user = entry.user
186 186 if user is None:
187 187 # fix deleted users
188 188 user = AttributeDict({'short_contact': entry.username,
189 189 'email': '',
190 190 'full_contact': ''})
191 191 action, action_extra, ico = h.action_parser(
192 192 self.request, entry, feed=True)
193 193 title = "%s - %s %s" % (user.short_contact, action(),
194 194 entry.repository.repo_name)
195 195 desc = action_extra()
196 196 _url = h.route_url('home')
197 197 if entry.repository is not None:
198 198 _url = h.route_url('repo_commits',
199 199 repo_name=entry.repository.repo_name)
200 200
201 201 feed.add_item(
202 202 unique_id=self.feed_uid(entry.user_log_id),
203 203 title=title,
204 204 pubdate=entry.action_date,
205 205 link=_url,
206 206 author_email=user.email,
207 207 author_name=user.full_contact,
208 208 description=desc)
209 209
210 210 response = Response(feed.writeString('utf-8'))
211 211 response.content_type = feed.content_type
212 212 return response
213 213
214 214 @LoginRequired()
215 215 @NotAnonymous()
216 216 def journal(self):
217 217 c = self.load_default_context()
218 218
219 219 p = safe_int(self.request.GET.get('page', 1), 1)
220 220 c.user = User.get(self._rhodecode_user.user_id)
221 221 following = Session().query(UserFollowing)\
222 222 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
223 223 .options(joinedload(UserFollowing.follows_repository))\
224 224 .all()
225 225
226 226 journal = self._get_journal_data(following, c.search_term)
227 227
228 228 def url_generator(page_num):
229 229 query_params = {
230 230 'page': page_num,
231 231 'filter': c.search_term
232 232 }
233 233 return self.request.current_route_path(_query=query_params)
234 234
235 235 c.journal_pager = SqlPage(
236 236 journal, page=p, items_per_page=20, url_maker=url_generator)
237 237 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
238 238
239 239 c.journal_data = render(
240 240 'rhodecode:templates/journal/journal_data.mako',
241 241 self._get_template_context(c), self.request)
242 242
243 243 if self.request.is_xhr:
244 244 return Response(c.journal_data)
245 245
246 246 html = render(
247 247 'rhodecode:templates/journal/journal.mako',
248 248 self._get_template_context(c), self.request)
249 249 return Response(html)
250 250
251 251 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
252 252 @NotAnonymous()
253 253 def journal_atom(self):
254 254 """
255 255 Produce an atom-1.0 feed via feedgenerator module
256 256 """
257 257 c = self.load_default_context()
258 258 following_repos = Session().query(UserFollowing)\
259 259 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
260 260 .options(joinedload(UserFollowing.follows_repository))\
261 261 .all()
262 262 return self._atom_feed(following_repos, c.search_term, public=False)
263 263
264 264 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
265 265 @NotAnonymous()
266 266 def journal_rss(self):
267 267 """
268 268 Produce an rss feed via feedgenerator module
269 269 """
270 270 c = self.load_default_context()
271 271 following_repos = Session().query(UserFollowing)\
272 272 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
273 273 .options(joinedload(UserFollowing.follows_repository))\
274 274 .all()
275 275 return self._rss_feed(following_repos, c.search_term, public=False)
276 276
277 277 @LoginRequired()
278 278 def journal_public(self):
279 279 c = self.load_default_context()
280 280 # Return a rendered template
281 281 p = safe_int(self.request.GET.get('page', 1), 1)
282 282
283 283 c.following = Session().query(UserFollowing)\
284 284 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
285 285 .options(joinedload(UserFollowing.follows_repository))\
286 286 .all()
287 287
288 288 journal = self._get_journal_data(c.following, c.search_term)
289 289
290 290 def url_generator(page_num):
291 291 query_params = {
292 292 'page': page_num
293 293 }
294 294 return self.request.current_route_path(_query=query_params)
295 295
296 296 c.journal_pager = SqlPage(
297 297 journal, page=p, items_per_page=20, url_maker=url_generator)
298 298 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
299 299
300 300 c.journal_data = render(
301 301 'rhodecode:templates/journal/journal_data.mako',
302 302 self._get_template_context(c), self.request)
303 303
304 304 if self.request.is_xhr:
305 305 return Response(c.journal_data)
306 306
307 307 html = render(
308 308 'rhodecode:templates/journal/public_journal.mako',
309 309 self._get_template_context(c), self.request)
310 310 return Response(html)
311 311
312 312 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
313 313 def journal_public_atom(self):
314 314 """
315 315 Produce an atom-1.0 feed via feedgenerator module
316 316 """
317 317 c = self.load_default_context()
318 318 following_repos = Session().query(UserFollowing)\
319 319 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
320 320 .options(joinedload(UserFollowing.follows_repository))\
321 321 .all()
322 322
323 323 return self._atom_feed(following_repos, c.search_term)
324 324
325 325 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
326 326 def journal_public_rss(self):
327 327 """
328 328 Produce an rss2 feed via feedgenerator module
329 329 """
330 330 c = self.load_default_context()
331 331 following_repos = Session().query(UserFollowing)\
332 332 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
333 333 .options(joinedload(UserFollowing.follows_repository))\
334 334 .all()
335 335
336 336 return self._rss_feed(following_repos, c.search_term)
337 337
338 338 @LoginRequired()
339 339 @NotAnonymous()
340 340 @CSRFRequired()
341 341 def toggle_following(self):
342 342 user_id = self.request.POST.get('follows_user_id')
343 343 if user_id:
344 344 try:
345 345 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
346 346 Session().commit()
347 347 return 'ok'
348 348 except Exception:
349 349 raise HTTPBadRequest()
350 350
351 351 repo_id = self.request.POST.get('follows_repo_id')
352 352 repo = Repository.get_or_404(repo_id)
353 353 perm_set = ['repository.read', 'repository.write', 'repository.admin']
354 354 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
355 355 if repo and has_perm:
356 356 try:
357 357 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
358 358 Session().commit()
359 359 return 'ok'
360 360 except Exception:
361 361 raise HTTPBadRequest()
362 362
363 363 raise HTTPBadRequest()
@@ -1,783 +1,784 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 29
30 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 31 from rhodecode import forms
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib import ext_json
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, NotAnonymous, CSRFRequired,
37 37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 38 from rhodecode.lib.channelstream import (
39 39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.hash_utils import md5_safe
40 41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 42 from rhodecode.model.auth_token import AuthTokenModel
42 43 from rhodecode.model.comment import CommentsModel
43 44 from rhodecode.model.db import (
44 45 IntegrityError, or_, in_filter_generator,
45 46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 47 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 48 from rhodecode.model.meta import Session
48 49 from rhodecode.model.pull_request import PullRequestModel
49 50 from rhodecode.model.user import UserModel
50 51 from rhodecode.model.user_group import UserGroupModel
51 52 from rhodecode.model.validation_schema.schemas import user_schema
52 53
53 54 log = logging.getLogger(__name__)
54 55
55 56
56 57 class MyAccountView(BaseAppView, DataGridAppView):
57 58 ALLOW_SCOPED_TOKENS = False
58 59 """
59 60 This view has alternative version inside EE, if modified please take a look
60 61 in there as well.
61 62 """
62 63
63 64 def load_default_context(self):
64 65 c = self._get_local_tmpl_context()
65 66 c.user = c.auth_user.get_instance()
66 67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 68 return c
68 69
69 70 @LoginRequired()
70 71 @NotAnonymous()
71 72 def my_account_profile(self):
72 73 c = self.load_default_context()
73 74 c.active = 'profile'
74 75 c.extern_type = c.user.extern_type
75 76 return self._get_template_context(c)
76 77
77 78 @LoginRequired()
78 79 @NotAnonymous()
79 80 def my_account_edit(self):
80 81 c = self.load_default_context()
81 82 c.active = 'profile_edit'
82 83 c.extern_type = c.user.extern_type
83 84 c.extern_name = c.user.extern_name
84 85
85 86 schema = user_schema.UserProfileSchema().bind(
86 87 username=c.user.username, user_emails=c.user.emails)
87 88 appstruct = {
88 89 'username': c.user.username,
89 90 'email': c.user.email,
90 91 'firstname': c.user.firstname,
91 92 'lastname': c.user.lastname,
92 93 'description': c.user.description,
93 94 }
94 95 c.form = forms.RcForm(
95 96 schema, appstruct=appstruct,
96 97 action=h.route_path('my_account_update'),
97 98 buttons=(forms.buttons.save, forms.buttons.reset))
98 99
99 100 return self._get_template_context(c)
100 101
101 102 @LoginRequired()
102 103 @NotAnonymous()
103 104 @CSRFRequired()
104 105 def my_account_update(self):
105 106 _ = self.request.translate
106 107 c = self.load_default_context()
107 108 c.active = 'profile_edit'
108 109 c.perm_user = c.auth_user
109 110 c.extern_type = c.user.extern_type
110 111 c.extern_name = c.user.extern_name
111 112
112 113 schema = user_schema.UserProfileSchema().bind(
113 114 username=c.user.username, user_emails=c.user.emails)
114 115 form = forms.RcForm(
115 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 119 try:
119 120 valid_data = form.validate(controls)
120 121 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 122 'new_password', 'password_confirmation']
122 123 if c.extern_type != "rhodecode":
123 124 # forbid updating username for external accounts
124 125 skip_attrs.append('username')
125 126 old_email = c.user.email
126 127 UserModel().update_user(
127 128 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 129 **valid_data)
129 130 if old_email != valid_data['email']:
130 131 old = UserEmailMap.query() \
131 132 .filter(UserEmailMap.user == c.user)\
132 133 .filter(UserEmailMap.email == valid_data['email'])\
133 134 .first()
134 135 old.email = old_email
135 136 h.flash(_('Your account was updated successfully'), category='success')
136 137 Session().commit()
137 138 except forms.ValidationFailure as e:
138 139 c.form = e
139 140 return self._get_template_context(c)
140 141 except Exception:
141 142 log.exception("Exception updating user")
142 143 h.flash(_('Error occurred during update of user'),
143 144 category='error')
144 145 raise HTTPFound(h.route_path('my_account_profile'))
145 146
146 147 @LoginRequired()
147 148 @NotAnonymous()
148 149 def my_account_password(self):
149 150 c = self.load_default_context()
150 151 c.active = 'password'
151 152 c.extern_type = c.user.extern_type
152 153
153 154 schema = user_schema.ChangePasswordSchema().bind(
154 155 username=c.user.username)
155 156
156 157 form = forms.Form(
157 158 schema,
158 159 action=h.route_path('my_account_password_update'),
159 160 buttons=(forms.buttons.save, forms.buttons.reset))
160 161
161 162 c.form = form
162 163 return self._get_template_context(c)
163 164
164 165 @LoginRequired()
165 166 @NotAnonymous()
166 167 @CSRFRequired()
167 168 def my_account_password_update(self):
168 169 _ = self.request.translate
169 170 c = self.load_default_context()
170 171 c.active = 'password'
171 172 c.extern_type = c.user.extern_type
172 173
173 174 schema = user_schema.ChangePasswordSchema().bind(
174 175 username=c.user.username)
175 176
176 177 form = forms.Form(
177 178 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178 179
179 180 if c.extern_type != 'rhodecode':
180 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 184 try:
184 185 valid_data = form.validate(controls)
185 186 UserModel().update_user(c.user.user_id, **valid_data)
186 187 c.user.update_userdata(force_password_change=False)
187 188 Session().commit()
188 189 except forms.ValidationFailure as e:
189 190 c.form = e
190 191 return self._get_template_context(c)
191 192
192 193 except Exception:
193 194 log.exception("Exception updating password")
194 195 h.flash(_('Error occurred during update of user password'),
195 196 category='error')
196 197 else:
197 198 instance = c.auth_user.get_instance()
198 199 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5(instance.password)})
200 {'password': md5_safe(instance.password)})
200 201 self.session.save()
201 202 h.flash(_("Successfully updated password"), category='success')
202 203
203 204 raise HTTPFound(self.request.route_path('my_account_password'))
204 205
205 206 @LoginRequired()
206 207 @NotAnonymous()
207 208 def my_account_auth_tokens(self):
208 209 _ = self.request.translate
209 210
210 211 c = self.load_default_context()
211 212 c.active = 'auth_tokens'
212 213 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 214 c.role_values = [
214 215 (x, AuthTokenModel.cls._get_role_name(x))
215 216 for x in AuthTokenModel.cls.ROLES]
216 217 c.role_options = [(c.role_values, _("Role"))]
217 218 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 219 c.user.user_id, show_expired=True)
219 220 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 221 return self._get_template_context(c)
221 222
222 223 @LoginRequired()
223 224 @NotAnonymous()
224 225 @CSRFRequired()
225 226 def my_account_auth_tokens_view(self):
226 227 _ = self.request.translate
227 228 c = self.load_default_context()
228 229
229 230 auth_token_id = self.request.POST.get('auth_token_id')
230 231
231 232 if auth_token_id:
232 233 token = UserApiKeys.get_or_404(auth_token_id)
233 234 if token.user.user_id != c.user.user_id:
234 235 raise HTTPNotFound()
235 236
236 237 return {
237 238 'auth_token': token.api_key
238 239 }
239 240
240 241 def maybe_attach_token_scope(self, token):
241 242 # implemented in EE edition
242 243 pass
243 244
244 245 @LoginRequired()
245 246 @NotAnonymous()
246 247 @CSRFRequired()
247 248 def my_account_auth_tokens_add(self):
248 249 _ = self.request.translate
249 250 c = self.load_default_context()
250 251
251 252 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 253 description = self.request.POST.get('description')
253 254 role = self.request.POST.get('role')
254 255
255 256 token = UserModel().add_auth_token(
256 257 user=c.user.user_id,
257 258 lifetime_minutes=lifetime, role=role, description=description,
258 259 scope_callback=self.maybe_attach_token_scope)
259 260 token_data = token.get_api_data()
260 261
261 262 audit_logger.store_web(
262 263 'user.edit.token.add', action_data={
263 264 'data': {'token': token_data, 'user': 'self'}},
264 265 user=self._rhodecode_user, )
265 266 Session().commit()
266 267
267 268 h.flash(_("Auth token successfully created"), category='success')
268 269 return HTTPFound(h.route_path('my_account_auth_tokens'))
269 270
270 271 @LoginRequired()
271 272 @NotAnonymous()
272 273 @CSRFRequired()
273 274 def my_account_auth_tokens_delete(self):
274 275 _ = self.request.translate
275 276 c = self.load_default_context()
276 277
277 278 del_auth_token = self.request.POST.get('del_auth_token')
278 279
279 280 if del_auth_token:
280 281 token = UserApiKeys.get_or_404(del_auth_token)
281 282 token_data = token.get_api_data()
282 283
283 284 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 285 audit_logger.store_web(
285 286 'user.edit.token.delete', action_data={
286 287 'data': {'token': token_data, 'user': 'self'}},
287 288 user=self._rhodecode_user,)
288 289 Session().commit()
289 290 h.flash(_("Auth token successfully deleted"), category='success')
290 291
291 292 return HTTPFound(h.route_path('my_account_auth_tokens'))
292 293
293 294 @LoginRequired()
294 295 @NotAnonymous()
295 296 def my_account_emails(self):
296 297 _ = self.request.translate
297 298
298 299 c = self.load_default_context()
299 300 c.active = 'emails'
300 301
301 302 c.user_email_map = UserEmailMap.query()\
302 303 .filter(UserEmailMap.user == c.user).all()
303 304
304 305 schema = user_schema.AddEmailSchema().bind(
305 306 username=c.user.username, user_emails=c.user.emails)
306 307
307 308 form = forms.RcForm(schema,
308 309 action=h.route_path('my_account_emails_add'),
309 310 buttons=(forms.buttons.save, forms.buttons.reset))
310 311
311 312 c.form = form
312 313 return self._get_template_context(c)
313 314
314 315 @LoginRequired()
315 316 @NotAnonymous()
316 317 @CSRFRequired()
317 318 def my_account_emails_add(self):
318 319 _ = self.request.translate
319 320 c = self.load_default_context()
320 321 c.active = 'emails'
321 322
322 323 schema = user_schema.AddEmailSchema().bind(
323 324 username=c.user.username, user_emails=c.user.emails)
324 325
325 326 form = forms.RcForm(
326 327 schema, action=h.route_path('my_account_emails_add'),
327 328 buttons=(forms.buttons.save, forms.buttons.reset))
328 329
329 controls = self.request.POST.items()
330 controls = list(self.request.POST.items())
330 331 try:
331 332 valid_data = form.validate(controls)
332 333 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 334 audit_logger.store_web(
334 335 'user.edit.email.add', action_data={
335 336 'data': {'email': valid_data['email'], 'user': 'self'}},
336 337 user=self._rhodecode_user,)
337 338 Session().commit()
338 339 except formencode.Invalid as error:
339 340 h.flash(h.escape(error.error_dict['email']), category='error')
340 341 except forms.ValidationFailure as e:
341 342 c.user_email_map = UserEmailMap.query() \
342 343 .filter(UserEmailMap.user == c.user).all()
343 344 c.form = e
344 345 return self._get_template_context(c)
345 346 except Exception:
346 347 log.exception("Exception adding email")
347 348 h.flash(_('Error occurred during adding email'),
348 349 category='error')
349 350 else:
350 351 h.flash(_("Successfully added email"), category='success')
351 352
352 353 raise HTTPFound(self.request.route_path('my_account_emails'))
353 354
354 355 @LoginRequired()
355 356 @NotAnonymous()
356 357 @CSRFRequired()
357 358 def my_account_emails_delete(self):
358 359 _ = self.request.translate
359 360 c = self.load_default_context()
360 361
361 362 del_email_id = self.request.POST.get('del_email_id')
362 363 if del_email_id:
363 364 email = UserEmailMap.get_or_404(del_email_id).email
364 365 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 366 audit_logger.store_web(
366 367 'user.edit.email.delete', action_data={
367 368 'data': {'email': email, 'user': 'self'}},
368 369 user=self._rhodecode_user,)
369 370 Session().commit()
370 371 h.flash(_("Email successfully deleted"),
371 372 category='success')
372 373 return HTTPFound(h.route_path('my_account_emails'))
373 374
374 375 @LoginRequired()
375 376 @NotAnonymous()
376 377 @CSRFRequired()
377 378 def my_account_notifications_test_channelstream(self):
378 379 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 380 self._rhodecode_user.username, datetime.datetime.now())
380 381 payload = {
381 382 # 'channel': 'broadcast',
382 383 'type': 'message',
383 384 'timestamp': datetime.datetime.utcnow(),
384 385 'user': 'system',
385 386 'pm_users': [self._rhodecode_user.username],
386 387 'message': {
387 388 'message': message,
388 389 'level': 'info',
389 390 'topic': '/notifications'
390 391 }
391 392 }
392 393
393 394 registry = self.request.registry
394 395 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 396 channelstream_config = rhodecode_plugins.get('channelstream', {})
396 397
397 398 try:
398 399 channelstream_request(channelstream_config, [payload], '/message')
399 400 except ChannelstreamException as e:
400 401 log.exception('Failed to send channelstream data')
401 402 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 403 return {"response": 'Channelstream data sent. '
403 404 'You should see a new live message now.'}
404 405
405 406 def _load_my_repos_data(self, watched=False):
406 407
407 408 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408 409
409 410 if watched:
410 411 # repos user watch
411 412 repo_list = Session().query(
412 413 Repository
413 414 ) \
414 415 .join(
415 416 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 417 ) \
417 418 .filter(
418 419 UserFollowing.user_id == self._rhodecode_user.user_id
419 420 ) \
420 421 .filter(or_(
421 422 # generate multiple IN to fix limitation problems
422 423 *in_filter_generator(Repository.repo_id, allowed_ids))
423 424 ) \
424 425 .order_by(Repository.repo_name) \
425 426 .all()
426 427
427 428 else:
428 429 # repos user is owner of
429 430 repo_list = Session().query(
430 431 Repository
431 432 ) \
432 433 .filter(
433 434 Repository.user_id == self._rhodecode_user.user_id
434 435 ) \
435 436 .filter(or_(
436 437 # generate multiple IN to fix limitation problems
437 438 *in_filter_generator(Repository.repo_id, allowed_ids))
438 439 ) \
439 440 .order_by(Repository.repo_name) \
440 441 .all()
441 442
442 443 _render = self.request.get_partial_renderer(
443 444 'rhodecode:templates/data_table/_dt_elements.mako')
444 445
445 446 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 447 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 448 short_name=False, admin=False)
448 449
449 450 repos_data = []
450 451 for repo in repo_list:
451 452 row = {
452 453 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 454 repo.private, repo.archived, repo.fork),
454 455 "name_raw": repo.repo_name.lower(),
455 456 }
456 457
457 458 repos_data.append(row)
458 459
459 460 # json used to render the grid
460 461 return ext_json.str_json(repos_data)
461 462
462 463 @LoginRequired()
463 464 @NotAnonymous()
464 465 def my_account_repos(self):
465 466 c = self.load_default_context()
466 467 c.active = 'repos'
467 468
468 469 # json used to render the grid
469 470 c.data = self._load_my_repos_data()
470 471 return self._get_template_context(c)
471 472
472 473 @LoginRequired()
473 474 @NotAnonymous()
474 475 def my_account_watched(self):
475 476 c = self.load_default_context()
476 477 c.active = 'watched'
477 478
478 479 # json used to render the grid
479 480 c.data = self._load_my_repos_data(watched=True)
480 481 return self._get_template_context(c)
481 482
482 483 @LoginRequired()
483 484 @NotAnonymous()
484 485 def my_account_bookmarks(self):
485 486 c = self.load_default_context()
486 487 c.active = 'bookmarks'
487 488 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 489 self._rhodecode_db_user.user_id, cache=False)
489 490 return self._get_template_context(c)
490 491
491 492 def _process_bookmark_entry(self, entry, user_id):
492 493 position = safe_int(entry.get('position'))
493 494 cur_position = safe_int(entry.get('cur_position'))
494 495 if position is None:
495 496 return
496 497
497 498 # check if this is an existing entry
498 499 is_new = False
499 500 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500 501
501 502 if db_entry and str2bool(entry.get('remove')):
502 503 log.debug('Marked bookmark %s for deletion', db_entry)
503 504 Session().delete(db_entry)
504 505 return
505 506
506 507 if not db_entry:
507 508 # new
508 509 db_entry = UserBookmark()
509 510 is_new = True
510 511
511 512 should_save = False
512 513 default_redirect_url = ''
513 514
514 515 # save repo
515 516 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 517 repo = Repository.get(entry['bookmark_repo'])
517 518 perm_check = HasRepoPermissionAny(
518 519 'repository.read', 'repository.write', 'repository.admin')
519 520 if repo and perm_check(repo_name=repo.repo_name):
520 521 db_entry.repository = repo
521 522 should_save = True
522 523 default_redirect_url = '${repo_url}'
523 524 # save repo group
524 525 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 526 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 527 perm_check = HasRepoGroupPermissionAny(
527 528 'group.read', 'group.write', 'group.admin')
528 529
529 530 if repo_group and perm_check(group_name=repo_group.group_name):
530 531 db_entry.repository_group = repo_group
531 532 should_save = True
532 533 default_redirect_url = '${repo_group_url}'
533 534 # save generic info
534 535 elif entry.get('title') and entry.get('redirect_url'):
535 536 should_save = True
536 537
537 538 if should_save:
538 539 # mark user and position
539 540 db_entry.user_id = user_id
540 541 db_entry.position = position
541 542 db_entry.title = entry.get('title')
542 543 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 544 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544 545
545 546 Session().add(db_entry)
546 547
547 548 @LoginRequired()
548 549 @NotAnonymous()
549 550 @CSRFRequired()
550 551 def my_account_bookmarks_update(self):
551 552 _ = self.request.translate
552 553 c = self.load_default_context()
553 554 c.active = 'bookmarks'
554 555
555 556 controls = peppercorn.parse(self.request.POST.items())
556 557 user_id = c.user.user_id
557 558
558 559 # validate positions
559 560 positions = {}
560 561 for entry in controls.get('bookmarks', []):
561 562 position = safe_int(entry['position'])
562 563 if position is None:
563 564 continue
564 565
565 566 if position in positions:
566 567 h.flash(_("Position {} is defined twice. "
567 568 "Please correct this error.").format(position), category='error')
568 569 return HTTPFound(h.route_path('my_account_bookmarks'))
569 570
570 571 entry['position'] = position
571 572 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 573 positions[position] = entry
573 574
574 575 try:
575 576 for entry in positions.values():
576 577 self._process_bookmark_entry(entry, user_id)
577 578
578 579 Session().commit()
579 580 h.flash(_("Update Bookmarks"), category='success')
580 581 except IntegrityError:
581 582 h.flash(_("Failed to update bookmarks. "
582 583 "Make sure an unique position is used."), category='error')
583 584
584 585 return HTTPFound(h.route_path('my_account_bookmarks'))
585 586
586 587 @LoginRequired()
587 588 @NotAnonymous()
588 589 def my_account_goto_bookmark(self):
589 590
590 591 bookmark_id = self.request.matchdict['bookmark_id']
591 592 user_bookmark = UserBookmark().query()\
592 593 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 594 .filter(UserBookmark.position == bookmark_id).scalar()
594 595
595 596 redirect_url = h.route_path('my_account_bookmarks')
596 597 if not user_bookmark:
597 598 raise HTTPFound(redirect_url)
598 599
599 600 # repository set
600 601 if user_bookmark.repository:
601 602 repo_name = user_bookmark.repository.repo_name
602 603 base_redirect_url = h.route_path(
603 604 'repo_summary', repo_name=repo_name)
604 605 if user_bookmark.redirect_url and \
605 606 '${repo_url}' in user_bookmark.redirect_url:
606 607 redirect_url = string.Template(user_bookmark.redirect_url)\
607 608 .safe_substitute({'repo_url': base_redirect_url})
608 609 else:
609 610 redirect_url = base_redirect_url
610 611 # repository group set
611 612 elif user_bookmark.repository_group:
612 613 repo_group_name = user_bookmark.repository_group.group_name
613 614 base_redirect_url = h.route_path(
614 615 'repo_group_home', repo_group_name=repo_group_name)
615 616 if user_bookmark.redirect_url and \
616 617 '${repo_group_url}' in user_bookmark.redirect_url:
617 618 redirect_url = string.Template(user_bookmark.redirect_url)\
618 619 .safe_substitute({'repo_group_url': base_redirect_url})
619 620 else:
620 621 redirect_url = base_redirect_url
621 622 # custom URL set
622 623 elif user_bookmark.redirect_url:
623 624 server_url = h.route_url('home').rstrip('/')
624 625 redirect_url = string.Template(user_bookmark.redirect_url) \
625 626 .safe_substitute({'server_url': server_url})
626 627
627 628 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 629 raise HTTPFound(redirect_url)
629 630
630 631 @LoginRequired()
631 632 @NotAnonymous()
632 633 def my_account_perms(self):
633 634 c = self.load_default_context()
634 635 c.active = 'perms'
635 636
636 637 c.perm_user = c.auth_user
637 638 return self._get_template_context(c)
638 639
639 640 @LoginRequired()
640 641 @NotAnonymous()
641 642 def my_notifications(self):
642 643 c = self.load_default_context()
643 644 c.active = 'notifications'
644 645
645 646 return self._get_template_context(c)
646 647
647 648 @LoginRequired()
648 649 @NotAnonymous()
649 650 @CSRFRequired()
650 651 def my_notifications_toggle_visibility(self):
651 652 user = self._rhodecode_db_user
652 653 new_status = not user.user_data.get('notification_status', True)
653 654 user.update_userdata(notification_status=new_status)
654 655 Session().commit()
655 656 return user.user_data['notification_status']
656 657
657 658 def _get_pull_requests_list(self, statuses, filter_type=None):
658 659 draw, start, limit = self._extract_chunk(self.request)
659 660 search_q, order_by, order_dir = self._extract_ordering(self.request)
660 661
661 662 _render = self.request.get_partial_renderer(
662 663 'rhodecode:templates/data_table/_dt_elements.mako')
663 664
664 665 if filter_type == 'awaiting_my_review':
665 666 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 667 user_id=self._rhodecode_user.user_id,
667 668 statuses=statuses, query=search_q,
668 669 offset=start, length=limit, order_by=order_by,
669 670 order_dir=order_dir)
670 671
671 672 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 673 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 674 else:
674 675 pull_requests = PullRequestModel().get_im_participating_in(
675 676 user_id=self._rhodecode_user.user_id,
676 677 statuses=statuses, query=search_q,
677 678 offset=start, length=limit, order_by=order_by,
678 679 order_dir=order_dir)
679 680
680 681 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 682 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682 683
683 684 data = []
684 685 comments_model = CommentsModel()
685 686 for pr in pull_requests:
686 687 repo_id = pr.target_repo_id
687 688 comments_count = comments_model.get_all_comments(
688 689 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 690 owned = pr.user_id == self._rhodecode_user.user_id
690 691
691 692 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 693 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 694 if review_statuses and review_statuses[4]:
694 695 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 696 my_review_status = statuses[0][1].status
696 697
697 698 data.append({
698 699 'target_repo': _render('pullrequest_target_repo',
699 700 pr.target_repo.repo_name),
700 701 'name': _render('pullrequest_name',
701 702 pr.pull_request_id, pr.pull_request_state,
702 703 pr.work_in_progress, pr.target_repo.repo_name,
703 704 short=True),
704 705 'name_raw': pr.pull_request_id,
705 706 'status': _render('pullrequest_status',
706 707 pr.calculated_review_status()),
707 708 'my_status': _render('pullrequest_status',
708 709 my_review_status),
709 710 'title': _render('pullrequest_title', pr.title, pr.description),
710 711 'description': h.escape(pr.description),
711 712 'updated_on': _render('pullrequest_updated_on',
712 713 h.datetime_to_time(pr.updated_on),
713 714 pr.versions_count),
714 715 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 716 'created_on': _render('pullrequest_updated_on',
716 717 h.datetime_to_time(pr.created_on)),
717 718 'created_on_raw': h.datetime_to_time(pr.created_on),
718 719 'state': pr.pull_request_state,
719 720 'author': _render('pullrequest_author',
720 721 pr.author.full_contact, ),
721 722 'author_raw': pr.author.full_name,
722 723 'comments': _render('pullrequest_comments', comments_count),
723 724 'comments_raw': comments_count,
724 725 'closed': pr.is_closed(),
725 726 'owned': owned
726 727 })
727 728
728 729 # json used to render the grid
729 730 data = ({
730 731 'draw': draw,
731 732 'data': data,
732 733 'recordsTotal': pull_requests_total_count,
733 734 'recordsFiltered': pull_requests_total_count,
734 735 })
735 736 return data
736 737
737 738 @LoginRequired()
738 739 @NotAnonymous()
739 740 def my_account_pullrequests(self):
740 741 c = self.load_default_context()
741 742 c.active = 'pullrequests'
742 743 req_get = self.request.GET
743 744
744 745 c.closed = str2bool(req_get.get('closed'))
745 746 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746 747
747 748 c.selected_filter = 'all'
748 749 if c.closed:
749 750 c.selected_filter = 'all_closed'
750 751 if c.awaiting_my_review:
751 752 c.selected_filter = 'awaiting_my_review'
752 753
753 754 return self._get_template_context(c)
754 755
755 756 @LoginRequired()
756 757 @NotAnonymous()
757 758 def my_account_pullrequests_data(self):
758 759 self.load_default_context()
759 760 req_get = self.request.GET
760 761
761 762 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 763 closed = str2bool(req_get.get('closed'))
763 764
764 765 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 766 if closed:
766 767 statuses += [PullRequest.STATUS_CLOSED]
767 768
768 769 filter_type = \
769 770 'awaiting_my_review' if awaiting_my_review \
770 771 else None
771 772
772 773 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 774 return data
774 775
775 776 @LoginRequired()
776 777 @NotAnonymous()
777 778 def my_account_user_group_membership(self):
778 779 c = self.load_default_context()
779 780 c.active = 'user_group_membership'
780 781 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 782 for group in self._rhodecode_db_user.group_member]
782 783 c.user_groups = ext_json.str_json(groups)
783 784 return self._get_template_context(c)
@@ -1,93 +1,93 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 30 from rhodecode.lib import helpers as h, rc_cache
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.model.meta import Session
33 33 from rhodecode.model.scm import ScmModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class RepoCachesView(RepoAppView):
39 39 def load_default_context(self):
40 40 c = self._get_local_tmpl_context()
41 41 return c
42 42
43 43 @LoginRequired()
44 44 @HasRepoPermissionAnyDecorator('repository.admin')
45 45 def repo_caches(self):
46 46 c = self.load_default_context()
47 47 c.active = 'caches'
48 48 cached_diffs_dir = c.rhodecode_db_repo.cached_diffs_dir
49 49 c.cached_diff_count = len(c.rhodecode_db_repo.cached_diffs())
50 50 c.cached_diff_size = 0
51 51 if os.path.isdir(cached_diffs_dir):
52 52 c.cached_diff_size = system_info.get_storage_size(cached_diffs_dir)
53 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 56 c.region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
57 57 c.backend = c.region.backend
58 58 c.repo_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
59 59
60 60 return self._get_template_context(c)
61 61
62 62 @LoginRequired()
63 63 @HasRepoPermissionAnyDecorator('repository.admin')
64 64 @CSRFRequired()
65 65 def repo_caches_purge(self):
66 66 _ = self.request.translate
67 67 c = self.load_default_context()
68 68 c.active = 'caches'
69 69 invalidated = 0
70 70
71 71 try:
72 72 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
73 73 Session().commit()
74 74 invalidated +=1
75 75 except Exception:
76 76 log.exception("Exception during cache invalidation")
77 77 h.flash(_('An error occurred during cache invalidation'),
78 78 category='error')
79 79
80 80 try:
81 81 invalidated += 1
82 82 self.rhodecode_vcs_repo.vcsserver_invalidate_cache(delete=True)
83 83 except Exception:
84 84 log.exception("Exception during vcsserver cache invalidation")
85 85 h.flash(_('An error occurred during vcsserver cache invalidation'),
86 86 category='error')
87 87
88 88 if invalidated:
89 89 h.flash(_('Cache invalidation successful. Stages {}/2').format(invalidated),
90 90 category='success')
91 91
92 92 raise HTTPFound(h.route_path(
93 93 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
@@ -1,355 +1,356 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24 24
25 25 from pyramid.renderers import render
26 26 from pyramid.response import Response
27 27
28 28 from rhodecode.apps._base import RepoAppView
29 29 import rhodecode.lib.helpers as h
30 30 from rhodecode.lib import ext_json
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator)
33 33
34 from rhodecode.lib.ext_json import json
35 34 from rhodecode.lib.graphmod import _colored, _dagwalker
36 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 38 from rhodecode.lib.vcs.exceptions import (
39 39 RepositoryError, CommitDoesNotExistError,
40 40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 DEFAULT_CHANGELOG_SIZE = 20
45 45
46 46
47 47 class RepoChangelogView(RepoAppView):
48 48
49 49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 50 """
51 51 This is a safe way to get commit. If an error occurs it redirects to
52 52 tip with proper message
53 53
54 54 :param commit_id: id of commit to fetch
55 55 :param redirect_after: toggle redirection
56 56 """
57 57 _ = self.request.translate
58 58
59 59 try:
60 60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return None
64 64
65 65 h.flash(h.literal(
66 66 _('There are no commits yet')), category='warning')
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69 69
70 70 except (CommitDoesNotExistError, LookupError):
71 71 msg = _('No such commit exists for this repository')
72 72 h.flash(msg, category='error')
73 73 raise HTTPNotFound()
74 74 except RepositoryError as e:
75 75 h.flash(h.escape(safe_str(e)), category='error')
76 76 raise HTTPNotFound()
77 77
78 78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 79 """
80 80 Generates a DAG graph for repo
81 81
82 82 :param repo: repo instance
83 83 :param commits: list of commits
84 84 """
85 85 if not commits:
86 return json.dumps([]), json.dumps([])
86 return ext_json.str_json([]), ext_json.str_json([])
87 87
88 88 def serialize(commit, parents=True):
89 89 data = dict(
90 90 raw_id=commit.raw_id,
91 91 idx=commit.idx,
92 92 branch=None,
93 93 )
94 94 if parents:
95 95 data['parents'] = [
96 96 serialize(x, parents=False) for x in commit.parents]
97 97 return data
98 98
99 99 prev_data = prev_data or []
100 100 next_data = next_data or []
101 101
102 102 current = [serialize(x) for x in commits]
103
103 104 commits = prev_data + current + next_data
104 105
105 106 dag = _dagwalker(repo, commits)
106 107
107 108 data = [[commit_id, vtx, edges, branch]
108 109 for commit_id, vtx, edges, branch in _colored(dag)]
109 110 return ext_json.str_json(data), ext_json.str_json(current)
110 111
111 112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 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 115 category='warning')
115 116 redirect_url = h.route_path(
116 117 'repo_commits_file', repo_name=repo_name,
117 118 commit_id=branch_name, f_path=f_path or '')
118 119 raise HTTPFound(redirect_url)
119 120
120 121 def _load_changelog_data(
121 122 self, c, collection, page, chunk_size, branch_name=None,
122 123 dynamic=False, f_path=None, commit_id=None):
123 124
124 125 def url_generator(page_num):
125 126 query_params = {
126 127 'page': page_num
127 128 }
128 129
129 130 if branch_name:
130 131 query_params.update({
131 132 'branch': branch_name
132 133 })
133 134
134 135 if f_path:
135 136 # changelog for file
136 137 return h.route_path(
137 138 'repo_commits_file',
138 139 repo_name=c.rhodecode_db_repo.repo_name,
139 140 commit_id=commit_id, f_path=f_path,
140 141 _query=query_params)
141 142 else:
142 143 return h.route_path(
143 144 'repo_commits',
144 145 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145 146
146 147 c.total_cs = len(collection)
147 148 c.showing_commits = min(chunk_size, c.total_cs)
148 149 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 150 items_per_page=chunk_size, url_maker=url_generator)
150 151
151 152 c.next_page = c.pagination.next_page
152 153 c.prev_page = c.pagination.previous_page
153 154
154 155 if dynamic:
155 156 if self.request.GET.get('chunk') != 'next':
156 157 c.next_page = None
157 158 if self.request.GET.get('chunk') != 'prev':
158 159 c.prev_page = None
159 160
160 161 page_commit_ids = [x.raw_id for x in c.pagination]
161 162 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 163 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163 164
164 165 def load_default_context(self):
165 166 c = self._get_local_tmpl_context(include_app_defaults=True)
166 167
167 168 c.rhodecode_repo = self.rhodecode_vcs_repo
168 169
169 170 return c
170 171
171 172 @LoginRequired()
172 173 @HasRepoPermissionAnyDecorator(
173 174 'repository.read', 'repository.write', 'repository.admin')
174 175 def repo_changelog(self):
175 176 c = self.load_default_context()
176 177
177 178 commit_id = self.request.matchdict.get('commit_id')
178 179 f_path = self._get_f_path(self.request.matchdict)
179 180 show_hidden = str2bool(self.request.GET.get('evolve'))
180 181
181 182 chunk_size = 20
182 183
183 184 c.branch_name = branch_name = self.request.GET.get('branch') or ''
184 185 c.book_name = book_name = self.request.GET.get('bookmark') or ''
185 186 c.f_path = f_path
186 187 c.commit_id = commit_id
187 188 c.show_hidden = show_hidden
188 189
189 190 hist_limit = safe_int(self.request.GET.get('limit')) or None
190 191
191 192 p = safe_int(self.request.GET.get('page', 1), 1)
192 193
193 194 c.selected_name = branch_name or book_name
194 195 if not commit_id and branch_name:
195 196 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
196 197
197 198 c.changelog_for_path = f_path
198 199 pre_load = self.get_commit_preload_attrs()
199 200
200 201 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
201 202
202 203 try:
203 204 if f_path:
204 205 log.debug('generating changelog for path %s', f_path)
205 206 # get the history for the file !
206 207 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207 208
208 209 try:
209 210 collection = base_commit.get_path_history(
210 211 f_path, limit=hist_limit, pre_load=pre_load)
211 212 if collection and partial_xhr:
212 213 # for ajax call we remove first one since we're looking
213 214 # at it right now in the context of a file commit
214 215 collection.pop(0)
215 216 except (NodeDoesNotExistError, CommitError):
216 217 # this node is not present at tip!
217 218 try:
218 219 commit = self._get_commit_or_redirect(commit_id)
219 220 collection = commit.get_path_history(f_path)
220 221 except RepositoryError as e:
221 222 h.flash(safe_str(e), category='warning')
222 223 redirect_url = h.route_path(
223 224 'repo_commits', repo_name=self.db_repo_name)
224 225 raise HTTPFound(redirect_url)
225 226 collection = list(reversed(collection))
226 227 else:
227 228 collection = self.rhodecode_vcs_repo.get_commits(
228 229 branch_name=branch_name, show_hidden=show_hidden,
229 230 pre_load=pre_load, translate_tags=False)
230 231
231 232 self._load_changelog_data(
232 233 c, collection, p, chunk_size, c.branch_name,
233 234 f_path=f_path, commit_id=commit_id)
234 235
235 236 except EmptyRepositoryError as e:
236 237 h.flash(h.escape(safe_str(e)), category='warning')
237 238 raise HTTPFound(
238 239 h.route_path('repo_summary', repo_name=self.db_repo_name))
239 240 except HTTPFound:
240 241 raise
241 242 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
242 243 log.exception(safe_str(e))
243 244 h.flash(h.escape(safe_str(e)), category='error')
244 245
245 246 if commit_id:
246 247 # from single commit page, we redirect to main commits
247 248 raise HTTPFound(
248 249 h.route_path('repo_commits', repo_name=self.db_repo_name))
249 250 else:
250 251 # otherwise we redirect to summary
251 252 raise HTTPFound(
252 253 h.route_path('repo_summary', repo_name=self.db_repo_name))
253 254
254 255
255 256
256 257 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
257 258 # case when loading dynamic file history in file view
258 259 # loading from ajax, we don't want the first result, it's popped
259 260 # in the code above
260 261 html = render(
261 262 'rhodecode:templates/commits/changelog_file_history.mako',
262 263 self._get_template_context(c), self.request)
263 264 return Response(html)
264 265
265 266 commit_ids = []
266 267 if not f_path:
267 268 # only load graph data when not in file history mode
268 269 commit_ids = c.pagination
269 270
270 271 c.graph_data, c.graph_commits = self._graph(
271 272 self.rhodecode_vcs_repo, commit_ids)
272 273
273 274 return self._get_template_context(c)
274 275
275 276 @LoginRequired()
276 277 @HasRepoPermissionAnyDecorator(
277 278 'repository.read', 'repository.write', 'repository.admin')
278 279 def repo_commits_elements(self):
279 280 c = self.load_default_context()
280 281 commit_id = self.request.matchdict.get('commit_id')
281 282 f_path = self._get_f_path(self.request.matchdict)
282 283 show_hidden = str2bool(self.request.GET.get('evolve'))
283 284
284 285 chunk_size = 20
285 286 hist_limit = safe_int(self.request.GET.get('limit')) or None
286 287
287 288 def wrap_for_error(err):
288 289 html = '<tr>' \
289 290 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
290 291 '</tr>'.format(err)
291 292 return Response(html)
292 293
293 294 c.branch_name = branch_name = self.request.GET.get('branch') or ''
294 295 c.book_name = book_name = self.request.GET.get('bookmark') or ''
295 296 c.f_path = f_path
296 297 c.commit_id = commit_id
297 298 c.show_hidden = show_hidden
298 299
299 300 c.selected_name = branch_name or book_name
300 301 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
301 302 return wrap_for_error(
302 303 safe_str('Branch: {} is not valid'.format(branch_name)))
303 304
304 305 pre_load = self.get_commit_preload_attrs()
305 306
306 307 if f_path:
307 308 try:
308 309 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
309 310 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
310 311 log.exception(safe_str(e))
311 312 raise HTTPFound(
312 313 h.route_path('repo_commits', repo_name=self.db_repo_name))
313 314
314 315 collection = base_commit.get_path_history(
315 316 f_path, limit=hist_limit, pre_load=pre_load)
316 317 collection = list(reversed(collection))
317 318 else:
318 319 collection = self.rhodecode_vcs_repo.get_commits(
319 320 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
320 321 translate_tags=False)
321 322
322 323 p = safe_int(self.request.GET.get('page', 1), 1)
323 324 try:
324 325 self._load_changelog_data(
325 326 c, collection, p, chunk_size, dynamic=True,
326 327 f_path=f_path, commit_id=commit_id)
327 328 except EmptyRepositoryError as e:
328 329 return wrap_for_error(safe_str(e))
329 330 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
330 331 log.exception('Failed to fetch commits')
331 332 return wrap_for_error(safe_str(e))
332 333
333 334 prev_data = None
334 335 next_data = None
335 336
336 337 try:
337 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
338 except json.JSONDecodeError:
338 prev_graph = ext_json.json.loads(self.request.POST.get('graph') or '{}')
339 except ext_json.json.JSONDecodeError:
339 340 prev_graph = {}
340 341
341 342 if self.request.GET.get('chunk') == 'prev':
342 343 next_data = prev_graph
343 344 elif self.request.GET.get('chunk') == 'next':
344 345 prev_data = prev_graph
345 346
346 347 commit_ids = []
347 348 if not f_path:
348 349 # only load graph data when not in file history mode
349 350 commit_ids = c.pagination
350 351
351 352 c.graph_data, c.graph_commits = self._graph(
352 353 self.rhodecode_vcs_repo, commit_ids,
353 354 prev_data=prev_data, next_data=next_data)
354 355
355 356 return self._get_template_context(c)
@@ -1,818 +1,819 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import logging
21 21 import collections
22 22
23 23 from pyramid.httpexceptions import (
24 24 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 25 from pyramid.renderers import render
26 26 from pyramid.response import Response
27 27
28 28 from rhodecode.apps._base import RepoAppView
29 29 from rhodecode.apps.file_store import utils as store_utils
30 30 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 31
32 32 from rhodecode.lib import diffs, codeblocks, channelstream
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 35 from rhodecode.lib import ext_json
36 36 from collections import OrderedDict
37 37 from rhodecode.lib.diffs import (
38 38 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 39 get_diff_whitespace_flag)
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 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 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 RepositoryError, CommitDoesNotExistError)
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 47 ChangesetCommentHistory
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import CommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.settings import VcsSettingsModel
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 def _update_with_GET(params, request):
57 57 for k in ['diff1', 'diff2', 'diff']:
58 58 params[k] += request.GET.getall(k)
59 59
60 60
61 61 class RepoCommitsView(RepoAppView):
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.rhodecode_repo = self.rhodecode_vcs_repo
65 65
66 66 return c
67 67
68 68 def _is_diff_cache_enabled(self, target_repo):
69 69 caching_enabled = self._get_general_setting(
70 70 target_repo, 'rhodecode_diff_cache')
71 71 log.debug('Diff caching enabled: %s', caching_enabled)
72 72 return caching_enabled
73 73
74 74 def _commit(self, commit_id_range, method):
75 75 _ = self.request.translate
76 76 c = self.load_default_context()
77 77 c.fulldiff = self.request.GET.get('fulldiff')
78 78 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79 79
80 80 # fetch global flags of ignore ws or context lines
81 81 diff_context = get_diff_context(self.request)
82 82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 83
84 84 # diff_limit will cut off the whole diff if the limit is applied
85 85 # otherwise it will just hide the big files from the front-end
86 86 diff_limit = c.visual.cut_off_limit_diff
87 87 file_limit = c.visual.cut_off_limit_file
88 88
89 89 # get ranges of commit ids if preset
90 90 commit_range = commit_id_range.split('...')[:2]
91 91
92 92 try:
93 93 pre_load = ['affected_files', 'author', 'branch', 'date',
94 94 'message', 'parents']
95 95 if self.rhodecode_vcs_repo.alias == 'hg':
96 96 pre_load += ['hidden', 'obsolete', 'phase']
97 97
98 98 if len(commit_range) == 2:
99 99 commits = self.rhodecode_vcs_repo.get_commits(
100 100 start_id=commit_range[0], end_id=commit_range[1],
101 101 pre_load=pre_load, translate_tags=False)
102 102 commits = list(commits)
103 103 else:
104 104 commits = [self.rhodecode_vcs_repo.get_commit(
105 105 commit_id=commit_id_range, pre_load=pre_load)]
106 106
107 107 c.commit_ranges = commits
108 108 if not c.commit_ranges:
109 109 raise RepositoryError('The commit range returned an empty result')
110 110 except CommitDoesNotExistError as e:
111 111 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 112 h.flash(msg, category='error')
113 113 raise HTTPNotFound()
114 114 except Exception:
115 115 log.exception("General failure")
116 116 raise HTTPNotFound()
117 117 single_commit = len(c.commit_ranges) == 1
118 118
119 119 if redirect_to_combined and not single_commit:
120 120 source_ref = getattr(c.commit_ranges[0].parents[0]
121 121 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 122 target_ref = c.commit_ranges[-1].raw_id
123 123 next_url = h.route_path(
124 124 'repo_compare',
125 125 repo_name=c.repo_name,
126 126 source_ref_type='rev',
127 127 source_ref=source_ref,
128 128 target_ref_type='rev',
129 129 target_ref=target_ref)
130 130 raise HTTPFound(next_url)
131 131
132 132 c.changes = OrderedDict()
133 133 c.lines_added = 0
134 134 c.lines_deleted = 0
135 135
136 136 # auto collapse if we have more than limit
137 137 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 138 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139 139
140 140 c.commit_statuses = ChangesetStatus.STATUSES
141 141 c.inline_comments = []
142 142 c.files = []
143 143
144 144 c.comments = []
145 145 c.unresolved_comments = []
146 146 c.resolved_comments = []
147 147
148 148 # Single commit
149 149 if single_commit:
150 150 commit = c.commit_ranges[0]
151 151 c.comments = CommentsModel().get_comments(
152 152 self.db_repo.repo_id,
153 153 revision=commit.raw_id)
154 154
155 155 # comments from PR
156 156 statuses = ChangesetStatusModel().get_statuses(
157 157 self.db_repo.repo_id, commit.raw_id,
158 158 with_revisions=True)
159 159
160 160 prs = set()
161 161 reviewers = list()
162 162 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 163 for c_status in statuses:
164 164
165 165 # extract associated pull-requests from votes
166 166 if c_status.pull_request:
167 167 prs.add(c_status.pull_request)
168 168
169 169 # extract reviewers
170 170 _user_id = c_status.author.user_id
171 171 if _user_id not in reviewers_duplicates:
172 172 reviewers.append(
173 173 StrictAttributeDict({
174 174 'user': c_status.author,
175 175
176 176 # fake attributed for commit, page that we don't have
177 177 # but we share the display with PR page
178 178 'mandatory': False,
179 179 'reasons': [],
180 180 'rule_user_group_data': lambda: None
181 181 })
182 182 )
183 183 reviewers_duplicates.add(_user_id)
184 184
185 185 c.reviewers_count = len(reviewers)
186 186 c.observers_count = 0
187 187
188 188 # from associated statuses, check the pull requests, and
189 189 # show comments from them
190 190 for pr in prs:
191 191 c.comments.extend(pr.comments)
192 192
193 193 c.unresolved_comments = CommentsModel()\
194 194 .get_commit_unresolved_todos(commit.raw_id)
195 195 c.resolved_comments = CommentsModel()\
196 196 .get_commit_resolved_todos(commit.raw_id)
197 197
198 198 c.inline_comments_flat = CommentsModel()\
199 199 .get_commit_inline_comments(commit.raw_id)
200 200
201 201 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 202 statuses, reviewers)
203 203
204 204 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205 205
206 206 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207 207
208 208 for review_obj, member, reasons, mandatory, status in review_statuses:
209 209 member_reviewer = h.reviewer_as_json(
210 210 member, reasons=reasons, mandatory=mandatory, role=None,
211 211 user_group=None
212 212 )
213 213
214 214 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 215 member_reviewer['review_status'] = current_review_status
216 216 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 217 member_reviewer['allowed_to_update'] = False
218 218 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219 219
220 220 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
221 221
222 222 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 223 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 224 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225 225
226 226 diff = None
227 227 # Iterate over ranges (default commit view is always one commit)
228 228 for commit in c.commit_ranges:
229 229 c.changes[commit.raw_id] = []
230 230
231 231 commit2 = commit
232 232 commit1 = commit.first_parent
233 233
234 234 if method == 'show':
235 235 inline_comments = CommentsModel().get_inline_comments(
236 236 self.db_repo.repo_id, revision=commit.raw_id)
237 237 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 238 inline_comments))
239 239 c.inline_comments = inline_comments
240 240
241 241 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 242 self.db_repo)
243 243 cache_file_path = diff_cache_exist(
244 244 cache_path, 'diff', commit.raw_id,
245 245 hide_whitespace_changes, diff_context, c.fulldiff)
246 246
247 247 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 248 force_recache = str2bool(self.request.GET.get('force_recache'))
249 249
250 250 cached_diff = None
251 251 if caching_enabled:
252 252 cached_diff = load_cached_diff(cache_file_path)
253 253
254 254 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 255 if not force_recache and has_proper_diff_cache:
256 256 diffset = cached_diff['diff']
257 257 else:
258 258 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 259 commit1, commit2,
260 260 ignore_whitespace=hide_whitespace_changes,
261 261 context=diff_context)
262 262
263 263 diff_processor = diffs.DiffProcessor(
264 264 vcs_diff, format='newdiff', diff_limit=diff_limit,
265 265 file_limit=file_limit, show_full_diff=c.fulldiff)
266 266
267 267 _parsed = diff_processor.prepare()
268 268
269 269 diffset = codeblocks.DiffSet(
270 270 repo_name=self.db_repo_name,
271 271 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 272 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 273
274 274 diffset = self.path_filter.render_patchset_filtered(
275 275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 276
277 277 # save cached diff
278 278 if caching_enabled:
279 279 cache_diff(cache_file_path, diffset, None)
280 280
281 281 c.limited_diff = diffset.limited_diff
282 282 c.changes[commit.raw_id] = diffset
283 283 else:
284 284 # TODO(marcink): no cache usage here...
285 285 _diff = self.rhodecode_vcs_repo.get_diff(
286 286 commit1, commit2,
287 287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 288 diff_processor = diffs.DiffProcessor(
289 289 _diff, format='newdiff', diff_limit=diff_limit,
290 290 file_limit=file_limit, show_full_diff=c.fulldiff)
291 291 # downloads/raw we only need RAW diff nothing else
292 292 diff = self.path_filter.get_raw_patch(diff_processor)
293 293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 294
295 295 # sort comments by how they were generated
296 296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 297 c.at_version_num = None
298 298
299 299 if len(c.commit_ranges) == 1:
300 300 c.commit = c.commit_ranges[0]
301 301 c.parent_tmpl = ''.join(
302 302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 303
304 304 if method == 'download':
305 305 response = Response(diff)
306 306 response.content_type = 'text/plain'
307 307 response.content_disposition = (
308 308 'attachment; filename=%s.diff' % commit_id_range[:12])
309 309 return response
310 310 elif method == 'patch':
311 c.diff = safe_unicode(diff)
311
312 c.diff = safe_str(diff)
312 313 patch = render(
313 314 'rhodecode:templates/changeset/patch_changeset.mako',
314 315 self._get_template_context(c), self.request)
315 316 response = Response(patch)
316 317 response.content_type = 'text/plain'
317 318 return response
318 319 elif method == 'raw':
319 320 response = Response(diff)
320 321 response.content_type = 'text/plain'
321 322 return response
322 323 elif method == 'show':
323 324 if len(c.commit_ranges) == 1:
324 325 html = render(
325 326 'rhodecode:templates/changeset/changeset.mako',
326 327 self._get_template_context(c), self.request)
327 328 return Response(html)
328 329 else:
329 330 c.ancestor = None
330 331 c.target_repo = self.db_repo
331 332 html = render(
332 333 'rhodecode:templates/changeset/changeset_range.mako',
333 334 self._get_template_context(c), self.request)
334 335 return Response(html)
335 336
336 337 raise HTTPBadRequest()
337 338
338 339 @LoginRequired()
339 340 @HasRepoPermissionAnyDecorator(
340 341 'repository.read', 'repository.write', 'repository.admin')
341 342 def repo_commit_show(self):
342 343 commit_id = self.request.matchdict['commit_id']
343 344 return self._commit(commit_id, method='show')
344 345
345 346 @LoginRequired()
346 347 @HasRepoPermissionAnyDecorator(
347 348 'repository.read', 'repository.write', 'repository.admin')
348 349 def repo_commit_raw(self):
349 350 commit_id = self.request.matchdict['commit_id']
350 351 return self._commit(commit_id, method='raw')
351 352
352 353 @LoginRequired()
353 354 @HasRepoPermissionAnyDecorator(
354 355 'repository.read', 'repository.write', 'repository.admin')
355 356 def repo_commit_patch(self):
356 357 commit_id = self.request.matchdict['commit_id']
357 358 return self._commit(commit_id, method='patch')
358 359
359 360 @LoginRequired()
360 361 @HasRepoPermissionAnyDecorator(
361 362 'repository.read', 'repository.write', 'repository.admin')
362 363 def repo_commit_download(self):
363 364 commit_id = self.request.matchdict['commit_id']
364 365 return self._commit(commit_id, method='download')
365 366
366 367 def _commit_comments_create(self, commit_id, comments):
367 368 _ = self.request.translate
368 369 data = {}
369 370 if not comments:
370 371 return
371 372
372 373 commit = self.db_repo.get_commit(commit_id)
373 374
374 375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 376 for entry in comments:
376 377 c = self.load_default_context()
377 378 comment_type = entry['comment_type']
378 379 text = entry['text']
379 380 status = entry['status']
380 381 is_draft = str2bool(entry['is_draft'])
381 382 resolves_comment_id = entry['resolves_comment_id']
382 383 f_path = entry['f_path']
383 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 387 if status:
387 388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 389 % {'transition_icon': '>',
389 390 'status': ChangesetStatus.get_status_lbl(status)})
390 391
391 392 comment = CommentsModel().create(
392 393 text=text,
393 394 repo=self.db_repo.repo_id,
394 395 user=self._rhodecode_db_user.user_id,
395 396 commit_id=commit_id,
396 397 f_path=f_path,
397 398 line_no=line_no,
398 399 status_change=(ChangesetStatus.get_status_lbl(status)
399 400 if status else None),
400 401 status_change_type=status,
401 402 comment_type=comment_type,
402 403 is_draft=is_draft,
403 404 resolves_comment_id=resolves_comment_id,
404 405 auth_user=self._rhodecode_user,
405 406 send_email=not is_draft, # skip notification for draft comments
406 407 )
407 408 is_inline = comment.is_inline
408 409
409 410 # get status if set !
410 411 if status:
411 412 # `dont_allow_on_closed_pull_request = True` means
412 413 # if latest status was from pull request and it's closed
413 414 # disallow changing status !
414 415
415 416 try:
416 417 ChangesetStatusModel().set_status(
417 418 self.db_repo.repo_id,
418 419 status,
419 420 self._rhodecode_db_user.user_id,
420 421 comment,
421 422 revision=commit_id,
422 423 dont_allow_on_closed_pull_request=True
423 424 )
424 425 except StatusChangeOnClosedPullRequestError:
425 426 msg = _('Changing the status of a commit associated with '
426 427 'a closed pull request is not allowed')
427 428 log.exception(msg)
428 429 h.flash(msg, category='warning')
429 430 raise HTTPFound(h.route_path(
430 431 'repo_commit', repo_name=self.db_repo_name,
431 432 commit_id=commit_id))
432 433
433 434 Session().flush()
434 435 # this is somehow required to get access to some relationship
435 436 # loaded on comment
436 437 Session().refresh(comment)
437 438
438 439 # skip notifications for drafts
439 440 if not is_draft:
440 441 CommentsModel().trigger_commit_comment_hook(
441 442 self.db_repo, self._rhodecode_user, 'create',
442 443 data={'comment': comment, 'commit': commit})
443 444
444 445 comment_id = comment.comment_id
445 446 data[comment_id] = {
446 447 'target_id': target_elem_id
447 448 }
448 449 Session().flush()
449 450
450 451 c.co = comment
451 452 c.at_version_num = 0
452 453 c.is_new = True
453 454 rendered_comment = render(
454 455 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 456 self._get_template_context(c), self.request)
456 457
457 458 data[comment_id].update(comment.get_dict())
458 459 data[comment_id].update({'rendered_text': rendered_comment})
459 460
460 461 # finalize, commit and redirect
461 462 Session().commit()
462 463
463 464 # skip channelstream for draft comments
464 465 if not all_drafts:
465 466 comment_broadcast_channel = channelstream.comment_channel(
466 467 self.db_repo_name, commit_obj=commit)
467 468
468 469 comment_data = data
469 470 posted_comment_type = 'inline' if is_inline else 'general'
470 471 if len(data) == 1:
471 472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 473 else:
473 474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474 475
475 476 channelstream.comment_channelstream_push(
476 477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 478 comment_data=comment_data)
478 479
479 480 return data
480 481
481 482 @LoginRequired()
482 483 @NotAnonymous()
483 484 @HasRepoPermissionAnyDecorator(
484 485 'repository.read', 'repository.write', 'repository.admin')
485 486 @CSRFRequired()
486 487 def repo_commit_comment_create(self):
487 488 _ = self.request.translate
488 489 commit_id = self.request.matchdict['commit_id']
489 490
490 491 multi_commit_ids = []
491 492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 494 if _commit_id not in multi_commit_ids:
494 495 multi_commit_ids.append(_commit_id)
495 496
496 497 commit_ids = multi_commit_ids or [commit_id]
497 498
498 499 data = []
499 500 # Multiple comments for each passed commit id
500 501 for current_id in filter(None, commit_ids):
501 502 comment_data = {
502 503 'comment_type': self.request.POST.get('comment_type'),
503 504 'text': self.request.POST.get('text'),
504 505 'status': self.request.POST.get('changeset_status', None),
505 506 'is_draft': self.request.POST.get('draft'),
506 507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 508 'close_pull_request': self.request.POST.get('close_pull_request'),
508 509 'f_path': self.request.POST.get('f_path'),
509 510 'line': self.request.POST.get('line'),
510 511 }
511 512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 513 data.append(comment)
513 514
514 515 return data if len(data) > 1 else data[0]
515 516
516 517 @LoginRequired()
517 518 @NotAnonymous()
518 519 @HasRepoPermissionAnyDecorator(
519 520 'repository.read', 'repository.write', 'repository.admin')
520 521 @CSRFRequired()
521 522 def repo_commit_comment_preview(self):
522 523 # Technically a CSRF token is not needed as no state changes with this
523 524 # call. However, as this is a POST is better to have it, so automated
524 525 # tools don't flag it as potential CSRF.
525 526 # Post is required because the payload could be bigger than the maximum
526 527 # allowed by GET.
527 528
528 529 text = self.request.POST.get('text')
529 530 renderer = self.request.POST.get('renderer') or 'rst'
530 531 if text:
531 532 return h.render(text, renderer=renderer, mentions=True,
532 533 repo_name=self.db_repo_name)
533 534 return ''
534 535
535 536 @LoginRequired()
536 537 @HasRepoPermissionAnyDecorator(
537 538 'repository.read', 'repository.write', 'repository.admin')
538 539 @CSRFRequired()
539 540 def repo_commit_comment_history_view(self):
540 541 c = self.load_default_context()
541 542 comment_id = self.request.matchdict['comment_id']
542 543 comment_history_id = self.request.matchdict['comment_history_id']
543 544
544 545 comment = ChangesetComment.get_or_404(comment_id)
545 546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
546 547 if comment.draft and not comment_owner:
547 548 # if we see draft comments history, we only allow this for owner
548 549 raise HTTPNotFound()
549 550
550 551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
551 552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
552 553
553 554 if is_repo_comment:
554 555 c.comment_history = comment_history
555 556
556 557 rendered_comment = render(
557 558 'rhodecode:templates/changeset/comment_history.mako',
558 559 self._get_template_context(c), self.request)
559 560 return rendered_comment
560 561 else:
561 562 log.warning('No permissions for user %s to show comment_history_id: %s',
562 563 self._rhodecode_db_user, comment_history_id)
563 564 raise HTTPNotFound()
564 565
565 566 @LoginRequired()
566 567 @NotAnonymous()
567 568 @HasRepoPermissionAnyDecorator(
568 569 'repository.read', 'repository.write', 'repository.admin')
569 570 @CSRFRequired()
570 571 def repo_commit_comment_attachment_upload(self):
571 572 c = self.load_default_context()
572 573 upload_key = 'attachment'
573 574
574 575 file_obj = self.request.POST.get(upload_key)
575 576
576 577 if file_obj is None:
577 578 self.request.response.status = 400
578 579 return {'store_fid': None,
579 580 'access_path': None,
580 581 'error': '{} data field is missing'.format(upload_key)}
581 582
582 583 if not hasattr(file_obj, 'filename'):
583 584 self.request.response.status = 400
584 585 return {'store_fid': None,
585 586 'access_path': None,
586 587 'error': 'filename cannot be read from the data field'}
587 588
588 589 filename = file_obj.filename
589 590 file_display_name = filename
590 591
591 592 metadata = {
592 593 'user_uploaded': {'username': self._rhodecode_user.username,
593 594 'user_id': self._rhodecode_user.user_id,
594 595 'ip': self._rhodecode_user.ip_addr}}
595 596
596 597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
597 598 allowed_extensions = [
598 599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
599 600 '.pptx', '.txt', '.xlsx', '.zip']
600 601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
601 602
602 603 try:
603 604 storage = store_utils.get_file_storage(self.request.registry.settings)
604 605 store_uid, metadata = storage.save_file(
605 606 file_obj.file, filename, extra_metadata=metadata,
606 607 extensions=allowed_extensions, max_filesize=max_file_size)
607 608 except FileNotAllowedException:
608 609 self.request.response.status = 400
609 610 permitted_extensions = ', '.join(allowed_extensions)
610 611 error_msg = 'File `{}` is not allowed. ' \
611 612 'Only following extensions are permitted: {}'.format(
612 613 filename, permitted_extensions)
613 614 return {'store_fid': None,
614 615 'access_path': None,
615 616 'error': error_msg}
616 617 except FileOverSizeException:
617 618 self.request.response.status = 400
618 619 limit_mb = h.format_byte_size_binary(max_file_size)
619 620 return {'store_fid': None,
620 621 'access_path': None,
621 622 'error': 'File {} is exceeding allowed limit of {}.'.format(
622 623 filename, limit_mb)}
623 624
624 625 try:
625 626 entry = FileStore.create(
626 627 file_uid=store_uid, filename=metadata["filename"],
627 628 file_hash=metadata["sha256"], file_size=metadata["size"],
628 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 631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
631 632 scope_repo_id=self.db_repo.repo_id
632 633 )
633 634 Session().add(entry)
634 635 Session().commit()
635 636 log.debug('Stored upload in DB as %s', entry)
636 637 except Exception:
637 638 log.exception('Failed to store file %s', filename)
638 639 self.request.response.status = 400
639 640 return {'store_fid': None,
640 641 'access_path': None,
641 642 'error': 'File {} failed to store in DB.'.format(filename)}
642 643
643 644 Session().commit()
644 645
645 646 return {
646 647 'store_fid': store_uid,
647 648 'access_path': h.route_path(
648 649 'download_file', fid=store_uid),
649 650 'fqn_access_path': h.route_url(
650 651 'download_file', fid=store_uid),
651 652 'repo_access_path': h.route_path(
652 653 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
653 654 'repo_fqn_access_path': h.route_url(
654 655 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
655 656 }
656 657
657 658 @LoginRequired()
658 659 @NotAnonymous()
659 660 @HasRepoPermissionAnyDecorator(
660 661 'repository.read', 'repository.write', 'repository.admin')
661 662 @CSRFRequired()
662 663 def repo_commit_comment_delete(self):
663 664 commit_id = self.request.matchdict['commit_id']
664 665 comment_id = self.request.matchdict['comment_id']
665 666
666 667 comment = ChangesetComment.get_or_404(comment_id)
667 668 if not comment:
668 669 log.debug('Comment with id:%s not found, skipping', comment_id)
669 670 # comment already deleted in another call probably
670 671 return True
671 672
672 673 if comment.immutable:
673 674 # don't allow deleting comments that are immutable
674 675 raise HTTPForbidden()
675 676
676 677 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
677 678 super_admin = h.HasPermissionAny('hg.admin')()
678 679 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
679 680 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
680 681 comment_repo_admin = is_repo_admin and is_repo_comment
681 682
682 683 if comment.draft and not comment_owner:
683 684 # We never allow to delete draft comments for other than owners
684 685 raise HTTPNotFound()
685 686
686 687 if super_admin or comment_owner or comment_repo_admin:
687 688 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
688 689 Session().commit()
689 690 return True
690 691 else:
691 692 log.warning('No permissions for user %s to delete comment_id: %s',
692 693 self._rhodecode_db_user, comment_id)
693 694 raise HTTPNotFound()
694 695
695 696 @LoginRequired()
696 697 @NotAnonymous()
697 698 @HasRepoPermissionAnyDecorator(
698 699 'repository.read', 'repository.write', 'repository.admin')
699 700 @CSRFRequired()
700 701 def repo_commit_comment_edit(self):
701 702 self.load_default_context()
702 703
703 704 commit_id = self.request.matchdict['commit_id']
704 705 comment_id = self.request.matchdict['comment_id']
705 706 comment = ChangesetComment.get_or_404(comment_id)
706 707
707 708 if comment.immutable:
708 709 # don't allow deleting comments that are immutable
709 710 raise HTTPForbidden()
710 711
711 712 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
712 713 super_admin = h.HasPermissionAny('hg.admin')()
713 714 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
714 715 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
715 716 comment_repo_admin = is_repo_admin and is_repo_comment
716 717
717 718 if super_admin or comment_owner or comment_repo_admin:
718 719 text = self.request.POST.get('text')
719 720 version = self.request.POST.get('version')
720 721 if text == comment.text:
721 722 log.warning(
722 723 'Comment(repo): '
723 724 'Trying to create new version '
724 725 'with the same comment body {}'.format(
725 726 comment_id,
726 727 )
727 728 )
728 729 raise HTTPNotFound()
729 730
730 731 if version.isdigit():
731 732 version = int(version)
732 733 else:
733 734 log.warning(
734 735 'Comment(repo): Wrong version type {} {} '
735 736 'for comment {}'.format(
736 737 version,
737 738 type(version),
738 739 comment_id,
739 740 )
740 741 )
741 742 raise HTTPNotFound()
742 743
743 744 try:
744 745 comment_history = CommentsModel().edit(
745 746 comment_id=comment_id,
746 747 text=text,
747 748 auth_user=self._rhodecode_user,
748 749 version=version,
749 750 )
750 751 except CommentVersionMismatch:
751 752 raise HTTPConflict()
752 753
753 754 if not comment_history:
754 755 raise HTTPNotFound()
755 756
756 757 if not comment.draft:
757 758 commit = self.db_repo.get_commit(commit_id)
758 759 CommentsModel().trigger_commit_comment_hook(
759 760 self.db_repo, self._rhodecode_user, 'edit',
760 761 data={'comment': comment, 'commit': commit})
761 762
762 763 Session().commit()
763 764 return {
764 765 'comment_history_id': comment_history.comment_history_id,
765 766 'comment_id': comment.comment_id,
766 767 'comment_version': comment_history.version,
767 768 'comment_author_username': comment_history.author.username,
768 769 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
769 770 'comment_created_on': h.age_component(comment_history.created_on,
770 771 time_is_local=True),
771 772 }
772 773 else:
773 774 log.warning('No permissions for user %s to edit comment_id: %s',
774 775 self._rhodecode_db_user, comment_id)
775 776 raise HTTPNotFound()
776 777
777 778 @LoginRequired()
778 779 @HasRepoPermissionAnyDecorator(
779 780 'repository.read', 'repository.write', 'repository.admin')
780 781 def repo_commit_data(self):
781 782 commit_id = self.request.matchdict['commit_id']
782 783 self.load_default_context()
783 784
784 785 try:
785 786 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
786 787 except CommitDoesNotExistError as e:
787 788 return EmptyCommit(message=str(e))
788 789
789 790 @LoginRequired()
790 791 @HasRepoPermissionAnyDecorator(
791 792 'repository.read', 'repository.write', 'repository.admin')
792 793 def repo_commit_children(self):
793 794 commit_id = self.request.matchdict['commit_id']
794 795 self.load_default_context()
795 796
796 797 try:
797 798 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 799 children = commit.children
799 800 except CommitDoesNotExistError:
800 801 children = []
801 802
802 803 result = {"results": children}
803 804 return result
804 805
805 806 @LoginRequired()
806 807 @HasRepoPermissionAnyDecorator(
807 808 'repository.read', 'repository.write', 'repository.admin')
808 809 def repo_commit_parents(self):
809 810 commit_id = self.request.matchdict['commit_id']
810 811 self.load_default_context()
811 812
812 813 try:
813 814 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
814 815 parents = commit.parents
815 816 except CommitDoesNotExistError:
816 817 parents = []
817 818 result = {"results": parents}
818 819 return result
@@ -1,305 +1,306 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 25
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 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 36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 39 NodeDoesNotExistError)
40 40 from rhodecode.model.db import Repository, ChangesetStatus
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class RepoCompareView(RepoAppView):
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 49 return c
50 50
51 51 def _get_commit_or_redirect(
52 52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 53 """
54 54 This is a safe way to get a commit. If an error occurs it
55 55 redirects to a commit with a proper message. If partial is set
56 56 then it does not do redirect raise and throws an exception instead.
57 57 """
58 58 _ = self.request.translate
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 if not partial:
67 67 raise HTTPFound(
68 68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 69 raise HTTPBadRequest()
70 70
71 71 except RepositoryError as e:
72 72 log.exception(safe_str(e))
73 73 h.flash(h.escape(safe_str(e)), category='warning')
74 74 if not partial:
75 75 raise HTTPFound(
76 76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 77 raise HTTPBadRequest()
78 78
79 79 @LoginRequired()
80 80 @HasRepoPermissionAnyDecorator(
81 81 'repository.read', 'repository.write', 'repository.admin')
82 82 def compare_select(self):
83 83 _ = self.request.translate
84 84 c = self.load_default_context()
85 85
86 86 source_repo = self.db_repo_name
87 87 target_repo = self.request.GET.get('target_repo', source_repo)
88 88 c.source_repo = Repository.get_by_repo_name(source_repo)
89 89 c.target_repo = Repository.get_by_repo_name(target_repo)
90 90
91 91 if c.source_repo is None or c.target_repo is None:
92 92 raise HTTPNotFound()
93 93
94 94 c.compare_home = True
95 95 c.commit_ranges = []
96 96 c.collapse_all_commits = False
97 97 c.diffset = None
98 98 c.limited_diff = False
99 99 c.source_ref = c.target_ref = _('Select commit')
100 100 c.source_ref_type = ""
101 101 c.target_ref_type = ""
102 102 c.commit_statuses = ChangesetStatus.STATUSES
103 103 c.preview_mode = False
104 104 c.file_path = None
105 105
106 106 return self._get_template_context(c)
107 107
108 108 @LoginRequired()
109 109 @HasRepoPermissionAnyDecorator(
110 110 'repository.read', 'repository.write', 'repository.admin')
111 111 def compare(self):
112 112 _ = self.request.translate
113 113 c = self.load_default_context()
114 114
115 115 source_ref_type = self.request.matchdict['source_ref_type']
116 116 source_ref = self.request.matchdict['source_ref']
117 117 target_ref_type = self.request.matchdict['target_ref_type']
118 118 target_ref = self.request.matchdict['target_ref']
119 119
120 120 # source_ref will be evaluated in source_repo
121 121 source_repo_name = self.db_repo_name
122 122 source_path, source_id = parse_path_ref(source_ref)
123 123
124 124 # target_ref will be evaluated in target_repo
125 125 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
126 126 target_path, target_id = parse_path_ref(
127 127 target_ref, default_path=self.request.GET.get('f_path', ''))
128 128
129 129 # if merge is True
130 130 # Show what changes since the shared ancestor commit of target/source
131 131 # the source would get if it was merged with target. Only commits
132 132 # which are in target but not in source will be shown.
133 133 merge = str2bool(self.request.GET.get('merge'))
134 134 # if merge is False
135 135 # Show a raw diff of source/target refs even if no ancestor exists
136 136
137 137 # c.fulldiff disables cut_off_limit
138 138 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
139 139
140 140 # fetch global flags of ignore ws or context lines
141 141 diff_context = diffs.get_diff_context(self.request)
142 142 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
143 143
144 144 c.file_path = target_path
145 145 c.commit_statuses = ChangesetStatus.STATUSES
146 146
147 147 # if partial, returns just compare_commits.html (commits log)
148 148 partial = self.request.is_xhr
149 149
150 150 # swap url for compare_diff page
151 151 c.swap_url = h.route_path(
152 152 'repo_compare',
153 153 repo_name=target_repo_name,
154 154 source_ref_type=target_ref_type,
155 155 source_ref=target_ref,
156 156 target_repo=source_repo_name,
157 157 target_ref_type=source_ref_type,
158 158 target_ref=source_ref,
159 159 _query=dict(merge=merge and '1' or '', f_path=target_path))
160 160
161 161 source_repo = Repository.get_by_repo_name(source_repo_name)
162 162 target_repo = Repository.get_by_repo_name(target_repo_name)
163 163
164 164 if source_repo is None:
165 165 log.error('Could not find the source repo: {}'
166 166 .format(source_repo_name))
167 167 h.flash(_('Could not find the source repo: `{}`')
168 168 .format(h.escape(source_repo_name)), category='error')
169 169 raise HTTPFound(
170 170 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
171 171
172 172 if target_repo is None:
173 173 log.error('Could not find the target repo: {}'
174 174 .format(source_repo_name))
175 175 h.flash(_('Could not find the target repo: `{}`')
176 176 .format(h.escape(target_repo_name)), category='error')
177 177 raise HTTPFound(
178 178 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
179 179
180 180 source_scm = source_repo.scm_instance()
181 181 target_scm = target_repo.scm_instance()
182 182
183 183 source_alias = source_scm.alias
184 184 target_alias = target_scm.alias
185 185 if source_alias != target_alias:
186 186 msg = _('The comparison of two different kinds of remote repos '
187 187 'is not available')
188 188 log.error(msg)
189 189 h.flash(msg, category='error')
190 190 raise HTTPFound(
191 191 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
192 192
193 193 source_commit = self._get_commit_or_redirect(
194 194 ref=source_id, ref_type=source_ref_type, repo=source_repo,
195 195 partial=partial)
196 196 target_commit = self._get_commit_or_redirect(
197 197 ref=target_id, ref_type=target_ref_type, repo=target_repo,
198 198 partial=partial)
199 199
200 200 c.compare_home = False
201 201 c.source_repo = source_repo
202 202 c.target_repo = target_repo
203 203 c.source_ref = source_ref
204 204 c.target_ref = target_ref
205 205 c.source_ref_type = source_ref_type
206 206 c.target_ref_type = target_ref_type
207 207
208 208 pre_load = ["author", "date", "message", "branch"]
209 209 c.ancestor = None
210 210
211 211 try:
212 212 c.commit_ranges = source_scm.compare(
213 213 source_commit.raw_id, target_commit.raw_id,
214 214 target_scm, merge, pre_load=pre_load) or []
215 215 if merge:
216 216 c.ancestor = source_scm.get_common_ancestor(
217 217 source_commit.raw_id, target_commit.raw_id, target_scm)
218 218 except RepositoryRequirementError:
219 219 msg = _('Could not compare repos with different '
220 220 'large file settings')
221 221 log.error(msg)
222 222 if partial:
223 223 return Response(msg)
224 224 h.flash(msg, category='error')
225 225 raise HTTPFound(
226 226 h.route_path('repo_compare_select',
227 227 repo_name=self.db_repo_name))
228 228
229 229 c.statuses = self.db_repo.statuses(
230 230 [x.raw_id for x in c.commit_ranges])
231 231
232 232 # auto collapse if we have more than limit
233 233 collapse_limit = diffs.DiffProcessor._collapse_commits_over
234 234 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
235 235
236 236 if partial: # for PR ajax commits loader
237 237 if not c.ancestor:
238 238 return Response('') # cannot merge if there is no ancestor
239 239
240 240 html = render(
241 241 'rhodecode:templates/compare/compare_commits.mako',
242 242 self._get_template_context(c), self.request)
243 243 return Response(html)
244 244
245 245 if c.ancestor:
246 246 # case we want a simple diff without incoming commits,
247 247 # previewing what will be merged.
248 248 # Make the diff on target repo (which is known to have target_ref)
249 249 log.debug('Using ancestor %s as source_ref instead of %s',
250 250 c.ancestor, source_ref)
251 251 source_repo = target_repo
252 252 source_commit = target_repo.get_commit(commit_id=c.ancestor)
253 253
254 254 # diff_limit will cut off the whole diff if the limit is applied
255 255 # otherwise it will just hide the big files from the front-end
256 256 diff_limit = c.visual.cut_off_limit_diff
257 257 file_limit = c.visual.cut_off_limit_file
258 258
259 259 log.debug('calculating diff between '
260 260 'source_ref:%s and target_ref:%s for repo `%s`',
261 261 source_commit, target_commit,
262 safe_unicode(source_repo.scm_instance().path))
262 safe_str(source_repo.scm_instance().path))
263 263
264 264 if source_commit.repository != target_commit.repository:
265
265 266 msg = _(
266 267 "Repositories unrelated. "
267 268 "Cannot compare commit %(commit1)s from repository %(repo1)s "
268 269 "with commit %(commit2)s from repository %(repo2)s.") % {
269 270 'commit1': h.show_id(source_commit),
270 271 'repo1': source_repo.repo_name,
271 272 'commit2': h.show_id(target_commit),
272 273 'repo2': target_repo.repo_name,
273 274 }
274 275 h.flash(msg, category='error')
275 276 raise HTTPFound(
276 277 h.route_path('repo_compare_select',
277 278 repo_name=self.db_repo_name))
278 279
279 280 txt_diff = source_repo.scm_instance().get_diff(
280 281 commit1=source_commit, commit2=target_commit,
281 282 path=target_path, path1=source_path,
282 283 ignore_whitespace=hide_whitespace_changes, context=diff_context)
283 284
284 285 diff_processor = diffs.DiffProcessor(
285 286 txt_diff, format='newdiff', diff_limit=diff_limit,
286 287 file_limit=file_limit, show_full_diff=c.fulldiff)
287 288 _parsed = diff_processor.prepare()
288 289
289 290 diffset = codeblocks.DiffSet(
290 291 repo_name=source_repo.repo_name,
291 292 source_node_getter=codeblocks.diffset_node_getter(source_commit),
292 293 target_repo_name=self.db_repo_name,
293 294 target_node_getter=codeblocks.diffset_node_getter(target_commit),
294 295 )
295 296 c.diffset = self.path_filter.render_patchset_filtered(
296 297 diffset, _parsed, source_ref, target_ref)
297 298
298 299 c.preview_mode = merge
299 300 c.source_commit = source_commit
300 301 c.target_commit = target_commit
301 302
302 303 html = render(
303 304 'rhodecode:templates/compare/compare_diff.mako',
304 305 self._get_template_context(c), self.request)
305 306 return Response(html) No newline at end of file
@@ -1,254 +1,254 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 36 import rhodecode.lib.helpers as h
37 from rhodecode.lib.str_utils import safe_str
37 38 from rhodecode.lib.celerylib.utils import get_task_id
38 39 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 40 from rhodecode.model.permission import PermissionModel
40 41 from rhodecode.model.repo import RepoModel
41 42 from rhodecode.model.forms import RepoForkForm
42 43 from rhodecode.model.scm import ScmModel, RepoGroupList
43 from rhodecode.lib.utils2 import safe_int, safe_unicode
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoForksView(RepoAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context(include_app_defaults=True)
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53
54 54 acl_groups = RepoGroupList(
55 55 RepoGroup.query().all(),
56 56 perm_set=['group.write', 'group.admin'])
57 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 60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61 61
62 62 return c
63 63
64 64 @LoginRequired()
65 65 @HasRepoPermissionAnyDecorator(
66 66 'repository.read', 'repository.write', 'repository.admin')
67 67 def repo_forks_show_all(self):
68 68 c = self.load_default_context()
69 69 return self._get_template_context(c)
70 70
71 71 @LoginRequired()
72 72 @HasRepoPermissionAnyDecorator(
73 73 'repository.read', 'repository.write', 'repository.admin')
74 74 def repo_forks_data(self):
75 75 _ = self.request.translate
76 76 self.load_default_context()
77 77 column_map = {
78 78 'fork_name': 'repo_name',
79 79 'fork_date': 'created_on',
80 80 'last_activity': 'updated_on'
81 81 }
82 82 draw, start, limit = self._extract_chunk(self.request)
83 83 search_q, order_by, order_dir = self._extract_ordering(
84 84 self.request, column_map=column_map)
85 85
86 86 acl_check = HasRepoPermissionAny(
87 87 'repository.read', 'repository.write', 'repository.admin')
88 88 repo_id = self.db_repo.repo_id
89 89 allowed_ids = [-1]
90 90 for f in Repository.query().filter(Repository.fork_id == repo_id):
91 91 if acl_check(f.repo_name, 'get forks check'):
92 92 allowed_ids.append(f.repo_id)
93 93
94 94 forks_data_total_count = Repository.query()\
95 95 .filter(Repository.fork_id == repo_id)\
96 96 .filter(Repository.repo_id.in_(allowed_ids))\
97 97 .count()
98 98
99 99 # json generate
100 100 base_q = Repository.query()\
101 101 .filter(Repository.fork_id == repo_id)\
102 102 .filter(Repository.repo_id.in_(allowed_ids))\
103 103
104 104 if search_q:
105 like_expression = u'%{}%'.format(safe_unicode(search_q))
105 like_expression = u'%{}%'.format(safe_str(search_q))
106 106 base_q = base_q.filter(or_(
107 107 Repository.repo_name.ilike(like_expression),
108 108 Repository.description.ilike(like_expression),
109 109 ))
110 110
111 111 forks_data_total_filtered_count = base_q.count()
112 112
113 113 sort_col = getattr(Repository, order_by, None)
114 114 if sort_col:
115 115 if order_dir == 'asc':
116 116 # handle null values properly to order by NULL last
117 117 if order_by in ['last_activity']:
118 118 sort_col = coalesce(sort_col, datetime.date.max)
119 119 sort_col = sort_col.asc()
120 120 else:
121 121 # handle null values properly to order by NULL last
122 122 if order_by in ['last_activity']:
123 123 sort_col = coalesce(sort_col, datetime.date.min)
124 124 sort_col = sort_col.desc()
125 125
126 126 base_q = base_q.order_by(sort_col)
127 127 base_q = base_q.offset(start).limit(limit)
128 128
129 129 fork_list = base_q.all()
130 130
131 131 def fork_actions(fork):
132 132 url_link = h.route_path(
133 133 'repo_compare',
134 134 repo_name=fork.repo_name,
135 135 source_ref_type=self.db_repo.landing_ref_type,
136 136 source_ref=self.db_repo.landing_ref_name,
137 137 target_ref_type=self.db_repo.landing_ref_type,
138 138 target_ref=self.db_repo.landing_ref_name,
139 139 _query=dict(merge=1, target_repo=f.repo_name))
140 140 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
141 141
142 142 def fork_name(fork):
143 143 return h.link_to(fork.repo_name,
144 144 h.route_path('repo_summary', repo_name=fork.repo_name))
145 145
146 146 forks_data = []
147 147 for fork in fork_list:
148 148 forks_data.append({
149 149 "username": h.gravatar_with_user(self.request, fork.user.username),
150 150 "fork_name": fork_name(fork),
151 151 "description": fork.description_safe,
152 152 "fork_date": h.age_component(fork.created_on, time_is_local=True),
153 153 "last_activity": h.format_date(fork.updated_on),
154 154 "action": fork_actions(fork),
155 155 })
156 156
157 157 data = ({
158 158 'draw': draw,
159 159 'data': forks_data,
160 160 'recordsTotal': forks_data_total_count,
161 161 'recordsFiltered': forks_data_total_filtered_count,
162 162 })
163 163
164 164 return data
165 165
166 166 @LoginRequired()
167 167 @NotAnonymous()
168 168 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
169 169 @HasRepoPermissionAnyDecorator(
170 170 'repository.read', 'repository.write', 'repository.admin')
171 171 def repo_fork_new(self):
172 172 c = self.load_default_context()
173 173
174 174 defaults = RepoModel()._get_defaults(self.db_repo_name)
175 175 # alter the description to indicate a fork
176 176 defaults['description'] = (
177 177 'fork of repository: %s \n%s' % (
178 178 defaults['repo_name'], defaults['description']))
179 179 # add suffix to fork
180 180 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
181 181
182 182 data = render('rhodecode:templates/forks/fork.mako',
183 183 self._get_template_context(c), self.request)
184 184 html = formencode.htmlfill.render(
185 185 data,
186 186 defaults=defaults,
187 187 encoding="UTF-8",
188 188 force_defaults=False
189 189 )
190 190 return Response(html)
191 191
192 192 @LoginRequired()
193 193 @NotAnonymous()
194 194 @HasPermissionAnyDecorator('hg.admin', PermissionModel.FORKING_ENABLED)
195 195 @HasRepoPermissionAnyDecorator(
196 196 'repository.read', 'repository.write', 'repository.admin')
197 197 @CSRFRequired()
198 198 def repo_fork_create(self):
199 199 _ = self.request.translate
200 200 c = self.load_default_context()
201 201
202 202 _form = RepoForkForm(self.request.translate,
203 203 old_data={'repo_type': self.db_repo.repo_type},
204 204 repo_groups=c.repo_groups_choices)()
205 205 post_data = dict(self.request.POST)
206 206
207 207 # forbid injecting other repo by forging a request
208 208 post_data['fork_parent_id'] = self.db_repo.repo_id
209 209 post_data['landing_rev'] = self.db_repo._landing_revision
210 210
211 211 form_result = {}
212 212 task_id = None
213 213 try:
214 214 form_result = _form.to_python(post_data)
215 215 copy_permissions = form_result.get('copy_permissions')
216 216 # create fork is done sometimes async on celery, db transaction
217 217 # management is handled there.
218 218 task = RepoModel().create_fork(
219 219 form_result, c.rhodecode_user.user_id)
220 220
221 221 task_id = get_task_id(task)
222 222 except formencode.Invalid as errors:
223 223 c.rhodecode_db_repo = self.db_repo
224 224
225 225 data = render('rhodecode:templates/forks/fork.mako',
226 226 self._get_template_context(c), self.request)
227 227 html = formencode.htmlfill.render(
228 228 data,
229 229 defaults=errors.value,
230 230 errors=errors.error_dict or {},
231 231 prefix_error=False,
232 232 encoding="UTF-8",
233 233 force_defaults=False
234 234 )
235 235 return Response(html)
236 236 except Exception:
237 237 log.exception(
238 238 u'Exception while trying to fork the repository %s', self.db_repo_name)
239 239 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
240 240 h.flash(msg, category='error')
241 241 raise HTTPFound(h.route_path('home'))
242 242
243 243 repo_name = form_result.get('repo_name_full', self.db_repo_name)
244 244
245 245 affected_user_ids = [self._rhodecode_user.user_id]
246 246 if copy_permissions:
247 247 # permission flush is done in repo creating
248 248 pass
249 249
250 250 PermissionModel().trigger_permission_flush(affected_user_ids)
251 251
252 252 raise HTTPFound(
253 253 h.route_path('repo_creating', repo_name=repo_name,
254 254 _query=dict(task_id=task_id)))
@@ -1,1877 +1,1877 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib import ext_json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 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 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name,
83 83 search_q=search_q, statuses=statuses,
84 84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name,
87 87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, self._rhodecode_user.user_id,
91 91 search_q=search_q, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, self._rhodecode_user.user_id,
95 95 search_q=search_q, statuses=statuses)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments_count = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr,
110 110 include_drafts=False, count_only=True)
111 111
112 112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 114 if review_statuses and review_statuses[4]:
115 115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 116 my_review_status = statuses[0][1].status
117 117
118 118 data.append({
119 119 'name': _render('pullrequest_name',
120 120 pr.pull_request_id, pr.pull_request_state,
121 121 pr.work_in_progress, pr.target_repo.repo_name,
122 122 short=True),
123 123 'name_raw': pr.pull_request_id,
124 124 'status': _render('pullrequest_status',
125 125 pr.calculated_review_status()),
126 126 'my_status': _render('pullrequest_status',
127 127 my_review_status),
128 128 'title': _render('pullrequest_title', pr.title, pr.description),
129 129 'description': h.escape(pr.description),
130 130 'updated_on': _render('pullrequest_updated_on',
131 131 h.datetime_to_time(pr.updated_on),
132 132 pr.versions_count),
133 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 134 'created_on': _render('pullrequest_updated_on',
135 135 h.datetime_to_time(pr.created_on)),
136 136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 137 'state': pr.pull_request_state,
138 138 'author': _render('pullrequest_author',
139 139 pr.author.full_contact, ),
140 140 'author_raw': pr.author.full_name,
141 141 'comments': _render('pullrequest_comments', comments_count),
142 142 'comments_raw': comments_count,
143 143 'closed': pr.is_closed(),
144 144 })
145 145
146 146 data = ({
147 147 'draw': draw,
148 148 'data': data,
149 149 'recordsTotal': pull_requests_total_count,
150 150 'recordsFiltered': pull_requests_total_count,
151 151 })
152 152 return data
153 153
154 154 @LoginRequired()
155 155 @HasRepoPermissionAnyDecorator(
156 156 'repository.read', 'repository.write', 'repository.admin')
157 157 def pull_request_list(self):
158 158 c = self.load_default_context()
159 159
160 160 req_get = self.request.GET
161 161 c.source = str2bool(req_get.get('source'))
162 162 c.closed = str2bool(req_get.get('closed'))
163 163 c.my = str2bool(req_get.get('my'))
164 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 166
167 167 c.active = 'open'
168 168 if c.my:
169 169 c.active = 'my'
170 170 if c.closed:
171 171 c.active = 'closed'
172 172 if c.awaiting_review and not c.source:
173 173 c.active = 'awaiting'
174 174 if c.source and not c.awaiting_review:
175 175 c.active = 'source'
176 176 if c.awaiting_my_review:
177 177 c.active = 'awaiting_my'
178 178
179 179 return self._get_template_context(c)
180 180
181 181 @LoginRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 ancestor_commit,
221 221 source_ref_id, target_ref_id,
222 222 target_commit, source_commit, diff_limit, file_limit,
223 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 224
225 225 target_commit_final = target_commit
226 226 source_commit_final = source_commit
227 227
228 228 if use_ancestor:
229 229 # we might want to not use it for versions
230 230 target_ref_id = ancestor_commit.raw_id
231 231 target_commit_final = ancestor_commit
232 232
233 233 vcs_diff = PullRequestModel().get_diff(
234 234 source_repo, source_ref_id, target_ref_id,
235 235 hide_whitespace_changes, diff_context)
236 236
237 237 diff_processor = diffs.DiffProcessor(
238 238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 239 file_limit=file_limit, show_full_diff=fulldiff)
240 240
241 241 _parsed = diff_processor.prepare()
242 242
243 243 diffset = codeblocks.DiffSet(
244 244 repo_name=self.db_repo_name,
245 245 source_repo_name=source_repo_name,
246 246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 248 )
249 249 diffset = self.path_filter.render_patchset_filtered(
250 250 diffset, _parsed, target_ref_id, source_ref_id)
251 251
252 252 return diffset
253 253
254 254 def _get_range_diffset(self, source_scm, source_repo,
255 255 commit1, commit2, diff_limit, file_limit,
256 256 fulldiff, hide_whitespace_changes, diff_context):
257 257 vcs_diff = source_scm.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(
263 263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 264 file_limit=file_limit, show_full_diff=fulldiff)
265 265
266 266 _parsed = diff_processor.prepare()
267 267
268 268 diffset = codeblocks.DiffSet(
269 269 repo_name=source_repo.repo_name,
270 270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 272
273 273 diffset = self.path_filter.render_patchset_filtered(
274 274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 275
276 276 return diffset
277 277
278 278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 279 comments_model = CommentsModel()
280 280
281 281 # GENERAL COMMENTS with versions #
282 282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 283 q = q.order_by(ChangesetComment.comment_id.asc())
284 284 if not include_drafts:
285 285 q = q.filter(ChangesetComment.draft == false())
286 286 general_comments = q
287 287
288 288 # pick comments we want to render at current version
289 289 c.comment_versions = comments_model.aggregate_comments(
290 290 general_comments, versions, c.at_version_num)
291 291
292 292 # INLINE COMMENTS with versions #
293 293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 294 q = q.order_by(ChangesetComment.comment_id.asc())
295 295 if not include_drafts:
296 296 q = q.filter(ChangesetComment.draft == false())
297 297 inline_comments = q
298 298
299 299 c.inline_versions = comments_model.aggregate_comments(
300 300 inline_comments, versions, c.at_version_num, inline=True)
301 301
302 302 # Comments inline+general
303 303 if c.at_version:
304 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 305 c.comments = c.comment_versions[c.at_version_num]['display']
306 306 else:
307 307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 308 c.comments = c.comment_versions[c.at_version_num]['until']
309 309
310 310 return general_comments, inline_comments
311 311
312 312 @LoginRequired()
313 313 @HasRepoPermissionAnyDecorator(
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 def pull_request_show(self):
316 316 _ = self.request.translate
317 317 c = self.load_default_context()
318 318
319 319 pull_request = PullRequest.get_or_404(
320 320 self.request.matchdict['pull_request_id'])
321 321 pull_request_id = pull_request.pull_request_id
322 322
323 323 c.state_progressing = pull_request.is_state_changing()
324 324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325 325
326 326 _new_state = {
327 327 'created': PullRequest.STATE_CREATED,
328 328 }.get(self.request.GET.get('force_state'))
329 329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
330 330
331 331 if can_force_state and _new_state:
332 332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
333 333 h.flash(
334 334 _('Pull Request state was force changed to `{}`').format(_new_state),
335 335 category='success')
336 336 Session().commit()
337 337
338 338 raise HTTPFound(h.route_path(
339 339 'pullrequest_show', repo_name=self.db_repo_name,
340 340 pull_request_id=pull_request_id))
341 341
342 342 version = self.request.GET.get('version')
343 343 from_version = self.request.GET.get('from_version') or version
344 344 merge_checks = self.request.GET.get('merge_checks')
345 345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
346 346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
347 347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
348 348
349 349 # fetch global flags of ignore ws or context lines
350 350 diff_context = diffs.get_diff_context(self.request)
351 351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
352 352
353 353 (pull_request_latest,
354 354 pull_request_at_ver,
355 355 pull_request_display_obj,
356 356 at_version) = PullRequestModel().get_pr_version(
357 357 pull_request_id, version=version)
358 358
359 359 pr_closed = pull_request_latest.is_closed()
360 360
361 361 if pr_closed and (version or from_version):
362 362 # not allow to browse versions for closed PR
363 363 raise HTTPFound(h.route_path(
364 364 'pullrequest_show', repo_name=self.db_repo_name,
365 365 pull_request_id=pull_request_id))
366 366
367 367 versions = pull_request_display_obj.versions()
368 368
369 369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
370 370
371 371 # used to store per-commit range diffs
372 372 c.changes = collections.OrderedDict()
373 373
374 374 c.at_version = at_version
375 375 c.at_version_num = (at_version
376 376 if at_version and at_version != PullRequest.LATEST_VER
377 377 else None)
378 378
379 379 c.at_version_index = ChangesetComment.get_index_from_version(
380 380 c.at_version_num, versions)
381 381
382 382 (prev_pull_request_latest,
383 383 prev_pull_request_at_ver,
384 384 prev_pull_request_display_obj,
385 385 prev_at_version) = PullRequestModel().get_pr_version(
386 386 pull_request_id, version=from_version)
387 387
388 388 c.from_version = prev_at_version
389 389 c.from_version_num = (prev_at_version
390 390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
391 391 else None)
392 392 c.from_version_index = ChangesetComment.get_index_from_version(
393 393 c.from_version_num, versions)
394 394
395 395 # define if we're in COMPARE mode or VIEW at version mode
396 396 compare = at_version != prev_at_version
397 397
398 398 # pull_requests repo_name we opened it against
399 399 # ie. target_repo must match
400 400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
401 401 log.warning('Mismatch between the current repo: %s, and target %s',
402 402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
403 403 raise HTTPNotFound()
404 404
405 405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
406 406
407 407 c.pull_request = pull_request_display_obj
408 408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
409 409 c.pull_request_latest = pull_request_latest
410 410
411 411 # inject latest version
412 412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
413 413 c.versions = versions + [latest_ver]
414 414
415 415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
416 416 c.allowed_to_change_status = False
417 417 c.allowed_to_update = False
418 418 c.allowed_to_merge = False
419 419 c.allowed_to_delete = False
420 420 c.allowed_to_comment = False
421 421 c.allowed_to_close = False
422 422 else:
423 423 can_change_status = PullRequestModel().check_user_change_status(
424 424 pull_request_at_ver, self._rhodecode_user)
425 425 c.allowed_to_change_status = can_change_status and not pr_closed
426 426
427 427 c.allowed_to_update = PullRequestModel().check_user_update(
428 428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 429 c.allowed_to_merge = PullRequestModel().check_user_merge(
430 430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 431 c.allowed_to_delete = PullRequestModel().check_user_delete(
432 432 pull_request_latest, self._rhodecode_user) and not pr_closed
433 433 c.allowed_to_comment = not pr_closed
434 434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
435 435
436 436 c.forbid_adding_reviewers = False
437 437
438 438 if pull_request_latest.reviewer_data and \
439 439 'rules' in pull_request_latest.reviewer_data:
440 440 rules = pull_request_latest.reviewer_data['rules'] or {}
441 441 try:
442 442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
443 443 except Exception:
444 444 pass
445 445
446 446 # check merge capabilities
447 447 _merge_check = MergeCheck.validate(
448 448 pull_request_latest, auth_user=self._rhodecode_user,
449 449 translator=self.request.translate,
450 450 force_shadow_repo_refresh=force_refresh)
451 451
452 452 c.pr_merge_errors = _merge_check.error_details
453 453 c.pr_merge_possible = not _merge_check.failed
454 454 c.pr_merge_message = _merge_check.merge_msg
455 455 c.pr_merge_source_commit = _merge_check.source_commit
456 456 c.pr_merge_target_commit = _merge_check.target_commit
457 457
458 458 c.pr_merge_info = MergeCheck.get_merge_conditions(
459 459 pull_request_latest, translator=self.request.translate)
460 460
461 461 c.pull_request_review_status = _merge_check.review_status
462 462 if merge_checks:
463 463 self.request.override_renderer = \
464 464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
465 465 return self._get_template_context(c)
466 466
467 467 c.reviewers_count = pull_request.reviewers_count
468 468 c.observers_count = pull_request.observers_count
469 469
470 470 # reviewers and statuses
471 471 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
472 472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
473 473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
474 474
475 475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
476 476 member_reviewer = h.reviewer_as_json(
477 477 member, reasons=reasons, mandatory=mandatory,
478 478 role=review_obj.role,
479 479 user_group=review_obj.rule_user_group_data()
480 480 )
481 481
482 482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
483 483 member_reviewer['review_status'] = current_review_status
484 484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
485 485 member_reviewer['allowed_to_update'] = c.allowed_to_update
486 486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
487 487
488 488 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
489 489
490 490 for observer_obj, member in pull_request_at_ver.observers():
491 491 member_observer = h.reviewer_as_json(
492 492 member, reasons=[], mandatory=False,
493 493 role=observer_obj.role,
494 494 user_group=observer_obj.rule_user_group_data()
495 495 )
496 496 member_observer['allowed_to_update'] = c.allowed_to_update
497 497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
498 498
499 499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
500 500
501 501 general_comments, inline_comments = \
502 502 self.register_comments_vars(c, pull_request_latest, versions)
503 503
504 504 # TODOs
505 505 c.unresolved_comments = CommentsModel() \
506 506 .get_pull_request_unresolved_todos(pull_request_latest)
507 507 c.resolved_comments = CommentsModel() \
508 508 .get_pull_request_resolved_todos(pull_request_latest)
509 509
510 510 # Drafts
511 511 c.draft_comments = CommentsModel().get_pull_request_drafts(
512 512 self._rhodecode_db_user.user_id,
513 513 pull_request_latest)
514 514
515 515 # if we use version, then do not show later comments
516 516 # than current version
517 517 display_inline_comments = collections.defaultdict(
518 518 lambda: collections.defaultdict(list))
519 519 for co in inline_comments:
520 520 if c.at_version_num:
521 521 # pick comments that are at least UPTO given version, so we
522 522 # don't render comments for higher version
523 523 should_render = co.pull_request_version_id and \
524 524 co.pull_request_version_id <= c.at_version_num
525 525 else:
526 526 # showing all, for 'latest'
527 527 should_render = True
528 528
529 529 if should_render:
530 530 display_inline_comments[co.f_path][co.line_no].append(co)
531 531
532 532 # load diff data into template context, if we use compare mode then
533 533 # diff is calculated based on changes between versions of PR
534 534
535 535 source_repo = pull_request_at_ver.source_repo
536 536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
537 537
538 538 target_repo = pull_request_at_ver.target_repo
539 539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
540 540
541 541 if compare:
542 542 # in compare switch the diff base to latest commit from prev version
543 543 target_ref_id = prev_pull_request_display_obj.revisions[0]
544 544
545 545 # despite opening commits for bookmarks/branches/tags, we always
546 546 # convert this to rev to prevent changes after bookmark or branch change
547 547 c.source_ref_type = 'rev'
548 548 c.source_ref = source_ref_id
549 549
550 550 c.target_ref_type = 'rev'
551 551 c.target_ref = target_ref_id
552 552
553 553 c.source_repo = source_repo
554 554 c.target_repo = target_repo
555 555
556 556 c.commit_ranges = []
557 557 source_commit = EmptyCommit()
558 558 target_commit = EmptyCommit()
559 559 c.missing_requirements = False
560 560
561 561 source_scm = source_repo.scm_instance()
562 562 target_scm = target_repo.scm_instance()
563 563
564 564 shadow_scm = None
565 565 try:
566 566 shadow_scm = pull_request_latest.get_shadow_repo()
567 567 except Exception:
568 568 log.debug('Failed to get shadow repo', exc_info=True)
569 569 # try first the existing source_repo, and then shadow
570 570 # repo if we can obtain one
571 571 commits_source_repo = source_scm
572 572 if shadow_scm:
573 573 commits_source_repo = shadow_scm
574 574
575 575 c.commits_source_repo = commits_source_repo
576 576 c.ancestor = None # set it to None, to hide it from PR view
577 577
578 578 # empty version means latest, so we keep this to prevent
579 579 # double caching
580 580 version_normalized = version or PullRequest.LATEST_VER
581 581 from_version_normalized = from_version or PullRequest.LATEST_VER
582 582
583 583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
584 584 cache_file_path = diff_cache_exist(
585 585 cache_path, 'pull_request', pull_request_id, version_normalized,
586 586 from_version_normalized, source_ref_id, target_ref_id,
587 587 hide_whitespace_changes, diff_context, c.fulldiff)
588 588
589 589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
590 590 force_recache = self.get_recache_flag()
591 591
592 592 cached_diff = None
593 593 if caching_enabled:
594 594 cached_diff = load_cached_diff(cache_file_path)
595 595
596 596 has_proper_commit_cache = (
597 597 cached_diff and cached_diff.get('commits')
598 598 and len(cached_diff.get('commits', [])) == 5
599 599 and cached_diff.get('commits')[0]
600 600 and cached_diff.get('commits')[3])
601 601
602 602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
603 603 diff_commit_cache = \
604 604 (ancestor_commit, commit_cache, missing_requirements,
605 605 source_commit, target_commit) = cached_diff['commits']
606 606 else:
607 607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
608 608 # merge errors resulting in potentially hidden commits in the shadow repo.
609 609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
610 610 and _merge_check.merge_response
611 611 maybe_unreachable = maybe_unreachable \
612 612 and _merge_check.merge_response.metadata.get('unresolved_files')
613 613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
614 614 diff_commit_cache = \
615 615 (ancestor_commit, commit_cache, missing_requirements,
616 616 source_commit, target_commit) = self.get_commits(
617 617 commits_source_repo,
618 618 pull_request_at_ver,
619 619 source_commit,
620 620 source_ref_id,
621 621 source_scm,
622 622 target_commit,
623 623 target_ref_id,
624 624 target_scm,
625 625 maybe_unreachable=maybe_unreachable)
626 626
627 627 # register our commit range
628 628 for comm in commit_cache.values():
629 629 c.commit_ranges.append(comm)
630 630
631 631 c.missing_requirements = missing_requirements
632 632 c.ancestor_commit = ancestor_commit
633 633 c.statuses = source_repo.statuses(
634 634 [x.raw_id for x in c.commit_ranges])
635 635
636 636 # auto collapse if we have more than limit
637 637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
638 638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
639 639 c.compare_mode = compare
640 640
641 641 # diff_limit is the old behavior, will cut off the whole diff
642 642 # if the limit is applied otherwise will just hide the
643 643 # big files from the front-end
644 644 diff_limit = c.visual.cut_off_limit_diff
645 645 file_limit = c.visual.cut_off_limit_file
646 646
647 647 c.missing_commits = False
648 648 if (c.missing_requirements
649 649 or isinstance(source_commit, EmptyCommit)
650 650 or source_commit == target_commit):
651 651
652 652 c.missing_commits = True
653 653 else:
654 654 c.inline_comments = display_inline_comments
655 655
656 656 use_ancestor = True
657 657 if from_version_normalized != version_normalized:
658 658 use_ancestor = False
659 659
660 660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
661 661 if not force_recache and has_proper_diff_cache:
662 662 c.diffset = cached_diff['diff']
663 663 else:
664 664 try:
665 665 c.diffset = self._get_diffset(
666 666 c.source_repo.repo_name, commits_source_repo,
667 667 c.ancestor_commit,
668 668 source_ref_id, target_ref_id,
669 669 target_commit, source_commit,
670 670 diff_limit, file_limit, c.fulldiff,
671 671 hide_whitespace_changes, diff_context,
672 672 use_ancestor=use_ancestor
673 673 )
674 674
675 675 # save cached diff
676 676 if caching_enabled:
677 677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
678 678 except CommitDoesNotExistError:
679 679 log.exception('Failed to generate diffset')
680 680 c.missing_commits = True
681 681
682 682 if not c.missing_commits:
683 683
684 684 c.limited_diff = c.diffset.limited_diff
685 685
686 686 # calculate removed files that are bound to comments
687 687 comment_deleted_files = [
688 688 fname for fname in display_inline_comments
689 689 if fname not in c.diffset.file_stats]
690 690
691 691 c.deleted_files_comments = collections.defaultdict(dict)
692 692 for fname, per_line_comments in display_inline_comments.items():
693 693 if fname in comment_deleted_files:
694 694 c.deleted_files_comments[fname]['stats'] = 0
695 695 c.deleted_files_comments[fname]['comments'] = list()
696 696 for lno, comments in per_line_comments.items():
697 697 c.deleted_files_comments[fname]['comments'].extend(comments)
698 698
699 699 # maybe calculate the range diff
700 700 if c.range_diff_on:
701 701 # TODO(marcink): set whitespace/context
702 702 context_lcl = 3
703 703 ign_whitespace_lcl = False
704 704
705 705 for commit in c.commit_ranges:
706 706 commit2 = commit
707 707 commit1 = commit.first_parent
708 708
709 709 range_diff_cache_file_path = diff_cache_exist(
710 710 cache_path, 'diff', commit.raw_id,
711 711 ign_whitespace_lcl, context_lcl, c.fulldiff)
712 712
713 713 cached_diff = None
714 714 if caching_enabled:
715 715 cached_diff = load_cached_diff(range_diff_cache_file_path)
716 716
717 717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
718 718 if not force_recache and has_proper_diff_cache:
719 719 diffset = cached_diff['diff']
720 720 else:
721 721 diffset = self._get_range_diffset(
722 722 commits_source_repo, source_repo,
723 723 commit1, commit2, diff_limit, file_limit,
724 724 c.fulldiff, ign_whitespace_lcl, context_lcl
725 725 )
726 726
727 727 # save cached diff
728 728 if caching_enabled:
729 729 cache_diff(range_diff_cache_file_path, diffset, None)
730 730
731 731 c.changes[commit.raw_id] = diffset
732 732
733 733 # this is a hack to properly display links, when creating PR, the
734 734 # compare view and others uses different notation, and
735 735 # compare_commits.mako renders links based on the target_repo.
736 736 # We need to swap that here to generate it properly on the html side
737 737 c.target_repo = c.source_repo
738 738
739 739 c.commit_statuses = ChangesetStatus.STATUSES
740 740
741 741 c.show_version_changes = not pr_closed
742 742 if c.show_version_changes:
743 743 cur_obj = pull_request_at_ver
744 744 prev_obj = prev_pull_request_at_ver
745 745
746 746 old_commit_ids = prev_obj.revisions
747 747 new_commit_ids = cur_obj.revisions
748 748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
749 749 old_commit_ids, new_commit_ids)
750 750 c.commit_changes_summary = commit_changes
751 751
752 752 # calculate the diff for commits between versions
753 753 c.commit_changes = []
754 754
755 755 def mark(cs, fw):
756 756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
757 757
758 758 for c_type, raw_id in mark(commit_changes.added, 'a') \
759 759 + mark(commit_changes.removed, 'r') \
760 760 + mark(commit_changes.common, 'c'):
761 761
762 762 if raw_id in commit_cache:
763 763 commit = commit_cache[raw_id]
764 764 else:
765 765 try:
766 766 commit = commits_source_repo.get_commit(raw_id)
767 767 except CommitDoesNotExistError:
768 768 # in case we fail extracting still use "dummy" commit
769 769 # for display in commit diff
770 770 commit = h.AttributeDict(
771 771 {'raw_id': raw_id,
772 772 'message': 'EMPTY or MISSING COMMIT'})
773 773 c.commit_changes.append([c_type, commit])
774 774
775 775 # current user review statuses for each version
776 776 c.review_versions = {}
777 777 is_reviewer = PullRequestModel().is_user_reviewer(
778 778 pull_request, self._rhodecode_user)
779 779 if is_reviewer:
780 780 for co in general_comments:
781 781 if co.author.user_id == self._rhodecode_user.user_id:
782 782 status = co.status_change
783 783 if status:
784 784 _ver_pr = status[0].comment.pull_request_version_id
785 785 c.review_versions[_ver_pr] = status[0]
786 786
787 787 return self._get_template_context(c)
788 788
789 789 def get_commits(
790 790 self, commits_source_repo, pull_request_at_ver, source_commit,
791 791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
792 792 maybe_unreachable=False):
793 793
794 794 commit_cache = collections.OrderedDict()
795 795 missing_requirements = False
796 796
797 797 try:
798 798 pre_load = ["author", "date", "message", "branch", "parents"]
799 799
800 800 pull_request_commits = pull_request_at_ver.revisions
801 801 log.debug('Loading %s commits from %s',
802 802 len(pull_request_commits), commits_source_repo)
803 803
804 804 for rev in pull_request_commits:
805 805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
806 806 maybe_unreachable=maybe_unreachable)
807 807 commit_cache[comm.raw_id] = comm
808 808
809 809 # Order here matters, we first need to get target, and then
810 810 # the source
811 811 target_commit = commits_source_repo.get_commit(
812 812 commit_id=safe_str(target_ref_id))
813 813
814 814 source_commit = commits_source_repo.get_commit(
815 815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
816 816 except CommitDoesNotExistError:
817 817 log.warning('Failed to get commit from `{}` repo'.format(
818 818 commits_source_repo), exc_info=True)
819 819 except RepositoryRequirementError:
820 820 log.warning('Failed to get all required data from repo', exc_info=True)
821 821 missing_requirements = True
822 822
823 823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
824 824
825 825 try:
826 826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
827 827 except Exception:
828 828 ancestor_commit = None
829 829
830 830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
831 831
832 832 def assure_not_empty_repo(self):
833 833 _ = self.request.translate
834 834
835 835 try:
836 836 self.db_repo.scm_instance().get_commit()
837 837 except EmptyRepositoryError:
838 838 h.flash(h.literal(_('There are no commits yet')),
839 839 category='warning')
840 840 raise HTTPFound(
841 841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
842 842
843 843 @LoginRequired()
844 844 @NotAnonymous()
845 845 @HasRepoPermissionAnyDecorator(
846 846 'repository.read', 'repository.write', 'repository.admin')
847 847 def pull_request_new(self):
848 848 _ = self.request.translate
849 849 c = self.load_default_context()
850 850
851 851 self.assure_not_empty_repo()
852 852 source_repo = self.db_repo
853 853
854 854 commit_id = self.request.GET.get('commit')
855 855 branch_ref = self.request.GET.get('branch')
856 856 bookmark_ref = self.request.GET.get('bookmark')
857 857
858 858 try:
859 859 source_repo_data = PullRequestModel().generate_repo_data(
860 860 source_repo, commit_id=commit_id,
861 861 branch=branch_ref, bookmark=bookmark_ref,
862 862 translator=self.request.translate)
863 863 except CommitDoesNotExistError as e:
864 864 log.exception(e)
865 865 h.flash(_('Commit does not exist'), 'error')
866 866 raise HTTPFound(
867 867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
868 868
869 869 default_target_repo = source_repo
870 870
871 871 if source_repo.parent and c.has_origin_repo_read_perm:
872 872 parent_vcs_obj = source_repo.parent.scm_instance()
873 873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
874 874 # change default if we have a parent repo
875 875 default_target_repo = source_repo.parent
876 876
877 877 target_repo_data = PullRequestModel().generate_repo_data(
878 878 default_target_repo, translator=self.request.translate)
879 879
880 880 selected_source_ref = source_repo_data['refs']['selected_ref']
881 881 title_source_ref = ''
882 882 if selected_source_ref:
883 883 title_source_ref = selected_source_ref.split(':', 2)[1]
884 884 c.default_title = PullRequestModel().generate_pullrequest_title(
885 885 source=source_repo.repo_name,
886 886 source_ref=title_source_ref,
887 887 target=default_target_repo.repo_name
888 888 )
889 889
890 890 c.default_repo_data = {
891 891 'source_repo_name': source_repo.repo_name,
892 892 'source_refs_json': ext_json.str_json(source_repo_data),
893 893 'target_repo_name': default_target_repo.repo_name,
894 894 'target_refs_json': ext_json.str_json(target_repo_data),
895 895 }
896 896 c.default_source_ref = selected_source_ref
897 897
898 898 return self._get_template_context(c)
899 899
900 900 @LoginRequired()
901 901 @NotAnonymous()
902 902 @HasRepoPermissionAnyDecorator(
903 903 'repository.read', 'repository.write', 'repository.admin')
904 904 def pull_request_repo_refs(self):
905 905 self.load_default_context()
906 906 target_repo_name = self.request.matchdict['target_repo_name']
907 907 repo = Repository.get_by_repo_name(target_repo_name)
908 908 if not repo:
909 909 raise HTTPNotFound()
910 910
911 911 target_perm = HasRepoPermissionAny(
912 912 'repository.read', 'repository.write', 'repository.admin')(
913 913 target_repo_name)
914 914 if not target_perm:
915 915 raise HTTPNotFound()
916 916
917 917 return PullRequestModel().generate_repo_data(
918 918 repo, translator=self.request.translate)
919 919
920 920 @LoginRequired()
921 921 @NotAnonymous()
922 922 @HasRepoPermissionAnyDecorator(
923 923 'repository.read', 'repository.write', 'repository.admin')
924 924 def pullrequest_repo_targets(self):
925 925 _ = self.request.translate
926 926 filter_query = self.request.GET.get('query')
927 927
928 928 # get the parents
929 929 parent_target_repos = []
930 930 if self.db_repo.parent:
931 931 parents_query = Repository.query() \
932 932 .order_by(func.length(Repository.repo_name)) \
933 933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934 934
935 935 if filter_query:
936 936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 937 parents_query = parents_query.filter(
938 938 Repository.repo_name.ilike(ilike_expression))
939 939 parents = parents_query.limit(20).all()
940 940
941 941 for parent in parents:
942 942 parent_vcs_obj = parent.scm_instance()
943 943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 944 parent_target_repos.append(parent)
945 945
946 946 # get other forks, and repo itself
947 947 query = Repository.query() \
948 948 .order_by(func.length(Repository.repo_name)) \
949 949 .filter(
950 950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 952 ) \
953 953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954 954
955 955 if filter_query:
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 ilike_expression = u'%{}%'.format(safe_str(filter_query))
957 957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958 958
959 959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 960 target_repos = query.limit(limit).all()
961 961
962 962 all_target_repos = target_repos + parent_target_repos
963 963
964 964 repos = []
965 965 # This checks permissions to the repositories
966 966 for obj in ScmModel().get_repos(all_target_repos):
967 967 repos.append({
968 968 'id': obj['name'],
969 969 'text': obj['name'],
970 970 'type': 'repo',
971 971 'repo_id': obj['dbrepo']['repo_id'],
972 972 'repo_type': obj['dbrepo']['repo_type'],
973 973 'private': obj['dbrepo']['private'],
974 974
975 975 })
976 976
977 977 data = {
978 978 'more': False,
979 979 'results': [{
980 980 'text': _('Repositories'),
981 981 'children': repos
982 982 }] if repos else []
983 983 }
984 984 return data
985 985
986 986 @classmethod
987 987 def get_comment_ids(cls, post_data):
988 988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989 989
990 990 @LoginRequired()
991 991 @NotAnonymous()
992 992 @HasRepoPermissionAnyDecorator(
993 993 'repository.read', 'repository.write', 'repository.admin')
994 994 def pullrequest_comments(self):
995 995 self.load_default_context()
996 996
997 997 pull_request = PullRequest.get_or_404(
998 998 self.request.matchdict['pull_request_id'])
999 999 pull_request_id = pull_request.pull_request_id
1000 1000 version = self.request.GET.get('version')
1001 1001
1002 1002 _render = self.request.get_partial_renderer(
1003 1003 'rhodecode:templates/base/sidebar.mako')
1004 1004 c = _render.get_call_context()
1005 1005
1006 1006 (pull_request_latest,
1007 1007 pull_request_at_ver,
1008 1008 pull_request_display_obj,
1009 1009 at_version) = PullRequestModel().get_pr_version(
1010 1010 pull_request_id, version=version)
1011 1011 versions = pull_request_display_obj.versions()
1012 1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1013 1013 c.versions = versions + [latest_ver]
1014 1014
1015 1015 c.at_version = at_version
1016 1016 c.at_version_num = (at_version
1017 1017 if at_version and at_version != PullRequest.LATEST_VER
1018 1018 else None)
1019 1019
1020 1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1021 1021 all_comments = c.inline_comments_flat + c.comments
1022 1022
1023 1023 existing_ids = self.get_comment_ids(self.request.POST)
1024 1024 return _render('comments_table', all_comments, len(all_comments),
1025 1025 existing_ids=existing_ids)
1026 1026
1027 1027 @LoginRequired()
1028 1028 @NotAnonymous()
1029 1029 @HasRepoPermissionAnyDecorator(
1030 1030 'repository.read', 'repository.write', 'repository.admin')
1031 1031 def pullrequest_todos(self):
1032 1032 self.load_default_context()
1033 1033
1034 1034 pull_request = PullRequest.get_or_404(
1035 1035 self.request.matchdict['pull_request_id'])
1036 1036 pull_request_id = pull_request.pull_request_id
1037 1037 version = self.request.GET.get('version')
1038 1038
1039 1039 _render = self.request.get_partial_renderer(
1040 1040 'rhodecode:templates/base/sidebar.mako')
1041 1041 c = _render.get_call_context()
1042 1042 (pull_request_latest,
1043 1043 pull_request_at_ver,
1044 1044 pull_request_display_obj,
1045 1045 at_version) = PullRequestModel().get_pr_version(
1046 1046 pull_request_id, version=version)
1047 1047 versions = pull_request_display_obj.versions()
1048 1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 1049 c.versions = versions + [latest_ver]
1050 1050
1051 1051 c.at_version = at_version
1052 1052 c.at_version_num = (at_version
1053 1053 if at_version and at_version != PullRequest.LATEST_VER
1054 1054 else None)
1055 1055
1056 1056 c.unresolved_comments = CommentsModel() \
1057 1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1058 1058 c.resolved_comments = CommentsModel() \
1059 1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1060 1060
1061 1061 all_comments = c.unresolved_comments + c.resolved_comments
1062 1062 existing_ids = self.get_comment_ids(self.request.POST)
1063 1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 1064 todo_comments=True, existing_ids=existing_ids)
1065 1065
1066 1066 @LoginRequired()
1067 1067 @NotAnonymous()
1068 1068 @HasRepoPermissionAnyDecorator(
1069 1069 'repository.read', 'repository.write', 'repository.admin')
1070 1070 def pullrequest_drafts(self):
1071 1071 self.load_default_context()
1072 1072
1073 1073 pull_request = PullRequest.get_or_404(
1074 1074 self.request.matchdict['pull_request_id'])
1075 1075 pull_request_id = pull_request.pull_request_id
1076 1076 version = self.request.GET.get('version')
1077 1077
1078 1078 _render = self.request.get_partial_renderer(
1079 1079 'rhodecode:templates/base/sidebar.mako')
1080 1080 c = _render.get_call_context()
1081 1081
1082 1082 (pull_request_latest,
1083 1083 pull_request_at_ver,
1084 1084 pull_request_display_obj,
1085 1085 at_version) = PullRequestModel().get_pr_version(
1086 1086 pull_request_id, version=version)
1087 1087 versions = pull_request_display_obj.versions()
1088 1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1089 1089 c.versions = versions + [latest_ver]
1090 1090
1091 1091 c.at_version = at_version
1092 1092 c.at_version_num = (at_version
1093 1093 if at_version and at_version != PullRequest.LATEST_VER
1094 1094 else None)
1095 1095
1096 1096 c.draft_comments = CommentsModel() \
1097 1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1098 1098
1099 1099 all_comments = c.draft_comments
1100 1100
1101 1101 existing_ids = self.get_comment_ids(self.request.POST)
1102 1102 return _render('comments_table', all_comments, len(all_comments),
1103 1103 existing_ids=existing_ids, draft_comments=True)
1104 1104
1105 1105 @LoginRequired()
1106 1106 @NotAnonymous()
1107 1107 @HasRepoPermissionAnyDecorator(
1108 1108 'repository.read', 'repository.write', 'repository.admin')
1109 1109 @CSRFRequired()
1110 1110 def pull_request_create(self):
1111 1111 _ = self.request.translate
1112 1112 self.assure_not_empty_repo()
1113 1113 self.load_default_context()
1114 1114
1115 1115 controls = peppercorn.parse(self.request.POST.items())
1116 1116
1117 1117 try:
1118 1118 form = PullRequestForm(
1119 1119 self.request.translate, self.db_repo.repo_id)()
1120 1120 _form = form.to_python(controls)
1121 1121 except formencode.Invalid as errors:
1122 1122 if errors.error_dict.get('revisions'):
1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1124 1124 elif errors.error_dict.get('pullrequest_title'):
1125 1125 msg = errors.error_dict.get('pullrequest_title')
1126 1126 else:
1127 1127 msg = _('Error creating pull request: {}').format(errors)
1128 1128 log.exception(msg)
1129 1129 h.flash(msg, 'error')
1130 1130
1131 1131 # would rather just go back to form ...
1132 1132 raise HTTPFound(
1133 1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1134 1134
1135 1135 source_repo = _form['source_repo']
1136 1136 source_ref = _form['source_ref']
1137 1137 target_repo = _form['target_repo']
1138 1138 target_ref = _form['target_ref']
1139 1139 commit_ids = _form['revisions'][::-1]
1140 1140 common_ancestor_id = _form['common_ancestor']
1141 1141
1142 1142 # find the ancestor for this pr
1143 1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1144 1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1145 1145
1146 1146 if not (source_db_repo or target_db_repo):
1147 1147 h.flash(_('source_repo or target repo not found'), category='error')
1148 1148 raise HTTPFound(
1149 1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1150 1150
1151 1151 # re-check permissions again here
1152 1152 # source_repo we must have read permissions
1153 1153
1154 1154 source_perm = HasRepoPermissionAny(
1155 1155 'repository.read', 'repository.write', 'repository.admin')(
1156 1156 source_db_repo.repo_name)
1157 1157 if not source_perm:
1158 1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1159 1159 source_db_repo.repo_name))
1160 1160 h.flash(msg, category='error')
1161 1161 # copy the args back to redirect
1162 1162 org_query = self.request.GET.mixed()
1163 1163 raise HTTPFound(
1164 1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1165 1165 _query=org_query))
1166 1166
1167 1167 # target repo we must have read permissions, and also later on
1168 1168 # we want to check branch permissions here
1169 1169 target_perm = HasRepoPermissionAny(
1170 1170 'repository.read', 'repository.write', 'repository.admin')(
1171 1171 target_db_repo.repo_name)
1172 1172 if not target_perm:
1173 1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1174 1174 target_db_repo.repo_name))
1175 1175 h.flash(msg, category='error')
1176 1176 # copy the args back to redirect
1177 1177 org_query = self.request.GET.mixed()
1178 1178 raise HTTPFound(
1179 1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1180 1180 _query=org_query))
1181 1181
1182 1182 source_scm = source_db_repo.scm_instance()
1183 1183 target_scm = target_db_repo.scm_instance()
1184 1184
1185 1185 source_ref_obj = unicode_to_reference(source_ref)
1186 1186 target_ref_obj = unicode_to_reference(target_ref)
1187 1187
1188 1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1189 1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1190 1190
1191 1191 ancestor = source_scm.get_common_ancestor(
1192 1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1193 1193
1194 1194 # recalculate target ref based on ancestor
1195 1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1196 1196
1197 1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1198 1198 PullRequestModel().get_reviewer_functions()
1199 1199
1200 1200 # recalculate reviewers logic, to make sure we can validate this
1201 1201 reviewer_rules = get_default_reviewers_data(
1202 1202 self._rhodecode_db_user,
1203 1203 source_db_repo,
1204 1204 source_ref_obj,
1205 1205 target_db_repo,
1206 1206 target_ref_obj,
1207 1207 include_diff_info=False)
1208 1208
1209 1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1210 1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1211 1211
1212 1212 pullrequest_title = _form['pullrequest_title']
1213 1213 title_source_ref = source_ref_obj.name
1214 1214 if not pullrequest_title:
1215 1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1216 1216 source=source_repo,
1217 1217 source_ref=title_source_ref,
1218 1218 target=target_repo
1219 1219 )
1220 1220
1221 1221 description = _form['pullrequest_desc']
1222 1222 description_renderer = _form['description_renderer']
1223 1223
1224 1224 try:
1225 1225 pull_request = PullRequestModel().create(
1226 1226 created_by=self._rhodecode_user.user_id,
1227 1227 source_repo=source_repo,
1228 1228 source_ref=source_ref,
1229 1229 target_repo=target_repo,
1230 1230 target_ref=target_ref,
1231 1231 revisions=commit_ids,
1232 1232 common_ancestor_id=common_ancestor_id,
1233 1233 reviewers=reviewers,
1234 1234 observers=observers,
1235 1235 title=pullrequest_title,
1236 1236 description=description,
1237 1237 description_renderer=description_renderer,
1238 1238 reviewer_data=reviewer_rules,
1239 1239 auth_user=self._rhodecode_user
1240 1240 )
1241 1241 Session().commit()
1242 1242
1243 1243 h.flash(_('Successfully opened new pull request'),
1244 1244 category='success')
1245 1245 except Exception:
1246 1246 msg = _('Error occurred during creation of this pull request.')
1247 1247 log.exception(msg)
1248 1248 h.flash(msg, category='error')
1249 1249
1250 1250 # copy the args back to redirect
1251 1251 org_query = self.request.GET.mixed()
1252 1252 raise HTTPFound(
1253 1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1254 1254 _query=org_query))
1255 1255
1256 1256 raise HTTPFound(
1257 1257 h.route_path('pullrequest_show', repo_name=target_repo,
1258 1258 pull_request_id=pull_request.pull_request_id))
1259 1259
1260 1260 @LoginRequired()
1261 1261 @NotAnonymous()
1262 1262 @HasRepoPermissionAnyDecorator(
1263 1263 'repository.read', 'repository.write', 'repository.admin')
1264 1264 @CSRFRequired()
1265 1265 def pull_request_update(self):
1266 1266 pull_request = PullRequest.get_or_404(
1267 1267 self.request.matchdict['pull_request_id'])
1268 1268 _ = self.request.translate
1269 1269
1270 1270 c = self.load_default_context()
1271 1271 redirect_url = None
1272 1272 # we do this check as first, because we want to know ASAP in the flow that
1273 1273 # pr is updating currently
1274 1274 is_state_changing = pull_request.is_state_changing()
1275 1275
1276 1276 if pull_request.is_closed():
1277 1277 log.debug('update: forbidden because pull request is closed')
1278 1278 msg = _(u'Cannot update closed pull requests.')
1279 1279 h.flash(msg, category='error')
1280 1280 return {'response': True,
1281 1281 'redirect_url': redirect_url}
1282 1282
1283 1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1284 1284
1285 1285 # only owner or admin can update it
1286 1286 allowed_to_update = PullRequestModel().check_user_update(
1287 1287 pull_request, self._rhodecode_user)
1288 1288
1289 1289 if allowed_to_update:
1290 1290 controls = peppercorn.parse(self.request.POST.items())
1291 1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1293 1293
1294 1294 if 'review_members' in controls:
1295 1295 self._update_reviewers(
1296 1296 c,
1297 1297 pull_request, controls['review_members'],
1298 1298 pull_request.reviewer_data,
1299 1299 PullRequestReviewers.ROLE_REVIEWER)
1300 1300 elif 'observer_members' in controls:
1301 1301 self._update_reviewers(
1302 1302 c,
1303 1303 pull_request, controls['observer_members'],
1304 1304 pull_request.reviewer_data,
1305 1305 PullRequestReviewers.ROLE_OBSERVER)
1306 1306 elif do_update_commits:
1307 1307 if is_state_changing:
1308 1308 log.debug('commits update: forbidden because pull request is in state %s',
1309 1309 pull_request.pull_request_state)
1310 1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1311 1311 u'Current state is: `{}`').format(
1312 1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1313 1313 h.flash(msg, category='error')
1314 1314 return {'response': True,
1315 1315 'redirect_url': redirect_url}
1316 1316
1317 1317 self._update_commits(c, pull_request)
1318 1318 if force_refresh:
1319 1319 redirect_url = h.route_path(
1320 1320 'pullrequest_show', repo_name=self.db_repo_name,
1321 1321 pull_request_id=pull_request.pull_request_id,
1322 1322 _query={"force_refresh": 1})
1323 1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1324 1324 self._edit_pull_request(pull_request)
1325 1325 else:
1326 1326 log.error('Unhandled update data.')
1327 1327 raise HTTPBadRequest()
1328 1328
1329 1329 return {'response': True,
1330 1330 'redirect_url': redirect_url}
1331 1331 raise HTTPForbidden()
1332 1332
1333 1333 def _edit_pull_request(self, pull_request):
1334 1334 """
1335 1335 Edit title and description
1336 1336 """
1337 1337 _ = self.request.translate
1338 1338
1339 1339 try:
1340 1340 PullRequestModel().edit(
1341 1341 pull_request,
1342 1342 self.request.POST.get('title'),
1343 1343 self.request.POST.get('description'),
1344 1344 self.request.POST.get('description_renderer'),
1345 1345 self._rhodecode_user)
1346 1346 except ValueError:
1347 1347 msg = _(u'Cannot update closed pull requests.')
1348 1348 h.flash(msg, category='error')
1349 1349 return
1350 1350 else:
1351 1351 Session().commit()
1352 1352
1353 1353 msg = _(u'Pull request title & description updated.')
1354 1354 h.flash(msg, category='success')
1355 1355 return
1356 1356
1357 1357 def _update_commits(self, c, pull_request):
1358 1358 _ = self.request.translate
1359 1359 log.debug('pull-request: running update commits actions')
1360 1360
1361 1361 @retry(exception=Exception, n_tries=3, delay=2)
1362 1362 def commits_update():
1363 1363 return PullRequestModel().update_commits(
1364 1364 pull_request, self._rhodecode_db_user)
1365 1365
1366 1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1367 1367 resp = commits_update() # retry x3
1368 1368
1369 1369 if resp.executed:
1370 1370
1371 1371 if resp.target_changed and resp.source_changed:
1372 1372 changed = 'target and source repositories'
1373 1373 elif resp.target_changed and not resp.source_changed:
1374 1374 changed = 'target repository'
1375 1375 elif not resp.target_changed and resp.source_changed:
1376 1376 changed = 'source repository'
1377 1377 else:
1378 1378 changed = 'nothing'
1379 1379
1380 1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1381 1381 u'{count_added} added, {count_removed} removed commits. '
1382 1382 u'Source of changes: {change_source}.')
1383 1383 msg = msg.format(
1384 1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1385 1385 count_added=len(resp.changes.added),
1386 1386 count_removed=len(resp.changes.removed),
1387 1387 change_source=changed)
1388 1388 h.flash(msg, category='success')
1389 1389 channelstream.pr_update_channelstream_push(
1390 1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 1391 else:
1392 1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1393 1393 warning_reasons = [
1394 1394 UpdateFailureReason.NO_CHANGE,
1395 1395 UpdateFailureReason.WRONG_REF_TYPE,
1396 1396 ]
1397 1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1398 1398 h.flash(msg, category=category)
1399 1399
1400 1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1401 1401 _ = self.request.translate
1402 1402
1403 1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1404 1404 PullRequestModel().get_reviewer_functions()
1405 1405
1406 1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1407 1407 try:
1408 1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1409 1409 except ValueError as e:
1410 1410 log.error('Reviewers Validation: {}'.format(e))
1411 1411 h.flash(e, category='error')
1412 1412 return
1413 1413
1414 1414 old_calculated_status = pull_request.calculated_review_status()
1415 1415 PullRequestModel().update_reviewers(
1416 1416 pull_request, reviewers, self._rhodecode_db_user)
1417 1417
1418 1418 Session().commit()
1419 1419
1420 1420 msg = _('Pull request reviewers updated.')
1421 1421 h.flash(msg, category='success')
1422 1422 channelstream.pr_update_channelstream_push(
1423 1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1424 1424
1425 1425 # trigger status changed if change in reviewers changes the status
1426 1426 calculated_status = pull_request.calculated_review_status()
1427 1427 if old_calculated_status != calculated_status:
1428 1428 PullRequestModel().trigger_pull_request_hook(
1429 1429 pull_request, self._rhodecode_user, 'review_status_change',
1430 1430 data={'status': calculated_status})
1431 1431
1432 1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1433 1433 try:
1434 1434 observers = validate_observers(review_members, reviewer_rules)
1435 1435 except ValueError as e:
1436 1436 log.error('Observers Validation: {}'.format(e))
1437 1437 h.flash(e, category='error')
1438 1438 return
1439 1439
1440 1440 PullRequestModel().update_observers(
1441 1441 pull_request, observers, self._rhodecode_db_user)
1442 1442
1443 1443 Session().commit()
1444 1444 msg = _('Pull request observers updated.')
1445 1445 h.flash(msg, category='success')
1446 1446 channelstream.pr_update_channelstream_push(
1447 1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1448 1448
1449 1449 @LoginRequired()
1450 1450 @NotAnonymous()
1451 1451 @HasRepoPermissionAnyDecorator(
1452 1452 'repository.read', 'repository.write', 'repository.admin')
1453 1453 @CSRFRequired()
1454 1454 def pull_request_merge(self):
1455 1455 """
1456 1456 Merge will perform a server-side merge of the specified
1457 1457 pull request, if the pull request is approved and mergeable.
1458 1458 After successful merging, the pull request is automatically
1459 1459 closed, with a relevant comment.
1460 1460 """
1461 1461 pull_request = PullRequest.get_or_404(
1462 1462 self.request.matchdict['pull_request_id'])
1463 1463 _ = self.request.translate
1464 1464
1465 1465 if pull_request.is_state_changing():
1466 1466 log.debug('show: forbidden because pull request is in state %s',
1467 1467 pull_request.pull_request_state)
1468 1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1469 1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1470 1470 pull_request.pull_request_state)
1471 1471 h.flash(msg, category='error')
1472 1472 raise HTTPFound(
1473 1473 h.route_path('pullrequest_show',
1474 1474 repo_name=pull_request.target_repo.repo_name,
1475 1475 pull_request_id=pull_request.pull_request_id))
1476 1476
1477 1477 self.load_default_context()
1478 1478
1479 1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1480 1480 check = MergeCheck.validate(
1481 1481 pull_request, auth_user=self._rhodecode_user,
1482 1482 translator=self.request.translate)
1483 1483 merge_possible = not check.failed
1484 1484
1485 1485 for err_type, error_msg in check.errors:
1486 1486 h.flash(error_msg, category=err_type)
1487 1487
1488 1488 if merge_possible:
1489 1489 log.debug("Pre-conditions checked, trying to merge.")
1490 1490 extras = vcs_operation_context(
1491 1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1492 1492 username=self._rhodecode_db_user.username, action='push',
1493 1493 scm=pull_request.target_repo.repo_type)
1494 1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1495 1495 self._merge_pull_request(
1496 1496 pull_request, self._rhodecode_db_user, extras)
1497 1497 else:
1498 1498 log.debug("Pre-conditions failed, NOT merging.")
1499 1499
1500 1500 raise HTTPFound(
1501 1501 h.route_path('pullrequest_show',
1502 1502 repo_name=pull_request.target_repo.repo_name,
1503 1503 pull_request_id=pull_request.pull_request_id))
1504 1504
1505 1505 def _merge_pull_request(self, pull_request, user, extras):
1506 1506 _ = self.request.translate
1507 1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1508 1508
1509 1509 if merge_resp.executed:
1510 1510 log.debug("The merge was successful, closing the pull request.")
1511 1511 PullRequestModel().close_pull_request(
1512 1512 pull_request.pull_request_id, user)
1513 1513 Session().commit()
1514 1514 msg = _('Pull request was successfully merged and closed.')
1515 1515 h.flash(msg, category='success')
1516 1516 else:
1517 1517 log.debug(
1518 1518 "The merge was not successful. Merge response: %s", merge_resp)
1519 1519 msg = merge_resp.merge_status_message
1520 1520 h.flash(msg, category='error')
1521 1521
1522 1522 @LoginRequired()
1523 1523 @NotAnonymous()
1524 1524 @HasRepoPermissionAnyDecorator(
1525 1525 'repository.read', 'repository.write', 'repository.admin')
1526 1526 @CSRFRequired()
1527 1527 def pull_request_delete(self):
1528 1528 _ = self.request.translate
1529 1529
1530 1530 pull_request = PullRequest.get_or_404(
1531 1531 self.request.matchdict['pull_request_id'])
1532 1532 self.load_default_context()
1533 1533
1534 1534 pr_closed = pull_request.is_closed()
1535 1535 allowed_to_delete = PullRequestModel().check_user_delete(
1536 1536 pull_request, self._rhodecode_user) and not pr_closed
1537 1537
1538 1538 # only owner can delete it !
1539 1539 if allowed_to_delete:
1540 1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1541 1541 Session().commit()
1542 1542 h.flash(_('Successfully deleted pull request'),
1543 1543 category='success')
1544 1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1545 1545 repo_name=self.db_repo_name))
1546 1546
1547 1547 log.warning('user %s tried to delete pull request without access',
1548 1548 self._rhodecode_user)
1549 1549 raise HTTPNotFound()
1550 1550
1551 1551 def _pull_request_comments_create(self, pull_request, comments):
1552 1552 _ = self.request.translate
1553 1553 data = {}
1554 1554 if not comments:
1555 1555 return
1556 1556 pull_request_id = pull_request.pull_request_id
1557 1557
1558 1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1559 1559
1560 1560 for entry in comments:
1561 1561 c = self.load_default_context()
1562 1562 comment_type = entry['comment_type']
1563 1563 text = entry['text']
1564 1564 status = entry['status']
1565 1565 is_draft = str2bool(entry['is_draft'])
1566 1566 resolves_comment_id = entry['resolves_comment_id']
1567 1567 close_pull_request = entry['close_pull_request']
1568 1568 f_path = entry['f_path']
1569 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 1572 # the logic here should work like following, if we submit close
1573 1573 # pr comment, use `close_pull_request_with_comment` function
1574 1574 # else handle regular comment logic
1575 1575
1576 1576 if close_pull_request:
1577 1577 # only owner or admin or person with write permissions
1578 1578 allowed_to_close = PullRequestModel().check_user_update(
1579 1579 pull_request, self._rhodecode_user)
1580 1580 if not allowed_to_close:
1581 1581 log.debug('comment: forbidden because not allowed to close '
1582 1582 'pull request %s', pull_request_id)
1583 1583 raise HTTPForbidden()
1584 1584
1585 1585 # This also triggers `review_status_change`
1586 1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1587 1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1588 1588 auth_user=self._rhodecode_user)
1589 1589 Session().flush()
1590 1590 is_inline = comment.is_inline
1591 1591
1592 1592 PullRequestModel().trigger_pull_request_hook(
1593 1593 pull_request, self._rhodecode_user, 'comment',
1594 1594 data={'comment': comment})
1595 1595
1596 1596 else:
1597 1597 # regular comment case, could be inline, or one with status.
1598 1598 # for that one we check also permissions
1599 1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1600 1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1601 1601 pull_request, self._rhodecode_user) and not is_draft
1602 1602
1603 1603 if status and allowed_to_change_status:
1604 1604 message = (_('Status change %(transition_icon)s %(status)s')
1605 1605 % {'transition_icon': '>',
1606 1606 'status': ChangesetStatus.get_status_lbl(status)})
1607 1607 text = text or message
1608 1608
1609 1609 comment = CommentsModel().create(
1610 1610 text=text,
1611 1611 repo=self.db_repo.repo_id,
1612 1612 user=self._rhodecode_user.user_id,
1613 1613 pull_request=pull_request,
1614 1614 f_path=f_path,
1615 1615 line_no=line_no,
1616 1616 status_change=(ChangesetStatus.get_status_lbl(status)
1617 1617 if status and allowed_to_change_status else None),
1618 1618 status_change_type=(status
1619 1619 if status and allowed_to_change_status else None),
1620 1620 comment_type=comment_type,
1621 1621 is_draft=is_draft,
1622 1622 resolves_comment_id=resolves_comment_id,
1623 1623 auth_user=self._rhodecode_user,
1624 1624 send_email=not is_draft, # skip notification for draft comments
1625 1625 )
1626 1626 is_inline = comment.is_inline
1627 1627
1628 1628 if allowed_to_change_status:
1629 1629 # calculate old status before we change it
1630 1630 old_calculated_status = pull_request.calculated_review_status()
1631 1631
1632 1632 # get status if set !
1633 1633 if status:
1634 1634 ChangesetStatusModel().set_status(
1635 1635 self.db_repo.repo_id,
1636 1636 status,
1637 1637 self._rhodecode_user.user_id,
1638 1638 comment,
1639 1639 pull_request=pull_request
1640 1640 )
1641 1641
1642 1642 Session().flush()
1643 1643 # this is somehow required to get access to some relationship
1644 1644 # loaded on comment
1645 1645 Session().refresh(comment)
1646 1646
1647 1647 # skip notifications for drafts
1648 1648 if not is_draft:
1649 1649 PullRequestModel().trigger_pull_request_hook(
1650 1650 pull_request, self._rhodecode_user, 'comment',
1651 1651 data={'comment': comment})
1652 1652
1653 1653 # we now calculate the status of pull request, and based on that
1654 1654 # calculation we set the commits status
1655 1655 calculated_status = pull_request.calculated_review_status()
1656 1656 if old_calculated_status != calculated_status:
1657 1657 PullRequestModel().trigger_pull_request_hook(
1658 1658 pull_request, self._rhodecode_user, 'review_status_change',
1659 1659 data={'status': calculated_status})
1660 1660
1661 1661 comment_id = comment.comment_id
1662 1662 data[comment_id] = {
1663 1663 'target_id': target_elem_id
1664 1664 }
1665 1665 Session().flush()
1666 1666
1667 1667 c.co = comment
1668 1668 c.at_version_num = None
1669 1669 c.is_new = True
1670 1670 rendered_comment = render(
1671 1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1672 1672 self._get_template_context(c), self.request)
1673 1673
1674 1674 data[comment_id].update(comment.get_dict())
1675 1675 data[comment_id].update({'rendered_text': rendered_comment})
1676 1676
1677 1677 Session().commit()
1678 1678
1679 1679 # skip channelstream for draft comments
1680 1680 if not all_drafts:
1681 1681 comment_broadcast_channel = channelstream.comment_channel(
1682 1682 self.db_repo_name, pull_request_obj=pull_request)
1683 1683
1684 1684 comment_data = data
1685 1685 posted_comment_type = 'inline' if is_inline else 'general'
1686 1686 if len(data) == 1:
1687 1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1688 1688 else:
1689 1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1690 1690
1691 1691 channelstream.comment_channelstream_push(
1692 1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1693 1693 comment_data=comment_data)
1694 1694
1695 1695 return data
1696 1696
1697 1697 @LoginRequired()
1698 1698 @NotAnonymous()
1699 1699 @HasRepoPermissionAnyDecorator(
1700 1700 'repository.read', 'repository.write', 'repository.admin')
1701 1701 @CSRFRequired()
1702 1702 def pull_request_comment_create(self):
1703 1703 _ = self.request.translate
1704 1704
1705 1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1706 1706
1707 1707 if pull_request.is_closed():
1708 1708 log.debug('comment: forbidden because pull request is closed')
1709 1709 raise HTTPForbidden()
1710 1710
1711 1711 allowed_to_comment = PullRequestModel().check_user_comment(
1712 1712 pull_request, self._rhodecode_user)
1713 1713 if not allowed_to_comment:
1714 1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1715 1715 raise HTTPForbidden()
1716 1716
1717 1717 comment_data = {
1718 1718 'comment_type': self.request.POST.get('comment_type'),
1719 1719 'text': self.request.POST.get('text'),
1720 1720 'status': self.request.POST.get('changeset_status', None),
1721 1721 'is_draft': self.request.POST.get('draft'),
1722 1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1723 1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1724 1724 'f_path': self.request.POST.get('f_path'),
1725 1725 'line': self.request.POST.get('line'),
1726 1726 }
1727 1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1728 1728
1729 1729 return data
1730 1730
1731 1731 @LoginRequired()
1732 1732 @NotAnonymous()
1733 1733 @HasRepoPermissionAnyDecorator(
1734 1734 'repository.read', 'repository.write', 'repository.admin')
1735 1735 @CSRFRequired()
1736 1736 def pull_request_comment_delete(self):
1737 1737 pull_request = PullRequest.get_or_404(
1738 1738 self.request.matchdict['pull_request_id'])
1739 1739
1740 1740 comment = ChangesetComment.get_or_404(
1741 1741 self.request.matchdict['comment_id'])
1742 1742 comment_id = comment.comment_id
1743 1743
1744 1744 if comment.immutable:
1745 1745 # don't allow deleting comments that are immutable
1746 1746 raise HTTPForbidden()
1747 1747
1748 1748 if pull_request.is_closed():
1749 1749 log.debug('comment: forbidden because pull request is closed')
1750 1750 raise HTTPForbidden()
1751 1751
1752 1752 if not comment:
1753 1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 1754 # comment already deleted in another call probably
1755 1755 return True
1756 1756
1757 1757 if comment.pull_request.is_closed():
1758 1758 # don't allow deleting comments on closed pull request
1759 1759 raise HTTPForbidden()
1760 1760
1761 1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 1762 super_admin = h.HasPermissionAny('hg.admin')()
1763 1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 1765 comment_repo_admin = is_repo_admin and is_repo_comment
1766 1766
1767 1767 if comment.draft and not comment_owner:
1768 1768 # We never allow to delete draft comments for other than owners
1769 1769 raise HTTPNotFound()
1770 1770
1771 1771 if super_admin or comment_owner or comment_repo_admin:
1772 1772 old_calculated_status = comment.pull_request.calculated_review_status()
1773 1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 1774 Session().commit()
1775 1775 calculated_status = comment.pull_request.calculated_review_status()
1776 1776 if old_calculated_status != calculated_status:
1777 1777 PullRequestModel().trigger_pull_request_hook(
1778 1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 1779 data={'status': calculated_status})
1780 1780 return True
1781 1781 else:
1782 1782 log.warning('No permissions for user %s to delete comment_id: %s',
1783 1783 self._rhodecode_db_user, comment_id)
1784 1784 raise HTTPNotFound()
1785 1785
1786 1786 @LoginRequired()
1787 1787 @NotAnonymous()
1788 1788 @HasRepoPermissionAnyDecorator(
1789 1789 'repository.read', 'repository.write', 'repository.admin')
1790 1790 @CSRFRequired()
1791 1791 def pull_request_comment_edit(self):
1792 1792 self.load_default_context()
1793 1793
1794 1794 pull_request = PullRequest.get_or_404(
1795 1795 self.request.matchdict['pull_request_id']
1796 1796 )
1797 1797 comment = ChangesetComment.get_or_404(
1798 1798 self.request.matchdict['comment_id']
1799 1799 )
1800 1800 comment_id = comment.comment_id
1801 1801
1802 1802 if comment.immutable:
1803 1803 # don't allow deleting comments that are immutable
1804 1804 raise HTTPForbidden()
1805 1805
1806 1806 if pull_request.is_closed():
1807 1807 log.debug('comment: forbidden because pull request is closed')
1808 1808 raise HTTPForbidden()
1809 1809
1810 1810 if comment.pull_request.is_closed():
1811 1811 # don't allow deleting comments on closed pull request
1812 1812 raise HTTPForbidden()
1813 1813
1814 1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 1815 super_admin = h.HasPermissionAny('hg.admin')()
1816 1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 1818 comment_repo_admin = is_repo_admin and is_repo_comment
1819 1819
1820 1820 if super_admin or comment_owner or comment_repo_admin:
1821 1821 text = self.request.POST.get('text')
1822 1822 version = self.request.POST.get('version')
1823 1823 if text == comment.text:
1824 1824 log.warning(
1825 1825 'Comment(PR): '
1826 1826 'Trying to create new version '
1827 1827 'with the same comment body {}'.format(
1828 1828 comment_id,
1829 1829 )
1830 1830 )
1831 1831 raise HTTPNotFound()
1832 1832
1833 1833 if version.isdigit():
1834 1834 version = int(version)
1835 1835 else:
1836 1836 log.warning(
1837 1837 'Comment(PR): Wrong version type {} {} '
1838 1838 'for comment {}'.format(
1839 1839 version,
1840 1840 type(version),
1841 1841 comment_id,
1842 1842 )
1843 1843 )
1844 1844 raise HTTPNotFound()
1845 1845
1846 1846 try:
1847 1847 comment_history = CommentsModel().edit(
1848 1848 comment_id=comment_id,
1849 1849 text=text,
1850 1850 auth_user=self._rhodecode_user,
1851 1851 version=version,
1852 1852 )
1853 1853 except CommentVersionMismatch:
1854 1854 raise HTTPConflict()
1855 1855
1856 1856 if not comment_history:
1857 1857 raise HTTPNotFound()
1858 1858
1859 1859 Session().commit()
1860 1860 if not comment.draft:
1861 1861 PullRequestModel().trigger_pull_request_hook(
1862 1862 pull_request, self._rhodecode_user, 'comment_edit',
1863 1863 data={'comment': comment})
1864 1864
1865 1865 return {
1866 1866 'comment_history_id': comment_history.comment_history_id,
1867 1867 'comment_id': comment.comment_id,
1868 1868 'comment_version': comment_history.version,
1869 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 1871 'comment_created_on': h.age_component(comment_history.created_on,
1872 1872 time_is_local=True),
1873 1873 }
1874 1874 else:
1875 1875 log.warning('No permissions for user %s to edit comment_id: %s',
1876 1876 self._rhodecode_db_user, comment_id)
1877 1877 raise HTTPNotFound()
@@ -1,251 +1,251 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import deform
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode import events
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.forms import RcForm
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib.auth import (
32 32 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
33 33 from rhodecode.model.db import RepositoryField, RepoGroup, Repository, User
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.permission import PermissionModel
36 36 from rhodecode.model.repo import RepoModel
37 37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 38 from rhodecode.model.validation_schema.schemas import repo_schema
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47
48 48 acl_groups = RepoGroupList(
49 49 RepoGroup.query().all(),
50 50 perm_set=['group.write', 'group.admin'])
51 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 54 # in case someone no longer have a group.write access to a repository
55 55 # pre fill the list with this entry, we don't care if this is the same
56 56 # but it will allow saving repo data properly.
57 57 repo_group = self.db_repo.group
58 58 if repo_group and repo_group.group_id not in c.repo_groups_choices:
59 59 c.repo_groups_choices.append(repo_group.group_id)
60 60 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
61 61
62 62 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
63 63 # we might be in missing requirement state, so we load things
64 64 # without touching scm_instance()
65 65 c.landing_revs_choices, c.landing_revs = \
66 66 ScmModel().get_repo_landing_revs(self.request.translate)
67 67 else:
68 68 c.landing_revs_choices, c.landing_revs = \
69 69 ScmModel().get_repo_landing_revs(
70 70 self.request.translate, self.db_repo)
71 71
72 72 c.personal_repo_group = c.auth_user.personal_repo_group
73 73 c.repo_fields = RepositoryField.query()\
74 74 .filter(RepositoryField.repository == self.db_repo).all()
75 75 return c
76 76
77 77 def _get_schema(self, c, old_values=None):
78 78 return repo_schema.RepoSettingsSchema().bind(
79 79 repo_type=self.db_repo.repo_type,
80 80 repo_type_options=[self.db_repo.repo_type],
81 81 repo_ref_options=c.landing_revs_choices,
82 82 repo_ref_items=c.landing_revs,
83 83 repo_repo_group_options=c.repo_groups_choices,
84 84 repo_repo_group_items=c.repo_groups,
85 85 # user caller
86 86 user=self._rhodecode_user,
87 87 old_values=old_values
88 88 )
89 89
90 90 @LoginRequired()
91 91 @HasRepoPermissionAnyDecorator('repository.admin')
92 92 def edit_settings(self):
93 93 c = self.load_default_context()
94 94 c.active = 'settings'
95 95
96 96 defaults = RepoModel()._get_defaults(self.db_repo_name)
97 97 defaults['repo_owner'] = defaults['user']
98 98 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
99 99
100 100 schema = self._get_schema(c)
101 101 c.form = RcForm(schema, appstruct=defaults)
102 102 return self._get_template_context(c)
103 103
104 104 @LoginRequired()
105 105 @HasRepoPermissionAnyDecorator('repository.admin')
106 106 @CSRFRequired()
107 107 def edit_settings_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'settings'
111 111 old_repo_name = self.db_repo_name
112 112
113 113 old_values = self.db_repo.get_api_data()
114 114 schema = self._get_schema(c, old_values=old_values)
115 115
116 116 c.form = RcForm(schema)
117 pstruct = self.request.POST.items()
117 pstruct = list(self.request.POST.items())
118 118 pstruct.append(('repo_type', self.db_repo.repo_type))
119 119 try:
120 120 schema_data = c.form.validate(pstruct)
121 121 except deform.ValidationFailure as err_form:
122 122 return self._get_template_context(c)
123 123
124 124 # data is now VALID, proceed with updates
125 125 # save validated data back into the updates dict
126 126 validated_updates = dict(
127 127 repo_name=schema_data['repo_group']['repo_name_without_group'],
128 128 repo_group=schema_data['repo_group']['repo_group_id'],
129 129
130 130 user=schema_data['repo_owner'],
131 131 repo_description=schema_data['repo_description'],
132 132 repo_private=schema_data['repo_private'],
133 133 clone_uri=schema_data['repo_clone_uri'],
134 134 push_uri=schema_data['repo_push_uri'],
135 135 repo_landing_rev=schema_data['repo_landing_commit_ref'],
136 136 repo_enable_statistics=schema_data['repo_enable_statistics'],
137 137 repo_enable_locking=schema_data['repo_enable_locking'],
138 138 repo_enable_downloads=schema_data['repo_enable_downloads'],
139 139 )
140 140 # detect if SYNC URI changed, if we get OLD means we keep old values
141 141 if schema_data['repo_clone_uri_change'] == 'OLD':
142 142 validated_updates['clone_uri'] = self.db_repo.clone_uri
143 143
144 144 if schema_data['repo_push_uri_change'] == 'OLD':
145 145 validated_updates['push_uri'] = self.db_repo.push_uri
146 146
147 147 # use the new full name for redirect
148 148 new_repo_name = schema_data['repo_group']['repo_name_with_group']
149 149
150 150 # save extra fields into our validated data
151 151 for key, value in pstruct:
152 152 if key.startswith(RepositoryField.PREFIX):
153 153 validated_updates[key] = value
154 154
155 155 try:
156 156 RepoModel().update(self.db_repo, **validated_updates)
157 157 ScmModel().mark_for_invalidation(new_repo_name)
158 158
159 159 audit_logger.store_web(
160 160 'repo.edit', action_data={'old_data': old_values},
161 161 user=self._rhodecode_user, repo=self.db_repo)
162 162
163 163 Session().commit()
164 164
165 165 h.flash(_('Repository `{}` updated successfully').format(old_repo_name),
166 166 category='success')
167 167 except Exception:
168 168 log.exception("Exception during update of repository")
169 169 h.flash(_('Error occurred during update of repository {}').format(
170 170 old_repo_name), category='error')
171 171
172 172 name_changed = old_repo_name != new_repo_name
173 173 if name_changed:
174 174 current_perms = self.db_repo.permissions(expand_from_user_groups=True)
175 175 affected_user_ids = [perm['user_id'] for perm in current_perms]
176 176
177 177 # NOTE(marcink): also add owner maybe it has changed
178 178 owner = User.get_by_username(schema_data['repo_owner'])
179 179 owner_id = owner.user_id if owner else self._rhodecode_user.user_id
180 180 affected_user_ids.extend([self._rhodecode_user.user_id, owner_id])
181 181 PermissionModel().trigger_permission_flush(affected_user_ids)
182 182
183 183 raise HTTPFound(
184 184 h.route_path('edit_repo', repo_name=new_repo_name))
185 185
186 186 @LoginRequired()
187 187 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
188 188 def toggle_locking(self):
189 189 """
190 190 Toggle locking of repository by simple GET call to url
191 191 """
192 192 _ = self.request.translate
193 193 repo = self.db_repo
194 194
195 195 try:
196 196 if repo.enable_locking:
197 197 if repo.locked[0]:
198 198 Repository.unlock(repo)
199 199 action = _('Unlocked')
200 200 else:
201 201 Repository.lock(
202 202 repo, self._rhodecode_user.user_id,
203 203 lock_reason=Repository.LOCK_WEB)
204 204 action = _('Locked')
205 205
206 206 h.flash(_('Repository has been %s') % action,
207 207 category='success')
208 208 except Exception:
209 209 log.exception("Exception during unlocking")
210 210 h.flash(_('An error occurred during unlocking'),
211 211 category='error')
212 212 raise HTTPFound(
213 213 h.route_path('repo_summary', repo_name=self.db_repo_name))
214 214
215 215 @LoginRequired()
216 216 @HasRepoPermissionAnyDecorator('repository.admin')
217 217 def edit_statistics_form(self):
218 218 c = self.load_default_context()
219 219
220 220 if self.db_repo.stats:
221 221 # this is on what revision we ended up so we add +1 for count
222 222 last_rev = self.db_repo.stats.stat_on_revision + 1
223 223 else:
224 224 last_rev = 0
225 225
226 226 c.active = 'statistics'
227 227 c.stats_revision = last_rev
228 228 c.repo_last_rev = self.rhodecode_vcs_repo.count()
229 229
230 230 if last_rev == 0 or c.repo_last_rev == 0:
231 231 c.stats_percentage = 0
232 232 else:
233 233 c.stats_percentage = '%.2f' % (
234 234 (float((last_rev)) / c.repo_last_rev) * 100)
235 235 return self._get_template_context(c)
236 236
237 237 @LoginRequired()
238 238 @HasRepoPermissionAnyDecorator('repository.admin')
239 239 @CSRFRequired()
240 240 def repo_statistics_reset(self):
241 241 _ = self.request.translate
242 242
243 243 try:
244 244 RepoModel().delete_stats(self.db_repo_name)
245 245 Session().commit()
246 246 except Exception:
247 247 log.exception('Edit statistics failure')
248 248 h.flash(_('An error occurred during deletion of repository stats'),
249 249 category='error')
250 250 raise HTTPFound(
251 251 h.route_path('edit_repo_statistics', repo_name=self.db_repo_name))
@@ -1,274 +1,274 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import string
23 23 import time
24 24
25 25 import rhodecode
26 26
27 27
28 28
29 29 from rhodecode.lib.view_utils import get_format_ref_id
30 30 from rhodecode.apps._base import RepoAppView
31 31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 32 from rhodecode.lib import helpers as h, rc_cache
33 33 from rhodecode.lib.utils2 import safe_str, safe_int
34 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib.vcs.exceptions import (
38 38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 39 from rhodecode.model.db import Statistics, CacheKey, User
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.scm import ScmModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class RepoSummaryView(RepoAppView):
47 47
48 48 def load_default_context(self):
49 49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 50 c.rhodecode_repo = None
51 51 if not c.repository_requirements_missing:
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53 return c
54 54
55 55 def _load_commits_context(self, c):
56 56 p = safe_int(self.request.GET.get('page'), 1)
57 57 size = safe_int(self.request.GET.get('size'), 10)
58 58
59 59 def url_generator(page_num):
60 60 query_params = {
61 61 'page': page_num,
62 62 'size': size
63 63 }
64 64 return h.route_path(
65 65 'repo_summary_commits',
66 66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67 67
68 68 pre_load = self.get_commit_preload_attrs()
69 69
70 70 try:
71 71 collection = self.rhodecode_vcs_repo.get_commits(
72 72 pre_load=pre_load, translate_tags=False)
73 73 except EmptyRepositoryError:
74 74 collection = self.rhodecode_vcs_repo
75 75
76 76 c.repo_commits = h.RepoPage(
77 77 collection, page=p, items_per_page=size, url_maker=url_generator)
78 78 page_ids = [x.raw_id for x in c.repo_commits]
79 79 c.comments = self.db_repo.get_comments(page_ids)
80 80 c.statuses = self.db_repo.statuses(page_ids)
81 81
82 82 @LoginRequired()
83 83 @HasRepoPermissionAnyDecorator(
84 84 'repository.read', 'repository.write', 'repository.admin')
85 85 def summary_commits(self):
86 86 c = self.load_default_context()
87 87 self._prepare_and_set_clone_url(c)
88 88 self._load_commits_context(c)
89 89 return self._get_template_context(c)
90 90
91 91 @LoginRequired()
92 92 @HasRepoPermissionAnyDecorator(
93 93 'repository.read', 'repository.write', 'repository.admin')
94 94 def summary(self):
95 95 c = self.load_default_context()
96 96
97 97 # Prepare the clone URL
98 98 self._prepare_and_set_clone_url(c)
99 99
100 100 # If enabled, get statistics data
101 101 c.show_stats = bool(self.db_repo.enable_statistics)
102 102
103 103 stats = Session().query(Statistics) \
104 104 .filter(Statistics.repository == self.db_repo) \
105 105 .scalar()
106 106
107 107 c.stats_percentage = 0
108 108
109 109 if stats and stats.languages:
110 110 c.no_data = False is self.db_repo.enable_statistics
111 111 lang_stats_d = json.loads(stats.languages)
112 112
113 113 # Sort first by decreasing count and second by the file extension,
114 114 # so we have a consistent output.
115 115 lang_stats_items = sorted(lang_stats_d.items(),
116 116 key=lambda k: (-k[1], k[0]))[:10]
117 117 lang_stats = [(x, {"count": y,
118 118 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
119 119 for x, y in lang_stats_items]
120 120
121 121 c.trending_languages = json.dumps(lang_stats)
122 122 else:
123 123 c.no_data = True
124 124 c.trending_languages = json.dumps({})
125 125
126 126 scm_model = ScmModel()
127 127 c.enable_downloads = self.db_repo.enable_downloads
128 128 c.repository_followers = scm_model.get_followers(self.db_repo)
129 129 c.repository_forks = scm_model.get_forks(self.db_repo)
130 130
131 131 # first interaction with the VCS instance after here...
132 132 if c.repository_requirements_missing:
133 133 self.request.override_renderer = \
134 134 'rhodecode:templates/summary/missing_requirements.mako'
135 135 return self._get_template_context(c)
136 136
137 137 c.readme_data, c.readme_file = \
138 138 self._get_readme_data(self.db_repo, c.visual.default_renderer)
139 139
140 140 # loads the summary commits template context
141 141 self._load_commits_context(c)
142 142
143 143 return self._get_template_context(c)
144 144
145 145 @LoginRequired()
146 146 @HasRepoPermissionAnyDecorator(
147 147 'repository.read', 'repository.write', 'repository.admin')
148 148 def repo_stats(self):
149 149 show_stats = bool(self.db_repo.enable_statistics)
150 150 repo_id = self.db_repo.repo_id
151 151
152 152 landing_commit = self.db_repo.get_landing_commit()
153 153 if isinstance(landing_commit, EmptyCommit):
154 154 return {'size': 0, 'code_stats': {}}
155 155
156 156 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
157 157 cache_on = cache_seconds > 0
158 158
159 159 log.debug(
160 160 'Computing REPO STATS for repo_id %s commit_id `%s` '
161 161 'with caching: %s[TTL: %ss]' % (
162 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 165 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
166 166
167 167 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
168 168 condition=cache_on)
169 169 def compute_stats(repo_id, commit_id, _show_stats):
170 170 code_stats = {}
171 171 size = 0
172 172 try:
173 173 commit = self.db_repo.get_commit(commit_id)
174 174
175 175 for node in commit.get_filenodes_generator():
176 176 size += node.size
177 177 if not _show_stats:
178 178 continue
179 179 ext = node.extension.lower()
180 180 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
181 181 if ext_info:
182 182 if ext in code_stats:
183 183 code_stats[ext]['count'] += 1
184 184 else:
185 185 code_stats[ext] = {"count": 1, "desc": ext_info}
186 186 except (EmptyRepositoryError, CommitDoesNotExistError):
187 187 pass
188 188 return {'size': h.format_byte_size_binary(size),
189 189 'code_stats': code_stats}
190 190
191 191 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
192 192 return stats
193 193
194 194 @LoginRequired()
195 195 @HasRepoPermissionAnyDecorator(
196 196 'repository.read', 'repository.write', 'repository.admin')
197 197 def repo_refs_data(self):
198 198 _ = self.request.translate
199 199 self.load_default_context()
200 200
201 201 repo = self.rhodecode_vcs_repo
202 202 refs_to_create = [
203 203 (_("Branch"), repo.branches, 'branch'),
204 204 (_("Tag"), repo.tags, 'tag'),
205 205 (_("Bookmark"), repo.bookmarks, 'book'),
206 206 ]
207 207 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
208 208 data = {
209 209 'more': False,
210 210 'results': res
211 211 }
212 212 return data
213 213
214 214 @LoginRequired()
215 215 @HasRepoPermissionAnyDecorator(
216 216 'repository.read', 'repository.write', 'repository.admin')
217 217 def repo_refs_changelog_data(self):
218 218 _ = self.request.translate
219 219 self.load_default_context()
220 220
221 221 repo = self.rhodecode_vcs_repo
222 222
223 223 refs_to_create = [
224 224 (_("Branches"), repo.branches, 'branch'),
225 225 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
226 226 # TODO: enable when vcs can handle bookmarks filters
227 227 # (_("Bookmarks"), repo.bookmarks, "book"),
228 228 ]
229 229 res = self._create_reference_data(
230 230 repo, self.db_repo_name, refs_to_create)
231 231 data = {
232 232 'more': False,
233 233 'results': res
234 234 }
235 235 return data
236 236
237 237 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
238 238 format_ref_id = get_format_ref_id(repo)
239 239
240 240 result = []
241 241 for title, refs, ref_type in refs_to_create:
242 242 if refs:
243 243 result.append({
244 244 'text': title,
245 245 'children': self._create_reference_items(
246 246 repo, full_repo_name, refs, ref_type,
247 247 format_ref_id),
248 248 })
249 249 return result
250 250
251 251 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
252 252 result = []
253 253 is_svn = h.is_svn(repo)
254 254 for ref_name, raw_id in refs.items():
255 255 files_url = self._create_files_url(
256 256 repo, full_repo_name, ref_name, raw_id, is_svn)
257 257 result.append({
258 258 'text': ref_name,
259 259 'id': format_ref_id(ref_name, raw_id),
260 260 'raw_id': raw_id,
261 261 'type': ref_type,
262 262 'files_url': files_url,
263 263 'idx': 0,
264 264 })
265 265 return result
266 266
267 267 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
268 268 use_commit_id = '/' in ref_name or is_svn
269 269 return h.route_path(
270 270 'repo_files',
271 271 repo_name=full_repo_name,
272 272 f_path=ref_name if is_svn else '',
273 273 commit_id=raw_id if use_commit_id else ref_name,
274 274 _query=dict(at=ref_name))
@@ -1,166 +1,168 b''
1 1
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 import urllib.request, urllib.parse, urllib.error
22 import urllib.request
23 import urllib.parse
24 import urllib.error
23 25
24 26 from webhelpers2.html.tools import update_params
25 27
26 28 from rhodecode.apps._base import BaseAppView, RepoAppView, RepoGroupAppView
27 29 from rhodecode.lib.auth import (
28 30 LoginRequired, HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
29 31 from rhodecode.lib.helpers import Page
30 32 from rhodecode.lib.utils2 import safe_str
31 33 from rhodecode.lib.index import searcher_from_config
32 34 from rhodecode.model import validation_schema
33 35 from rhodecode.model.validation_schema.schemas import search_schema
34 36
35 37 log = logging.getLogger(__name__)
36 38
37 39
38 40 def perform_search(request, tmpl_context, repo_name=None, repo_group_name=None):
39 41 searcher = searcher_from_config(request.registry.settings)
40 42 formatted_results = []
41 43 execution_time = ''
42 44
43 45 schema = search_schema.SearchParamsSchema()
44 46 search_tags = []
45 47 search_params = {}
46 48 errors = []
47 49
48 50 try:
49 51 search_params = schema.deserialize(
50 52 dict(
51 53 search_query=request.GET.get('q'),
52 54 search_type=request.GET.get('type'),
53 55 search_sort=request.GET.get('sort'),
54 56 search_max_lines=request.GET.get('max_lines'),
55 57 page_limit=request.GET.get('page_limit'),
56 58 requested_page=request.GET.get('page'),
57 59 )
58 60 )
59 61 except validation_schema.Invalid as e:
60 62 errors = e.children
61 63
62 64 def url_generator(page_num):
63 65
64 66 query_params = {
65 67 'page': page_num,
66 68 'q': safe_str(search_query),
67 69 'type': safe_str(search_type),
68 70 'max_lines': search_max_lines,
69 71 'sort': search_sort
70 72 }
71 73
72 74 return '?' + urllib.parse.urlencode(query_params)
73 75
74 76 c = tmpl_context
75 77 search_query = search_params.get('search_query')
76 78 search_type = search_params.get('search_type')
77 79 search_sort = search_params.get('search_sort')
78 80 search_max_lines = search_params.get('search_max_lines')
79 81 if search_params.get('search_query'):
80 82 page_limit = search_params['page_limit']
81 83 requested_page = search_params['requested_page']
82 84
83 85 try:
84 86 search_result = searcher.search(
85 87 search_query, search_type, c.auth_user, repo_name, repo_group_name,
86 88 requested_page=requested_page, page_limit=page_limit, sort=search_sort)
87 89
88 90 formatted_results = Page(
89 91 search_result['results'], page=requested_page,
90 92 item_count=search_result['count'],
91 93 items_per_page=page_limit, url_maker=url_generator)
92 94 finally:
93 95 searcher.cleanup()
94 96
95 97 search_tags = searcher.extract_search_tags(search_query)
96 98
97 99 if not search_result['error']:
98 100 execution_time = '%s results (%.4f seconds)' % (
99 101 search_result['count'],
100 102 search_result['runtime'])
101 103 elif not errors:
102 104 node = schema['search_query']
103 105 errors = [
104 106 validation_schema.Invalid(node, search_result['error'])]
105 107
106 108 c.perm_user = c.auth_user
107 109 c.repo_name = repo_name
108 110 c.repo_group_name = repo_group_name
109 111 c.errors = errors
110 112 c.formatted_results = formatted_results
111 113 c.runtime = execution_time
112 114 c.cur_query = search_query
113 115 c.search_type = search_type
114 116 c.searcher = searcher
115 117 c.search_tags = search_tags
116 118
117 119 direction, sort_field = searcher.get_sort(search_type, search_sort)
118 120 sort_definition = searcher.sort_def(search_type, direction, sort_field)
119 121 c.sort = ''
120 122 c.sort_tag = None
121 123 c.sort_tag_dir = direction
122 124 if sort_definition:
123 125 c.sort = '{}:{}'.format(direction, sort_field)
124 126 c.sort_tag = sort_field
125 127
126 128
127 129 class SearchView(BaseAppView):
128 130 def load_default_context(self):
129 131 c = self._get_local_tmpl_context()
130 132 return c
131 133
132 134 @LoginRequired()
133 135 def search(self):
134 136 c = self.load_default_context()
135 137 perform_search(self.request, c)
136 138 return self._get_template_context(c)
137 139
138 140
139 141 class SearchRepoView(RepoAppView):
140 142 def load_default_context(self):
141 143 c = self._get_local_tmpl_context()
142 144 c.active = 'search'
143 145 return c
144 146
145 147 @LoginRequired()
146 148 @HasRepoPermissionAnyDecorator(
147 149 'repository.read', 'repository.write', 'repository.admin')
148 150 def search_repo(self):
149 151 c = self.load_default_context()
150 152 perform_search(self.request, c, repo_name=self.db_repo_name)
151 153 return self._get_template_context(c)
152 154
153 155
154 156 class SearchRepoGroupView(RepoGroupAppView):
155 157 def load_default_context(self):
156 158 c = self._get_local_tmpl_context()
157 159 c.active = 'search'
158 160 return c
159 161
160 162 @LoginRequired()
161 163 @HasRepoGroupPermissionAnyDecorator(
162 164 'group.read', 'group.write', 'group.admin')
163 165 def search_repo_group(self):
164 166 c = self.load_default_context()
165 167 perform_search(self.request, c, repo_group_name=self.db_repo_group_name)
166 168 return self._get_template_context(c)
@@ -1,163 +1,163 b''
1 1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 import json
24 23 import logging
25 24
26 25 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.ext_json import sjson as json
27 27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class VcsServer(object):
34 34 repo_user_agent = None # set in child classes
35 35 _path = None # set executable path for hg/git/svn binary
36 36 backend = None # set in child classes
37 37 tunnel = None # subprocess handling tunnel
38 38 write_perms = ['repository.admin', 'repository.write']
39 39 read_perms = ['repository.read', 'repository.admin', 'repository.write']
40 40
41 41 def __init__(self, user, user_permissions, config, env):
42 42 self.user = user
43 43 self.user_permissions = user_permissions
44 44 self.config = config
45 45 self.env = env
46 46 self.stdin = sys.stdin
47 47
48 48 self.repo_name = None
49 49 self.repo_mode = None
50 50 self.store = ''
51 51 self.ini_path = ''
52 52
53 53 def _invalidate_cache(self, repo_name):
54 54 """
55 55 Set's cache for this repository for invalidation on next access
56 56
57 57 :param repo_name: full repo name, also a cache key
58 58 """
59 59 ScmModel().mark_for_invalidation(repo_name)
60 60
61 61 def has_write_perm(self):
62 62 permission = self.user_permissions.get(self.repo_name)
63 63 if permission in ['repository.write', 'repository.admin']:
64 64 return True
65 65
66 66 return False
67 67
68 68 def _check_permissions(self, action):
69 69 permission = self.user_permissions.get(self.repo_name)
70 70 log.debug('permission for %s on %s are: %s',
71 71 self.user, self.repo_name, permission)
72 72
73 73 if not permission:
74 74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 75 self.user, self.repo_name)
76 76 return -2
77 77
78 78 if action == 'pull':
79 79 if permission in self.read_perms:
80 80 log.info(
81 81 'READ Permissions for User "%s" detected to repo "%s"!',
82 82 self.user, self.repo_name)
83 83 return 0
84 84 else:
85 85 if permission in self.write_perms:
86 86 log.info(
87 87 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
88 88 self.user, self.repo_name)
89 89 return 0
90 90
91 91 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 92 'Permissions: %s, vcs action: %s',
93 93 self.user, permission, action)
94 94 return -2
95 95
96 96 def update_environment(self, action, extras=None):
97 97
98 98 scm_data = {
99 99 'ip': os.environ['SSH_CLIENT'].split()[0],
100 100 'username': self.user.username,
101 101 'user_id': self.user.user_id,
102 102 'action': action,
103 103 'repository': self.repo_name,
104 104 'scm': self.backend,
105 105 'config': self.ini_path,
106 106 'repo_store': self.store,
107 107 'make_lock': None,
108 108 'locked_by': [None, None],
109 109 'server_url': None,
110 110 'user_agent': '{}/ssh-user-agent'.format(self.repo_user_agent),
111 111 'hooks': ['push', 'pull'],
112 112 'hooks_module': 'rhodecode.lib.hooks_daemon',
113 113 'is_shadow_repo': False,
114 114 'detect_force_push': False,
115 115 'check_branch_perms': False,
116 116
117 117 'SSH': True,
118 118 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
119 119 }
120 120 if extras:
121 121 scm_data.update(extras)
122 122 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
123 123
124 124 def get_root_store(self):
125 125 root_store = self.store
126 126 if not root_store.endswith('/'):
127 127 # always append trailing slash
128 128 root_store = root_store + '/'
129 129 return root_store
130 130
131 131 def _handle_tunnel(self, extras):
132 132 # pre-auth
133 133 action = 'pull'
134 134 exit_code = self._check_permissions(action)
135 135 if exit_code:
136 136 return exit_code, False
137 137
138 138 req = self.env['request']
139 139 server_url = req.host_url + req.script_name
140 140 extras['server_url'] = server_url
141 141
142 142 log.debug('Using %s binaries from path %s', self.backend, self._path)
143 143 exit_code = self.tunnel.run(extras)
144 144
145 145 return exit_code, action == "push"
146 146
147 147 def run(self, tunnel_extras=None):
148 148 tunnel_extras = tunnel_extras or {}
149 149 extras = {}
150 150 extras.update(tunnel_extras)
151 151
152 152 callback_daemon, extras = prepare_callback_daemon(
153 153 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
154 154 host=vcs_settings.HOOKS_HOST,
155 155 use_direct_calls=False)
156 156
157 157 with callback_daemon:
158 158 try:
159 159 return self._handle_tunnel(extras)
160 160 finally:
161 161 log.debug('Running cleanup with cache invalidation')
162 162 if self.repo_name:
163 163 self._invalidate_cache(self.repo_name)
@@ -1,258 +1,258 b''
1 # -*- coding: utf-8 -*-
1
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import sys
24 24 import logging
25 25 import signal
26 26 import tempfile
27 27 from subprocess import Popen, PIPE
28 28 import urllib.parse
29 29
30 30 from .base import VcsServer
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class SubversionTunnelWrapper(object):
36 36 process = None
37 37
38 38 def __init__(self, server):
39 39 self.server = server
40 40 self.timeout = 30
41 41 self.stdin = sys.stdin
42 42 self.stdout = sys.stdout
43 43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45 45
46 46 self.read_only = True # flag that we set to make the hooks readonly
47 47
48 48 def create_svn_config(self):
49 49 content = (
50 50 '[general]\n'
51 51 'hooks-env = {}\n').format(self.hooks_env_path)
52 52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 53 config_file.write(content)
54 54
55 55 def create_hooks_env(self):
56 56 content = (
57 57 '[default]\n'
58 58 'LANG = en_US.UTF-8\n')
59 59 if self.read_only:
60 60 content += 'SSH_READ_ONLY = 1\n'
61 61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 62 hooks_env_file.write(content)
63 63
64 64 def remove_configs(self):
65 65 os.remove(self.svn_conf_path)
66 66 os.remove(self.hooks_env_path)
67 67
68 68 def command(self):
69 69 root = self.server.get_root_store()
70 70 username = self.server.user.username
71 71
72 72 command = [
73 73 self.server.svn_path, '-t',
74 74 '--config-file', self.svn_conf_path,
75 75 '--tunnel-user', username,
76 76 '-r', root]
77 77 log.debug("Final CMD: %s", ' '.join(command))
78 78 return command
79 79
80 80 def start(self):
81 81 command = self.command()
82 82 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
83 83
84 84 def sync(self):
85 85 while self.process.poll() is None:
86 86 next_byte = self.stdin.read(1)
87 87 if not next_byte:
88 88 break
89 89 self.process.stdin.write(next_byte)
90 90 self.remove_configs()
91 91
92 92 @property
93 93 def return_code(self):
94 94 return self.process.returncode
95 95
96 96 def get_first_client_response(self):
97 97 signal.signal(signal.SIGALRM, self.interrupt)
98 98 signal.alarm(self.timeout)
99 99 first_response = self._read_first_client_response()
100 100 signal.alarm(0)
101 101 return (self._parse_first_client_response(first_response)
102 102 if first_response else None)
103 103
104 104 def patch_first_client_response(self, response, **kwargs):
105 105 self.create_hooks_env()
106 106 data = response.copy()
107 107 data.update(kwargs)
108 108 data['url'] = self._svn_string(data['url'])
109 109 data['ra_client'] = self._svn_string(data['ra_client'])
110 110 data['client'] = data['client'] or ''
111 111 buffer_ = (
112 112 "( {version} ( {capabilities} ) {url}{ra_client}"
113 113 "( {client}) ) ".format(**data))
114 114 self.process.stdin.write(buffer_)
115 115
116 116 def fail(self, message):
117 117 print("( failure ( ( 210005 {message} 0: 0 ) ) )".format(
118 118 message=self._svn_string(message)))
119 119 self.remove_configs()
120 120 self.process.kill()
121 121 return 1
122 122
123 123 def interrupt(self, signum, frame):
124 124 self.fail("Exited by timeout")
125 125
126 126 def _svn_string(self, str_):
127 127 if not str_:
128 128 return ''
129 129 return '{length}:{string} '.format(length=len(str_), string=str_)
130 130
131 131 def _read_first_client_response(self):
132 132 buffer_ = ""
133 133 brackets_stack = []
134 134 while True:
135 135 next_byte = self.stdin.read(1)
136 136 buffer_ += next_byte
137 137 if next_byte == "(":
138 138 brackets_stack.append(next_byte)
139 139 elif next_byte == ")":
140 140 brackets_stack.pop()
141 141 elif next_byte == " " and not brackets_stack:
142 142 break
143 143
144 144 return buffer_
145 145
146 146 def _parse_first_client_response(self, buffer_):
147 147 """
148 148 According to the Subversion RA protocol, the first request
149 149 should look like:
150 150
151 151 ( version:number ( cap:word ... ) url:string ? ra-client:string
152 152 ( ? client:string ) )
153 153
154 154 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
155 155 """
156 156 version_re = r'(?P<version>\d+)'
157 157 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
158 158 url_re = r'\d+\:(?P<url>[\W\w]+)'
159 159 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
160 160 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
161 161 regex = re.compile(
162 162 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
163 163 r'\(\s{client}\)\s\)\s*$'.format(
164 164 version=version_re, capabilities=capabilities_re,
165 165 url=url_re, ra_client=ra_client_re, client=client_re))
166 166 matcher = regex.match(buffer_)
167 167
168 168 return matcher.groupdict() if matcher else None
169 169
170 170 def _match_repo_name(self, url):
171 171 """
172 172 Given an server url, try to match it against ALL known repository names.
173 173 This handles a tricky SVN case for SSH and subdir commits.
174 174 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
175 175 result in the url with this subdir added.
176 176 """
177 177 # case 1 direct match, we don't do any "heavy" lookups
178 178 if url in self.server.user_permissions:
179 179 return url
180 180
181 181 log.debug('Extracting repository name from subdir path %s', url)
182 182 # case 2 we check all permissions, and match closes possible case...
183 183 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
184 184 # to assume that it will have the repo name as prefix, we ensure the prefix
185 185 # for similar repositories isn't matched by adding a /
186 186 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
187 187 for repo_name in self.server.user_permissions:
188 188 repo_name_prefix = repo_name + '/'
189 189 if url.startswith(repo_name_prefix):
190 190 log.debug('Found prefix %s match, returning proper repository name',
191 191 repo_name_prefix)
192 192 return repo_name
193 193
194 194 return
195 195
196 196 def run(self, extras):
197 197 action = 'pull'
198 198 self.create_svn_config()
199 199 self.start()
200 200
201 201 first_response = self.get_first_client_response()
202 202 if not first_response:
203 203 return self.fail("Repository name cannot be extracted")
204 204
205 205 url_parts = urllib.parse.urlparse(first_response['url'])
206 206
207 207 self.server.repo_name = self._match_repo_name(url_parts.path.strip('/'))
208 208
209 209 exit_code = self.server._check_permissions(action)
210 210 if exit_code:
211 211 return exit_code
212 212
213 213 # set the readonly flag to False if we have proper permissions
214 214 if self.server.has_write_perm():
215 215 self.read_only = False
216 216 self.server.update_environment(action=action, extras=extras)
217 217
218 218 self.patch_first_client_response(first_response)
219 219 self.sync()
220 220 return self.return_code
221 221
222 222
223 223 class SubversionServer(VcsServer):
224 224 backend = 'svn'
225 225 repo_user_agent = 'svn'
226 226
227 227 def __init__(self, store, ini_path, repo_name,
228 228 user, user_permissions, config, env):
229 229 super(SubversionServer, self)\
230 230 .__init__(user, user_permissions, config, env)
231 231 self.store = store
232 232 self.ini_path = ini_path
233 233 # NOTE(dan): repo_name at this point is empty,
234 234 # this is set later in .run() based from parsed input stream
235 235 self.repo_name = repo_name
236 236 self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn')
237 237
238 238 self.tunnel = SubversionTunnelWrapper(server=self)
239 239
240 240 def _handle_tunnel(self, extras):
241 241
242 242 # pre-auth
243 243 action = 'pull'
244 244 # Special case for SVN, we extract repo name at later stage
245 245 # exit_code = self._check_permissions(action)
246 246 # if exit_code:
247 247 # return exit_code, False
248 248
249 249 req = self.env['request']
250 250 server_url = req.host_url + req.script_name
251 251 extras['server_url'] = server_url
252 252
253 253 log.debug('Using %s binaries from path %s', self.backend, self._path)
254 254 exit_code = self.tunnel.run(extras)
255 255
256 256 return exit_code, action == "push"
257 257
258 258
@@ -1,49 +1,48 b''
1 # -*- coding: utf-8 -*-
2 1
3 2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 3 #
5 4 # This program is free software: you can redistribute it and/or modify
6 5 # it under the terms of the GNU Affero General Public License, version 3
7 6 # (only), as published by the Free Software Foundation.
8 7 #
9 8 # This program is distributed in the hope that it will be useful,
10 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 11 # GNU General Public License for more details.
13 12 #
14 13 # You should have received a copy of the GNU Affero General Public License
15 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 15 #
17 16 # This program is dual-licensed. If you wish to learn more about the
18 17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 19
21 20 """
22 21 Base module for form rendering / validation - currently just a wrapper for
23 22 deform - later can be replaced with something custom.
24 23 """
25 24
26 25 from rhodecode.translation import _
27 26 from rhodecode.translation import TranslationString
28 27
29 28 from mako.template import Template
30 29 from deform import Button, Form, widget, ValidationFailure
31 30
32 31
33 32 class buttons:
34 33 save = Button(name='Save', type='submit')
35 34 reset = Button(name=_('Reset'), type='reset')
36 35 delete = Button(name=_('Delete'), type='submit')
37 36
38 37
39 38 class RcForm(Form):
40 39 def render_error(self, request, field):
41 40 html = ''
42 41 if field.error:
43 42 for err in field.error.messages():
44 43 if isinstance(err, TranslationString):
45 44 err = request.translate(err)
46 45 html = Template(
47 46 '<span class="error-message">${err}</span>').render(err=err)
48 47
49 48 return html
@@ -1,305 +1,305 b''
1 1
2 2
3 3 # Copyright (C) 2017-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from rhodecode.lib.jsonalchemy import JsonRaw
25 25 from rhodecode.model import meta
26 26 from rhodecode.model.db import User, UserLog, Repository
27 from rhodecode.lib.str_utils import safe_str
27 28
28 29
29 30 log = logging.getLogger(__name__)
30 31
31 32 # action as key, and expected action_data as value
32 33 ACTIONS_V1 = {
33 34 'user.login.success': {'user_agent': ''},
34 35 'user.login.failure': {'user_agent': ''},
35 36 'user.logout': {'user_agent': ''},
36 37 'user.register': {},
37 38 'user.password.reset_request': {},
38 39 'user.push': {'user_agent': '', 'commit_ids': []},
39 40 'user.pull': {'user_agent': ''},
40 41
41 42 'user.create': {'data': {}},
42 43 'user.delete': {'old_data': {}},
43 44 'user.edit': {'old_data': {}},
44 45 'user.edit.permissions': {},
45 46 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 47 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 48 'user.edit.token.add': {'token': {}, 'user': {}},
48 49 'user.edit.token.delete': {'token': {}, 'user': {}},
49 50 'user.edit.email.add': {'email': ''},
50 51 'user.edit.email.delete': {'email': ''},
51 52 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 53 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 54 'user.edit.password_reset.enabled': {},
54 55 'user.edit.password_reset.disabled': {},
55 56
56 57 'user_group.create': {'data': {}},
57 58 'user_group.delete': {'old_data': {}},
58 59 'user_group.edit': {'old_data': {}},
59 60 'user_group.edit.permissions': {},
60 61 'user_group.edit.member.add': {'user': {}},
61 62 'user_group.edit.member.delete': {'user': {}},
62 63
63 64 'repo.create': {'data': {}},
64 65 'repo.fork': {'data': {}},
65 66 'repo.edit': {'old_data': {}},
66 67 'repo.edit.permissions': {},
67 68 'repo.edit.permissions.branch': {},
68 69 'repo.archive': {'old_data': {}},
69 70 'repo.delete': {'old_data': {}},
70 71
71 72 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 73 'archive_spec': '', 'archive_cached': ''},
73 74
74 75 'repo.permissions.branch_rule.create': {},
75 76 'repo.permissions.branch_rule.edit': {},
76 77 'repo.permissions.branch_rule.delete': {},
77 78
78 79 'repo.pull_request.create': '',
79 80 'repo.pull_request.edit': '',
80 81 'repo.pull_request.delete': '',
81 82 'repo.pull_request.close': '',
82 83 'repo.pull_request.merge': '',
83 84 'repo.pull_request.vote': '',
84 85 'repo.pull_request.comment.create': '',
85 86 'repo.pull_request.comment.edit': '',
86 87 'repo.pull_request.comment.delete': '',
87 88
88 89 'repo.pull_request.reviewer.add': '',
89 90 'repo.pull_request.reviewer.delete': '',
90 91
91 92 'repo.pull_request.observer.add': '',
92 93 'repo.pull_request.observer.delete': '',
93 94
94 95 'repo.commit.strip': {'commit_id': ''},
95 96 'repo.commit.comment.create': {'data': {}},
96 97 'repo.commit.comment.delete': {'data': {}},
97 98 'repo.commit.comment.edit': {'data': {}},
98 99 'repo.commit.vote': '',
99 100
100 101 'repo.artifact.add': '',
101 102 'repo.artifact.delete': '',
102 103
103 104 'repo_group.create': {'data': {}},
104 105 'repo_group.edit': {'old_data': {}},
105 106 'repo_group.edit.permissions': {},
106 107 'repo_group.delete': {'old_data': {}},
107 108 }
108 109
109 110 ACTIONS = ACTIONS_V1
110 111
111 112 SOURCE_WEB = 'source_web'
112 113 SOURCE_API = 'source_api'
113 114
114 115
115 116 class UserWrap(object):
116 117 """
117 118 Fake object used to imitate AuthUser
118 119 """
119 120
120 121 def __init__(self, user_id=None, username=None, ip_addr=None):
121 122 self.user_id = user_id
122 123 self.username = username
123 124 self.ip_addr = ip_addr
124 125
125 126
126 127 class RepoWrap(object):
127 128 """
128 129 Fake object used to imitate RepoObject that audit logger requires
129 130 """
130 131
131 132 def __init__(self, repo_id=None, repo_name=None):
132 133 self.repo_id = repo_id
133 134 self.repo_name = repo_name
134 135
135 136
136 137 def _store_log(action_name, action_data, user_id, username, user_data,
137 138 ip_address, repository_id, repository_name):
138 139 user_log = UserLog()
139 140 user_log.version = UserLog.VERSION_2
140 141
141 142 user_log.action = action_name
142 143 user_log.action_data = action_data or JsonRaw('{}')
143 144
144 145 user_log.user_ip = ip_address
145 146
146 147 user_log.user_id = user_id
147 148 user_log.username = username
148 149 user_log.user_data = user_data or JsonRaw('{}')
149 150
150 151 user_log.repository_id = repository_id
151 152 user_log.repository_name = repository_name
152 153
153 154 user_log.action_date = datetime.datetime.now()
154 155
155 156 return user_log
156 157
157 158
158 159 def store_web(*args, **kwargs):
159 160 action_data = {}
160 161 org_action_data = kwargs.pop('action_data', {})
161 162 action_data.update(org_action_data)
162 163 action_data['source'] = SOURCE_WEB
163 164 kwargs['action_data'] = action_data
164 165
165 166 return store(*args, **kwargs)
166 167
167 168
168 169 def store_api(*args, **kwargs):
169 170 action_data = {}
170 171 org_action_data = kwargs.pop('action_data', {})
171 172 action_data.update(org_action_data)
172 173 action_data['source'] = SOURCE_API
173 174 kwargs['action_data'] = action_data
174 175
175 176 return store(*args, **kwargs)
176 177
177 178
178 179 def store(action, user, action_data=None, user_data=None, ip_addr=None,
179 180 repo=None, sa_session=None, commit=False):
180 181 """
181 182 Audit logger for various actions made by users, typically this
182 183 results in a call such::
183 184
184 185 from rhodecode.lib import audit_logger
185 186
186 187 audit_logger.store(
187 188 'repo.edit', user=self._rhodecode_user)
188 189 audit_logger.store(
189 190 'repo.delete', action_data={'data': repo_data},
190 191 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
191 192
192 193 # repo action
193 194 audit_logger.store(
194 195 'repo.delete',
195 196 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
196 197 repo=audit_logger.RepoWrap(repo_name='some-repo'))
197 198
198 199 # repo action, when we know and have the repository object already
199 200 audit_logger.store(
200 201 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
201 202 user=self._rhodecode_user,
202 203 repo=repo_object)
203 204
204 205 # alternative wrapper to the above
205 206 audit_logger.store_web(
206 207 'repo.delete', action_data={},
207 208 user=self._rhodecode_user,
208 209 repo=repo_object)
209 210
210 211 # without an user ?
211 212 audit_logger.store(
212 213 'user.login.failure',
213 214 user=audit_logger.UserWrap(
214 215 username=self.request.params.get('username'),
215 216 ip_addr=self.request.remote_addr))
216 217
217 218 """
218 from rhodecode.lib.utils2 import safe_unicode
219 219 from rhodecode.lib.auth import AuthUser
220 220
221 221 action_spec = ACTIONS.get(action, None)
222 222 if action_spec is None:
223 223 raise ValueError('Action `{}` is not supported'.format(action))
224 224
225 225 if not sa_session:
226 226 sa_session = meta.Session()
227 227
228 228 try:
229 229 username = getattr(user, 'username', None)
230 230 if not username:
231 231 pass
232 232
233 233 user_id = getattr(user, 'user_id', None)
234 234 if not user_id:
235 235 # maybe we have username ? Try to figure user_id from username
236 236 if username:
237 237 user_id = getattr(
238 238 User.get_by_username(username), 'user_id', None)
239 239
240 240 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
241 241 if not ip_addr:
242 242 pass
243 243
244 244 if not user_data:
245 245 # try to get this from the auth user
246 246 if isinstance(user, AuthUser):
247 247 user_data = {
248 248 'username': user.username,
249 249 'email': user.email,
250 250 }
251 251
252 252 repository_name = getattr(repo, 'repo_name', None)
253 253 repository_id = getattr(repo, 'repo_id', None)
254 254 if not repository_id:
255 255 # maybe we have repo_name ? Try to figure repo_id from repo_name
256 256 if repository_name:
257 257 repository_id = getattr(
258 258 Repository.get_by_repo_name(repository_name), 'repo_id', None)
259 259
260 action_name = safe_unicode(action)
261 ip_address = safe_unicode(ip_addr)
260 action_name = safe_str(action)
261 ip_address = safe_str(ip_addr)
262 262
263 263 with sa_session.no_autoflush:
264 264
265 265 user_log = _store_log(
266 266 action_name=action_name,
267 267 action_data=action_data or {},
268 268 user_id=user_id,
269 269 username=username,
270 270 user_data=user_data or {},
271 271 ip_address=ip_address,
272 272 repository_id=repository_id,
273 273 repository_name=repository_name
274 274 )
275 275
276 276 sa_session.add(user_log)
277 277 if commit:
278 278 sa_session.commit()
279 279 entry_id = user_log.entry_id or ''
280 280
281 281 update_user_last_activity(sa_session, user_id)
282 282
283 283 if commit:
284 284 sa_session.commit()
285 285
286 286 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
287 287 entry_id, action_name, user_id, username, ip_address,
288 288 extra={"entry_id": entry_id, "action": action_name,
289 289 "user_id": user_id, "ip": ip_address})
290 290
291 291 except Exception:
292 292 log.exception('AUDIT: failed to store audit log')
293 293
294 294
295 295 def update_user_last_activity(sa_session, user_id):
296 296 _last_activity = datetime.datetime.now()
297 297 try:
298 298 sa_session.query(User).filter(User.user_id == user_id).update(
299 299 {"last_activity": _last_activity})
300 300 log.debug(
301 301 'updated user `%s` last activity to:%s', user_id, _last_activity)
302 302 except Exception:
303 303 log.exception("Failed last activity update for user_id: %s", user_id)
304 304 sa_session.rollback()
305 305
@@ -1,140 +1,140 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 import os
21 import json
22 20 import logging
23 21 import datetime
24 22 import time
25 23
26 24 from functools import partial
27 25
28 26 import configparser
29 27 from celery.result import AsyncResult
30 28 import celery.loaders.base
31 29 import celery.schedules
32 30
31 from rhodecode.lib.ext_json import sjson as json
32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 def get_task_id(task):
37 37 task_id = None
38 38 if isinstance(task, AsyncResult):
39 39 task_id = task.task_id
40 40
41 41 return task_id
42 42
43 43
44 44 def crontab(value):
45 45 return celery.schedules.crontab(**value)
46 46
47 47
48 48 def timedelta(value):
49 49 return datetime.timedelta(**value)
50 50
51 51
52 52 def safe_json(get, section, key):
53 53 value = ''
54 54 try:
55 55 value = get(key)
56 56 json_value = json.loads(value)
57 57 except ValueError:
58 58 msg = 'The %s=%s is not valid json in section %s' % (
59 59 key, value, section
60 60 )
61 61 raise ValueError(msg)
62 62
63 63 return json_value
64 64
65 65
66 66 def raw_2_schedule(schedule_value, schedule_type):
67 67 schedule_type_map = {
68 68 'crontab': crontab,
69 69 'timedelta': timedelta,
70 70 'integer': int
71 71 }
72 72 scheduler_cls = schedule_type_map.get(schedule_type)
73 73
74 74 if scheduler_cls is None:
75 75 raise ValueError(
76 76 'schedule type %s in section is invalid' % (
77 77 schedule_type,
78 78 )
79 79 )
80 80 try:
81 81 schedule = scheduler_cls(schedule_value)
82 82 except TypeError:
83 83 log.exception('Failed to compose a schedule from value: %r', schedule_value)
84 84 schedule = None
85 85 return schedule
86 86
87 87
88 88 def get_beat_config(parser, section):
89 89
90 90 get = partial(parser.get, section)
91 91 has_option = partial(parser.has_option, section)
92 92
93 93 schedule_type = get('type')
94 94 schedule_value = safe_json(get, section, 'schedule')
95 95
96 96 config = {
97 97 'schedule_type': schedule_type,
98 98 'schedule_value': schedule_value,
99 99 'task': get('task'),
100 100 }
101 101 schedule = raw_2_schedule(schedule_value, schedule_type)
102 102 if schedule:
103 103 config['schedule'] = schedule
104 104
105 105 if has_option('args'):
106 106 config['args'] = safe_json(get, section, 'args')
107 107
108 108 if has_option('kwargs'):
109 109 config['kwargs'] = safe_json(get, section, 'kwargs')
110 110
111 111 if has_option('force_update'):
112 112 config['force_update'] = get('force_update')
113 113
114 114 return config
115 115
116 116
117 117 def parse_ini_vars(ini_vars):
118 118 options = {}
119 119 for pairs in ini_vars.split(','):
120 120 key, value = pairs.split('=')
121 121 options[key] = value
122 122 return options
123 123
124 124
125 125 def ping_db():
126 126 from rhodecode.model import meta
127 127 from rhodecode.model.db import DbMigrateVersion
128 128 log.info('Testing DB connection...')
129 129
130 130 for test in range(10):
131 131 try:
132 132 scalar = DbMigrateVersion.query().scalar()
133 133 log.debug('DB PING %s@%s', scalar, scalar.version)
134 134 break
135 135 except Exception:
136 136 retry = 1
137 137 log.debug('DB not ready, next try in %ss', retry)
138 138 time.sleep(retry)
139 139 finally:
140 140 meta.Session.remove()
@@ -1,78 +1,78 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 """
21 21 Provides utilities around date and time handling
22 22 """
23 23
24 24 import datetime
25 25 import time
26 26
27 27
28 28 def makedate():
29 29 lt = time.localtime()
30 30 if lt[8] == 1 and time.daylight:
31 31 tz = time.altzone
32 32 else:
33 33 tz = time.timezone
34 34 return time.mktime(lt), tz
35 35
36 36
37 37 def utcdate_fromtimestamp(unixts, tzoffset=0):
38 38 """
39 39 Makes a local datetime object out of unix timestamp
40 40
41 41 :param unixts:
42 42 :param tzoffset:
43 43 """
44 44
45 45 return datetime.datetime.utcfromtimestamp(float(unixts))
46 46
47 47
48 48 def date_astimestamp(value):
49 49 """
50 50 Convert a given `datetime.datetime` into a `float` like `time.time`
51 51 """
52 52 return time.mktime(value.timetuple()) + value.microsecond / 1E6
53 53
54 54
55 55 def date_to_timestamp_plus_offset(value):
56 56 """
57 57 Convert a given `datetime.datetime` into a unix timestamp and offset.
58 58 """
59 59 # TODO: johbo: The time handling looks quite fragile here since we mix
60 60 # system time zones with naive datetime instances.
61 61 if value is None:
62 62 value = time.time()
63 63 elif isinstance(value, datetime.datetime):
64 64 assert not is_aware(value), (
65 65 "This code is not prepared to handle aware datetime instances")
66 66 value = date_astimestamp(value)
67 return (value, time.timezone)
67 return value, time.timezone
68 68
69 69
70 70 def is_aware(value):
71 71 """
72 72 Determines if a given datetime.time is aware.
73 73
74 74 The logic is described in Python's docs:
75 75 http://docs.python.org/library/datetime.html#datetime.tzinfo
76 76 """
77 77 return (value.tzinfo is not None
78 78 and value.tzinfo.utcoffset(value) is not None)
@@ -1,57 +1,57 b''
1 1 """
2 2 Utilities for XML generation/parsing.
3 3 """
4 4
5 5 import six
6 6
7 7 from xml.sax.saxutils import XMLGenerator, quoteattr
8 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 12 class SimplerXMLGenerator(XMLGenerator):
13 13 def addQuickElement(self, name, contents=None, attrs=None):
14 14 "Convenience method for adding an element with no children"
15 15 if attrs is None:
16 16 attrs = {}
17 17 self.startElement(name, attrs)
18 18 if contents is not None:
19 19 self.characters(contents)
20 20 self.endElement(name)
21 21
22 22 def startElement(self, name, attrs):
23 23 self._write('<' + name)
24 24 # sort attributes for consistent output
25 25 for (name, value) in sorted(attrs.items()):
26 26 self._write(' %s=%s' % (name, quoteattr(value)))
27 27 self._write(six.u('>'))
28 28
29 29
30 30 def iri_to_uri(iri):
31 31 """
32 32 Convert an Internationalized Resource Identifier (IRI) portion to a URI
33 33 portion that is suitable for inclusion in a URL.
34 34 This is the algorithm from section 3.1 of RFC 3987. However, since we are
35 35 assuming input is either UTF-8 or unicode already, we can simplify things a
36 36 little from the full method.
37 37 Returns an ASCII string containing the encoded result.
38 38 """
39 39 # The list of safe characters here is constructed from the "reserved" and
40 40 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
41 41 # reserved = gen-delims / sub-delims
42 42 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
43 43 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
44 44 # / "*" / "+" / "," / ";" / "="
45 45 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
46 46 # Of the unreserved characters, urllib.quote already considers all but
47 47 # the ~ safe.
48 48 # The % character is also added to the list of safe characters here, as the
49 49 # end of section 3.1 of RFC 3987 specifically mentions that % must not be
50 50 # converted.
51 51 if iri is None:
52 52 return iri
53 53 return quote(safe_str(iri), safe=b"/#%[]=:;$&()+,!?*@'~")
54 54
55 55
56 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 3 # Copyright (C) 2020-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 # base64 filter e.g ${ example | base64,n }
22 22 def base64(text):
23 import base64
24 from rhodecode.lib.helpers import safe_str, safe_bytes
25 return safe_str(base64.encodebytes(safe_bytes(text)))
23 from rhodecode.lib.str_utils import base64_to_str
24 return base64_to_str(text)
25
@@ -1,311 +1,311 b''
1 1
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Index schema for RhodeCode
23 23 """
24 24
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29
30 30 from whoosh import query as query_lib
31 31 from whoosh.highlight import HtmlFormatter, ContextFragmenter
32 32 from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
33 33 from whoosh.qparser import QueryParser, QueryParserError
34 34
35 35 import rhodecode.lib.helpers as h
36 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 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 try:
43 43 # we first try to import from rhodecode tools, fallback to copies if
44 44 # we're unable to
45 45 from rhodecode_tools.lib.fts_index.whoosh_schema import (
46 46 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
47 47 COMMIT_SCHEMA)
48 48 except ImportError:
49 49 log.warning('rhodecode_tools schema not available, doing a fallback '
50 50 'import from `rhodecode.lib.index.whoosh_fallback_schema`')
51 51 from rhodecode.lib.index.whoosh_fallback_schema import (
52 52 ANALYZER, FILE_INDEX_NAME, FILE_SCHEMA, COMMIT_INDEX_NAME,
53 53 COMMIT_SCHEMA)
54 54
55 55
56 56 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
57 57 FRAGMENTER = ContextFragmenter(200)
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class WhooshSearcher(BaseSearcher):
63 63 # this also shows in UI
64 64 query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html'
65 65 name = 'whoosh'
66 66
67 67 def __init__(self, config):
68 68 super(Searcher, self).__init__()
69 69 self.config = config
70 70 if not os.path.isdir(self.config['location']):
71 71 os.makedirs(self.config['location'])
72 72
73 73 opener = create_in
74 74 if exists_in(self.config['location'], indexname=FILE_INDEX_NAME):
75 75 opener = open_dir
76 76 file_index = opener(self.config['location'], schema=FILE_SCHEMA,
77 77 indexname=FILE_INDEX_NAME)
78 78
79 79 opener = create_in
80 80 if exists_in(self.config['location'], indexname=COMMIT_INDEX_NAME):
81 81 opener = open_dir
82 82 changeset_index = opener(self.config['location'], schema=COMMIT_SCHEMA,
83 83 indexname=COMMIT_INDEX_NAME)
84 84
85 85 self.commit_schema = COMMIT_SCHEMA
86 86 self.commit_index = changeset_index
87 87 self.file_schema = FILE_SCHEMA
88 88 self.file_index = file_index
89 89 self.searcher = None
90 90
91 91 def cleanup(self):
92 92 if self.searcher:
93 93 self.searcher.close()
94 94
95 95 def _extend_query(self, query):
96 96 hashes = re.compile('([0-9a-f]{5,40})').findall(query)
97 97 if hashes:
98 98 hashes_or_query = ' OR '.join('commit_id:%s*' % h for h in hashes)
99 99 query = u'(%s) OR %s' % (query, hashes_or_query)
100 100 return query
101 101
102 102 def sort_def(self, search_type, direction, sort_field):
103 103
104 104 if search_type == 'commit':
105 105 field_defs = {
106 106 'message': 'message',
107 107 'date': 'date',
108 108 'author_email': 'author',
109 109 }
110 110 elif search_type == 'path':
111 111 field_defs = {
112 112 'file': 'path',
113 113 'size': 'size',
114 114 'lines': 'lines',
115 115 }
116 116 elif search_type == 'content':
117 117 # NOTE(dan): content doesn't support any sorting
118 118 field_defs = {}
119 119 else:
120 120 return ''
121 121
122 122 if sort_field in field_defs:
123 123 return field_defs[sort_field]
124 124
125 125 def search(self, query, document_type, search_user,
126 126 repo_name=None, repo_group_name=None,
127 127 requested_page=1, page_limit=10, sort=None, raise_on_exc=True):
128 128
129 129 original_query = query
130 130 query = self._extend_query(query)
131 131
132 132 log.debug(u'QUERY: %s on %s', query, document_type)
133 133 result = {
134 134 'results': [],
135 135 'count': 0,
136 136 'error': None,
137 137 'runtime': 0
138 138 }
139 139 search_type, index_name, schema_defn = self._prepare_for_search(
140 140 document_type)
141 141 self._init_searcher(index_name)
142 142 try:
143 143 qp = QueryParser(search_type, schema=schema_defn)
144 144 allowed_repos_filter = self._get_repo_filter(
145 145 search_user, repo_name)
146 146 try:
147 query = qp.parse(safe_unicode(query))
147 query = qp.parse(safe_str(query))
148 148 log.debug('query: %s (%s)', query, repr(query))
149 149
150 150 reverse, sorted_by = False, None
151 151 direction, sort_field = self.get_sort(search_type, sort)
152 152 if sort_field:
153 153 sort_definition = self.sort_def(search_type, direction, sort_field)
154 154 if sort_definition:
155 155 sorted_by = sort_definition
156 156 if direction == Searcher.DIRECTION_DESC:
157 157 reverse = True
158 158 if direction == Searcher.DIRECTION_ASC:
159 159 reverse = False
160 160
161 161 whoosh_results = self.searcher.search(
162 162 query, filter=allowed_repos_filter, limit=None,
163 163 sortedby=sorted_by, reverse=reverse)
164 164
165 165 # fixes for 32k limit that whoosh uses for highlight
166 166 whoosh_results.fragmenter.charlimit = None
167 167 res_ln = whoosh_results.scored_length()
168 168 result['runtime'] = whoosh_results.runtime
169 169 result['count'] = res_ln
170 170 result['results'] = WhooshResultWrapper(
171 171 search_type, res_ln, whoosh_results)
172 172
173 173 except QueryParserError:
174 174 result['error'] = 'Invalid search query. Try quoting it.'
175 175 except (EmptyIndexError, IOError, OSError):
176 176 msg = 'There is no index to search in. Please run whoosh indexer'
177 177 log.exception(msg)
178 178 result['error'] = msg
179 179 except Exception:
180 180 msg = 'An error occurred during this search operation'
181 181 log.exception(msg)
182 182 result['error'] = msg
183 183
184 184 return result
185 185
186 186 def statistics(self, translator):
187 187 _ = translator
188 188 stats = [
189 189 {'key': _('Index Type'), 'value': 'Whoosh'},
190 190 {'sep': True},
191 191
192 192 {'key': _('File Index'), 'value': str(self.file_index)},
193 193 {'key': _('Indexed documents'), 'value': self.file_index.doc_count()},
194 194 {'key': _('Last update'), 'value': h.time_to_datetime(self.file_index.last_modified())},
195 195
196 196 {'sep': True},
197 197
198 198 {'key': _('Commit index'), 'value': str(self.commit_index)},
199 199 {'key': _('Indexed documents'), 'value': str(self.commit_index.doc_count())},
200 200 {'key': _('Last update'), 'value': h.time_to_datetime(self.commit_index.last_modified())}
201 201 ]
202 202 return stats
203 203
204 204 def _get_repo_filter(self, auth_user, repo_name):
205 205
206 206 allowed_to_search = [
207 207 repo for repo, perm in
208 208 auth_user.permissions['repositories'].items()
209 209 if perm != 'repository.none']
210 210
211 211 if repo_name:
212 212 repo_filter = [query_lib.Term('repository', repo_name)]
213 213
214 214 elif 'hg.admin' in auth_user.permissions.get('global', []):
215 215 return None
216 216
217 217 else:
218 218 repo_filter = [query_lib.Term('repository', _rn)
219 219 for _rn in allowed_to_search]
220 220 # in case we're not allowed to search anywhere, it's a trick
221 221 # to tell whoosh we're filtering, on ALL results
222 222 repo_filter = repo_filter or [query_lib.Term('repository', '')]
223 223
224 224 return query_lib.Or(repo_filter)
225 225
226 226 def _prepare_for_search(self, cur_type):
227 227 search_type = {
228 228 'content': 'content',
229 229 'commit': 'message',
230 230 'path': 'path',
231 231 'repository': 'repository'
232 232 }.get(cur_type, 'content')
233 233
234 234 index_name = {
235 235 'content': FILE_INDEX_NAME,
236 236 'commit': COMMIT_INDEX_NAME,
237 237 'path': FILE_INDEX_NAME
238 238 }.get(cur_type, FILE_INDEX_NAME)
239 239
240 240 schema_defn = {
241 241 'content': self.file_schema,
242 242 'commit': self.commit_schema,
243 243 'path': self.file_schema
244 244 }.get(cur_type, self.file_schema)
245 245
246 246 log.debug('IDX: %s', index_name)
247 247 log.debug('SCHEMA: %s', schema_defn)
248 248 return search_type, index_name, schema_defn
249 249
250 250 def _init_searcher(self, index_name):
251 251 idx = open_dir(self.config['location'], indexname=index_name)
252 252 self.searcher = idx.searcher()
253 253 return self.searcher
254 254
255 255
256 256 Searcher = WhooshSearcher
257 257
258 258
259 259 class WhooshResultWrapper(object):
260 260 def __init__(self, search_type, total_hits, results):
261 261 self.search_type = search_type
262 262 self.results = results
263 263 self.total_hits = total_hits
264 264
265 265 def __str__(self):
266 266 return '<%s at %s>' % (self.__class__.__name__, len(self))
267 267
268 268 def __repr__(self):
269 269 return self.__str__()
270 270
271 271 def __len__(self):
272 272 return self.total_hits
273 273
274 274 def __iter__(self):
275 275 """
276 276 Allows Iteration over results,and lazy generate content
277 277
278 278 *Requires* implementation of ``__getitem__`` method.
279 279 """
280 280 for hit in self.results:
281 281 yield self.get_full_content(hit)
282 282
283 283 def __getitem__(self, key):
284 284 """
285 285 Slicing of resultWrapper
286 286 """
287 287 i, j = key.start, key.stop
288 288 for hit in self.results[i:j]:
289 289 yield self.get_full_content(hit)
290 290
291 291 def get_full_content(self, hit):
292 292 # TODO: marcink: this feels like an overkill, there's a lot of data
293 293 # inside hit object, and we don't need all
294 294 res = dict(hit)
295 295 # elastic search uses that, we set it empty so it fallbacks to regular HL logic
296 296 res['content_highlight'] = ''
297 297
298 298 f_path = '' # pragma: no cover
299 299 if self.search_type in ['content', 'path']:
300 300 f_path = res['path'][len(res['repository']):]
301 301 f_path = f_path.lstrip(os.sep)
302 302
303 303 if self.search_type == 'content':
304 304 res.update({'content_short_hl': hit.highlights('content'),
305 305 'f_path': f_path})
306 306 elif self.search_type == 'path':
307 307 res.update({'f_path': f_path})
308 308 elif self.search_type == 'message':
309 309 res.update({'message_hl': hit.highlights('message')})
310 310
311 311 return res
@@ -1,230 +1,232 b''
1 1
2 2 # Copyright (C) 2010-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import base64
21 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 import urllib.parse
24 26
25 27 import requests
26 28 from pyramid.httpexceptions import HTTPNotAcceptable
27 29
28 30 from rhodecode.lib import rc_cache
29 31 from rhodecode.lib.middleware import simplevcs
30 32 from rhodecode.lib.middleware.utils import get_path_info
31 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 36 from rhodecode.lib.ext_json import json
34 37 from rhodecode.lib.hooks_daemon import store_txn_id_data
35 38
36 39
37 40 log = logging.getLogger(__name__)
38 41
39 42
40 43 class SimpleSvnApp(object):
41 44 IGNORED_HEADERS = [
42 45 'connection', 'keep-alive', 'content-encoding',
43 46 'transfer-encoding', 'content-length']
44 47 rc_extras = {}
45 48
46 49 def __init__(self, config):
47 50 self.config = config
48 51
49 52 def __call__(self, environ, start_response):
50 53 request_headers = self._get_request_headers(environ)
51 54 data = environ['wsgi.input']
52 55 req_method = environ['REQUEST_METHOD']
53 56 has_content_length = 'CONTENT_LENGTH' in environ
54 57
55 58 path_info = self._get_url(
56 59 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
57 60 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
58 61 log.debug('Handling: %s method via `%s`', req_method, path_info)
59 62
60 63 # stream control flag, based on request and content type...
61 64 stream = False
62 65
63 66 if req_method in ['MKCOL'] or has_content_length:
64 67 data_processed = False
65 68 # read chunk to check if we have txn-with-props
66 69 initial_data = data.read(1024)
67 70 if initial_data.startswith('(create-txn-with-props'):
68 71 data = initial_data + data.read()
69 72 # store on-the-fly our rc_extra using svn revision properties
70 73 # those can be read later on in hooks executed so we have a way
71 74 # to pass in the data into svn hooks
72 75 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
73 76 rc_data_len = len(rc_data)
74 77 # header defines data length, and serialized data
75 78 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
76 79 data = data[:-2] + skel + '))'
77 80 data_processed = True
78 81
79 82 if not data_processed:
80 83 # NOTE(johbo): Avoid that we end up with sending the request in chunked
81 84 # transfer encoding (mainly on Gunicorn). If we know the content
82 85 # length, then we should transfer the payload in one request.
83 86 data = initial_data + data.read()
84 87
85 88 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
86 89 # NOTE(marcink): when getting/uploading files we want to STREAM content
87 90 # back to the client/proxy instead of buffering it here...
88 91 stream = True
89 92
90 93 stream = stream
91 94 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
92 95 path_info, req_method, stream)
93 96 try:
94 97 response = requests.request(
95 98 req_method, path_info,
96 99 data=data, headers=request_headers, stream=stream)
97 100 except requests.ConnectionError:
98 101 log.exception('ConnectionError occurred for endpoint %s', path_info)
99 102 raise
100 103
101 104 if response.status_code not in [200, 401]:
102 from rhodecode.lib.utils2 import safe_str
103 105 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
104 106 if response.status_code >= 500:
105 107 log.error('Got SVN response:%s with text:`%s`', response, text)
106 108 else:
107 109 log.debug('Got SVN response:%s with text:`%s`', response, text)
108 110 else:
109 111 log.debug('got response code: %s', response.status_code)
110 112
111 113 response_headers = self._get_response_headers(response.headers)
112 114
113 115 if response.headers.get('SVN-Txn-name'):
114 116 svn_tx_id = response.headers.get('SVN-Txn-name')
115 117 txn_id = rc_cache.utils.compute_key_from_params(
116 118 self.config['repository'], svn_tx_id)
117 119 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
118 120 store_txn_id_data(txn_id, {'port': port})
119 121
120 122 start_response(
121 123 '{} {}'.format(response.status_code, response.reason),
122 124 response_headers)
123 125 return response.iter_content(chunk_size=1024)
124 126
125 127 def _get_url(self, svn_http_server, path):
126 128 svn_http_server_url = (svn_http_server or '').rstrip('/')
127 129 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
128 130 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
129 131 return url_path
130 132
131 133 def _get_request_headers(self, environ):
132 134 headers = {}
133 135
134 136 for key in environ:
135 137 if not key.startswith('HTTP_'):
136 138 continue
137 139 new_key = key.split('_')
138 140 new_key = [k.capitalize() for k in new_key[1:]]
139 141 new_key = '-'.join(new_key)
140 142 headers[new_key] = environ[key]
141 143
142 144 if 'CONTENT_TYPE' in environ:
143 145 headers['Content-Type'] = environ['CONTENT_TYPE']
144 146
145 147 if 'CONTENT_LENGTH' in environ:
146 148 headers['Content-Length'] = environ['CONTENT_LENGTH']
147 149
148 150 return headers
149 151
150 152 def _get_response_headers(self, headers):
151 153 headers = [
152 154 (h, headers[h])
153 155 for h in headers
154 156 if h.lower() not in self.IGNORED_HEADERS
155 157 ]
156 158
157 159 return headers
158 160
159 161
160 162 class DisabledSimpleSvnApp(object):
161 163 def __init__(self, config):
162 164 self.config = config
163 165
164 166 def __call__(self, environ, start_response):
165 167 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
166 168 log.warning(reason)
167 169 return HTTPNotAcceptable(reason)(environ, start_response)
168 170
169 171
170 172 class SimpleSvn(simplevcs.SimpleVCS):
171 173
172 174 SCM = 'svn'
173 175 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
174 176 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
175 177
176 178 def _get_repository_name(self, environ):
177 179 """
178 180 Gets repository name out of PATH_INFO header
179 181
180 182 :param environ: environ where PATH_INFO is stored
181 183 """
182 184 path = get_path_info(environ).split('!')
183 185 repo_name = path[0].strip('/')
184 186
185 187 # SVN includes the whole path in it's requests, including
186 188 # subdirectories inside the repo. Therefore we have to search for
187 189 # the repo root directory.
188 190 if not is_valid_repo(
189 191 repo_name, self.base_path, explicit_scm=self.SCM):
190 192 current_path = ''
191 193 for component in repo_name.split('/'):
192 194 current_path += component
193 195 if is_valid_repo(
194 196 current_path, self.base_path, explicit_scm=self.SCM):
195 197 return current_path
196 198 current_path += '/'
197 199
198 200 return repo_name
199 201
200 202 def _get_action(self, environ):
201 203 return (
202 204 'pull'
203 205 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
204 206 else 'push')
205 207
206 208 def _should_use_callback_daemon(self, extras, environ, action):
207 209 # only MERGE command triggers hooks, so we don't want to start
208 210 # hooks server too many times. POST however starts the svn transaction
209 211 # so we also need to run the init of callback daemon of POST
210 212 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
211 213 return True
212 214 return False
213 215
214 216 def _create_wsgi_app(self, repo_path, repo_name, config):
215 217 if self._is_svn_enabled():
216 218 return SimpleSvnApp(config)
217 219 # we don't have http proxy enabled return dummy request handler
218 220 return DisabledSimpleSvnApp(config)
219 221
220 222 def _is_svn_enabled(self):
221 223 conf = self.repo_vcs_config
222 224 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
223 225
224 226 def _create_config(self, extras, repo_name, scheme='http'):
225 227 conf = self.repo_vcs_config
226 228 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
227 229 server_url = server_url or self.DEFAULT_HTTP_SERVER
228 230
229 231 extras['subversion_http_server_url'] = server_url
230 232 return extras
@@ -1,683 +1,689 b''
1 1
2 2
3 3 # Copyright (C) 2014-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import io
29 29 import logging
30 30 import importlib
31 31 from functools import wraps
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 49 from rhodecode.lib.middleware import appenlight
50 50 from rhodecode.lib.middleware.utils import scm_app_http
51 51 from rhodecode.lib.str_utils import safe_bytes
52 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 54 from rhodecode.lib.vcs.conf import settings as vcs_settings
55 55 from rhodecode.lib.vcs.backends import base
56 56
57 57 from rhodecode.model import meta
58 58 from rhodecode.model.db import User, Repository, PullRequest
59 59 from rhodecode.model.scm import ScmModel
60 60 from rhodecode.model.pull_request import PullRequestModel
61 61 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 def extract_svn_txn_id(acl_repo_name, data):
67 67 """
68 68 Helper method for extraction of svn txn_id from submitted XML data during
69 69 POST operations
70 70 """
71 71 try:
72 72 root = etree.fromstring(data)
73 73 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 74 for el in root:
75 75 if el.tag == '{DAV:}source':
76 76 for sub_el in el:
77 77 if sub_el.tag == '{DAV:}href':
78 78 match = pat.search(sub_el.text)
79 79 if match:
80 80 svn_tx_id = match.groupdict()['txn_id']
81 81 txn_id = rc_cache.utils.compute_key_from_params(
82 82 acl_repo_name, svn_tx_id)
83 83 return txn_id
84 84 except Exception:
85 85 log.exception('Failed to extract txn_id')
86 86
87 87
88 88 def initialize_generator(factory):
89 89 """
90 90 Initializes the returned generator by draining its first element.
91 91
92 92 This can be used to give a generator an initializer, which is the code
93 93 up to the first yield statement. This decorator enforces that the first
94 94 produced element has the value ``"__init__"`` to make its special
95 95 purpose very explicit in the using code.
96 96 """
97 97
98 98 @wraps(factory)
99 99 def wrapper(*args, **kwargs):
100 100 gen = factory(*args, **kwargs)
101 101 try:
102 102 init = next(gen)
103 103 except StopIteration:
104 104 raise ValueError('Generator must yield at least one element.')
105 105 if init != "__init__":
106 106 raise ValueError('First yielded element must be "__init__".')
107 107 return gen
108 108 return wrapper
109 109
110 110
111 111 class SimpleVCS(object):
112 112 """Common functionality for SCM HTTP handlers."""
113 113
114 114 SCM = 'unknown'
115 115
116 116 acl_repo_name = None
117 117 url_repo_name = None
118 118 vcs_repo_name = None
119 119 rc_extras = {}
120 120
121 121 # We have to handle requests to shadow repositories different than requests
122 122 # to normal repositories. Therefore we have to distinguish them. To do this
123 123 # we use this regex which will match only on URLs pointing to shadow
124 124 # repositories.
125 125 shadow_repo_re = re.compile(
126 126 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
127 127 '(?P<target>{slug_pat})/' # target repo
128 128 'pull-request/(?P<pr_id>\\d+)/' # pull request
129 129 'repository$' # shadow repo
130 130 .format(slug_pat=SLUG_RE.pattern))
131 131
132 132 def __init__(self, config, registry):
133 133 self.registry = registry
134 134 self.config = config
135 135 # re-populated by specialized middleware
136 136 self.repo_vcs_config = base.Config()
137 137
138 138 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
139 139 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
140 140
141 141 # authenticate this VCS request using authfunc
142 142 auth_ret_code_detection = \
143 143 str2bool(self.config.get('auth_ret_code_detection', False))
144 144 self.authenticate = BasicAuth(
145 145 '', authenticate, registry, config.get('auth_ret_code'),
146 146 auth_ret_code_detection, rc_realm=realm)
147 147 self.ip_addr = '0.0.0.0'
148 148
149 149 @LazyProperty
150 150 def global_vcs_config(self):
151 151 try:
152 152 return VcsSettingsModel().get_ui_settings_as_config_obj()
153 153 except Exception:
154 154 return base.Config()
155 155
156 156 @property
157 157 def base_path(self):
158 158 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159 159
160 160 if not settings_path:
161 161 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162 162
163 163 if not settings_path:
164 164 # try, maybe we passed in explicitly as config option
165 165 settings_path = self.config.get('base_path')
166 166
167 167 if not settings_path:
168 168 raise ValueError('FATAL: base_path is empty')
169 169 return settings_path
170 170
171 171 def set_repo_names(self, environ):
172 172 """
173 173 This will populate the attributes acl_repo_name, url_repo_name,
174 174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 175 shadow) repositories all names are equal. In case of requests to a
176 176 shadow repository the acl-name points to the target repo of the pull
177 177 request and the vcs-name points to the shadow repo file system path.
178 178 The url-name is always the URL used by the vcs client program.
179 179
180 180 Example in case of a shadow repo:
181 181 acl_repo_name = RepoGroup/MyRepo
182 182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 184 """
185 185 # First we set the repo name from URL for all attributes. This is the
186 186 # default if handling normal (non shadow) repo requests.
187 187 self.url_repo_name = self._get_repository_name(environ)
188 188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 189 self.is_shadow_repo = False
190 190
191 191 # Check if this is a request to a shadow repository.
192 192 match = self.shadow_repo_re.match(self.url_repo_name)
193 193 if match:
194 194 match_dict = match.groupdict()
195 195
196 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 198 groups=match_dict['groups'] or '',
199 199 target=match_dict['target']))
200 200
201 201 # Retrieve pull request instance by ID from regex match.
202 202 pull_request = PullRequest.get(match_dict['pr_id'])
203 203
204 204 # Only proceed if we got a pull request and if acl repo name from
205 205 # URL equals the target repo name of the pull request.
206 206 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
207 207
208 208 # Get file system path to shadow repository.
209 209 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 210 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
211 211
212 212 # Store names for later usage.
213 213 self.vcs_repo_name = vcs_repo_name
214 214 self.acl_repo_name = acl_repo_name
215 215 self.is_shadow_repo = True
216 216
217 217 log.debug('Setting all VCS repository names: %s', {
218 218 'acl_repo_name': self.acl_repo_name,
219 219 'url_repo_name': self.url_repo_name,
220 220 'vcs_repo_name': self.vcs_repo_name,
221 221 })
222 222
223 223 @property
224 224 def scm_app(self):
225 225 custom_implementation = self.config['vcs.scm_app_implementation']
226 226 if custom_implementation == 'http':
227 227 log.debug('Using HTTP implementation of scm app.')
228 228 scm_app_impl = scm_app_http
229 229 else:
230 230 log.debug('Using custom implementation of scm_app: "{}"'.format(
231 231 custom_implementation))
232 232 scm_app_impl = importlib.import_module(custom_implementation)
233 233 return scm_app_impl
234 234
235 235 def _get_by_id(self, repo_name):
236 236 """
237 237 Gets a special pattern _<ID> from clone url and tries to replace it
238 238 with a repository_name for support of _<ID> non changeable urls
239 239 """
240 240
241 241 data = repo_name.split('/')
242 242 if len(data) >= 2:
243 243 from rhodecode.model.repo import RepoModel
244 244 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 245 if by_id_match:
246 246 data[1] = by_id_match.repo_name
247 247
248 248 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
249 249 # and we use this data
250 250 maybe_new_path = '/'.join(data)
251 251 return safe_bytes(maybe_new_path).decode('latin1')
252 252
253 253 def _invalidate_cache(self, repo_name):
254 254 """
255 255 Set's cache for this repository for invalidation on next access
256 256
257 257 :param repo_name: full repo name, also a cache key
258 258 """
259 259 ScmModel().mark_for_invalidation(repo_name)
260 260
261 261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 262 db_repo = Repository.get_by_repo_name(repo_name)
263 263 if not db_repo:
264 264 log.debug('Repository `%s` not found inside the database.',
265 265 repo_name)
266 266 return False
267 267
268 268 if db_repo.repo_type != scm_type:
269 269 log.warning(
270 270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 271 repo_name, db_repo.repo_type, scm_type)
272 272 return False
273 273
274 274 config = db_repo._config
275 275 config.set('extensions', 'largefiles', '')
276 276 return is_valid_repo(
277 277 repo_name, base_path,
278 278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 279
280 280 def valid_and_active_user(self, user):
281 281 """
282 282 Checks if that user is not empty, and if it's actually object it checks
283 283 if he's active.
284 284
285 285 :param user: user object or None
286 286 :return: boolean
287 287 """
288 288 if user is None:
289 289 return False
290 290
291 291 elif user.active:
292 292 return True
293 293
294 294 return False
295 295
296 296 @property
297 297 def is_shadow_repo_dir(self):
298 298 return os.path.isdir(self.vcs_repo_name)
299 299
300 300 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 302 """
303 303 Checks permissions using action (push/pull) user and repository
304 304 name. If plugin_cache and ttl is set it will use the plugin which
305 305 authenticated the user to store the cached permissions result for N
306 306 amount of seconds as in cache_ttl
307 307
308 308 :param action: push or pull action
309 309 :param user: user instance
310 310 :param repo_name: repository name
311 311 """
312 312
313 313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 314 plugin_id, plugin_cache_active, cache_ttl)
315 315
316 316 user_id = user.user_id
317 317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 319
320 320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 321 expiration_time=cache_ttl,
322 322 condition=plugin_cache_active)
323 323 def compute_perm_vcs(
324 324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 325
326 326 log.debug('auth: calculating permission access now...')
327 327 # check IP
328 328 inherit = user.inherit_default_permissions
329 329 ip_allowed = AuthUser.check_ip_allowed(
330 330 user_id, ip_addr, inherit_from_default=inherit)
331 331 if ip_allowed:
332 332 log.info('Access for IP:%s allowed', ip_addr)
333 333 else:
334 334 return False
335 335
336 336 if action == 'push':
337 337 perms = ('repository.write', 'repository.admin')
338 338 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 339 return False
340 340
341 341 else:
342 342 # any other action need at least read permission
343 343 perms = (
344 344 'repository.read', 'repository.write', 'repository.admin')
345 345 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 346 return False
347 347
348 348 return True
349 349
350 350 start = time.time()
351 351 log.debug('Running plugin `%s` permissions check', plugin_id)
352 352
353 353 # for environ based auth, password can be empty, but then the validation is
354 354 # on the server that fills in the env data needed for authentication
355 355 perm_result = compute_perm_vcs(
356 356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 357
358 358 auth_time = time.time() - start
359 359 log.debug('Permissions for plugin `%s` completed in %.4fs, '
360 360 'expiration time of fetched cache %.1fs.',
361 361 plugin_id, auth_time, cache_ttl)
362 362
363 363 return perm_result
364 364
365 365 def _get_http_scheme(self, environ):
366 366 try:
367 367 return environ['wsgi.url_scheme']
368 368 except Exception:
369 369 log.exception('Failed to read http scheme')
370 370 return 'http'
371 371
372 372 def _check_ssl(self, environ, start_response):
373 373 """
374 374 Checks the SSL check flag and returns False if SSL is not present
375 375 and required True otherwise
376 376 """
377 377 org_proto = environ['wsgi._org_proto']
378 378 # check if we have SSL required ! if not it's a bad request !
379 379 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
380 380 if require_ssl and org_proto == 'http':
381 381 log.debug(
382 382 'Bad request: detected protocol is `%s` and '
383 383 'SSL/HTTPS is required.', org_proto)
384 384 return False
385 385 return True
386 386
387 387 def _get_default_cache_ttl(self):
388 388 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
389 389 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
390 390 plugin_settings = plugin.get_settings()
391 391 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
392 392 plugin_settings) or (False, 0)
393 393 return plugin_cache_active, cache_ttl
394 394
395 395 def __call__(self, environ, start_response):
396 396 try:
397 397 return self._handle_request(environ, start_response)
398 398 except Exception:
399 399 log.exception("Exception while handling request")
400 400 appenlight.track_exception(environ)
401 401 return HTTPInternalServerError()(environ, start_response)
402 402 finally:
403 403 meta.Session.remove()
404 404
405 405 def _handle_request(self, environ, start_response):
406 406 if not self._check_ssl(environ, start_response):
407 407 reason = ('SSL required, while RhodeCode was unable '
408 408 'to detect this as SSL request')
409 409 log.debug('User not allowed to proceed, %s', reason)
410 410 return HTTPNotAcceptable(reason)(environ, start_response)
411 411
412 412 if not self.url_repo_name:
413 413 log.warning('Repository name is empty: %s', self.url_repo_name)
414 414 # failed to get repo name, we fail now
415 415 return HTTPNotFound()(environ, start_response)
416 416 log.debug('Extracted repo name is %s', self.url_repo_name)
417 417
418 418 ip_addr = get_ip_addr(environ)
419 419 user_agent = get_user_agent(environ)
420 420 username = None
421 421
422 422 # skip passing error to error controller
423 423 environ['pylons.status_code_redirect'] = True
424 424
425 425 # ======================================================================
426 426 # GET ACTION PULL or PUSH
427 427 # ======================================================================
428 428 action = self._get_action(environ)
429 429
430 430 # ======================================================================
431 431 # Check if this is a request to a shadow repository of a pull request.
432 432 # In this case only pull action is allowed.
433 433 # ======================================================================
434 434 if self.is_shadow_repo and action != 'pull':
435 435 reason = 'Only pull action is allowed for shadow repositories.'
436 436 log.debug('User not allowed to proceed, %s', reason)
437 437 return HTTPNotAcceptable(reason)(environ, start_response)
438 438
439 439 # Check if the shadow repo actually exists, in case someone refers
440 440 # to it, and it has been deleted because of successful merge.
441 441 if self.is_shadow_repo and not self.is_shadow_repo_dir:
442 442 log.debug(
443 443 'Shadow repo detected, and shadow repo dir `%s` is missing',
444 444 self.is_shadow_repo_dir)
445 445 return HTTPNotFound()(environ, start_response)
446 446
447 447 # ======================================================================
448 448 # CHECK ANONYMOUS PERMISSION
449 449 # ======================================================================
450 450 detect_force_push = False
451 451 check_branch_perms = False
452 452 if action in ['pull', 'push']:
453 453 user_obj = anonymous_user = User.get_default_user()
454 454 auth_user = user_obj.AuthUser()
455 455 username = anonymous_user.username
456 456 if anonymous_user.active:
457 457 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
458 458 # ONLY check permissions if the user is activated
459 459 anonymous_perm = self._check_permission(
460 460 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
461 461 plugin_id='anonymous_access',
462 462 plugin_cache_active=plugin_cache_active,
463 463 cache_ttl=cache_ttl,
464 464 )
465 465 else:
466 466 anonymous_perm = False
467 467
468 468 if not anonymous_user.active or not anonymous_perm:
469 469 if not anonymous_user.active:
470 470 log.debug('Anonymous access is disabled, running '
471 471 'authentication')
472 472
473 473 if not anonymous_perm:
474 log.debug('Not enough credentials to access this '
475 'repository as anonymous user')
474 log.debug('Not enough credentials to access repo: `%s` '
475 'repository as anonymous user', self.acl_repo_name)
476
476 477
477 478 username = None
478 479 # ==============================================================
479 480 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
480 481 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
481 482 # ==============================================================
482 483
483 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 488 pre_auth = authenticate(
486 489 '', '', environ, VCS_TYPE, registry=self.registry,
487 490 acl_repo_name=self.acl_repo_name)
491
488 492 if pre_auth and pre_auth.get('username'):
489 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 495 if pre_auth:
492 496 log.debug('PRE-AUTH successful from %s',
493 497 pre_auth.get('auth_data', {}).get('_plugin'))
494 498
495 499 # If not authenticated by the container, running basic auth
496 500 # before inject the calling repo_name for special scope checks
497 501 self.authenticate.acl_repo_name = self.acl_repo_name
498 502
499 503 plugin_cache_active, cache_ttl = False, 0
500 504 plugin = None
505
506 # regular auth chain
501 507 if not username:
502 508 self.authenticate.realm = self.authenticate.get_rc_realm()
503 509
504 510 try:
505 511 auth_result = self.authenticate(environ)
506 512 except (UserCreationError, NotAllowedToCreateUserError) as e:
507 513 log.error(e)
508 514 reason = safe_str(e)
509 515 return HTTPNotAcceptable(reason)(environ, start_response)
510 516
511 517 if isinstance(auth_result, dict):
512 518 AUTH_TYPE.update(environ, 'basic')
513 519 REMOTE_USER.update(environ, auth_result['username'])
514 520 username = auth_result['username']
515 521 plugin = auth_result.get('auth_data', {}).get('_plugin')
516 522 log.info(
517 523 'MAIN-AUTH successful for user `%s` from %s plugin',
518 524 username, plugin)
519 525
520 526 plugin_cache_active, cache_ttl = auth_result.get(
521 527 'auth_data', {}).get('_ttl_cache') or (False, 0)
522 528 else:
523 529 return auth_result.wsgi_application(environ, start_response)
524 530
525 531 # ==============================================================
526 532 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
527 533 # ==============================================================
528 534 user = User.get_by_username(username)
529 535 if not self.valid_and_active_user(user):
530 536 return HTTPForbidden()(environ, start_response)
531 537 username = user.username
532 538 user_id = user.user_id
533 539
534 540 # check user attributes for password change flag
535 541 user_obj = user
536 542 auth_user = user_obj.AuthUser()
537 543 if user_obj and user_obj.username != User.DEFAULT_USER and \
538 544 user_obj.user_data.get('force_password_change'):
539 545 reason = 'password change required'
540 546 log.debug('User not allowed to authenticate, %s', reason)
541 547 return HTTPNotAcceptable(reason)(environ, start_response)
542 548
543 549 # check permissions for this repository
544 550 perm = self._check_permission(
545 551 action, user, auth_user, self.acl_repo_name, ip_addr,
546 552 plugin, plugin_cache_active, cache_ttl)
547 553 if not perm:
548 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 557 if action == 'push':
552 558 perms = auth_user.get_branch_permissions(self.acl_repo_name)
553 559 if perms:
554 560 check_branch_perms = True
555 561 detect_force_push = True
556 562
557 563 # extras are injected into UI object and later available
558 564 # in hooks executed by RhodeCode
559 565 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
560 566
561 567 extras = vcs_operation_context(
562 568 environ, repo_name=self.acl_repo_name, username=username,
563 569 action=action, scm=self.SCM, check_locking=check_locking,
564 570 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
565 571 detect_force_push=detect_force_push
566 572 )
567 573
568 574 # ======================================================================
569 575 # REQUEST HANDLING
570 576 # ======================================================================
571 577 repo_path = os.path.join(
572 578 safe_str(self.base_path), safe_str(self.vcs_repo_name))
573 579 log.debug('Repository path is %s', repo_path)
574 580
575 581 fix_PATH()
576 582
577 583 log.info(
578 584 '%s action on %s repo "%s" by "%s" from %s %s',
579 585 action, self.SCM, safe_str(self.url_repo_name),
580 586 safe_str(username), ip_addr, user_agent)
581 587
582 588 return self._generate_vcs_response(
583 589 environ, start_response, repo_path, extras, action)
584 590
585 591 @initialize_generator
586 592 def _generate_vcs_response(
587 593 self, environ, start_response, repo_path, extras, action):
588 594 """
589 595 Returns a generator for the response content.
590 596
591 597 This method is implemented as a generator, so that it can trigger
592 598 the cache validation after all content sent back to the client. It
593 599 also handles the locking exceptions which will be triggered when
594 600 the first chunk is produced by the underlying WSGI application.
595 601 """
596 602 txn_id = ''
597 603 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
598 604 # case for SVN, we want to re-use the callback daemon port
599 605 # so we use the txn_id, for this we peek the body, and still save
600 606 # it as wsgi.input
601 607 data = environ['wsgi.input'].read()
602 608 environ['wsgi.input'] = io.StringIO(data)
603 609 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
604 610
605 611 callback_daemon, extras = self._prepare_callback_daemon(
606 612 extras, environ, action, txn_id=txn_id)
607 613 log.debug('HOOKS extras is %s', extras)
608 614
609 615 http_scheme = self._get_http_scheme(environ)
610 616
611 617 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
612 618 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
613 619 with callback_daemon:
614 620 app.rc_extras = extras
615 621
616 622 try:
617 623 response = app(environ, start_response)
618 624 finally:
619 625 # This statement works together with the decorator
620 626 # "initialize_generator" above. The decorator ensures that
621 627 # we hit the first yield statement before the generator is
622 628 # returned back to the WSGI server. This is needed to
623 629 # ensure that the call to "app" above triggers the
624 630 # needed callback to "start_response" before the
625 631 # generator is actually used.
626 632 yield "__init__"
627 633
628 634 # iter content
629 635 for chunk in response:
630 636 yield chunk
631 637
632 638 try:
633 639 # invalidate cache on push
634 640 if action == 'push':
635 641 self._invalidate_cache(self.url_repo_name)
636 642 finally:
637 643 meta.Session.remove()
638 644
639 645 def _get_repository_name(self, environ):
640 646 """Get repository name out of the environmnent
641 647
642 648 :param environ: WSGI environment
643 649 """
644 650 raise NotImplementedError()
645 651
646 652 def _get_action(self, environ):
647 653 """Map request commands into a pull or push command.
648 654
649 655 :param environ: WSGI environment
650 656 """
651 657 raise NotImplementedError()
652 658
653 659 def _create_wsgi_app(self, repo_path, repo_name, config):
654 660 """Return the WSGI app that will finally handle the request."""
655 661 raise NotImplementedError()
656 662
657 663 def _create_config(self, extras, repo_name, scheme='http'):
658 664 """Create a safe config representation."""
659 665 raise NotImplementedError()
660 666
661 667 def _should_use_callback_daemon(self, extras, environ, action):
662 668 if extras.get('is_shadow_repo'):
663 669 # we don't want to execute hooks, and callback daemon for shadow repos
664 670 return False
665 671 return True
666 672
667 673 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
668 674 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
669 675 if not self._should_use_callback_daemon(extras, environ, action):
670 676 # disable callback daemon for actions that don't require it
671 677 direct_calls = True
672 678
673 679 return prepare_callback_daemon(
674 680 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
675 681 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
676 682
677 683
678 684 def _should_check_locking(query_string):
679 685 # this is kind of hacky, but due to how mercurial handles client-server
680 686 # server see all operation on commit; bookmarks, phases and
681 687 # obsolescence marker in different transaction, we don't want to check
682 688 # locking on those
683 689 return query_string not in ['cmd=listkeys']
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 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