##// END OF EJS Templates
issue-trackers: enforce a http or / patterns to avoid JS injections.
marcink -
r2334:0804fe0e default
parent child Browse files
Show More
@@ -1,730 +1,730 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.apps._base import ADMIN_PREFIX
26 26 from rhodecode.lib.utils2 import md5
27 27 from rhodecode.model.db import RhodeCodeUi
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 30 from rhodecode.tests import assert_session_flash
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 UPDATE_DATA_QUALNAME = (
35 35 'rhodecode.apps.admin.views.system_info.AdminSystemInfoSettingsView.get_update_data')
36 36
37 37
38 38 def route_path(name, params=None, **kwargs):
39 39 import urllib
40 40 from rhodecode.apps._base import ADMIN_PREFIX
41 41
42 42 base_url = {
43 43
44 44 'admin_settings':
45 45 ADMIN_PREFIX +'/settings',
46 46 'admin_settings_update':
47 47 ADMIN_PREFIX + '/settings/update',
48 48 'admin_settings_global':
49 49 ADMIN_PREFIX + '/settings/global',
50 50 'admin_settings_global_update':
51 51 ADMIN_PREFIX + '/settings/global/update',
52 52 'admin_settings_vcs':
53 53 ADMIN_PREFIX + '/settings/vcs',
54 54 'admin_settings_vcs_update':
55 55 ADMIN_PREFIX + '/settings/vcs/update',
56 56 'admin_settings_vcs_svn_pattern_delete':
57 57 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
58 58 'admin_settings_mapping':
59 59 ADMIN_PREFIX + '/settings/mapping',
60 60 'admin_settings_mapping_update':
61 61 ADMIN_PREFIX + '/settings/mapping/update',
62 62 'admin_settings_visual':
63 63 ADMIN_PREFIX + '/settings/visual',
64 64 'admin_settings_visual_update':
65 65 ADMIN_PREFIX + '/settings/visual/update',
66 66 'admin_settings_issuetracker':
67 67 ADMIN_PREFIX + '/settings/issue-tracker',
68 68 'admin_settings_issuetracker_update':
69 69 ADMIN_PREFIX + '/settings/issue-tracker/update',
70 70 'admin_settings_issuetracker_test':
71 71 ADMIN_PREFIX + '/settings/issue-tracker/test',
72 72 'admin_settings_issuetracker_delete':
73 73 ADMIN_PREFIX + '/settings/issue-tracker/delete',
74 74 'admin_settings_email':
75 75 ADMIN_PREFIX + '/settings/email',
76 76 'admin_settings_email_update':
77 77 ADMIN_PREFIX + '/settings/email/update',
78 78 'admin_settings_hooks':
79 79 ADMIN_PREFIX + '/settings/hooks',
80 80 'admin_settings_hooks_update':
81 81 ADMIN_PREFIX + '/settings/hooks/update',
82 82 'admin_settings_hooks_delete':
83 83 ADMIN_PREFIX + '/settings/hooks/delete',
84 84 'admin_settings_search':
85 85 ADMIN_PREFIX + '/settings/search',
86 86 'admin_settings_labs':
87 87 ADMIN_PREFIX + '/settings/labs',
88 88 'admin_settings_labs_update':
89 89 ADMIN_PREFIX + '/settings/labs/update',
90 90
91 91 'admin_settings_sessions':
92 92 ADMIN_PREFIX + '/settings/sessions',
93 93 'admin_settings_sessions_cleanup':
94 94 ADMIN_PREFIX + '/settings/sessions/cleanup',
95 95 'admin_settings_system':
96 96 ADMIN_PREFIX + '/settings/system',
97 97 'admin_settings_system_update':
98 98 ADMIN_PREFIX + '/settings/system/updates',
99 99 'admin_settings_open_source':
100 100 ADMIN_PREFIX + '/settings/open_source',
101 101
102 102
103 103 }[name].format(**kwargs)
104 104
105 105 if params:
106 106 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
107 107 return base_url
108 108
109 109
110 110 @pytest.mark.usefixtures('autologin_user', 'app')
111 111 class TestAdminSettingsController(object):
112 112
113 113 @pytest.mark.parametrize('urlname', [
114 114 'admin_settings_vcs',
115 115 'admin_settings_mapping',
116 116 'admin_settings_global',
117 117 'admin_settings_visual',
118 118 'admin_settings_email',
119 119 'admin_settings_hooks',
120 120 'admin_settings_search',
121 121 ])
122 122 def test_simple_get(self, urlname):
123 123 self.app.get(route_path(urlname))
124 124
125 125 def test_create_custom_hook(self, csrf_token):
126 126 response = self.app.post(
127 127 route_path('admin_settings_hooks_update'),
128 128 params={
129 129 'new_hook_ui_key': 'test_hooks_1',
130 130 'new_hook_ui_value': 'cd /tmp',
131 131 'csrf_token': csrf_token})
132 132
133 133 response = response.follow()
134 134 response.mustcontain('test_hooks_1')
135 135 response.mustcontain('cd /tmp')
136 136
137 137 def test_create_custom_hook_delete(self, csrf_token):
138 138 response = self.app.post(
139 139 route_path('admin_settings_hooks_update'),
140 140 params={
141 141 'new_hook_ui_key': 'test_hooks_2',
142 142 'new_hook_ui_value': 'cd /tmp2',
143 143 'csrf_token': csrf_token})
144 144
145 145 response = response.follow()
146 146 response.mustcontain('test_hooks_2')
147 147 response.mustcontain('cd /tmp2')
148 148
149 149 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
150 150
151 151 # delete
152 152 self.app.post(
153 153 route_path('admin_settings_hooks_delete'),
154 154 params={'hook_id': hook_id, 'csrf_token': csrf_token})
155 155 response = self.app.get(route_path('admin_settings_hooks'))
156 156 response.mustcontain(no=['test_hooks_2'])
157 157 response.mustcontain(no=['cd /tmp2'])
158 158
159 159
160 160 @pytest.mark.usefixtures('autologin_user', 'app')
161 161 class TestAdminSettingsGlobal(object):
162 162
163 163 def test_pre_post_code_code_active(self, csrf_token):
164 164 pre_code = 'rc-pre-code-187652122'
165 165 post_code = 'rc-postcode-98165231'
166 166
167 167 response = self.post_and_verify_settings({
168 168 'rhodecode_pre_code': pre_code,
169 169 'rhodecode_post_code': post_code,
170 170 'csrf_token': csrf_token,
171 171 })
172 172
173 173 response = response.follow()
174 174 response.mustcontain(pre_code, post_code)
175 175
176 176 def test_pre_post_code_code_inactive(self, csrf_token):
177 177 pre_code = 'rc-pre-code-187652122'
178 178 post_code = 'rc-postcode-98165231'
179 179 response = self.post_and_verify_settings({
180 180 'rhodecode_pre_code': '',
181 181 'rhodecode_post_code': '',
182 182 'csrf_token': csrf_token,
183 183 })
184 184
185 185 response = response.follow()
186 186 response.mustcontain(no=[pre_code, post_code])
187 187
188 188 def test_captcha_activate(self, csrf_token):
189 189 self.post_and_verify_settings({
190 190 'rhodecode_captcha_private_key': '1234567890',
191 191 'rhodecode_captcha_public_key': '1234567890',
192 192 'csrf_token': csrf_token,
193 193 })
194 194
195 195 response = self.app.get(ADMIN_PREFIX + '/register')
196 196 response.mustcontain('captcha')
197 197
198 198 def test_captcha_deactivate(self, csrf_token):
199 199 self.post_and_verify_settings({
200 200 'rhodecode_captcha_private_key': '',
201 201 'rhodecode_captcha_public_key': '1234567890',
202 202 'csrf_token': csrf_token,
203 203 })
204 204
205 205 response = self.app.get(ADMIN_PREFIX + '/register')
206 206 response.mustcontain(no=['captcha'])
207 207
208 208 def test_title_change(self, csrf_token):
209 209 old_title = 'RhodeCode'
210 210
211 211 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
212 212 response = self.post_and_verify_settings({
213 213 'rhodecode_title': new_title,
214 214 'csrf_token': csrf_token,
215 215 })
216 216
217 217 response = response.follow()
218 218 response.mustcontain(
219 219 """<div class="branding">- %s</div>""" % new_title)
220 220
221 221 def post_and_verify_settings(self, settings):
222 222 old_title = 'RhodeCode'
223 223 old_realm = 'RhodeCode authentication'
224 224 params = {
225 225 'rhodecode_title': old_title,
226 226 'rhodecode_realm': old_realm,
227 227 'rhodecode_pre_code': '',
228 228 'rhodecode_post_code': '',
229 229 'rhodecode_captcha_private_key': '',
230 230 'rhodecode_captcha_public_key': '',
231 231 'rhodecode_create_personal_repo_group': False,
232 232 'rhodecode_personal_repo_group_pattern': '${username}',
233 233 }
234 234 params.update(settings)
235 235 response = self.app.post(
236 236 route_path('admin_settings_global_update'), params=params)
237 237
238 238 assert_session_flash(response, 'Updated application settings')
239 239 app_settings = SettingsModel().get_all_settings()
240 240 del settings['csrf_token']
241 241 for key, value in settings.iteritems():
242 242 assert app_settings[key] == value.decode('utf-8')
243 243
244 244 return response
245 245
246 246
247 247 @pytest.mark.usefixtures('autologin_user', 'app')
248 248 class TestAdminSettingsVcs(object):
249 249
250 250 def test_contains_svn_default_patterns(self):
251 251 response = self.app.get(route_path('admin_settings_vcs'))
252 252 expected_patterns = [
253 253 '/trunk',
254 254 '/branches/*',
255 255 '/tags/*',
256 256 ]
257 257 for pattern in expected_patterns:
258 258 response.mustcontain(pattern)
259 259
260 260 def test_add_new_svn_branch_and_tag_pattern(
261 261 self, backend_svn, form_defaults, disable_sql_cache,
262 262 csrf_token):
263 263 form_defaults.update({
264 264 'new_svn_branch': '/exp/branches/*',
265 265 'new_svn_tag': '/important_tags/*',
266 266 'csrf_token': csrf_token,
267 267 })
268 268
269 269 response = self.app.post(
270 270 route_path('admin_settings_vcs_update'),
271 271 params=form_defaults, status=302)
272 272 response = response.follow()
273 273
274 274 # Expect to find the new values on the page
275 275 response.mustcontain('/exp/branches/*')
276 276 response.mustcontain('/important_tags/*')
277 277
278 278 # Expect that those patterns are used to match branches and tags now
279 279 repo = backend_svn['svn-simple-layout'].scm_instance()
280 280 assert 'exp/branches/exp-sphinx-docs' in repo.branches
281 281 assert 'important_tags/v0.5' in repo.tags
282 282
283 283 def test_add_same_svn_value_twice_shows_an_error_message(
284 284 self, form_defaults, csrf_token, settings_util):
285 285 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
286 286 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
287 287
288 288 response = self.app.post(
289 289 route_path('admin_settings_vcs_update'),
290 290 params={
291 291 'paths_root_path': form_defaults['paths_root_path'],
292 292 'new_svn_branch': '/test',
293 293 'new_svn_tag': '/test',
294 294 'csrf_token': csrf_token,
295 295 },
296 296 status=200)
297 297
298 298 response.mustcontain("Pattern already exists")
299 299 response.mustcontain("Some form inputs contain invalid data.")
300 300
301 301 @pytest.mark.parametrize('section', [
302 302 'vcs_svn_branch',
303 303 'vcs_svn_tag',
304 304 ])
305 305 def test_delete_svn_patterns(
306 306 self, section, csrf_token, settings_util):
307 307 setting = settings_util.create_rhodecode_ui(
308 308 section, '/test_delete', cleanup=False)
309 309
310 310 self.app.post(
311 311 route_path('admin_settings_vcs_svn_pattern_delete'),
312 312 params={
313 313 'delete_svn_pattern': setting.ui_id,
314 314 'csrf_token': csrf_token},
315 315 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
316 316
317 317 @pytest.mark.parametrize('section', [
318 318 'vcs_svn_branch',
319 319 'vcs_svn_tag',
320 320 ])
321 321 def test_delete_svn_patterns_raises_404_when_no_xhr(
322 322 self, section, csrf_token, settings_util):
323 323 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
324 324
325 325 self.app.post(
326 326 route_path('admin_settings_vcs_svn_pattern_delete'),
327 327 params={
328 328 'delete_svn_pattern': setting.ui_id,
329 329 'csrf_token': csrf_token},
330 330 status=404)
331 331
332 332 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
333 333 form_defaults.update({
334 334 'csrf_token': csrf_token,
335 335 'extensions_hgsubversion': 'True',
336 336 })
337 337 response = self.app.post(
338 338 route_path('admin_settings_vcs_update'),
339 339 params=form_defaults,
340 340 status=302)
341 341
342 342 response = response.follow()
343 343 extensions_input = (
344 344 '<input id="extensions_hgsubversion" '
345 345 'name="extensions_hgsubversion" type="checkbox" '
346 346 'value="True" checked="checked" />')
347 347 response.mustcontain(extensions_input)
348 348
349 349 def test_extensions_hgevolve(self, form_defaults, csrf_token):
350 350 form_defaults.update({
351 351 'csrf_token': csrf_token,
352 352 'extensions_evolve': 'True',
353 353 })
354 354 response = self.app.post(
355 355 route_path('admin_settings_vcs_update'),
356 356 params=form_defaults,
357 357 status=302)
358 358
359 359 response = response.follow()
360 360 extensions_input = (
361 361 '<input id="extensions_evolve" '
362 362 'name="extensions_evolve" type="checkbox" '
363 363 'value="True" checked="checked" />')
364 364 response.mustcontain(extensions_input)
365 365
366 366 def test_has_a_section_for_pull_request_settings(self):
367 367 response = self.app.get(route_path('admin_settings_vcs'))
368 368 response.mustcontain('Pull Request Settings')
369 369
370 370 def test_has_an_input_for_invalidation_of_inline_comments(self):
371 371 response = self.app.get(route_path('admin_settings_vcs'))
372 372 assert_response = AssertResponse(response)
373 373 assert_response.one_element_exists(
374 374 '[name=rhodecode_use_outdated_comments]')
375 375
376 376 @pytest.mark.parametrize('new_value', [True, False])
377 377 def test_allows_to_change_invalidation_of_inline_comments(
378 378 self, form_defaults, csrf_token, new_value):
379 379 setting_key = 'use_outdated_comments'
380 380 setting = SettingsModel().create_or_update_setting(
381 381 setting_key, not new_value, 'bool')
382 382 Session().add(setting)
383 383 Session().commit()
384 384
385 385 form_defaults.update({
386 386 'csrf_token': csrf_token,
387 387 'rhodecode_use_outdated_comments': str(new_value),
388 388 })
389 389 response = self.app.post(
390 390 route_path('admin_settings_vcs_update'),
391 391 params=form_defaults,
392 392 status=302)
393 393 response = response.follow()
394 394 setting = SettingsModel().get_setting_by_name(setting_key)
395 395 assert setting.app_settings_value is new_value
396 396
397 397 @pytest.mark.parametrize('new_value', [True, False])
398 398 def test_allows_to_change_hg_rebase_merge_strategy(
399 399 self, form_defaults, csrf_token, new_value):
400 400 setting_key = 'hg_use_rebase_for_merging'
401 401
402 402 form_defaults.update({
403 403 'csrf_token': csrf_token,
404 404 'rhodecode_' + setting_key: str(new_value),
405 405 })
406 406
407 407 with mock.patch.dict(
408 408 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
409 409 self.app.post(
410 410 route_path('admin_settings_vcs_update'),
411 411 params=form_defaults,
412 412 status=302)
413 413
414 414 setting = SettingsModel().get_setting_by_name(setting_key)
415 415 assert setting.app_settings_value is new_value
416 416
417 417 @pytest.fixture
418 418 def disable_sql_cache(self, request):
419 419 patcher = mock.patch(
420 420 'rhodecode.lib.caching_query.FromCache.process_query')
421 421 request.addfinalizer(patcher.stop)
422 422 patcher.start()
423 423
424 424 @pytest.fixture
425 425 def form_defaults(self):
426 426 from rhodecode.apps.admin.views.settings import AdminSettingsView
427 427 return AdminSettingsView._form_defaults()
428 428
429 429 # TODO: johbo: What we really want is to checkpoint before a test run and
430 430 # reset the session afterwards.
431 431 @pytest.fixture(scope='class', autouse=True)
432 432 def cleanup_settings(self, request, pylonsapp):
433 433 ui_id = RhodeCodeUi.ui_id
434 434 original_ids = list(
435 435 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
436 436
437 437 @request.addfinalizer
438 438 def cleanup():
439 439 RhodeCodeUi.query().filter(
440 440 ui_id.notin_(original_ids)).delete(False)
441 441
442 442
443 443 @pytest.mark.usefixtures('autologin_user', 'app')
444 444 class TestLabsSettings(object):
445 445 def test_get_settings_page_disabled(self):
446 446 with mock.patch.dict(
447 447 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
448 448
449 449 response = self.app.get(
450 450 route_path('admin_settings_labs'), status=302)
451 451
452 452 assert response.location.endswith(route_path('admin_settings'))
453 453
454 454 def test_get_settings_page_enabled(self):
455 455 from rhodecode.apps.admin.views import settings
456 456 lab_settings = [
457 457 settings.LabSetting(
458 458 key='rhodecode_bool',
459 459 type='bool',
460 460 group='bool group',
461 461 label='bool label',
462 462 help='bool help'
463 463 ),
464 464 settings.LabSetting(
465 465 key='rhodecode_text',
466 466 type='unicode',
467 467 group='text group',
468 468 label='text label',
469 469 help='text help'
470 470 ),
471 471 ]
472 472 with mock.patch.dict(rhodecode.CONFIG,
473 473 {'labs_settings_active': 'true'}):
474 474 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
475 475 response = self.app.get(route_path('admin_settings_labs'))
476 476
477 477 assert '<label>bool group:</label>' in response
478 478 assert '<label for="rhodecode_bool">bool label</label>' in response
479 479 assert '<p class="help-block">bool help</p>' in response
480 480 assert 'name="rhodecode_bool" type="checkbox"' in response
481 481
482 482 assert '<label>text group:</label>' in response
483 483 assert '<label for="rhodecode_text">text label</label>' in response
484 484 assert '<p class="help-block">text help</p>' in response
485 485 assert 'name="rhodecode_text" size="60" type="text"' in response
486 486
487 487
488 488 @pytest.mark.usefixtures('app')
489 489 class TestOpenSourceLicenses(object):
490 490
491 491 def test_records_are_displayed(self, autologin_user):
492 492 sample_licenses = {
493 493 "python2.7-pytest-2.7.1": {
494 494 "UNKNOWN": None
495 495 },
496 496 "python2.7-Markdown-2.6.2": {
497 497 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
498 498 }
499 499 }
500 500 read_licenses_patch = mock.patch(
501 501 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
502 502 return_value=sample_licenses)
503 503 with read_licenses_patch:
504 504 response = self.app.get(
505 505 route_path('admin_settings_open_source'), status=200)
506 506
507 507 assert_response = AssertResponse(response)
508 508 assert_response.element_contains(
509 509 '.panel-heading', 'Licenses of Third Party Packages')
510 510 for name in sample_licenses:
511 511 response.mustcontain(name)
512 512 for license in sample_licenses[name]:
513 513 assert_response.element_contains('.panel-body', license)
514 514
515 515 def test_records_can_be_read(self, autologin_user):
516 516 response = self.app.get(
517 517 route_path('admin_settings_open_source'), status=200)
518 518 assert_response = AssertResponse(response)
519 519 assert_response.element_contains(
520 520 '.panel-heading', 'Licenses of Third Party Packages')
521 521
522 522 def test_forbidden_when_normal_user(self, autologin_regular_user):
523 523 self.app.get(
524 524 route_path('admin_settings_open_source'), status=404)
525 525
526 526
527 527 @pytest.mark.usefixtures('app')
528 528 class TestUserSessions(object):
529 529
530 530 def test_forbidden_when_normal_user(self, autologin_regular_user):
531 531 self.app.get(route_path('admin_settings_sessions'), status=404)
532 532
533 533 def test_show_sessions_page(self, autologin_user):
534 534 response = self.app.get(route_path('admin_settings_sessions'), status=200)
535 535 response.mustcontain('file')
536 536
537 537 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
538 538
539 539 post_data = {
540 540 'csrf_token': csrf_token,
541 541 'expire_days': '60'
542 542 }
543 543 response = self.app.post(
544 544 route_path('admin_settings_sessions_cleanup'), params=post_data,
545 545 status=302)
546 546 assert_session_flash(response, 'Cleaned up old sessions')
547 547
548 548
549 549 @pytest.mark.usefixtures('app')
550 550 class TestAdminSystemInfo(object):
551 551
552 552 def test_forbidden_when_normal_user(self, autologin_regular_user):
553 553 self.app.get(route_path('admin_settings_system'), status=404)
554 554
555 555 def test_system_info_page(self, autologin_user):
556 556 response = self.app.get(route_path('admin_settings_system'))
557 557 response.mustcontain('RhodeCode Community Edition, version {}'.format(
558 558 rhodecode.__version__))
559 559
560 560 def test_system_update_new_version(self, autologin_user):
561 561 update_data = {
562 562 'versions': [
563 563 {
564 564 'version': '100.3.1415926535',
565 565 'general': 'The latest version we are ever going to ship'
566 566 },
567 567 {
568 568 'version': '0.0.0',
569 569 'general': 'The first version we ever shipped'
570 570 }
571 571 ]
572 572 }
573 573 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
574 574 response = self.app.get(route_path('admin_settings_system_update'))
575 575 response.mustcontain('A <b>new version</b> is available')
576 576
577 577 def test_system_update_nothing_new(self, autologin_user):
578 578 update_data = {
579 579 'versions': [
580 580 {
581 581 'version': '0.0.0',
582 582 'general': 'The first version we ever shipped'
583 583 }
584 584 ]
585 585 }
586 586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 587 response = self.app.get(route_path('admin_settings_system_update'))
588 588 response.mustcontain(
589 589 'You already have the <b>latest</b> stable version.')
590 590
591 591 def test_system_update_bad_response(self, autologin_user):
592 592 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
593 593 response = self.app.get(route_path('admin_settings_system_update'))
594 594 response.mustcontain(
595 595 'Bad data sent from update server')
596 596
597 597
598 598 @pytest.mark.usefixtures("app")
599 599 class TestAdminSettingsIssueTracker(object):
600 600 RC_PREFIX = 'rhodecode_'
601 601 SHORT_PATTERN_KEY = 'issuetracker_pat_'
602 602 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
603 603
604 604 def test_issuetracker_index(self, autologin_user):
605 605 response = self.app.get(route_path('admin_settings_issuetracker'))
606 606 assert response.status_code == 200
607 607
608 608 def test_add_empty_issuetracker_pattern(
609 609 self, request, autologin_user, csrf_token):
610 610 post_url = route_path('admin_settings_issuetracker_update')
611 611 post_data = {
612 612 'csrf_token': csrf_token
613 613 }
614 614 self.app.post(post_url, post_data, status=302)
615 615
616 616 def test_add_issuetracker_pattern(
617 617 self, request, autologin_user, csrf_token):
618 618 pattern = 'issuetracker_pat'
619 619 another_pattern = pattern+'1'
620 620 post_url = route_path('admin_settings_issuetracker_update')
621 621 post_data = {
622 622 'new_pattern_pattern_0': pattern,
623 'new_pattern_url_0': 'url',
623 'new_pattern_url_0': 'http://url',
624 624 'new_pattern_prefix_0': 'prefix',
625 625 'new_pattern_description_0': 'description',
626 626 'new_pattern_pattern_1': another_pattern,
627 'new_pattern_url_1': 'url1',
627 'new_pattern_url_1': 'https://url1',
628 628 'new_pattern_prefix_1': 'prefix1',
629 629 'new_pattern_description_1': 'description1',
630 630 'csrf_token': csrf_token
631 631 }
632 632 self.app.post(post_url, post_data, status=302)
633 633 settings = SettingsModel().get_all_settings()
634 634 self.uid = md5(pattern)
635 635 assert settings[self.PATTERN_KEY+self.uid] == pattern
636 636 self.another_uid = md5(another_pattern)
637 637 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
638 638
639 639 @request.addfinalizer
640 640 def cleanup():
641 641 defaults = SettingsModel().get_all_settings()
642 642
643 643 entries = [name for name in defaults if (
644 644 (self.uid in name) or (self.another_uid) in name)]
645 645 start = len(self.RC_PREFIX)
646 646 for del_key in entries:
647 647 # TODO: anderson: get_by_name needs name without prefix
648 648 entry = SettingsModel().get_setting_by_name(del_key[start:])
649 649 Session().delete(entry)
650 650
651 651 Session().commit()
652 652
653 653 def test_edit_issuetracker_pattern(
654 654 self, autologin_user, backend, csrf_token, request):
655 655 old_pattern = 'issuetracker_pat'
656 656 old_uid = md5(old_pattern)
657 657 pattern = 'issuetracker_pat_new'
658 658 self.new_uid = md5(pattern)
659 659
660 660 SettingsModel().create_or_update_setting(
661 661 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
662 662
663 663 post_url = route_path('admin_settings_issuetracker_update')
664 664 post_data = {
665 665 'new_pattern_pattern_0': pattern,
666 'new_pattern_url_0': 'url',
666 'new_pattern_url_0': 'https://url',
667 667 'new_pattern_prefix_0': 'prefix',
668 668 'new_pattern_description_0': 'description',
669 669 'uid': old_uid,
670 670 'csrf_token': csrf_token
671 671 }
672 672 self.app.post(post_url, post_data, status=302)
673 673 settings = SettingsModel().get_all_settings()
674 674 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
675 675 assert self.PATTERN_KEY+old_uid not in settings
676 676
677 677 @request.addfinalizer
678 678 def cleanup():
679 679 IssueTrackerSettingsModel().delete_entries(self.new_uid)
680 680
681 681 def test_replace_issuetracker_pattern_description(
682 682 self, autologin_user, csrf_token, request, settings_util):
683 683 prefix = 'issuetracker'
684 684 pattern = 'issuetracker_pat'
685 685 self.uid = md5(pattern)
686 686 pattern_key = '_'.join([prefix, 'pat', self.uid])
687 687 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
688 688 desc_key = '_'.join([prefix, 'desc', self.uid])
689 689 rc_desc_key = '_'.join(['rhodecode', desc_key])
690 690 new_description = 'new_description'
691 691
692 692 settings_util.create_rhodecode_setting(
693 693 pattern_key, pattern, 'unicode', cleanup=False)
694 694 settings_util.create_rhodecode_setting(
695 695 desc_key, 'old description', 'unicode', cleanup=False)
696 696
697 697 post_url = route_path('admin_settings_issuetracker_update')
698 698 post_data = {
699 699 'new_pattern_pattern_0': pattern,
700 'new_pattern_url_0': 'url',
700 'new_pattern_url_0': 'https://url',
701 701 'new_pattern_prefix_0': 'prefix',
702 702 'new_pattern_description_0': new_description,
703 703 'uid': self.uid,
704 704 'csrf_token': csrf_token
705 705 }
706 706 self.app.post(post_url, post_data, status=302)
707 707 settings = SettingsModel().get_all_settings()
708 708 assert settings[rc_pattern_key] == pattern
709 709 assert settings[rc_desc_key] == new_description
710 710
711 711 @request.addfinalizer
712 712 def cleanup():
713 713 IssueTrackerSettingsModel().delete_entries(self.uid)
714 714
715 715 def test_delete_issuetracker_pattern(
716 716 self, autologin_user, backend, csrf_token, settings_util):
717 717 pattern = 'issuetracker_pat'
718 718 uid = md5(pattern)
719 719 settings_util.create_rhodecode_setting(
720 720 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
721 721
722 722 post_url = route_path('admin_settings_issuetracker_delete')
723 723 post_data = {
724 724 '_method': 'delete',
725 725 'uid': uid,
726 726 'csrf_token': csrf_token
727 727 }
728 728 self.app.post(post_url, post_data, status=302)
729 729 settings = SettingsModel().get_all_settings()
730 730 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
@@ -1,754 +1,762 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 import datetime
26 26 import formencode
27 27 import formencode.htmlfill
28 28
29 29 import rhodecode
30 30 from pyramid.view import view_config
31 31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 from rhodecode.apps._base import BaseAppView
36 36 from rhodecode.apps.admin.navigation import navigation_list
37 37 from rhodecode.apps.svn_support.config_keys import generate_config
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 41 from rhodecode.lib.celerylib import tasks, run_task
42 42 from rhodecode.lib.utils import repo2db_mapper
43 43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 44 from rhodecode.lib.index import searcher_from_config
45 45
46 46 from rhodecode.model.db import RhodeCodeUi, Repository
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.notification import EmailNotificationModel
54 54 from rhodecode.model.meta import Session
55 55 from rhodecode.model.settings import (
56 56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 57 SettingsModel)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class AdminSettingsView(BaseAppView):
64 64
65 65 def load_default_context(self):
66 66 c = self._get_local_tmpl_context()
67 67 c.labs_active = str2bool(
68 68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 69 c.navlist = navigation_list(self.request)
70 70 self._register_global_c(c)
71 71 return c
72 72
73 73 @classmethod
74 74 def _get_ui_settings(cls):
75 75 ret = RhodeCodeUi.query().all()
76 76
77 77 if not ret:
78 78 raise Exception('Could not get application ui settings !')
79 79 settings = {}
80 80 for each in ret:
81 81 k = each.ui_key
82 82 v = each.ui_value
83 83 if k == '/':
84 84 k = 'root_path'
85 85
86 86 if k in ['push_ssl', 'publish', 'enabled']:
87 87 v = str2bool(v)
88 88
89 89 if k.find('.') != -1:
90 90 k = k.replace('.', '_')
91 91
92 92 if each.ui_section in ['hooks', 'extensions']:
93 93 v = each.ui_active
94 94
95 95 settings[each.ui_section + '_' + k] = v
96 96 return settings
97 97
98 98 @classmethod
99 99 def _form_defaults(cls):
100 100 defaults = SettingsModel().get_all_settings()
101 101 defaults.update(cls._get_ui_settings())
102 102
103 103 defaults.update({
104 104 'new_svn_branch': '',
105 105 'new_svn_tag': '',
106 106 })
107 107 return defaults
108 108
109 109 @LoginRequired()
110 110 @HasPermissionAllDecorator('hg.admin')
111 111 @view_config(
112 112 route_name='admin_settings_vcs', request_method='GET',
113 113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 114 def settings_vcs(self):
115 115 c = self.load_default_context()
116 116 c.active = 'vcs'
117 117 model = VcsSettingsModel()
118 118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120 120
121 121 settings = self.request.registry.settings
122 122 c.svn_proxy_generate_config = settings[generate_config]
123 123
124 124 defaults = self._form_defaults()
125 125
126 126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127 127
128 128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 129 self._get_template_context(c), self.request)
130 130 html = formencode.htmlfill.render(
131 131 data,
132 132 defaults=defaults,
133 133 encoding="UTF-8",
134 134 force_defaults=False
135 135 )
136 136 return Response(html)
137 137
138 138 @LoginRequired()
139 139 @HasPermissionAllDecorator('hg.admin')
140 140 @CSRFRequired()
141 141 @view_config(
142 142 route_name='admin_settings_vcs_update', request_method='POST',
143 143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 144 def settings_vcs_update(self):
145 145 _ = self.request.translate
146 146 c = self.load_default_context()
147 147 c.active = 'vcs'
148 148
149 149 model = VcsSettingsModel()
150 150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152 152
153 153 settings = self.request.registry.settings
154 154 c.svn_proxy_generate_config = settings[generate_config]
155 155
156 156 application_form = ApplicationUiSettingsForm()()
157 157
158 158 try:
159 159 form_result = application_form.to_python(dict(self.request.POST))
160 160 except formencode.Invalid as errors:
161 161 h.flash(
162 162 _("Some form inputs contain invalid data."),
163 163 category='error')
164 164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 165 self._get_template_context(c), self.request)
166 166 html = formencode.htmlfill.render(
167 167 data,
168 168 defaults=errors.value,
169 169 errors=errors.error_dict or {},
170 170 prefix_error=False,
171 171 encoding="UTF-8",
172 172 force_defaults=False
173 173 )
174 174 return Response(html)
175 175
176 176 try:
177 177 if c.visual.allow_repo_location_change:
178 178 model.update_global_path_setting(
179 179 form_result['paths_root_path'])
180 180
181 181 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 182 model.update_global_hook_settings(form_result)
183 183
184 184 model.create_or_update_global_svn_settings(form_result)
185 185 model.create_or_update_global_hg_settings(form_result)
186 186 model.create_or_update_global_git_settings(form_result)
187 187 model.create_or_update_global_pr_settings(form_result)
188 188 except Exception:
189 189 log.exception("Exception while updating settings")
190 190 h.flash(_('Error occurred during updating '
191 191 'application settings'), category='error')
192 192 else:
193 193 Session().commit()
194 194 h.flash(_('Updated VCS settings'), category='success')
195 195 raise HTTPFound(h.route_path('admin_settings_vcs'))
196 196
197 197 data = render('rhodecode:templates/admin/settings/settings.mako',
198 198 self._get_template_context(c), self.request)
199 199 html = formencode.htmlfill.render(
200 200 data,
201 201 defaults=self._form_defaults(),
202 202 encoding="UTF-8",
203 203 force_defaults=False
204 204 )
205 205 return Response(html)
206 206
207 207 @LoginRequired()
208 208 @HasPermissionAllDecorator('hg.admin')
209 209 @CSRFRequired()
210 210 @view_config(
211 211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 212 renderer='json_ext', xhr=True)
213 213 def settings_vcs_delete_svn_pattern(self):
214 214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 215 model = VcsSettingsModel()
216 216 try:
217 217 model.delete_global_svn_pattern(delete_pattern_id)
218 218 except SettingNotFound:
219 219 log.exception(
220 220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 221 raise HTTPNotFound()
222 222
223 223 Session().commit()
224 224 return True
225 225
226 226 @LoginRequired()
227 227 @HasPermissionAllDecorator('hg.admin')
228 228 @view_config(
229 229 route_name='admin_settings_mapping', request_method='GET',
230 230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 231 def settings_mapping(self):
232 232 c = self.load_default_context()
233 233 c.active = 'mapping'
234 234
235 235 data = render('rhodecode:templates/admin/settings/settings.mako',
236 236 self._get_template_context(c), self.request)
237 237 html = formencode.htmlfill.render(
238 238 data,
239 239 defaults=self._form_defaults(),
240 240 encoding="UTF-8",
241 241 force_defaults=False
242 242 )
243 243 return Response(html)
244 244
245 245 @LoginRequired()
246 246 @HasPermissionAllDecorator('hg.admin')
247 247 @CSRFRequired()
248 248 @view_config(
249 249 route_name='admin_settings_mapping_update', request_method='POST',
250 250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 251 def settings_mapping_update(self):
252 252 _ = self.request.translate
253 253 c = self.load_default_context()
254 254 c.active = 'mapping'
255 255 rm_obsolete = self.request.POST.get('destroy', False)
256 256 invalidate_cache = self.request.POST.get('invalidate', False)
257 257 log.debug(
258 258 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
259 259
260 260 if invalidate_cache:
261 261 log.debug('invalidating all repositories cache')
262 262 for repo in Repository.get_all():
263 263 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
264 264
265 265 filesystem_repos = ScmModel().repo_scan()
266 266 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
267 267 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
268 268 h.flash(_('Repositories successfully '
269 269 'rescanned added: %s ; removed: %s') %
270 270 (_repr(added), _repr(removed)),
271 271 category='success')
272 272 raise HTTPFound(h.route_path('admin_settings_mapping'))
273 273
274 274 @LoginRequired()
275 275 @HasPermissionAllDecorator('hg.admin')
276 276 @view_config(
277 277 route_name='admin_settings', request_method='GET',
278 278 renderer='rhodecode:templates/admin/settings/settings.mako')
279 279 @view_config(
280 280 route_name='admin_settings_global', request_method='GET',
281 281 renderer='rhodecode:templates/admin/settings/settings.mako')
282 282 def settings_global(self):
283 283 c = self.load_default_context()
284 284 c.active = 'global'
285 285 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 286 .get_personal_group_name_pattern()
287 287
288 288 data = render('rhodecode:templates/admin/settings/settings.mako',
289 289 self._get_template_context(c), self.request)
290 290 html = formencode.htmlfill.render(
291 291 data,
292 292 defaults=self._form_defaults(),
293 293 encoding="UTF-8",
294 294 force_defaults=False
295 295 )
296 296 return Response(html)
297 297
298 298 @LoginRequired()
299 299 @HasPermissionAllDecorator('hg.admin')
300 300 @CSRFRequired()
301 301 @view_config(
302 302 route_name='admin_settings_update', request_method='POST',
303 303 renderer='rhodecode:templates/admin/settings/settings.mako')
304 304 @view_config(
305 305 route_name='admin_settings_global_update', request_method='POST',
306 306 renderer='rhodecode:templates/admin/settings/settings.mako')
307 307 def settings_global_update(self):
308 308 _ = self.request.translate
309 309 c = self.load_default_context()
310 310 c.active = 'global'
311 311 c.personal_repo_group_default_pattern = RepoGroupModel()\
312 312 .get_personal_group_name_pattern()
313 313 application_form = ApplicationSettingsForm()()
314 314 try:
315 315 form_result = application_form.to_python(dict(self.request.POST))
316 316 except formencode.Invalid as errors:
317 317 data = render('rhodecode:templates/admin/settings/settings.mako',
318 318 self._get_template_context(c), self.request)
319 319 html = formencode.htmlfill.render(
320 320 data,
321 321 defaults=errors.value,
322 322 errors=errors.error_dict or {},
323 323 prefix_error=False,
324 324 encoding="UTF-8",
325 325 force_defaults=False
326 326 )
327 327 return Response(html)
328 328
329 329 settings = [
330 330 ('title', 'rhodecode_title', 'unicode'),
331 331 ('realm', 'rhodecode_realm', 'unicode'),
332 332 ('pre_code', 'rhodecode_pre_code', 'unicode'),
333 333 ('post_code', 'rhodecode_post_code', 'unicode'),
334 334 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
335 335 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
336 336 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
337 337 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
338 338 ]
339 339 try:
340 340 for setting, form_key, type_ in settings:
341 341 sett = SettingsModel().create_or_update_setting(
342 342 setting, form_result[form_key], type_)
343 343 Session().add(sett)
344 344
345 345 Session().commit()
346 346 SettingsModel().invalidate_settings_cache()
347 347 h.flash(_('Updated application settings'), category='success')
348 348 except Exception:
349 349 log.exception("Exception while updating application settings")
350 350 h.flash(
351 351 _('Error occurred during updating application settings'),
352 352 category='error')
353 353
354 354 raise HTTPFound(h.route_path('admin_settings_global'))
355 355
356 356 @LoginRequired()
357 357 @HasPermissionAllDecorator('hg.admin')
358 358 @view_config(
359 359 route_name='admin_settings_visual', request_method='GET',
360 360 renderer='rhodecode:templates/admin/settings/settings.mako')
361 361 def settings_visual(self):
362 362 c = self.load_default_context()
363 363 c.active = 'visual'
364 364
365 365 data = render('rhodecode:templates/admin/settings/settings.mako',
366 366 self._get_template_context(c), self.request)
367 367 html = formencode.htmlfill.render(
368 368 data,
369 369 defaults=self._form_defaults(),
370 370 encoding="UTF-8",
371 371 force_defaults=False
372 372 )
373 373 return Response(html)
374 374
375 375 @LoginRequired()
376 376 @HasPermissionAllDecorator('hg.admin')
377 377 @CSRFRequired()
378 378 @view_config(
379 379 route_name='admin_settings_visual_update', request_method='POST',
380 380 renderer='rhodecode:templates/admin/settings/settings.mako')
381 381 def settings_visual_update(self):
382 382 _ = self.request.translate
383 383 c = self.load_default_context()
384 384 c.active = 'visual'
385 385 application_form = ApplicationVisualisationForm()()
386 386 try:
387 387 form_result = application_form.to_python(dict(self.request.POST))
388 388 except formencode.Invalid as errors:
389 389 data = render('rhodecode:templates/admin/settings/settings.mako',
390 390 self._get_template_context(c), self.request)
391 391 html = formencode.htmlfill.render(
392 392 data,
393 393 defaults=errors.value,
394 394 errors=errors.error_dict or {},
395 395 prefix_error=False,
396 396 encoding="UTF-8",
397 397 force_defaults=False
398 398 )
399 399 return Response(html)
400 400
401 401 try:
402 402 settings = [
403 403 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
404 404 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
405 405 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
406 406 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
407 407 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
408 408 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
409 409 ('show_version', 'rhodecode_show_version', 'bool'),
410 410 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
411 411 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
412 412 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
413 413 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
414 414 ('support_url', 'rhodecode_support_url', 'unicode'),
415 415 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
416 416 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
417 417 ]
418 418 for setting, form_key, type_ in settings:
419 419 sett = SettingsModel().create_or_update_setting(
420 420 setting, form_result[form_key], type_)
421 421 Session().add(sett)
422 422
423 423 Session().commit()
424 424 SettingsModel().invalidate_settings_cache()
425 425 h.flash(_('Updated visualisation settings'), category='success')
426 426 except Exception:
427 427 log.exception("Exception updating visualization settings")
428 428 h.flash(_('Error occurred during updating '
429 429 'visualisation settings'),
430 430 category='error')
431 431
432 432 raise HTTPFound(h.route_path('admin_settings_visual'))
433 433
434 434 @LoginRequired()
435 435 @HasPermissionAllDecorator('hg.admin')
436 436 @view_config(
437 437 route_name='admin_settings_issuetracker', request_method='GET',
438 438 renderer='rhodecode:templates/admin/settings/settings.mako')
439 439 def settings_issuetracker(self):
440 440 c = self.load_default_context()
441 441 c.active = 'issuetracker'
442 442 defaults = SettingsModel().get_all_settings()
443 443
444 444 entry_key = 'rhodecode_issuetracker_pat_'
445 445
446 446 c.issuetracker_entries = {}
447 447 for k, v in defaults.items():
448 448 if k.startswith(entry_key):
449 449 uid = k[len(entry_key):]
450 450 c.issuetracker_entries[uid] = None
451 451
452 452 for uid in c.issuetracker_entries:
453 453 c.issuetracker_entries[uid] = AttributeDict({
454 454 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
455 455 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
456 456 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
457 457 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
458 458 })
459 459
460 460 return self._get_template_context(c)
461 461
462 462 @LoginRequired()
463 463 @HasPermissionAllDecorator('hg.admin')
464 464 @CSRFRequired()
465 465 @view_config(
466 466 route_name='admin_settings_issuetracker_test', request_method='POST',
467 467 renderer='string', xhr=True)
468 468 def settings_issuetracker_test(self):
469 469 return h.urlify_commit_message(
470 470 self.request.POST.get('test_text', ''),
471 471 'repo_group/test_repo1')
472 472
473 473 @LoginRequired()
474 474 @HasPermissionAllDecorator('hg.admin')
475 475 @CSRFRequired()
476 476 @view_config(
477 477 route_name='admin_settings_issuetracker_update', request_method='POST',
478 478 renderer='rhodecode:templates/admin/settings/settings.mako')
479 479 def settings_issuetracker_update(self):
480 480 _ = self.request.translate
481 481 self.load_default_context()
482 482 settings_model = IssueTrackerSettingsModel()
483 483
484 try:
484 485 form = IssueTrackerPatternsForm()().to_python(self.request.POST)
486 except formencode.Invalid as errors:
487 log.exception('Failed to add new pattern')
488 error = errors
489 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
490 category='error')
491 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
492
485 493 if form:
486 494 for uid in form.get('delete_patterns', []):
487 495 settings_model.delete_entries(uid)
488 496
489 497 for pattern in form.get('patterns', []):
490 498 for setting, value, type_ in pattern:
491 499 sett = settings_model.create_or_update_setting(
492 500 setting, value, type_)
493 501 Session().add(sett)
494 502
495 503 Session().commit()
496 504
497 505 SettingsModel().invalidate_settings_cache()
498 506 h.flash(_('Updated issue tracker entries'), category='success')
499 507 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
500 508
501 509 @LoginRequired()
502 510 @HasPermissionAllDecorator('hg.admin')
503 511 @CSRFRequired()
504 512 @view_config(
505 513 route_name='admin_settings_issuetracker_delete', request_method='POST',
506 514 renderer='rhodecode:templates/admin/settings/settings.mako')
507 515 def settings_issuetracker_delete(self):
508 516 _ = self.request.translate
509 517 self.load_default_context()
510 518 uid = self.request.POST.get('uid')
511 519 try:
512 520 IssueTrackerSettingsModel().delete_entries(uid)
513 521 except Exception:
514 522 log.exception('Failed to delete issue tracker setting %s', uid)
515 523 raise HTTPNotFound()
516 524 h.flash(_('Removed issue tracker entry'), category='success')
517 525 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
518 526
519 527 @LoginRequired()
520 528 @HasPermissionAllDecorator('hg.admin')
521 529 @view_config(
522 530 route_name='admin_settings_email', request_method='GET',
523 531 renderer='rhodecode:templates/admin/settings/settings.mako')
524 532 def settings_email(self):
525 533 c = self.load_default_context()
526 534 c.active = 'email'
527 535 c.rhodecode_ini = rhodecode.CONFIG
528 536
529 537 data = render('rhodecode:templates/admin/settings/settings.mako',
530 538 self._get_template_context(c), self.request)
531 539 html = formencode.htmlfill.render(
532 540 data,
533 541 defaults=self._form_defaults(),
534 542 encoding="UTF-8",
535 543 force_defaults=False
536 544 )
537 545 return Response(html)
538 546
539 547 @LoginRequired()
540 548 @HasPermissionAllDecorator('hg.admin')
541 549 @CSRFRequired()
542 550 @view_config(
543 551 route_name='admin_settings_email_update', request_method='POST',
544 552 renderer='rhodecode:templates/admin/settings/settings.mako')
545 553 def settings_email_update(self):
546 554 _ = self.request.translate
547 555 c = self.load_default_context()
548 556 c.active = 'email'
549 557
550 558 test_email = self.request.POST.get('test_email')
551 559
552 560 if not test_email:
553 561 h.flash(_('Please enter email address'), category='error')
554 562 raise HTTPFound(h.route_path('admin_settings_email'))
555 563
556 564 email_kwargs = {
557 565 'date': datetime.datetime.now(),
558 566 'user': c.rhodecode_user,
559 567 'rhodecode_version': c.rhodecode_version
560 568 }
561 569
562 570 (subject, headers, email_body,
563 571 email_body_plaintext) = EmailNotificationModel().render_email(
564 572 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
565 573
566 574 recipients = [test_email] if test_email else None
567 575
568 576 run_task(tasks.send_email, recipients, subject,
569 577 email_body_plaintext, email_body)
570 578
571 579 h.flash(_('Send email task created'), category='success')
572 580 raise HTTPFound(h.route_path('admin_settings_email'))
573 581
574 582 @LoginRequired()
575 583 @HasPermissionAllDecorator('hg.admin')
576 584 @view_config(
577 585 route_name='admin_settings_hooks', request_method='GET',
578 586 renderer='rhodecode:templates/admin/settings/settings.mako')
579 587 def settings_hooks(self):
580 588 c = self.load_default_context()
581 589 c.active = 'hooks'
582 590
583 591 model = SettingsModel()
584 592 c.hooks = model.get_builtin_hooks()
585 593 c.custom_hooks = model.get_custom_hooks()
586 594
587 595 data = render('rhodecode:templates/admin/settings/settings.mako',
588 596 self._get_template_context(c), self.request)
589 597 html = formencode.htmlfill.render(
590 598 data,
591 599 defaults=self._form_defaults(),
592 600 encoding="UTF-8",
593 601 force_defaults=False
594 602 )
595 603 return Response(html)
596 604
597 605 @LoginRequired()
598 606 @HasPermissionAllDecorator('hg.admin')
599 607 @CSRFRequired()
600 608 @view_config(
601 609 route_name='admin_settings_hooks_update', request_method='POST',
602 610 renderer='rhodecode:templates/admin/settings/settings.mako')
603 611 @view_config(
604 612 route_name='admin_settings_hooks_delete', request_method='POST',
605 613 renderer='rhodecode:templates/admin/settings/settings.mako')
606 614 def settings_hooks_update(self):
607 615 _ = self.request.translate
608 616 c = self.load_default_context()
609 617 c.active = 'hooks'
610 618 if c.visual.allow_custom_hooks_settings:
611 619 ui_key = self.request.POST.get('new_hook_ui_key')
612 620 ui_value = self.request.POST.get('new_hook_ui_value')
613 621
614 622 hook_id = self.request.POST.get('hook_id')
615 623 new_hook = False
616 624
617 625 model = SettingsModel()
618 626 try:
619 627 if ui_value and ui_key:
620 628 model.create_or_update_hook(ui_key, ui_value)
621 629 h.flash(_('Added new hook'), category='success')
622 630 new_hook = True
623 631 elif hook_id:
624 632 RhodeCodeUi.delete(hook_id)
625 633 Session().commit()
626 634
627 635 # check for edits
628 636 update = False
629 637 _d = self.request.POST.dict_of_lists()
630 638 for k, v in zip(_d.get('hook_ui_key', []),
631 639 _d.get('hook_ui_value_new', [])):
632 640 model.create_or_update_hook(k, v)
633 641 update = True
634 642
635 643 if update and not new_hook:
636 644 h.flash(_('Updated hooks'), category='success')
637 645 Session().commit()
638 646 except Exception:
639 647 log.exception("Exception during hook creation")
640 648 h.flash(_('Error occurred during hook creation'),
641 649 category='error')
642 650
643 651 raise HTTPFound(h.route_path('admin_settings_hooks'))
644 652
645 653 @LoginRequired()
646 654 @HasPermissionAllDecorator('hg.admin')
647 655 @view_config(
648 656 route_name='admin_settings_search', request_method='GET',
649 657 renderer='rhodecode:templates/admin/settings/settings.mako')
650 658 def settings_search(self):
651 659 c = self.load_default_context()
652 660 c.active = 'search'
653 661
654 662 searcher = searcher_from_config(self.request.registry.settings)
655 663 c.statistics = searcher.statistics()
656 664
657 665 return self._get_template_context(c)
658 666
659 667 @LoginRequired()
660 668 @HasPermissionAllDecorator('hg.admin')
661 669 @view_config(
662 670 route_name='admin_settings_labs', request_method='GET',
663 671 renderer='rhodecode:templates/admin/settings/settings.mako')
664 672 def settings_labs(self):
665 673 c = self.load_default_context()
666 674 if not c.labs_active:
667 675 raise HTTPFound(h.route_path('admin_settings'))
668 676
669 677 c.active = 'labs'
670 678 c.lab_settings = _LAB_SETTINGS
671 679
672 680 data = render('rhodecode:templates/admin/settings/settings.mako',
673 681 self._get_template_context(c), self.request)
674 682 html = formencode.htmlfill.render(
675 683 data,
676 684 defaults=self._form_defaults(),
677 685 encoding="UTF-8",
678 686 force_defaults=False
679 687 )
680 688 return Response(html)
681 689
682 690 @LoginRequired()
683 691 @HasPermissionAllDecorator('hg.admin')
684 692 @CSRFRequired()
685 693 @view_config(
686 694 route_name='admin_settings_labs_update', request_method='POST',
687 695 renderer='rhodecode:templates/admin/settings/settings.mako')
688 696 def settings_labs_update(self):
689 697 _ = self.request.translate
690 698 c = self.load_default_context()
691 699 c.active = 'labs'
692 700
693 701 application_form = LabsSettingsForm()()
694 702 try:
695 703 form_result = application_form.to_python(dict(self.request.POST))
696 704 except formencode.Invalid as errors:
697 705 h.flash(
698 706 _('Some form inputs contain invalid data.'),
699 707 category='error')
700 708 data = render('rhodecode:templates/admin/settings/settings.mako',
701 709 self._get_template_context(c), self.request)
702 710 html = formencode.htmlfill.render(
703 711 data,
704 712 defaults=errors.value,
705 713 errors=errors.error_dict or {},
706 714 prefix_error=False,
707 715 encoding="UTF-8",
708 716 force_defaults=False
709 717 )
710 718 return Response(html)
711 719
712 720 try:
713 721 session = Session()
714 722 for setting in _LAB_SETTINGS:
715 723 setting_name = setting.key[len('rhodecode_'):]
716 724 sett = SettingsModel().create_or_update_setting(
717 725 setting_name, form_result[setting.key], setting.type)
718 726 session.add(sett)
719 727
720 728 except Exception:
721 729 log.exception('Exception while updating lab settings')
722 730 h.flash(_('Error occurred during updating labs settings'),
723 731 category='error')
724 732 else:
725 733 Session().commit()
726 734 SettingsModel().invalidate_settings_cache()
727 735 h.flash(_('Updated Labs settings'), category='success')
728 736 raise HTTPFound(h.route_path('admin_settings_labs'))
729 737
730 738 data = render('rhodecode:templates/admin/settings/settings.mako',
731 739 self._get_template_context(c), self.request)
732 740 html = formencode.htmlfill.render(
733 741 data,
734 742 defaults=self._form_defaults(),
735 743 encoding="UTF-8",
736 744 force_defaults=False
737 745 )
738 746 return Response(html)
739 747
740 748
741 749 # :param key: name of the setting including the 'rhodecode_' prefix
742 750 # :param type: the RhodeCodeSetting type to use.
743 751 # :param group: the i18ned group in which we should dispaly this setting
744 752 # :param label: the i18ned label we should display for this setting
745 753 # :param help: the i18ned help we should dispaly for this setting
746 754 LabSetting = collections.namedtuple(
747 755 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
748 756
749 757
750 758 # This list has to be kept in sync with the form
751 759 # rhodecode.model.forms.LabsSettingsForm.
752 760 _LAB_SETTINGS = [
753 761
754 762 ]
@@ -1,148 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.lib.utils2 import md5
24 24 from rhodecode.model.db import Repository
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
27 27
28 28
29 29 def route_path(name, params=None, **kwargs):
30 30 import urllib
31 31
32 32 base_url = {
33 33 'repo_summary': '/{repo_name}',
34 34 'edit_repo_issuetracker': '/{repo_name}/settings/issue_trackers',
35 35 'edit_repo_issuetracker_test': '/{repo_name}/settings/issue_trackers/test',
36 36 'edit_repo_issuetracker_delete': '/{repo_name}/settings/issue_trackers/delete',
37 37 'edit_repo_issuetracker_update': '/{repo_name}/settings/issue_trackers/update',
38 38 }[name].format(**kwargs)
39 39
40 40 if params:
41 41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 42 return base_url
43 43
44 44
45 45 @pytest.mark.usefixtures("app")
46 46 class TestRepoIssueTracker(object):
47 47 def test_issuetracker_index(self, autologin_user, backend):
48 48 repo = backend.create_repo()
49 49 response = self.app.get(route_path('edit_repo_issuetracker',
50 50 repo_name=repo.repo_name))
51 51 assert response.status_code == 200
52 52
53 53 def test_add_and_test_issuetracker_patterns(
54 54 self, autologin_user, backend, csrf_token, request, xhr_header):
55 55 pattern = 'issuetracker_pat'
56 56 another_pattern = pattern+'1'
57 57 post_url = route_path(
58 58 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
59 59 post_data = {
60 60 'new_pattern_pattern_0': pattern,
61 'new_pattern_url_0': 'url',
61 'new_pattern_url_0': 'http://url',
62 62 'new_pattern_prefix_0': 'prefix',
63 63 'new_pattern_description_0': 'description',
64 64 'new_pattern_pattern_1': another_pattern,
65 'new_pattern_url_1': 'url1',
65 'new_pattern_url_1': '/url1',
66 66 'new_pattern_prefix_1': 'prefix1',
67 67 'new_pattern_description_1': 'description1',
68 68 'csrf_token': csrf_token
69 69 }
70 70 self.app.post(post_url, post_data, status=302)
71 71 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
72 72 settings = self.settings_model.get_repo_settings()
73 73 self.uid = md5(pattern)
74 74 assert settings[self.uid]['pat'] == pattern
75 75 self.another_uid = md5(another_pattern)
76 76 assert settings[self.another_uid]['pat'] == another_pattern
77 77
78 78 # test pattern
79 79 data = {'test_text': 'example of issuetracker_pat replacement',
80 80 'csrf_token': csrf_token}
81 81 response = self.app.post(
82 82 route_path('edit_repo_issuetracker_test',
83 83 repo_name=backend.repo.repo_name),
84 84 extra_environ=xhr_header, params=data)
85 85
86 86 assert response.body == \
87 'example of <a class="issue-tracker-link" href="url">prefix</a> replacement'
87 'example of <a class="issue-tracker-link" href="http://url">prefix</a> replacement'
88 88
89 89 @request.addfinalizer
90 90 def cleanup():
91 91 self.settings_model.delete_entries(self.uid)
92 92 self.settings_model.delete_entries(self.another_uid)
93 93
94 94 def test_edit_issuetracker_pattern(
95 95 self, autologin_user, backend, csrf_token, request):
96 96 entry_key = 'issuetracker_pat_'
97 97 pattern = 'issuetracker_pat2'
98 98 old_pattern = 'issuetracker_pat'
99 99 old_uid = md5(old_pattern)
100 100
101 101 sett = SettingsModel(repo=backend.repo).create_or_update_setting(
102 102 entry_key+old_uid, old_pattern, 'unicode')
103 103 Session().add(sett)
104 104 Session().commit()
105 105 post_url = route_path(
106 106 'edit_repo_issuetracker_update', repo_name=backend.repo.repo_name)
107 107 post_data = {
108 108 'new_pattern_pattern_0': pattern,
109 'new_pattern_url_0': 'url',
109 'new_pattern_url_0': '/url',
110 110 'new_pattern_prefix_0': 'prefix',
111 111 'new_pattern_description_0': 'description',
112 112 'uid': old_uid,
113 113 'csrf_token': csrf_token
114 114 }
115 115 self.app.post(post_url, post_data, status=302)
116 116 self.settings_model = IssueTrackerSettingsModel(repo=backend.repo)
117 117 settings = self.settings_model.get_repo_settings()
118 118 self.uid = md5(pattern)
119 119 assert settings[self.uid]['pat'] == pattern
120 120 with pytest.raises(KeyError):
121 121 key = settings[old_uid]
122 122
123 123 @request.addfinalizer
124 124 def cleanup():
125 125 self.settings_model.delete_entries(self.uid)
126 126
127 127 def test_delete_issuetracker_pattern(
128 128 self, autologin_user, backend, csrf_token, settings_util):
129 129 repo = backend.create_repo()
130 130 repo_name = repo.repo_name
131 131 entry_key = 'issuetracker_pat_'
132 132 pattern = 'issuetracker_pat3'
133 133 uid = md5(pattern)
134 134 settings_util.create_repo_rhodecode_setting(
135 135 repo=backend.repo, name=entry_key+uid,
136 136 value=entry_key, type_='unicode', cleanup=False)
137 137
138 138 self.app.post(
139 139 route_path(
140 140 'edit_repo_issuetracker_delete',
141 141 repo_name=backend.repo.repo_name),
142 142 {
143 143 'uid': uid,
144 144 'csrf_token': csrf_token
145 145 }, status=302)
146 146 settings = IssueTrackerSettingsModel(
147 147 repo=Repository.get_by_repo_name(repo_name)).get_repo_settings()
148 148 assert 'rhodecode_%s%s' % (entry_key, uid) not in settings
@@ -1,126 +1,137 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24 from pyramid.view import view_config
25 import formencode
25 26
26 27 from rhodecode.apps._base import RepoAppView
27 28 from rhodecode.lib import audit_logger
28 29 from rhodecode.lib import helpers as h
29 30 from rhodecode.lib.auth import (
30 31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 32 from rhodecode.model.forms import IssueTrackerPatternsForm
32 33 from rhodecode.model.meta import Session
33 34 from rhodecode.model.settings import IssueTrackerSettingsModel
34 35
35 36 log = logging.getLogger(__name__)
36 37
37 38
38 39 class RepoSettingsIssueTrackersView(RepoAppView):
39 40 def load_default_context(self):
40 41 c = self._get_local_tmpl_context()
41 42
42 43 self._register_global_c(c)
43 44 return c
44 45
45 46 @LoginRequired()
46 47 @HasRepoPermissionAnyDecorator('repository.admin')
47 48 @view_config(
48 49 route_name='edit_repo_issuetracker', request_method='GET',
49 50 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
50 51 def repo_issuetracker(self):
51 52 c = self.load_default_context()
52 53 c.active = 'issuetracker'
53 54 c.data = 'data'
54 55
55 56 c.settings_model = IssueTrackerSettingsModel(repo=self.db_repo)
56 57 c.global_patterns = c.settings_model.get_global_settings()
57 58 c.repo_patterns = c.settings_model.get_repo_settings()
58 59
59 60 return self._get_template_context(c)
60 61
61 62 @LoginRequired()
62 63 @HasRepoPermissionAnyDecorator('repository.admin')
63 64 @CSRFRequired()
64 65 @view_config(
65 66 route_name='edit_repo_issuetracker_test', request_method='POST',
66 67 xhr=True, renderer='string')
67 68 def repo_issuetracker_test(self):
68 69 return h.urlify_commit_message(
69 70 self.request.POST.get('test_text', ''),
70 71 self.db_repo_name)
71 72
72 73 @LoginRequired()
73 74 @HasRepoPermissionAnyDecorator('repository.admin')
74 75 @CSRFRequired()
75 76 @view_config(
76 77 route_name='edit_repo_issuetracker_delete', request_method='POST',
77 78 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
78 79 def repo_issuetracker_delete(self):
79 80 _ = self.request.translate
80 81 uid = self.request.POST.get('uid')
81 82 repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name)
82 83 try:
83 84 repo_settings.delete_entries(uid)
84 85 except Exception:
85 86 h.flash(_('Error occurred during deleting issue tracker entry'),
86 87 category='error')
87 88 else:
88 89 h.flash(_('Removed issue tracker entry'), category='success')
89 90 raise HTTPFound(
90 91 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
91 92
92 93 def _update_patterns(self, form, repo_settings):
93 94 for uid in form['delete_patterns']:
94 95 repo_settings.delete_entries(uid)
95 96
96 97 for pattern_data in form['patterns']:
97 98 for setting_key, pattern, type_ in pattern_data:
98 99 sett = repo_settings.create_or_update_setting(
99 100 setting_key, pattern.strip(), type_)
100 101 Session().add(sett)
101 102
102 103 Session().commit()
103 104
104 105 @LoginRequired()
105 106 @HasRepoPermissionAnyDecorator('repository.admin')
106 107 @CSRFRequired()
107 108 @view_config(
108 109 route_name='edit_repo_issuetracker_update', request_method='POST',
109 110 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
110 111 def repo_issuetracker_update(self):
111 112 _ = self.request.translate
112 113 # Save inheritance
113 114 repo_settings = IssueTrackerSettingsModel(repo=self.db_repo_name)
114 115 inherited = (
115 116 self.request.POST.get('inherit_global_issuetracker') == "inherited")
116 117 repo_settings.inherit_global_settings = inherited
117 118 Session().commit()
118 119
120 try:
119 121 form = IssueTrackerPatternsForm()().to_python(self.request.POST)
122 except formencode.Invalid as errors:
123 log.exception('Failed to add new pattern')
124 error = errors
125 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
126 category='error')
127 raise HTTPFound(
128 h.route_path('edit_repo_issuetracker',
129 repo_name=self.db_repo_name))
130
120 131 if form:
121 132 self._update_patterns(form, repo_settings)
122 133
123 134 h.flash(_('Updated issue tracker entries'), category='success')
124 135 raise HTTPFound(
125 136 h.route_path('edit_repo_issuetracker', repo_name=self.db_repo_name))
126 137
@@ -1,1122 +1,1132 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Set of generic validators
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 from collections import defaultdict
29 29
30 30 import formencode
31 31 import ipaddress
32 32 from formencode.validators import (
33 33 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
34 34 NotEmpty, IPAddress, CIDR, String, FancyValidator
35 35 )
36 36 from pylons.i18n.translation import _
37 37 from sqlalchemy.sql.expression import true
38 38 from sqlalchemy.util import OrderedSet
39 39 from webhelpers.pylonslib.secure_form import authentication_token
40 40
41 41 from rhodecode.authentication import (
42 42 legacy_plugin_prefix, _import_legacy_plugin)
43 43 from rhodecode.authentication.base import loadplugin
44 44 from rhodecode.apps._base import ADMIN_PREFIX
45 45 from rhodecode.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
46 46 from rhodecode.lib.utils import repo_name_slug, make_db_config
47 47 from rhodecode.lib.utils2 import safe_int, str2bool, aslist, md5, safe_unicode
48 48 from rhodecode.lib.vcs.backends.git.repository import GitRepository
49 49 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
50 50 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
51 51 from rhodecode.model.db import (
52 52 RepoGroup, Repository, UserGroup, User, ChangesetStatus, Gist)
53 53 from rhodecode.model.settings import VcsSettingsModel
54 54
55 55 # silence warnings and pylint
56 56 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
57 57 NotEmpty, IPAddress, CIDR, String, FancyValidator
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class _Missing(object):
63 63 pass
64 64
65 65 Missing = _Missing()
66 66
67 67
68 68 class StateObj(object):
69 69 """
70 70 this is needed to translate the messages using _() in validators
71 71 """
72 72 _ = staticmethod(_)
73 73
74 74
75 75 def M(self, key, state=None, **kwargs):
76 76 """
77 77 returns string from self.message based on given key,
78 78 passed kw params are used to substitute %(named)s params inside
79 79 translated strings
80 80
81 81 :param msg:
82 82 :param state:
83 83 """
84 84 if state is None:
85 85 state = StateObj()
86 86 else:
87 87 state._ = staticmethod(_)
88 88 # inject validator into state object
89 89 return self.message(key, state, **kwargs)
90 90
91 91
92 92 def UniqueList(convert=None):
93 93 class _UniqueList(formencode.FancyValidator):
94 94 """
95 95 Unique List !
96 96 """
97 97 messages = {
98 98 'empty': _(u'Value cannot be an empty list'),
99 99 'missing_value': _(u'Value cannot be an empty list'),
100 100 }
101 101
102 102 def _to_python(self, value, state):
103 103 ret_val = []
104 104
105 105 def make_unique(value):
106 106 seen = []
107 107 return [c for c in value if not (c in seen or seen.append(c))]
108 108
109 109 if isinstance(value, list):
110 110 ret_val = make_unique(value)
111 111 elif isinstance(value, set):
112 112 ret_val = make_unique(list(value))
113 113 elif isinstance(value, tuple):
114 114 ret_val = make_unique(list(value))
115 115 elif value is None:
116 116 ret_val = []
117 117 else:
118 118 ret_val = [value]
119 119
120 120 if convert:
121 121 ret_val = map(convert, ret_val)
122 122 return ret_val
123 123
124 124 def empty_value(self, value):
125 125 return []
126 126
127 127 return _UniqueList
128 128
129 129
130 130 def UniqueListFromString():
131 131 class _UniqueListFromString(UniqueList()):
132 132 def _to_python(self, value, state):
133 133 if isinstance(value, basestring):
134 134 value = aslist(value, ',')
135 135 return super(_UniqueListFromString, self)._to_python(value, state)
136 136 return _UniqueListFromString
137 137
138 138
139 139 def ValidSvnPattern(section, repo_name=None):
140 140 class _validator(formencode.validators.FancyValidator):
141 141 messages = {
142 142 'pattern_exists': _(u'Pattern already exists'),
143 143 }
144 144
145 145 def validate_python(self, value, state):
146 146 if not value:
147 147 return
148 148 model = VcsSettingsModel(repo=repo_name)
149 149 ui_settings = model.get_svn_patterns(section=section)
150 150 for entry in ui_settings:
151 151 if value == entry.value:
152 152 msg = M(self, 'pattern_exists', state)
153 153 raise formencode.Invalid(msg, value, state)
154 154 return _validator
155 155
156 156
157 157 def ValidUsername(edit=False, old_data={}):
158 158 class _validator(formencode.validators.FancyValidator):
159 159 messages = {
160 160 'username_exists': _(u'Username "%(username)s" already exists'),
161 161 'system_invalid_username':
162 162 _(u'Username "%(username)s" is forbidden'),
163 163 'invalid_username':
164 164 _(u'Username may only contain alphanumeric characters '
165 165 u'underscores, periods or dashes and must begin with '
166 166 u'alphanumeric character or underscore')
167 167 }
168 168
169 169 def validate_python(self, value, state):
170 170 if value in ['default', 'new_user']:
171 171 msg = M(self, 'system_invalid_username', state, username=value)
172 172 raise formencode.Invalid(msg, value, state)
173 173 # check if user is unique
174 174 old_un = None
175 175 if edit:
176 176 old_un = User.get(old_data.get('user_id')).username
177 177
178 178 if old_un != value or not edit:
179 179 if User.get_by_username(value, case_insensitive=True):
180 180 msg = M(self, 'username_exists', state, username=value)
181 181 raise formencode.Invalid(msg, value, state)
182 182
183 183 if (re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value)
184 184 is None):
185 185 msg = M(self, 'invalid_username', state)
186 186 raise formencode.Invalid(msg, value, state)
187 187 return _validator
188 188
189 189
190 190 def ValidRegex(msg=None):
191 191 class _validator(formencode.validators.Regex):
192 192 messages = {'invalid': msg or _(u'The input is not valid')}
193 193 return _validator
194 194
195 195
196 196 def ValidRepoUser(allow_disabled=False):
197 197 class _validator(formencode.validators.FancyValidator):
198 198 messages = {
199 199 'invalid_username': _(u'Username %(username)s is not valid'),
200 200 'disabled_username': _(u'Username %(username)s is disabled')
201 201 }
202 202
203 203 def validate_python(self, value, state):
204 204 try:
205 205 user = User.query().filter(User.username == value).one()
206 206 except Exception:
207 207 msg = M(self, 'invalid_username', state, username=value)
208 208 raise formencode.Invalid(
209 209 msg, value, state, error_dict={'username': msg}
210 210 )
211 211 if user and (not allow_disabled and not user.active):
212 212 msg = M(self, 'disabled_username', state, username=value)
213 213 raise formencode.Invalid(
214 214 msg, value, state, error_dict={'username': msg}
215 215 )
216 216
217 217 return _validator
218 218
219 219
220 220 def ValidUserGroup(edit=False, old_data={}):
221 221 class _validator(formencode.validators.FancyValidator):
222 222 messages = {
223 223 'invalid_group': _(u'Invalid user group name'),
224 224 'group_exist': _(u'User group `%(usergroup)s` already exists'),
225 225 'invalid_usergroup_name':
226 226 _(u'user group name may only contain alphanumeric '
227 227 u'characters underscores, periods or dashes and must begin '
228 228 u'with alphanumeric character')
229 229 }
230 230
231 231 def validate_python(self, value, state):
232 232 if value in ['default']:
233 233 msg = M(self, 'invalid_group', state)
234 234 raise formencode.Invalid(
235 235 msg, value, state, error_dict={'users_group_name': msg}
236 236 )
237 237 # check if group is unique
238 238 old_ugname = None
239 239 if edit:
240 240 old_id = old_data.get('users_group_id')
241 241 old_ugname = UserGroup.get(old_id).users_group_name
242 242
243 243 if old_ugname != value or not edit:
244 244 is_existing_group = UserGroup.get_by_group_name(
245 245 value, case_insensitive=True)
246 246 if is_existing_group:
247 247 msg = M(self, 'group_exist', state, usergroup=value)
248 248 raise formencode.Invalid(
249 249 msg, value, state, error_dict={'users_group_name': msg}
250 250 )
251 251
252 252 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
253 253 msg = M(self, 'invalid_usergroup_name', state)
254 254 raise formencode.Invalid(
255 255 msg, value, state, error_dict={'users_group_name': msg}
256 256 )
257 257
258 258 return _validator
259 259
260 260
261 261 def ValidRepoGroup(edit=False, old_data={}, can_create_in_root=False):
262 262 class _validator(formencode.validators.FancyValidator):
263 263 messages = {
264 264 'group_parent_id': _(u'Cannot assign this group as parent'),
265 265 'group_exists': _(u'Group "%(group_name)s" already exists'),
266 266 'repo_exists': _(u'Repository with name "%(group_name)s" '
267 267 u'already exists'),
268 268 'permission_denied': _(u"no permission to store repository group"
269 269 u"in this location"),
270 270 'permission_denied_root': _(
271 271 u"no permission to store repository group "
272 272 u"in root location")
273 273 }
274 274
275 275 def _to_python(self, value, state):
276 276 group_name = repo_name_slug(value.get('group_name', ''))
277 277 group_parent_id = safe_int(value.get('group_parent_id'))
278 278 gr = RepoGroup.get(group_parent_id)
279 279 if gr:
280 280 parent_group_path = gr.full_path
281 281 # value needs to be aware of group name in order to check
282 282 # db key This is an actual just the name to store in the
283 283 # database
284 284 group_name_full = (
285 285 parent_group_path + RepoGroup.url_sep() + group_name)
286 286 else:
287 287 group_name_full = group_name
288 288
289 289 value['group_name'] = group_name
290 290 value['group_name_full'] = group_name_full
291 291 value['group_parent_id'] = group_parent_id
292 292 return value
293 293
294 294 def validate_python(self, value, state):
295 295
296 296 old_group_name = None
297 297 group_name = value.get('group_name')
298 298 group_name_full = value.get('group_name_full')
299 299 group_parent_id = safe_int(value.get('group_parent_id'))
300 300 if group_parent_id == -1:
301 301 group_parent_id = None
302 302
303 303 group_obj = RepoGroup.get(old_data.get('group_id'))
304 304 parent_group_changed = False
305 305 if edit:
306 306 old_group_name = group_obj.group_name
307 307 old_group_parent_id = group_obj.group_parent_id
308 308
309 309 if group_parent_id != old_group_parent_id:
310 310 parent_group_changed = True
311 311
312 312 # TODO: mikhail: the following if statement is not reached
313 313 # since group_parent_id's OneOf validation fails before.
314 314 # Can be removed.
315 315
316 316 # check against setting a parent of self
317 317 parent_of_self = (
318 318 old_data['group_id'] == group_parent_id
319 319 if group_parent_id else False
320 320 )
321 321 if parent_of_self:
322 322 msg = M(self, 'group_parent_id', state)
323 323 raise formencode.Invalid(
324 324 msg, value, state, error_dict={'group_parent_id': msg}
325 325 )
326 326
327 327 # group we're moving current group inside
328 328 child_group = None
329 329 if group_parent_id:
330 330 child_group = RepoGroup.query().filter(
331 331 RepoGroup.group_id == group_parent_id).scalar()
332 332
333 333 # do a special check that we cannot move a group to one of
334 334 # it's children
335 335 if edit and child_group:
336 336 parents = [x.group_id for x in child_group.parents]
337 337 move_to_children = old_data['group_id'] in parents
338 338 if move_to_children:
339 339 msg = M(self, 'group_parent_id', state)
340 340 raise formencode.Invalid(
341 341 msg, value, state, error_dict={'group_parent_id': msg})
342 342
343 343 # Check if we have permission to store in the parent.
344 344 # Only check if the parent group changed.
345 345 if parent_group_changed:
346 346 if child_group is None:
347 347 if not can_create_in_root:
348 348 msg = M(self, 'permission_denied_root', state)
349 349 raise formencode.Invalid(
350 350 msg, value, state,
351 351 error_dict={'group_parent_id': msg})
352 352 else:
353 353 valid = HasRepoGroupPermissionAny('group.admin')
354 354 forbidden = not valid(
355 355 child_group.group_name, 'can create group validator')
356 356 if forbidden:
357 357 msg = M(self, 'permission_denied', state)
358 358 raise formencode.Invalid(
359 359 msg, value, state,
360 360 error_dict={'group_parent_id': msg})
361 361
362 362 # if we change the name or it's new group, check for existing names
363 363 # or repositories with the same name
364 364 if old_group_name != group_name_full or not edit:
365 365 # check group
366 366 gr = RepoGroup.get_by_group_name(group_name_full)
367 367 if gr:
368 368 msg = M(self, 'group_exists', state, group_name=group_name)
369 369 raise formencode.Invalid(
370 370 msg, value, state, error_dict={'group_name': msg})
371 371
372 372 # check for same repo
373 373 repo = Repository.get_by_repo_name(group_name_full)
374 374 if repo:
375 375 msg = M(self, 'repo_exists', state, group_name=group_name)
376 376 raise formencode.Invalid(
377 377 msg, value, state, error_dict={'group_name': msg})
378 378
379 379 return _validator
380 380
381 381
382 382 def ValidPassword():
383 383 class _validator(formencode.validators.FancyValidator):
384 384 messages = {
385 385 'invalid_password':
386 386 _(u'Invalid characters (non-ascii) in password')
387 387 }
388 388
389 389 def validate_python(self, value, state):
390 390 try:
391 391 (value or '').decode('ascii')
392 392 except UnicodeError:
393 393 msg = M(self, 'invalid_password', state)
394 394 raise formencode.Invalid(msg, value, state,)
395 395 return _validator
396 396
397 397
398 398 def ValidOldPassword(username):
399 399 class _validator(formencode.validators.FancyValidator):
400 400 messages = {
401 401 'invalid_password': _(u'Invalid old password')
402 402 }
403 403
404 404 def validate_python(self, value, state):
405 405 from rhodecode.authentication.base import authenticate, HTTP_TYPE
406 406 if not authenticate(username, value, '', HTTP_TYPE):
407 407 msg = M(self, 'invalid_password', state)
408 408 raise formencode.Invalid(
409 409 msg, value, state, error_dict={'current_password': msg}
410 410 )
411 411 return _validator
412 412
413 413
414 414 def ValidPasswordsMatch(
415 415 passwd='new_password', passwd_confirmation='password_confirmation'):
416 416 class _validator(formencode.validators.FancyValidator):
417 417 messages = {
418 418 'password_mismatch': _(u'Passwords do not match'),
419 419 }
420 420
421 421 def validate_python(self, value, state):
422 422
423 423 pass_val = value.get('password') or value.get(passwd)
424 424 if pass_val != value[passwd_confirmation]:
425 425 msg = M(self, 'password_mismatch', state)
426 426 raise formencode.Invalid(
427 427 msg, value, state,
428 428 error_dict={passwd: msg, passwd_confirmation: msg}
429 429 )
430 430 return _validator
431 431
432 432
433 433 def ValidAuth():
434 434 class _validator(formencode.validators.FancyValidator):
435 435 messages = {
436 436 'invalid_password': _(u'invalid password'),
437 437 'invalid_username': _(u'invalid user name'),
438 438 'disabled_account': _(u'Your account is disabled')
439 439 }
440 440
441 441 def validate_python(self, value, state):
442 442 from rhodecode.authentication.base import authenticate, HTTP_TYPE
443 443
444 444 password = value['password']
445 445 username = value['username']
446 446
447 447 if not authenticate(username, password, '', HTTP_TYPE,
448 448 skip_missing=True):
449 449 user = User.get_by_username(username)
450 450 if user and not user.active:
451 451 log.warning('user %s is disabled', username)
452 452 msg = M(self, 'disabled_account', state)
453 453 raise formencode.Invalid(
454 454 msg, value, state, error_dict={'username': msg}
455 455 )
456 456 else:
457 457 log.warning('user `%s` failed to authenticate', username)
458 458 msg = M(self, 'invalid_username', state)
459 459 msg2 = M(self, 'invalid_password', state)
460 460 raise formencode.Invalid(
461 461 msg, value, state,
462 462 error_dict={'username': msg, 'password': msg2}
463 463 )
464 464 return _validator
465 465
466 466
467 467 def ValidAuthToken():
468 468 class _validator(formencode.validators.FancyValidator):
469 469 messages = {
470 470 'invalid_token': _(u'Token mismatch')
471 471 }
472 472
473 473 def validate_python(self, value, state):
474 474 if value != authentication_token():
475 475 msg = M(self, 'invalid_token', state)
476 476 raise formencode.Invalid(msg, value, state)
477 477 return _validator
478 478
479 479
480 480 def ValidRepoName(edit=False, old_data={}):
481 481 class _validator(formencode.validators.FancyValidator):
482 482 messages = {
483 483 'invalid_repo_name':
484 484 _(u'Repository name %(repo)s is disallowed'),
485 485 # top level
486 486 'repository_exists': _(u'Repository with name %(repo)s '
487 487 u'already exists'),
488 488 'group_exists': _(u'Repository group with name "%(repo)s" '
489 489 u'already exists'),
490 490 # inside a group
491 491 'repository_in_group_exists': _(u'Repository with name %(repo)s '
492 492 u'exists in group "%(group)s"'),
493 493 'group_in_group_exists': _(
494 494 u'Repository group with name "%(repo)s" '
495 495 u'exists in group "%(group)s"'),
496 496 }
497 497
498 498 def _to_python(self, value, state):
499 499 repo_name = repo_name_slug(value.get('repo_name', ''))
500 500 repo_group = value.get('repo_group')
501 501 if repo_group:
502 502 gr = RepoGroup.get(repo_group)
503 503 group_path = gr.full_path
504 504 group_name = gr.group_name
505 505 # value needs to be aware of group name in order to check
506 506 # db key This is an actual just the name to store in the
507 507 # database
508 508 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
509 509 else:
510 510 group_name = group_path = ''
511 511 repo_name_full = repo_name
512 512
513 513 value['repo_name'] = repo_name
514 514 value['repo_name_full'] = repo_name_full
515 515 value['group_path'] = group_path
516 516 value['group_name'] = group_name
517 517 return value
518 518
519 519 def validate_python(self, value, state):
520 520
521 521 repo_name = value.get('repo_name')
522 522 repo_name_full = value.get('repo_name_full')
523 523 group_path = value.get('group_path')
524 524 group_name = value.get('group_name')
525 525
526 526 if repo_name in [ADMIN_PREFIX, '']:
527 527 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
528 528 raise formencode.Invalid(
529 529 msg, value, state, error_dict={'repo_name': msg})
530 530
531 531 rename = old_data.get('repo_name') != repo_name_full
532 532 create = not edit
533 533 if rename or create:
534 534
535 535 if group_path:
536 536 if Repository.get_by_repo_name(repo_name_full):
537 537 msg = M(self, 'repository_in_group_exists', state,
538 538 repo=repo_name, group=group_name)
539 539 raise formencode.Invalid(
540 540 msg, value, state, error_dict={'repo_name': msg})
541 541 if RepoGroup.get_by_group_name(repo_name_full):
542 542 msg = M(self, 'group_in_group_exists', state,
543 543 repo=repo_name, group=group_name)
544 544 raise formencode.Invalid(
545 545 msg, value, state, error_dict={'repo_name': msg})
546 546 else:
547 547 if RepoGroup.get_by_group_name(repo_name_full):
548 548 msg = M(self, 'group_exists', state, repo=repo_name)
549 549 raise formencode.Invalid(
550 550 msg, value, state, error_dict={'repo_name': msg})
551 551
552 552 if Repository.get_by_repo_name(repo_name_full):
553 553 msg = M(
554 554 self, 'repository_exists', state, repo=repo_name)
555 555 raise formencode.Invalid(
556 556 msg, value, state, error_dict={'repo_name': msg})
557 557 return value
558 558 return _validator
559 559
560 560
561 561 def ValidForkName(*args, **kwargs):
562 562 return ValidRepoName(*args, **kwargs)
563 563
564 564
565 565 def SlugifyName():
566 566 class _validator(formencode.validators.FancyValidator):
567 567
568 568 def _to_python(self, value, state):
569 569 return repo_name_slug(value)
570 570
571 571 def validate_python(self, value, state):
572 572 pass
573 573
574 574 return _validator
575 575
576 576
577 577 def CannotHaveGitSuffix():
578 578 class _validator(formencode.validators.FancyValidator):
579 579 messages = {
580 580 'has_git_suffix':
581 581 _(u'Repository name cannot end with .git'),
582 582 }
583 583
584 584 def _to_python(self, value, state):
585 585 return value
586 586
587 587 def validate_python(self, value, state):
588 588 if value and value.endswith('.git'):
589 589 msg = M(
590 590 self, 'has_git_suffix', state)
591 591 raise formencode.Invalid(
592 592 msg, value, state, error_dict={'repo_name': msg})
593 593
594 594 return _validator
595 595
596 596
597 597 def ValidCloneUri():
598 598 class InvalidCloneUrl(Exception):
599 599 allowed_prefixes = ()
600 600
601 601 def url_handler(repo_type, url):
602 602 config = make_db_config(clear_session=False)
603 603 if repo_type == 'hg':
604 604 allowed_prefixes = ('http', 'svn+http', 'git+http')
605 605
606 606 if 'http' in url[:4]:
607 607 # initially check if it's at least the proper URL
608 608 # or does it pass basic auth
609 609 MercurialRepository.check_url(url, config)
610 610 elif 'svn+http' in url[:8]: # svn->hg import
611 611 SubversionRepository.check_url(url, config)
612 612 elif 'git+http' in url[:8]: # git->hg import
613 613 raise NotImplementedError()
614 614 else:
615 615 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
616 616 'Allowed url must start with one of %s'
617 617 % (url, ','.join(allowed_prefixes)))
618 618 exc.allowed_prefixes = allowed_prefixes
619 619 raise exc
620 620
621 621 elif repo_type == 'git':
622 622 allowed_prefixes = ('http', 'svn+http', 'hg+http')
623 623 if 'http' in url[:4]:
624 624 # initially check if it's at least the proper URL
625 625 # or does it pass basic auth
626 626 GitRepository.check_url(url, config)
627 627 elif 'svn+http' in url[:8]: # svn->git import
628 628 raise NotImplementedError()
629 629 elif 'hg+http' in url[:8]: # hg->git import
630 630 raise NotImplementedError()
631 631 else:
632 632 exc = InvalidCloneUrl('Clone from URI %s not allowed. '
633 633 'Allowed url must start with one of %s'
634 634 % (url, ','.join(allowed_prefixes)))
635 635 exc.allowed_prefixes = allowed_prefixes
636 636 raise exc
637 637
638 638 class _validator(formencode.validators.FancyValidator):
639 639 messages = {
640 640 'clone_uri': _(u'invalid clone url for %(rtype)s repository'),
641 641 'invalid_clone_uri': _(
642 642 u'Invalid clone url, provide a valid clone '
643 643 u'url starting with one of %(allowed_prefixes)s')
644 644 }
645 645
646 646 def validate_python(self, value, state):
647 647 repo_type = value.get('repo_type')
648 648 url = value.get('clone_uri')
649 649
650 650 if url:
651 651 try:
652 652 url_handler(repo_type, url)
653 653 except InvalidCloneUrl as e:
654 654 log.warning(e)
655 655 msg = M(self, 'invalid_clone_uri', rtype=repo_type,
656 656 allowed_prefixes=','.join(e.allowed_prefixes))
657 657 raise formencode.Invalid(msg, value, state,
658 658 error_dict={'clone_uri': msg})
659 659 except Exception:
660 660 log.exception('Url validation failed')
661 661 msg = M(self, 'clone_uri', rtype=repo_type)
662 662 raise formencode.Invalid(msg, value, state,
663 663 error_dict={'clone_uri': msg})
664 664 return _validator
665 665
666 666
667 667 def ValidForkType(old_data={}):
668 668 class _validator(formencode.validators.FancyValidator):
669 669 messages = {
670 670 'invalid_fork_type': _(u'Fork have to be the same type as parent')
671 671 }
672 672
673 673 def validate_python(self, value, state):
674 674 if old_data['repo_type'] != value:
675 675 msg = M(self, 'invalid_fork_type', state)
676 676 raise formencode.Invalid(
677 677 msg, value, state, error_dict={'repo_type': msg}
678 678 )
679 679 return _validator
680 680
681 681
682 682 def CanWriteGroup(old_data=None):
683 683 class _validator(formencode.validators.FancyValidator):
684 684 messages = {
685 685 'permission_denied': _(
686 686 u"You do not have the permission "
687 687 u"to create repositories in this group."),
688 688 'permission_denied_root': _(
689 689 u"You do not have the permission to store repositories in "
690 690 u"the root location.")
691 691 }
692 692
693 693 def _to_python(self, value, state):
694 694 # root location
695 695 if value in [-1, "-1"]:
696 696 return None
697 697 return value
698 698
699 699 def validate_python(self, value, state):
700 700 gr = RepoGroup.get(value)
701 701 gr_name = gr.group_name if gr else None # None means ROOT location
702 702 # create repositories with write permission on group is set to true
703 703 create_on_write = HasPermissionAny(
704 704 'hg.create.write_on_repogroup.true')()
705 705 group_admin = HasRepoGroupPermissionAny('group.admin')(
706 706 gr_name, 'can write into group validator')
707 707 group_write = HasRepoGroupPermissionAny('group.write')(
708 708 gr_name, 'can write into group validator')
709 709 forbidden = not (group_admin or (group_write and create_on_write))
710 710 can_create_repos = HasPermissionAny(
711 711 'hg.admin', 'hg.create.repository')
712 712 gid = (old_data['repo_group'].get('group_id')
713 713 if (old_data and 'repo_group' in old_data) else None)
714 714 value_changed = gid != safe_int(value)
715 715 new = not old_data
716 716 # do check if we changed the value, there's a case that someone got
717 717 # revoked write permissions to a repository, he still created, we
718 718 # don't need to check permission if he didn't change the value of
719 719 # groups in form box
720 720 if value_changed or new:
721 721 # parent group need to be existing
722 722 if gr and forbidden:
723 723 msg = M(self, 'permission_denied', state)
724 724 raise formencode.Invalid(
725 725 msg, value, state, error_dict={'repo_type': msg}
726 726 )
727 727 # check if we can write to root location !
728 728 elif gr is None and not can_create_repos():
729 729 msg = M(self, 'permission_denied_root', state)
730 730 raise formencode.Invalid(
731 731 msg, value, state, error_dict={'repo_type': msg}
732 732 )
733 733
734 734 return _validator
735 735
736 736
737 737 def ValidPerms(type_='repo'):
738 738 if type_ == 'repo_group':
739 739 EMPTY_PERM = 'group.none'
740 740 elif type_ == 'repo':
741 741 EMPTY_PERM = 'repository.none'
742 742 elif type_ == 'user_group':
743 743 EMPTY_PERM = 'usergroup.none'
744 744
745 745 class _validator(formencode.validators.FancyValidator):
746 746 messages = {
747 747 'perm_new_member_name':
748 748 _(u'This username or user group name is not valid')
749 749 }
750 750
751 751 def _to_python(self, value, state):
752 752 perm_updates = OrderedSet()
753 753 perm_additions = OrderedSet()
754 754 perm_deletions = OrderedSet()
755 755 # build a list of permission to update/delete and new permission
756 756
757 757 # Read the perm_new_member/perm_del_member attributes and group
758 758 # them by they IDs
759 759 new_perms_group = defaultdict(dict)
760 760 del_perms_group = defaultdict(dict)
761 761 for k, v in value.copy().iteritems():
762 762 if k.startswith('perm_del_member'):
763 763 # delete from org storage so we don't process that later
764 764 del value[k]
765 765 # part is `id`, `type`
766 766 _type, part = k.split('perm_del_member_')
767 767 args = part.split('_')
768 768 if len(args) == 2:
769 769 _key, pos = args
770 770 del_perms_group[pos][_key] = v
771 771 if k.startswith('perm_new_member'):
772 772 # delete from org storage so we don't process that later
773 773 del value[k]
774 774 # part is `id`, `type`, `perm`
775 775 _type, part = k.split('perm_new_member_')
776 776 args = part.split('_')
777 777 if len(args) == 2:
778 778 _key, pos = args
779 779 new_perms_group[pos][_key] = v
780 780
781 781 # store the deletes
782 782 for k in sorted(del_perms_group.keys()):
783 783 perm_dict = del_perms_group[k]
784 784 del_member = perm_dict.get('id')
785 785 del_type = perm_dict.get('type')
786 786 if del_member and del_type:
787 787 perm_deletions.add(
788 788 (del_member, None, del_type))
789 789
790 790 # store additions in order of how they were added in web form
791 791 for k in sorted(new_perms_group.keys()):
792 792 perm_dict = new_perms_group[k]
793 793 new_member = perm_dict.get('id')
794 794 new_type = perm_dict.get('type')
795 795 new_perm = perm_dict.get('perm')
796 796 if new_member and new_perm and new_type:
797 797 perm_additions.add(
798 798 (new_member, new_perm, new_type))
799 799
800 800 # get updates of permissions
801 801 # (read the existing radio button states)
802 802 default_user_id = User.get_default_user().user_id
803 803 for k, update_value in value.iteritems():
804 804 if k.startswith('u_perm_') or k.startswith('g_perm_'):
805 805 member = k[7:]
806 806 update_type = {'u': 'user',
807 807 'g': 'users_group'}[k[0]]
808 808
809 809 if safe_int(member) == default_user_id:
810 810 if str2bool(value.get('repo_private')):
811 811 # prevent from updating default user permissions
812 812 # when this repository is marked as private
813 813 update_value = EMPTY_PERM
814 814
815 815 perm_updates.add(
816 816 (member, update_value, update_type))
817 817
818 818 value['perm_additions'] = [] # propagated later
819 819 value['perm_updates'] = list(perm_updates)
820 820 value['perm_deletions'] = list(perm_deletions)
821 821
822 822 updates_map = dict(
823 823 (x[0], (x[1], x[2])) for x in value['perm_updates'])
824 824 # make sure Additions don't override updates.
825 825 for member_id, perm, member_type in list(perm_additions):
826 826 if member_id in updates_map:
827 827 perm = updates_map[member_id][0]
828 828 value['perm_additions'].append((member_id, perm, member_type))
829 829
830 830 # on new entries validate users they exist and they are active !
831 831 # this leaves feedback to the form
832 832 try:
833 833 if member_type == 'user':
834 834 User.query()\
835 835 .filter(User.active == true())\
836 836 .filter(User.user_id == member_id).one()
837 837 if member_type == 'users_group':
838 838 UserGroup.query()\
839 839 .filter(UserGroup.users_group_active == true())\
840 840 .filter(UserGroup.users_group_id == member_id)\
841 841 .one()
842 842
843 843 except Exception:
844 844 log.exception('Updated permission failed: org_exc:')
845 845 msg = M(self, 'perm_new_member_type', state)
846 846 raise formencode.Invalid(
847 847 msg, value, state, error_dict={
848 848 'perm_new_member_name': msg}
849 849 )
850 850 return value
851 851 return _validator
852 852
853 853
854 854 def ValidSettings():
855 855 class _validator(formencode.validators.FancyValidator):
856 856 def _to_python(self, value, state):
857 857 # settings form for users that are not admin
858 858 # can't edit certain parameters, it's extra backup if they mangle
859 859 # with forms
860 860
861 861 forbidden_params = [
862 862 'user', 'repo_type', 'repo_enable_locking',
863 863 'repo_enable_downloads', 'repo_enable_statistics'
864 864 ]
865 865
866 866 for param in forbidden_params:
867 867 if param in value:
868 868 del value[param]
869 869 return value
870 870
871 871 def validate_python(self, value, state):
872 872 pass
873 873 return _validator
874 874
875 875
876 876 def ValidPath():
877 877 class _validator(formencode.validators.FancyValidator):
878 878 messages = {
879 879 'invalid_path': _(u'This is not a valid path')
880 880 }
881 881
882 882 def validate_python(self, value, state):
883 883 if not os.path.isdir(value):
884 884 msg = M(self, 'invalid_path', state)
885 885 raise formencode.Invalid(
886 886 msg, value, state, error_dict={'paths_root_path': msg}
887 887 )
888 888 return _validator
889 889
890 890
891 891 def UniqSystemEmail(old_data={}):
892 892 class _validator(formencode.validators.FancyValidator):
893 893 messages = {
894 894 'email_taken': _(u'This e-mail address is already taken')
895 895 }
896 896
897 897 def _to_python(self, value, state):
898 898 return value.lower()
899 899
900 900 def validate_python(self, value, state):
901 901 if (old_data.get('email') or '').lower() != value:
902 902 user = User.get_by_email(value, case_insensitive=True)
903 903 if user:
904 904 msg = M(self, 'email_taken', state)
905 905 raise formencode.Invalid(
906 906 msg, value, state, error_dict={'email': msg}
907 907 )
908 908 return _validator
909 909
910 910
911 911 def ValidSystemEmail():
912 912 class _validator(formencode.validators.FancyValidator):
913 913 messages = {
914 914 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
915 915 }
916 916
917 917 def _to_python(self, value, state):
918 918 return value.lower()
919 919
920 920 def validate_python(self, value, state):
921 921 user = User.get_by_email(value, case_insensitive=True)
922 922 if user is None:
923 923 msg = M(self, 'non_existing_email', state, email=value)
924 924 raise formencode.Invalid(
925 925 msg, value, state, error_dict={'email': msg}
926 926 )
927 927
928 928 return _validator
929 929
930 930
931 931 def NotReviewedRevisions(repo_id):
932 932 class _validator(formencode.validators.FancyValidator):
933 933 messages = {
934 934 'rev_already_reviewed':
935 935 _(u'Revisions %(revs)s are already part of pull request '
936 936 u'or have set status'),
937 937 }
938 938
939 939 def validate_python(self, value, state):
940 940 # check revisions if they are not reviewed, or a part of another
941 941 # pull request
942 942 statuses = ChangesetStatus.query()\
943 943 .filter(ChangesetStatus.revision.in_(value))\
944 944 .filter(ChangesetStatus.repo_id == repo_id)\
945 945 .all()
946 946
947 947 errors = []
948 948 for status in statuses:
949 949 if status.pull_request_id:
950 950 errors.append(['pull_req', status.revision[:12]])
951 951 elif status.status:
952 952 errors.append(['status', status.revision[:12]])
953 953
954 954 if errors:
955 955 revs = ','.join([x[1] for x in errors])
956 956 msg = M(self, 'rev_already_reviewed', state, revs=revs)
957 957 raise formencode.Invalid(
958 958 msg, value, state, error_dict={'revisions': revs})
959 959
960 960 return _validator
961 961
962 962
963 963 def ValidIp():
964 964 class _validator(CIDR):
965 965 messages = {
966 966 'badFormat': _(u'Please enter a valid IPv4 or IpV6 address'),
967 967 'illegalBits': _(
968 968 u'The network size (bits) must be within the range '
969 969 u'of 0-32 (not %(bits)r)'),
970 970 }
971 971
972 972 # we ovveride the default to_python() call
973 973 def to_python(self, value, state):
974 974 v = super(_validator, self).to_python(value, state)
975 975 v = safe_unicode(v.strip())
976 976 net = ipaddress.ip_network(address=v, strict=False)
977 977 return str(net)
978 978
979 979 def validate_python(self, value, state):
980 980 try:
981 981 addr = safe_unicode(value.strip())
982 982 # this raises an ValueError if address is not IpV4 or IpV6
983 983 ipaddress.ip_network(addr, strict=False)
984 984 except ValueError:
985 985 raise formencode.Invalid(self.message('badFormat', state),
986 986 value, state)
987 987
988 988 return _validator
989 989
990 990
991 991 def FieldKey():
992 992 class _validator(formencode.validators.FancyValidator):
993 993 messages = {
994 994 'badFormat': _(
995 995 u'Key name can only consist of letters, '
996 996 u'underscore, dash or numbers'),
997 997 }
998 998
999 999 def validate_python(self, value, state):
1000 1000 if not re.match('[a-zA-Z0-9_-]+$', value):
1001 1001 raise formencode.Invalid(self.message('badFormat', state),
1002 1002 value, state)
1003 1003 return _validator
1004 1004
1005 1005
1006 1006 def ValidAuthPlugins():
1007 1007 class _validator(formencode.validators.FancyValidator):
1008 1008 messages = {
1009 1009 'import_duplicate': _(
1010 1010 u'Plugins %(loaded)s and %(next_to_load)s '
1011 1011 u'both export the same name'),
1012 1012 'missing_includeme': _(
1013 1013 u'The plugin "%(plugin_id)s" is missing an includeme '
1014 1014 u'function.'),
1015 1015 'import_error': _(
1016 1016 u'Can not load plugin "%(plugin_id)s"'),
1017 1017 'no_plugin': _(
1018 1018 u'No plugin available with ID "%(plugin_id)s"'),
1019 1019 }
1020 1020
1021 1021 def _to_python(self, value, state):
1022 1022 # filter empty values
1023 1023 return filter(lambda s: s not in [None, ''], value)
1024 1024
1025 1025 def _validate_legacy_plugin_id(self, plugin_id, value, state):
1026 1026 """
1027 1027 Validates that the plugin import works. It also checks that the
1028 1028 plugin has an includeme attribute.
1029 1029 """
1030 1030 try:
1031 1031 plugin = _import_legacy_plugin(plugin_id)
1032 1032 except Exception as e:
1033 1033 log.exception(
1034 1034 'Exception during import of auth legacy plugin "{}"'
1035 1035 .format(plugin_id))
1036 1036 msg = M(self, 'import_error', plugin_id=plugin_id)
1037 1037 raise formencode.Invalid(msg, value, state)
1038 1038
1039 1039 if not hasattr(plugin, 'includeme'):
1040 1040 msg = M(self, 'missing_includeme', plugin_id=plugin_id)
1041 1041 raise formencode.Invalid(msg, value, state)
1042 1042
1043 1043 return plugin
1044 1044
1045 1045 def _validate_plugin_id(self, plugin_id, value, state):
1046 1046 """
1047 1047 Plugins are already imported during app start up. Therefore this
1048 1048 validation only retrieves the plugin from the plugin registry and
1049 1049 if it returns something not None everything is OK.
1050 1050 """
1051 1051 plugin = loadplugin(plugin_id)
1052 1052
1053 1053 if plugin is None:
1054 1054 msg = M(self, 'no_plugin', plugin_id=plugin_id)
1055 1055 raise formencode.Invalid(msg, value, state)
1056 1056
1057 1057 return plugin
1058 1058
1059 1059 def validate_python(self, value, state):
1060 1060 unique_names = {}
1061 1061 for plugin_id in value:
1062 1062
1063 1063 # Validate legacy or normal plugin.
1064 1064 if plugin_id.startswith(legacy_plugin_prefix):
1065 1065 plugin = self._validate_legacy_plugin_id(
1066 1066 plugin_id, value, state)
1067 1067 else:
1068 1068 plugin = self._validate_plugin_id(plugin_id, value, state)
1069 1069
1070 1070 # Only allow unique plugin names.
1071 1071 if plugin.name in unique_names:
1072 1072 msg = M(self, 'import_duplicate', state,
1073 1073 loaded=unique_names[plugin.name],
1074 1074 next_to_load=plugin)
1075 1075 raise formencode.Invalid(msg, value, state)
1076 1076 unique_names[plugin.name] = plugin
1077 1077
1078 1078 return _validator
1079 1079
1080 1080
1081 1081 def ValidPattern():
1082 1082
1083 1083 class _Validator(formencode.validators.FancyValidator):
1084 messages = {
1085 'bad_format': _(u'Url must start with http or /'),
1086 }
1084 1087
1085 1088 def _to_python(self, value, state):
1086 1089 patterns = []
1087 1090
1088 1091 prefix = 'new_pattern'
1089 1092 for name, v in value.iteritems():
1090 1093 pattern_name = '_'.join((prefix, 'pattern'))
1091 1094 if name.startswith(pattern_name):
1092 1095 new_item_id = name[len(pattern_name)+1:]
1093 1096
1094 1097 def _field(name):
1095 1098 return '%s_%s_%s' % (prefix, name, new_item_id)
1096 1099
1097 1100 values = {
1098 1101 'issuetracker_pat': value.get(_field('pattern')),
1099 'issuetracker_pat': value.get(_field('pattern')),
1100 1102 'issuetracker_url': value.get(_field('url')),
1101 1103 'issuetracker_pref': value.get(_field('prefix')),
1102 1104 'issuetracker_desc': value.get(_field('description'))
1103 1105 }
1104 1106 new_uid = md5(values['issuetracker_pat'])
1105 1107
1106 1108 has_required_fields = (
1107 1109 values['issuetracker_pat']
1108 1110 and values['issuetracker_url'])
1109 1111
1110 1112 if has_required_fields:
1113 # validate url that it starts with http or /
1114 # otherwise it can lead to JS injections
1115 # e.g specifig javascript:<malicios code>
1116 if not values['issuetracker_url'].startswith(('http', '/')):
1117 raise formencode.Invalid(
1118 self.message('bad_format', state),
1119 value, state)
1120
1111 1121 settings = [
1112 1122 ('_'.join((key, new_uid)), values[key], 'unicode')
1113 1123 for key in values]
1114 1124 patterns.append(settings)
1115 1125
1116 1126 value['patterns'] = patterns
1117 1127 delete_patterns = value.get('uid') or []
1118 1128 if not isinstance(delete_patterns, (list, tuple)):
1119 1129 delete_patterns = [delete_patterns]
1120 1130 value['delete_patterns'] = delete_patterns
1121 1131 return value
1122 1132 return _Validator
General Comments 0
You need to be logged in to leave comments. Login now