##// END OF EJS Templates
home: moved home and repo group views into pyramid....
marcink -
r1774:90a81bb6 default
parent child Browse files
Show More

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

@@ -0,0 +1,33 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
21
22
23 def includeme(config):
24
25 # Summary
26 add_route_with_slash(
27 config,
28 name='repo_group_home',
29 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
30
31 # Scan module for configuration decorators.
32 config.scan()
33
@@ -1,101 +1,101 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 pytest
23 23
24 24 from rhodecode.model.db import Gist
25 25 from rhodecode.api.tests.utils import (
26 26 build_data, api_call, assert_error, assert_ok)
27 27
28 28
29 29 @pytest.mark.usefixtures("testuser_api", "app")
30 30 class TestApiGetGist(object):
31 def test_api_get_gist(self, gist_util):
31 def test_api_get_gist(self, gist_util, http_host_stub):
32 32 gist = gist_util.create_gist()
33 33 gist_id = gist.gist_access_id
34 34 gist_created_on = gist.created_on
35 35 gist_modified_at = gist.modified_at
36 36 id_, params = build_data(
37 37 self.apikey, 'get_gist', gistid=gist_id, )
38 38 response = api_call(self.app, params)
39 39
40 40 expected = {
41 41 'access_id': gist_id,
42 42 'created_on': gist_created_on,
43 43 'modified_at': gist_modified_at,
44 44 'description': 'new-gist',
45 45 'expires': -1.0,
46 46 'gist_id': int(gist_id),
47 47 'type': 'public',
48 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
48 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
49 49 'acl_level': Gist.ACL_LEVEL_PUBLIC,
50 50 'content': None,
51 51 }
52 52
53 53 assert_ok(id_, expected, given=response.body)
54 54
55 def test_api_get_gist_with_content(self, gist_util):
55 def test_api_get_gist_with_content(self, gist_util, http_host_stub):
56 56 mapping = {
57 57 u'filename1.txt': {'content': u'hello world'},
58 58 u'filename1ą.txt': {'content': u'hello worldę'}
59 59 }
60 60 gist = gist_util.create_gist(gist_mapping=mapping)
61 61 gist_id = gist.gist_access_id
62 62 gist_created_on = gist.created_on
63 63 gist_modified_at = gist.modified_at
64 64 id_, params = build_data(
65 65 self.apikey, 'get_gist', gistid=gist_id, content=True)
66 66 response = api_call(self.app, params)
67 67
68 68 expected = {
69 69 'access_id': gist_id,
70 70 'created_on': gist_created_on,
71 71 'modified_at': gist_modified_at,
72 72 'description': 'new-gist',
73 73 'expires': -1.0,
74 74 'gist_id': int(gist_id),
75 75 'type': 'public',
76 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
76 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
77 77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
78 78 'content': {
79 79 u'filename1.txt': u'hello world',
80 80 u'filename1ą.txt': u'hello worldę'
81 81 },
82 82 }
83 83
84 84 assert_ok(id_, expected, given=response.body)
85 85
86 86 def test_api_get_gist_not_existing(self):
87 87 id_, params = build_data(
88 88 self.apikey_regular, 'get_gist', gistid='12345', )
89 89 response = api_call(self.app, params)
90 90 expected = 'gist `%s` does not exist' % ('12345',)
91 91 assert_error(id_, expected, given=response.body)
92 92
93 93 def test_api_get_gist_private_gist_without_permission(self, gist_util):
94 94 gist = gist_util.create_gist()
95 95 gist_id = gist.gist_access_id
96 96 id_, params = build_data(
97 97 self.apikey_regular, 'get_gist', gistid=gist_id, )
98 98 response = api_call(self.app, params)
99 99
100 100 expected = 'gist `%s` does not exist' % (gist_id,)
101 101 assert_error(id_, expected, given=response.body)
@@ -1,134 +1,134 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 import mock
23 22 import pytest
24 23 import urlobject
25 24 from pylons import url
26 25
27 26 from rhodecode.api.tests.utils import (
28 27 build_data, api_call, assert_error, assert_ok)
28 from rhodecode.lib.utils2 import safe_unicode
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequest(object):
35 35
36 def test_api_get_pull_request(self, pr_util):
36 def test_api_get_pull_request(self, pr_util, http_host_stub, http_host_only_stub):
37 37 from rhodecode.model.pull_request import PullRequestModel
38 38 pull_request = pr_util.create_pull_request(mergeable=True)
39 39 id_, params = build_data(
40 40 self.apikey, 'get_pull_request',
41 41 repoid=pull_request.target_repo.repo_name,
42 42 pullrequestid=pull_request.pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 assert response.status == '200 OK'
47 47
48 48 url_obj = urlobject.URLObject(
49 49 url(
50 50 'pullrequest_show',
51 51 repo_name=pull_request.target_repo.repo_name,
52 52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 pr_url = unicode(
54 url_obj.with_netloc('test.example.com:80'))
55 source_url = unicode(
56 pull_request.source_repo.clone_url()
57 .with_netloc('test.example.com:80'))
58 target_url = unicode(
59 pull_request.target_repo.clone_url()
60 .with_netloc('test.example.com:80'))
61 shadow_url = unicode(
53
54 pr_url = safe_unicode(
55 url_obj.with_netloc(http_host_stub))
56 source_url = safe_unicode(
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = safe_unicode(
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 shadow_url = safe_unicode(
62 61 PullRequestModel().get_shadow_clone_url(pull_request))
62
63 63 expected = {
64 64 'pull_request_id': pull_request.pull_request_id,
65 65 'url': pr_url,
66 66 'title': pull_request.title,
67 67 'description': pull_request.description,
68 68 'status': pull_request.status,
69 69 'created_on': pull_request.created_on,
70 70 'updated_on': pull_request.updated_on,
71 71 'commit_ids': pull_request.revisions,
72 72 'review_status': pull_request.calculated_review_status(),
73 73 'mergeable': {
74 74 'status': True,
75 75 'message': 'This pull request can be automatically merged.',
76 76 },
77 77 'source': {
78 78 'clone_url': source_url,
79 79 'repository': pull_request.source_repo.repo_name,
80 80 'reference': {
81 81 'name': pull_request.source_ref_parts.name,
82 82 'type': pull_request.source_ref_parts.type,
83 83 'commit_id': pull_request.source_ref_parts.commit_id,
84 84 },
85 85 },
86 86 'target': {
87 87 'clone_url': target_url,
88 88 'repository': pull_request.target_repo.repo_name,
89 89 'reference': {
90 90 'name': pull_request.target_ref_parts.name,
91 91 'type': pull_request.target_ref_parts.type,
92 92 'commit_id': pull_request.target_ref_parts.commit_id,
93 93 },
94 94 },
95 95 'merge': {
96 96 'clone_url': shadow_url,
97 97 'reference': {
98 98 'name': pull_request.shadow_merge_ref.name,
99 99 'type': pull_request.shadow_merge_ref.type,
100 100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 101 },
102 102 },
103 103 'author': pull_request.author.get_api_data(include_secrets=False,
104 104 details='basic'),
105 105 'reviewers': [
106 106 {
107 107 'user': reviewer.get_api_data(include_secrets=False,
108 108 details='basic'),
109 109 'reasons': reasons,
110 110 'review_status': st[0][1].status if st else 'not_reviewed',
111 111 }
112 112 for reviewer, reasons, mandatory, st in
113 113 pull_request.reviewers_statuses()
114 114 ]
115 115 }
116 116 assert_ok(id_, expected, response.body)
117 117
118 118 def test_api_get_pull_request_repo_error(self):
119 119 id_, params = build_data(
120 120 self.apikey, 'get_pull_request',
121 121 repoid=666, pullrequestid=1)
122 122 response = api_call(self.app, params)
123 123
124 124 expected = 'repository `666` does not exist'
125 125 assert_error(id_, expected, given=response.body)
126 126
127 127 def test_api_get_pull_request_pull_request_error(self):
128 128 id_, params = build_data(
129 129 self.apikey, 'get_pull_request',
130 130 repoid=1, pullrequestid=666)
131 131 response = api_call(self.app, params)
132 132
133 133 expected = 'pull request `666` does not exist'
134 134 assert_error(id_, expected, given=response.body)
@@ -1,189 +1,190 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 from rhodecode.model.repo import RepoModel
25 25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
26 26 from rhodecode.api.tests.utils import (
27 27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
28 28 from rhodecode.tests.fixture import Fixture
29
29 from rhodecode.tests.plugin import http_host_stub, http_host_only_stub
30 30
31 31 fixture = Fixture()
32 32
33 33 UPDATE_REPO_NAME = 'api_update_me'
34 34
35 35
36 36 class SAME_AS_UPDATES(object):
37 37 """ Constant used for tests below """
38 38
39 39
40 40 @pytest.mark.usefixtures("testuser_api", "app")
41 41 class TestApiUpdateRepo(object):
42 42
43 43 @pytest.mark.parametrize("updates, expected", [
44 44 ({'owner': TEST_USER_REGULAR_LOGIN},
45 45 SAME_AS_UPDATES),
46 46
47 47 ({'description': 'new description'},
48 48 SAME_AS_UPDATES),
49 49
50 50 ({'clone_uri': 'http://foo.com/repo'},
51 51 SAME_AS_UPDATES),
52 52
53 53 ({'clone_uri': None},
54 54 {'clone_uri': ''}),
55 55
56 56 ({'clone_uri': ''},
57 57 {'clone_uri': ''}),
58 58
59 59 ({'landing_rev': 'rev:tip'},
60 60 {'landing_rev': ['rev', 'tip']}),
61 61
62 62 ({'enable_statistics': True},
63 63 SAME_AS_UPDATES),
64 64
65 65 ({'enable_locking': True},
66 66 SAME_AS_UPDATES),
67 67
68 68 ({'enable_downloads': True},
69 69 SAME_AS_UPDATES),
70 70
71 71 ({'repo_name': 'new_repo_name'},
72 72 {
73 73 'repo_name': 'new_repo_name',
74 'url': 'http://test.example.com:80/new_repo_name'
74 'url': 'http://{}/new_repo_name'.format(http_host_only_stub())
75 75 }),
76 76
77 77 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
78 78 '_group': 'test_group_for_update'},
79 79 {
80 80 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
81 'url': 'http://test.example.com:80/test_group_for_update/{}'.format(UPDATE_REPO_NAME)
81 'url': 'http://{}/test_group_for_update/{}'.format(
82 http_host_only_stub(), UPDATE_REPO_NAME)
82 83 }),
83 84 ])
84 85 def test_api_update_repo(self, updates, expected, backend):
85 86 repo_name = UPDATE_REPO_NAME
86 87 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
87 88 if updates.get('_group'):
88 89 fixture.create_repo_group(updates['_group'])
89 90
90 91 expected_api_data = repo.get_api_data(include_secrets=True)
91 92 if expected is SAME_AS_UPDATES:
92 93 expected_api_data.update(updates)
93 94 else:
94 95 expected_api_data.update(expected)
95 96
96 97 id_, params = build_data(
97 98 self.apikey, 'update_repo', repoid=repo_name, **updates)
98 99 response = api_call(self.app, params)
99 100
100 101 if updates.get('repo_name'):
101 102 repo_name = updates['repo_name']
102 103
103 104 try:
104 105 expected = {
105 106 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
106 107 'repository': jsonify(expected_api_data)
107 108 }
108 109 assert_ok(id_, expected, given=response.body)
109 110 finally:
110 111 fixture.destroy_repo(repo_name)
111 112 if updates.get('_group'):
112 113 fixture.destroy_repo_group(updates['_group'])
113 114
114 115 def test_api_update_repo_fork_of_field(self, backend):
115 116 master_repo = backend.create_repo()
116 117 repo = backend.create_repo()
117 118 updates = {
118 119 'fork_of': master_repo.repo_name
119 120 }
120 121 expected_api_data = repo.get_api_data(include_secrets=True)
121 122 expected_api_data.update(updates)
122 123
123 124 id_, params = build_data(
124 125 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
125 126 response = api_call(self.app, params)
126 127 expected = {
127 128 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
128 129 'repository': jsonify(expected_api_data)
129 130 }
130 131 assert_ok(id_, expected, given=response.body)
131 132 result = response.json['result']['repository']
132 133 assert result['fork_of'] == master_repo.repo_name
133 134
134 135 def test_api_update_repo_fork_of_not_found(self, backend):
135 136 master_repo_name = 'fake-parent-repo'
136 137 repo = backend.create_repo()
137 138 updates = {
138 139 'fork_of': master_repo_name
139 140 }
140 141 id_, params = build_data(
141 142 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
142 143 response = api_call(self.app, params)
143 144 expected = {
144 145 'repo_fork_of': 'Fork with id `{}` does not exists'.format(
145 146 master_repo_name)}
146 147 assert_error(id_, expected, given=response.body)
147 148
148 149 def test_api_update_repo_with_repo_group_not_existing(self):
149 150 repo_name = 'admin_owned'
150 151 fake_repo_group = 'test_group_for_update'
151 152 fixture.create_repo(repo_name)
152 153 updates = {'repo_name': '{}/{}'.format(fake_repo_group, repo_name)}
153 154 id_, params = build_data(
154 155 self.apikey, 'update_repo', repoid=repo_name, **updates)
155 156 response = api_call(self.app, params)
156 157 try:
157 158 expected = {
158 159 'repo_group': 'Repository group `{}` does not exist'.format(fake_repo_group)
159 160 }
160 161 assert_error(id_, expected, given=response.body)
161 162 finally:
162 163 fixture.destroy_repo(repo_name)
163 164
164 165 def test_api_update_repo_regular_user_not_allowed(self):
165 166 repo_name = 'admin_owned'
166 167 fixture.create_repo(repo_name)
167 168 updates = {'active': False}
168 169 id_, params = build_data(
169 170 self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
170 171 response = api_call(self.app, params)
171 172 try:
172 173 expected = 'repository `%s` does not exist' % (repo_name,)
173 174 assert_error(id_, expected, given=response.body)
174 175 finally:
175 176 fixture.destroy_repo(repo_name)
176 177
177 178 @mock.patch.object(RepoModel, 'update', crash)
178 179 def test_api_update_repo_exception_occurred(self, backend):
179 180 repo_name = UPDATE_REPO_NAME
180 181 fixture.create_repo(repo_name, repo_type=backend.alias)
181 182 id_, params = build_data(
182 183 self.apikey, 'update_repo', repoid=repo_name,
183 184 owner=TEST_USER_ADMIN_LOGIN,)
184 185 response = api_call(self.app, params)
185 186 try:
186 187 expected = 'failed to update repo `%s`' % (repo_name,)
187 188 assert_error(id_, expected, given=response.body)
188 189 finally:
189 190 fixture.destroy_repo(repo_name)
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,308 +1,338 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 time
22 22 import logging
23 23 from pylons import tmpl_context as c
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.lib.utils import PartialRenderer
28 28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.model import repo
32 from rhodecode.model import repo_group
32 33 from rhodecode.model.db import User
33 34 from rhodecode.model.scm import ScmModel
34 35
35 36 log = logging.getLogger(__name__)
36 37
37 38
38 39 ADMIN_PREFIX = '/_admin'
39 40 STATIC_FILE_PREFIX = '/_static'
40 41
41 42
43 def add_route_with_slash(config,name, pattern, **kw):
44 config.add_route(name, pattern, **kw)
45 if not pattern.endswith('/'):
46 config.add_route(name + '_slash', pattern + '/', **kw)
47
48
42 49 def get_format_ref_id(repo):
43 50 """Returns a `repo` specific reference formatter function"""
44 51 if h.is_svn(repo):
45 52 return _format_ref_id_svn
46 53 else:
47 54 return _format_ref_id
48 55
49 56
50 57 def _format_ref_id(name, raw_id):
51 58 """Default formatting of a given reference `name`"""
52 59 return name
53 60
54 61
55 62 def _format_ref_id_svn(name, raw_id):
56 63 """Special way of formatting a reference for Subversion including path"""
57 64 return '%s@%s' % (name, raw_id)
58 65
59 66
60 67 class TemplateArgs(StrictAttributeDict):
61 68 pass
62 69
63 70
64 71 class BaseAppView(object):
65 72
66 73 def __init__(self, context, request):
67 74 self.request = request
68 75 self.context = context
69 76 self.session = request.session
70 77 self._rhodecode_user = request.user # auth user
71 78 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 79 self._maybe_needs_password_change(
73 80 request.matched_route.name, self._rhodecode_db_user)
74 81
75 82 def _maybe_needs_password_change(self, view_name, user_obj):
76 83 log.debug('Checking if user %s needs password change on view %s',
77 84 user_obj, view_name)
78 85 skip_user_views = [
79 86 'logout', 'login',
80 87 'my_account_password', 'my_account_password_update'
81 88 ]
82 89
83 90 if not user_obj:
84 91 return
85 92
86 93 if user_obj.username == User.DEFAULT_USER:
87 94 return
88 95
89 96 now = time.time()
90 97 should_change = user_obj.user_data.get('force_password_change')
91 98 change_after = safe_int(should_change) or 0
92 99 if should_change and now > change_after:
93 100 log.debug('User %s requires password change', user_obj)
94 101 h.flash('You are required to change your password', 'warning',
95 102 ignore_duplicate=True)
96 103
97 104 if view_name not in skip_user_views:
98 105 raise HTTPFound(
99 106 self.request.route_path('my_account_password'))
100 107
101 108 def _get_local_tmpl_context(self):
102 109 c = TemplateArgs()
103 110 c.auth_user = self.request.user
104 111 return c
105 112
106 113 def _register_global_c(self, tmpl_args):
107 114 """
108 115 Registers attributes to pylons global `c`
109 116 """
110 117 # TODO(marcink): remove once pyramid migration is finished
111 118 for k, v in tmpl_args.items():
112 119 setattr(c, k, v)
113 120
114 121 def _get_template_context(self, tmpl_args):
115 122 self._register_global_c(tmpl_args)
116 123
117 124 local_tmpl_args = {
118 125 'defaults': {},
119 126 'errors': {},
120 127 }
121 128 local_tmpl_args.update(tmpl_args)
122 129 return local_tmpl_args
123 130
124 131 def load_default_context(self):
125 132 """
126 133 example:
127 134
128 135 def load_default_context(self):
129 136 c = self._get_local_tmpl_context()
130 137 c.custom_var = 'foobar'
131 138 self._register_global_c(c)
132 139 return c
133 140 """
134 141 raise NotImplementedError('Needs implementation in view class')
135 142
136 143
137 144 class RepoAppView(BaseAppView):
138 145
139 146 def __init__(self, context, request):
140 147 super(RepoAppView, self).__init__(context, request)
141 148 self.db_repo = request.db_repo
142 149 self.db_repo_name = self.db_repo.repo_name
143 150 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144 151
145 152 def _handle_missing_requirements(self, error):
146 153 log.error(
147 154 'Requirements are missing for repository %s: %s',
148 155 self.db_repo_name, error.message)
149 156
150 157 def _get_local_tmpl_context(self):
151 158 c = super(RepoAppView, self)._get_local_tmpl_context()
152 159 # register common vars for this type of view
153 160 c.rhodecode_db_repo = self.db_repo
154 161 c.repo_name = self.db_repo_name
155 162 c.repository_pull_requests = self.db_repo_pull_requests
156 163
157 164 c.repository_requirements_missing = False
158 165 try:
159 166 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 167 except RepositoryRequirementError as e:
161 168 c.repository_requirements_missing = True
162 169 self._handle_missing_requirements(e)
163 170
164 171 return c
165 172
166 173
167 174 class DataGridAppView(object):
168 175 """
169 176 Common class to have re-usable grid rendering components
170 177 """
171 178
172 179 def _extract_ordering(self, request, column_map=None):
173 180 column_map = column_map or {}
174 181 column_index = safe_int(request.GET.get('order[0][column]'))
175 182 order_dir = request.GET.get(
176 183 'order[0][dir]', 'desc')
177 184 order_by = request.GET.get(
178 185 'columns[%s][data][sort]' % column_index, 'name_raw')
179 186
180 187 # translate datatable to DB columns
181 188 order_by = column_map.get(order_by) or order_by
182 189
183 190 search_q = request.GET.get('search[value]')
184 191 return search_q, order_by, order_dir
185 192
186 193 def _extract_chunk(self, request):
187 194 start = safe_int(request.GET.get('start'), 0)
188 195 length = safe_int(request.GET.get('length'), 25)
189 196 draw = safe_int(request.GET.get('draw'))
190 197 return draw, start, length
191 198
192 199
193 200 class BaseReferencesView(RepoAppView):
194 201 """
195 202 Base for reference view for branches, tags and bookmarks.
196 203 """
197 204 def load_default_context(self):
198 205 c = self._get_local_tmpl_context()
199 206
200 207 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 208 c.repo_info = self.db_repo
202 209
203 210 self._register_global_c(c)
204 211 return c
205 212
206 213 def load_refs_context(self, ref_items, partials_template):
207 214 _render = PartialRenderer(partials_template)
208 215 _data = []
209 216 pre_load = ["author", "date", "message"]
210 217
211 218 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 219 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213 220
214 221 for ref_name, commit_id in ref_items:
215 222 commit = self.rhodecode_vcs_repo.get_commit(
216 223 commit_id=commit_id, pre_load=pre_load)
217 224
218 225 # TODO: johbo: Unify generation of reference links
219 226 use_commit_id = '/' in ref_name or is_svn
220 227 files_url = h.url(
221 228 'files_home',
222 229 repo_name=c.repo_name,
223 230 f_path=ref_name if is_svn else '',
224 231 revision=commit_id if use_commit_id else ref_name,
225 232 at=ref_name)
226 233
227 234 _data.append({
228 235 "name": _render('name', ref_name, files_url),
229 236 "name_raw": ref_name,
230 237 "date": _render('date', commit.date),
231 238 "date_raw": datetime_to_time(commit.date),
232 239 "author": _render('author', commit.author),
233 240 "commit": _render(
234 241 'commit', commit.message, commit.raw_id, commit.idx),
235 242 "commit_raw": commit.idx,
236 243 "compare": _render(
237 244 'compare', format_ref_id(ref_name, commit.raw_id)),
238 245 })
239 246 c.has_references = bool(_data)
240 247 c.data = json.dumps(_data)
241 248
242 249
243 250 class RepoRoutePredicate(object):
244 251 def __init__(self, val, config):
245 252 self.val = val
246 253
247 254 def text(self):
248 255 return 'repo_route = %s' % self.val
249 256
250 257 phash = text
251 258
252 259 def __call__(self, info, request):
253 260 repo_name = info['match']['repo_name']
254 261 repo_model = repo.RepoModel()
255 262 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
256 # if we match quickly from database, short circuit the operation,
257 # and validate repo based on the type.
263
258 264 if by_name_match:
259 265 # register this as request object we can re-use later
260 266 request.db_repo = by_name_match
261 267 return True
262 268
263 269 by_id_match = repo_model.get_repo_by_id(repo_name)
264 270 if by_id_match:
265 271 request.db_repo = by_id_match
266 272 return True
267 273
268 274 return False
269 275
270 276
271 277 class RepoTypeRoutePredicate(object):
272 278 def __init__(self, val, config):
273 279 self.val = val or ['hg', 'git', 'svn']
274 280
275 281 def text(self):
276 282 return 'repo_accepted_type = %s' % self.val
277 283
278 284 phash = text
279 285
280 286 def __call__(self, info, request):
281 287
282 288 rhodecode_db_repo = request.db_repo
283 289
284 290 log.debug(
285 291 '%s checking repo type for %s in %s',
286 292 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287 293
288 294 if rhodecode_db_repo.repo_type in self.val:
289 295 return True
290 296 else:
291 297 log.warning('Current view is not supported for repo type:%s',
292 298 rhodecode_db_repo.repo_type)
293 299 #
294 300 # h.flash(h.literal(
295 301 # _('Action not supported for %s.' % rhodecode_repo.alias)),
296 302 # category='warning')
297 303 # return redirect(
298 304 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
299 305
300 306 return False
301 307
302 308
309 class RepoGroupRoutePredicate(object):
310 def __init__(self, val, config):
311 self.val = val
312
313 def text(self):
314 return 'repo_group_route = %s' % self.val
315
316 phash = text
317
318 def __call__(self, info, request):
319 repo_group_name = info['match']['repo_group_name']
320 repo_group_model = repo_group.RepoGroupModel()
321 by_name_match = repo_group_model.get_by_group_name(
322 repo_group_name, cache=True)
323
324 if by_name_match:
325 # register this as request object we can re-use later
326 request.db_repo_group = by_name_match
327 return True
328
329 return False
330
303 331
304 332 def includeme(config):
305 333 config.add_route_predicate(
306 334 'repo_route', RepoRoutePredicate)
307 335 config.add_route_predicate(
308 'repo_accepted_types', RepoTypeRoutePredicate) No newline at end of file
336 'repo_accepted_types', RepoTypeRoutePredicate)
337 config.add_route_predicate(
338 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,129 +1,137 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 pytest
23 23 from pylons import tmpl_context as c
24 24
25 25 import rhodecode
26 26 from rhodecode.model.db import Repository, User
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.repo import RepoModel
29 29 from rhodecode.model.repo_group import RepoGroupModel
30 30 from rhodecode.model.settings import SettingsModel
31 from rhodecode.tests import TestController, url
31 from rhodecode.tests import TestController
32 32 from rhodecode.tests.fixture import Fixture
33 33
34 34
35 35 fixture = Fixture()
36 36
37 37
38 def route_path(name, **kwargs):
39 return {
40 'home': '/',
41 'repo_group_home': '/{repo_group_name}'
42 }[name].format(**kwargs)
43
44
38 45 class TestHomeController(TestController):
39 46
40 47 def test_index(self):
41 48 self.log_user()
42 response = self.app.get(url(controller='home', action='index'))
49 response = self.app.get(route_path('home'))
43 50 # if global permission is set
44 51 response.mustcontain('Add Repository')
45 52
46 53 # search for objects inside the JavaScript JSON
47 54 for repo in Repository.getAll():
48 55 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
49 56
50 57 def test_index_contains_statics_with_ver(self):
51 58 self.log_user()
52 response = self.app.get(url(controller='home', action='index'))
59 response = self.app.get(route_path('home'))
53 60
54 61 rhodecode_version_hash = c.rhodecode_version_hash
55 62 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
56 63 response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash))
57 64
58 65 def test_index_contains_backend_specific_details(self, backend):
59 66 self.log_user()
60 response = self.app.get(url(controller='home', action='index'))
67 response = self.app.get(route_path('home'))
61 68 tip = backend.repo.get_commit().raw_id
62 69
63 70 # html in javascript variable:
64 71 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
65 72 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
66 73
67 74 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
68 75 response.mustcontain("""Added a symlink""")
69 76
70 77 def test_index_with_anonymous_access_disabled(self):
71 78 with fixture.anon_access(False):
72 response = self.app.get(url(controller='home', action='index'),
73 status=302)
79 response = self.app.get(route_path('home'), status=302)
74 80 assert 'login' in response.location
75 81
76 82 def test_index_page_on_groups(self, autologin_user, repo_group):
77 response = self.app.get(url('repo_group_home', group_name='gr1'))
83 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1'))
78 84 response.mustcontain("gr1/repo_in_group")
79 85
80 86 def test_index_page_on_group_with_trailing_slash(
81 87 self, autologin_user, repo_group):
82 response = self.app.get(url('repo_group_home', group_name='gr1') + '/')
88 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/')
83 89 response.mustcontain("gr1/repo_in_group")
84 90
85 91 @pytest.fixture(scope='class')
86 92 def repo_group(self, request):
87 93 gr = fixture.create_repo_group('gr1')
88 94 fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
89 95
90 96 @request.addfinalizer
91 97 def cleanup():
92 98 RepoModel().delete('gr1/repo_in_group')
93 99 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
94 100 Session().commit()
95 101
96 def test_index_with_name_with_tags(self, autologin_user):
97 user = User.get_by_username('test_admin')
102 def test_index_with_name_with_tags(self, user_util, autologin_user):
103 user = user_util.create_user()
104 username = user.username
98 105 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
99 106 user.lastname = (
100 107 '<img src="/image2" onload="alert(\'Hello, World!\');">')
101 108 Session().add(user)
102 109 Session().commit()
110 user_util.create_repo(owner=username)
103 111
104 response = self.app.get(url(controller='home', action='index'))
112 response = self.app.get(route_path('home'))
105 113 response.mustcontain(
106 114 '&lt;img src=&#34;/image1&#34; onload=&#34;'
107 115 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
108 116 response.mustcontain(
109 117 '&lt;img src=&#34;/image2&#34; onload=&#34;'
110 118 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
111 119
112 120 @pytest.mark.parametrize("name, state", [
113 121 ('Disabled', False),
114 122 ('Enabled', True),
115 123 ])
116 124 def test_index_show_version(self, autologin_user, name, state):
117 125 version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
118 126
119 127 sett = SettingsModel().create_or_update_setting(
120 128 'show_version', state, 'bool')
121 129 Session().add(sett)
122 130 Session().commit()
123 131 SettingsModel().invalidate_settings_cache()
124 132
125 response = self.app.get(url(controller='home', action='index'))
133 response = self.app.get(route_path('home'))
126 134 if state is True:
127 135 response.mustcontain(version_string)
128 136 if state is False:
129 137 response.mustcontain(no=[version_string])
@@ -1,248 +1,304 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 re
22 22 import logging
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, \
29 HasRepoGroupPermissionAnyDecorator
29 30 from rhodecode.lib.index import searcher_from_config
30 31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 from rhodecode.lib.ext_json import json
31 33 from rhodecode.model.db import func, Repository, RepoGroup
32 34 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.scm import ScmModel
35 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
34 37 from rhodecode.model.user import UserModel
35 38 from rhodecode.model.user_group import UserGroupModel
36 39
37 40 log = logging.getLogger(__name__)
38 41
39 42
40 43 class HomeView(BaseAppView):
41 44
42 45 def load_default_context(self):
43 46 c = self._get_local_tmpl_context()
44 47 c.user = c.auth_user.get_instance()
45 48 self._register_global_c(c)
46 49 return c
47 50
48 51 @LoginRequired()
49 52 @view_config(
50 53 route_name='user_autocomplete_data', request_method='GET',
51 54 renderer='json_ext', xhr=True)
52 55 def user_autocomplete_data(self):
53 56 query = self.request.GET.get('query')
54 57 active = str2bool(self.request.GET.get('active') or True)
55 58 include_groups = str2bool(self.request.GET.get('user_groups'))
56 59 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
57 60 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
58 61
59 62 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
60 63 query, active, include_groups)
61 64
62 65 _users = UserModel().get_users(
63 66 name_contains=query, only_active=active)
64 67
65 68 def maybe_skip_default_user(usr):
66 69 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
67 70 return False
68 71 return True
69 72 _users = filter(maybe_skip_default_user, _users)
70 73
71 74 if include_groups:
72 75 # extend with user groups
73 76 _user_groups = UserGroupModel().get_user_groups(
74 77 name_contains=query, only_active=active,
75 78 expand_groups=expand_groups)
76 79 _users = _users + _user_groups
77 80
78 81 return {'suggestions': _users}
79 82
80 83 @LoginRequired()
81 84 @NotAnonymous()
82 85 @view_config(
83 86 route_name='user_group_autocomplete_data', request_method='GET',
84 87 renderer='json_ext', xhr=True)
85 88 def user_group_autocomplete_data(self):
86 89 query = self.request.GET.get('query')
87 90 active = str2bool(self.request.GET.get('active') or True)
88 91 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
89 92
90 93 log.debug('generating user group list, query:%s, active:%s',
91 94 query, active)
92 95
93 96 _user_groups = UserGroupModel().get_user_groups(
94 97 name_contains=query, only_active=active,
95 98 expand_groups=expand_groups)
96 99 _user_groups = _user_groups
97 100
98 101 return {'suggestions': _user_groups}
99 102
100 103 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
101 104 query = Repository.query()\
102 105 .order_by(func.length(Repository.repo_name))\
103 106 .order_by(Repository.repo_name)
104 107
105 108 if repo_type:
106 109 query = query.filter(Repository.repo_type == repo_type)
107 110
108 111 if name_contains:
109 112 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
110 113 query = query.filter(
111 114 Repository.repo_name.ilike(ilike_expression))
112 115 query = query.limit(limit)
113 116
114 117 all_repos = query.all()
115 118 # permission checks are inside this function
116 119 repo_iter = ScmModel().get_repos(all_repos)
117 120 return [
118 121 {
119 122 'id': obj['name'],
120 123 'text': obj['name'],
121 124 'type': 'repo',
122 125 'obj': obj['dbrepo'],
123 126 'url': h.url('summary_home', repo_name=obj['name'])
124 127 }
125 128 for obj in repo_iter]
126 129
127 130 def _get_repo_group_list(self, name_contains=None, limit=20):
128 131 query = RepoGroup.query()\
129 132 .order_by(func.length(RepoGroup.group_name))\
130 133 .order_by(RepoGroup.group_name)
131 134
132 135 if name_contains:
133 136 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
134 137 query = query.filter(
135 138 RepoGroup.group_name.ilike(ilike_expression))
136 139 query = query.limit(limit)
137 140
138 141 all_groups = query.all()
139 142 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
140 143 return [
141 144 {
142 145 'id': obj.group_name,
143 146 'text': obj.group_name,
144 147 'type': 'group',
145 148 'obj': {},
146 'url': h.url('repo_group_home', group_name=obj.group_name)
149 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name)
147 150 }
148 151 for obj in repo_groups_iter]
149 152
150 153 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
151 154 if not hash_starts_with or len(hash_starts_with) < 3:
152 155 return []
153 156
154 157 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
155 158
156 159 if len(commit_hashes) != 1:
157 160 return []
158 161
159 162 commit_hash_prefix = commit_hashes[0]
160 163
161 164 searcher = searcher_from_config(self.request.registry.settings)
162 165 result = searcher.search(
163 166 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
164 167 raise_on_exc=False)
165 168
166 169 return [
167 170 {
168 171 'id': entry['commit_id'],
169 172 'text': entry['commit_id'],
170 173 'type': 'commit',
171 174 'obj': {'repo': entry['repository']},
172 175 'url': h.url('changeset_home',
173 176 repo_name=entry['repository'],
174 177 revision=entry['commit_id'])
175 178 }
176 179 for entry in result['results']]
177 180
178 181 @LoginRequired()
179 182 @view_config(
180 183 route_name='repo_list_data', request_method='GET',
181 184 renderer='json_ext', xhr=True)
182 185 def repo_list_data(self):
183 186 _ = self.request.translate
184 187
185 188 query = self.request.GET.get('query')
186 189 repo_type = self.request.GET.get('repo_type')
187 190 log.debug('generating repo list, query:%s, repo_type:%s',
188 191 query, repo_type)
189 192
190 193 res = []
191 194 repos = self._get_repo_list(query, repo_type=repo_type)
192 195 if repos:
193 196 res.append({
194 197 'text': _('Repositories'),
195 198 'children': repos
196 199 })
197 200
198 201 data = {
199 202 'more': False,
200 203 'results': res
201 204 }
202 205 return data
203 206
204 207 @LoginRequired()
205 208 @view_config(
206 209 route_name='goto_switcher_data', request_method='GET',
207 210 renderer='json_ext', xhr=True)
208 211 def goto_switcher_data(self):
209 212 c = self.load_default_context()
210 213
211 214 _ = self.request.translate
212 215
213 216 query = self.request.GET.get('query')
214 217 log.debug('generating goto switcher list, query %s', query)
215 218
216 219 res = []
217 220 repo_groups = self._get_repo_group_list(query)
218 221 if repo_groups:
219 222 res.append({
220 223 'text': _('Groups'),
221 224 'children': repo_groups
222 225 })
223 226
224 227 repos = self._get_repo_list(query)
225 228 if repos:
226 229 res.append({
227 230 'text': _('Repositories'),
228 231 'children': repos
229 232 })
230 233
231 234 commits = self._get_hash_commit_list(c.auth_user, query)
232 235 if commits:
233 236 unique_repos = {}
234 237 for commit in commits:
235 238 unique_repos.setdefault(commit['obj']['repo'], []
236 239 ).append(commit)
237 240
238 241 for repo in unique_repos:
239 242 res.append({
240 243 'text': _('Commits in %(repo)s') % {'repo': repo},
241 244 'children': unique_repos[repo]
242 245 })
243 246
244 247 data = {
245 248 'more': False,
246 249 'results': res
247 250 }
248 251 return data
252
253 def _get_groups_and_repos(self, repo_group_id=None):
254 # repo groups groups
255 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
256 _perms = ['group.read', 'group.write', 'group.admin']
257 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
258 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
259 repo_group_list=repo_group_list_acl, admin=False)
260
261 # repositories
262 repo_list = Repository.get_all_repos(group_id=repo_group_id)
263 _perms = ['repository.read', 'repository.write', 'repository.admin']
264 repo_list_acl = RepoList(repo_list, perm_set=_perms)
265 repo_data = RepoModel().get_repos_as_dict(
266 repo_list=repo_list_acl, admin=False)
267
268 return repo_data, repo_group_data
269
270 @LoginRequired()
271 @view_config(
272 route_name='home', request_method='GET',
273 renderer='rhodecode:templates/index.mako')
274 def main_page(self):
275 c = self.load_default_context()
276 c.repo_group = None
277
278 repo_data, repo_group_data = self._get_groups_and_repos()
279 # json used to render the grids
280 c.repos_data = json.dumps(repo_data)
281 c.repo_groups_data = json.dumps(repo_group_data)
282
283 return self._get_template_context(c)
284
285 @LoginRequired()
286 @HasRepoGroupPermissionAnyDecorator(
287 'group.read', 'group.write', 'group.admin')
288 @view_config(
289 route_name='repo_group_home', request_method='GET',
290 renderer='rhodecode:templates/index_repo_group.mako')
291 @view_config(
292 route_name='repo_group_home_slash', request_method='GET',
293 renderer='rhodecode:templates/index_repo_group.mako')
294 def repo_group_main_page(self):
295 c = self.load_default_context()
296 c.repo_group = self.request.db_repo_group
297 repo_data, repo_group_data = self._get_groups_and_repos(
298 c.repo_group.group_id)
299
300 # json used to render the grids
301 c.repos_data = json.dumps(repo_data)
302 c.repo_groups_data = json.dumps(repo_group_data)
303
304 return self._get_template_context(c)
@@ -1,425 +1,425 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 time
22 22 import collections
23 23 import datetime
24 24 import formencode
25 25 import logging
26 26 import urlparse
27 27
28 from pylons import url
29 28 from pyramid.httpexceptions import HTTPFound
30 29 from pyramid.view import view_config
31 30 from recaptcha.client.captcha import submit
32 31
33 32 from rhodecode.apps._base import BaseAppView
34 33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 34 from rhodecode.events import UserRegistered
36 35 from rhodecode.lib import helpers as h
37 36 from rhodecode.lib import audit_logger
38 37 from rhodecode.lib.auth import (
39 38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 39 from rhodecode.lib.base import get_ip_addr
41 40 from rhodecode.lib.exceptions import UserCreationError
42 41 from rhodecode.lib.utils2 import safe_str
43 42 from rhodecode.model.db import User, UserApiKeys
44 43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 44 from rhodecode.model.meta import Session
46 45 from rhodecode.model.auth_token import AuthTokenModel
47 46 from rhodecode.model.settings import SettingsModel
48 47 from rhodecode.model.user import UserModel
49 48 from rhodecode.translation import _
50 49
51 50
52 51 log = logging.getLogger(__name__)
53 52
54 53 CaptchaData = collections.namedtuple(
55 54 'CaptchaData', 'active, private_key, public_key')
56 55
57 56
58 57 def _store_user_in_session(session, username, remember=False):
59 58 user = User.get_by_username(username, case_insensitive=True)
60 59 auth_user = AuthUser(user.user_id)
61 60 auth_user.set_authenticated()
62 61 cs = auth_user.get_cookie_store()
63 62 session['rhodecode_user'] = cs
64 63 user.update_lastlogin()
65 64 Session().commit()
66 65
67 66 # If they want to be remembered, update the cookie
68 67 if remember:
69 68 _year = (datetime.datetime.now() +
70 69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 70 session._set_cookie_expires(_year)
72 71
73 72 session.save()
74 73
75 74 safe_cs = cs.copy()
76 75 safe_cs['password'] = '****'
77 76 log.info('user %s is now authenticated and stored in '
78 77 'session, session attrs %s', username, safe_cs)
79 78
80 79 # dumps session attrs back to cookie
81 80 session._update_cookie_out()
82 81 # we set new cookie
83 82 headers = None
84 83 if session.request['set_cookie']:
85 84 # send set-cookie headers back to response to update cookie
86 85 headers = [('Set-Cookie', session.request['cookie_out'])]
87 86 return headers
88 87
89 88
90 89 def get_came_from(request):
91 90 came_from = safe_str(request.GET.get('came_from', ''))
92 91 parsed = urlparse.urlparse(came_from)
93 92 allowed_schemes = ['http', 'https']
93 default_came_from = h.route_path('home')
94 94 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 95 log.error('Suspicious URL scheme detected %s for url %s' %
96 96 (parsed.scheme, parsed))
97 came_from = url('home')
97 came_from = default_came_from
98 98 elif parsed.netloc and request.host != parsed.netloc:
99 99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 100 'is: %s' % (parsed.netloc, parsed, request.host))
101 came_from = url('home')
101 came_from = default_came_from
102 102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 103 log.error('Header injection detected `%s` for url %s server url ' %
104 104 (parsed.path, parsed))
105 came_from = url('home')
105 came_from = default_came_from
106 106
107 return came_from or url('home')
107 return came_from or default_came_from
108 108
109 109
110 110 class LoginView(BaseAppView):
111 111
112 112 def load_default_context(self):
113 113 c = self._get_local_tmpl_context()
114 114 c.came_from = get_came_from(self.request)
115 115 self._register_global_c(c)
116 116 return c
117 117
118 118 def _get_captcha_data(self):
119 119 settings = SettingsModel().get_all_settings()
120 120 private_key = settings.get('rhodecode_captcha_private_key')
121 121 public_key = settings.get('rhodecode_captcha_public_key')
122 122 active = bool(private_key)
123 123 return CaptchaData(
124 124 active=active, private_key=private_key, public_key=public_key)
125 125
126 126 @view_config(
127 127 route_name='login', request_method='GET',
128 128 renderer='rhodecode:templates/login.mako')
129 129 def login(self):
130 130 c = self.load_default_context()
131 131 auth_user = self._rhodecode_user
132 132
133 133 # redirect if already logged in
134 134 if (auth_user.is_authenticated and
135 135 not auth_user.is_default and auth_user.ip_allowed):
136 136 raise HTTPFound(c.came_from)
137 137
138 138 # check if we use headers plugin, and try to login using it.
139 139 try:
140 140 log.debug('Running PRE-AUTH for headers based authentication')
141 141 auth_info = authenticate(
142 142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 143 if auth_info:
144 144 headers = _store_user_in_session(
145 145 self.session, auth_info.get('username'))
146 146 raise HTTPFound(c.came_from, headers=headers)
147 147 except UserCreationError as e:
148 148 log.error(e)
149 149 self.session.flash(e, queue='error')
150 150
151 151 return self._get_template_context(c)
152 152
153 153 @view_config(
154 154 route_name='login', request_method='POST',
155 155 renderer='rhodecode:templates/login.mako')
156 156 def login_post(self):
157 157 c = self.load_default_context()
158 158
159 159 login_form = LoginForm()()
160 160
161 161 try:
162 162 self.session.invalidate()
163 163 form_result = login_form.to_python(self.request.params)
164 164 # form checks for username/password, now we're authenticated
165 165 headers = _store_user_in_session(
166 166 self.session,
167 167 username=form_result['username'],
168 168 remember=form_result['remember'])
169 169 log.debug('Redirecting to "%s" after login.', c.came_from)
170 170
171 171 audit_user = audit_logger.UserWrap(
172 172 username=self.request.params.get('username'),
173 173 ip_addr=self.request.remote_addr)
174 174 action_data = {'user_agent': self.request.user_agent}
175 175 audit_logger.store(
176 176 action='user.login.success', action_data=action_data,
177 177 user=audit_user, commit=True)
178 178
179 179 raise HTTPFound(c.came_from, headers=headers)
180 180 except formencode.Invalid as errors:
181 181 defaults = errors.value
182 182 # remove password from filling in form again
183 183 defaults.pop('password', None)
184 184 render_ctx = self._get_template_context(c)
185 185 render_ctx.update({
186 186 'errors': errors.error_dict,
187 187 'defaults': defaults,
188 188 })
189 189
190 190 audit_user = audit_logger.UserWrap(
191 191 username=self.request.params.get('username'),
192 192 ip_addr=self.request.remote_addr)
193 193 action_data = {'user_agent': self.request.user_agent}
194 194 audit_logger.store(
195 195 action='user.login.failure', action_data=action_data,
196 196 user=audit_user, commit=True)
197 197 return render_ctx
198 198
199 199 except UserCreationError as e:
200 200 # headers auth or other auth functions that create users on
201 201 # the fly can throw this exception signaling that there's issue
202 202 # with user creation, explanation should be provided in
203 203 # Exception itself
204 204 self.session.flash(e, queue='error')
205 205 return self._get_template_context(c)
206 206
207 207 @CSRFRequired()
208 208 @view_config(route_name='logout', request_method='POST')
209 209 def logout(self):
210 210 auth_user = self._rhodecode_user
211 211 log.info('Deleting session for user: `%s`', auth_user)
212 212
213 213 action_data = {'user_agent': self.request.user_agent}
214 214 audit_logger.store(
215 215 action='user.logout', action_data=action_data,
216 216 user=auth_user, commit=True)
217 217 self.session.delete()
218 return HTTPFound(url('home'))
218 return HTTPFound(h.route_path('home'))
219 219
220 220 @HasPermissionAnyDecorator(
221 221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 222 @view_config(
223 223 route_name='register', request_method='GET',
224 224 renderer='rhodecode:templates/register.mako',)
225 225 def register(self, defaults=None, errors=None):
226 226 c = self.load_default_context()
227 227 defaults = defaults or {}
228 228 errors = errors or {}
229 229
230 230 settings = SettingsModel().get_all_settings()
231 231 register_message = settings.get('rhodecode_register_message') or ''
232 232 captcha = self._get_captcha_data()
233 233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 234 .AuthUser.permissions['global']
235 235
236 236 render_ctx = self._get_template_context(c)
237 237 render_ctx.update({
238 238 'defaults': defaults,
239 239 'errors': errors,
240 240 'auto_active': auto_active,
241 241 'captcha_active': captcha.active,
242 242 'captcha_public_key': captcha.public_key,
243 243 'register_message': register_message,
244 244 })
245 245 return render_ctx
246 246
247 247 @HasPermissionAnyDecorator(
248 248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 249 @view_config(
250 250 route_name='register', request_method='POST',
251 251 renderer='rhodecode:templates/register.mako')
252 252 def register_post(self):
253 253 captcha = self._get_captcha_data()
254 254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 255 .AuthUser.permissions['global']
256 256
257 257 register_form = RegisterForm()()
258 258 try:
259 259 form_result = register_form.to_python(self.request.params)
260 260 form_result['active'] = auto_active
261 261
262 262 if captcha.active:
263 263 response = submit(
264 264 self.request.params.get('recaptcha_challenge_field'),
265 265 self.request.params.get('recaptcha_response_field'),
266 266 private_key=captcha.private_key,
267 267 remoteip=get_ip_addr(self.request.environ))
268 268 if not response.is_valid:
269 269 _value = form_result
270 270 _msg = _('Bad captcha')
271 271 error_dict = {'recaptcha_field': _msg}
272 272 raise formencode.Invalid(_msg, _value, None,
273 273 error_dict=error_dict)
274 274
275 275 new_user = UserModel().create_registration(form_result)
276 276 event = UserRegistered(user=new_user, session=self.session)
277 277 self.request.registry.notify(event)
278 278 self.session.flash(
279 279 _('You have successfully registered with RhodeCode'),
280 280 queue='success')
281 281 Session().commit()
282 282
283 283 redirect_ro = self.request.route_path('login')
284 284 raise HTTPFound(redirect_ro)
285 285
286 286 except formencode.Invalid as errors:
287 287 errors.value.pop('password', None)
288 288 errors.value.pop('password_confirmation', None)
289 289 return self.register(
290 290 defaults=errors.value, errors=errors.error_dict)
291 291
292 292 except UserCreationError as e:
293 293 # container auth or other auth functions that create users on
294 294 # the fly can throw this exception signaling that there's issue
295 295 # with user creation, explanation should be provided in
296 296 # Exception itself
297 297 self.session.flash(e, queue='error')
298 298 return self.register()
299 299
300 300 @view_config(
301 301 route_name='reset_password', request_method=('GET', 'POST'),
302 302 renderer='rhodecode:templates/password_reset.mako')
303 303 def password_reset(self):
304 304 captcha = self._get_captcha_data()
305 305
306 306 render_ctx = {
307 307 'captcha_active': captcha.active,
308 308 'captcha_public_key': captcha.public_key,
309 309 'defaults': {},
310 310 'errors': {},
311 311 }
312 312
313 313 # always send implicit message to prevent from discovery of
314 314 # matching emails
315 315 msg = _('If such email exists, a password reset link was sent to it.')
316 316
317 317 if self.request.POST:
318 318 if h.HasPermissionAny('hg.password_reset.disabled')():
319 319 _email = self.request.POST.get('email', '')
320 320 log.error('Failed attempt to reset password for `%s`.', _email)
321 321 self.session.flash(_('Password reset has been disabled.'),
322 322 queue='error')
323 323 return HTTPFound(self.request.route_path('reset_password'))
324 324
325 325 password_reset_form = PasswordResetForm()()
326 326 try:
327 327 form_result = password_reset_form.to_python(
328 328 self.request.params)
329 329 user_email = form_result['email']
330 330
331 331 if captcha.active:
332 332 response = submit(
333 333 self.request.params.get('recaptcha_challenge_field'),
334 334 self.request.params.get('recaptcha_response_field'),
335 335 private_key=captcha.private_key,
336 336 remoteip=get_ip_addr(self.request.environ))
337 337 if not response.is_valid:
338 338 _value = form_result
339 339 _msg = _('Bad captcha')
340 340 error_dict = {'recaptcha_field': _msg}
341 341 raise formencode.Invalid(
342 342 _msg, _value, None, error_dict=error_dict)
343 343
344 344 # Generate reset URL and send mail.
345 345 user = User.get_by_email(user_email)
346 346
347 347 # generate password reset token that expires in 10minutes
348 348 desc = 'Generated token for password reset from {}'.format(
349 349 datetime.datetime.now().isoformat())
350 350 reset_token = AuthTokenModel().create(
351 351 user, lifetime=10,
352 352 description=desc,
353 353 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 354 Session().commit()
355 355
356 356 log.debug('Successfully created password recovery token')
357 357 password_reset_url = self.request.route_url(
358 358 'reset_password_confirmation',
359 359 _query={'key': reset_token.api_key})
360 360 UserModel().reset_password_link(
361 361 form_result, password_reset_url)
362 362 # Display success message and redirect.
363 363 self.session.flash(msg, queue='success')
364 364
365 365 action_data = {'email': user_email,
366 366 'user_agent': self.request.user_agent}
367 367 audit_logger.store(action='user.password.reset_request',
368 368 action_data=action_data,
369 369 user=self._rhodecode_user, commit=True)
370 370 return HTTPFound(self.request.route_path('reset_password'))
371 371
372 372 except formencode.Invalid as errors:
373 373 render_ctx.update({
374 374 'defaults': errors.value,
375 375 'errors': errors.error_dict,
376 376 })
377 377 if not self.request.params.get('email'):
378 378 # case of empty email, we want to report that
379 379 return render_ctx
380 380
381 381 if 'recaptcha_field' in errors.error_dict:
382 382 # case of failed captcha
383 383 return render_ctx
384 384
385 385 log.debug('faking response on invalid password reset')
386 386 # make this take 2s, to prevent brute forcing.
387 387 time.sleep(2)
388 388 self.session.flash(msg, queue='success')
389 389 return HTTPFound(self.request.route_path('reset_password'))
390 390
391 391 return render_ctx
392 392
393 393 @view_config(route_name='reset_password_confirmation',
394 394 request_method='GET')
395 395 def password_reset_confirmation(self):
396 396
397 397 if self.request.GET and self.request.GET.get('key'):
398 398 # make this take 2s, to prevent brute forcing.
399 399 time.sleep(2)
400 400
401 401 token = AuthTokenModel().get_auth_token(
402 402 self.request.GET.get('key'))
403 403
404 404 # verify token is the correct role
405 405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 406 log.debug('Got token with role:%s expected is %s',
407 407 getattr(token, 'role', 'EMPTY_TOKEN'),
408 408 UserApiKeys.ROLE_PASSWORD_RESET)
409 409 self.session.flash(
410 410 _('Given reset token is invalid'), queue='error')
411 411 return HTTPFound(self.request.route_path('reset_password'))
412 412
413 413 try:
414 414 owner = token.user
415 415 data = {'email': owner.email, 'token': token.api_key}
416 416 UserModel().reset_password(data)
417 417 self.session.flash(
418 418 _('Your password reset was successful, '
419 419 'a new password has been sent to your email'),
420 420 queue='success')
421 421 except Exception as e:
422 422 log.error(e)
423 423 return HTTPFound(self.request.route_path('reset_password'))
424 424
425 425 return HTTPFound(self.request.route_path('login'))
@@ -1,137 +1,138 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 import mock
23 23
24 24 from rhodecode.apps._base import ADMIN_PREFIX
25 25 from rhodecode.lib import helpers as h
26 26 from rhodecode.lib.auth import check_password
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.model.user import UserModel
29 29 from rhodecode.tests import assert_session_flash
30 30 from rhodecode.tests.fixture import Fixture, TestController, error_function
31 31
32 32 fixture = Fixture()
33 33
34 34
35 35 def route_path(name, **kwargs):
36 36 return {
37 37 'home': '/',
38 38 'my_account_password':
39 39 ADMIN_PREFIX + '/my_account/password',
40 40 }[name].format(**kwargs)
41 41
42 42
43 43 test_user_1 = 'testme'
44 44 test_user_1_password = '0jd83nHNS/d23n'
45 45
46 46
47 47 class TestMyAccountPassword(TestController):
48 48 def test_valid_change_password(self, user_util):
49 49 new_password = 'my_new_valid_password'
50 50 user = user_util.create_user(password=test_user_1_password)
51 51 self.log_user(user.username, test_user_1_password)
52 52
53 53 form_data = [
54 54 ('current_password', test_user_1_password),
55 55 ('__start__', 'new_password:mapping'),
56 56 ('new_password', new_password),
57 57 ('new_password-confirm', new_password),
58 58 ('__end__', 'new_password:mapping'),
59 59 ('csrf_token', self.csrf_token),
60 60 ]
61 61 response = self.app.post(route_path('my_account_password'), form_data).follow()
62 62 assert 'Successfully updated password' in response
63 63
64 64 # check_password depends on user being in session
65 65 Session().add(user)
66 66 try:
67 67 assert check_password(new_password, user.password)
68 68 finally:
69 69 Session().expunge(user)
70 70
71 71 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
72 72 ('', 'abcdef123', 'abcdef123'),
73 73 ('wrong_pw', 'abcdef123', 'abcdef123'),
74 74 (test_user_1_password, test_user_1_password, test_user_1_password),
75 75 (test_user_1_password, '', ''),
76 76 (test_user_1_password, 'abcdef123', ''),
77 77 (test_user_1_password, '', 'abcdef123'),
78 78 (test_user_1_password, 'not_the', 'same_pw'),
79 79 (test_user_1_password, 'short', 'short'),
80 80 ])
81 81 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
82 82 user_util):
83 83 user = user_util.create_user(password=test_user_1_password)
84 84 self.log_user(user.username, test_user_1_password)
85 85
86 86 form_data = [
87 87 ('current_password', current_pw),
88 88 ('__start__', 'new_password:mapping'),
89 89 ('new_password', new_pw),
90 90 ('new_password-confirm', confirm_pw),
91 91 ('__end__', 'new_password:mapping'),
92 92 ('csrf_token', self.csrf_token),
93 93 ]
94 94 response = self.app.post(route_path('my_account_password'), form_data)
95 95
96 96 assert_response = response.assert_response()
97 97 assert assert_response.get_elements('.error-block')
98 98
99 99 @mock.patch.object(UserModel, 'update_user', error_function)
100 100 def test_invalid_change_password_exception(self, user_util):
101 101 user = user_util.create_user(password=test_user_1_password)
102 102 self.log_user(user.username, test_user_1_password)
103 103
104 104 form_data = [
105 105 ('current_password', test_user_1_password),
106 106 ('__start__', 'new_password:mapping'),
107 107 ('new_password', '123456'),
108 108 ('new_password-confirm', '123456'),
109 109 ('__end__', 'new_password:mapping'),
110 110 ('csrf_token', self.csrf_token),
111 111 ]
112 112 response = self.app.post(route_path('my_account_password'), form_data)
113 113 assert_session_flash(
114 114 response, 'Error occurred during update of user password')
115 115
116 116 def test_password_is_updated_in_session_on_password_change(self, user_util):
117 117 old_password = 'abcdef123'
118 118 new_password = 'abcdef124'
119 119
120 120 user = user_util.create_user(password=old_password)
121 121 session = self.log_user(user.username, old_password)
122 122 old_password_hash = session['password']
123 123
124 124 form_data = [
125 125 ('current_password', old_password),
126 126 ('__start__', 'new_password:mapping'),
127 127 ('new_password', new_password),
128 128 ('new_password-confirm', new_password),
129 129 ('__end__', 'new_password:mapping'),
130 130 ('csrf_token', self.csrf_token),
131 131 ]
132 132 self.app.post(route_path('my_account_password'), form_data)
133 133
134 134 response = self.app.get(route_path('home'))
135 new_password_hash = response.session['rhodecode_user']['password']
135 session = response.get_session_from_response()
136 new_password_hash = session['rhodecode_user']['password']
136 137
137 138 assert old_password_hash != new_password_hash No newline at end of file
@@ -1,129 +1,133 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 def includeme(config):
23 23
24 24 # Summary
25 25 config.add_route(
26 name='repo_summary',
27 pattern='/{repo_name:.*?[^/]}', repo_route=True)
28
29 config.add_route(
26 30 name='repo_summary_explicit',
27 31 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
28 32
29 33 # Tags
30 34 config.add_route(
31 35 name='tags_home',
32 36 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
33 37
34 38 # Branches
35 39 config.add_route(
36 40 name='branches_home',
37 41 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
38 42
39 43 # Bookmarks
40 44 config.add_route(
41 45 name='bookmarks_home',
42 46 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
43 47
44 48 # Pull Requests
45 49 config.add_route(
46 50 name='pullrequest_show',
47 51 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
48 52 repo_route=True)
49 53
50 54 config.add_route(
51 55 name='pullrequest_show_all',
52 56 pattern='/{repo_name:.*?[^/]}/pull-request',
53 57 repo_route=True, repo_accepted_types=['hg', 'git'])
54 58
55 59 config.add_route(
56 60 name='pullrequest_show_all_data',
57 61 pattern='/{repo_name:.*?[^/]}/pull-request-data',
58 62 repo_route=True, repo_accepted_types=['hg', 'git'])
59 63
60 64 # Settings
61 65 config.add_route(
62 66 name='edit_repo',
63 67 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
64 68
65 69 # Settings advanced
66 70 config.add_route(
67 71 name='edit_repo_advanced',
68 72 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
69 73 config.add_route(
70 74 name='edit_repo_advanced_delete',
71 75 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
72 76 config.add_route(
73 77 name='edit_repo_advanced_locking',
74 78 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
75 79 config.add_route(
76 80 name='edit_repo_advanced_journal',
77 81 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
78 82 config.add_route(
79 83 name='edit_repo_advanced_fork',
80 84 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
81 85
82 86 # Caches
83 87 config.add_route(
84 88 name='edit_repo_caches',
85 89 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
86 90
87 91 # Permissions
88 92 config.add_route(
89 93 name='edit_repo_perms',
90 94 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
91 95
92 96 # Repo Review Rules
93 97 config.add_route(
94 98 name='repo_reviewers',
95 99 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
96 100
97 101 config.add_route(
98 102 name='repo_default_reviewers_data',
99 103 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
100 104
101 105 # Maintenance
102 106 config.add_route(
103 107 name='repo_maintenance',
104 108 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
105 109
106 110 config.add_route(
107 111 name='repo_maintenance_execute',
108 112 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
109 113
110 114 # Strip
111 115 config.add_route(
112 116 name='strip',
113 117 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
114 118
115 119 config.add_route(
116 120 name='strip_check',
117 121 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
118 122
119 123 config.add_route(
120 124 name='strip_execute',
121 125 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
122 126
123 127 # NOTE(marcink): needs to be at the end for catch-all
124 128 # config.add_route(
125 129 # name='repo_summary',
126 130 # pattern='/{repo_name:.*?[^/]}', repo_route=True)
127 131
128 132 # Scan module for configuration decorators.
129 133 config.scan()
@@ -1,515 +1,516 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 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 47 from rhodecode.lib.middleware import csrf
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.error_handling import (
50 50 PylonsErrorHandlingMiddleware)
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import (
56 56 scan_repositories_if_enabled, write_js_routes_if_enabled,
57 57 write_metadata_if_needed)
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 # this is used to avoid avoid the route lookup overhead in routesmiddleware
64 64 # for certain routes which won't go to pylons to - eg. static files, debugger
65 65 # it is only needed for the pylons migration and can be removed once complete
66 66 class SkippableRoutesMiddleware(RoutesMiddleware):
67 67 """ Routes middleware that allows you to skip prefixes """
68 68
69 69 def __init__(self, *args, **kw):
70 70 self.skip_prefixes = kw.pop('skip_prefixes', [])
71 71 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
72 72
73 73 def __call__(self, environ, start_response):
74 74 for prefix in self.skip_prefixes:
75 75 if environ['PATH_INFO'].startswith(prefix):
76 76 # added to avoid the case when a missing /_static route falls
77 77 # through to pylons and causes an exception as pylons is
78 78 # expecting wsgiorg.routingargs to be set in the environ
79 79 # by RoutesMiddleware.
80 80 if 'wsgiorg.routing_args' not in environ:
81 81 environ['wsgiorg.routing_args'] = (None, {})
82 82 return self.app(environ, start_response)
83 83
84 84 return super(SkippableRoutesMiddleware, self).__call__(
85 85 environ, start_response)
86 86
87 87
88 88 def make_app(global_conf, static_files=True, **app_conf):
89 89 """Create a Pylons WSGI application and return it
90 90
91 91 ``global_conf``
92 92 The inherited configuration for this application. Normally from
93 93 the [DEFAULT] section of the Paste ini file.
94 94
95 95 ``app_conf``
96 96 The application's local configuration. Normally specified in
97 97 the [app:<name>] section of the Paste ini file (where <name>
98 98 defaults to main).
99 99
100 100 """
101 101 # Apply compatibility patches
102 102 patches.kombu_1_5_1_python_2_7_11()
103 103 patches.inspect_getargspec()
104 104
105 105 # Configure the Pylons environment
106 106 config = load_environment(global_conf, app_conf)
107 107
108 108 # The Pylons WSGI app
109 109 app = PylonsApp(config=config)
110 110 if rhodecode.is_test:
111 111 app = csrf.CSRFDetector(app)
112 112
113 113 expected_origin = config.get('expected_origin')
114 114 if expected_origin:
115 115 # The API can be accessed from other Origins.
116 116 app = csrf.OriginChecker(app, expected_origin,
117 117 skip_urls=[routes.util.url_for('api')])
118 118
119 119 # Establish the Registry for this application
120 120 app = RegistryManager(app)
121 121
122 122 app.config = config
123 123
124 124 return app
125 125
126 126
127 127 def make_pyramid_app(global_config, **settings):
128 128 """
129 129 Constructs the WSGI application based on Pyramid and wraps the Pylons based
130 130 application.
131 131
132 132 Specials:
133 133
134 134 * We migrate from Pylons to Pyramid. While doing this, we keep both
135 135 frameworks functional. This involves moving some WSGI middlewares around
136 136 and providing access to some data internals, so that the old code is
137 137 still functional.
138 138
139 139 * The application can also be integrated like a plugin via the call to
140 140 `includeme`. This is accompanied with the other utility functions which
141 141 are called. Changing this should be done with great care to not break
142 142 cases when these fragments are assembled from another place.
143 143
144 144 """
145 145 # The edition string should be available in pylons too, so we add it here
146 146 # before copying the settings.
147 147 settings.setdefault('rhodecode.edition', 'Community Edition')
148 148
149 149 # As long as our Pylons application does expect "unprepared" settings, make
150 150 # sure that we keep an unmodified copy. This avoids unintentional change of
151 151 # behavior in the old application.
152 152 settings_pylons = settings.copy()
153 153
154 154 sanitize_settings_and_apply_defaults(settings)
155 155 config = Configurator(settings=settings)
156 156 add_pylons_compat_data(config.registry, global_config, settings_pylons)
157 157
158 158 load_pyramid_environment(global_config, settings)
159 159
160 160 includeme_first(config)
161 161 includeme(config)
162 162 pyramid_app = config.make_wsgi_app()
163 163 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
164 164 pyramid_app.config = config
165 165
166 166 # creating the app uses a connection - return it after we are done
167 167 meta.Session.remove()
168 168
169 169 return pyramid_app
170 170
171 171
172 172 def make_not_found_view(config):
173 173 """
174 174 This creates the view which should be registered as not-found-view to
175 175 pyramid. Basically it contains of the old pylons app, converted to a view.
176 176 Additionally it is wrapped by some other middlewares.
177 177 """
178 178 settings = config.registry.settings
179 179 vcs_server_enabled = settings['vcs.server.enable']
180 180
181 181 # Make pylons app from unprepared settings.
182 182 pylons_app = make_app(
183 183 config.registry._pylons_compat_global_config,
184 184 **config.registry._pylons_compat_settings)
185 185 config.registry._pylons_compat_config = pylons_app.config
186 186
187 187 # Appenlight monitoring.
188 188 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
189 189 pylons_app, settings)
190 190
191 191 # The pylons app is executed inside of the pyramid 404 exception handler.
192 192 # Exceptions which are raised inside of it are not handled by pyramid
193 193 # again. Therefore we add a middleware that invokes the error handler in
194 194 # case of an exception or error response. This way we return proper error
195 195 # HTML pages in case of an error.
196 196 reraise = (settings.get('debugtoolbar.enabled', False) or
197 197 rhodecode.disable_error_handler)
198 198 pylons_app = PylonsErrorHandlingMiddleware(
199 199 pylons_app, error_handler, reraise)
200 200
201 201 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
202 202 # view to handle the request. Therefore it is wrapped around the pylons
203 203 # app. It has to be outside of the error handling otherwise error responses
204 204 # from the vcsserver are converted to HTML error pages. This confuses the
205 205 # command line tools and the user won't get a meaningful error message.
206 206 if vcs_server_enabled:
207 207 pylons_app = VCSMiddleware(
208 208 pylons_app, settings, appenlight_client, registry=config.registry)
209 209
210 210 # Convert WSGI app to pyramid view and return it.
211 211 return wsgiapp(pylons_app)
212 212
213 213
214 214 def add_pylons_compat_data(registry, global_config, settings):
215 215 """
216 216 Attach data to the registry to support the Pylons integration.
217 217 """
218 218 registry._pylons_compat_global_config = global_config
219 219 registry._pylons_compat_settings = settings
220 220
221 221
222 222 def error_handler(exception, request):
223 223 import rhodecode
224 224 from rhodecode.lib.utils2 import AttributeDict
225 225 from rhodecode.lib import helpers
226 226
227 227 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
228 228
229 229 base_response = HTTPInternalServerError()
230 230 # prefer original exception for the response since it may have headers set
231 231 if isinstance(exception, HTTPException):
232 232 base_response = exception
233 233
234 234 def is_http_error(response):
235 235 # error which should have traceback
236 236 return response.status_code > 499
237 237
238 238 if is_http_error(base_response):
239 239 log.exception(
240 240 'error occurred handling this request for path: %s', request.path)
241 241
242 242 c = AttributeDict()
243 243 c.error_message = base_response.status
244 244 c.error_explanation = base_response.explanation or str(base_response)
245 245 c.visual = AttributeDict()
246 246
247 247 c.visual.rhodecode_support_url = (
248 248 request.registry.settings.get('rhodecode_support_url') or
249 249 request.route_url('rhodecode_support')
250 250 )
251 251 c.redirect_time = 0
252 252 c.rhodecode_name = rhodecode_title
253 253 if not c.rhodecode_name:
254 254 c.rhodecode_name = 'Rhodecode'
255 255
256 256 c.causes = []
257 257 if hasattr(base_response, 'causes'):
258 258 c.causes = base_response.causes
259 259 c.messages = helpers.flash.pop_messages()
260 260 response = render_to_response(
261 261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
262 262 response=base_response)
263 263
264 264 return response
265 265
266 266
267 267 def includeme(config):
268 268 settings = config.registry.settings
269 269
270 270 # plugin information
271 271 config.registry.rhodecode_plugins = OrderedDict()
272 272
273 273 config.add_directive(
274 274 'register_rhodecode_plugin', register_rhodecode_plugin)
275 275
276 276 if asbool(settings.get('appenlight', 'false')):
277 277 config.include('appenlight_client.ext.pyramid_tween')
278 278
279 279 # Includes which are required. The application would fail without them.
280 280 config.include('pyramid_mako')
281 281 config.include('pyramid_beaker')
282 282
283 283 config.include('rhodecode.authentication')
284 284 config.include('rhodecode.integrations')
285 285
286 286 # apps
287 287 config.include('rhodecode.apps._base')
288 288 config.include('rhodecode.apps.ops')
289 289
290 290 config.include('rhodecode.apps.admin')
291 291 config.include('rhodecode.apps.channelstream')
292 292 config.include('rhodecode.apps.login')
293 293 config.include('rhodecode.apps.home')
294 294 config.include('rhodecode.apps.repository')
295 config.include('rhodecode.apps.repo_group')
295 296 config.include('rhodecode.apps.search')
296 297 config.include('rhodecode.apps.user_profile')
297 298 config.include('rhodecode.apps.my_account')
298 299 config.include('rhodecode.apps.svn_support')
299 300
300 301 config.include('rhodecode.tweens')
301 302 config.include('rhodecode.api')
302 303
303 304 config.add_route(
304 305 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
305 306
306 307 config.add_translation_dirs('rhodecode:i18n/')
307 308 settings['default_locale_name'] = settings.get('lang', 'en')
308 309
309 310 # Add subscribers.
310 311 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 312 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
312 313 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
313 314
314 315 # Set the authorization policy.
315 316 authz_policy = ACLAuthorizationPolicy()
316 317 config.set_authorization_policy(authz_policy)
317 318
318 319 # Set the default renderer for HTML templates to mako.
319 320 config.add_mako_renderer('.html')
320 321
321 322 config.add_renderer(
322 323 name='json_ext',
323 324 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
324 325
325 326 # include RhodeCode plugins
326 327 includes = aslist(settings.get('rhodecode.includes', []))
327 328 for inc in includes:
328 329 config.include(inc)
329 330
330 331 # This is the glue which allows us to migrate in chunks. By registering the
331 332 # pylons based application as the "Not Found" view in Pyramid, we will
332 333 # fallback to the old application each time the new one does not yet know
333 334 # how to handle a request.
334 335 config.add_notfound_view(make_not_found_view(config))
335 336
336 337 if not settings.get('debugtoolbar.enabled', False):
337 338 # if no toolbar, then any exception gets caught and rendered
338 339 config.add_view(error_handler, context=Exception)
339 340
340 341 config.add_view(error_handler, context=HTTPError)
341 342
342 343
343 344 def includeme_first(config):
344 345 # redirect automatic browser favicon.ico requests to correct place
345 346 def favicon_redirect(context, request):
346 347 return HTTPFound(
347 348 request.static_path('rhodecode:public/images/favicon.ico'))
348 349
349 350 config.add_view(favicon_redirect, route_name='favicon')
350 351 config.add_route('favicon', '/favicon.ico')
351 352
352 353 def robots_redirect(context, request):
353 354 return HTTPFound(
354 355 request.static_path('rhodecode:public/robots.txt'))
355 356
356 357 config.add_view(robots_redirect, route_name='robots')
357 358 config.add_route('robots', '/robots.txt')
358 359
359 360 config.add_static_view(
360 361 '_static/deform', 'deform:static')
361 362 config.add_static_view(
362 363 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
363 364
364 365
365 366 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
366 367 """
367 368 Apply outer WSGI middlewares around the application.
368 369
369 370 Part of this has been moved up from the Pylons layer, so that the
370 371 data is also available if old Pylons code is hit through an already ported
371 372 view.
372 373 """
373 374 settings = config.registry.settings
374 375
375 376 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
376 377 pyramid_app = HttpsFixup(pyramid_app, settings)
377 378
378 379 # Add RoutesMiddleware to support the pylons compatibility tween during
379 380 # migration to pyramid.
380 381 pyramid_app = SkippableRoutesMiddleware(
381 382 pyramid_app, config.registry._pylons_compat_config['routes.map'],
382 383 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
383 384
384 385 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
385 386
386 387 if settings['gzip_responses']:
387 388 pyramid_app = make_gzip_middleware(
388 389 pyramid_app, settings, compress_level=1)
389 390
390 391 # this should be the outer most middleware in the wsgi stack since
391 392 # middleware like Routes make database calls
392 393 def pyramid_app_with_cleanup(environ, start_response):
393 394 try:
394 395 return pyramid_app(environ, start_response)
395 396 finally:
396 397 # Dispose current database session and rollback uncommitted
397 398 # transactions.
398 399 meta.Session.remove()
399 400
400 401 # In a single threaded mode server, on non sqlite db we should have
401 402 # '0 Current Checked out connections' at the end of a request,
402 403 # if not, then something, somewhere is leaving a connection open
403 404 pool = meta.Base.metadata.bind.engine.pool
404 405 log.debug('sa pool status: %s', pool.status())
405 406
406 407
407 408 return pyramid_app_with_cleanup
408 409
409 410
410 411 def sanitize_settings_and_apply_defaults(settings):
411 412 """
412 413 Applies settings defaults and does all type conversion.
413 414
414 415 We would move all settings parsing and preparation into this place, so that
415 416 we have only one place left which deals with this part. The remaining parts
416 417 of the application would start to rely fully on well prepared settings.
417 418
418 419 This piece would later be split up per topic to avoid a big fat monster
419 420 function.
420 421 """
421 422
422 423 # Pyramid's mako renderer has to search in the templates folder so that the
423 424 # old templates still work. Ported and new templates are expected to use
424 425 # real asset specifications for the includes.
425 426 mako_directories = settings.setdefault('mako.directories', [
426 427 # Base templates of the original Pylons application
427 428 'rhodecode:templates',
428 429 ])
429 430 log.debug(
430 431 "Using the following Mako template directories: %s",
431 432 mako_directories)
432 433
433 434 # Default includes, possible to change as a user
434 435 pyramid_includes = settings.setdefault('pyramid.includes', [
435 436 'rhodecode.lib.middleware.request_wrapper',
436 437 ])
437 438 log.debug(
438 439 "Using the following pyramid.includes: %s",
439 440 pyramid_includes)
440 441
441 442 # TODO: johbo: Re-think this, usually the call to config.include
442 443 # should allow to pass in a prefix.
443 444 settings.setdefault('rhodecode.api.url', '/_admin/api')
444 445
445 446 # Sanitize generic settings.
446 447 _list_setting(settings, 'default_encoding', 'UTF-8')
447 448 _bool_setting(settings, 'is_test', 'false')
448 449 _bool_setting(settings, 'gzip_responses', 'false')
449 450
450 451 # Call split out functions that sanitize settings for each topic.
451 452 _sanitize_appenlight_settings(settings)
452 453 _sanitize_vcs_settings(settings)
453 454
454 455 return settings
455 456
456 457
457 458 def _sanitize_appenlight_settings(settings):
458 459 _bool_setting(settings, 'appenlight', 'false')
459 460
460 461
461 462 def _sanitize_vcs_settings(settings):
462 463 """
463 464 Applies settings defaults and does type conversion for all VCS related
464 465 settings.
465 466 """
466 467 _string_setting(settings, 'vcs.svn.compatible_version', '')
467 468 _string_setting(settings, 'git_rev_filter', '--all')
468 469 _string_setting(settings, 'vcs.hooks.protocol', 'http')
469 470 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
470 471 _string_setting(settings, 'vcs.server', '')
471 472 _string_setting(settings, 'vcs.server.log_level', 'debug')
472 473 _string_setting(settings, 'vcs.server.protocol', 'http')
473 474 _bool_setting(settings, 'startup.import_repos', 'false')
474 475 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
475 476 _bool_setting(settings, 'vcs.server.enable', 'true')
476 477 _bool_setting(settings, 'vcs.start_server', 'false')
477 478 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
478 479 _int_setting(settings, 'vcs.connection_timeout', 3600)
479 480
480 481 # Support legacy values of vcs.scm_app_implementation. Legacy
481 482 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
482 483 # which is now mapped to 'http'.
483 484 scm_app_impl = settings['vcs.scm_app_implementation']
484 485 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
485 486 settings['vcs.scm_app_implementation'] = 'http'
486 487
487 488
488 489 def _int_setting(settings, name, default):
489 490 settings[name] = int(settings.get(name, default))
490 491
491 492
492 493 def _bool_setting(settings, name, default):
493 494 input = settings.get(name, default)
494 495 if isinstance(input, unicode):
495 496 input = input.encode('utf8')
496 497 settings[name] = asbool(input)
497 498
498 499
499 500 def _list_setting(settings, name, default):
500 501 raw_value = settings.get(name, default)
501 502
502 503 old_separator = ','
503 504 if old_separator in raw_value:
504 505 # If we get a comma separated list, pass it to our own function.
505 506 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
506 507 else:
507 508 # Otherwise we assume it uses pyramids space/newline separation.
508 509 settings[name] = aslist(raw_value)
509 510
510 511
511 512 def _string_setting(settings, name, default, lower=True):
512 513 value = settings.get(name, default)
513 514 if lower:
514 515 value = value.lower()
515 516 settings[name] = value
@@ -1,1027 +1,1017 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 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 # prefix for non repository related links needs to be prefixed with `/`
36 36 ADMIN_PREFIX = '/_admin'
37 37 STATIC_FILE_PREFIX = '/_static'
38 38
39 39 # Default requirements for URL parts
40 40 URL_NAME_REQUIREMENTS = {
41 41 # group name can have a slash in them, but they must not end with a slash
42 42 'group_name': r'.*?[^/]',
43 43 'repo_group_name': r'.*?[^/]',
44 44 # repo names can have a slash in them, but they must not end with a slash
45 45 'repo_name': r'.*?[^/]',
46 46 # file path eats up everything at the end
47 47 'f_path': r'.*',
48 48 # reference types
49 49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 51 }
52 52
53 53
54 54 def add_route_requirements(route_path, requirements):
55 55 """
56 56 Adds regex requirements to pyramid routes using a mapping dict
57 57
58 58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 59 '/{action}/{id:\d+}'
60 60
61 61 """
62 62 for key, regex in requirements.items():
63 63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 64 return route_path
65 65
66 66
67 67 class JSRoutesMapper(Mapper):
68 68 """
69 69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
70 70 """
71 71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
72 72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
73 73 def __init__(self, *args, **kw):
74 74 super(JSRoutesMapper, self).__init__(*args, **kw)
75 75 self._jsroutes = []
76 76
77 77 def connect(self, *args, **kw):
78 78 """
79 79 Wrapper for connect to take an extra argument jsroute=True
80 80
81 81 :param jsroute: boolean, if True will add the route to the pyroutes list
82 82 """
83 83 if kw.pop('jsroute', False):
84 84 if not self._named_route_regex.match(args[0]):
85 85 raise Exception('only named routes can be added to pyroutes')
86 86 self._jsroutes.append(args[0])
87 87
88 88 super(JSRoutesMapper, self).connect(*args, **kw)
89 89
90 90 def _extract_route_information(self, route):
91 91 """
92 92 Convert a route into tuple(name, path, args), eg:
93 93 ('show_user', '/profile/%(username)s', ['username'])
94 94 """
95 95 routepath = route.routepath
96 96 def replace(matchobj):
97 97 if matchobj.group(1):
98 98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
99 99 else:
100 100 return "%%(%s)s" % matchobj.group(2)
101 101
102 102 routepath = self._argument_prog.sub(replace, routepath)
103 103 return (
104 104 route.name,
105 105 routepath,
106 106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
107 107 for arg in self._argument_prog.findall(route.routepath)]
108 108 )
109 109
110 110 def jsroutes(self):
111 111 """
112 112 Return a list of pyroutes.js compatible routes
113 113 """
114 114 for route_name in self._jsroutes:
115 115 yield self._extract_route_information(self._routenames[route_name])
116 116
117 117
118 118 def make_map(config):
119 119 """Create, configure and return the routes Mapper"""
120 120 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
121 121 always_scan=config['debug'])
122 122 rmap.minimization = False
123 123 rmap.explicit = False
124 124
125 125 from rhodecode.lib.utils2 import str2bool
126 126 from rhodecode.model import repo, repo_group
127 127
128 128 def check_repo(environ, match_dict):
129 129 """
130 130 check for valid repository for proper 404 handling
131 131
132 132 :param environ:
133 133 :param match_dict:
134 134 """
135 135 repo_name = match_dict.get('repo_name')
136 136
137 137 if match_dict.get('f_path'):
138 138 # fix for multiple initial slashes that causes errors
139 139 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
140 140 repo_model = repo.RepoModel()
141 141 by_name_match = repo_model.get_by_repo_name(repo_name)
142 142 # if we match quickly from database, short circuit the operation,
143 143 # and validate repo based on the type.
144 144 if by_name_match:
145 145 return True
146 146
147 147 by_id_match = repo_model.get_repo_by_id(repo_name)
148 148 if by_id_match:
149 149 repo_name = by_id_match.repo_name
150 150 match_dict['repo_name'] = repo_name
151 151 return True
152 152
153 153 return False
154 154
155 155 def check_group(environ, match_dict):
156 156 """
157 157 check for valid repository group path for proper 404 handling
158 158
159 159 :param environ:
160 160 :param match_dict:
161 161 """
162 162 repo_group_name = match_dict.get('group_name')
163 163 repo_group_model = repo_group.RepoGroupModel()
164 164 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
165 165 if by_name_match:
166 166 return True
167 167
168 168 return False
169 169
170 170 def check_user_group(environ, match_dict):
171 171 """
172 172 check for valid user group for proper 404 handling
173 173
174 174 :param environ:
175 175 :param match_dict:
176 176 """
177 177 return True
178 178
179 179 def check_int(environ, match_dict):
180 180 return match_dict.get('id').isdigit()
181 181
182 182
183 183 #==========================================================================
184 184 # CUSTOM ROUTES HERE
185 185 #==========================================================================
186 186
187 # MAIN PAGE
188 rmap.connect('home', '/', controller='home', action='index')
189
190 187 # ping and pylons error test
191 188 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
192 189 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
193 190
194 191 # ADMIN REPOSITORY ROUTES
195 192 with rmap.submapper(path_prefix=ADMIN_PREFIX,
196 193 controller='admin/repos') as m:
197 194 m.connect('repos', '/repos',
198 195 action='create', conditions={'method': ['POST']})
199 196 m.connect('repos', '/repos',
200 197 action='index', conditions={'method': ['GET']})
201 198 m.connect('new_repo', '/create_repository', jsroute=True,
202 199 action='create_repository', conditions={'method': ['GET']})
203 200 m.connect('delete_repo', '/repos/{repo_name}',
204 201 action='delete', conditions={'method': ['DELETE']},
205 202 requirements=URL_NAME_REQUIREMENTS)
206 203 m.connect('repo', '/repos/{repo_name}',
207 204 action='show', conditions={'method': ['GET'],
208 205 'function': check_repo},
209 206 requirements=URL_NAME_REQUIREMENTS)
210 207
211 208 # ADMIN REPOSITORY GROUPS ROUTES
212 209 with rmap.submapper(path_prefix=ADMIN_PREFIX,
213 210 controller='admin/repo_groups') as m:
214 211 m.connect('repo_groups', '/repo_groups',
215 212 action='create', conditions={'method': ['POST']})
216 213 m.connect('repo_groups', '/repo_groups',
217 214 action='index', conditions={'method': ['GET']})
218 215 m.connect('new_repo_group', '/repo_groups/new',
219 216 action='new', conditions={'method': ['GET']})
220 217 m.connect('update_repo_group', '/repo_groups/{group_name}',
221 218 action='update', conditions={'method': ['PUT'],
222 219 'function': check_group},
223 220 requirements=URL_NAME_REQUIREMENTS)
224 221
225 222 # EXTRAS REPO GROUP ROUTES
226 223 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
227 224 action='edit',
228 225 conditions={'method': ['GET'], 'function': check_group},
229 226 requirements=URL_NAME_REQUIREMENTS)
230 227 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
231 228 action='edit',
232 229 conditions={'method': ['PUT'], 'function': check_group},
233 230 requirements=URL_NAME_REQUIREMENTS)
234 231
235 232 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
236 233 action='edit_repo_group_advanced',
237 234 conditions={'method': ['GET'], 'function': check_group},
238 235 requirements=URL_NAME_REQUIREMENTS)
239 236 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
240 237 action='edit_repo_group_advanced',
241 238 conditions={'method': ['PUT'], 'function': check_group},
242 239 requirements=URL_NAME_REQUIREMENTS)
243 240
244 241 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
245 242 action='edit_repo_group_perms',
246 243 conditions={'method': ['GET'], 'function': check_group},
247 244 requirements=URL_NAME_REQUIREMENTS)
248 245 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
249 246 action='update_perms',
250 247 conditions={'method': ['PUT'], 'function': check_group},
251 248 requirements=URL_NAME_REQUIREMENTS)
252 249
253 250 m.connect('delete_repo_group', '/repo_groups/{group_name}',
254 251 action='delete', conditions={'method': ['DELETE'],
255 252 'function': check_group},
256 253 requirements=URL_NAME_REQUIREMENTS)
257 254
258 255 # ADMIN USER ROUTES
259 256 with rmap.submapper(path_prefix=ADMIN_PREFIX,
260 257 controller='admin/users') as m:
261 258 m.connect('users', '/users',
262 259 action='create', conditions={'method': ['POST']})
263 260 m.connect('new_user', '/users/new',
264 261 action='new', conditions={'method': ['GET']})
265 262 m.connect('update_user', '/users/{user_id}',
266 263 action='update', conditions={'method': ['PUT']})
267 264 m.connect('delete_user', '/users/{user_id}',
268 265 action='delete', conditions={'method': ['DELETE']})
269 266 m.connect('edit_user', '/users/{user_id}/edit',
270 267 action='edit', conditions={'method': ['GET']}, jsroute=True)
271 268 m.connect('user', '/users/{user_id}',
272 269 action='show', conditions={'method': ['GET']})
273 270 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
274 271 action='reset_password', conditions={'method': ['POST']})
275 272 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
276 273 action='create_personal_repo_group', conditions={'method': ['POST']})
277 274
278 275 # EXTRAS USER ROUTES
279 276 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
280 277 action='edit_advanced', conditions={'method': ['GET']})
281 278 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
282 279 action='update_advanced', conditions={'method': ['PUT']})
283 280
284 281 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
285 282 action='edit_global_perms', conditions={'method': ['GET']})
286 283 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
287 284 action='update_global_perms', conditions={'method': ['PUT']})
288 285
289 286 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
290 287 action='edit_perms_summary', conditions={'method': ['GET']})
291 288
292 289 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
293 290 action='edit_emails', conditions={'method': ['GET']})
294 291 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
295 292 action='add_email', conditions={'method': ['PUT']})
296 293 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
297 294 action='delete_email', conditions={'method': ['DELETE']})
298 295
299 296 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
300 297 action='edit_ips', conditions={'method': ['GET']})
301 298 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
302 299 action='add_ip', conditions={'method': ['PUT']})
303 300 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
304 301 action='delete_ip', conditions={'method': ['DELETE']})
305 302
306 303 # ADMIN USER GROUPS REST ROUTES
307 304 with rmap.submapper(path_prefix=ADMIN_PREFIX,
308 305 controller='admin/user_groups') as m:
309 306 m.connect('users_groups', '/user_groups',
310 307 action='create', conditions={'method': ['POST']})
311 308 m.connect('users_groups', '/user_groups',
312 309 action='index', conditions={'method': ['GET']})
313 310 m.connect('new_users_group', '/user_groups/new',
314 311 action='new', conditions={'method': ['GET']})
315 312 m.connect('update_users_group', '/user_groups/{user_group_id}',
316 313 action='update', conditions={'method': ['PUT']})
317 314 m.connect('delete_users_group', '/user_groups/{user_group_id}',
318 315 action='delete', conditions={'method': ['DELETE']})
319 316 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
320 317 action='edit', conditions={'method': ['GET']},
321 318 function=check_user_group)
322 319
323 320 # EXTRAS USER GROUP ROUTES
324 321 m.connect('edit_user_group_global_perms',
325 322 '/user_groups/{user_group_id}/edit/global_permissions',
326 323 action='edit_global_perms', conditions={'method': ['GET']})
327 324 m.connect('edit_user_group_global_perms',
328 325 '/user_groups/{user_group_id}/edit/global_permissions',
329 326 action='update_global_perms', conditions={'method': ['PUT']})
330 327 m.connect('edit_user_group_perms_summary',
331 328 '/user_groups/{user_group_id}/edit/permissions_summary',
332 329 action='edit_perms_summary', conditions={'method': ['GET']})
333 330
334 331 m.connect('edit_user_group_perms',
335 332 '/user_groups/{user_group_id}/edit/permissions',
336 333 action='edit_perms', conditions={'method': ['GET']})
337 334 m.connect('edit_user_group_perms',
338 335 '/user_groups/{user_group_id}/edit/permissions',
339 336 action='update_perms', conditions={'method': ['PUT']})
340 337
341 338 m.connect('edit_user_group_advanced',
342 339 '/user_groups/{user_group_id}/edit/advanced',
343 340 action='edit_advanced', conditions={'method': ['GET']})
344 341
345 342 m.connect('edit_user_group_advanced_sync',
346 343 '/user_groups/{user_group_id}/edit/advanced/sync',
347 344 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
348 345
349 346 m.connect('edit_user_group_members',
350 347 '/user_groups/{user_group_id}/edit/members', jsroute=True,
351 348 action='user_group_members', conditions={'method': ['GET']})
352 349
353 350 # ADMIN PERMISSIONS ROUTES
354 351 with rmap.submapper(path_prefix=ADMIN_PREFIX,
355 352 controller='admin/permissions') as m:
356 353 m.connect('admin_permissions_application', '/permissions/application',
357 354 action='permission_application_update', conditions={'method': ['POST']})
358 355 m.connect('admin_permissions_application', '/permissions/application',
359 356 action='permission_application', conditions={'method': ['GET']})
360 357
361 358 m.connect('admin_permissions_global', '/permissions/global',
362 359 action='permission_global_update', conditions={'method': ['POST']})
363 360 m.connect('admin_permissions_global', '/permissions/global',
364 361 action='permission_global', conditions={'method': ['GET']})
365 362
366 363 m.connect('admin_permissions_object', '/permissions/object',
367 364 action='permission_objects_update', conditions={'method': ['POST']})
368 365 m.connect('admin_permissions_object', '/permissions/object',
369 366 action='permission_objects', conditions={'method': ['GET']})
370 367
371 368 m.connect('admin_permissions_ips', '/permissions/ips',
372 369 action='permission_ips', conditions={'method': ['POST']})
373 370 m.connect('admin_permissions_ips', '/permissions/ips',
374 371 action='permission_ips', conditions={'method': ['GET']})
375 372
376 373 m.connect('admin_permissions_overview', '/permissions/overview',
377 374 action='permission_perms', conditions={'method': ['GET']})
378 375
379 376 # ADMIN DEFAULTS REST ROUTES
380 377 with rmap.submapper(path_prefix=ADMIN_PREFIX,
381 378 controller='admin/defaults') as m:
382 379 m.connect('admin_defaults_repositories', '/defaults/repositories',
383 380 action='update_repository_defaults', conditions={'method': ['POST']})
384 381 m.connect('admin_defaults_repositories', '/defaults/repositories',
385 382 action='index', conditions={'method': ['GET']})
386 383
387 384 # ADMIN DEBUG STYLE ROUTES
388 385 if str2bool(config.get('debug_style')):
389 386 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
390 387 controller='debug_style') as m:
391 388 m.connect('debug_style_home', '',
392 389 action='index', conditions={'method': ['GET']})
393 390 m.connect('debug_style_template', '/t/{t_path}',
394 391 action='template', conditions={'method': ['GET']})
395 392
396 393 # ADMIN SETTINGS ROUTES
397 394 with rmap.submapper(path_prefix=ADMIN_PREFIX,
398 395 controller='admin/settings') as m:
399 396
400 397 # default
401 398 m.connect('admin_settings', '/settings',
402 399 action='settings_global_update',
403 400 conditions={'method': ['POST']})
404 401 m.connect('admin_settings', '/settings',
405 402 action='settings_global', conditions={'method': ['GET']})
406 403
407 404 m.connect('admin_settings_vcs', '/settings/vcs',
408 405 action='settings_vcs_update',
409 406 conditions={'method': ['POST']})
410 407 m.connect('admin_settings_vcs', '/settings/vcs',
411 408 action='settings_vcs',
412 409 conditions={'method': ['GET']})
413 410 m.connect('admin_settings_vcs', '/settings/vcs',
414 411 action='delete_svn_pattern',
415 412 conditions={'method': ['DELETE']})
416 413
417 414 m.connect('admin_settings_mapping', '/settings/mapping',
418 415 action='settings_mapping_update',
419 416 conditions={'method': ['POST']})
420 417 m.connect('admin_settings_mapping', '/settings/mapping',
421 418 action='settings_mapping', conditions={'method': ['GET']})
422 419
423 420 m.connect('admin_settings_global', '/settings/global',
424 421 action='settings_global_update',
425 422 conditions={'method': ['POST']})
426 423 m.connect('admin_settings_global', '/settings/global',
427 424 action='settings_global', conditions={'method': ['GET']})
428 425
429 426 m.connect('admin_settings_visual', '/settings/visual',
430 427 action='settings_visual_update',
431 428 conditions={'method': ['POST']})
432 429 m.connect('admin_settings_visual', '/settings/visual',
433 430 action='settings_visual', conditions={'method': ['GET']})
434 431
435 432 m.connect('admin_settings_issuetracker',
436 433 '/settings/issue-tracker', action='settings_issuetracker',
437 434 conditions={'method': ['GET']})
438 435 m.connect('admin_settings_issuetracker_save',
439 436 '/settings/issue-tracker/save',
440 437 action='settings_issuetracker_save',
441 438 conditions={'method': ['POST']})
442 439 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
443 440 action='settings_issuetracker_test',
444 441 conditions={'method': ['POST']})
445 442 m.connect('admin_issuetracker_delete',
446 443 '/settings/issue-tracker/delete',
447 444 action='settings_issuetracker_delete',
448 445 conditions={'method': ['DELETE']})
449 446
450 447 m.connect('admin_settings_email', '/settings/email',
451 448 action='settings_email_update',
452 449 conditions={'method': ['POST']})
453 450 m.connect('admin_settings_email', '/settings/email',
454 451 action='settings_email', conditions={'method': ['GET']})
455 452
456 453 m.connect('admin_settings_hooks', '/settings/hooks',
457 454 action='settings_hooks_update',
458 455 conditions={'method': ['POST', 'DELETE']})
459 456 m.connect('admin_settings_hooks', '/settings/hooks',
460 457 action='settings_hooks', conditions={'method': ['GET']})
461 458
462 459 m.connect('admin_settings_search', '/settings/search',
463 460 action='settings_search', conditions={'method': ['GET']})
464 461
465 462 m.connect('admin_settings_supervisor', '/settings/supervisor',
466 463 action='settings_supervisor', conditions={'method': ['GET']})
467 464 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
468 465 action='settings_supervisor_log', conditions={'method': ['GET']})
469 466
470 467 m.connect('admin_settings_labs', '/settings/labs',
471 468 action='settings_labs_update',
472 469 conditions={'method': ['POST']})
473 470 m.connect('admin_settings_labs', '/settings/labs',
474 471 action='settings_labs', conditions={'method': ['GET']})
475 472
476 473 # ADMIN MY ACCOUNT
477 474 with rmap.submapper(path_prefix=ADMIN_PREFIX,
478 475 controller='admin/my_account') as m:
479 476
480 477 m.connect('my_account_edit', '/my_account/edit',
481 478 action='my_account_edit', conditions={'method': ['GET']})
482 479 m.connect('my_account', '/my_account/update',
483 480 action='my_account_update', conditions={'method': ['POST']})
484 481
485 482 # NOTE(marcink): this needs to be kept for password force flag to be
486 483 # handler, remove after migration to pyramid
487 484 m.connect('my_account_password', '/my_account/password',
488 485 action='my_account_password', conditions={'method': ['GET']})
489 486
490 487 m.connect('my_account_repos', '/my_account/repos',
491 488 action='my_account_repos', conditions={'method': ['GET']})
492 489
493 490 m.connect('my_account_watched', '/my_account/watched',
494 491 action='my_account_watched', conditions={'method': ['GET']})
495 492
496 493 m.connect('my_account_pullrequests', '/my_account/pull_requests',
497 494 action='my_account_pullrequests', conditions={'method': ['GET']})
498 495
499 496 m.connect('my_account_perms', '/my_account/perms',
500 497 action='my_account_perms', conditions={'method': ['GET']})
501 498
502 499 m.connect('my_account_emails', '/my_account/emails',
503 500 action='my_account_emails', conditions={'method': ['GET']})
504 501 m.connect('my_account_emails', '/my_account/emails',
505 502 action='my_account_emails_add', conditions={'method': ['POST']})
506 503 m.connect('my_account_emails', '/my_account/emails',
507 504 action='my_account_emails_delete', conditions={'method': ['DELETE']})
508 505
509 506 m.connect('my_account_notifications', '/my_account/notifications',
510 507 action='my_notifications',
511 508 conditions={'method': ['GET']})
512 509 m.connect('my_account_notifications_toggle_visibility',
513 510 '/my_account/toggle_visibility',
514 511 action='my_notifications_toggle_visibility',
515 512 conditions={'method': ['POST']})
516 513
517 514 # NOTIFICATION REST ROUTES
518 515 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 516 controller='admin/notifications') as m:
520 517 m.connect('notifications', '/notifications',
521 518 action='index', conditions={'method': ['GET']})
522 519 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
523 520 action='mark_all_read', conditions={'method': ['POST']})
524 521 m.connect('/notifications/{notification_id}',
525 522 action='update', conditions={'method': ['PUT']})
526 523 m.connect('/notifications/{notification_id}',
527 524 action='delete', conditions={'method': ['DELETE']})
528 525 m.connect('notification', '/notifications/{notification_id}',
529 526 action='show', conditions={'method': ['GET']})
530 527
531 528 # ADMIN GIST
532 529 with rmap.submapper(path_prefix=ADMIN_PREFIX,
533 530 controller='admin/gists') as m:
534 531 m.connect('gists', '/gists',
535 532 action='create', conditions={'method': ['POST']})
536 533 m.connect('gists', '/gists', jsroute=True,
537 534 action='index', conditions={'method': ['GET']})
538 535 m.connect('new_gist', '/gists/new', jsroute=True,
539 536 action='new', conditions={'method': ['GET']})
540 537
541 538 m.connect('/gists/{gist_id}',
542 539 action='delete', conditions={'method': ['DELETE']})
543 540 m.connect('edit_gist', '/gists/{gist_id}/edit',
544 541 action='edit_form', conditions={'method': ['GET']})
545 542 m.connect('edit_gist', '/gists/{gist_id}/edit',
546 543 action='edit', conditions={'method': ['POST']})
547 544 m.connect(
548 545 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
549 546 action='check_revision', conditions={'method': ['GET']})
550 547
551 548 m.connect('gist', '/gists/{gist_id}',
552 549 action='show', conditions={'method': ['GET']})
553 550 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
554 551 revision='tip',
555 552 action='show', conditions={'method': ['GET']})
556 553 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
557 554 revision='tip',
558 555 action='show', conditions={'method': ['GET']})
559 556 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
560 557 revision='tip',
561 558 action='show', conditions={'method': ['GET']},
562 559 requirements=URL_NAME_REQUIREMENTS)
563 560
564 561 # USER JOURNAL
565 562 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
566 563 controller='journal', action='index')
567 564 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
568 565 controller='journal', action='journal_rss')
569 566 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
570 567 controller='journal', action='journal_atom')
571 568
572 569 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
573 570 controller='journal', action='public_journal')
574 571
575 572 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
576 573 controller='journal', action='public_journal_rss')
577 574
578 575 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
579 576 controller='journal', action='public_journal_rss')
580 577
581 578 rmap.connect('public_journal_atom',
582 579 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
583 580 action='public_journal_atom')
584 581
585 582 rmap.connect('public_journal_atom_old',
586 583 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
587 584 action='public_journal_atom')
588 585
589 586 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
590 587 controller='journal', action='toggle_following', jsroute=True,
591 588 conditions={'method': ['POST']})
592 589
593 590 # FEEDS
594 591 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
595 592 controller='feed', action='rss',
596 593 conditions={'function': check_repo},
597 594 requirements=URL_NAME_REQUIREMENTS)
598 595
599 596 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
600 597 controller='feed', action='atom',
601 598 conditions={'function': check_repo},
602 599 requirements=URL_NAME_REQUIREMENTS)
603 600
604 601 #==========================================================================
605 602 # REPOSITORY ROUTES
606 603 #==========================================================================
607 604
608 605 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
609 606 controller='admin/repos', action='repo_creating',
610 607 requirements=URL_NAME_REQUIREMENTS)
611 608 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
612 609 controller='admin/repos', action='repo_check',
613 610 requirements=URL_NAME_REQUIREMENTS)
614 611
615 612 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
616 613 controller='summary', action='repo_stats',
617 614 conditions={'function': check_repo},
618 615 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
619 616
620 617 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
621 618 controller='summary', action='repo_refs_data',
622 619 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
623 620 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
624 621 controller='summary', action='repo_refs_changelog_data',
625 622 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
626 623
627 624 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
628 625 controller='changeset', revision='tip',
629 626 conditions={'function': check_repo},
630 627 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
631 628 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
632 629 controller='changeset', revision='tip', action='changeset_children',
633 630 conditions={'function': check_repo},
634 631 requirements=URL_NAME_REQUIREMENTS)
635 632 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
636 633 controller='changeset', revision='tip', action='changeset_parents',
637 634 conditions={'function': check_repo},
638 635 requirements=URL_NAME_REQUIREMENTS)
639 636
640 637 # repo edit options
641 638 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
642 639 controller='admin/repos', action='edit_fields',
643 640 conditions={'method': ['GET'], 'function': check_repo},
644 641 requirements=URL_NAME_REQUIREMENTS)
645 642 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
646 643 controller='admin/repos', action='create_repo_field',
647 644 conditions={'method': ['PUT'], 'function': check_repo},
648 645 requirements=URL_NAME_REQUIREMENTS)
649 646 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
650 647 controller='admin/repos', action='delete_repo_field',
651 648 conditions={'method': ['DELETE'], 'function': check_repo},
652 649 requirements=URL_NAME_REQUIREMENTS)
653 650
654 651 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
655 652 controller='admin/repos', action='toggle_locking',
656 653 conditions={'method': ['GET'], 'function': check_repo},
657 654 requirements=URL_NAME_REQUIREMENTS)
658 655
659 656 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
660 657 controller='admin/repos', action='edit_remote_form',
661 658 conditions={'method': ['GET'], 'function': check_repo},
662 659 requirements=URL_NAME_REQUIREMENTS)
663 660 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
664 661 controller='admin/repos', action='edit_remote',
665 662 conditions={'method': ['PUT'], 'function': check_repo},
666 663 requirements=URL_NAME_REQUIREMENTS)
667 664
668 665 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
669 666 controller='admin/repos', action='edit_statistics_form',
670 667 conditions={'method': ['GET'], 'function': check_repo},
671 668 requirements=URL_NAME_REQUIREMENTS)
672 669 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
673 670 controller='admin/repos', action='edit_statistics',
674 671 conditions={'method': ['PUT'], 'function': check_repo},
675 672 requirements=URL_NAME_REQUIREMENTS)
676 673 rmap.connect('repo_settings_issuetracker',
677 674 '/{repo_name}/settings/issue-tracker',
678 675 controller='admin/repos', action='repo_issuetracker',
679 676 conditions={'method': ['GET'], 'function': check_repo},
680 677 requirements=URL_NAME_REQUIREMENTS)
681 678 rmap.connect('repo_issuetracker_test',
682 679 '/{repo_name}/settings/issue-tracker/test',
683 680 controller='admin/repos', action='repo_issuetracker_test',
684 681 conditions={'method': ['POST'], 'function': check_repo},
685 682 requirements=URL_NAME_REQUIREMENTS)
686 683 rmap.connect('repo_issuetracker_delete',
687 684 '/{repo_name}/settings/issue-tracker/delete',
688 685 controller='admin/repos', action='repo_issuetracker_delete',
689 686 conditions={'method': ['DELETE'], 'function': check_repo},
690 687 requirements=URL_NAME_REQUIREMENTS)
691 688 rmap.connect('repo_issuetracker_save',
692 689 '/{repo_name}/settings/issue-tracker/save',
693 690 controller='admin/repos', action='repo_issuetracker_save',
694 691 conditions={'method': ['POST'], 'function': check_repo},
695 692 requirements=URL_NAME_REQUIREMENTS)
696 693 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
697 694 controller='admin/repos', action='repo_settings_vcs_update',
698 695 conditions={'method': ['POST'], 'function': check_repo},
699 696 requirements=URL_NAME_REQUIREMENTS)
700 697 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
701 698 controller='admin/repos', action='repo_settings_vcs',
702 699 conditions={'method': ['GET'], 'function': check_repo},
703 700 requirements=URL_NAME_REQUIREMENTS)
704 701 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
705 702 controller='admin/repos', action='repo_delete_svn_pattern',
706 703 conditions={'method': ['DELETE'], 'function': check_repo},
707 704 requirements=URL_NAME_REQUIREMENTS)
708 705 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
709 706 controller='admin/repos', action='repo_settings_pullrequest',
710 707 conditions={'method': ['GET', 'POST'], 'function': check_repo},
711 708 requirements=URL_NAME_REQUIREMENTS)
712 709
713 710 # still working url for backward compat.
714 711 rmap.connect('raw_changeset_home_depraced',
715 712 '/{repo_name}/raw-changeset/{revision}',
716 713 controller='changeset', action='changeset_raw',
717 714 revision='tip', conditions={'function': check_repo},
718 715 requirements=URL_NAME_REQUIREMENTS)
719 716
720 717 # new URLs
721 718 rmap.connect('changeset_raw_home',
722 719 '/{repo_name}/changeset-diff/{revision}',
723 720 controller='changeset', action='changeset_raw',
724 721 revision='tip', conditions={'function': check_repo},
725 722 requirements=URL_NAME_REQUIREMENTS)
726 723
727 724 rmap.connect('changeset_patch_home',
728 725 '/{repo_name}/changeset-patch/{revision}',
729 726 controller='changeset', action='changeset_patch',
730 727 revision='tip', conditions={'function': check_repo},
731 728 requirements=URL_NAME_REQUIREMENTS)
732 729
733 730 rmap.connect('changeset_download_home',
734 731 '/{repo_name}/changeset-download/{revision}',
735 732 controller='changeset', action='changeset_download',
736 733 revision='tip', conditions={'function': check_repo},
737 734 requirements=URL_NAME_REQUIREMENTS)
738 735
739 736 rmap.connect('changeset_comment',
740 737 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
741 738 controller='changeset', revision='tip', action='comment',
742 739 conditions={'function': check_repo},
743 740 requirements=URL_NAME_REQUIREMENTS)
744 741
745 742 rmap.connect('changeset_comment_preview',
746 743 '/{repo_name}/changeset/comment/preview', jsroute=True,
747 744 controller='changeset', action='preview_comment',
748 745 conditions={'function': check_repo, 'method': ['POST']},
749 746 requirements=URL_NAME_REQUIREMENTS)
750 747
751 748 rmap.connect('changeset_comment_delete',
752 749 '/{repo_name}/changeset/comment/{comment_id}/delete',
753 750 controller='changeset', action='delete_comment',
754 751 conditions={'function': check_repo, 'method': ['DELETE']},
755 752 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
756 753
757 754 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
758 755 controller='changeset', action='changeset_info',
759 756 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
760 757
761 758 rmap.connect('compare_home',
762 759 '/{repo_name}/compare',
763 760 controller='compare', action='index',
764 761 conditions={'function': check_repo},
765 762 requirements=URL_NAME_REQUIREMENTS)
766 763
767 764 rmap.connect('compare_url',
768 765 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
769 766 controller='compare', action='compare',
770 767 conditions={'function': check_repo},
771 768 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
772 769
773 770 rmap.connect('pullrequest_home',
774 771 '/{repo_name}/pull-request/new', controller='pullrequests',
775 772 action='index', conditions={'function': check_repo,
776 773 'method': ['GET']},
777 774 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
778 775
779 776 rmap.connect('pullrequest',
780 777 '/{repo_name}/pull-request/new', controller='pullrequests',
781 778 action='create', conditions={'function': check_repo,
782 779 'method': ['POST']},
783 780 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
784 781
785 782 rmap.connect('pullrequest_repo_refs',
786 783 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
787 784 controller='pullrequests',
788 785 action='get_repo_refs',
789 786 conditions={'function': check_repo, 'method': ['GET']},
790 787 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
791 788
792 789 rmap.connect('pullrequest_repo_destinations',
793 790 '/{repo_name}/pull-request/repo-destinations',
794 791 controller='pullrequests',
795 792 action='get_repo_destinations',
796 793 conditions={'function': check_repo, 'method': ['GET']},
797 794 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
798 795
799 796 rmap.connect('pullrequest_show',
800 797 '/{repo_name}/pull-request/{pull_request_id}',
801 798 controller='pullrequests',
802 799 action='show', conditions={'function': check_repo,
803 800 'method': ['GET']},
804 801 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
805 802
806 803 rmap.connect('pullrequest_update',
807 804 '/{repo_name}/pull-request/{pull_request_id}',
808 805 controller='pullrequests',
809 806 action='update', conditions={'function': check_repo,
810 807 'method': ['PUT']},
811 808 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
812 809
813 810 rmap.connect('pullrequest_merge',
814 811 '/{repo_name}/pull-request/{pull_request_id}',
815 812 controller='pullrequests',
816 813 action='merge', conditions={'function': check_repo,
817 814 'method': ['POST']},
818 815 requirements=URL_NAME_REQUIREMENTS)
819 816
820 817 rmap.connect('pullrequest_delete',
821 818 '/{repo_name}/pull-request/{pull_request_id}',
822 819 controller='pullrequests',
823 820 action='delete', conditions={'function': check_repo,
824 821 'method': ['DELETE']},
825 822 requirements=URL_NAME_REQUIREMENTS)
826 823
827 824 rmap.connect('pullrequest_comment',
828 825 '/{repo_name}/pull-request-comment/{pull_request_id}',
829 826 controller='pullrequests',
830 827 action='comment', conditions={'function': check_repo,
831 828 'method': ['POST']},
832 829 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
833 830
834 831 rmap.connect('pullrequest_comment_delete',
835 832 '/{repo_name}/pull-request-comment/{comment_id}/delete',
836 833 controller='pullrequests', action='delete_comment',
837 834 conditions={'function': check_repo, 'method': ['DELETE']},
838 835 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
839 836
840 837 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
841 838 controller='summary', conditions={'function': check_repo},
842 839 requirements=URL_NAME_REQUIREMENTS)
843 840
844 841 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
845 842 controller='changelog', conditions={'function': check_repo},
846 843 requirements=URL_NAME_REQUIREMENTS)
847 844
848 845 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
849 846 controller='changelog', action='changelog_summary',
850 847 conditions={'function': check_repo},
851 848 requirements=URL_NAME_REQUIREMENTS)
852 849
853 850 rmap.connect('changelog_file_home',
854 851 '/{repo_name}/changelog/{revision}/{f_path}',
855 852 controller='changelog', f_path=None,
856 853 conditions={'function': check_repo},
857 854 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
858 855
859 856 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
860 857 controller='changelog', action='changelog_elements',
861 858 conditions={'function': check_repo},
862 859 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
863 860
864 861 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
865 862 controller='files', revision='tip', f_path='',
866 863 conditions={'function': check_repo},
867 864 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
868 865
869 866 rmap.connect('files_home_simple_catchrev',
870 867 '/{repo_name}/files/{revision}',
871 868 controller='files', revision='tip', f_path='',
872 869 conditions={'function': check_repo},
873 870 requirements=URL_NAME_REQUIREMENTS)
874 871
875 872 rmap.connect('files_home_simple_catchall',
876 873 '/{repo_name}/files',
877 874 controller='files', revision='tip', f_path='',
878 875 conditions={'function': check_repo},
879 876 requirements=URL_NAME_REQUIREMENTS)
880 877
881 878 rmap.connect('files_history_home',
882 879 '/{repo_name}/history/{revision}/{f_path}',
883 880 controller='files', action='history', revision='tip', f_path='',
884 881 conditions={'function': check_repo},
885 882 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
886 883
887 884 rmap.connect('files_authors_home',
888 885 '/{repo_name}/authors/{revision}/{f_path}',
889 886 controller='files', action='authors', revision='tip', f_path='',
890 887 conditions={'function': check_repo},
891 888 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
892 889
893 890 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
894 891 controller='files', action='diff', f_path='',
895 892 conditions={'function': check_repo},
896 893 requirements=URL_NAME_REQUIREMENTS)
897 894
898 895 rmap.connect('files_diff_2way_home',
899 896 '/{repo_name}/diff-2way/{f_path}',
900 897 controller='files', action='diff_2way', f_path='',
901 898 conditions={'function': check_repo},
902 899 requirements=URL_NAME_REQUIREMENTS)
903 900
904 901 rmap.connect('files_rawfile_home',
905 902 '/{repo_name}/rawfile/{revision}/{f_path}',
906 903 controller='files', action='rawfile', revision='tip',
907 904 f_path='', conditions={'function': check_repo},
908 905 requirements=URL_NAME_REQUIREMENTS)
909 906
910 907 rmap.connect('files_raw_home',
911 908 '/{repo_name}/raw/{revision}/{f_path}',
912 909 controller='files', action='raw', revision='tip', f_path='',
913 910 conditions={'function': check_repo},
914 911 requirements=URL_NAME_REQUIREMENTS)
915 912
916 913 rmap.connect('files_render_home',
917 914 '/{repo_name}/render/{revision}/{f_path}',
918 915 controller='files', action='index', revision='tip', f_path='',
919 916 rendered=True, conditions={'function': check_repo},
920 917 requirements=URL_NAME_REQUIREMENTS)
921 918
922 919 rmap.connect('files_annotate_home',
923 920 '/{repo_name}/annotate/{revision}/{f_path}',
924 921 controller='files', action='index', revision='tip',
925 922 f_path='', annotate=True, conditions={'function': check_repo},
926 923 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927 924
928 925 rmap.connect('files_annotate_previous',
929 926 '/{repo_name}/annotate-previous/{revision}/{f_path}',
930 927 controller='files', action='annotate_previous', revision='tip',
931 928 f_path='', annotate=True, conditions={'function': check_repo},
932 929 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
933 930
934 931 rmap.connect('files_edit',
935 932 '/{repo_name}/edit/{revision}/{f_path}',
936 933 controller='files', action='edit', revision='tip',
937 934 f_path='',
938 935 conditions={'function': check_repo, 'method': ['POST']},
939 936 requirements=URL_NAME_REQUIREMENTS)
940 937
941 938 rmap.connect('files_edit_home',
942 939 '/{repo_name}/edit/{revision}/{f_path}',
943 940 controller='files', action='edit_home', revision='tip',
944 941 f_path='', conditions={'function': check_repo},
945 942 requirements=URL_NAME_REQUIREMENTS)
946 943
947 944 rmap.connect('files_add',
948 945 '/{repo_name}/add/{revision}/{f_path}',
949 946 controller='files', action='add', revision='tip',
950 947 f_path='',
951 948 conditions={'function': check_repo, 'method': ['POST']},
952 949 requirements=URL_NAME_REQUIREMENTS)
953 950
954 951 rmap.connect('files_add_home',
955 952 '/{repo_name}/add/{revision}/{f_path}',
956 953 controller='files', action='add_home', revision='tip',
957 954 f_path='', conditions={'function': check_repo},
958 955 requirements=URL_NAME_REQUIREMENTS)
959 956
960 957 rmap.connect('files_delete',
961 958 '/{repo_name}/delete/{revision}/{f_path}',
962 959 controller='files', action='delete', revision='tip',
963 960 f_path='',
964 961 conditions={'function': check_repo, 'method': ['POST']},
965 962 requirements=URL_NAME_REQUIREMENTS)
966 963
967 964 rmap.connect('files_delete_home',
968 965 '/{repo_name}/delete/{revision}/{f_path}',
969 966 controller='files', action='delete_home', revision='tip',
970 967 f_path='', conditions={'function': check_repo},
971 968 requirements=URL_NAME_REQUIREMENTS)
972 969
973 970 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
974 971 controller='files', action='archivefile',
975 972 conditions={'function': check_repo},
976 973 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
977 974
978 975 rmap.connect('files_nodelist_home',
979 976 '/{repo_name}/nodelist/{revision}/{f_path}',
980 977 controller='files', action='nodelist',
981 978 conditions={'function': check_repo},
982 979 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
983 980
984 981 rmap.connect('files_nodetree_full',
985 982 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
986 983 controller='files', action='nodetree_full',
987 984 conditions={'function': check_repo},
988 985 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
989 986
990 987 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
991 988 controller='forks', action='fork_create',
992 989 conditions={'function': check_repo, 'method': ['POST']},
993 990 requirements=URL_NAME_REQUIREMENTS)
994 991
995 992 rmap.connect('repo_fork_home', '/{repo_name}/fork',
996 993 controller='forks', action='fork',
997 994 conditions={'function': check_repo},
998 995 requirements=URL_NAME_REQUIREMENTS)
999 996
1000 997 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1001 998 controller='forks', action='forks',
1002 999 conditions={'function': check_repo},
1003 1000 requirements=URL_NAME_REQUIREMENTS)
1004 1001
1005 # must be here for proper group/repo catching pattern
1006 _connect_with_slash(
1007 rmap, 'repo_group_home', '/{group_name}',
1008 controller='home', action='index_repo_group',
1009 conditions={'function': check_group},
1010 requirements=URL_NAME_REQUIREMENTS)
1011
1012 1002 # catch all, at the end
1013 1003 _connect_with_slash(
1014 1004 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1015 1005 controller='summary', action='index',
1016 1006 conditions={'function': check_repo},
1017 1007 requirements=URL_NAME_REQUIREMENTS)
1018 1008
1019 1009 return rmap
1020 1010
1021 1011
1022 1012 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1023 1013 """
1024 1014 Connect a route with an optional trailing slash in `path`.
1025 1015 """
1026 1016 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1027 1017 mapper.connect(name, path, *args, **kwargs)
@@ -1,406 +1,404 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 """
23 23 Repository groups controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import formencode
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _, ungettext
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, NotAnonymous, HasPermissionAll,
40 40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.db import RepoGroup, User
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.repo_group import RepoGroupModel
45 45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.lib.utils2 import safe_int
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class RepoGroupsController(BaseController):
54 54 """REST Controller styled on the Atom Publishing Protocol"""
55 55
56 56 @LoginRequired()
57 57 def __before__(self):
58 58 super(RepoGroupsController, self).__before__()
59 59
60 60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
61 61 if self._can_create_repo_group():
62 62 # we're global admin, we're ok and we can create TOP level groups
63 63 allow_empty_group = True
64 64
65 65 # override the choices for this form, we need to filter choices
66 66 # and display only those we have ADMIN right
67 67 groups_with_admin_rights = RepoGroupList(
68 68 RepoGroup.query().all(),
69 69 perm_set=['group.admin'])
70 70 c.repo_groups = RepoGroup.groups_choices(
71 71 groups=groups_with_admin_rights,
72 72 show_empty_group=allow_empty_group)
73 73
74 74 if repo_group:
75 75 # exclude filtered ids
76 76 exclude_group_ids = [repo_group.group_id]
77 77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
78 78 c.repo_groups)
79 79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
80 80 parent_group = repo_group.parent_group
81 81
82 82 add_parent_group = (parent_group and (
83 83 unicode(parent_group.group_id) not in c.repo_groups_choices))
84 84 if add_parent_group:
85 85 c.repo_groups_choices.append(unicode(parent_group.group_id))
86 86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
87 87
88 88 def __load_data(self, group_id):
89 89 """
90 90 Load defaults settings for edit, and update
91 91
92 92 :param group_id:
93 93 """
94 94 repo_group = RepoGroup.get_or_404(group_id)
95 95 data = repo_group.get_dict()
96 96 data['group_name'] = repo_group.name
97 97
98 98 # fill owner
99 99 if repo_group.user:
100 100 data.update({'user': repo_group.user.username})
101 101 else:
102 102 replacement_user = User.get_first_super_admin().username
103 103 data.update({'user': replacement_user})
104 104
105 105 # fill repository group users
106 106 for p in repo_group.repo_group_to_perm:
107 107 data.update({
108 108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
109 109
110 110 # fill repository group user groups
111 111 for p in repo_group.users_group_to_perm:
112 112 data.update({
113 113 'g_perm_%s' % p.users_group.users_group_id:
114 114 p.permission.permission_name})
115 115 # html and form expects -1 as empty parent group
116 116 data['group_parent_id'] = data['group_parent_id'] or -1
117 117 return data
118 118
119 119 def _revoke_perms_on_yourself(self, form_result):
120 120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
121 121 form_result['perm_updates'])
122 122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
123 123 form_result['perm_additions'])
124 124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
125 125 form_result['perm_deletions'])
126 126 admin_perm = 'group.admin'
127 127 if _updates and _updates[0][1] != admin_perm or \
128 128 _additions and _additions[0][1] != admin_perm or \
129 129 _deletions and _deletions[0][1] != admin_perm:
130 130 return True
131 131 return False
132 132
133 133 def _can_create_repo_group(self, parent_group_id=None):
134 134 is_admin = HasPermissionAll('hg.admin')('group create controller')
135 135 create_repo_group = HasPermissionAll(
136 136 'hg.repogroup.create.true')('group create controller')
137 137 if is_admin or (create_repo_group and not parent_group_id):
138 138 # we're global admin, or we have global repo group create
139 139 # permission
140 140 # we're ok and we can create TOP level groups
141 141 return True
142 142 elif parent_group_id:
143 143 # we check the permission if we can write to parent group
144 144 group = RepoGroup.get(parent_group_id)
145 145 group_name = group.group_name if group else None
146 146 if HasRepoGroupPermissionAll('group.admin')(
147 147 group_name, 'check if user is an admin of group'):
148 148 # we're an admin of passed in group, we're ok.
149 149 return True
150 150 else:
151 151 return False
152 152 return False
153 153
154 154 @NotAnonymous()
155 155 def index(self):
156 156 """GET /repo_groups: All items in the collection"""
157 157 # url('repo_groups')
158 158
159 159 repo_group_list = RepoGroup.get_all_repo_groups()
160 160 _perms = ['group.admin']
161 161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
162 162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
163 163 repo_group_list=repo_group_list_acl, admin=True)
164 164 c.data = json.dumps(repo_group_data)
165 165 return render('admin/repo_groups/repo_groups.mako')
166 166
167 167 # perm checks inside
168 168 @NotAnonymous()
169 169 @auth.CSRFRequired()
170 170 def create(self):
171 171 """POST /repo_groups: Create a new item"""
172 172 # url('repo_groups')
173 173
174 174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
175 175 can_create = self._can_create_repo_group(parent_group_id)
176 176
177 177 self.__load_defaults()
178 178 # permissions for can create group based on parent_id are checked
179 179 # here in the Form
180 180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
181 181 repo_group_form = RepoGroupForm(available_groups=available_groups,
182 182 can_create_in_root=can_create)()
183 183 try:
184 184 owner = c.rhodecode_user
185 185 form_result = repo_group_form.to_python(dict(request.POST))
186 186 RepoGroupModel().create(
187 187 group_name=form_result['group_name_full'],
188 188 group_description=form_result['group_description'],
189 189 owner=owner.user_id,
190 190 copy_permissions=form_result['group_copy_permissions']
191 191 )
192 192 Session().commit()
193 193 _new_group_name = form_result['group_name_full']
194 194 repo_group_url = h.link_to(
195 195 _new_group_name,
196 h.url('repo_group_home', group_name=_new_group_name))
196 h.route_path('repo_group_home', repo_group_name=_new_group_name))
197 197 h.flash(h.literal(_('Created repository group %s')
198 198 % repo_group_url), category='success')
199 199 # TODO: in futureaction_logger(, '', '', '', self.sa)
200 200 except formencode.Invalid as errors:
201 201 return htmlfill.render(
202 202 render('admin/repo_groups/repo_group_add.mako'),
203 203 defaults=errors.value,
204 204 errors=errors.error_dict or {},
205 205 prefix_error=False,
206 206 encoding="UTF-8",
207 207 force_defaults=False)
208 208 except Exception:
209 209 log.exception("Exception during creation of repository group")
210 210 h.flash(_('Error occurred during creation of repository group %s')
211 211 % request.POST.get('group_name'), category='error')
212 212
213 213 # TODO: maybe we should get back to the main view, not the admin one
214 214 return redirect(url('repo_groups', parent_group=parent_group_id))
215 215
216 216 # perm checks inside
217 217 @NotAnonymous()
218 218 def new(self):
219 219 """GET /repo_groups/new: Form to create a new item"""
220 220 # url('new_repo_group')
221 221 # perm check for admin, create_group perm or admin of parent_group
222 222 parent_group_id = safe_int(request.GET.get('parent_group'))
223 223 if not self._can_create_repo_group(parent_group_id):
224 224 return abort(403)
225 225
226 226 self.__load_defaults()
227 227 return render('admin/repo_groups/repo_group_add.mako')
228 228
229 229 @HasRepoGroupPermissionAnyDecorator('group.admin')
230 230 @auth.CSRFRequired()
231 231 def update(self, group_name):
232 232 """PUT /repo_groups/group_name: Update an existing item"""
233 233 # Forms posted to this method should contain a hidden field:
234 234 # <input type="hidden" name="_method" value="PUT" />
235 235 # Or using helpers:
236 236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
237 # url('repo_group_home', group_name=GROUP_NAME)
238 237
239 238 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
240 239 can_create_in_root = self._can_create_repo_group()
241 240 show_root_location = can_create_in_root
242 241 if not c.repo_group.parent_group:
243 242 # this group don't have a parrent so we should show empty value
244 243 show_root_location = True
245 244 self.__load_defaults(allow_empty_group=show_root_location,
246 245 repo_group=c.repo_group)
247 246
248 247 repo_group_form = RepoGroupForm(
249 248 edit=True, old_data=c.repo_group.get_dict(),
250 249 available_groups=c.repo_groups_choices,
251 250 can_create_in_root=can_create_in_root, allow_disabled=True)()
252 251
253 252 try:
254 253 form_result = repo_group_form.to_python(dict(request.POST))
255 254 gr_name = form_result['group_name']
256 255 new_gr = RepoGroupModel().update(group_name, form_result)
257 256 Session().commit()
258 257 h.flash(_('Updated repository group %s') % (gr_name,),
259 258 category='success')
260 259 # we now have new name !
261 260 group_name = new_gr.group_name
262 261 # TODO: in future action_logger(, '', '', '', self.sa)
263 262 except formencode.Invalid as errors:
264 263 c.active = 'settings'
265 264 return htmlfill.render(
266 265 render('admin/repo_groups/repo_group_edit.mako'),
267 266 defaults=errors.value,
268 267 errors=errors.error_dict or {},
269 268 prefix_error=False,
270 269 encoding="UTF-8",
271 270 force_defaults=False)
272 271 except Exception:
273 272 log.exception("Exception during update or repository group")
274 273 h.flash(_('Error occurred during update of repository group %s')
275 274 % request.POST.get('group_name'), category='error')
276 275
277 276 return redirect(url('edit_repo_group', group_name=group_name))
278 277
279 278 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 279 @auth.CSRFRequired()
281 280 def delete(self, group_name):
282 281 """DELETE /repo_groups/group_name: Delete an existing item"""
283 282 # Forms posted to this method should contain a hidden field:
284 283 # <input type="hidden" name="_method" value="DELETE" />
285 284 # Or using helpers:
286 285 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
287 # url('repo_group_home', group_name=GROUP_NAME)
288 286
289 287 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
290 288 repos = gr.repositories.all()
291 289 if repos:
292 290 msg = ungettext(
293 291 'This group contains %(num)d repository and cannot be deleted',
294 292 'This group contains %(num)d repositories and cannot be'
295 293 ' deleted',
296 294 len(repos)) % {'num': len(repos)}
297 295 h.flash(msg, category='warning')
298 296 return redirect(url('repo_groups'))
299 297
300 298 children = gr.children.all()
301 299 if children:
302 300 msg = ungettext(
303 301 'This group contains %(num)d subgroup and cannot be deleted',
304 302 'This group contains %(num)d subgroups and cannot be deleted',
305 303 len(children)) % {'num': len(children)}
306 304 h.flash(msg, category='warning')
307 305 return redirect(url('repo_groups'))
308 306
309 307 try:
310 308 RepoGroupModel().delete(group_name)
311 309 Session().commit()
312 310 h.flash(_('Removed repository group %s') % group_name,
313 311 category='success')
314 312 # TODO: in future action_logger(, '', '', '', self.sa)
315 313 except Exception:
316 314 log.exception("Exception during deletion of repository group")
317 315 h.flash(_('Error occurred during deletion of repository group %s')
318 316 % group_name, category='error')
319 317
320 318 return redirect(url('repo_groups'))
321 319
322 320 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 321 def edit(self, group_name):
324 322 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
325 323 # url('edit_repo_group', group_name=GROUP_NAME)
326 324 c.active = 'settings'
327 325
328 326 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
329 327 # we can only allow moving empty group if it's already a top-level
330 328 # group, ie has no parents, or we're admin
331 329 can_create_in_root = self._can_create_repo_group()
332 330 show_root_location = can_create_in_root
333 331 if not c.repo_group.parent_group:
334 332 # this group don't have a parrent so we should show empty value
335 333 show_root_location = True
336 334 self.__load_defaults(allow_empty_group=show_root_location,
337 335 repo_group=c.repo_group)
338 336 defaults = self.__load_data(c.repo_group.group_id)
339 337
340 338 return htmlfill.render(
341 339 render('admin/repo_groups/repo_group_edit.mako'),
342 340 defaults=defaults,
343 341 encoding="UTF-8",
344 342 force_defaults=False
345 343 )
346 344
347 345 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 346 def edit_repo_group_advanced(self, group_name):
349 347 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 348 # url('edit_repo_group', group_name=GROUP_NAME)
351 349 c.active = 'advanced'
352 350 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
353 351
354 352 return render('admin/repo_groups/repo_group_edit.mako')
355 353
356 354 @HasRepoGroupPermissionAnyDecorator('group.admin')
357 355 def edit_repo_group_perms(self, group_name):
358 356 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
359 357 # url('edit_repo_group', group_name=GROUP_NAME)
360 358 c.active = 'perms'
361 359 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
362 360 self.__load_defaults()
363 361 defaults = self.__load_data(c.repo_group.group_id)
364 362
365 363 return htmlfill.render(
366 364 render('admin/repo_groups/repo_group_edit.mako'),
367 365 defaults=defaults,
368 366 encoding="UTF-8",
369 367 force_defaults=False
370 368 )
371 369
372 370 @HasRepoGroupPermissionAnyDecorator('group.admin')
373 371 @auth.CSRFRequired()
374 372 def update_perms(self, group_name):
375 373 """
376 374 Update permissions for given repository group
377 375
378 376 :param group_name:
379 377 """
380 378
381 379 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 380 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
383 381 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
384 382 request.POST)
385 383
386 384 if not c.rhodecode_user.is_admin:
387 385 if self._revoke_perms_on_yourself(form):
388 386 msg = _('Cannot change permission for yourself as admin')
389 387 h.flash(msg, category='warning')
390 388 return redirect(
391 389 url('edit_repo_group_perms', group_name=group_name))
392 390
393 391 # iterate over all members(if in recursive mode) of this groups and
394 392 # set the permissions !
395 393 # this can be potentially heavy operation
396 394 RepoGroupModel().update_permissions(
397 395 c.repo_group,
398 396 form['perm_additions'], form['perm_updates'],
399 397 form['perm_deletions'], form['recursive'])
400 398
401 399 # TODO: implement this
402 400 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
403 401 # repo_name, self.ip_addr, self.sa)
404 402 Session().commit()
405 403 h.flash(_('Repository Group permissions updated'), category='success')
406 404 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -1,610 +1,610 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-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 """
23 23 Repositories controller for RhodeCode
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 import formencode
30 30 from formencode import htmlfill
31 31 from pylons import request, tmpl_context as c, url
32 32 from pylons.controllers.util import redirect
33 33 from pylons.i18n.translation import _
34 34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
35 35
36 36 import rhodecode
37 37 from rhodecode.lib import auth, helpers as h
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator,
40 40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
41 41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.exceptions import AttachedForksError
45 45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
46 46 from rhodecode.lib.utils2 import safe_int, str2bool
47 47 from rhodecode.lib.vcs import RepositoryError
48 48 from rhodecode.model.db import (
49 49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
50 50 from rhodecode.model.forms import (
51 51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
52 52 IssueTrackerPatternsForm)
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.repo import RepoModel
55 55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
56 56 from rhodecode.model.settings import (
57 57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
58 58 SettingNotFound)
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class ReposController(BaseRepoController):
64 64 """
65 65 REST Controller styled on the Atom Publishing Protocol"""
66 66 # To properly map this controller, ensure your config/routing.py
67 67 # file has a resource setup:
68 68 # map.resource('repo', 'repos')
69 69
70 70 @LoginRequired()
71 71 def __before__(self):
72 72 super(ReposController, self).__before__()
73 73
74 74 def _load_repo(self, repo_name):
75 75 repo_obj = Repository.get_by_repo_name(repo_name)
76 76
77 77 if repo_obj is None:
78 78 h.not_mapped_error(repo_name)
79 79 return redirect(url('repos'))
80 80
81 81 return repo_obj
82 82
83 83 def __load_defaults(self, repo=None):
84 84 acl_groups = RepoGroupList(RepoGroup.query().all(),
85 85 perm_set=['group.write', 'group.admin'])
86 86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
87 87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
88 88
89 89 # in case someone no longer have a group.write access to a repository
90 90 # pre fill the list with this entry, we don't care if this is the same
91 91 # but it will allow saving repo data properly.
92 92
93 93 repo_group = None
94 94 if repo:
95 95 repo_group = repo.group
96 96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
97 97 c.repo_groups_choices.append(unicode(repo_group.group_id))
98 98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
99 99
100 100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
101 101 c.landing_revs_choices = choices
102 102
103 103 def __load_data(self, repo_name=None):
104 104 """
105 105 Load defaults settings for edit, and update
106 106
107 107 :param repo_name:
108 108 """
109 109 c.repo_info = self._load_repo(repo_name)
110 110 self.__load_defaults(c.repo_info)
111 111
112 112 # override defaults for exact repo info here git/hg etc
113 113 if not c.repository_requirements_missing:
114 114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
115 115 c.repo_info)
116 116 c.landing_revs_choices = choices
117 117 defaults = RepoModel()._get_defaults(repo_name)
118 118
119 119 return defaults
120 120
121 121 def _log_creation_exception(self, e, repo_name):
122 122 reason = None
123 123 if len(e.args) == 2:
124 124 reason = e.args[1]
125 125
126 126 if reason == 'INVALID_CERTIFICATE':
127 127 log.exception(
128 128 'Exception creating a repository: invalid certificate')
129 129 msg = (_('Error creating repository %s: invalid certificate')
130 130 % repo_name)
131 131 else:
132 132 log.exception("Exception creating a repository")
133 133 msg = (_('Error creating repository %s')
134 134 % repo_name)
135 135
136 136 return msg
137 137
138 138 @NotAnonymous()
139 139 def index(self, format='html'):
140 140 """GET /repos: All items in the collection"""
141 141 # url('repos')
142 142
143 143 repo_list = Repository.get_all_repos()
144 144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
145 145 repos_data = RepoModel().get_repos_as_dict(
146 146 repo_list=c.repo_list, admin=True, super_user_actions=True)
147 147 # json used to render the grid
148 148 c.data = json.dumps(repos_data)
149 149
150 150 return render('admin/repos/repos.mako')
151 151
152 152 # perms check inside
153 153 @NotAnonymous()
154 154 @auth.CSRFRequired()
155 155 def create(self):
156 156 """
157 157 POST /repos: Create a new item"""
158 158 # url('repos')
159 159
160 160 self.__load_defaults()
161 161 form_result = {}
162 162 task_id = None
163 163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
164 164 try:
165 165 # CanWriteToGroup validators checks permissions of this POST
166 166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
167 167 landing_revs=c.landing_revs_choices)()\
168 168 .to_python(dict(request.POST))
169 169
170 170 # create is done sometimes async on celery, db transaction
171 171 # management is handled there.
172 172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
173 173 from celery.result import BaseAsyncResult
174 174 if isinstance(task, BaseAsyncResult):
175 175 task_id = task.task_id
176 176 except formencode.Invalid as errors:
177 177 return htmlfill.render(
178 178 render('admin/repos/repo_add.mako'),
179 179 defaults=errors.value,
180 180 errors=errors.error_dict or {},
181 181 prefix_error=False,
182 182 encoding="UTF-8",
183 183 force_defaults=False)
184 184
185 185 except Exception as e:
186 186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
187 187 h.flash(msg, category='error')
188 return redirect(url('home'))
188 return redirect(h.route_path('home'))
189 189
190 190 return redirect(h.url('repo_creating_home',
191 191 repo_name=form_result['repo_name_full'],
192 192 task_id=task_id))
193 193
194 194 # perms check inside
195 195 @NotAnonymous()
196 196 def create_repository(self):
197 197 """GET /_admin/create_repository: Form to create a new item"""
198 198 new_repo = request.GET.get('repo', '')
199 199 parent_group = safe_int(request.GET.get('parent_group'))
200 200 _gr = RepoGroup.get(parent_group)
201 201
202 202 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
203 203 # you're not super admin nor have global create permissions,
204 204 # but maybe you have at least write permission to a parent group ?
205 205
206 206 gr_name = _gr.group_name if _gr else None
207 207 # create repositories with write permission on group is set to true
208 208 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
209 209 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
210 210 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
211 211 if not (group_admin or (group_write and create_on_write)):
212 212 raise HTTPForbidden
213 213
214 214 acl_groups = RepoGroupList(RepoGroup.query().all(),
215 215 perm_set=['group.write', 'group.admin'])
216 216 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
217 217 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
218 218 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
219 219 c.personal_repo_group = c.rhodecode_user.personal_repo_group
220 220 c.new_repo = repo_name_slug(new_repo)
221 221
222 222 # apply the defaults from defaults page
223 223 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
224 224 # set checkbox to autochecked
225 225 defaults['repo_copy_permissions'] = True
226 226
227 227 parent_group_choice = '-1'
228 228 if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group:
229 229 parent_group_choice = c.rhodecode_user.personal_repo_group
230 230
231 231 if parent_group and _gr:
232 232 if parent_group in [x[0] for x in c.repo_groups]:
233 233 parent_group_choice = unicode(parent_group)
234 234
235 235 defaults.update({'repo_group': parent_group_choice})
236 236
237 237 return htmlfill.render(
238 238 render('admin/repos/repo_add.mako'),
239 239 defaults=defaults,
240 240 errors={},
241 241 prefix_error=False,
242 242 encoding="UTF-8",
243 243 force_defaults=False
244 244 )
245 245
246 246 @NotAnonymous()
247 247 def repo_creating(self, repo_name):
248 248 c.repo = repo_name
249 249 c.task_id = request.GET.get('task_id')
250 250 if not c.repo:
251 251 raise HTTPNotFound()
252 252 return render('admin/repos/repo_creating.mako')
253 253
254 254 @NotAnonymous()
255 255 @jsonify
256 256 def repo_check(self, repo_name):
257 257 c.repo = repo_name
258 258 task_id = request.GET.get('task_id')
259 259
260 260 if task_id and task_id not in ['None']:
261 261 import rhodecode
262 262 from celery.result import AsyncResult
263 263 if rhodecode.CELERY_ENABLED:
264 264 task = AsyncResult(task_id)
265 265 if task.failed():
266 266 msg = self._log_creation_exception(task.result, c.repo)
267 267 h.flash(msg, category='error')
268 return redirect(url('home'), code=501)
268 return redirect(h.route_path('home'), code=501)
269 269
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271 if repo and repo.repo_state == Repository.STATE_CREATED:
272 272 if repo.clone_uri:
273 273 clone_uri = repo.clone_uri_hidden
274 274 h.flash(_('Created repository %s from %s')
275 275 % (repo.repo_name, clone_uri), category='success')
276 276 else:
277 277 repo_url = h.link_to(repo.repo_name,
278 278 h.url('summary_home',
279 279 repo_name=repo.repo_name))
280 280 fork = repo.fork
281 281 if fork:
282 282 fork_name = fork.repo_name
283 283 h.flash(h.literal(_('Forked repository %s as %s')
284 284 % (fork_name, repo_url)), category='success')
285 285 else:
286 286 h.flash(h.literal(_('Created repository %s') % repo_url),
287 287 category='success')
288 288 return {'result': True}
289 289 return {'result': False}
290 290
291 291 @HasPermissionAllDecorator('hg.admin')
292 292 def show(self, repo_name, format='html'):
293 293 """GET /repos/repo_name: Show a specific item"""
294 294 # url('repo', repo_name=ID)
295 295
296 296 @HasRepoPermissionAllDecorator('repository.admin')
297 297 def edit_fields(self, repo_name):
298 298 """GET /repo_name/settings: Form to edit an existing item"""
299 299 c.repo_info = self._load_repo(repo_name)
300 300 c.repo_fields = RepositoryField.query()\
301 301 .filter(RepositoryField.repository == c.repo_info).all()
302 302 c.active = 'fields'
303 303 if request.POST:
304 304
305 305 return redirect(url('repo_edit_fields'))
306 306 return render('admin/repos/repo_edit.mako')
307 307
308 308 @HasRepoPermissionAllDecorator('repository.admin')
309 309 @auth.CSRFRequired()
310 310 def create_repo_field(self, repo_name):
311 311 try:
312 312 form_result = RepoFieldForm()().to_python(dict(request.POST))
313 313 RepoModel().add_repo_field(
314 314 repo_name, form_result['new_field_key'],
315 315 field_type=form_result['new_field_type'],
316 316 field_value=form_result['new_field_value'],
317 317 field_label=form_result['new_field_label'],
318 318 field_desc=form_result['new_field_desc'])
319 319
320 320 Session().commit()
321 321 except Exception as e:
322 322 log.exception("Exception creating field")
323 323 msg = _('An error occurred during creation of field')
324 324 if isinstance(e, formencode.Invalid):
325 325 msg += ". " + e.msg
326 326 h.flash(msg, category='error')
327 327 return redirect(url('edit_repo_fields', repo_name=repo_name))
328 328
329 329 @HasRepoPermissionAllDecorator('repository.admin')
330 330 @auth.CSRFRequired()
331 331 def delete_repo_field(self, repo_name, field_id):
332 332 field = RepositoryField.get_or_404(field_id)
333 333 try:
334 334 RepoModel().delete_repo_field(repo_name, field.field_key)
335 335 Session().commit()
336 336 except Exception as e:
337 337 log.exception("Exception during removal of field")
338 338 msg = _('An error occurred during removal of field')
339 339 h.flash(msg, category='error')
340 340 return redirect(url('edit_repo_fields', repo_name=repo_name))
341 341
342 342 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
343 343 @auth.CSRFRequired()
344 344 def toggle_locking(self, repo_name):
345 345 """
346 346 Toggle locking of repository by simple GET call to url
347 347
348 348 :param repo_name:
349 349 """
350 350
351 351 try:
352 352 repo = Repository.get_by_repo_name(repo_name)
353 353
354 354 if repo.enable_locking:
355 355 if repo.locked[0]:
356 356 Repository.unlock(repo)
357 357 action = _('Unlocked')
358 358 else:
359 359 Repository.lock(repo, c.rhodecode_user.user_id,
360 360 lock_reason=Repository.LOCK_WEB)
361 361 action = _('Locked')
362 362
363 363 h.flash(_('Repository has been %s') % action,
364 364 category='success')
365 365 except Exception:
366 366 log.exception("Exception during unlocking")
367 367 h.flash(_('An error occurred during unlocking'),
368 368 category='error')
369 369 return redirect(url('summary_home', repo_name=repo_name))
370 370
371 371 @HasRepoPermissionAllDecorator('repository.admin')
372 372 @auth.CSRFRequired()
373 373 def edit_remote(self, repo_name):
374 374 """PUT /{repo_name}/settings/remote: edit the repo remote."""
375 375 try:
376 376 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
377 377 h.flash(_('Pulled from remote location'), category='success')
378 378 except Exception:
379 379 log.exception("Exception during pull from remote")
380 380 h.flash(_('An error occurred during pull from remote location'),
381 381 category='error')
382 382 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
383 383
384 384 @HasRepoPermissionAllDecorator('repository.admin')
385 385 def edit_remote_form(self, repo_name):
386 386 """GET /repo_name/settings: Form to edit an existing item"""
387 387 c.repo_info = self._load_repo(repo_name)
388 388 c.active = 'remote'
389 389
390 390 return render('admin/repos/repo_edit.mako')
391 391
392 392 @HasRepoPermissionAllDecorator('repository.admin')
393 393 @auth.CSRFRequired()
394 394 def edit_statistics(self, repo_name):
395 395 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
396 396 try:
397 397 RepoModel().delete_stats(repo_name)
398 398 Session().commit()
399 399 except Exception as e:
400 400 log.error(traceback.format_exc())
401 401 h.flash(_('An error occurred during deletion of repository stats'),
402 402 category='error')
403 403 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
404 404
405 405 @HasRepoPermissionAllDecorator('repository.admin')
406 406 def edit_statistics_form(self, repo_name):
407 407 """GET /repo_name/settings: Form to edit an existing item"""
408 408 c.repo_info = self._load_repo(repo_name)
409 409 repo = c.repo_info.scm_instance()
410 410
411 411 if c.repo_info.stats:
412 412 # this is on what revision we ended up so we add +1 for count
413 413 last_rev = c.repo_info.stats.stat_on_revision + 1
414 414 else:
415 415 last_rev = 0
416 416 c.stats_revision = last_rev
417 417
418 418 c.repo_last_rev = repo.count()
419 419
420 420 if last_rev == 0 or c.repo_last_rev == 0:
421 421 c.stats_percentage = 0
422 422 else:
423 423 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
424 424
425 425 c.active = 'statistics'
426 426
427 427 return render('admin/repos/repo_edit.mako')
428 428
429 429 @HasRepoPermissionAllDecorator('repository.admin')
430 430 @auth.CSRFRequired()
431 431 def repo_issuetracker_test(self, repo_name):
432 432 if request.is_xhr:
433 433 return h.urlify_commit_message(
434 434 request.POST.get('test_text', ''),
435 435 repo_name)
436 436 else:
437 437 raise HTTPBadRequest()
438 438
439 439 @HasRepoPermissionAllDecorator('repository.admin')
440 440 @auth.CSRFRequired()
441 441 def repo_issuetracker_delete(self, repo_name):
442 442 uid = request.POST.get('uid')
443 443 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
444 444 try:
445 445 repo_settings.delete_entries(uid)
446 446 except Exception:
447 447 h.flash(_('Error occurred during deleting issue tracker entry'),
448 448 category='error')
449 449 else:
450 450 h.flash(_('Removed issue tracker entry'), category='success')
451 451 return redirect(url('repo_settings_issuetracker',
452 452 repo_name=repo_name))
453 453
454 454 def _update_patterns(self, form, repo_settings):
455 455 for uid in form['delete_patterns']:
456 456 repo_settings.delete_entries(uid)
457 457
458 458 for pattern in form['patterns']:
459 459 for setting, value, type_ in pattern:
460 460 sett = repo_settings.create_or_update_setting(
461 461 setting, value, type_)
462 462 Session().add(sett)
463 463
464 464 Session().commit()
465 465
466 466 @HasRepoPermissionAllDecorator('repository.admin')
467 467 @auth.CSRFRequired()
468 468 def repo_issuetracker_save(self, repo_name):
469 469 # Save inheritance
470 470 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
471 471 inherited = (request.POST.get('inherit_global_issuetracker')
472 472 == "inherited")
473 473 repo_settings.inherit_global_settings = inherited
474 474 Session().commit()
475 475
476 476 form = IssueTrackerPatternsForm()().to_python(request.POST)
477 477 if form:
478 478 self._update_patterns(form, repo_settings)
479 479
480 480 h.flash(_('Updated issue tracker entries'), category='success')
481 481 return redirect(url('repo_settings_issuetracker',
482 482 repo_name=repo_name))
483 483
484 484 @HasRepoPermissionAllDecorator('repository.admin')
485 485 def repo_issuetracker(self, repo_name):
486 486 """GET /admin/settings/issue-tracker: All items in the collection"""
487 487 c.active = 'issuetracker'
488 488 c.data = 'data'
489 489 c.repo_info = self._load_repo(repo_name)
490 490
491 491 repo = Repository.get_by_repo_name(repo_name)
492 492 c.settings_model = IssueTrackerSettingsModel(repo=repo)
493 493 c.global_patterns = c.settings_model.get_global_settings()
494 494 c.repo_patterns = c.settings_model.get_repo_settings()
495 495
496 496 return render('admin/repos/repo_edit.mako')
497 497
498 498 @HasRepoPermissionAllDecorator('repository.admin')
499 499 def repo_settings_vcs(self, repo_name):
500 500 """GET /{repo_name}/settings/vcs/: All items in the collection"""
501 501
502 502 model = VcsSettingsModel(repo=repo_name)
503 503
504 504 c.active = 'vcs'
505 505 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
506 506 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
507 507 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
508 508 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
509 509 c.repo_info = self._load_repo(repo_name)
510 510 defaults = self._vcs_form_defaults(repo_name)
511 511 c.inherit_global_settings = defaults['inherit_global_settings']
512 512 c.labs_active = str2bool(
513 513 rhodecode.CONFIG.get('labs_settings_active', 'true'))
514 514
515 515 return htmlfill.render(
516 516 render('admin/repos/repo_edit.mako'),
517 517 defaults=defaults,
518 518 encoding="UTF-8",
519 519 force_defaults=False)
520 520
521 521 @HasRepoPermissionAllDecorator('repository.admin')
522 522 @auth.CSRFRequired()
523 523 def repo_settings_vcs_update(self, repo_name):
524 524 """POST /{repo_name}/settings/vcs/: All items in the collection"""
525 525 c.active = 'vcs'
526 526
527 527 model = VcsSettingsModel(repo=repo_name)
528 528 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
529 529 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
530 530 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
531 531 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
532 532 c.repo_info = self._load_repo(repo_name)
533 533 defaults = self._vcs_form_defaults(repo_name)
534 534 c.inherit_global_settings = defaults['inherit_global_settings']
535 535
536 536 application_form = RepoVcsSettingsForm(repo_name)()
537 537 try:
538 538 form_result = application_form.to_python(dict(request.POST))
539 539 except formencode.Invalid as errors:
540 540 h.flash(
541 541 _("Some form inputs contain invalid data."),
542 542 category='error')
543 543 return htmlfill.render(
544 544 render('admin/repos/repo_edit.mako'),
545 545 defaults=errors.value,
546 546 errors=errors.error_dict or {},
547 547 prefix_error=False,
548 548 encoding="UTF-8",
549 549 force_defaults=False
550 550 )
551 551
552 552 try:
553 553 inherit_global_settings = form_result['inherit_global_settings']
554 554 model.create_or_update_repo_settings(
555 555 form_result, inherit_global_settings=inherit_global_settings)
556 556 except Exception:
557 557 log.exception("Exception while updating settings")
558 558 h.flash(
559 559 _('Error occurred during updating repository VCS settings'),
560 560 category='error')
561 561 else:
562 562 Session().commit()
563 563 h.flash(_('Updated VCS settings'), category='success')
564 564 return redirect(url('repo_vcs_settings', repo_name=repo_name))
565 565
566 566 return htmlfill.render(
567 567 render('admin/repos/repo_edit.mako'),
568 568 defaults=self._vcs_form_defaults(repo_name),
569 569 encoding="UTF-8",
570 570 force_defaults=False)
571 571
572 572 @HasRepoPermissionAllDecorator('repository.admin')
573 573 @auth.CSRFRequired()
574 574 @jsonify
575 575 def repo_delete_svn_pattern(self, repo_name):
576 576 if not request.is_xhr:
577 577 return False
578 578
579 579 delete_pattern_id = request.POST.get('delete_svn_pattern')
580 580 model = VcsSettingsModel(repo=repo_name)
581 581 try:
582 582 model.delete_repo_svn_pattern(delete_pattern_id)
583 583 except SettingNotFound:
584 584 raise HTTPBadRequest()
585 585
586 586 Session().commit()
587 587 return True
588 588
589 589 def _vcs_form_defaults(self, repo_name):
590 590 model = VcsSettingsModel(repo=repo_name)
591 591 global_defaults = model.get_global_settings()
592 592
593 593 repo_defaults = {}
594 594 repo_defaults.update(global_defaults)
595 595 repo_defaults.update(model.get_repo_settings())
596 596
597 597 global_defaults = {
598 598 '{}_inherited'.format(k): global_defaults[k]
599 599 for k in global_defaults}
600 600
601 601 defaults = {
602 602 'inherit_global_settings': model.inherit_global_settings
603 603 }
604 604 defaults.update(global_defaults)
605 605 defaults.update(repo_defaults)
606 606 defaults.update({
607 607 'new_svn_branch': '',
608 608 'new_svn_tag': '',
609 609 })
610 610 return defaults
@@ -1,196 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 forks controller for rhodecode
23 23 """
24 24
25 25 import formencode
26 26 import logging
27 27 from formencode import htmlfill
28 28
29 29 from pylons import tmpl_context as c, request, url
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 import rhodecode.lib.helpers as h
34 34
35 35 from rhodecode.lib import auth
36 36 from rhodecode.lib.helpers import Page
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
39 39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.forms import RepoForkForm
44 44 from rhodecode.model.scm import ScmModel, RepoGroupList
45 45 from rhodecode.lib.utils2 import safe_int
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class ForksController(BaseRepoController):
51 51
52 52 def __before__(self):
53 53 super(ForksController, self).__before__()
54 54
55 55 def __load_defaults(self):
56 56 acl_groups = RepoGroupList(
57 57 RepoGroup.query().all(),
58 58 perm_set=['group.write', 'group.admin'])
59 59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
61 61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
62 62 c.landing_revs_choices = choices
63 63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
64 64
65 65 def __load_data(self, repo_name=None):
66 66 """
67 67 Load defaults settings for edit, and update
68 68
69 69 :param repo_name:
70 70 """
71 71 self.__load_defaults()
72 72
73 73 c.repo_info = Repository.get_by_repo_name(repo_name)
74 74 repo = c.repo_info.scm_instance()
75 75
76 76 if c.repo_info is None:
77 77 h.not_mapped_error(repo_name)
78 78 return redirect(url('repos'))
79 79
80 80 c.default_user_id = User.get_default_user().user_id
81 81 c.in_public_journal = UserFollowing.query()\
82 82 .filter(UserFollowing.user_id == c.default_user_id)\
83 83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
84 84
85 85 if c.repo_info.stats:
86 86 last_rev = c.repo_info.stats.stat_on_revision+1
87 87 else:
88 88 last_rev = 0
89 89 c.stats_revision = last_rev
90 90
91 91 c.repo_last_rev = repo.count()
92 92
93 93 if last_rev == 0 or c.repo_last_rev == 0:
94 94 c.stats_percentage = 0
95 95 else:
96 96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
97 97 c.repo_last_rev) * 100)
98 98
99 99 defaults = RepoModel()._get_defaults(repo_name)
100 100 # alter the description to indicate a fork
101 101 defaults['description'] = ('fork of repository: %s \n%s'
102 102 % (defaults['repo_name'],
103 103 defaults['description']))
104 104 # add suffix to fork
105 105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
106 106
107 107 return defaults
108 108
109 109 @LoginRequired()
110 110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
111 111 'repository.admin')
112 112 @HasAcceptedRepoType('git', 'hg')
113 113 def forks(self, repo_name):
114 114 p = safe_int(request.GET.get('page', 1), 1)
115 115 repo_id = c.rhodecode_db_repo.repo_id
116 116 d = []
117 117 for r in Repository.get_repo_forks(repo_id):
118 118 if not HasRepoPermissionAny(
119 119 'repository.read', 'repository.write', 'repository.admin'
120 120 )(r.repo_name, 'get forks check'):
121 121 continue
122 122 d.append(r)
123 123 c.forks_pager = Page(d, page=p, items_per_page=20)
124 124
125 125 c.forks_data = render('/forks/forks_data.mako')
126 126
127 127 if request.environ.get('HTTP_X_PJAX'):
128 128 return c.forks_data
129 129
130 130 return render('/forks/forks.mako')
131 131
132 132 @LoginRequired()
133 133 @NotAnonymous()
134 134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
135 135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 136 'repository.admin')
137 137 @HasAcceptedRepoType('git', 'hg')
138 138 def fork(self, repo_name):
139 139 c.repo_info = Repository.get_by_repo_name(repo_name)
140 140 if not c.repo_info:
141 141 h.not_mapped_error(repo_name)
142 return redirect(url('home'))
142 return redirect(h.route_path('home'))
143 143
144 144 defaults = self.__load_data(repo_name)
145 145
146 146 return htmlfill.render(
147 147 render('forks/fork.mako'),
148 148 defaults=defaults,
149 149 encoding="UTF-8",
150 150 force_defaults=False
151 151 )
152 152
153 153 @LoginRequired()
154 154 @NotAnonymous()
155 155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
156 156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
157 157 'repository.admin')
158 158 @HasAcceptedRepoType('git', 'hg')
159 159 @auth.CSRFRequired()
160 160 def fork_create(self, repo_name):
161 161 self.__load_defaults()
162 162 c.repo_info = Repository.get_by_repo_name(repo_name)
163 163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
164 164 repo_groups=c.repo_groups_choices,
165 165 landing_revs=c.landing_revs_choices)()
166 166 form_result = {}
167 167 task_id = None
168 168 try:
169 169 form_result = _form.to_python(dict(request.POST))
170 170 # create fork is done sometimes async on celery, db transaction
171 171 # management is handled there.
172 172 task = RepoModel().create_fork(
173 173 form_result, c.rhodecode_user.user_id)
174 174 from celery.result import BaseAsyncResult
175 175 if isinstance(task, BaseAsyncResult):
176 176 task_id = task.task_id
177 177 except formencode.Invalid as errors:
178 178 c.new_repo = errors.value['repo_name']
179 179 return htmlfill.render(
180 180 render('forks/fork.mako'),
181 181 defaults=errors.value,
182 182 errors=errors.error_dict or {},
183 183 prefix_error=False,
184 184 encoding="UTF-8",
185 185 force_defaults=False)
186 186 except Exception:
187 187 log.exception(
188 188 u'Exception while trying to fork the repository %s', repo_name)
189 189 msg = (
190 190 _('An error occurred during repository forking %s') %
191 191 (repo_name, ))
192 192 h.flash(msg, category='error')
193 193
194 194 return redirect(h.url('repo_creating_home',
195 195 repo_name=form_result['repo_name_full'],
196 196 task_id=task_id))
@@ -1,110 +1,67 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 Home controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 import time
27 27
28 28 from pylons import tmpl_context as c
29 29
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasPermissionAllDecorator,
32 32 HasRepoGroupPermissionAnyDecorator)
33 33 from rhodecode.lib.base import BaseController, render
34 34
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.model.db import Repository, RepoGroup
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.repo_group import RepoGroupModel
39 39 from rhodecode.model.scm import RepoList, RepoGroupList
40 40
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class HomeController(BaseController):
46 46 def __before__(self):
47 47 super(HomeController, self).__before__()
48 48
49 49 def ping(self):
50 50 """
51 51 Ping, doesn't require login, good for checking out the platform
52 52 """
53 53 instance_id = getattr(c, 'rhodecode_instanceid', '')
54 54 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
55 55
56 56 @LoginRequired()
57 57 @HasPermissionAllDecorator('hg.admin')
58 58 def error_test(self):
59 59 """
60 60 Test exception handling and emails on errors
61 61 """
62 62 class TestException(Exception):
63 63 pass
64 64
65 65 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
66 66 % (c.rhodecode_name, time.time()))
67 67 raise TestException(msg)
68
69 def _get_groups_and_repos(self, repo_group_id=None):
70 # repo groups groups
71 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
72 _perms = ['group.read', 'group.write', 'group.admin']
73 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
74 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
75 repo_group_list=repo_group_list_acl, admin=False)
76
77 # repositories
78 repo_list = Repository.get_all_repos(group_id=repo_group_id)
79 _perms = ['repository.read', 'repository.write', 'repository.admin']
80 repo_list_acl = RepoList(repo_list, perm_set=_perms)
81 repo_data = RepoModel().get_repos_as_dict(
82 repo_list=repo_list_acl, admin=False)
83
84 return repo_data, repo_group_data
85
86 @LoginRequired()
87 def index(self):
88 c.repo_group = None
89
90 repo_data, repo_group_data = self._get_groups_and_repos()
91 # json used to render the grids
92 c.repos_data = json.dumps(repo_data)
93 c.repo_groups_data = json.dumps(repo_group_data)
94
95 return render('/index.mako')
96
97 @LoginRequired()
98 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
99 'group.admin')
100 def index_repo_group(self, group_name):
101 """GET /repo_group_name: Show a specific item"""
102 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
103 repo_data, repo_group_data = self._get_groups_and_repos(
104 c.repo_group.group_id)
105
106 # json used to render the grids
107 c.repos_data = json.dumps(repo_data)
108 c.repo_groups_data = json.dumps(repo_group_data)
109
110 return render('index_repo_group.mako')
@@ -1,103 +1,103 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20 20 from datetime import datetime
21 21 from pyramid.threadlocal import get_current_request
22 22 from rhodecode.lib.utils2 import AttributeDict
23 23
24 24
25 25 # this is a user object to be used for events caused by the system (eg. shell)
26 26 SYSTEM_USER = AttributeDict(dict(
27 27 username='__SYSTEM__',
28 28 user_id='__SYSTEM_ID__'
29 29 ))
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class RhodecodeEvent(object):
35 35 """
36 36 Base event class for all Rhodecode events
37 37 """
38 38 name = "RhodeCodeEvent"
39 39
40 40 def __init__(self):
41 41 self.request = get_current_request()
42 42 self.utc_timestamp = datetime.utcnow()
43 43
44 44 @property
45 45 def auth_user(self):
46 46 if not self.request:
47 47 return
48 48
49 49 user = getattr(self.request, 'user', None)
50 50 if user:
51 51 return user
52 52
53 53 api_user = getattr(self.request, 'rpc_user', None)
54 54 if api_user:
55 55 return api_user
56 56
57 57 @property
58 58 def actor(self):
59 59 auth_user = self.auth_user
60 60
61 61 if auth_user:
62 62 instance = auth_user.get_instance()
63 63 if not instance:
64 64 return AttributeDict(dict(
65 65 username=auth_user.username,
66 66 user_id=auth_user.user_id,
67 67 ))
68 68 return instance
69 69
70 70 return SYSTEM_USER
71 71
72 72 @property
73 73 def actor_ip(self):
74 74 auth_user = self.auth_user
75 75 if auth_user:
76 76 return auth_user.ip_addr
77 77 return '<no ip available>'
78 78
79 79 @property
80 80 def server_url(self):
81 81 default = '<no server_url available>'
82 82 if self.request:
83 83 from rhodecode.lib import helpers as h
84 84 try:
85 return h.url('home', qualified=True)
85 return h.route_url('home')
86 86 except Exception:
87 87 log.exception('Failed to fetch URL for server')
88 88 return default
89 89
90 90 return default
91 91
92 92 def as_dict(self):
93 93 data = {
94 94 'name': self.name,
95 95 'utc_timestamp': self.utc_timestamp,
96 96 'actor_ip': self.actor_ip,
97 97 'actor': {
98 98 'username': self.actor.username,
99 99 'user_id': self.actor.user_id
100 100 },
101 101 'server_url': self.server_url
102 102 }
103 103 return data
@@ -1,1973 +1,1993 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 authentication and permission libraries
23 23 """
24 24
25 25 import os
26 26 import inspect
27 27 import collections
28 28 import fnmatch
29 29 import hashlib
30 30 import itertools
31 31 import logging
32 32 import random
33 33 import traceback
34 34 from functools import wraps
35 35
36 36 import ipaddress
37 37 from pyramid.httpexceptions import HTTPForbidden, HTTPFound
38 38 from pylons import url, request
39 39 from pylons.controllers.util import abort, redirect
40 40 from pylons.i18n.translation import _
41 41 from sqlalchemy.orm.exc import ObjectDeletedError
42 42 from sqlalchemy.orm import joinedload
43 43 from zope.cachedescriptors.property import Lazy as LazyProperty
44 44
45 45 import rhodecode
46 46 from rhodecode.model import meta
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import (
50 50 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
51 51 UserIpMap, UserApiKeys, RepoGroup)
52 52 from rhodecode.lib import caches
53 53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
54 54 from rhodecode.lib.utils import (
55 55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
56 56 from rhodecode.lib.caching_query import FromCache
57 57
58 58
59 59 if rhodecode.is_unix:
60 60 import bcrypt
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 csrf_token_key = "csrf_token"
65 65
66 66
67 67 class PasswordGenerator(object):
68 68 """
69 69 This is a simple class for generating password from different sets of
70 70 characters
71 71 usage::
72 72
73 73 passwd_gen = PasswordGenerator()
74 74 #print 8-letter password containing only big and small letters
75 75 of alphabet
76 76 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
77 77 """
78 78 ALPHABETS_NUM = r'''1234567890'''
79 79 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
80 80 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
81 81 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
82 82 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
83 83 + ALPHABETS_NUM + ALPHABETS_SPECIAL
84 84 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
85 85 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
86 86 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
87 87 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
88 88
89 89 def __init__(self, passwd=''):
90 90 self.passwd = passwd
91 91
92 92 def gen_password(self, length, type_=None):
93 93 if type_ is None:
94 94 type_ = self.ALPHABETS_FULL
95 95 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
96 96 return self.passwd
97 97
98 98
99 99 class _RhodeCodeCryptoBase(object):
100 100 ENC_PREF = None
101 101
102 102 def hash_create(self, str_):
103 103 """
104 104 hash the string using
105 105
106 106 :param str_: password to hash
107 107 """
108 108 raise NotImplementedError
109 109
110 110 def hash_check_with_upgrade(self, password, hashed):
111 111 """
112 112 Returns tuple in which first element is boolean that states that
113 113 given password matches it's hashed version, and the second is new hash
114 114 of the password, in case this password should be migrated to new
115 115 cipher.
116 116 """
117 117 checked_hash = self.hash_check(password, hashed)
118 118 return checked_hash, None
119 119
120 120 def hash_check(self, password, hashed):
121 121 """
122 122 Checks matching password with it's hashed value.
123 123
124 124 :param password: password
125 125 :param hashed: password in hashed form
126 126 """
127 127 raise NotImplementedError
128 128
129 129 def _assert_bytes(self, value):
130 130 """
131 131 Passing in an `unicode` object can lead to hard to detect issues
132 132 if passwords contain non-ascii characters. Doing a type check
133 133 during runtime, so that such mistakes are detected early on.
134 134 """
135 135 if not isinstance(value, str):
136 136 raise TypeError(
137 137 "Bytestring required as input, got %r." % (value, ))
138 138
139 139
140 140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
141 141 ENC_PREF = ('$2a$10', '$2b$10')
142 142
143 143 def hash_create(self, str_):
144 144 self._assert_bytes(str_)
145 145 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
146 146
147 147 def hash_check_with_upgrade(self, password, hashed):
148 148 """
149 149 Returns tuple in which first element is boolean that states that
150 150 given password matches it's hashed version, and the second is new hash
151 151 of the password, in case this password should be migrated to new
152 152 cipher.
153 153
154 154 This implements special upgrade logic which works like that:
155 155 - check if the given password == bcrypted hash, if yes then we
156 156 properly used password and it was already in bcrypt. Proceed
157 157 without any changes
158 158 - if bcrypt hash check is not working try with sha256. If hash compare
159 159 is ok, it means we using correct but old hashed password. indicate
160 160 hash change and proceed
161 161 """
162 162
163 163 new_hash = None
164 164
165 165 # regular pw check
166 166 password_match_bcrypt = self.hash_check(password, hashed)
167 167
168 168 # now we want to know if the password was maybe from sha256
169 169 # basically calling _RhodeCodeCryptoSha256().hash_check()
170 170 if not password_match_bcrypt:
171 171 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
172 172 new_hash = self.hash_create(password) # make new bcrypt hash
173 173 password_match_bcrypt = True
174 174
175 175 return password_match_bcrypt, new_hash
176 176
177 177 def hash_check(self, password, hashed):
178 178 """
179 179 Checks matching password with it's hashed value.
180 180
181 181 :param password: password
182 182 :param hashed: password in hashed form
183 183 """
184 184 self._assert_bytes(password)
185 185 try:
186 186 return bcrypt.hashpw(password, hashed) == hashed
187 187 except ValueError as e:
188 188 # we're having a invalid salt here probably, we should not crash
189 189 # just return with False as it would be a wrong password.
190 190 log.debug('Failed to check password hash using bcrypt %s',
191 191 safe_str(e))
192 192
193 193 return False
194 194
195 195
196 196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
197 197 ENC_PREF = '_'
198 198
199 199 def hash_create(self, str_):
200 200 self._assert_bytes(str_)
201 201 return hashlib.sha256(str_).hexdigest()
202 202
203 203 def hash_check(self, password, hashed):
204 204 """
205 205 Checks matching password with it's hashed value.
206 206
207 207 :param password: password
208 208 :param hashed: password in hashed form
209 209 """
210 210 self._assert_bytes(password)
211 211 return hashlib.sha256(password).hexdigest() == hashed
212 212
213 213
214 214 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
215 215 ENC_PREF = '_'
216 216
217 217 def hash_create(self, str_):
218 218 self._assert_bytes(str_)
219 219 return hashlib.md5(str_).hexdigest()
220 220
221 221 def hash_check(self, password, hashed):
222 222 """
223 223 Checks matching password with it's hashed value.
224 224
225 225 :param password: password
226 226 :param hashed: password in hashed form
227 227 """
228 228 self._assert_bytes(password)
229 229 return hashlib.md5(password).hexdigest() == hashed
230 230
231 231
232 232 def crypto_backend():
233 233 """
234 234 Return the matching crypto backend.
235 235
236 236 Selection is based on if we run tests or not, we pick md5 backend to run
237 237 tests faster since BCRYPT is expensive to calculate
238 238 """
239 239 if rhodecode.is_test:
240 240 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
241 241 else:
242 242 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
243 243
244 244 return RhodeCodeCrypto
245 245
246 246
247 247 def get_crypt_password(password):
248 248 """
249 249 Create the hash of `password` with the active crypto backend.
250 250
251 251 :param password: The cleartext password.
252 252 :type password: unicode
253 253 """
254 254 password = safe_str(password)
255 255 return crypto_backend().hash_create(password)
256 256
257 257
258 258 def check_password(password, hashed):
259 259 """
260 260 Check if the value in `password` matches the hash in `hashed`.
261 261
262 262 :param password: The cleartext password.
263 263 :type password: unicode
264 264
265 265 :param hashed: The expected hashed version of the password.
266 266 :type hashed: The hash has to be passed in in text representation.
267 267 """
268 268 password = safe_str(password)
269 269 return crypto_backend().hash_check(password, hashed)
270 270
271 271
272 272 def generate_auth_token(data, salt=None):
273 273 """
274 274 Generates API KEY from given string
275 275 """
276 276
277 277 if salt is None:
278 278 salt = os.urandom(16)
279 279 return hashlib.sha1(safe_str(data) + salt).hexdigest()
280 280
281 281
282 282 class CookieStoreWrapper(object):
283 283
284 284 def __init__(self, cookie_store):
285 285 self.cookie_store = cookie_store
286 286
287 287 def __repr__(self):
288 288 return 'CookieStore<%s>' % (self.cookie_store)
289 289
290 290 def get(self, key, other=None):
291 291 if isinstance(self.cookie_store, dict):
292 292 return self.cookie_store.get(key, other)
293 293 elif isinstance(self.cookie_store, AuthUser):
294 294 return self.cookie_store.__dict__.get(key, other)
295 295
296 296
297 297 def _cached_perms_data(user_id, scope, user_is_admin,
298 298 user_inherit_default_permissions, explicit, algo):
299 299
300 300 permissions = PermissionCalculator(
301 301 user_id, scope, user_is_admin, user_inherit_default_permissions,
302 302 explicit, algo)
303 303 return permissions.calculate()
304 304
305 305 class PermOrigin:
306 306 ADMIN = 'superadmin'
307 307
308 308 REPO_USER = 'user:%s'
309 309 REPO_USERGROUP = 'usergroup:%s'
310 310 REPO_OWNER = 'repo.owner'
311 311 REPO_DEFAULT = 'repo.default'
312 312 REPO_PRIVATE = 'repo.private'
313 313
314 314 REPOGROUP_USER = 'user:%s'
315 315 REPOGROUP_USERGROUP = 'usergroup:%s'
316 316 REPOGROUP_OWNER = 'group.owner'
317 317 REPOGROUP_DEFAULT = 'group.default'
318 318
319 319 USERGROUP_USER = 'user:%s'
320 320 USERGROUP_USERGROUP = 'usergroup:%s'
321 321 USERGROUP_OWNER = 'usergroup.owner'
322 322 USERGROUP_DEFAULT = 'usergroup.default'
323 323
324 324
325 325 class PermOriginDict(dict):
326 326 """
327 327 A special dict used for tracking permissions along with their origins.
328 328
329 329 `__setitem__` has been overridden to expect a tuple(perm, origin)
330 330 `__getitem__` will return only the perm
331 331 `.perm_origin_stack` will return the stack of (perm, origin) set per key
332 332
333 333 >>> perms = PermOriginDict()
334 334 >>> perms['resource'] = 'read', 'default'
335 335 >>> perms['resource']
336 336 'read'
337 337 >>> perms['resource'] = 'write', 'admin'
338 338 >>> perms['resource']
339 339 'write'
340 340 >>> perms.perm_origin_stack
341 341 {'resource': [('read', 'default'), ('write', 'admin')]}
342 342 """
343 343
344 344
345 345 def __init__(self, *args, **kw):
346 346 dict.__init__(self, *args, **kw)
347 347 self.perm_origin_stack = {}
348 348
349 349 def __setitem__(self, key, (perm, origin)):
350 350 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
351 351 dict.__setitem__(self, key, perm)
352 352
353 353
354 354 class PermissionCalculator(object):
355 355
356 356 def __init__(
357 357 self, user_id, scope, user_is_admin,
358 358 user_inherit_default_permissions, explicit, algo):
359 359 self.user_id = user_id
360 360 self.user_is_admin = user_is_admin
361 361 self.inherit_default_permissions = user_inherit_default_permissions
362 362 self.explicit = explicit
363 363 self.algo = algo
364 364
365 365 scope = scope or {}
366 366 self.scope_repo_id = scope.get('repo_id')
367 367 self.scope_repo_group_id = scope.get('repo_group_id')
368 368 self.scope_user_group_id = scope.get('user_group_id')
369 369
370 370 self.default_user_id = User.get_default_user(cache=True).user_id
371 371
372 372 self.permissions_repositories = PermOriginDict()
373 373 self.permissions_repository_groups = PermOriginDict()
374 374 self.permissions_user_groups = PermOriginDict()
375 375 self.permissions_global = set()
376 376
377 377 self.default_repo_perms = Permission.get_default_repo_perms(
378 378 self.default_user_id, self.scope_repo_id)
379 379 self.default_repo_groups_perms = Permission.get_default_group_perms(
380 380 self.default_user_id, self.scope_repo_group_id)
381 381 self.default_user_group_perms = \
382 382 Permission.get_default_user_group_perms(
383 383 self.default_user_id, self.scope_user_group_id)
384 384
385 385 def calculate(self):
386 386 if self.user_is_admin:
387 387 return self._admin_permissions()
388 388
389 389 self._calculate_global_default_permissions()
390 390 self._calculate_global_permissions()
391 391 self._calculate_default_permissions()
392 392 self._calculate_repository_permissions()
393 393 self._calculate_repository_group_permissions()
394 394 self._calculate_user_group_permissions()
395 395 return self._permission_structure()
396 396
397 397 def _admin_permissions(self):
398 398 """
399 399 admin user have all default rights for repositories
400 400 and groups set to admin
401 401 """
402 402 self.permissions_global.add('hg.admin')
403 403 self.permissions_global.add('hg.create.write_on_repogroup.true')
404 404
405 405 # repositories
406 406 for perm in self.default_repo_perms:
407 407 r_k = perm.UserRepoToPerm.repository.repo_name
408 408 p = 'repository.admin'
409 409 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
410 410
411 411 # repository groups
412 412 for perm in self.default_repo_groups_perms:
413 413 rg_k = perm.UserRepoGroupToPerm.group.group_name
414 414 p = 'group.admin'
415 415 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
416 416
417 417 # user groups
418 418 for perm in self.default_user_group_perms:
419 419 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
420 420 p = 'usergroup.admin'
421 421 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
422 422
423 423 return self._permission_structure()
424 424
425 425 def _calculate_global_default_permissions(self):
426 426 """
427 427 global permissions taken from the default user
428 428 """
429 429 default_global_perms = UserToPerm.query()\
430 430 .filter(UserToPerm.user_id == self.default_user_id)\
431 431 .options(joinedload(UserToPerm.permission))
432 432
433 433 for perm in default_global_perms:
434 434 self.permissions_global.add(perm.permission.permission_name)
435 435
436 436 def _calculate_global_permissions(self):
437 437 """
438 438 Set global system permissions with user permissions or permissions
439 439 taken from the user groups of the current user.
440 440
441 441 The permissions include repo creating, repo group creating, forking
442 442 etc.
443 443 """
444 444
445 445 # now we read the defined permissions and overwrite what we have set
446 446 # before those can be configured from groups or users explicitly.
447 447
448 448 # TODO: johbo: This seems to be out of sync, find out the reason
449 449 # for the comment below and update it.
450 450
451 451 # In case we want to extend this list we should be always in sync with
452 452 # User.DEFAULT_USER_PERMISSIONS definitions
453 453 _configurable = frozenset([
454 454 'hg.fork.none', 'hg.fork.repository',
455 455 'hg.create.none', 'hg.create.repository',
456 456 'hg.usergroup.create.false', 'hg.usergroup.create.true',
457 457 'hg.repogroup.create.false', 'hg.repogroup.create.true',
458 458 'hg.create.write_on_repogroup.false',
459 459 'hg.create.write_on_repogroup.true',
460 460 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
461 461 ])
462 462
463 463 # USER GROUPS comes first user group global permissions
464 464 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
465 465 .options(joinedload(UserGroupToPerm.permission))\
466 466 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
467 467 UserGroupMember.users_group_id))\
468 468 .filter(UserGroupMember.user_id == self.user_id)\
469 469 .order_by(UserGroupToPerm.users_group_id)\
470 470 .all()
471 471
472 472 # need to group here by groups since user can be in more than
473 473 # one group, so we get all groups
474 474 _explicit_grouped_perms = [
475 475 [x, list(y)] for x, y in
476 476 itertools.groupby(user_perms_from_users_groups,
477 477 lambda _x: _x.users_group)]
478 478
479 479 for gr, perms in _explicit_grouped_perms:
480 480 # since user can be in multiple groups iterate over them and
481 481 # select the lowest permissions first (more explicit)
482 482 # TODO: marcink: do this^^
483 483
484 484 # group doesn't inherit default permissions so we actually set them
485 485 if not gr.inherit_default_permissions:
486 486 # NEED TO IGNORE all previously set configurable permissions
487 487 # and replace them with explicitly set from this user
488 488 # group permissions
489 489 self.permissions_global = self.permissions_global.difference(
490 490 _configurable)
491 491 for perm in perms:
492 492 self.permissions_global.add(perm.permission.permission_name)
493 493
494 494 # user explicit global permissions
495 495 user_perms = Session().query(UserToPerm)\
496 496 .options(joinedload(UserToPerm.permission))\
497 497 .filter(UserToPerm.user_id == self.user_id).all()
498 498
499 499 if not self.inherit_default_permissions:
500 500 # NEED TO IGNORE all configurable permissions and
501 501 # replace them with explicitly set from this user permissions
502 502 self.permissions_global = self.permissions_global.difference(
503 503 _configurable)
504 504 for perm in user_perms:
505 505 self.permissions_global.add(perm.permission.permission_name)
506 506
507 507 def _calculate_default_permissions(self):
508 508 """
509 509 Set default user permissions for repositories, repository groups
510 510 taken from the default user.
511 511
512 512 Calculate inheritance of object permissions based on what we have now
513 513 in GLOBAL permissions. We check if .false is in GLOBAL since this is
514 514 explicitly set. Inherit is the opposite of .false being there.
515 515
516 516 .. note::
517 517
518 518 the syntax is little bit odd but what we need to check here is
519 519 the opposite of .false permission being in the list so even for
520 520 inconsistent state when both .true/.false is there
521 521 .false is more important
522 522
523 523 """
524 524 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
525 525 in self.permissions_global)
526 526
527 527 # defaults for repositories, taken from `default` user permissions
528 528 # on given repo
529 529 for perm in self.default_repo_perms:
530 530 r_k = perm.UserRepoToPerm.repository.repo_name
531 531 o = PermOrigin.REPO_DEFAULT
532 532 if perm.Repository.private and not (
533 533 perm.Repository.user_id == self.user_id):
534 534 # disable defaults for private repos,
535 535 p = 'repository.none'
536 536 o = PermOrigin.REPO_PRIVATE
537 537 elif perm.Repository.user_id == self.user_id:
538 538 # set admin if owner
539 539 p = 'repository.admin'
540 540 o = PermOrigin.REPO_OWNER
541 541 else:
542 542 p = perm.Permission.permission_name
543 543 # if we decide this user isn't inheriting permissions from
544 544 # default user we set him to .none so only explicit
545 545 # permissions work
546 546 if not user_inherit_object_permissions:
547 547 p = 'repository.none'
548 548 self.permissions_repositories[r_k] = p, o
549 549
550 550 # defaults for repository groups taken from `default` user permission
551 551 # on given group
552 552 for perm in self.default_repo_groups_perms:
553 553 rg_k = perm.UserRepoGroupToPerm.group.group_name
554 554 o = PermOrigin.REPOGROUP_DEFAULT
555 555 if perm.RepoGroup.user_id == self.user_id:
556 556 # set admin if owner
557 557 p = 'group.admin'
558 558 o = PermOrigin.REPOGROUP_OWNER
559 559 else:
560 560 p = perm.Permission.permission_name
561 561
562 562 # if we decide this user isn't inheriting permissions from default
563 563 # user we set him to .none so only explicit permissions work
564 564 if not user_inherit_object_permissions:
565 565 p = 'group.none'
566 566 self.permissions_repository_groups[rg_k] = p, o
567 567
568 568 # defaults for user groups taken from `default` user permission
569 569 # on given user group
570 570 for perm in self.default_user_group_perms:
571 571 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
572 572 o = PermOrigin.USERGROUP_DEFAULT
573 573 if perm.UserGroup.user_id == self.user_id:
574 574 # set admin if owner
575 575 p = 'usergroup.admin'
576 576 o = PermOrigin.USERGROUP_OWNER
577 577 else:
578 578 p = perm.Permission.permission_name
579 579
580 580 # if we decide this user isn't inheriting permissions from default
581 581 # user we set him to .none so only explicit permissions work
582 582 if not user_inherit_object_permissions:
583 583 p = 'usergroup.none'
584 584 self.permissions_user_groups[u_k] = p, o
585 585
586 586 def _calculate_repository_permissions(self):
587 587 """
588 588 Repository permissions for the current user.
589 589
590 590 Check if the user is part of user groups for this repository and
591 591 fill in the permission from it. `_choose_permission` decides of which
592 592 permission should be selected based on selected method.
593 593 """
594 594
595 595 # user group for repositories permissions
596 596 user_repo_perms_from_user_group = Permission\
597 597 .get_default_repo_perms_from_user_group(
598 598 self.user_id, self.scope_repo_id)
599 599
600 600 multiple_counter = collections.defaultdict(int)
601 601 for perm in user_repo_perms_from_user_group:
602 602 r_k = perm.UserGroupRepoToPerm.repository.repo_name
603 603 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
604 604 multiple_counter[r_k] += 1
605 605 p = perm.Permission.permission_name
606 606 o = PermOrigin.REPO_USERGROUP % ug_k
607 607
608 608 if perm.Repository.user_id == self.user_id:
609 609 # set admin if owner
610 610 p = 'repository.admin'
611 611 o = PermOrigin.REPO_OWNER
612 612 else:
613 613 if multiple_counter[r_k] > 1:
614 614 cur_perm = self.permissions_repositories[r_k]
615 615 p = self._choose_permission(p, cur_perm)
616 616 self.permissions_repositories[r_k] = p, o
617 617
618 618 # user explicit permissions for repositories, overrides any specified
619 619 # by the group permission
620 620 user_repo_perms = Permission.get_default_repo_perms(
621 621 self.user_id, self.scope_repo_id)
622 622 for perm in user_repo_perms:
623 623 r_k = perm.UserRepoToPerm.repository.repo_name
624 624 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
625 625 # set admin if owner
626 626 if perm.Repository.user_id == self.user_id:
627 627 p = 'repository.admin'
628 628 o = PermOrigin.REPO_OWNER
629 629 else:
630 630 p = perm.Permission.permission_name
631 631 if not self.explicit:
632 632 cur_perm = self.permissions_repositories.get(
633 633 r_k, 'repository.none')
634 634 p = self._choose_permission(p, cur_perm)
635 635 self.permissions_repositories[r_k] = p, o
636 636
637 637 def _calculate_repository_group_permissions(self):
638 638 """
639 639 Repository group permissions for the current user.
640 640
641 641 Check if the user is part of user groups for repository groups and
642 642 fill in the permissions from it. `_choose_permmission` decides of which
643 643 permission should be selected based on selected method.
644 644 """
645 645 # user group for repo groups permissions
646 646 user_repo_group_perms_from_user_group = Permission\
647 647 .get_default_group_perms_from_user_group(
648 648 self.user_id, self.scope_repo_group_id)
649 649
650 650 multiple_counter = collections.defaultdict(int)
651 651 for perm in user_repo_group_perms_from_user_group:
652 652 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
653 653 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
654 654 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
655 655 multiple_counter[g_k] += 1
656 656 p = perm.Permission.permission_name
657 657 if perm.RepoGroup.user_id == self.user_id:
658 658 # set admin if owner, even for member of other user group
659 659 p = 'group.admin'
660 660 o = PermOrigin.REPOGROUP_OWNER
661 661 else:
662 662 if multiple_counter[g_k] > 1:
663 663 cur_perm = self.permissions_repository_groups[g_k]
664 664 p = self._choose_permission(p, cur_perm)
665 665 self.permissions_repository_groups[g_k] = p, o
666 666
667 667 # user explicit permissions for repository groups
668 668 user_repo_groups_perms = Permission.get_default_group_perms(
669 669 self.user_id, self.scope_repo_group_id)
670 670 for perm in user_repo_groups_perms:
671 671 rg_k = perm.UserRepoGroupToPerm.group.group_name
672 672 u_k = perm.UserRepoGroupToPerm.user.username
673 673 o = PermOrigin.REPOGROUP_USER % u_k
674 674
675 675 if perm.RepoGroup.user_id == self.user_id:
676 676 # set admin if owner
677 677 p = 'group.admin'
678 678 o = PermOrigin.REPOGROUP_OWNER
679 679 else:
680 680 p = perm.Permission.permission_name
681 681 if not self.explicit:
682 682 cur_perm = self.permissions_repository_groups.get(
683 683 rg_k, 'group.none')
684 684 p = self._choose_permission(p, cur_perm)
685 685 self.permissions_repository_groups[rg_k] = p, o
686 686
687 687 def _calculate_user_group_permissions(self):
688 688 """
689 689 User group permissions for the current user.
690 690 """
691 691 # user group for user group permissions
692 692 user_group_from_user_group = Permission\
693 693 .get_default_user_group_perms_from_user_group(
694 694 self.user_id, self.scope_user_group_id)
695 695
696 696 multiple_counter = collections.defaultdict(int)
697 697 for perm in user_group_from_user_group:
698 698 g_k = perm.UserGroupUserGroupToPerm\
699 699 .target_user_group.users_group_name
700 700 u_k = perm.UserGroupUserGroupToPerm\
701 701 .user_group.users_group_name
702 702 o = PermOrigin.USERGROUP_USERGROUP % u_k
703 703 multiple_counter[g_k] += 1
704 704 p = perm.Permission.permission_name
705 705
706 706 if perm.UserGroup.user_id == self.user_id:
707 707 # set admin if owner, even for member of other user group
708 708 p = 'usergroup.admin'
709 709 o = PermOrigin.USERGROUP_OWNER
710 710 else:
711 711 if multiple_counter[g_k] > 1:
712 712 cur_perm = self.permissions_user_groups[g_k]
713 713 p = self._choose_permission(p, cur_perm)
714 714 self.permissions_user_groups[g_k] = p, o
715 715
716 716 # user explicit permission for user groups
717 717 user_user_groups_perms = Permission.get_default_user_group_perms(
718 718 self.user_id, self.scope_user_group_id)
719 719 for perm in user_user_groups_perms:
720 720 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
721 721 u_k = perm.UserUserGroupToPerm.user.username
722 722 o = PermOrigin.USERGROUP_USER % u_k
723 723
724 724 if perm.UserGroup.user_id == self.user_id:
725 725 # set admin if owner
726 726 p = 'usergroup.admin'
727 727 o = PermOrigin.USERGROUP_OWNER
728 728 else:
729 729 p = perm.Permission.permission_name
730 730 if not self.explicit:
731 731 cur_perm = self.permissions_user_groups.get(
732 732 ug_k, 'usergroup.none')
733 733 p = self._choose_permission(p, cur_perm)
734 734 self.permissions_user_groups[ug_k] = p, o
735 735
736 736 def _choose_permission(self, new_perm, cur_perm):
737 737 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
738 738 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
739 739 if self.algo == 'higherwin':
740 740 if new_perm_val > cur_perm_val:
741 741 return new_perm
742 742 return cur_perm
743 743 elif self.algo == 'lowerwin':
744 744 if new_perm_val < cur_perm_val:
745 745 return new_perm
746 746 return cur_perm
747 747
748 748 def _permission_structure(self):
749 749 return {
750 750 'global': self.permissions_global,
751 751 'repositories': self.permissions_repositories,
752 752 'repositories_groups': self.permissions_repository_groups,
753 753 'user_groups': self.permissions_user_groups,
754 754 }
755 755
756 756
757 757 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
758 758 """
759 759 Check if given controller_name is in whitelist of auth token access
760 760 """
761 761 if not whitelist:
762 762 from rhodecode import CONFIG
763 763 whitelist = aslist(
764 764 CONFIG.get('api_access_controllers_whitelist'), sep=',')
765 765 log.debug(
766 766 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
767 767
768 768 auth_token_access_valid = False
769 769 for entry in whitelist:
770 770 if fnmatch.fnmatch(controller_name, entry):
771 771 auth_token_access_valid = True
772 772 break
773 773
774 774 if auth_token_access_valid:
775 775 log.debug('controller:%s matches entry in whitelist'
776 776 % (controller_name,))
777 777 else:
778 778 msg = ('controller: %s does *NOT* match any entry in whitelist'
779 779 % (controller_name,))
780 780 if auth_token:
781 781 # if we use auth token key and don't have access it's a warning
782 782 log.warning(msg)
783 783 else:
784 784 log.debug(msg)
785 785
786 786 return auth_token_access_valid
787 787
788 788
789 789 class AuthUser(object):
790 790 """
791 791 A simple object that handles all attributes of user in RhodeCode
792 792
793 793 It does lookup based on API key,given user, or user present in session
794 794 Then it fills all required information for such user. It also checks if
795 795 anonymous access is enabled and if so, it returns default user as logged in
796 796 """
797 797 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
798 798
799 799 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
800 800
801 801 self.user_id = user_id
802 802 self._api_key = api_key
803 803
804 804 self.api_key = None
805 805 self.feed_token = ''
806 806 self.username = username
807 807 self.ip_addr = ip_addr
808 808 self.name = ''
809 809 self.lastname = ''
810 810 self.email = ''
811 811 self.is_authenticated = False
812 812 self.admin = False
813 813 self.inherit_default_permissions = False
814 814 self.password = ''
815 815
816 816 self.anonymous_user = None # propagated on propagate_data
817 817 self.propagate_data()
818 818 self._instance = None
819 819 self._permissions_scoped_cache = {} # used to bind scoped calculation
820 820
821 821 @LazyProperty
822 822 def permissions(self):
823 823 return self.get_perms(user=self, cache=False)
824 824
825 825 def permissions_with_scope(self, scope):
826 826 """
827 827 Call the get_perms function with scoped data. The scope in that function
828 828 narrows the SQL calls to the given ID of objects resulting in fetching
829 829 Just particular permission we want to obtain. If scope is an empty dict
830 830 then it basically narrows the scope to GLOBAL permissions only.
831 831
832 832 :param scope: dict
833 833 """
834 834 if 'repo_name' in scope:
835 835 obj = Repository.get_by_repo_name(scope['repo_name'])
836 836 if obj:
837 837 scope['repo_id'] = obj.repo_id
838 838 _scope = {
839 839 'repo_id': -1,
840 840 'user_group_id': -1,
841 841 'repo_group_id': -1,
842 842 }
843 843 _scope.update(scope)
844 844 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
845 845 _scope.items())))
846 846 if cache_key not in self._permissions_scoped_cache:
847 847 # store in cache to mimic how the @LazyProperty works,
848 848 # the difference here is that we use the unique key calculated
849 849 # from params and values
850 850 res = self.get_perms(user=self, cache=False, scope=_scope)
851 851 self._permissions_scoped_cache[cache_key] = res
852 852 return self._permissions_scoped_cache[cache_key]
853 853
854 854 def get_instance(self):
855 855 return User.get(self.user_id)
856 856
857 857 def update_lastactivity(self):
858 858 if self.user_id:
859 859 User.get(self.user_id).update_lastactivity()
860 860
861 861 def propagate_data(self):
862 862 """
863 863 Fills in user data and propagates values to this instance. Maps fetched
864 864 user attributes to this class instance attributes
865 865 """
866 866 log.debug('starting data propagation for new potential AuthUser')
867 867 user_model = UserModel()
868 868 anon_user = self.anonymous_user = User.get_default_user(cache=True)
869 869 is_user_loaded = False
870 870
871 871 # lookup by userid
872 872 if self.user_id is not None and self.user_id != anon_user.user_id:
873 873 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
874 874 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
875 875
876 876 # try go get user by api key
877 877 elif self._api_key and self._api_key != anon_user.api_key:
878 878 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
879 879 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
880 880
881 881 # lookup by username
882 882 elif self.username:
883 883 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
884 884 is_user_loaded = user_model.fill_data(self, username=self.username)
885 885 else:
886 886 log.debug('No data in %s that could been used to log in' % self)
887 887
888 888 if not is_user_loaded:
889 889 log.debug('Failed to load user. Fallback to default user')
890 890 # if we cannot authenticate user try anonymous
891 891 if anon_user.active:
892 892 user_model.fill_data(self, user_id=anon_user.user_id)
893 893 # then we set this user is logged in
894 894 self.is_authenticated = True
895 895 else:
896 896 # in case of disabled anonymous user we reset some of the
897 897 # parameters so such user is "corrupted", skipping the fill_data
898 898 for attr in ['user_id', 'username', 'admin', 'active']:
899 899 setattr(self, attr, None)
900 900 self.is_authenticated = False
901 901
902 902 if not self.username:
903 903 self.username = 'None'
904 904
905 905 log.debug('Auth User is now %s' % self)
906 906
907 907 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
908 908 cache=False):
909 909 """
910 910 Fills user permission attribute with permissions taken from database
911 911 works for permissions given for repositories, and for permissions that
912 912 are granted to groups
913 913
914 914 :param user: instance of User object from database
915 915 :param explicit: In case there are permissions both for user and a group
916 916 that user is part of, explicit flag will defiine if user will
917 917 explicitly override permissions from group, if it's False it will
918 918 make decision based on the algo
919 919 :param algo: algorithm to decide what permission should be choose if
920 920 it's multiple defined, eg user in two different groups. It also
921 921 decides if explicit flag is turned off how to specify the permission
922 922 for case when user is in a group + have defined separate permission
923 923 """
924 924 user_id = user.user_id
925 925 user_is_admin = user.is_admin
926 926
927 927 # inheritance of global permissions like create repo/fork repo etc
928 928 user_inherit_default_permissions = user.inherit_default_permissions
929 929
930 930 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
931 931 compute = caches.conditional_cache(
932 932 'short_term', 'cache_desc',
933 933 condition=cache, func=_cached_perms_data)
934 934 result = compute(user_id, scope, user_is_admin,
935 935 user_inherit_default_permissions, explicit, algo)
936 936
937 937 result_repr = []
938 938 for k in result:
939 939 result_repr.append((k, len(result[k])))
940 940
941 941 log.debug('PERMISSION tree computed %s' % (result_repr,))
942 942 return result
943 943
944 944 @property
945 945 def is_default(self):
946 946 return self.username == User.DEFAULT_USER
947 947
948 948 @property
949 949 def is_admin(self):
950 950 return self.admin
951 951
952 952 @property
953 953 def is_user_object(self):
954 954 return self.user_id is not None
955 955
956 956 @property
957 957 def repositories_admin(self):
958 958 """
959 959 Returns list of repositories you're an admin of
960 960 """
961 961 return [
962 962 x[0] for x in self.permissions['repositories'].iteritems()
963 963 if x[1] == 'repository.admin']
964 964
965 965 @property
966 966 def repository_groups_admin(self):
967 967 """
968 968 Returns list of repository groups you're an admin of
969 969 """
970 970 return [
971 971 x[0] for x in self.permissions['repositories_groups'].iteritems()
972 972 if x[1] == 'group.admin']
973 973
974 974 @property
975 975 def user_groups_admin(self):
976 976 """
977 977 Returns list of user groups you're an admin of
978 978 """
979 979 return [
980 980 x[0] for x in self.permissions['user_groups'].iteritems()
981 981 if x[1] == 'usergroup.admin']
982 982
983 983 @property
984 984 def ip_allowed(self):
985 985 """
986 986 Checks if ip_addr used in constructor is allowed from defined list of
987 987 allowed ip_addresses for user
988 988
989 989 :returns: boolean, True if ip is in allowed ip range
990 990 """
991 991 # check IP
992 992 inherit = self.inherit_default_permissions
993 993 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
994 994 inherit_from_default=inherit)
995 995 @property
996 996 def personal_repo_group(self):
997 997 return RepoGroup.get_user_personal_repo_group(self.user_id)
998 998
999 999 @classmethod
1000 1000 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1001 1001 allowed_ips = AuthUser.get_allowed_ips(
1002 1002 user_id, cache=True, inherit_from_default=inherit_from_default)
1003 1003 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1004 1004 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
1005 1005 return True
1006 1006 else:
1007 1007 log.info('Access for IP:%s forbidden, '
1008 1008 'not in %s' % (ip_addr, allowed_ips))
1009 1009 return False
1010 1010
1011 1011 def __repr__(self):
1012 1012 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1013 1013 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1014 1014
1015 1015 def set_authenticated(self, authenticated=True):
1016 1016 if self.user_id != self.anonymous_user.user_id:
1017 1017 self.is_authenticated = authenticated
1018 1018
1019 1019 def get_cookie_store(self):
1020 1020 return {
1021 1021 'username': self.username,
1022 1022 'password': md5(self.password),
1023 1023 'user_id': self.user_id,
1024 1024 'is_authenticated': self.is_authenticated
1025 1025 }
1026 1026
1027 1027 @classmethod
1028 1028 def from_cookie_store(cls, cookie_store):
1029 1029 """
1030 1030 Creates AuthUser from a cookie store
1031 1031
1032 1032 :param cls:
1033 1033 :param cookie_store:
1034 1034 """
1035 1035 user_id = cookie_store.get('user_id')
1036 1036 username = cookie_store.get('username')
1037 1037 api_key = cookie_store.get('api_key')
1038 1038 return AuthUser(user_id, api_key, username)
1039 1039
1040 1040 @classmethod
1041 1041 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1042 1042 _set = set()
1043 1043
1044 1044 if inherit_from_default:
1045 1045 default_ips = UserIpMap.query().filter(
1046 1046 UserIpMap.user == User.get_default_user(cache=True))
1047 1047 if cache:
1048 1048 default_ips = default_ips.options(
1049 1049 FromCache("sql_cache_short", "get_user_ips_default"))
1050 1050
1051 1051 # populate from default user
1052 1052 for ip in default_ips:
1053 1053 try:
1054 1054 _set.add(ip.ip_addr)
1055 1055 except ObjectDeletedError:
1056 1056 # since we use heavy caching sometimes it happens that
1057 1057 # we get deleted objects here, we just skip them
1058 1058 pass
1059 1059
1060 1060 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1061 1061 if cache:
1062 1062 user_ips = user_ips.options(
1063 1063 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1064 1064
1065 1065 for ip in user_ips:
1066 1066 try:
1067 1067 _set.add(ip.ip_addr)
1068 1068 except ObjectDeletedError:
1069 1069 # since we use heavy caching sometimes it happens that we get
1070 1070 # deleted objects here, we just skip them
1071 1071 pass
1072 1072 return _set or set(['0.0.0.0/0', '::/0'])
1073 1073
1074 1074
1075 1075 def set_available_permissions(config):
1076 1076 """
1077 1077 This function will propagate pylons globals with all available defined
1078 1078 permission given in db. We don't want to check each time from db for new
1079 1079 permissions since adding a new permission also requires application restart
1080 1080 ie. to decorate new views with the newly created permission
1081 1081
1082 1082 :param config: current pylons config instance
1083 1083
1084 1084 """
1085 1085 log.info('getting information about all available permissions')
1086 1086 try:
1087 1087 sa = meta.Session
1088 1088 all_perms = sa.query(Permission).all()
1089 1089 config['available_permissions'] = [x.permission_name for x in all_perms]
1090 1090 except Exception:
1091 1091 log.error(traceback.format_exc())
1092 1092 finally:
1093 1093 meta.Session.remove()
1094 1094
1095 1095
1096 1096 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1097 1097 """
1098 1098 Return the current authentication token, creating one if one doesn't
1099 1099 already exist and the save_if_missing flag is present.
1100 1100
1101 1101 :param session: pass in the pylons session, else we use the global ones
1102 1102 :param force_new: force to re-generate the token and store it in session
1103 1103 :param save_if_missing: save the newly generated token if it's missing in
1104 1104 session
1105 1105 """
1106 1106 if not session:
1107 1107 from pylons import session
1108 1108
1109 1109 if (csrf_token_key not in session and save_if_missing) or force_new:
1110 1110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1111 1111 session[csrf_token_key] = token
1112 1112 if hasattr(session, 'save'):
1113 1113 session.save()
1114 1114 return session.get(csrf_token_key)
1115 1115
1116 1116
1117 1117 # CHECK DECORATORS
1118 1118 class CSRFRequired(object):
1119 1119 """
1120 1120 Decorator for authenticating a form
1121 1121
1122 1122 This decorator uses an authorization token stored in the client's
1123 1123 session for prevention of certain Cross-site request forgery (CSRF)
1124 1124 attacks (See
1125 1125 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1126 1126 information).
1127 1127
1128 1128 For use with the ``webhelpers.secure_form`` helper functions.
1129 1129
1130 1130 """
1131 1131 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1132 1132 except_methods=None):
1133 1133 self.token = token
1134 1134 self.header = header
1135 1135 self.except_methods = except_methods or []
1136 1136
1137 1137 def __call__(self, func):
1138 1138 return get_cython_compat_decorator(self.__wrapper, func)
1139 1139
1140 1140 def _get_csrf(self, _request):
1141 1141 return _request.POST.get(self.token, _request.headers.get(self.header))
1142 1142
1143 1143 def check_csrf(self, _request, cur_token):
1144 1144 supplied_token = self._get_csrf(_request)
1145 1145 return supplied_token and supplied_token == cur_token
1146 1146
1147 1147 def __wrapper(self, func, *fargs, **fkwargs):
1148 1148 if request.method in self.except_methods:
1149 1149 return func(*fargs, **fkwargs)
1150 1150
1151 1151 cur_token = get_csrf_token(save_if_missing=False)
1152 1152 if self.check_csrf(request, cur_token):
1153 1153 if request.POST.get(self.token):
1154 1154 del request.POST[self.token]
1155 1155 return func(*fargs, **fkwargs)
1156 1156 else:
1157 1157 reason = 'token-missing'
1158 1158 supplied_token = self._get_csrf(request)
1159 1159 if supplied_token and cur_token != supplied_token:
1160 1160 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1161 1161 supplied_token or ''[:6])
1162 1162
1163 1163 csrf_message = \
1164 1164 ("Cross-site request forgery detected, request denied. See "
1165 1165 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1166 1166 "more information.")
1167 1167 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1168 1168 'REMOTE_ADDR:%s, HEADERS:%s' % (
1169 1169 request, reason, request.remote_addr, request.headers))
1170 1170
1171 1171 raise HTTPForbidden(explanation=csrf_message)
1172 1172
1173 1173
1174 1174 class LoginRequired(object):
1175 1175 """
1176 1176 Must be logged in to execute this function else
1177 1177 redirect to login page
1178 1178
1179 1179 :param api_access: if enabled this checks only for valid auth token
1180 1180 and grants access based on valid token
1181 1181 """
1182 1182 def __init__(self, auth_token_access=None):
1183 1183 self.auth_token_access = auth_token_access
1184 1184
1185 1185 def __call__(self, func):
1186 1186 return get_cython_compat_decorator(self.__wrapper, func)
1187 1187
1188 1188 def __wrapper(self, func, *fargs, **fkwargs):
1189 1189 from rhodecode.lib import helpers as h
1190 1190 cls = fargs[0]
1191 1191 user = cls._rhodecode_user
1192 1192 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1193 1193 log.debug('Starting login restriction checks for user: %s' % (user,))
1194 1194 # check if our IP is allowed
1195 1195 ip_access_valid = True
1196 1196 if not user.ip_allowed:
1197 1197 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1198 1198 category='warning')
1199 1199 ip_access_valid = False
1200 1200
1201 1201 # check if we used an APIKEY and it's a valid one
1202 1202 # defined white-list of controllers which API access will be enabled
1203 1203 _auth_token = request.GET.get(
1204 1204 'auth_token', '') or request.GET.get('api_key', '')
1205 1205 auth_token_access_valid = allowed_auth_token_access(
1206 1206 loc, auth_token=_auth_token)
1207 1207
1208 1208 # explicit controller is enabled or API is in our whitelist
1209 1209 if self.auth_token_access or auth_token_access_valid:
1210 1210 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1211 1211 db_user = user.get_instance()
1212 1212
1213 1213 if db_user:
1214 1214 if self.auth_token_access:
1215 1215 roles = self.auth_token_access
1216 1216 else:
1217 1217 roles = [UserApiKeys.ROLE_HTTP]
1218 1218 token_match = db_user.authenticate_by_token(
1219 1219 _auth_token, roles=roles)
1220 1220 else:
1221 1221 log.debug('Unable to fetch db instance for auth user: %s', user)
1222 1222 token_match = False
1223 1223
1224 1224 if _auth_token and token_match:
1225 1225 auth_token_access_valid = True
1226 1226 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1227 1227 else:
1228 1228 auth_token_access_valid = False
1229 1229 if not _auth_token:
1230 1230 log.debug("AUTH TOKEN *NOT* present in request")
1231 1231 else:
1232 1232 log.warning(
1233 1233 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1234 1234
1235 1235 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1236 1236 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1237 1237 else 'AUTH_TOKEN_AUTH'
1238 1238
1239 1239 if ip_access_valid and (
1240 1240 user.is_authenticated or auth_token_access_valid):
1241 1241 log.info(
1242 1242 'user %s authenticating with:%s IS authenticated on func %s'
1243 1243 % (user, reason, loc))
1244 1244
1245 1245 # update user data to check last activity
1246 1246 user.update_lastactivity()
1247 1247 Session().commit()
1248 1248 return func(*fargs, **fkwargs)
1249 1249 else:
1250 1250 log.warning(
1251 1251 'user %s authenticating with:%s NOT authenticated on '
1252 1252 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1253 1253 % (user, reason, loc, ip_access_valid,
1254 1254 auth_token_access_valid))
1255 1255 # we preserve the get PARAM
1256 1256 came_from = request.path_qs
1257 1257 log.debug('redirecting to login page with %s' % (came_from,))
1258 1258 return redirect(
1259 1259 h.route_path('login', _query={'came_from': came_from}))
1260 1260
1261 1261
1262 1262 class NotAnonymous(object):
1263 1263 """
1264 1264 Must be logged in to execute this function else
1265 1265 redirect to login page"""
1266 1266
1267 1267 def __call__(self, func):
1268 1268 return get_cython_compat_decorator(self.__wrapper, func)
1269 1269
1270 1270 def __wrapper(self, func, *fargs, **fkwargs):
1271 1271 import rhodecode.lib.helpers as h
1272 1272 cls = fargs[0]
1273 1273 self.user = cls._rhodecode_user
1274 1274
1275 1275 log.debug('Checking if user is not anonymous @%s' % cls)
1276 1276
1277 1277 anonymous = self.user.username == User.DEFAULT_USER
1278 1278
1279 1279 if anonymous:
1280 1280 came_from = request.path_qs
1281 1281 h.flash(_('You need to be a registered user to '
1282 1282 'perform this action'),
1283 1283 category='warning')
1284 1284 return redirect(
1285 1285 h.route_path('login', _query={'came_from': came_from}))
1286 1286 else:
1287 1287 return func(*fargs, **fkwargs)
1288 1288
1289 1289
1290 1290 class XHRRequired(object):
1291 1291 def __call__(self, func):
1292 1292 return get_cython_compat_decorator(self.__wrapper, func)
1293 1293
1294 1294 def __wrapper(self, func, *fargs, **fkwargs):
1295 1295 log.debug('Checking if request is XMLHttpRequest (XHR)')
1296 1296 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1297 1297 if not request.is_xhr:
1298 1298 abort(400, detail=xhr_message)
1299 1299
1300 1300 return func(*fargs, **fkwargs)
1301 1301
1302 1302
1303 1303 class HasAcceptedRepoType(object):
1304 1304 """
1305 1305 Check if requested repo is within given repo type aliases
1306 1306
1307 1307 TODO: anderson: not sure where to put this decorator
1308 1308 """
1309 1309
1310 1310 def __init__(self, *repo_type_list):
1311 1311 self.repo_type_list = set(repo_type_list)
1312 1312
1313 1313 def __call__(self, func):
1314 1314 return get_cython_compat_decorator(self.__wrapper, func)
1315 1315
1316 1316 def __wrapper(self, func, *fargs, **fkwargs):
1317 1317 import rhodecode.lib.helpers as h
1318 1318 cls = fargs[0]
1319 1319 rhodecode_repo = cls.rhodecode_repo
1320 1320
1321 1321 log.debug('%s checking repo type for %s in %s',
1322 1322 self.__class__.__name__,
1323 1323 rhodecode_repo.alias, self.repo_type_list)
1324 1324
1325 1325 if rhodecode_repo.alias in self.repo_type_list:
1326 1326 return func(*fargs, **fkwargs)
1327 1327 else:
1328 1328 h.flash(h.literal(
1329 1329 _('Action not supported for %s.' % rhodecode_repo.alias)),
1330 1330 category='warning')
1331 1331 return redirect(
1332 1332 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1333 1333
1334 1334
1335 1335 class PermsDecorator(object):
1336 1336 """
1337 1337 Base class for controller decorators, we extract the current user from
1338 1338 the class itself, which has it stored in base controllers
1339 1339 """
1340 1340
1341 1341 def __init__(self, *required_perms):
1342 1342 self.required_perms = set(required_perms)
1343 1343
1344 1344 def __call__(self, func):
1345 1345 return get_cython_compat_decorator(self.__wrapper, func)
1346 1346
1347 1347 def _get_request(self):
1348 1348 from pyramid.threadlocal import get_current_request
1349 1349 pyramid_request = get_current_request()
1350 1350 if not pyramid_request:
1351 1351 # return global request of pylons in case pyramid isn't available
1352 1352 return request
1353 1353 return pyramid_request
1354 1354
1355 1355 def _get_came_from(self):
1356 1356 _request = self._get_request()
1357 1357
1358 1358 # both pylons/pyramid has this attribute
1359 1359 return _request.path_qs
1360 1360
1361 1361 def __wrapper(self, func, *fargs, **fkwargs):
1362 1362 import rhodecode.lib.helpers as h
1363 1363 cls = fargs[0]
1364 1364 _user = cls._rhodecode_user
1365 1365
1366 1366 log.debug('checking %s permissions %s for %s %s',
1367 1367 self.__class__.__name__, self.required_perms, cls, _user)
1368 1368
1369 1369 if self.check_permissions(_user):
1370 1370 log.debug('Permission granted for %s %s', cls, _user)
1371 1371 return func(*fargs, **fkwargs)
1372 1372
1373 1373 else:
1374 1374 log.debug('Permission denied for %s %s', cls, _user)
1375 1375 anonymous = _user.username == User.DEFAULT_USER
1376 1376
1377 1377 if anonymous:
1378 1378 came_from = self._get_came_from()
1379 1379 h.flash(_('You need to be signed in to view this page'),
1380 1380 category='warning')
1381 1381 raise HTTPFound(
1382 1382 h.route_path('login', _query={'came_from': came_from}))
1383 1383
1384 1384 else:
1385 1385 # redirect with forbidden ret code
1386 1386 raise HTTPForbidden()
1387 1387
1388 1388 def check_permissions(self, user):
1389 1389 """Dummy function for overriding"""
1390 1390 raise NotImplementedError(
1391 1391 'You have to write this function in child class')
1392 1392
1393 1393
1394 1394 class HasPermissionAllDecorator(PermsDecorator):
1395 1395 """
1396 1396 Checks for access permission for all given predicates. All of them
1397 1397 have to be meet in order to fulfill the request
1398 1398 """
1399 1399
1400 1400 def check_permissions(self, user):
1401 1401 perms = user.permissions_with_scope({})
1402 1402 if self.required_perms.issubset(perms['global']):
1403 1403 return True
1404 1404 return False
1405 1405
1406 1406
1407 1407 class HasPermissionAnyDecorator(PermsDecorator):
1408 1408 """
1409 1409 Checks for access permission for any of given predicates. In order to
1410 1410 fulfill the request any of predicates must be meet
1411 1411 """
1412 1412
1413 1413 def check_permissions(self, user):
1414 1414 perms = user.permissions_with_scope({})
1415 1415 if self.required_perms.intersection(perms['global']):
1416 1416 return True
1417 1417 return False
1418 1418
1419 1419
1420 1420 class HasRepoPermissionAllDecorator(PermsDecorator):
1421 1421 """
1422 1422 Checks for access permission for all given predicates for specific
1423 1423 repository. All of them have to be meet in order to fulfill the request
1424 1424 """
1425 1425 def _get_repo_name(self):
1426 1426 _request = self._get_request()
1427 1427 return get_repo_slug(_request)
1428 1428
1429 1429 def check_permissions(self, user):
1430 1430 perms = user.permissions
1431 1431 repo_name = self._get_repo_name()
1432
1432 1433 try:
1433 1434 user_perms = set([perms['repositories'][repo_name]])
1434 1435 except KeyError:
1436 log.debug('cannot locate repo with name: `%s` in permissions defs',
1437 repo_name)
1435 1438 return False
1439
1440 log.debug('checking `%s` permissions for repo `%s`',
1441 user_perms, repo_name)
1436 1442 if self.required_perms.issubset(user_perms):
1437 1443 return True
1438 1444 return False
1439 1445
1440 1446
1441 1447 class HasRepoPermissionAnyDecorator(PermsDecorator):
1442 1448 """
1443 1449 Checks for access permission for any of given predicates for specific
1444 1450 repository. In order to fulfill the request any of predicates must be meet
1445 1451 """
1446 1452 def _get_repo_name(self):
1447 1453 _request = self._get_request()
1448 1454 return get_repo_slug(_request)
1449 1455
1450 1456 def check_permissions(self, user):
1451 1457 perms = user.permissions
1452 1458 repo_name = self._get_repo_name()
1459
1453 1460 try:
1454 1461 user_perms = set([perms['repositories'][repo_name]])
1455 1462 except KeyError:
1463 log.debug('cannot locate repo with name: `%s` in permissions defs',
1464 repo_name)
1456 1465 return False
1457 1466
1467 log.debug('checking `%s` permissions for repo `%s`',
1468 user_perms, repo_name)
1458 1469 if self.required_perms.intersection(user_perms):
1459 1470 return True
1460 1471 return False
1461 1472
1462 1473
1463 1474 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1464 1475 """
1465 1476 Checks for access permission for all given predicates for specific
1466 1477 repository group. All of them have to be meet in order to
1467 1478 fulfill the request
1468 1479 """
1469 1480 def _get_repo_group_name(self):
1470 1481 _request = self._get_request()
1471 1482 return get_repo_group_slug(_request)
1472 1483
1473 1484 def check_permissions(self, user):
1474 1485 perms = user.permissions
1475 1486 group_name = self._get_repo_group_name()
1476 1487 try:
1477 1488 user_perms = set([perms['repositories_groups'][group_name]])
1478 1489 except KeyError:
1490 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1491 group_name)
1479 1492 return False
1480 1493
1494 log.debug('checking `%s` permissions for repo group `%s`',
1495 user_perms, group_name)
1481 1496 if self.required_perms.issubset(user_perms):
1482 1497 return True
1483 1498 return False
1484 1499
1485 1500
1486 1501 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1487 1502 """
1488 1503 Checks for access permission for any of given predicates for specific
1489 1504 repository group. In order to fulfill the request any
1490 1505 of predicates must be met
1491 1506 """
1492 1507 def _get_repo_group_name(self):
1493 1508 _request = self._get_request()
1494 1509 return get_repo_group_slug(_request)
1495 1510
1496 1511 def check_permissions(self, user):
1497 1512 perms = user.permissions
1498 1513 group_name = self._get_repo_group_name()
1514
1499 1515 try:
1500 1516 user_perms = set([perms['repositories_groups'][group_name]])
1501 1517 except KeyError:
1518 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1519 group_name)
1502 1520 return False
1503 1521
1522 log.debug('checking `%s` permissions for repo group `%s`',
1523 user_perms, group_name)
1504 1524 if self.required_perms.intersection(user_perms):
1505 1525 return True
1506 1526 return False
1507 1527
1508 1528
1509 1529 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1510 1530 """
1511 1531 Checks for access permission for all given predicates for specific
1512 1532 user group. All of them have to be meet in order to fulfill the request
1513 1533 """
1514 1534 def _get_user_group_name(self):
1515 1535 _request = self._get_request()
1516 1536 return get_user_group_slug(_request)
1517 1537
1518 1538 def check_permissions(self, user):
1519 1539 perms = user.permissions
1520 1540 group_name = self._get_user_group_name()
1521 1541 try:
1522 1542 user_perms = set([perms['user_groups'][group_name]])
1523 1543 except KeyError:
1524 1544 return False
1525 1545
1526 1546 if self.required_perms.issubset(user_perms):
1527 1547 return True
1528 1548 return False
1529 1549
1530 1550
1531 1551 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1532 1552 """
1533 1553 Checks for access permission for any of given predicates for specific
1534 1554 user group. In order to fulfill the request any of predicates must be meet
1535 1555 """
1536 1556 def _get_user_group_name(self):
1537 1557 _request = self._get_request()
1538 1558 return get_user_group_slug(_request)
1539 1559
1540 1560 def check_permissions(self, user):
1541 1561 perms = user.permissions
1542 1562 group_name = self._get_user_group_name()
1543 1563 try:
1544 1564 user_perms = set([perms['user_groups'][group_name]])
1545 1565 except KeyError:
1546 1566 return False
1547 1567
1548 1568 if self.required_perms.intersection(user_perms):
1549 1569 return True
1550 1570 return False
1551 1571
1552 1572
1553 1573 # CHECK FUNCTIONS
1554 1574 class PermsFunction(object):
1555 1575 """Base function for other check functions"""
1556 1576
1557 1577 def __init__(self, *perms):
1558 1578 self.required_perms = set(perms)
1559 1579 self.repo_name = None
1560 1580 self.repo_group_name = None
1561 1581 self.user_group_name = None
1562 1582
1563 1583 def __bool__(self):
1564 1584 frame = inspect.currentframe()
1565 1585 stack_trace = traceback.format_stack(frame)
1566 1586 log.error('Checking bool value on a class instance of perm '
1567 1587 'function is not allowed: %s' % ''.join(stack_trace))
1568 1588 # rather than throwing errors, here we always return False so if by
1569 1589 # accident someone checks truth for just an instance it will always end
1570 1590 # up in returning False
1571 1591 return False
1572 1592 __nonzero__ = __bool__
1573 1593
1574 1594 def __call__(self, check_location='', user=None):
1575 1595 if not user:
1576 1596 log.debug('Using user attribute from global request')
1577 1597 # TODO: remove this someday,put as user as attribute here
1578 1598 user = request.user
1579 1599
1580 1600 # init auth user if not already given
1581 1601 if not isinstance(user, AuthUser):
1582 1602 log.debug('Wrapping user %s into AuthUser', user)
1583 1603 user = AuthUser(user.user_id)
1584 1604
1585 1605 cls_name = self.__class__.__name__
1586 1606 check_scope = self._get_check_scope(cls_name)
1587 1607 check_location = check_location or 'unspecified location'
1588 1608
1589 1609 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1590 1610 self.required_perms, user, check_scope, check_location)
1591 1611 if not user:
1592 1612 log.warning('Empty user given for permission check')
1593 1613 return False
1594 1614
1595 1615 if self.check_permissions(user):
1596 1616 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1597 1617 check_scope, user, check_location)
1598 1618 return True
1599 1619
1600 1620 else:
1601 1621 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1602 1622 check_scope, user, check_location)
1603 1623 return False
1604 1624
1605 1625 def _get_request(self):
1606 1626 from pyramid.threadlocal import get_current_request
1607 1627 pyramid_request = get_current_request()
1608 1628 if not pyramid_request:
1609 1629 # return global request of pylons incase pyramid one isn't available
1610 1630 return request
1611 1631 return pyramid_request
1612 1632
1613 1633 def _get_check_scope(self, cls_name):
1614 1634 return {
1615 1635 'HasPermissionAll': 'GLOBAL',
1616 1636 'HasPermissionAny': 'GLOBAL',
1617 1637 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1618 1638 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1619 1639 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1620 1640 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1621 1641 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1622 1642 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1623 1643 }.get(cls_name, '?:%s' % cls_name)
1624 1644
1625 1645 def check_permissions(self, user):
1626 1646 """Dummy function for overriding"""
1627 1647 raise Exception('You have to write this function in child class')
1628 1648
1629 1649
1630 1650 class HasPermissionAll(PermsFunction):
1631 1651 def check_permissions(self, user):
1632 1652 perms = user.permissions_with_scope({})
1633 1653 if self.required_perms.issubset(perms.get('global')):
1634 1654 return True
1635 1655 return False
1636 1656
1637 1657
1638 1658 class HasPermissionAny(PermsFunction):
1639 1659 def check_permissions(self, user):
1640 1660 perms = user.permissions_with_scope({})
1641 1661 if self.required_perms.intersection(perms.get('global')):
1642 1662 return True
1643 1663 return False
1644 1664
1645 1665
1646 1666 class HasRepoPermissionAll(PermsFunction):
1647 1667 def __call__(self, repo_name=None, check_location='', user=None):
1648 1668 self.repo_name = repo_name
1649 1669 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1650 1670
1651 1671 def _get_repo_name(self):
1652 1672 if not self.repo_name:
1653 1673 _request = self._get_request()
1654 1674 self.repo_name = get_repo_slug(_request)
1655 1675 return self.repo_name
1656 1676
1657 1677 def check_permissions(self, user):
1658 1678 self.repo_name = self._get_repo_name()
1659 1679 perms = user.permissions
1660 1680 try:
1661 1681 user_perms = set([perms['repositories'][self.repo_name]])
1662 1682 except KeyError:
1663 1683 return False
1664 1684 if self.required_perms.issubset(user_perms):
1665 1685 return True
1666 1686 return False
1667 1687
1668 1688
1669 1689 class HasRepoPermissionAny(PermsFunction):
1670 1690 def __call__(self, repo_name=None, check_location='', user=None):
1671 1691 self.repo_name = repo_name
1672 1692 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1673 1693
1674 1694 def _get_repo_name(self):
1675 1695 if not self.repo_name:
1676 1696 self.repo_name = get_repo_slug(request)
1677 1697 return self.repo_name
1678 1698
1679 1699 def check_permissions(self, user):
1680 1700 self.repo_name = self._get_repo_name()
1681 1701 perms = user.permissions
1682 1702 try:
1683 1703 user_perms = set([perms['repositories'][self.repo_name]])
1684 1704 except KeyError:
1685 1705 return False
1686 1706 if self.required_perms.intersection(user_perms):
1687 1707 return True
1688 1708 return False
1689 1709
1690 1710
1691 1711 class HasRepoGroupPermissionAny(PermsFunction):
1692 1712 def __call__(self, group_name=None, check_location='', user=None):
1693 1713 self.repo_group_name = group_name
1694 1714 return super(HasRepoGroupPermissionAny, self).__call__(
1695 1715 check_location, user)
1696 1716
1697 1717 def check_permissions(self, user):
1698 1718 perms = user.permissions
1699 1719 try:
1700 1720 user_perms = set(
1701 1721 [perms['repositories_groups'][self.repo_group_name]])
1702 1722 except KeyError:
1703 1723 return False
1704 1724 if self.required_perms.intersection(user_perms):
1705 1725 return True
1706 1726 return False
1707 1727
1708 1728
1709 1729 class HasRepoGroupPermissionAll(PermsFunction):
1710 1730 def __call__(self, group_name=None, check_location='', user=None):
1711 1731 self.repo_group_name = group_name
1712 1732 return super(HasRepoGroupPermissionAll, self).__call__(
1713 1733 check_location, user)
1714 1734
1715 1735 def check_permissions(self, user):
1716 1736 perms = user.permissions
1717 1737 try:
1718 1738 user_perms = set(
1719 1739 [perms['repositories_groups'][self.repo_group_name]])
1720 1740 except KeyError:
1721 1741 return False
1722 1742 if self.required_perms.issubset(user_perms):
1723 1743 return True
1724 1744 return False
1725 1745
1726 1746
1727 1747 class HasUserGroupPermissionAny(PermsFunction):
1728 1748 def __call__(self, user_group_name=None, check_location='', user=None):
1729 1749 self.user_group_name = user_group_name
1730 1750 return super(HasUserGroupPermissionAny, self).__call__(
1731 1751 check_location, user)
1732 1752
1733 1753 def check_permissions(self, user):
1734 1754 perms = user.permissions
1735 1755 try:
1736 1756 user_perms = set([perms['user_groups'][self.user_group_name]])
1737 1757 except KeyError:
1738 1758 return False
1739 1759 if self.required_perms.intersection(user_perms):
1740 1760 return True
1741 1761 return False
1742 1762
1743 1763
1744 1764 class HasUserGroupPermissionAll(PermsFunction):
1745 1765 def __call__(self, user_group_name=None, check_location='', user=None):
1746 1766 self.user_group_name = user_group_name
1747 1767 return super(HasUserGroupPermissionAll, self).__call__(
1748 1768 check_location, user)
1749 1769
1750 1770 def check_permissions(self, user):
1751 1771 perms = user.permissions
1752 1772 try:
1753 1773 user_perms = set([perms['user_groups'][self.user_group_name]])
1754 1774 except KeyError:
1755 1775 return False
1756 1776 if self.required_perms.issubset(user_perms):
1757 1777 return True
1758 1778 return False
1759 1779
1760 1780
1761 1781 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1762 1782 class HasPermissionAnyMiddleware(object):
1763 1783 def __init__(self, *perms):
1764 1784 self.required_perms = set(perms)
1765 1785
1766 1786 def __call__(self, user, repo_name):
1767 1787 # repo_name MUST be unicode, since we handle keys in permission
1768 1788 # dict by unicode
1769 1789 repo_name = safe_unicode(repo_name)
1770 1790 user = AuthUser(user.user_id)
1771 1791 log.debug(
1772 1792 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1773 1793 self.required_perms, user, repo_name)
1774 1794
1775 1795 if self.check_permissions(user, repo_name):
1776 1796 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1777 1797 repo_name, user, 'PermissionMiddleware')
1778 1798 return True
1779 1799
1780 1800 else:
1781 1801 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1782 1802 repo_name, user, 'PermissionMiddleware')
1783 1803 return False
1784 1804
1785 1805 def check_permissions(self, user, repo_name):
1786 1806 perms = user.permissions_with_scope({'repo_name': repo_name})
1787 1807
1788 1808 try:
1789 1809 user_perms = set([perms['repositories'][repo_name]])
1790 1810 except Exception:
1791 1811 log.exception('Error while accessing user permissions')
1792 1812 return False
1793 1813
1794 1814 if self.required_perms.intersection(user_perms):
1795 1815 return True
1796 1816 return False
1797 1817
1798 1818
1799 1819 # SPECIAL VERSION TO HANDLE API AUTH
1800 1820 class _BaseApiPerm(object):
1801 1821 def __init__(self, *perms):
1802 1822 self.required_perms = set(perms)
1803 1823
1804 1824 def __call__(self, check_location=None, user=None, repo_name=None,
1805 1825 group_name=None, user_group_name=None):
1806 1826 cls_name = self.__class__.__name__
1807 1827 check_scope = 'global:%s' % (self.required_perms,)
1808 1828 if repo_name:
1809 1829 check_scope += ', repo_name:%s' % (repo_name,)
1810 1830
1811 1831 if group_name:
1812 1832 check_scope += ', repo_group_name:%s' % (group_name,)
1813 1833
1814 1834 if user_group_name:
1815 1835 check_scope += ', user_group_name:%s' % (user_group_name,)
1816 1836
1817 1837 log.debug(
1818 1838 'checking cls:%s %s %s @ %s'
1819 1839 % (cls_name, self.required_perms, check_scope, check_location))
1820 1840 if not user:
1821 1841 log.debug('Empty User passed into arguments')
1822 1842 return False
1823 1843
1824 1844 # process user
1825 1845 if not isinstance(user, AuthUser):
1826 1846 user = AuthUser(user.user_id)
1827 1847 if not check_location:
1828 1848 check_location = 'unspecified'
1829 1849 if self.check_permissions(user.permissions, repo_name, group_name,
1830 1850 user_group_name):
1831 1851 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1832 1852 check_scope, user, check_location)
1833 1853 return True
1834 1854
1835 1855 else:
1836 1856 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1837 1857 check_scope, user, check_location)
1838 1858 return False
1839 1859
1840 1860 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1841 1861 user_group_name=None):
1842 1862 """
1843 1863 implement in child class should return True if permissions are ok,
1844 1864 False otherwise
1845 1865
1846 1866 :param perm_defs: dict with permission definitions
1847 1867 :param repo_name: repo name
1848 1868 """
1849 1869 raise NotImplementedError()
1850 1870
1851 1871
1852 1872 class HasPermissionAllApi(_BaseApiPerm):
1853 1873 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1854 1874 user_group_name=None):
1855 1875 if self.required_perms.issubset(perm_defs.get('global')):
1856 1876 return True
1857 1877 return False
1858 1878
1859 1879
1860 1880 class HasPermissionAnyApi(_BaseApiPerm):
1861 1881 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1862 1882 user_group_name=None):
1863 1883 if self.required_perms.intersection(perm_defs.get('global')):
1864 1884 return True
1865 1885 return False
1866 1886
1867 1887
1868 1888 class HasRepoPermissionAllApi(_BaseApiPerm):
1869 1889 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1870 1890 user_group_name=None):
1871 1891 try:
1872 1892 _user_perms = set([perm_defs['repositories'][repo_name]])
1873 1893 except KeyError:
1874 1894 log.warning(traceback.format_exc())
1875 1895 return False
1876 1896 if self.required_perms.issubset(_user_perms):
1877 1897 return True
1878 1898 return False
1879 1899
1880 1900
1881 1901 class HasRepoPermissionAnyApi(_BaseApiPerm):
1882 1902 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1883 1903 user_group_name=None):
1884 1904 try:
1885 1905 _user_perms = set([perm_defs['repositories'][repo_name]])
1886 1906 except KeyError:
1887 1907 log.warning(traceback.format_exc())
1888 1908 return False
1889 1909 if self.required_perms.intersection(_user_perms):
1890 1910 return True
1891 1911 return False
1892 1912
1893 1913
1894 1914 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1895 1915 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1896 1916 user_group_name=None):
1897 1917 try:
1898 1918 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1899 1919 except KeyError:
1900 1920 log.warning(traceback.format_exc())
1901 1921 return False
1902 1922 if self.required_perms.intersection(_user_perms):
1903 1923 return True
1904 1924 return False
1905 1925
1906 1926
1907 1927 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1908 1928 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1909 1929 user_group_name=None):
1910 1930 try:
1911 1931 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1912 1932 except KeyError:
1913 1933 log.warning(traceback.format_exc())
1914 1934 return False
1915 1935 if self.required_perms.issubset(_user_perms):
1916 1936 return True
1917 1937 return False
1918 1938
1919 1939
1920 1940 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1921 1941 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1922 1942 user_group_name=None):
1923 1943 try:
1924 1944 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1925 1945 except KeyError:
1926 1946 log.warning(traceback.format_exc())
1927 1947 return False
1928 1948 if self.required_perms.intersection(_user_perms):
1929 1949 return True
1930 1950 return False
1931 1951
1932 1952
1933 1953 def check_ip_access(source_ip, allowed_ips=None):
1934 1954 """
1935 1955 Checks if source_ip is a subnet of any of allowed_ips.
1936 1956
1937 1957 :param source_ip:
1938 1958 :param allowed_ips: list of allowed ips together with mask
1939 1959 """
1940 1960 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1941 1961 source_ip_address = ipaddress.ip_address(source_ip)
1942 1962 if isinstance(allowed_ips, (tuple, list, set)):
1943 1963 for ip in allowed_ips:
1944 1964 try:
1945 1965 network_address = ipaddress.ip_network(ip, strict=False)
1946 1966 if source_ip_address in network_address:
1947 1967 log.debug('IP %s is network %s' %
1948 1968 (source_ip_address, network_address))
1949 1969 return True
1950 1970 # for any case we cannot determine the IP, don't crash just
1951 1971 # skip it and log as error, we want to say forbidden still when
1952 1972 # sending bad IP
1953 1973 except Exception:
1954 1974 log.error(traceback.format_exc())
1955 1975 continue
1956 1976 return False
1957 1977
1958 1978
1959 1979 def get_cython_compat_decorator(wrapper, func):
1960 1980 """
1961 1981 Creates a cython compatible decorator. The previously used
1962 1982 decorator.decorator() function seems to be incompatible with cython.
1963 1983
1964 1984 :param wrapper: __wrapper method of the decorator class
1965 1985 :param func: decorated function
1966 1986 """
1967 1987 @wraps(func)
1968 1988 def local_wrapper(*args, **kwds):
1969 1989 return wrapper(func, *args, **kwds)
1970 1990 local_wrapper.__wrapped__ = func
1971 1991 return local_wrapper
1972 1992
1973 1993
@@ -1,594 +1,594 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import ipaddress
31 31 import pyramid.threadlocal
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36 from pylons import config, tmpl_context as c, request, session, url
37 37 from pylons.controllers import WSGIController
38 38 from pylons.controllers.util import redirect
39 39 from pylons.i18n import translation
40 40 # marcink: don't remove this import
41 41 from pylons.templating import render_mako as render # noqa
42 42 from pylons.i18n.translation import _
43 43 from webob.exc import HTTPFound
44 44
45 45
46 46 import rhodecode
47 47 from rhodecode.authentication.base import VCS_TYPE
48 48 from rhodecode.lib import auth, utils2
49 49 from rhodecode.lib import helpers as h
50 50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 51 from rhodecode.lib.exceptions import UserCreationError
52 52 from rhodecode.lib.utils import (
53 53 get_repo_slug, set_rhodecode_config, password_changed,
54 54 get_enabled_hook_classes)
55 55 from rhodecode.lib.utils2 import (
56 56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 58 from rhodecode.model import meta
59 59 from rhodecode.model.db import Repository, User, ChangesetComment
60 60 from rhodecode.model.notification import NotificationModel
61 61 from rhodecode.model.scm import ScmModel
62 62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 def _filter_proxy(ip):
69 69 """
70 70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 71 ips. Those comma separated IPs are passed from various proxies in the
72 72 chain of request processing. The left-most being the original client.
73 73 We only care about the first IP which came from the org. client.
74 74
75 75 :param ip: ip string from headers
76 76 """
77 77 if ',' in ip:
78 78 _ips = ip.split(',')
79 79 _first_ip = _ips[0].strip()
80 80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 81 return _first_ip
82 82 return ip
83 83
84 84
85 85 def _filter_port(ip):
86 86 """
87 87 Removes a port from ip, there are 4 main cases to handle here.
88 88 - ipv4 eg. 127.0.0.1
89 89 - ipv6 eg. ::1
90 90 - ipv4+port eg. 127.0.0.1:8080
91 91 - ipv6+port eg. [::1]:8080
92 92
93 93 :param ip:
94 94 """
95 95 def is_ipv6(ip_addr):
96 96 if hasattr(socket, 'inet_pton'):
97 97 try:
98 98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 99 except socket.error:
100 100 return False
101 101 else:
102 102 # fallback to ipaddress
103 103 try:
104 104 ipaddress.IPv6Address(ip_addr)
105 105 except Exception:
106 106 return False
107 107 return True
108 108
109 109 if ':' not in ip: # must be ipv4 pure ip
110 110 return ip
111 111
112 112 if '[' in ip and ']' in ip: # ipv6 with port
113 113 return ip.split(']')[0][1:].lower()
114 114
115 115 # must be ipv6 or ipv4 with port
116 116 if is_ipv6(ip):
117 117 return ip
118 118 else:
119 119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 120 return ip
121 121
122 122
123 123 def get_ip_addr(environ):
124 124 proxy_key = 'HTTP_X_REAL_IP'
125 125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 126 def_key = 'REMOTE_ADDR'
127 127 _filters = lambda x: _filter_port(_filter_proxy(x))
128 128
129 129 ip = environ.get(proxy_key)
130 130 if ip:
131 131 return _filters(ip)
132 132
133 133 ip = environ.get(proxy_key2)
134 134 if ip:
135 135 return _filters(ip)
136 136
137 137 ip = environ.get(def_key, '0.0.0.0')
138 138 return _filters(ip)
139 139
140 140
141 141 def get_server_ip_addr(environ, log_errors=True):
142 142 hostname = environ.get('SERVER_NAME')
143 143 try:
144 144 return socket.gethostbyname(hostname)
145 145 except Exception as e:
146 146 if log_errors:
147 147 # in some cases this lookup is not possible, and we don't want to
148 148 # make it an exception in logs
149 149 log.exception('Could not retrieve server ip address: %s', e)
150 150 return hostname
151 151
152 152
153 153 def get_server_port(environ):
154 154 return environ.get('SERVER_PORT')
155 155
156 156
157 157 def get_access_path(environ):
158 158 path = environ.get('PATH_INFO')
159 159 org_req = environ.get('pylons.original_request')
160 160 if org_req:
161 161 path = org_req.environ.get('PATH_INFO')
162 162 return path
163 163
164 164
165 165 def get_user_agent(environ):
166 166 return environ.get('HTTP_USER_AGENT')
167 167
168 168
169 169 def vcs_operation_context(
170 170 environ, repo_name, username, action, scm, check_locking=True,
171 171 is_shadow_repo=False):
172 172 """
173 173 Generate the context for a vcs operation, e.g. push or pull.
174 174
175 175 This context is passed over the layers so that hooks triggered by the
176 176 vcs operation know details like the user, the user's IP address etc.
177 177
178 178 :param check_locking: Allows to switch of the computation of the locking
179 179 data. This serves mainly the need of the simplevcs middleware to be
180 180 able to disable this for certain operations.
181 181
182 182 """
183 183 # Tri-state value: False: unlock, None: nothing, True: lock
184 184 make_lock = None
185 185 locked_by = [None, None, None]
186 186 is_anonymous = username == User.DEFAULT_USER
187 187 if not is_anonymous and check_locking:
188 188 log.debug('Checking locking on repository "%s"', repo_name)
189 189 user = User.get_by_username(username)
190 190 repo = Repository.get_by_repo_name(repo_name)
191 191 make_lock, __, locked_by = repo.get_locking_state(
192 192 action, user.user_id)
193 193
194 194 settings_model = VcsSettingsModel(repo=repo_name)
195 195 ui_settings = settings_model.get_ui_settings()
196 196
197 197 extras = {
198 198 'ip': get_ip_addr(environ),
199 199 'username': username,
200 200 'action': action,
201 201 'repository': repo_name,
202 202 'scm': scm,
203 203 'config': rhodecode.CONFIG['__file__'],
204 204 'make_lock': make_lock,
205 205 'locked_by': locked_by,
206 206 'server_url': utils2.get_server_url(environ),
207 207 'user_agent': get_user_agent(environ),
208 208 'hooks': get_enabled_hook_classes(ui_settings),
209 209 'is_shadow_repo': is_shadow_repo,
210 210 }
211 211 return extras
212 212
213 213
214 214 class BasicAuth(AuthBasicAuthenticator):
215 215
216 216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 217 initial_call_detection=False, acl_repo_name=None):
218 218 self.realm = realm
219 219 self.initial_call = initial_call_detection
220 220 self.authfunc = authfunc
221 221 self.registry = registry
222 222 self.acl_repo_name = acl_repo_name
223 223 self._rc_auth_http_code = auth_http_code
224 224
225 225 def _get_response_from_code(self, http_code):
226 226 try:
227 227 return get_exception(safe_int(http_code))
228 228 except Exception:
229 229 log.exception('Failed to fetch response for code %s' % http_code)
230 230 return HTTPForbidden
231 231
232 232 def build_authentication(self):
233 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 234 if self._rc_auth_http_code and not self.initial_call:
235 235 # return alternative HTTP code if alternative http return code
236 236 # is specified in RhodeCode config, but ONLY if it's not the
237 237 # FIRST call
238 238 custom_response_klass = self._get_response_from_code(
239 239 self._rc_auth_http_code)
240 240 return custom_response_klass(headers=head)
241 241 return HTTPUnauthorized(headers=head)
242 242
243 243 def authenticate(self, environ):
244 244 authorization = AUTHORIZATION(environ)
245 245 if not authorization:
246 246 return self.build_authentication()
247 247 (authmeth, auth) = authorization.split(' ', 1)
248 248 if 'basic' != authmeth.lower():
249 249 return self.build_authentication()
250 250 auth = auth.strip().decode('base64')
251 251 _parts = auth.split(':', 1)
252 252 if len(_parts) == 2:
253 253 username, password = _parts
254 254 if self.authfunc(
255 255 username, password, environ, VCS_TYPE,
256 256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 257 return username
258 258 if username and password:
259 259 # we mark that we actually executed authentication once, at
260 260 # that point we can use the alternative auth code
261 261 self.initial_call = False
262 262
263 263 return self.build_authentication()
264 264
265 265 __call__ = authenticate
266 266
267 267
268 268 def attach_context_attributes(context, request):
269 269 """
270 270 Attach variables into template context called `c`, please note that
271 271 request could be pylons or pyramid request in here.
272 272 """
273 273 rc_config = SettingsModel().get_all_settings(cache=True)
274 274
275 275 context.rhodecode_version = rhodecode.__version__
276 276 context.rhodecode_edition = config.get('rhodecode.edition')
277 277 # unique secret + version does not leak the version but keep consistency
278 278 context.rhodecode_version_hash = md5(
279 279 config.get('beaker.session.secret', '') +
280 280 rhodecode.__version__)[:8]
281 281
282 282 # Default language set for the incoming request
283 283 context.language = translation.get_lang()[0]
284 284
285 285 # Visual options
286 286 context.visual = AttributeDict({})
287 287
288 288 # DB stored Visual Items
289 289 context.visual.show_public_icon = str2bool(
290 290 rc_config.get('rhodecode_show_public_icon'))
291 291 context.visual.show_private_icon = str2bool(
292 292 rc_config.get('rhodecode_show_private_icon'))
293 293 context.visual.stylify_metatags = str2bool(
294 294 rc_config.get('rhodecode_stylify_metatags'))
295 295 context.visual.dashboard_items = safe_int(
296 296 rc_config.get('rhodecode_dashboard_items', 100))
297 297 context.visual.admin_grid_items = safe_int(
298 298 rc_config.get('rhodecode_admin_grid_items', 100))
299 299 context.visual.repository_fields = str2bool(
300 300 rc_config.get('rhodecode_repository_fields'))
301 301 context.visual.show_version = str2bool(
302 302 rc_config.get('rhodecode_show_version'))
303 303 context.visual.use_gravatar = str2bool(
304 304 rc_config.get('rhodecode_use_gravatar'))
305 305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 306 context.visual.default_renderer = rc_config.get(
307 307 'rhodecode_markup_renderer', 'rst')
308 308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 309 context.visual.rhodecode_support_url = \
310 310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311 311
312 312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 313 context.post_code = rc_config.get('rhodecode_post_code')
314 314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 316 # if we have specified default_encoding in the request, it has more
317 317 # priority
318 318 if request.GET.get('default_encoding'):
319 319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321 321
322 322 # INI stored
323 323 context.labs_active = str2bool(
324 324 config.get('labs_settings_active', 'false'))
325 325 context.visual.allow_repo_location_change = str2bool(
326 326 config.get('allow_repo_location_change', True))
327 327 context.visual.allow_custom_hooks_settings = str2bool(
328 328 config.get('allow_custom_hooks_settings', True))
329 329 context.debug_style = str2bool(config.get('debug_style', False))
330 330
331 331 context.rhodecode_instanceid = config.get('instance_id')
332 332
333 333 # AppEnlight
334 334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
335 335 context.appenlight_api_public_key = config.get(
336 336 'appenlight.api_public_key', '')
337 337 context.appenlight_server_url = config.get('appenlight.server_url', '')
338 338
339 339 # JS template context
340 340 context.template_context = {
341 341 'repo_name': None,
342 342 'repo_type': None,
343 343 'repo_landing_commit': None,
344 344 'rhodecode_user': {
345 345 'username': None,
346 346 'email': None,
347 347 'notification_status': False
348 348 },
349 349 'visual': {
350 350 'default_renderer': None
351 351 },
352 352 'commit_data': {
353 353 'commit_id': None
354 354 },
355 355 'pull_request_data': {'pull_request_id': None},
356 356 'timeago': {
357 357 'refresh_time': 120 * 1000,
358 358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
359 359 },
360 360 'pylons_dispatch': {
361 361 # 'controller': request.environ['pylons.routes_dict']['controller'],
362 362 # 'action': request.environ['pylons.routes_dict']['action'],
363 363 },
364 364 'pyramid_dispatch': {
365 365
366 366 },
367 367 'extra': {'plugins': {}}
368 368 }
369 369 # END CONFIG VARS
370 370
371 371 # TODO: This dosn't work when called from pylons compatibility tween.
372 372 # Fix this and remove it from base controller.
373 373 # context.repo_name = get_repo_slug(request) # can be empty
374 374
375 375 diffmode = 'sideside'
376 376 if request.GET.get('diffmode'):
377 377 if request.GET['diffmode'] == 'unified':
378 378 diffmode = 'unified'
379 379 elif request.session.get('diffmode'):
380 380 diffmode = request.session['diffmode']
381 381
382 382 context.diffmode = diffmode
383 383
384 384 if request.session.get('diffmode') != diffmode:
385 385 request.session['diffmode'] = diffmode
386 386
387 387 context.csrf_token = auth.get_csrf_token()
388 388 context.backends = rhodecode.BACKENDS.keys()
389 389 context.backends.sort()
390 390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
391 391 context.rhodecode_user.user_id)
392 392
393 393 context.pyramid_request = pyramid.threadlocal.get_current_request()
394 394
395 395
396 396 def get_auth_user(environ):
397 397 ip_addr = get_ip_addr(environ)
398 398 # make sure that we update permissions each time we call controller
399 399 _auth_token = (request.GET.get('auth_token', '') or
400 400 request.GET.get('api_key', ''))
401 401
402 402 if _auth_token:
403 403 # when using API_KEY we assume user exists, and
404 404 # doesn't need auth based on cookies.
405 405 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
406 406 authenticated = False
407 407 else:
408 408 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
409 409 try:
410 410 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
411 411 ip_addr=ip_addr)
412 412 except UserCreationError as e:
413 413 h.flash(e, 'error')
414 414 # container auth or other auth functions that create users
415 415 # on the fly can throw this exception signaling that there's
416 416 # issue with user creation, explanation should be provided
417 417 # in Exception itself. We then create a simple blank
418 418 # AuthUser
419 419 auth_user = AuthUser(ip_addr=ip_addr)
420 420
421 421 if password_changed(auth_user, session):
422 422 session.invalidate()
423 423 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
424 424 auth_user = AuthUser(ip_addr=ip_addr)
425 425
426 426 authenticated = cookie_store.get('is_authenticated')
427 427
428 428 if not auth_user.is_authenticated and auth_user.is_user_object:
429 429 # user is not authenticated and not empty
430 430 auth_user.set_authenticated(authenticated)
431 431
432 432 return auth_user
433 433
434 434
435 435 class BaseController(WSGIController):
436 436
437 437 def __before__(self):
438 438 """
439 439 __before__ is called before controller methods and after __call__
440 440 """
441 441 # on each call propagate settings calls into global settings.
442 442 set_rhodecode_config(config)
443 443 attach_context_attributes(c, request)
444 444
445 445 # TODO: Remove this when fixed in attach_context_attributes()
446 446 c.repo_name = get_repo_slug(request) # can be empty
447 447
448 448 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
449 449 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
450 450 self.sa = meta.Session
451 451 self.scm_model = ScmModel(self.sa)
452 452
453 453 # set user language
454 454 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
455 455 if user_lang:
456 456 translation.set_lang(user_lang)
457 457 log.debug('set language to %s for user %s',
458 458 user_lang, self._rhodecode_user)
459 459
460 460 def _dispatch_redirect(self, with_url, environ, start_response):
461 461 resp = HTTPFound(with_url)
462 462 environ['SCRIPT_NAME'] = '' # handle prefix middleware
463 463 environ['PATH_INFO'] = with_url
464 464 return resp(environ, start_response)
465 465
466 466 def __call__(self, environ, start_response):
467 467 """Invoke the Controller"""
468 468 # WSGIController.__call__ dispatches to the Controller method
469 469 # the request is routed to. This routing information is
470 470 # available in environ['pylons.routes_dict']
471 471 from rhodecode.lib import helpers as h
472 472
473 473 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
474 474 if environ.get('debugtoolbar.wants_pylons_context', False):
475 475 environ['debugtoolbar.pylons_context'] = c._current_obj()
476 476
477 477 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
478 478 environ['pylons.routes_dict']['action']])
479 479
480 480 self.rc_config = SettingsModel().get_all_settings(cache=True)
481 481 self.ip_addr = get_ip_addr(environ)
482 482
483 483 # The rhodecode auth user is looked up and passed through the
484 484 # environ by the pylons compatibility tween in pyramid.
485 485 # So we can just grab it from there.
486 486 auth_user = environ['rc_auth_user']
487 487
488 488 # set globals for auth user
489 489 request.user = auth_user
490 490 c.rhodecode_user = self._rhodecode_user = auth_user
491 491
492 492 log.info('IP: %s User: %s accessed %s [%s]' % (
493 493 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
494 494 _route_name)
495 495 )
496 496
497 497 user_obj = auth_user.get_instance()
498 498 if user_obj and user_obj.user_data.get('force_password_change'):
499 499 h.flash('You are required to change your password', 'warning',
500 500 ignore_duplicate=True)
501 501 return self._dispatch_redirect(
502 502 url('my_account_password'), environ, start_response)
503 503
504 504 return WSGIController.__call__(self, environ, start_response)
505 505
506 506
507 507 class BaseRepoController(BaseController):
508 508 """
509 509 Base class for controllers responsible for loading all needed data for
510 510 repository loaded items are
511 511
512 512 c.rhodecode_repo: instance of scm repository
513 513 c.rhodecode_db_repo: instance of db
514 514 c.repository_requirements_missing: shows that repository specific data
515 515 could not be displayed due to the missing requirements
516 516 c.repository_pull_requests: show number of open pull requests
517 517 """
518 518
519 519 def __before__(self):
520 520 super(BaseRepoController, self).__before__()
521 521 if c.repo_name: # extracted from routes
522 522 db_repo = Repository.get_by_repo_name(c.repo_name)
523 523 if not db_repo:
524 524 return
525 525
526 526 log.debug(
527 527 'Found repository in database %s with state `%s`',
528 528 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
529 529 route = getattr(request.environ.get('routes.route'), 'name', '')
530 530
531 531 # allow to delete repos that are somehow damages in filesystem
532 532 if route in ['delete_repo']:
533 533 return
534 534
535 535 if db_repo.repo_state in [Repository.STATE_PENDING]:
536 536 if route in ['repo_creating_home']:
537 537 return
538 538 check_url = url('repo_creating_home', repo_name=c.repo_name)
539 539 return redirect(check_url)
540 540
541 541 self.rhodecode_db_repo = db_repo
542 542
543 543 missing_requirements = False
544 544 try:
545 545 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
546 546 except RepositoryRequirementError as e:
547 547 missing_requirements = True
548 548 self._handle_missing_requirements(e)
549 549
550 550 if self.rhodecode_repo is None and not missing_requirements:
551 551 log.error('%s this repository is present in database but it '
552 552 'cannot be created as an scm instance', c.repo_name)
553 553
554 554 h.flash(_(
555 555 "The repository at %(repo_name)s cannot be located.") %
556 556 {'repo_name': c.repo_name},
557 557 category='error', ignore_duplicate=True)
558 redirect(url('home'))
558 redirect(h.route_path('home'))
559 559
560 560 # update last change according to VCS data
561 561 if not missing_requirements:
562 562 commit = db_repo.get_commit(
563 563 pre_load=["author", "date", "message", "parents"])
564 564 db_repo.update_commit_cache(commit)
565 565
566 566 # Prepare context
567 567 c.rhodecode_db_repo = db_repo
568 568 c.rhodecode_repo = self.rhodecode_repo
569 569 c.repository_requirements_missing = missing_requirements
570 570
571 571 self._update_global_counters(self.scm_model, db_repo)
572 572
573 573 def _update_global_counters(self, scm_model, db_repo):
574 574 """
575 575 Base variables that are exposed to every page of repository
576 576 """
577 577 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
578 578
579 579 def _handle_missing_requirements(self, error):
580 580 self.rhodecode_repo = None
581 581 log.error(
582 582 'Requirements are missing for repository %s: %s',
583 583 c.repo_name, error.message)
584 584
585 585 summary_url = url('summary_home', repo_name=c.repo_name)
586 586 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
587 587 settings_update_url = url('repo', repo_name=c.repo_name)
588 588 path = request.path
589 589 should_redirect = (
590 590 path not in (summary_url, settings_update_url)
591 591 and '/settings' not in path or path == statistics_url
592 592 )
593 593 if should_redirect:
594 594 redirect(summary_url)
@@ -1,2027 +1,2027 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51 from pylons import url as pylons_url
52 52 from pylons.i18n.translation import _, ungettext
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from webhelpers.html import literal, HTML, escape
56 56 from webhelpers.html.tools import *
57 57 from webhelpers.html.builder import make_tag
58 58 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
59 59 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
60 60 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
61 61 submit, text, password, textarea, title, ul, xml_declaration, radio
62 62 from webhelpers.html.tools import auto_link, button_to, highlight, \
63 63 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
64 64 from webhelpers.pylonslib import Flash as _Flash
65 65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 68 from webhelpers.date import time_ago_in_words
69 69 from webhelpers.paginate import Page as _Page
70 70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 72 from webhelpers2.number import format_byte_size
73 73
74 74 from rhodecode.lib.action_parser import action_parser
75 75 from rhodecode.lib.ext_json import json
76 76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 79 AttributeDict, safe_int, md5, md5_safe
80 80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 83 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
84 84 from rhodecode.model.changeset_status import ChangesetStatusModel
85 85 from rhodecode.model.db import Permission, User, Repository
86 86 from rhodecode.model.repo_group import RepoGroupModel
87 87 from rhodecode.model.settings import IssueTrackerSettingsModel
88 88
89 89 log = logging.getLogger(__name__)
90 90
91 91
92 92 DEFAULT_USER = User.DEFAULT_USER
93 93 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
94 94
95 95
96 96 def url(*args, **kw):
97 97 return pylons_url(*args, **kw)
98 98
99 99
100 100 def pylons_url_current(*args, **kw):
101 101 """
102 102 This function overrides pylons.url.current() which returns the current
103 103 path so that it will also work from a pyramid only context. This
104 104 should be removed once port to pyramid is complete.
105 105 """
106 106 if not args and not kw:
107 107 request = get_current_request()
108 108 return request.path
109 109 return pylons_url.current(*args, **kw)
110 110
111 111 url.current = pylons_url_current
112 112
113 113
114 114 def url_replace(**qargs):
115 115 """ Returns the current request url while replacing query string args """
116 116
117 117 request = get_current_request()
118 118 new_args = request.GET.mixed()
119 119 new_args.update(qargs)
120 120 return url('', **new_args)
121 121
122 122
123 123 def asset(path, ver=None, **kwargs):
124 124 """
125 125 Helper to generate a static asset file path for rhodecode assets
126 126
127 127 eg. h.asset('images/image.png', ver='3923')
128 128
129 129 :param path: path of asset
130 130 :param ver: optional version query param to append as ?ver=
131 131 """
132 132 request = get_current_request()
133 133 query = {}
134 134 query.update(kwargs)
135 135 if ver:
136 136 query = {'ver': ver}
137 137 return request.static_path(
138 138 'rhodecode:public/{}'.format(path), _query=query)
139 139
140 140
141 141 default_html_escape_table = {
142 142 ord('&'): u'&amp;',
143 143 ord('<'): u'&lt;',
144 144 ord('>'): u'&gt;',
145 145 ord('"'): u'&quot;',
146 146 ord("'"): u'&#39;',
147 147 }
148 148
149 149
150 150 def html_escape(text, html_escape_table=default_html_escape_table):
151 151 """Produce entities within text."""
152 152 return text.translate(html_escape_table)
153 153
154 154
155 155 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
156 156 """
157 157 Truncate string ``s`` at the first occurrence of ``sub``.
158 158
159 159 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
160 160 """
161 161 suffix_if_chopped = suffix_if_chopped or ''
162 162 pos = s.find(sub)
163 163 if pos == -1:
164 164 return s
165 165
166 166 if inclusive:
167 167 pos += len(sub)
168 168
169 169 chopped = s[:pos]
170 170 left = s[pos:].strip()
171 171
172 172 if left and suffix_if_chopped:
173 173 chopped += suffix_if_chopped
174 174
175 175 return chopped
176 176
177 177
178 178 def shorter(text, size=20):
179 179 postfix = '...'
180 180 if len(text) > size:
181 181 return text[:size - len(postfix)] + postfix
182 182 return text
183 183
184 184
185 185 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 186 """
187 187 Reset button
188 188 """
189 189 _set_input_attrs(attrs, type, name, value)
190 190 _set_id_attr(attrs, id, name)
191 191 convert_boolean_attrs(attrs, ["disabled"])
192 192 return HTML.input(**attrs)
193 193
194 194 reset = _reset
195 195 safeid = _make_safe_id_component
196 196
197 197
198 198 def branding(name, length=40):
199 199 return truncate(name, length, indicator="")
200 200
201 201
202 202 def FID(raw_id, path):
203 203 """
204 204 Creates a unique ID for filenode based on it's hash of path and commit
205 205 it's safe to use in urls
206 206
207 207 :param raw_id:
208 208 :param path:
209 209 """
210 210
211 211 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
212 212
213 213
214 214 class _GetError(object):
215 215 """Get error from form_errors, and represent it as span wrapped error
216 216 message
217 217
218 218 :param field_name: field to fetch errors for
219 219 :param form_errors: form errors dict
220 220 """
221 221
222 222 def __call__(self, field_name, form_errors):
223 223 tmpl = """<span class="error_msg">%s</span>"""
224 224 if form_errors and field_name in form_errors:
225 225 return literal(tmpl % form_errors.get(field_name))
226 226
227 227 get_error = _GetError()
228 228
229 229
230 230 class _ToolTip(object):
231 231
232 232 def __call__(self, tooltip_title, trim_at=50):
233 233 """
234 234 Special function just to wrap our text into nice formatted
235 235 autowrapped text
236 236
237 237 :param tooltip_title:
238 238 """
239 239 tooltip_title = escape(tooltip_title)
240 240 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
241 241 return tooltip_title
242 242 tooltip = _ToolTip()
243 243
244 244
245 245 def files_breadcrumbs(repo_name, commit_id, file_path):
246 246 if isinstance(file_path, str):
247 247 file_path = safe_unicode(file_path)
248 248
249 249 # TODO: johbo: Is this always a url like path, or is this operating
250 250 # system dependent?
251 251 path_segments = file_path.split('/')
252 252
253 253 repo_name_html = escape(repo_name)
254 254 if len(path_segments) == 1 and path_segments[0] == '':
255 255 url_segments = [repo_name_html]
256 256 else:
257 257 url_segments = [
258 258 link_to(
259 259 repo_name_html,
260 260 url('files_home',
261 261 repo_name=repo_name,
262 262 revision=commit_id,
263 263 f_path=''),
264 264 class_='pjax-link')]
265 265
266 266 last_cnt = len(path_segments) - 1
267 267 for cnt, segment in enumerate(path_segments):
268 268 if not segment:
269 269 continue
270 270 segment_html = escape(segment)
271 271
272 272 if cnt != last_cnt:
273 273 url_segments.append(
274 274 link_to(
275 275 segment_html,
276 276 url('files_home',
277 277 repo_name=repo_name,
278 278 revision=commit_id,
279 279 f_path='/'.join(path_segments[:cnt + 1])),
280 280 class_='pjax-link'))
281 281 else:
282 282 url_segments.append(segment_html)
283 283
284 284 return literal('/'.join(url_segments))
285 285
286 286
287 287 class CodeHtmlFormatter(HtmlFormatter):
288 288 """
289 289 My code Html Formatter for source codes
290 290 """
291 291
292 292 def wrap(self, source, outfile):
293 293 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
294 294
295 295 def _wrap_code(self, source):
296 296 for cnt, it in enumerate(source):
297 297 i, t = it
298 298 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
299 299 yield i, t
300 300
301 301 def _wrap_tablelinenos(self, inner):
302 302 dummyoutfile = StringIO.StringIO()
303 303 lncount = 0
304 304 for t, line in inner:
305 305 if t:
306 306 lncount += 1
307 307 dummyoutfile.write(line)
308 308
309 309 fl = self.linenostart
310 310 mw = len(str(lncount + fl - 1))
311 311 sp = self.linenospecial
312 312 st = self.linenostep
313 313 la = self.lineanchors
314 314 aln = self.anchorlinenos
315 315 nocls = self.noclasses
316 316 if sp:
317 317 lines = []
318 318
319 319 for i in range(fl, fl + lncount):
320 320 if i % st == 0:
321 321 if i % sp == 0:
322 322 if aln:
323 323 lines.append('<a href="#%s%d" class="special">%*d</a>' %
324 324 (la, i, mw, i))
325 325 else:
326 326 lines.append('<span class="special">%*d</span>' % (mw, i))
327 327 else:
328 328 if aln:
329 329 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
330 330 else:
331 331 lines.append('%*d' % (mw, i))
332 332 else:
333 333 lines.append('')
334 334 ls = '\n'.join(lines)
335 335 else:
336 336 lines = []
337 337 for i in range(fl, fl + lncount):
338 338 if i % st == 0:
339 339 if aln:
340 340 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
341 341 else:
342 342 lines.append('%*d' % (mw, i))
343 343 else:
344 344 lines.append('')
345 345 ls = '\n'.join(lines)
346 346
347 347 # in case you wonder about the seemingly redundant <div> here: since the
348 348 # content in the other cell also is wrapped in a div, some browsers in
349 349 # some configurations seem to mess up the formatting...
350 350 if nocls:
351 351 yield 0, ('<table class="%stable">' % self.cssclass +
352 352 '<tr><td><div class="linenodiv" '
353 353 'style="background-color: #f0f0f0; padding-right: 10px">'
354 354 '<pre style="line-height: 125%">' +
355 355 ls + '</pre></div></td><td id="hlcode" class="code">')
356 356 else:
357 357 yield 0, ('<table class="%stable">' % self.cssclass +
358 358 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
359 359 ls + '</pre></div></td><td id="hlcode" class="code">')
360 360 yield 0, dummyoutfile.getvalue()
361 361 yield 0, '</td></tr></table>'
362 362
363 363
364 364 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
365 365 def __init__(self, **kw):
366 366 # only show these line numbers if set
367 367 self.only_lines = kw.pop('only_line_numbers', [])
368 368 self.query_terms = kw.pop('query_terms', [])
369 369 self.max_lines = kw.pop('max_lines', 5)
370 370 self.line_context = kw.pop('line_context', 3)
371 371 self.url = kw.pop('url', None)
372 372
373 373 super(CodeHtmlFormatter, self).__init__(**kw)
374 374
375 375 def _wrap_code(self, source):
376 376 for cnt, it in enumerate(source):
377 377 i, t = it
378 378 t = '<pre>%s</pre>' % t
379 379 yield i, t
380 380
381 381 def _wrap_tablelinenos(self, inner):
382 382 yield 0, '<table class="code-highlight %stable">' % self.cssclass
383 383
384 384 last_shown_line_number = 0
385 385 current_line_number = 1
386 386
387 387 for t, line in inner:
388 388 if not t:
389 389 yield t, line
390 390 continue
391 391
392 392 if current_line_number in self.only_lines:
393 393 if last_shown_line_number + 1 != current_line_number:
394 394 yield 0, '<tr>'
395 395 yield 0, '<td class="line">...</td>'
396 396 yield 0, '<td id="hlcode" class="code"></td>'
397 397 yield 0, '</tr>'
398 398
399 399 yield 0, '<tr>'
400 400 if self.url:
401 401 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
402 402 self.url, current_line_number, current_line_number)
403 403 else:
404 404 yield 0, '<td class="line"><a href="">%i</a></td>' % (
405 405 current_line_number)
406 406 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
407 407 yield 0, '</tr>'
408 408
409 409 last_shown_line_number = current_line_number
410 410
411 411 current_line_number += 1
412 412
413 413
414 414 yield 0, '</table>'
415 415
416 416
417 417 def extract_phrases(text_query):
418 418 """
419 419 Extracts phrases from search term string making sure phrases
420 420 contained in double quotes are kept together - and discarding empty values
421 421 or fully whitespace values eg.
422 422
423 423 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
424 424
425 425 """
426 426
427 427 in_phrase = False
428 428 buf = ''
429 429 phrases = []
430 430 for char in text_query:
431 431 if in_phrase:
432 432 if char == '"': # end phrase
433 433 phrases.append(buf)
434 434 buf = ''
435 435 in_phrase = False
436 436 continue
437 437 else:
438 438 buf += char
439 439 continue
440 440 else:
441 441 if char == '"': # start phrase
442 442 in_phrase = True
443 443 phrases.append(buf)
444 444 buf = ''
445 445 continue
446 446 elif char == ' ':
447 447 phrases.append(buf)
448 448 buf = ''
449 449 continue
450 450 else:
451 451 buf += char
452 452
453 453 phrases.append(buf)
454 454 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
455 455 return phrases
456 456
457 457
458 458 def get_matching_offsets(text, phrases):
459 459 """
460 460 Returns a list of string offsets in `text` that the list of `terms` match
461 461
462 462 >>> get_matching_offsets('some text here', ['some', 'here'])
463 463 [(0, 4), (10, 14)]
464 464
465 465 """
466 466 offsets = []
467 467 for phrase in phrases:
468 468 for match in re.finditer(phrase, text):
469 469 offsets.append((match.start(), match.end()))
470 470
471 471 return offsets
472 472
473 473
474 474 def normalize_text_for_matching(x):
475 475 """
476 476 Replaces all non alnum characters to spaces and lower cases the string,
477 477 useful for comparing two text strings without punctuation
478 478 """
479 479 return re.sub(r'[^\w]', ' ', x.lower())
480 480
481 481
482 482 def get_matching_line_offsets(lines, terms):
483 483 """ Return a set of `lines` indices (starting from 1) matching a
484 484 text search query, along with `context` lines above/below matching lines
485 485
486 486 :param lines: list of strings representing lines
487 487 :param terms: search term string to match in lines eg. 'some text'
488 488 :param context: number of lines above/below a matching line to add to result
489 489 :param max_lines: cut off for lines of interest
490 490 eg.
491 491
492 492 text = '''
493 493 words words words
494 494 words words words
495 495 some text some
496 496 words words words
497 497 words words words
498 498 text here what
499 499 '''
500 500 get_matching_line_offsets(text, 'text', context=1)
501 501 {3: [(5, 9)], 6: [(0, 4)]]
502 502
503 503 """
504 504 matching_lines = {}
505 505 phrases = [normalize_text_for_matching(phrase)
506 506 for phrase in extract_phrases(terms)]
507 507
508 508 for line_index, line in enumerate(lines, start=1):
509 509 match_offsets = get_matching_offsets(
510 510 normalize_text_for_matching(line), phrases)
511 511 if match_offsets:
512 512 matching_lines[line_index] = match_offsets
513 513
514 514 return matching_lines
515 515
516 516
517 517 def hsv_to_rgb(h, s, v):
518 518 """ Convert hsv color values to rgb """
519 519
520 520 if s == 0.0:
521 521 return v, v, v
522 522 i = int(h * 6.0) # XXX assume int() truncates!
523 523 f = (h * 6.0) - i
524 524 p = v * (1.0 - s)
525 525 q = v * (1.0 - s * f)
526 526 t = v * (1.0 - s * (1.0 - f))
527 527 i = i % 6
528 528 if i == 0:
529 529 return v, t, p
530 530 if i == 1:
531 531 return q, v, p
532 532 if i == 2:
533 533 return p, v, t
534 534 if i == 3:
535 535 return p, q, v
536 536 if i == 4:
537 537 return t, p, v
538 538 if i == 5:
539 539 return v, p, q
540 540
541 541
542 542 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
543 543 """
544 544 Generator for getting n of evenly distributed colors using
545 545 hsv color and golden ratio. It always return same order of colors
546 546
547 547 :param n: number of colors to generate
548 548 :param saturation: saturation of returned colors
549 549 :param lightness: lightness of returned colors
550 550 :returns: RGB tuple
551 551 """
552 552
553 553 golden_ratio = 0.618033988749895
554 554 h = 0.22717784590367374
555 555
556 556 for _ in xrange(n):
557 557 h += golden_ratio
558 558 h %= 1
559 559 HSV_tuple = [h, saturation, lightness]
560 560 RGB_tuple = hsv_to_rgb(*HSV_tuple)
561 561 yield map(lambda x: str(int(x * 256)), RGB_tuple)
562 562
563 563
564 564 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
565 565 """
566 566 Returns a function which when called with an argument returns a unique
567 567 color for that argument, eg.
568 568
569 569 :param n: number of colors to generate
570 570 :param saturation: saturation of returned colors
571 571 :param lightness: lightness of returned colors
572 572 :returns: css RGB string
573 573
574 574 >>> color_hash = color_hasher()
575 575 >>> color_hash('hello')
576 576 'rgb(34, 12, 59)'
577 577 >>> color_hash('hello')
578 578 'rgb(34, 12, 59)'
579 579 >>> color_hash('other')
580 580 'rgb(90, 224, 159)'
581 581 """
582 582
583 583 color_dict = {}
584 584 cgenerator = unique_color_generator(
585 585 saturation=saturation, lightness=lightness)
586 586
587 587 def get_color_string(thing):
588 588 if thing in color_dict:
589 589 col = color_dict[thing]
590 590 else:
591 591 col = color_dict[thing] = cgenerator.next()
592 592 return "rgb(%s)" % (', '.join(col))
593 593
594 594 return get_color_string
595 595
596 596
597 597 def get_lexer_safe(mimetype=None, filepath=None):
598 598 """
599 599 Tries to return a relevant pygments lexer using mimetype/filepath name,
600 600 defaulting to plain text if none could be found
601 601 """
602 602 lexer = None
603 603 try:
604 604 if mimetype:
605 605 lexer = get_lexer_for_mimetype(mimetype)
606 606 if not lexer:
607 607 lexer = get_lexer_for_filename(filepath)
608 608 except pygments.util.ClassNotFound:
609 609 pass
610 610
611 611 if not lexer:
612 612 lexer = get_lexer_by_name('text')
613 613
614 614 return lexer
615 615
616 616
617 617 def get_lexer_for_filenode(filenode):
618 618 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
619 619 return lexer
620 620
621 621
622 622 def pygmentize(filenode, **kwargs):
623 623 """
624 624 pygmentize function using pygments
625 625
626 626 :param filenode:
627 627 """
628 628 lexer = get_lexer_for_filenode(filenode)
629 629 return literal(code_highlight(filenode.content, lexer,
630 630 CodeHtmlFormatter(**kwargs)))
631 631
632 632
633 633 def is_following_repo(repo_name, user_id):
634 634 from rhodecode.model.scm import ScmModel
635 635 return ScmModel().is_following_repo(repo_name, user_id)
636 636
637 637
638 638 class _Message(object):
639 639 """A message returned by ``Flash.pop_messages()``.
640 640
641 641 Converting the message to a string returns the message text. Instances
642 642 also have the following attributes:
643 643
644 644 * ``message``: the message text.
645 645 * ``category``: the category specified when the message was created.
646 646 """
647 647
648 648 def __init__(self, category, message):
649 649 self.category = category
650 650 self.message = message
651 651
652 652 def __str__(self):
653 653 return self.message
654 654
655 655 __unicode__ = __str__
656 656
657 657 def __html__(self):
658 658 return escape(safe_unicode(self.message))
659 659
660 660
661 661 class Flash(_Flash):
662 662
663 663 def pop_messages(self):
664 664 """Return all accumulated messages and delete them from the session.
665 665
666 666 The return value is a list of ``Message`` objects.
667 667 """
668 668 from pylons import session
669 669
670 670 messages = []
671 671
672 672 # Pop the 'old' pylons flash messages. They are tuples of the form
673 673 # (category, message)
674 674 for cat, msg in session.pop(self.session_key, []):
675 675 messages.append(_Message(cat, msg))
676 676
677 677 # Pop the 'new' pyramid flash messages for each category as list
678 678 # of strings.
679 679 for cat in self.categories:
680 680 for msg in session.pop_flash(queue=cat):
681 681 messages.append(_Message(cat, msg))
682 682 # Map messages from the default queue to the 'notice' category.
683 683 for msg in session.pop_flash():
684 684 messages.append(_Message('notice', msg))
685 685
686 686 session.save()
687 687 return messages
688 688
689 689 def json_alerts(self):
690 690 payloads = []
691 691 messages = flash.pop_messages()
692 692 if messages:
693 693 for message in messages:
694 694 subdata = {}
695 695 if hasattr(message.message, 'rsplit'):
696 696 flash_data = message.message.rsplit('|DELIM|', 1)
697 697 org_message = flash_data[0]
698 698 if len(flash_data) > 1:
699 699 subdata = json.loads(flash_data[1])
700 700 else:
701 701 org_message = message.message
702 702 payloads.append({
703 703 'message': {
704 704 'message': u'{}'.format(org_message),
705 705 'level': message.category,
706 706 'force': True,
707 707 'subdata': subdata
708 708 }
709 709 })
710 710 return json.dumps(payloads)
711 711
712 712 flash = Flash()
713 713
714 714 #==============================================================================
715 715 # SCM FILTERS available via h.
716 716 #==============================================================================
717 717 from rhodecode.lib.vcs.utils import author_name, author_email
718 718 from rhodecode.lib.utils2 import credentials_filter, age as _age
719 719 from rhodecode.model.db import User, ChangesetStatus
720 720
721 721 age = _age
722 722 capitalize = lambda x: x.capitalize()
723 723 email = author_email
724 724 short_id = lambda x: x[:12]
725 725 hide_credentials = lambda x: ''.join(credentials_filter(x))
726 726
727 727
728 728 def age_component(datetime_iso, value=None, time_is_local=False):
729 729 title = value or format_date(datetime_iso)
730 730 tzinfo = '+00:00'
731 731
732 732 # detect if we have a timezone info, otherwise, add it
733 733 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
734 734 if time_is_local:
735 735 tzinfo = time.strftime("+%H:%M",
736 736 time.gmtime(
737 737 (datetime.now() - datetime.utcnow()).seconds + 1
738 738 )
739 739 )
740 740
741 741 return literal(
742 742 '<time class="timeago tooltip" '
743 743 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
744 744 datetime_iso, title, tzinfo))
745 745
746 746
747 747 def _shorten_commit_id(commit_id):
748 748 from rhodecode import CONFIG
749 749 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
750 750 return commit_id[:def_len]
751 751
752 752
753 753 def show_id(commit):
754 754 """
755 755 Configurable function that shows ID
756 756 by default it's r123:fffeeefffeee
757 757
758 758 :param commit: commit instance
759 759 """
760 760 from rhodecode import CONFIG
761 761 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
762 762
763 763 raw_id = _shorten_commit_id(commit.raw_id)
764 764 if show_idx:
765 765 return 'r%s:%s' % (commit.idx, raw_id)
766 766 else:
767 767 return '%s' % (raw_id, )
768 768
769 769
770 770 def format_date(date):
771 771 """
772 772 use a standardized formatting for dates used in RhodeCode
773 773
774 774 :param date: date/datetime object
775 775 :return: formatted date
776 776 """
777 777
778 778 if date:
779 779 _fmt = "%a, %d %b %Y %H:%M:%S"
780 780 return safe_unicode(date.strftime(_fmt))
781 781
782 782 return u""
783 783
784 784
785 785 class _RepoChecker(object):
786 786
787 787 def __init__(self, backend_alias):
788 788 self._backend_alias = backend_alias
789 789
790 790 def __call__(self, repository):
791 791 if hasattr(repository, 'alias'):
792 792 _type = repository.alias
793 793 elif hasattr(repository, 'repo_type'):
794 794 _type = repository.repo_type
795 795 else:
796 796 _type = repository
797 797 return _type == self._backend_alias
798 798
799 799 is_git = _RepoChecker('git')
800 800 is_hg = _RepoChecker('hg')
801 801 is_svn = _RepoChecker('svn')
802 802
803 803
804 804 def get_repo_type_by_name(repo_name):
805 805 repo = Repository.get_by_repo_name(repo_name)
806 806 return repo.repo_type
807 807
808 808
809 809 def is_svn_without_proxy(repository):
810 810 if is_svn(repository):
811 811 from rhodecode.model.settings import VcsSettingsModel
812 812 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
813 813 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
814 814 return False
815 815
816 816
817 817 def discover_user(author):
818 818 """
819 819 Tries to discover RhodeCode User based on the autho string. Author string
820 820 is typically `FirstName LastName <email@address.com>`
821 821 """
822 822
823 823 # if author is already an instance use it for extraction
824 824 if isinstance(author, User):
825 825 return author
826 826
827 827 # Valid email in the attribute passed, see if they're in the system
828 828 _email = author_email(author)
829 829 if _email != '':
830 830 user = User.get_by_email(_email, case_insensitive=True, cache=True)
831 831 if user is not None:
832 832 return user
833 833
834 834 # Maybe it's a username, we try to extract it and fetch by username ?
835 835 _author = author_name(author)
836 836 user = User.get_by_username(_author, case_insensitive=True, cache=True)
837 837 if user is not None:
838 838 return user
839 839
840 840 return None
841 841
842 842
843 843 def email_or_none(author):
844 844 # extract email from the commit string
845 845 _email = author_email(author)
846 846
847 847 # If we have an email, use it, otherwise
848 848 # see if it contains a username we can get an email from
849 849 if _email != '':
850 850 return _email
851 851 else:
852 852 user = User.get_by_username(
853 853 author_name(author), case_insensitive=True, cache=True)
854 854
855 855 if user is not None:
856 856 return user.email
857 857
858 858 # No valid email, not a valid user in the system, none!
859 859 return None
860 860
861 861
862 862 def link_to_user(author, length=0, **kwargs):
863 863 user = discover_user(author)
864 864 # user can be None, but if we have it already it means we can re-use it
865 865 # in the person() function, so we save 1 intensive-query
866 866 if user:
867 867 author = user
868 868
869 869 display_person = person(author, 'username_or_name_or_email')
870 870 if length:
871 871 display_person = shorter(display_person, length)
872 872
873 873 if user:
874 874 return link_to(
875 875 escape(display_person),
876 876 route_path('user_profile', username=user.username),
877 877 **kwargs)
878 878 else:
879 879 return escape(display_person)
880 880
881 881
882 882 def person(author, show_attr="username_and_name"):
883 883 user = discover_user(author)
884 884 if user:
885 885 return getattr(user, show_attr)
886 886 else:
887 887 _author = author_name(author)
888 888 _email = email(author)
889 889 return _author or _email
890 890
891 891
892 892 def author_string(email):
893 893 if email:
894 894 user = User.get_by_email(email, case_insensitive=True, cache=True)
895 895 if user:
896 896 if user.firstname or user.lastname:
897 897 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
898 898 else:
899 899 return email
900 900 else:
901 901 return email
902 902 else:
903 903 return None
904 904
905 905
906 906 def person_by_id(id_, show_attr="username_and_name"):
907 907 # attr to return from fetched user
908 908 person_getter = lambda usr: getattr(usr, show_attr)
909 909
910 910 #maybe it's an ID ?
911 911 if str(id_).isdigit() or isinstance(id_, int):
912 912 id_ = int(id_)
913 913 user = User.get(id_)
914 914 if user is not None:
915 915 return person_getter(user)
916 916 return id_
917 917
918 918
919 919 def gravatar_with_user(author, show_disabled=False):
920 920 from rhodecode.lib.utils import PartialRenderer
921 921 _render = PartialRenderer('base/base.mako')
922 922 return _render('gravatar_with_user', author, show_disabled=show_disabled)
923 923
924 924
925 925 def desc_stylize(value):
926 926 """
927 927 converts tags from value into html equivalent
928 928
929 929 :param value:
930 930 """
931 931 if not value:
932 932 return ''
933 933
934 934 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
935 935 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
936 936 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
937 937 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
938 938 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
939 939 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
940 940 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
941 941 '<div class="metatag" tag="lang">\\2</div>', value)
942 942 value = re.sub(r'\[([a-z]+)\]',
943 943 '<div class="metatag" tag="\\1">\\1</div>', value)
944 944
945 945 return value
946 946
947 947
948 948 def escaped_stylize(value):
949 949 """
950 950 converts tags from value into html equivalent, but escaping its value first
951 951 """
952 952 if not value:
953 953 return ''
954 954
955 955 # Using default webhelper escape method, but has to force it as a
956 956 # plain unicode instead of a markup tag to be used in regex expressions
957 957 value = unicode(escape(safe_unicode(value)))
958 958
959 959 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
960 960 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
961 961 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
962 962 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
963 963 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
964 964 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
965 965 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
966 966 '<div class="metatag" tag="lang">\\2</div>', value)
967 967 value = re.sub(r'\[([a-z]+)\]',
968 968 '<div class="metatag" tag="\\1">\\1</div>', value)
969 969
970 970 return value
971 971
972 972
973 973 def bool2icon(value):
974 974 """
975 975 Returns boolean value of a given value, represented as html element with
976 976 classes that will represent icons
977 977
978 978 :param value: given value to convert to html node
979 979 """
980 980
981 981 if value: # does bool conversion
982 982 return HTML.tag('i', class_="icon-true")
983 983 else: # not true as bool
984 984 return HTML.tag('i', class_="icon-false")
985 985
986 986
987 987 #==============================================================================
988 988 # PERMS
989 989 #==============================================================================
990 990 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
991 991 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
992 992 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
993 993 csrf_token_key
994 994
995 995
996 996 #==============================================================================
997 997 # GRAVATAR URL
998 998 #==============================================================================
999 999 class InitialsGravatar(object):
1000 1000 def __init__(self, email_address, first_name, last_name, size=30,
1001 1001 background=None, text_color='#fff'):
1002 1002 self.size = size
1003 1003 self.first_name = first_name
1004 1004 self.last_name = last_name
1005 1005 self.email_address = email_address
1006 1006 self.background = background or self.str2color(email_address)
1007 1007 self.text_color = text_color
1008 1008
1009 1009 def get_color_bank(self):
1010 1010 """
1011 1011 returns a predefined list of colors that gravatars can use.
1012 1012 Those are randomized distinct colors that guarantee readability and
1013 1013 uniqueness.
1014 1014
1015 1015 generated with: http://phrogz.net/css/distinct-colors.html
1016 1016 """
1017 1017 return [
1018 1018 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1019 1019 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1020 1020 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1021 1021 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1022 1022 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1023 1023 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1024 1024 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1025 1025 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1026 1026 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1027 1027 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1028 1028 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1029 1029 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1030 1030 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1031 1031 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1032 1032 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1033 1033 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1034 1034 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1035 1035 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1036 1036 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1037 1037 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1038 1038 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1039 1039 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1040 1040 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1041 1041 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1042 1042 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1043 1043 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1044 1044 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1045 1045 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1046 1046 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1047 1047 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1048 1048 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1049 1049 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1050 1050 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1051 1051 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1052 1052 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1053 1053 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1054 1054 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1055 1055 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1056 1056 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1057 1057 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1058 1058 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1059 1059 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1060 1060 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1061 1061 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1062 1062 '#4f8c46', '#368dd9', '#5c0073'
1063 1063 ]
1064 1064
1065 1065 def rgb_to_hex_color(self, rgb_tuple):
1066 1066 """
1067 1067 Converts an rgb_tuple passed to an hex color.
1068 1068
1069 1069 :param rgb_tuple: tuple with 3 ints represents rgb color space
1070 1070 """
1071 1071 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1072 1072
1073 1073 def email_to_int_list(self, email_str):
1074 1074 """
1075 1075 Get every byte of the hex digest value of email and turn it to integer.
1076 1076 It's going to be always between 0-255
1077 1077 """
1078 1078 digest = md5_safe(email_str.lower())
1079 1079 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1080 1080
1081 1081 def pick_color_bank_index(self, email_str, color_bank):
1082 1082 return self.email_to_int_list(email_str)[0] % len(color_bank)
1083 1083
1084 1084 def str2color(self, email_str):
1085 1085 """
1086 1086 Tries to map in a stable algorithm an email to color
1087 1087
1088 1088 :param email_str:
1089 1089 """
1090 1090 color_bank = self.get_color_bank()
1091 1091 # pick position (module it's length so we always find it in the
1092 1092 # bank even if it's smaller than 256 values
1093 1093 pos = self.pick_color_bank_index(email_str, color_bank)
1094 1094 return color_bank[pos]
1095 1095
1096 1096 def normalize_email(self, email_address):
1097 1097 import unicodedata
1098 1098 # default host used to fill in the fake/missing email
1099 1099 default_host = u'localhost'
1100 1100
1101 1101 if not email_address:
1102 1102 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1103 1103
1104 1104 email_address = safe_unicode(email_address)
1105 1105
1106 1106 if u'@' not in email_address:
1107 1107 email_address = u'%s@%s' % (email_address, default_host)
1108 1108
1109 1109 if email_address.endswith(u'@'):
1110 1110 email_address = u'%s%s' % (email_address, default_host)
1111 1111
1112 1112 email_address = unicodedata.normalize('NFKD', email_address)\
1113 1113 .encode('ascii', 'ignore')
1114 1114 return email_address
1115 1115
1116 1116 def get_initials(self):
1117 1117 """
1118 1118 Returns 2 letter initials calculated based on the input.
1119 1119 The algorithm picks first given email address, and takes first letter
1120 1120 of part before @, and then the first letter of server name. In case
1121 1121 the part before @ is in a format of `somestring.somestring2` it replaces
1122 1122 the server letter with first letter of somestring2
1123 1123
1124 1124 In case function was initialized with both first and lastname, this
1125 1125 overrides the extraction from email by first letter of the first and
1126 1126 last name. We add special logic to that functionality, In case Full name
1127 1127 is compound, like Guido Von Rossum, we use last part of the last name
1128 1128 (Von Rossum) picking `R`.
1129 1129
1130 1130 Function also normalizes the non-ascii characters to they ascii
1131 1131 representation, eg Ą => A
1132 1132 """
1133 1133 import unicodedata
1134 1134 # replace non-ascii to ascii
1135 1135 first_name = unicodedata.normalize(
1136 1136 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1137 1137 last_name = unicodedata.normalize(
1138 1138 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1139 1139
1140 1140 # do NFKD encoding, and also make sure email has proper format
1141 1141 email_address = self.normalize_email(self.email_address)
1142 1142
1143 1143 # first push the email initials
1144 1144 prefix, server = email_address.split('@', 1)
1145 1145
1146 1146 # check if prefix is maybe a 'firstname.lastname' syntax
1147 1147 _dot_split = prefix.rsplit('.', 1)
1148 1148 if len(_dot_split) == 2:
1149 1149 initials = [_dot_split[0][0], _dot_split[1][0]]
1150 1150 else:
1151 1151 initials = [prefix[0], server[0]]
1152 1152
1153 1153 # then try to replace either firtname or lastname
1154 1154 fn_letter = (first_name or " ")[0].strip()
1155 1155 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1156 1156
1157 1157 if fn_letter:
1158 1158 initials[0] = fn_letter
1159 1159
1160 1160 if ln_letter:
1161 1161 initials[1] = ln_letter
1162 1162
1163 1163 return ''.join(initials).upper()
1164 1164
1165 1165 def get_img_data_by_type(self, font_family, img_type):
1166 1166 default_user = """
1167 1167 <svg xmlns="http://www.w3.org/2000/svg"
1168 1168 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1169 1169 viewBox="-15 -10 439.165 429.164"
1170 1170
1171 1171 xml:space="preserve"
1172 1172 style="background:{background};" >
1173 1173
1174 1174 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1175 1175 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1176 1176 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1177 1177 168.596,153.916,216.671,
1178 1178 204.583,216.671z" fill="{text_color}"/>
1179 1179 <path d="M407.164,374.717L360.88,
1180 1180 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1181 1181 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1182 1182 15.366-44.203,23.488-69.076,23.488c-24.877,
1183 1183 0-48.762-8.122-69.078-23.488
1184 1184 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1185 1185 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1186 1186 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1187 1187 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1188 1188 19.402-10.527 C409.699,390.129,
1189 1189 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1190 1190 </svg>""".format(
1191 1191 size=self.size,
1192 1192 background='#979797', # @grey4
1193 1193 text_color=self.text_color,
1194 1194 font_family=font_family)
1195 1195
1196 1196 return {
1197 1197 "default_user": default_user
1198 1198 }[img_type]
1199 1199
1200 1200 def get_img_data(self, svg_type=None):
1201 1201 """
1202 1202 generates the svg metadata for image
1203 1203 """
1204 1204
1205 1205 font_family = ','.join([
1206 1206 'proximanovaregular',
1207 1207 'Proxima Nova Regular',
1208 1208 'Proxima Nova',
1209 1209 'Arial',
1210 1210 'Lucida Grande',
1211 1211 'sans-serif'
1212 1212 ])
1213 1213 if svg_type:
1214 1214 return self.get_img_data_by_type(font_family, svg_type)
1215 1215
1216 1216 initials = self.get_initials()
1217 1217 img_data = """
1218 1218 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1219 1219 width="{size}" height="{size}"
1220 1220 style="width: 100%; height: 100%; background-color: {background}"
1221 1221 viewBox="0 0 {size} {size}">
1222 1222 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1223 1223 pointer-events="auto" fill="{text_color}"
1224 1224 font-family="{font_family}"
1225 1225 style="font-weight: 400; font-size: {f_size}px;">{text}
1226 1226 </text>
1227 1227 </svg>""".format(
1228 1228 size=self.size,
1229 1229 f_size=self.size/1.85, # scale the text inside the box nicely
1230 1230 background=self.background,
1231 1231 text_color=self.text_color,
1232 1232 text=initials.upper(),
1233 1233 font_family=font_family)
1234 1234
1235 1235 return img_data
1236 1236
1237 1237 def generate_svg(self, svg_type=None):
1238 1238 img_data = self.get_img_data(svg_type)
1239 1239 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1240 1240
1241 1241
1242 1242 def initials_gravatar(email_address, first_name, last_name, size=30):
1243 1243 svg_type = None
1244 1244 if email_address == User.DEFAULT_USER_EMAIL:
1245 1245 svg_type = 'default_user'
1246 1246 klass = InitialsGravatar(email_address, first_name, last_name, size)
1247 1247 return klass.generate_svg(svg_type=svg_type)
1248 1248
1249 1249
1250 1250 def gravatar_url(email_address, size=30):
1251 1251 # doh, we need to re-import those to mock it later
1252 1252 from pylons import tmpl_context as c
1253 1253
1254 1254 _use_gravatar = c.visual.use_gravatar
1255 1255 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1256 1256
1257 1257 email_address = email_address or User.DEFAULT_USER_EMAIL
1258 1258 if isinstance(email_address, unicode):
1259 1259 # hashlib crashes on unicode items
1260 1260 email_address = safe_str(email_address)
1261 1261
1262 1262 # empty email or default user
1263 1263 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1264 1264 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1265 1265
1266 1266 if _use_gravatar:
1267 1267 # TODO: Disuse pyramid thread locals. Think about another solution to
1268 1268 # get the host and schema here.
1269 1269 request = get_current_request()
1270 1270 tmpl = safe_str(_gravatar_url)
1271 1271 tmpl = tmpl.replace('{email}', email_address)\
1272 1272 .replace('{md5email}', md5_safe(email_address.lower())) \
1273 1273 .replace('{netloc}', request.host)\
1274 1274 .replace('{scheme}', request.scheme)\
1275 1275 .replace('{size}', safe_str(size))
1276 1276 return tmpl
1277 1277 else:
1278 1278 return initials_gravatar(email_address, '', '', size=size)
1279 1279
1280 1280
1281 1281 class Page(_Page):
1282 1282 """
1283 1283 Custom pager to match rendering style with paginator
1284 1284 """
1285 1285
1286 1286 def _get_pos(self, cur_page, max_page, items):
1287 1287 edge = (items / 2) + 1
1288 1288 if (cur_page <= edge):
1289 1289 radius = max(items / 2, items - cur_page)
1290 1290 elif (max_page - cur_page) < edge:
1291 1291 radius = (items - 1) - (max_page - cur_page)
1292 1292 else:
1293 1293 radius = items / 2
1294 1294
1295 1295 left = max(1, (cur_page - (radius)))
1296 1296 right = min(max_page, cur_page + (radius))
1297 1297 return left, cur_page, right
1298 1298
1299 1299 def _range(self, regexp_match):
1300 1300 """
1301 1301 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1302 1302
1303 1303 Arguments:
1304 1304
1305 1305 regexp_match
1306 1306 A "re" (regular expressions) match object containing the
1307 1307 radius of linked pages around the current page in
1308 1308 regexp_match.group(1) as a string
1309 1309
1310 1310 This function is supposed to be called as a callable in
1311 1311 re.sub.
1312 1312
1313 1313 """
1314 1314 radius = int(regexp_match.group(1))
1315 1315
1316 1316 # Compute the first and last page number within the radius
1317 1317 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1318 1318 # -> leftmost_page = 5
1319 1319 # -> rightmost_page = 9
1320 1320 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1321 1321 self.last_page,
1322 1322 (radius * 2) + 1)
1323 1323 nav_items = []
1324 1324
1325 1325 # Create a link to the first page (unless we are on the first page
1326 1326 # or there would be no need to insert '..' spacers)
1327 1327 if self.page != self.first_page and self.first_page < leftmost_page:
1328 1328 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1329 1329
1330 1330 # Insert dots if there are pages between the first page
1331 1331 # and the currently displayed page range
1332 1332 if leftmost_page - self.first_page > 1:
1333 1333 # Wrap in a SPAN tag if nolink_attr is set
1334 1334 text = '..'
1335 1335 if self.dotdot_attr:
1336 1336 text = HTML.span(c=text, **self.dotdot_attr)
1337 1337 nav_items.append(text)
1338 1338
1339 1339 for thispage in xrange(leftmost_page, rightmost_page + 1):
1340 1340 # Hilight the current page number and do not use a link
1341 1341 if thispage == self.page:
1342 1342 text = '%s' % (thispage,)
1343 1343 # Wrap in a SPAN tag if nolink_attr is set
1344 1344 if self.curpage_attr:
1345 1345 text = HTML.span(c=text, **self.curpage_attr)
1346 1346 nav_items.append(text)
1347 1347 # Otherwise create just a link to that page
1348 1348 else:
1349 1349 text = '%s' % (thispage,)
1350 1350 nav_items.append(self._pagerlink(thispage, text))
1351 1351
1352 1352 # Insert dots if there are pages between the displayed
1353 1353 # page numbers and the end of the page range
1354 1354 if self.last_page - rightmost_page > 1:
1355 1355 text = '..'
1356 1356 # Wrap in a SPAN tag if nolink_attr is set
1357 1357 if self.dotdot_attr:
1358 1358 text = HTML.span(c=text, **self.dotdot_attr)
1359 1359 nav_items.append(text)
1360 1360
1361 1361 # Create a link to the very last page (unless we are on the last
1362 1362 # page or there would be no need to insert '..' spacers)
1363 1363 if self.page != self.last_page and rightmost_page < self.last_page:
1364 1364 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1365 1365
1366 1366 ## prerender links
1367 1367 #_page_link = url.current()
1368 1368 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1369 1369 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 1370 return self.separator.join(nav_items)
1371 1371
1372 1372 def pager(self, format='~2~', page_param='page', partial_param='partial',
1373 1373 show_if_single_page=False, separator=' ', onclick=None,
1374 1374 symbol_first='<<', symbol_last='>>',
1375 1375 symbol_previous='<', symbol_next='>',
1376 1376 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1377 1377 curpage_attr={'class': 'pager_curpage'},
1378 1378 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1379 1379
1380 1380 self.curpage_attr = curpage_attr
1381 1381 self.separator = separator
1382 1382 self.pager_kwargs = kwargs
1383 1383 self.page_param = page_param
1384 1384 self.partial_param = partial_param
1385 1385 self.onclick = onclick
1386 1386 self.link_attr = link_attr
1387 1387 self.dotdot_attr = dotdot_attr
1388 1388
1389 1389 # Don't show navigator if there is no more than one page
1390 1390 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1391 1391 return ''
1392 1392
1393 1393 from string import Template
1394 1394 # Replace ~...~ in token format by range of pages
1395 1395 result = re.sub(r'~(\d+)~', self._range, format)
1396 1396
1397 1397 # Interpolate '%' variables
1398 1398 result = Template(result).safe_substitute({
1399 1399 'first_page': self.first_page,
1400 1400 'last_page': self.last_page,
1401 1401 'page': self.page,
1402 1402 'page_count': self.page_count,
1403 1403 'items_per_page': self.items_per_page,
1404 1404 'first_item': self.first_item,
1405 1405 'last_item': self.last_item,
1406 1406 'item_count': self.item_count,
1407 1407 'link_first': self.page > self.first_page and \
1408 1408 self._pagerlink(self.first_page, symbol_first) or '',
1409 1409 'link_last': self.page < self.last_page and \
1410 1410 self._pagerlink(self.last_page, symbol_last) or '',
1411 1411 'link_previous': self.previous_page and \
1412 1412 self._pagerlink(self.previous_page, symbol_previous) \
1413 1413 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1414 1414 'link_next': self.next_page and \
1415 1415 self._pagerlink(self.next_page, symbol_next) \
1416 1416 or HTML.span(symbol_next, class_="pg-next disabled")
1417 1417 })
1418 1418
1419 1419 return literal(result)
1420 1420
1421 1421
1422 1422 #==============================================================================
1423 1423 # REPO PAGER, PAGER FOR REPOSITORY
1424 1424 #==============================================================================
1425 1425 class RepoPage(Page):
1426 1426
1427 1427 def __init__(self, collection, page=1, items_per_page=20,
1428 1428 item_count=None, url=None, **kwargs):
1429 1429
1430 1430 """Create a "RepoPage" instance. special pager for paging
1431 1431 repository
1432 1432 """
1433 1433 self._url_generator = url
1434 1434
1435 1435 # Safe the kwargs class-wide so they can be used in the pager() method
1436 1436 self.kwargs = kwargs
1437 1437
1438 1438 # Save a reference to the collection
1439 1439 self.original_collection = collection
1440 1440
1441 1441 self.collection = collection
1442 1442
1443 1443 # The self.page is the number of the current page.
1444 1444 # The first page has the number 1!
1445 1445 try:
1446 1446 self.page = int(page) # make it int() if we get it as a string
1447 1447 except (ValueError, TypeError):
1448 1448 self.page = 1
1449 1449
1450 1450 self.items_per_page = items_per_page
1451 1451
1452 1452 # Unless the user tells us how many items the collections has
1453 1453 # we calculate that ourselves.
1454 1454 if item_count is not None:
1455 1455 self.item_count = item_count
1456 1456 else:
1457 1457 self.item_count = len(self.collection)
1458 1458
1459 1459 # Compute the number of the first and last available page
1460 1460 if self.item_count > 0:
1461 1461 self.first_page = 1
1462 1462 self.page_count = int(math.ceil(float(self.item_count) /
1463 1463 self.items_per_page))
1464 1464 self.last_page = self.first_page + self.page_count - 1
1465 1465
1466 1466 # Make sure that the requested page number is the range of
1467 1467 # valid pages
1468 1468 if self.page > self.last_page:
1469 1469 self.page = self.last_page
1470 1470 elif self.page < self.first_page:
1471 1471 self.page = self.first_page
1472 1472
1473 1473 # Note: the number of items on this page can be less than
1474 1474 # items_per_page if the last page is not full
1475 1475 self.first_item = max(0, (self.item_count) - (self.page *
1476 1476 items_per_page))
1477 1477 self.last_item = ((self.item_count - 1) - items_per_page *
1478 1478 (self.page - 1))
1479 1479
1480 1480 self.items = list(self.collection[self.first_item:self.last_item + 1])
1481 1481
1482 1482 # Links to previous and next page
1483 1483 if self.page > self.first_page:
1484 1484 self.previous_page = self.page - 1
1485 1485 else:
1486 1486 self.previous_page = None
1487 1487
1488 1488 if self.page < self.last_page:
1489 1489 self.next_page = self.page + 1
1490 1490 else:
1491 1491 self.next_page = None
1492 1492
1493 1493 # No items available
1494 1494 else:
1495 1495 self.first_page = None
1496 1496 self.page_count = 0
1497 1497 self.last_page = None
1498 1498 self.first_item = None
1499 1499 self.last_item = None
1500 1500 self.previous_page = None
1501 1501 self.next_page = None
1502 1502 self.items = []
1503 1503
1504 1504 # This is a subclass of the 'list' type. Initialise the list now.
1505 1505 list.__init__(self, reversed(self.items))
1506 1506
1507 1507
1508 1508 def changed_tooltip(nodes):
1509 1509 """
1510 1510 Generates a html string for changed nodes in commit page.
1511 1511 It limits the output to 30 entries
1512 1512
1513 1513 :param nodes: LazyNodesGenerator
1514 1514 """
1515 1515 if nodes:
1516 1516 pref = ': <br/> '
1517 1517 suf = ''
1518 1518 if len(nodes) > 30:
1519 1519 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1520 1520 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1521 1521 for x in nodes[:30]]) + suf)
1522 1522 else:
1523 1523 return ': ' + _('No Files')
1524 1524
1525 1525
1526 1526 def breadcrumb_repo_link(repo):
1527 1527 """
1528 1528 Makes a breadcrumbs path link to repo
1529 1529
1530 1530 ex::
1531 1531 group >> subgroup >> repo
1532 1532
1533 1533 :param repo: a Repository instance
1534 1534 """
1535 1535
1536 1536 path = [
1537 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1537 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1538 1538 for group in repo.groups_with_parents
1539 1539 ] + [
1540 1540 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1541 1541 ]
1542 1542
1543 1543 return literal(' &raquo; '.join(path))
1544 1544
1545 1545
1546 1546 def format_byte_size_binary(file_size):
1547 1547 """
1548 1548 Formats file/folder sizes to standard.
1549 1549 """
1550 1550 formatted_size = format_byte_size(file_size, binary=True)
1551 1551 return formatted_size
1552 1552
1553 1553
1554 1554 def urlify_text(text_, safe=True):
1555 1555 """
1556 1556 Extrac urls from text and make html links out of them
1557 1557
1558 1558 :param text_:
1559 1559 """
1560 1560
1561 1561 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1562 1562 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1563 1563
1564 1564 def url_func(match_obj):
1565 1565 url_full = match_obj.groups()[0]
1566 1566 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1567 1567 _newtext = url_pat.sub(url_func, text_)
1568 1568 if safe:
1569 1569 return literal(_newtext)
1570 1570 return _newtext
1571 1571
1572 1572
1573 1573 def urlify_commits(text_, repository):
1574 1574 """
1575 1575 Extract commit ids from text and make link from them
1576 1576
1577 1577 :param text_:
1578 1578 :param repository: repo name to build the URL with
1579 1579 """
1580 1580 from pylons import url # doh, we need to re-import url to mock it later
1581 1581 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1582 1582
1583 1583 def url_func(match_obj):
1584 1584 commit_id = match_obj.groups()[1]
1585 1585 pref = match_obj.groups()[0]
1586 1586 suf = match_obj.groups()[2]
1587 1587
1588 1588 tmpl = (
1589 1589 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1590 1590 '%(commit_id)s</a>%(suf)s'
1591 1591 )
1592 1592 return tmpl % {
1593 1593 'pref': pref,
1594 1594 'cls': 'revision-link',
1595 1595 'url': url('changeset_home', repo_name=repository,
1596 1596 revision=commit_id, qualified=True),
1597 1597 'commit_id': commit_id,
1598 1598 'suf': suf
1599 1599 }
1600 1600
1601 1601 newtext = URL_PAT.sub(url_func, text_)
1602 1602
1603 1603 return newtext
1604 1604
1605 1605
1606 1606 def _process_url_func(match_obj, repo_name, uid, entry,
1607 1607 return_raw_data=False, link_format='html'):
1608 1608 pref = ''
1609 1609 if match_obj.group().startswith(' '):
1610 1610 pref = ' '
1611 1611
1612 1612 issue_id = ''.join(match_obj.groups())
1613 1613
1614 1614 if link_format == 'html':
1615 1615 tmpl = (
1616 1616 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1617 1617 '%(issue-prefix)s%(id-repr)s'
1618 1618 '</a>')
1619 1619 elif link_format == 'rst':
1620 1620 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1621 1621 elif link_format == 'markdown':
1622 1622 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1623 1623 else:
1624 1624 raise ValueError('Bad link_format:{}'.format(link_format))
1625 1625
1626 1626 (repo_name_cleaned,
1627 1627 parent_group_name) = RepoGroupModel().\
1628 1628 _get_group_name_and_parent(repo_name)
1629 1629
1630 1630 # variables replacement
1631 1631 named_vars = {
1632 1632 'id': issue_id,
1633 1633 'repo': repo_name,
1634 1634 'repo_name': repo_name_cleaned,
1635 1635 'group_name': parent_group_name
1636 1636 }
1637 1637 # named regex variables
1638 1638 named_vars.update(match_obj.groupdict())
1639 1639 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1640 1640
1641 1641 data = {
1642 1642 'pref': pref,
1643 1643 'cls': 'issue-tracker-link',
1644 1644 'url': _url,
1645 1645 'id-repr': issue_id,
1646 1646 'issue-prefix': entry['pref'],
1647 1647 'serv': entry['url'],
1648 1648 }
1649 1649 if return_raw_data:
1650 1650 return {
1651 1651 'id': issue_id,
1652 1652 'url': _url
1653 1653 }
1654 1654 return tmpl % data
1655 1655
1656 1656
1657 1657 def process_patterns(text_string, repo_name, link_format='html'):
1658 1658 allowed_formats = ['html', 'rst', 'markdown']
1659 1659 if link_format not in allowed_formats:
1660 1660 raise ValueError('Link format can be only one of:{} got {}'.format(
1661 1661 allowed_formats, link_format))
1662 1662
1663 1663 repo = None
1664 1664 if repo_name:
1665 1665 # Retrieving repo_name to avoid invalid repo_name to explode on
1666 1666 # IssueTrackerSettingsModel but still passing invalid name further down
1667 1667 repo = Repository.get_by_repo_name(repo_name, cache=True)
1668 1668
1669 1669 settings_model = IssueTrackerSettingsModel(repo=repo)
1670 1670 active_entries = settings_model.get_settings(cache=True)
1671 1671
1672 1672 issues_data = []
1673 1673 newtext = text_string
1674 1674
1675 1675 for uid, entry in active_entries.items():
1676 1676 log.debug('found issue tracker entry with uid %s' % (uid,))
1677 1677
1678 1678 if not (entry['pat'] and entry['url']):
1679 1679 log.debug('skipping due to missing data')
1680 1680 continue
1681 1681
1682 1682 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1683 1683 % (uid, entry['pat'], entry['url'], entry['pref']))
1684 1684
1685 1685 try:
1686 1686 pattern = re.compile(r'%s' % entry['pat'])
1687 1687 except re.error:
1688 1688 log.exception(
1689 1689 'issue tracker pattern: `%s` failed to compile',
1690 1690 entry['pat'])
1691 1691 continue
1692 1692
1693 1693 data_func = partial(
1694 1694 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1695 1695 return_raw_data=True)
1696 1696
1697 1697 for match_obj in pattern.finditer(text_string):
1698 1698 issues_data.append(data_func(match_obj))
1699 1699
1700 1700 url_func = partial(
1701 1701 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1702 1702 link_format=link_format)
1703 1703
1704 1704 newtext = pattern.sub(url_func, newtext)
1705 1705 log.debug('processed prefix:uid `%s`' % (uid,))
1706 1706
1707 1707 return newtext, issues_data
1708 1708
1709 1709
1710 1710 def urlify_commit_message(commit_text, repository=None):
1711 1711 """
1712 1712 Parses given text message and makes proper links.
1713 1713 issues are linked to given issue-server, and rest is a commit link
1714 1714
1715 1715 :param commit_text:
1716 1716 :param repository:
1717 1717 """
1718 1718 from pylons import url # doh, we need to re-import url to mock it later
1719 1719
1720 1720 def escaper(string):
1721 1721 return string.replace('<', '&lt;').replace('>', '&gt;')
1722 1722
1723 1723 newtext = escaper(commit_text)
1724 1724
1725 1725 # extract http/https links and make them real urls
1726 1726 newtext = urlify_text(newtext, safe=False)
1727 1727
1728 1728 # urlify commits - extract commit ids and make link out of them, if we have
1729 1729 # the scope of repository present.
1730 1730 if repository:
1731 1731 newtext = urlify_commits(newtext, repository)
1732 1732
1733 1733 # process issue tracker patterns
1734 1734 newtext, issues = process_patterns(newtext, repository or '')
1735 1735
1736 1736 return literal(newtext)
1737 1737
1738 1738
1739 1739 def render_binary(repo_name, file_obj):
1740 1740 """
1741 1741 Choose how to render a binary file
1742 1742 """
1743 1743 filename = file_obj.name
1744 1744
1745 1745 # images
1746 1746 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1747 1747 if fnmatch.fnmatch(filename, pat=ext):
1748 1748 alt = filename
1749 1749 src = url('files_raw_home', repo_name=repo_name,
1750 1750 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1751 1751 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1752 1752
1753 1753
1754 1754 def renderer_from_filename(filename, exclude=None):
1755 1755 """
1756 1756 choose a renderer based on filename, this works only for text based files
1757 1757 """
1758 1758
1759 1759 # ipython
1760 1760 for ext in ['*.ipynb']:
1761 1761 if fnmatch.fnmatch(filename, pat=ext):
1762 1762 return 'jupyter'
1763 1763
1764 1764 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1765 1765 if is_markup:
1766 1766 return is_markup
1767 1767 return None
1768 1768
1769 1769
1770 1770 def render(source, renderer='rst', mentions=False, relative_url=None,
1771 1771 repo_name=None):
1772 1772
1773 1773 def maybe_convert_relative_links(html_source):
1774 1774 if relative_url:
1775 1775 return relative_links(html_source, relative_url)
1776 1776 return html_source
1777 1777
1778 1778 if renderer == 'rst':
1779 1779 if repo_name:
1780 1780 # process patterns on comments if we pass in repo name
1781 1781 source, issues = process_patterns(
1782 1782 source, repo_name, link_format='rst')
1783 1783
1784 1784 return literal(
1785 1785 '<div class="rst-block">%s</div>' %
1786 1786 maybe_convert_relative_links(
1787 1787 MarkupRenderer.rst(source, mentions=mentions)))
1788 1788 elif renderer == 'markdown':
1789 1789 if repo_name:
1790 1790 # process patterns on comments if we pass in repo name
1791 1791 source, issues = process_patterns(
1792 1792 source, repo_name, link_format='markdown')
1793 1793
1794 1794 return literal(
1795 1795 '<div class="markdown-block">%s</div>' %
1796 1796 maybe_convert_relative_links(
1797 1797 MarkupRenderer.markdown(source, flavored=True,
1798 1798 mentions=mentions)))
1799 1799 elif renderer == 'jupyter':
1800 1800 return literal(
1801 1801 '<div class="ipynb">%s</div>' %
1802 1802 maybe_convert_relative_links(
1803 1803 MarkupRenderer.jupyter(source)))
1804 1804
1805 1805 # None means just show the file-source
1806 1806 return None
1807 1807
1808 1808
1809 1809 def commit_status(repo, commit_id):
1810 1810 return ChangesetStatusModel().get_status(repo, commit_id)
1811 1811
1812 1812
1813 1813 def commit_status_lbl(commit_status):
1814 1814 return dict(ChangesetStatus.STATUSES).get(commit_status)
1815 1815
1816 1816
1817 1817 def commit_time(repo_name, commit_id):
1818 1818 repo = Repository.get_by_repo_name(repo_name)
1819 1819 commit = repo.get_commit(commit_id=commit_id)
1820 1820 return commit.date
1821 1821
1822 1822
1823 1823 def get_permission_name(key):
1824 1824 return dict(Permission.PERMS).get(key)
1825 1825
1826 1826
1827 1827 def journal_filter_help():
1828 1828 return _(
1829 1829 'Example filter terms:\n' +
1830 1830 ' repository:vcs\n' +
1831 1831 ' username:marcin\n' +
1832 1832 ' action:*push*\n' +
1833 1833 ' ip:127.0.0.1\n' +
1834 1834 ' date:20120101\n' +
1835 1835 ' date:[20120101100000 TO 20120102]\n' +
1836 1836 '\n' +
1837 1837 'Generate wildcards using \'*\' character:\n' +
1838 1838 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1839 1839 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1840 1840 '\n' +
1841 1841 'Optional AND / OR operators in queries\n' +
1842 1842 ' "repository:vcs OR repository:test"\n' +
1843 1843 ' "username:test AND repository:test*"\n'
1844 1844 )
1845 1845
1846 1846
1847 1847 def search_filter_help(searcher):
1848 1848
1849 1849 terms = ''
1850 1850 return _(
1851 1851 'Example filter terms for `{searcher}` search:\n' +
1852 1852 '{terms}\n' +
1853 1853 'Generate wildcards using \'*\' character:\n' +
1854 1854 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1855 1855 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1856 1856 '\n' +
1857 1857 'Optional AND / OR operators in queries\n' +
1858 1858 ' "repo_name:vcs OR repo_name:test"\n' +
1859 1859 ' "owner:test AND repo_name:test*"\n' +
1860 1860 'More: {search_doc}'
1861 1861 ).format(searcher=searcher.name,
1862 1862 terms=terms, search_doc=searcher.query_lang_doc)
1863 1863
1864 1864
1865 1865 def not_mapped_error(repo_name):
1866 1866 flash(_('%s repository is not mapped to db perhaps'
1867 1867 ' it was created or renamed from the filesystem'
1868 1868 ' please run the application again'
1869 1869 ' in order to rescan repositories') % repo_name, category='error')
1870 1870
1871 1871
1872 1872 def ip_range(ip_addr):
1873 1873 from rhodecode.model.db import UserIpMap
1874 1874 s, e = UserIpMap._get_ip_range(ip_addr)
1875 1875 return '%s - %s' % (s, e)
1876 1876
1877 1877
1878 1878 def form(url, method='post', needs_csrf_token=True, **attrs):
1879 1879 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1880 1880 if method.lower() != 'get' and needs_csrf_token:
1881 1881 raise Exception(
1882 1882 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1883 1883 'CSRF token. If the endpoint does not require such token you can ' +
1884 1884 'explicitly set the parameter needs_csrf_token to false.')
1885 1885
1886 1886 return wh_form(url, method=method, **attrs)
1887 1887
1888 1888
1889 1889 def secure_form(url, method="POST", multipart=False, **attrs):
1890 1890 """Start a form tag that points the action to an url. This
1891 1891 form tag will also include the hidden field containing
1892 1892 the auth token.
1893 1893
1894 1894 The url options should be given either as a string, or as a
1895 1895 ``url()`` function. The method for the form defaults to POST.
1896 1896
1897 1897 Options:
1898 1898
1899 1899 ``multipart``
1900 1900 If set to True, the enctype is set to "multipart/form-data".
1901 1901 ``method``
1902 1902 The method to use when submitting the form, usually either
1903 1903 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1904 1904 hidden input with name _method is added to simulate the verb
1905 1905 over POST.
1906 1906
1907 1907 """
1908 1908 from webhelpers.pylonslib.secure_form import insecure_form
1909 1909 form = insecure_form(url, method, multipart, **attrs)
1910 1910 token = csrf_input()
1911 1911 return literal("%s\n%s" % (form, token))
1912 1912
1913 1913 def csrf_input():
1914 1914 return literal(
1915 1915 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1916 1916 csrf_token_key, csrf_token_key, get_csrf_token()))
1917 1917
1918 1918 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1919 1919 select_html = select(name, selected, options, **attrs)
1920 1920 select2 = """
1921 1921 <script>
1922 1922 $(document).ready(function() {
1923 1923 $('#%s').select2({
1924 1924 containerCssClass: 'drop-menu',
1925 1925 dropdownCssClass: 'drop-menu-dropdown',
1926 1926 dropdownAutoWidth: true%s
1927 1927 });
1928 1928 });
1929 1929 </script>
1930 1930 """
1931 1931 filter_option = """,
1932 1932 minimumResultsForSearch: -1
1933 1933 """
1934 1934 input_id = attrs.get('id') or name
1935 1935 filter_enabled = "" if enable_filter else filter_option
1936 1936 select_script = literal(select2 % (input_id, filter_enabled))
1937 1937
1938 1938 return literal(select_html+select_script)
1939 1939
1940 1940
1941 1941 def get_visual_attr(tmpl_context_var, attr_name):
1942 1942 """
1943 1943 A safe way to get a variable from visual variable of template context
1944 1944
1945 1945 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1946 1946 :param attr_name: name of the attribute we fetch from the c.visual
1947 1947 """
1948 1948 visual = getattr(tmpl_context_var, 'visual', None)
1949 1949 if not visual:
1950 1950 return
1951 1951 else:
1952 1952 return getattr(visual, attr_name, None)
1953 1953
1954 1954
1955 1955 def get_last_path_part(file_node):
1956 1956 if not file_node.path:
1957 1957 return u''
1958 1958
1959 1959 path = safe_unicode(file_node.path.split('/')[-1])
1960 1960 return u'../' + path
1961 1961
1962 1962
1963 def route_url(*args, **kwds):
1963 def route_url(*args, **kwargs):
1964 1964 """
1965 1965 Wrapper around pyramids `route_url` (fully qualified url) function.
1966 1966 It is used to generate URLs from within pylons views or templates.
1967 1967 This will be removed when pyramid migration if finished.
1968 1968 """
1969 1969 req = get_current_request()
1970 return req.route_url(*args, **kwds)
1970 return req.route_url(*args, **kwargs)
1971 1971
1972 1972
1973 def route_path(*args, **kwds):
1973 def route_path(*args, **kwargs):
1974 1974 """
1975 1975 Wrapper around pyramids `route_path` function. It is used to generate
1976 1976 URLs from within pylons views or templates. This will be removed when
1977 1977 pyramid migration if finished.
1978 1978 """
1979 1979 req = get_current_request()
1980 return req.route_path(*args, **kwds)
1980 return req.route_path(*args, **kwargs)
1981 1981
1982 1982
1983 1983 def route_path_or_none(*args, **kwargs):
1984 1984 try:
1985 1985 return route_path(*args, **kwargs)
1986 1986 except KeyError:
1987 1987 return None
1988 1988
1989 1989
1990 1990 def static_url(*args, **kwds):
1991 1991 """
1992 1992 Wrapper around pyramids `route_path` function. It is used to generate
1993 1993 URLs from within pylons views or templates. This will be removed when
1994 1994 pyramid migration if finished.
1995 1995 """
1996 1996 req = get_current_request()
1997 1997 return req.static_url(*args, **kwds)
1998 1998
1999 1999
2000 2000 def resource_path(*args, **kwds):
2001 2001 """
2002 2002 Wrapper around pyramids `route_path` function. It is used to generate
2003 2003 URLs from within pylons views or templates. This will be removed when
2004 2004 pyramid migration if finished.
2005 2005 """
2006 2006 req = get_current_request()
2007 2007 return req.resource_path(*args, **kwds)
2008 2008
2009 2009
2010 2010 def api_call_example(method, args):
2011 2011 """
2012 2012 Generates an API call example via CURL
2013 2013 """
2014 2014 args_json = json.dumps(OrderedDict([
2015 2015 ('id', 1),
2016 2016 ('auth_token', 'SECRET'),
2017 2017 ('method', method),
2018 2018 ('args', args)
2019 2019 ]))
2020 2020 return literal(
2021 2021 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2022 2022 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2023 2023 "and needs to be of `api calls` role."
2024 2024 .format(
2025 2025 api_url=route_url('apiv2'),
2026 2026 token_url=route_url('my_account_auth_tokens'),
2027 2027 data=args_json))
@@ -1,1043 +1,1044 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 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 import hashlib
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from paste.script.command import Command, BadCommand
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from mako import exceptions
44 44 from pyramid.threadlocal import get_current_registry
45 45 from pyramid.request import Request
46 46
47 47 from rhodecode.lib.fakemod import create_module
48 48 from rhodecode.lib.vcs.backends.base import Config
49 49 from rhodecode.lib.vcs.exceptions import VCSError
50 50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 51 from rhodecode.lib.utils2 import (
52 52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 53 from rhodecode.model import meta
54 54 from rhodecode.model.db import (
55 55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 62
63 63 # String which contains characters that are not allowed in slug names for
64 64 # repositories or repository groups. It is properly escaped to use it in
65 65 # regular expressions.
66 66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67 67
68 68 # Regex that matches forbidden characters in repo/group slugs.
69 69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70 70
71 71 # Regex that matches allowed characters in repo/group slugs.
72 72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73 73
74 74 # Regex that matches whole repo/group slugs.
75 75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76 76
77 77 _license_cache = None
78 78
79 79
80 80 def repo_name_slug(value):
81 81 """
82 82 Return slug of name of repository
83 83 This function is called on each creation/modification
84 84 of repository to prevent bad names in repo
85 85 """
86 86 replacement_char = '-'
87 87
88 88 slug = remove_formatting(value)
89 89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 90 slug = re.sub('[\s]+', '-', slug)
91 91 slug = collapse(slug, replacement_char)
92 92 return slug
93 93
94 94
95 95 #==============================================================================
96 96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 97 #==============================================================================
98 98 def get_repo_slug(request):
99 if isinstance(request, Request) and getattr(request, 'matchdict', None):
99 if isinstance(request, Request) and getattr(request, 'db_repo', None):
100 100 # pyramid
101 _repo = request.matchdict.get('repo_name')
101 _repo = request.db_repo.repo_name
102 102 else:
103 # TODO(marcink): remove after pylons migration...
103 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104 105
105 106 if _repo:
106 107 _repo = _repo.rstrip('/')
107 108 return _repo
108 109
109 110
110 111 def get_repo_group_slug(request):
111 112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
112 113 # pyramid
113 _group = request.matchdict.get('group_name')
114 _group = request.matchdict.get('repo_group_name')
114 115 else:
115 116 _group = request.environ['pylons.routes_dict'].get('group_name')
116 117
117 118 if _group:
118 119 _group = _group.rstrip('/')
119 120 return _group
120 121
121 122
122 123 def get_user_group_slug(request):
123 124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
124 125 # pyramid
125 126 _group = request.matchdict.get('user_group_id')
126 127 else:
127 128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
128 129
129 130 try:
130 131 _group = UserGroup.get(_group)
131 132 if _group:
132 133 _group = _group.users_group_name
133 134 except Exception:
134 135 log.debug(traceback.format_exc())
135 136 # catch all failures here
136 137 pass
137 138
138 139 return _group
139 140
140 141
141 142 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
142 143 """
143 144 Action logger for various actions made by users
144 145
145 146 :param user: user that made this action, can be a unique username string or
146 147 object containing user_id attribute
147 148 :param action: action to log, should be on of predefined unique actions for
148 149 easy translations
149 150 :param repo: string name of repository or object containing repo_id,
150 151 that action was made on
151 152 :param ipaddr: optional ip address from what the action was made
152 153 :param sa: optional sqlalchemy session
153 154
154 155 """
155 156
156 157 if not sa:
157 158 sa = meta.Session()
158 159 # if we don't get explicit IP address try to get one from registered user
159 160 # in tmpl context var
160 161 if not ipaddr:
161 162 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
162 163
163 164 try:
164 165 if getattr(user, 'user_id', None):
165 166 user_obj = User.get(user.user_id)
166 167 elif isinstance(user, basestring):
167 168 user_obj = User.get_by_username(user)
168 169 else:
169 170 raise Exception('You have to provide a user object or a username')
170 171
171 172 if getattr(repo, 'repo_id', None):
172 173 repo_obj = Repository.get(repo.repo_id)
173 174 repo_name = repo_obj.repo_name
174 175 elif isinstance(repo, basestring):
175 176 repo_name = repo.lstrip('/')
176 177 repo_obj = Repository.get_by_repo_name(repo_name)
177 178 else:
178 179 repo_obj = None
179 180 repo_name = ''
180 181
181 182 user_log = UserLog()
182 183 user_log.user_id = user_obj.user_id
183 184 user_log.username = user_obj.username
184 185 action = safe_unicode(action)
185 186 user_log.action = action[:1200000]
186 187
187 188 user_log.repository = repo_obj
188 189 user_log.repository_name = repo_name
189 190
190 191 user_log.action_date = datetime.datetime.now()
191 192 user_log.user_ip = ipaddr
192 193 sa.add(user_log)
193 194
194 195 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
195 196 action, safe_unicode(repo), user_obj, ipaddr)
196 197 if commit:
197 198 sa.commit()
198 199 except Exception:
199 200 log.error(traceback.format_exc())
200 201 raise
201 202
202 203
203 204 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
204 205 """
205 206 Scans given path for repos and return (name,(type,path)) tuple
206 207
207 208 :param path: path to scan for repositories
208 209 :param recursive: recursive search and return names with subdirs in front
209 210 """
210 211
211 212 # remove ending slash for better results
212 213 path = path.rstrip(os.sep)
213 214 log.debug('now scanning in %s location recursive:%s...', path, recursive)
214 215
215 216 def _get_repos(p):
216 217 dirpaths = _get_dirpaths(p)
217 218 if not _is_dir_writable(p):
218 219 log.warning('repo path without write access: %s', p)
219 220
220 221 for dirpath in dirpaths:
221 222 if os.path.isfile(os.path.join(p, dirpath)):
222 223 continue
223 224 cur_path = os.path.join(p, dirpath)
224 225
225 226 # skip removed repos
226 227 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
227 228 continue
228 229
229 230 #skip .<somethin> dirs
230 231 if dirpath.startswith('.'):
231 232 continue
232 233
233 234 try:
234 235 scm_info = get_scm(cur_path)
235 236 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
236 237 except VCSError:
237 238 if not recursive:
238 239 continue
239 240 #check if this dir containts other repos for recursive scan
240 241 rec_path = os.path.join(p, dirpath)
241 242 if os.path.isdir(rec_path):
242 243 for inner_scm in _get_repos(rec_path):
243 244 yield inner_scm
244 245
245 246 return _get_repos(path)
246 247
247 248
248 249 def _get_dirpaths(p):
249 250 try:
250 251 # OS-independable way of checking if we have at least read-only
251 252 # access or not.
252 253 dirpaths = os.listdir(p)
253 254 except OSError:
254 255 log.warning('ignoring repo path without read access: %s', p)
255 256 return []
256 257
257 258 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
258 259 # decode paths and suddenly returns unicode objects itself. The items it
259 260 # cannot decode are returned as strings and cause issues.
260 261 #
261 262 # Those paths are ignored here until a solid solution for path handling has
262 263 # been built.
263 264 expected_type = type(p)
264 265
265 266 def _has_correct_type(item):
266 267 if type(item) is not expected_type:
267 268 log.error(
268 269 u"Ignoring path %s since it cannot be decoded into unicode.",
269 270 # Using "repr" to make sure that we see the byte value in case
270 271 # of support.
271 272 repr(item))
272 273 return False
273 274 return True
274 275
275 276 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
276 277
277 278 return dirpaths
278 279
279 280
280 281 def _is_dir_writable(path):
281 282 """
282 283 Probe if `path` is writable.
283 284
284 285 Due to trouble on Cygwin / Windows, this is actually probing if it is
285 286 possible to create a file inside of `path`, stat does not produce reliable
286 287 results in this case.
287 288 """
288 289 try:
289 290 with tempfile.TemporaryFile(dir=path):
290 291 pass
291 292 except OSError:
292 293 return False
293 294 return True
294 295
295 296
296 297 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
297 298 """
298 299 Returns True if given path is a valid repository False otherwise.
299 300 If expect_scm param is given also, compare if given scm is the same
300 301 as expected from scm parameter. If explicit_scm is given don't try to
301 302 detect the scm, just use the given one to check if repo is valid
302 303
303 304 :param repo_name:
304 305 :param base_path:
305 306 :param expect_scm:
306 307 :param explicit_scm:
307 308
308 309 :return True: if given path is a valid repository
309 310 """
310 311 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
311 312 log.debug('Checking if `%s` is a valid path for repository. '
312 313 'Explicit type: %s', repo_name, explicit_scm)
313 314
314 315 try:
315 316 if explicit_scm:
316 317 detected_scms = [get_scm_backend(explicit_scm)]
317 318 else:
318 319 detected_scms = get_scm(full_path)
319 320
320 321 if expect_scm:
321 322 return detected_scms[0] == expect_scm
322 323 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
323 324 return True
324 325 except VCSError:
325 326 log.debug('path: %s is not a valid repo !', full_path)
326 327 return False
327 328
328 329
329 330 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
330 331 """
331 332 Returns True if given path is a repository group, False otherwise
332 333
333 334 :param repo_name:
334 335 :param base_path:
335 336 """
336 337 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
337 338 log.debug('Checking if `%s` is a valid path for repository group',
338 339 repo_group_name)
339 340
340 341 # check if it's not a repo
341 342 if is_valid_repo(repo_group_name, base_path):
342 343 log.debug('Repo called %s exist, it is not a valid '
343 344 'repo group' % repo_group_name)
344 345 return False
345 346
346 347 try:
347 348 # we need to check bare git repos at higher level
348 349 # since we might match branches/hooks/info/objects or possible
349 350 # other things inside bare git repo
350 351 scm_ = get_scm(os.path.dirname(full_path))
351 352 log.debug('path: %s is a vcs object:%s, not valid '
352 353 'repo group' % (full_path, scm_))
353 354 return False
354 355 except VCSError:
355 356 pass
356 357
357 358 # check if it's a valid path
358 359 if skip_path_check or os.path.isdir(full_path):
359 360 log.debug('path: %s is a valid repo group !', full_path)
360 361 return True
361 362
362 363 log.debug('path: %s is not a valid repo group !', full_path)
363 364 return False
364 365
365 366
366 367 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
367 368 while True:
368 369 ok = raw_input(prompt)
369 370 if ok.lower() in ('y', 'ye', 'yes'):
370 371 return True
371 372 if ok.lower() in ('n', 'no', 'nop', 'nope'):
372 373 return False
373 374 retries = retries - 1
374 375 if retries < 0:
375 376 raise IOError
376 377 print(complaint)
377 378
378 379 # propagated from mercurial documentation
379 380 ui_sections = [
380 381 'alias', 'auth',
381 382 'decode/encode', 'defaults',
382 383 'diff', 'email',
383 384 'extensions', 'format',
384 385 'merge-patterns', 'merge-tools',
385 386 'hooks', 'http_proxy',
386 387 'smtp', 'patch',
387 388 'paths', 'profiling',
388 389 'server', 'trusted',
389 390 'ui', 'web', ]
390 391
391 392
392 393 def config_data_from_db(clear_session=True, repo=None):
393 394 """
394 395 Read the configuration data from the database and return configuration
395 396 tuples.
396 397 """
397 398 from rhodecode.model.settings import VcsSettingsModel
398 399
399 400 config = []
400 401
401 402 sa = meta.Session()
402 403 settings_model = VcsSettingsModel(repo=repo, sa=sa)
403 404
404 405 ui_settings = settings_model.get_ui_settings()
405 406
406 407 for setting in ui_settings:
407 408 if setting.active:
408 409 log.debug(
409 410 'settings ui from db: [%s] %s=%s',
410 411 setting.section, setting.key, setting.value)
411 412 config.append((
412 413 safe_str(setting.section), safe_str(setting.key),
413 414 safe_str(setting.value)))
414 415 if setting.key == 'push_ssl':
415 416 # force set push_ssl requirement to False, rhodecode
416 417 # handles that
417 418 config.append((
418 419 safe_str(setting.section), safe_str(setting.key), False))
419 420 if clear_session:
420 421 meta.Session.remove()
421 422
422 423 # TODO: mikhail: probably it makes no sense to re-read hooks information.
423 424 # It's already there and activated/deactivated
424 425 skip_entries = []
425 426 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
426 427 if 'pull' not in enabled_hook_classes:
427 428 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
428 429 if 'push' not in enabled_hook_classes:
429 430 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
430 431 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
431 432 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
432 433
433 434 config = [entry for entry in config if entry[:2] not in skip_entries]
434 435
435 436 return config
436 437
437 438
438 439 def make_db_config(clear_session=True, repo=None):
439 440 """
440 441 Create a :class:`Config` instance based on the values in the database.
441 442 """
442 443 config = Config()
443 444 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
444 445 for section, option, value in config_data:
445 446 config.set(section, option, value)
446 447 return config
447 448
448 449
449 450 def get_enabled_hook_classes(ui_settings):
450 451 """
451 452 Return the enabled hook classes.
452 453
453 454 :param ui_settings: List of ui_settings as returned
454 455 by :meth:`VcsSettingsModel.get_ui_settings`
455 456
456 457 :return: a list with the enabled hook classes. The order is not guaranteed.
457 458 :rtype: list
458 459 """
459 460 enabled_hooks = []
460 461 active_hook_keys = [
461 462 key for section, key, value, active in ui_settings
462 463 if section == 'hooks' and active]
463 464
464 465 hook_names = {
465 466 RhodeCodeUi.HOOK_PUSH: 'push',
466 467 RhodeCodeUi.HOOK_PULL: 'pull',
467 468 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
468 469 }
469 470
470 471 for key in active_hook_keys:
471 472 hook = hook_names.get(key)
472 473 if hook:
473 474 enabled_hooks.append(hook)
474 475
475 476 return enabled_hooks
476 477
477 478
478 479 def set_rhodecode_config(config):
479 480 """
480 481 Updates pylons config with new settings from database
481 482
482 483 :param config:
483 484 """
484 485 from rhodecode.model.settings import SettingsModel
485 486 app_settings = SettingsModel().get_all_settings()
486 487
487 488 for k, v in app_settings.items():
488 489 config[k] = v
489 490
490 491
491 492 def get_rhodecode_realm():
492 493 """
493 494 Return the rhodecode realm from database.
494 495 """
495 496 from rhodecode.model.settings import SettingsModel
496 497 realm = SettingsModel().get_setting_by_name('realm')
497 498 return safe_str(realm.app_settings_value)
498 499
499 500
500 501 def get_rhodecode_base_path():
501 502 """
502 503 Returns the base path. The base path is the filesystem path which points
503 504 to the repository store.
504 505 """
505 506 from rhodecode.model.settings import SettingsModel
506 507 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
507 508 return safe_str(paths_ui.ui_value)
508 509
509 510
510 511 def map_groups(path):
511 512 """
512 513 Given a full path to a repository, create all nested groups that this
513 514 repo is inside. This function creates parent-child relationships between
514 515 groups and creates default perms for all new groups.
515 516
516 517 :param paths: full path to repository
517 518 """
518 519 from rhodecode.model.repo_group import RepoGroupModel
519 520 sa = meta.Session()
520 521 groups = path.split(Repository.NAME_SEP)
521 522 parent = None
522 523 group = None
523 524
524 525 # last element is repo in nested groups structure
525 526 groups = groups[:-1]
526 527 rgm = RepoGroupModel(sa)
527 528 owner = User.get_first_super_admin()
528 529 for lvl, group_name in enumerate(groups):
529 530 group_name = '/'.join(groups[:lvl] + [group_name])
530 531 group = RepoGroup.get_by_group_name(group_name)
531 532 desc = '%s group' % group_name
532 533
533 534 # skip folders that are now removed repos
534 535 if REMOVED_REPO_PAT.match(group_name):
535 536 break
536 537
537 538 if group is None:
538 539 log.debug('creating group level: %s group_name: %s',
539 540 lvl, group_name)
540 541 group = RepoGroup(group_name, parent)
541 542 group.group_description = desc
542 543 group.user = owner
543 544 sa.add(group)
544 545 perm_obj = rgm._create_default_perms(group)
545 546 sa.add(perm_obj)
546 547 sa.flush()
547 548
548 549 parent = group
549 550 return group
550 551
551 552
552 553 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
553 554 """
554 555 maps all repos given in initial_repo_list, non existing repositories
555 556 are created, if remove_obsolete is True it also checks for db entries
556 557 that are not in initial_repo_list and removes them.
557 558
558 559 :param initial_repo_list: list of repositories found by scanning methods
559 560 :param remove_obsolete: check for obsolete entries in database
560 561 """
561 562 from rhodecode.model.repo import RepoModel
562 563 from rhodecode.model.scm import ScmModel
563 564 from rhodecode.model.repo_group import RepoGroupModel
564 565 from rhodecode.model.settings import SettingsModel
565 566
566 567 sa = meta.Session()
567 568 repo_model = RepoModel()
568 569 user = User.get_first_super_admin()
569 570 added = []
570 571
571 572 # creation defaults
572 573 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
573 574 enable_statistics = defs.get('repo_enable_statistics')
574 575 enable_locking = defs.get('repo_enable_locking')
575 576 enable_downloads = defs.get('repo_enable_downloads')
576 577 private = defs.get('repo_private')
577 578
578 579 for name, repo in initial_repo_list.items():
579 580 group = map_groups(name)
580 581 unicode_name = safe_unicode(name)
581 582 db_repo = repo_model.get_by_repo_name(unicode_name)
582 583 # found repo that is on filesystem not in RhodeCode database
583 584 if not db_repo:
584 585 log.info('repository %s not found, creating now', name)
585 586 added.append(name)
586 587 desc = (repo.description
587 588 if repo.description != 'unknown'
588 589 else '%s repository' % name)
589 590
590 591 db_repo = repo_model._create_repo(
591 592 repo_name=name,
592 593 repo_type=repo.alias,
593 594 description=desc,
594 595 repo_group=getattr(group, 'group_id', None),
595 596 owner=user,
596 597 enable_locking=enable_locking,
597 598 enable_downloads=enable_downloads,
598 599 enable_statistics=enable_statistics,
599 600 private=private,
600 601 state=Repository.STATE_CREATED
601 602 )
602 603 sa.commit()
603 604 # we added that repo just now, and make sure we updated server info
604 605 if db_repo.repo_type == 'git':
605 606 git_repo = db_repo.scm_instance()
606 607 # update repository server-info
607 608 log.debug('Running update server info')
608 609 git_repo._update_server_info()
609 610
610 611 db_repo.update_commit_cache()
611 612
612 613 config = db_repo._config
613 614 config.set('extensions', 'largefiles', '')
614 615 ScmModel().install_hooks(
615 616 db_repo.scm_instance(config=config),
616 617 repo_type=db_repo.repo_type)
617 618
618 619 removed = []
619 620 if remove_obsolete:
620 621 # remove from database those repositories that are not in the filesystem
621 622 for repo in sa.query(Repository).all():
622 623 if repo.repo_name not in initial_repo_list.keys():
623 624 log.debug("Removing non-existing repository found in db `%s`",
624 625 repo.repo_name)
625 626 try:
626 627 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
627 628 sa.commit()
628 629 removed.append(repo.repo_name)
629 630 except Exception:
630 631 # don't hold further removals on error
631 632 log.error(traceback.format_exc())
632 633 sa.rollback()
633 634
634 635 def splitter(full_repo_name):
635 636 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
636 637 gr_name = None
637 638 if len(_parts) == 2:
638 639 gr_name = _parts[0]
639 640 return gr_name
640 641
641 642 initial_repo_group_list = [splitter(x) for x in
642 643 initial_repo_list.keys() if splitter(x)]
643 644
644 645 # remove from database those repository groups that are not in the
645 646 # filesystem due to parent child relationships we need to delete them
646 647 # in a specific order of most nested first
647 648 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
648 649 nested_sort = lambda gr: len(gr.split('/'))
649 650 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
650 651 if group_name not in initial_repo_group_list:
651 652 repo_group = RepoGroup.get_by_group_name(group_name)
652 653 if (repo_group.children.all() or
653 654 not RepoGroupModel().check_exist_filesystem(
654 655 group_name=group_name, exc_on_failure=False)):
655 656 continue
656 657
657 658 log.info(
658 659 'Removing non-existing repository group found in db `%s`',
659 660 group_name)
660 661 try:
661 662 RepoGroupModel(sa).delete(group_name, fs_remove=False)
662 663 sa.commit()
663 664 removed.append(group_name)
664 665 except Exception:
665 666 # don't hold further removals on error
666 667 log.exception(
667 668 'Unable to remove repository group `%s`',
668 669 group_name)
669 670 sa.rollback()
670 671 raise
671 672
672 673 return added, removed
673 674
674 675
675 676 def get_default_cache_settings(settings):
676 677 cache_settings = {}
677 678 for key in settings.keys():
678 679 for prefix in ['beaker.cache.', 'cache.']:
679 680 if key.startswith(prefix):
680 681 name = key.split(prefix)[1].strip()
681 682 cache_settings[name] = settings[key].strip()
682 683 return cache_settings
683 684
684 685
685 686 # set cache regions for beaker so celery can utilise it
686 687 def add_cache(settings):
687 688 from rhodecode.lib import caches
688 689 cache_settings = {'regions': None}
689 690 # main cache settings used as default ...
690 691 cache_settings.update(get_default_cache_settings(settings))
691 692
692 693 if cache_settings['regions']:
693 694 for region in cache_settings['regions'].split(','):
694 695 region = region.strip()
695 696 region_settings = {}
696 697 for key, value in cache_settings.items():
697 698 if key.startswith(region):
698 699 region_settings[key.split('.')[1]] = value
699 700
700 701 caches.configure_cache_region(
701 702 region, region_settings, cache_settings)
702 703
703 704
704 705 def load_rcextensions(root_path):
705 706 import rhodecode
706 707 from rhodecode.config import conf
707 708
708 709 path = os.path.join(root_path, 'rcextensions', '__init__.py')
709 710 if os.path.isfile(path):
710 711 rcext = create_module('rc', path)
711 712 EXT = rhodecode.EXTENSIONS = rcext
712 713 log.debug('Found rcextensions now loading %s...', rcext)
713 714
714 715 # Additional mappings that are not present in the pygments lexers
715 716 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
716 717
717 718 # auto check if the module is not missing any data, set to default if is
718 719 # this will help autoupdate new feature of rcext module
719 720 #from rhodecode.config import rcextensions
720 721 #for k in dir(rcextensions):
721 722 # if not k.startswith('_') and not hasattr(EXT, k):
722 723 # setattr(EXT, k, getattr(rcextensions, k))
723 724
724 725
725 726 def get_custom_lexer(extension):
726 727 """
727 728 returns a custom lexer if it is defined in rcextensions module, or None
728 729 if there's no custom lexer defined
729 730 """
730 731 import rhodecode
731 732 from pygments import lexers
732 733
733 734 # custom override made by RhodeCode
734 735 if extension in ['mako']:
735 736 return lexers.get_lexer_by_name('html+mako')
736 737
737 738 # check if we didn't define this extension as other lexer
738 739 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
739 740 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
740 741 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
741 742 return lexers.get_lexer_by_name(_lexer_name)
742 743
743 744
744 745 #==============================================================================
745 746 # TEST FUNCTIONS AND CREATORS
746 747 #==============================================================================
747 748 def create_test_index(repo_location, config):
748 749 """
749 750 Makes default test index.
750 751 """
751 752 import rc_testdata
752 753
753 754 rc_testdata.extract_search_index(
754 755 'vcs_search_index', os.path.dirname(config['search.location']))
755 756
756 757
757 758 def create_test_directory(test_path):
758 759 """
759 760 Create test directory if it doesn't exist.
760 761 """
761 762 if not os.path.isdir(test_path):
762 763 log.debug('Creating testdir %s', test_path)
763 764 os.makedirs(test_path)
764 765
765 766
766 767 def create_test_database(test_path, config):
767 768 """
768 769 Makes a fresh database.
769 770 """
770 771 from rhodecode.lib.db_manage import DbManage
771 772
772 773 # PART ONE create db
773 774 dbconf = config['sqlalchemy.db1.url']
774 775 log.debug('making test db %s', dbconf)
775 776
776 777 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
777 778 tests=True, cli_args={'force_ask': True})
778 779 dbmanage.create_tables(override=True)
779 780 dbmanage.set_db_version()
780 781 # for tests dynamically set new root paths based on generated content
781 782 dbmanage.create_settings(dbmanage.config_prompt(test_path))
782 783 dbmanage.create_default_user()
783 784 dbmanage.create_test_admin_and_users()
784 785 dbmanage.create_permissions()
785 786 dbmanage.populate_default_permissions()
786 787 Session().commit()
787 788
788 789
789 790 def create_test_repositories(test_path, config):
790 791 """
791 792 Creates test repositories in the temporary directory. Repositories are
792 793 extracted from archives within the rc_testdata package.
793 794 """
794 795 import rc_testdata
795 796 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
796 797
797 798 log.debug('making test vcs repositories')
798 799
799 800 idx_path = config['search.location']
800 801 data_path = config['cache_dir']
801 802
802 803 # clean index and data
803 804 if idx_path and os.path.exists(idx_path):
804 805 log.debug('remove %s', idx_path)
805 806 shutil.rmtree(idx_path)
806 807
807 808 if data_path and os.path.exists(data_path):
808 809 log.debug('remove %s', data_path)
809 810 shutil.rmtree(data_path)
810 811
811 812 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
812 813 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
813 814
814 815 # Note: Subversion is in the process of being integrated with the system,
815 816 # until we have a properly packed version of the test svn repository, this
816 817 # tries to copy over the repo from a package "rc_testdata"
817 818 svn_repo_path = rc_testdata.get_svn_repo_archive()
818 819 with tarfile.open(svn_repo_path) as tar:
819 820 tar.extractall(jn(test_path, SVN_REPO))
820 821
821 822
822 823 #==============================================================================
823 824 # PASTER COMMANDS
824 825 #==============================================================================
825 826 class BasePasterCommand(Command):
826 827 """
827 828 Abstract Base Class for paster commands.
828 829
829 830 The celery commands are somewhat aggressive about loading
830 831 celery.conf, and since our module sets the `CELERY_LOADER`
831 832 environment variable to our loader, we have to bootstrap a bit and
832 833 make sure we've had a chance to load the pylons config off of the
833 834 command line, otherwise everything fails.
834 835 """
835 836 min_args = 1
836 837 min_args_error = "Please provide a paster config file as an argument."
837 838 takes_config_file = 1
838 839 requires_config_file = True
839 840
840 841 def notify_msg(self, msg, log=False):
841 842 """Make a notification to user, additionally if logger is passed
842 843 it logs this action using given logger
843 844
844 845 :param msg: message that will be printed to user
845 846 :param log: logging instance, to use to additionally log this message
846 847
847 848 """
848 849 if log and isinstance(log, logging):
849 850 log(msg)
850 851
851 852 def run(self, args):
852 853 """
853 854 Overrides Command.run
854 855
855 856 Checks for a config file argument and loads it.
856 857 """
857 858 if len(args) < self.min_args:
858 859 raise BadCommand(
859 860 self.min_args_error % {'min_args': self.min_args,
860 861 'actual_args': len(args)})
861 862
862 863 # Decrement because we're going to lob off the first argument.
863 864 # @@ This is hacky
864 865 self.min_args -= 1
865 866 self.bootstrap_config(args[0])
866 867 self.update_parser()
867 868 return super(BasePasterCommand, self).run(args[1:])
868 869
869 870 def update_parser(self):
870 871 """
871 872 Abstract method. Allows for the class' parser to be updated
872 873 before the superclass' `run` method is called. Necessary to
873 874 allow options/arguments to be passed through to the underlying
874 875 celery command.
875 876 """
876 877 raise NotImplementedError("Abstract Method.")
877 878
878 879 def bootstrap_config(self, conf):
879 880 """
880 881 Loads the pylons configuration.
881 882 """
882 883 from pylons import config as pylonsconfig
883 884
884 885 self.path_to_ini_file = os.path.realpath(conf)
885 886 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
886 887 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
887 888
888 889 def _init_session(self):
889 890 """
890 891 Inits SqlAlchemy Session
891 892 """
892 893 logging.config.fileConfig(self.path_to_ini_file)
893 894 from pylons import config
894 895 from rhodecode.config.utils import initialize_database
895 896
896 897 # get to remove repos !!
897 898 add_cache(config)
898 899 initialize_database(config)
899 900
900 901
901 902 @decorator.decorator
902 903 def jsonify(func, *args, **kwargs):
903 904 """Action decorator that formats output for JSON
904 905
905 906 Given a function that will return content, this decorator will turn
906 907 the result into JSON, with a content-type of 'application/json' and
907 908 output it.
908 909
909 910 """
910 911 from pylons.decorators.util import get_pylons
911 912 from rhodecode.lib.ext_json import json
912 913 pylons = get_pylons(args)
913 914 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
914 915 data = func(*args, **kwargs)
915 916 if isinstance(data, (list, tuple)):
916 917 msg = "JSON responses with Array envelopes are susceptible to " \
917 918 "cross-site data leak attacks, see " \
918 919 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
919 920 warnings.warn(msg, Warning, 2)
920 921 log.warning(msg)
921 922 log.debug("Returning JSON wrapped action output")
922 923 return json.dumps(data, encoding='utf-8')
923 924
924 925
925 926 class PartialRenderer(object):
926 927 """
927 928 Partial renderer used to render chunks of html used in datagrids
928 929 use like::
929 930
930 931 _render = PartialRenderer('data_table/_dt_elements.mako')
931 932 _render('quick_menu', args, kwargs)
932 933 PartialRenderer.h,
933 934 c,
934 935 _,
935 936 ungettext
936 937 are the template stuff initialized inside and can be re-used later
937 938
938 939 :param tmpl_name: template path relate to /templates/ dir
939 940 """
940 941
941 942 def __init__(self, tmpl_name):
942 943 import rhodecode
943 944 from pylons import request, tmpl_context as c
944 945 from pylons.i18n.translation import _, ungettext
945 946 from rhodecode.lib import helpers as h
946 947
947 948 self.tmpl_name = tmpl_name
948 949 self.rhodecode = rhodecode
949 950 self.c = c
950 951 self._ = _
951 952 self.ungettext = ungettext
952 953 self.h = h
953 954 self.request = request
954 955
955 956 def _mako_lookup(self):
956 957 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
957 958 return _tmpl_lookup.get_template(self.tmpl_name)
958 959
959 960 def _update_kwargs_for_render(self, kwargs):
960 961 """
961 962 Inject params required for Mako rendering
962 963 """
963 964 _kwargs = {
964 965 '_': self._,
965 966 'h': self.h,
966 967 'c': self.c,
967 968 'request': self.request,
968 969 'ungettext': self.ungettext,
969 970 }
970 971 _kwargs.update(kwargs)
971 972 return _kwargs
972 973
973 974 def _render_with_exc(self, render_func, args, kwargs):
974 975 try:
975 976 return render_func.render(*args, **kwargs)
976 977 except:
977 978 log.error(exceptions.text_error_template().render())
978 979 raise
979 980
980 981 def _get_template(self, template_obj, def_name):
981 982 if def_name:
982 983 tmpl = template_obj.get_def(def_name)
983 984 else:
984 985 tmpl = template_obj
985 986 return tmpl
986 987
987 988 def render(self, def_name, *args, **kwargs):
988 989 lookup_obj = self._mako_lookup()
989 990 tmpl = self._get_template(lookup_obj, def_name=def_name)
990 991 kwargs = self._update_kwargs_for_render(kwargs)
991 992 return self._render_with_exc(tmpl, args, kwargs)
992 993
993 994 def __call__(self, tmpl, *args, **kwargs):
994 995 return self.render(tmpl, *args, **kwargs)
995 996
996 997
997 998 def password_changed(auth_user, session):
998 999 # Never report password change in case of default user or anonymous user.
999 1000 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
1000 1001 return False
1001 1002
1002 1003 password_hash = md5(auth_user.password) if auth_user.password else None
1003 1004 rhodecode_user = session.get('rhodecode_user', {})
1004 1005 session_password_hash = rhodecode_user.get('password', '')
1005 1006 return password_hash != session_password_hash
1006 1007
1007 1008
1008 1009 def read_opensource_licenses():
1009 1010 global _license_cache
1010 1011
1011 1012 if not _license_cache:
1012 1013 licenses = pkg_resources.resource_string(
1013 1014 'rhodecode', 'config/licenses.json')
1014 1015 _license_cache = json.loads(licenses)
1015 1016
1016 1017 return _license_cache
1017 1018
1018 1019
1019 1020 def get_registry(request):
1020 1021 """
1021 1022 Utility to get the pyramid registry from a request. During migration to
1022 1023 pyramid we sometimes want to use the pyramid registry from pylons context.
1023 1024 Therefore this utility returns `request.registry` for pyramid requests and
1024 1025 uses `get_current_registry()` for pylons requests.
1025 1026 """
1026 1027 try:
1027 1028 return request.registry
1028 1029 except AttributeError:
1029 1030 return get_current_registry()
1030 1031
1031 1032
1032 1033 def generate_platform_uuid():
1033 1034 """
1034 1035 Generates platform UUID based on it's name
1035 1036 """
1036 1037 import platform
1037 1038
1038 1039 try:
1039 1040 uuid_list = [platform.platform()]
1040 1041 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
1041 1042 except Exception as e:
1042 1043 log.error('Failed to generate host uuid: %s' % e)
1043 1044 return 'UNDEFINED'
@@ -1,962 +1,963 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-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 """
23 23 Some simple helper functions
24 24 """
25 25
26 26
27 27 import collections
28 28 import datetime
29 29 import dateutil.relativedelta
30 30 import hashlib
31 31 import logging
32 32 import re
33 33 import sys
34 34 import time
35 35 import threading
36 36 import urllib
37 37 import urlobject
38 38 import uuid
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import webob
44 44 import routes.util
45 45
46 46 import rhodecode
47 47 from rhodecode.translation import _, _pluralize
48 48
49 49
50 50 def md5(s):
51 51 return hashlib.md5(s).hexdigest()
52 52
53 53
54 54 def md5_safe(s):
55 55 return md5(safe_str(s))
56 56
57 57
58 58 def __get_lem(extra_mapping=None):
59 59 """
60 60 Get language extension map based on what's inside pygments lexers
61 61 """
62 62 d = collections.defaultdict(lambda: [])
63 63
64 64 def __clean(s):
65 65 s = s.lstrip('*')
66 66 s = s.lstrip('.')
67 67
68 68 if s.find('[') != -1:
69 69 exts = []
70 70 start, stop = s.find('['), s.find(']')
71 71
72 72 for suffix in s[start + 1:stop]:
73 73 exts.append(s[:s.find('[')] + suffix)
74 74 return [e.lower() for e in exts]
75 75 else:
76 76 return [s.lower()]
77 77
78 78 for lx, t in sorted(pygments.lexers.LEXERS.items()):
79 79 m = map(__clean, t[-2])
80 80 if m:
81 81 m = reduce(lambda x, y: x + y, m)
82 82 for ext in m:
83 83 desc = lx.replace('Lexer', '')
84 84 d[ext].append(desc)
85 85
86 86 data = dict(d)
87 87
88 88 extra_mapping = extra_mapping or {}
89 89 if extra_mapping:
90 90 for k, v in extra_mapping.items():
91 91 if k not in data:
92 92 # register new mapping2lexer
93 93 data[k] = [v]
94 94
95 95 return data
96 96
97 97
98 98 def str2bool(_str):
99 99 """
100 100 returns True/False value from given string, it tries to translate the
101 101 string into boolean
102 102
103 103 :param _str: string value to translate into boolean
104 104 :rtype: boolean
105 105 :returns: boolean from given string
106 106 """
107 107 if _str is None:
108 108 return False
109 109 if _str in (True, False):
110 110 return _str
111 111 _str = str(_str).strip().lower()
112 112 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
113 113
114 114
115 115 def aslist(obj, sep=None, strip=True):
116 116 """
117 117 Returns given string separated by sep as list
118 118
119 119 :param obj:
120 120 :param sep:
121 121 :param strip:
122 122 """
123 123 if isinstance(obj, (basestring,)):
124 124 lst = obj.split(sep)
125 125 if strip:
126 126 lst = [v.strip() for v in lst]
127 127 return lst
128 128 elif isinstance(obj, (list, tuple)):
129 129 return obj
130 130 elif obj is None:
131 131 return []
132 132 else:
133 133 return [obj]
134 134
135 135
136 136 def convert_line_endings(line, mode):
137 137 """
138 138 Converts a given line "line end" accordingly to given mode
139 139
140 140 Available modes are::
141 141 0 - Unix
142 142 1 - Mac
143 143 2 - DOS
144 144
145 145 :param line: given line to convert
146 146 :param mode: mode to convert to
147 147 :rtype: str
148 148 :return: converted line according to mode
149 149 """
150 150 if mode == 0:
151 151 line = line.replace('\r\n', '\n')
152 152 line = line.replace('\r', '\n')
153 153 elif mode == 1:
154 154 line = line.replace('\r\n', '\r')
155 155 line = line.replace('\n', '\r')
156 156 elif mode == 2:
157 157 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
158 158 return line
159 159
160 160
161 161 def detect_mode(line, default):
162 162 """
163 163 Detects line break for given line, if line break couldn't be found
164 164 given default value is returned
165 165
166 166 :param line: str line
167 167 :param default: default
168 168 :rtype: int
169 169 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
170 170 """
171 171 if line.endswith('\r\n'):
172 172 return 2
173 173 elif line.endswith('\n'):
174 174 return 0
175 175 elif line.endswith('\r'):
176 176 return 1
177 177 else:
178 178 return default
179 179
180 180
181 181 def safe_int(val, default=None):
182 182 """
183 183 Returns int() of val if val is not convertable to int use default
184 184 instead
185 185
186 186 :param val:
187 187 :param default:
188 188 """
189 189
190 190 try:
191 191 val = int(val)
192 192 except (ValueError, TypeError):
193 193 val = default
194 194
195 195 return val
196 196
197 197
198 198 def safe_unicode(str_, from_encoding=None):
199 199 """
200 200 safe unicode function. Does few trick to turn str_ into unicode
201 201
202 202 In case of UnicodeDecode error, we try to return it with encoding detected
203 203 by chardet library if it fails fallback to unicode with errors replaced
204 204
205 205 :param str_: string to decode
206 206 :rtype: unicode
207 207 :returns: unicode object
208 208 """
209 209 if isinstance(str_, unicode):
210 210 return str_
211 211
212 212 if not from_encoding:
213 213 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
214 214 'utf8'), sep=',')
215 215 from_encoding = DEFAULT_ENCODINGS
216 216
217 217 if not isinstance(from_encoding, (list, tuple)):
218 218 from_encoding = [from_encoding]
219 219
220 220 try:
221 221 return unicode(str_)
222 222 except UnicodeDecodeError:
223 223 pass
224 224
225 225 for enc in from_encoding:
226 226 try:
227 227 return unicode(str_, enc)
228 228 except UnicodeDecodeError:
229 229 pass
230 230
231 231 try:
232 232 import chardet
233 233 encoding = chardet.detect(str_)['encoding']
234 234 if encoding is None:
235 235 raise Exception()
236 236 return str_.decode(encoding)
237 237 except (ImportError, UnicodeDecodeError, Exception):
238 238 return unicode(str_, from_encoding[0], 'replace')
239 239
240 240
241 241 def safe_str(unicode_, to_encoding=None):
242 242 """
243 243 safe str function. Does few trick to turn unicode_ into string
244 244
245 245 In case of UnicodeEncodeError, we try to return it with encoding detected
246 246 by chardet library if it fails fallback to string with errors replaced
247 247
248 248 :param unicode_: unicode to encode
249 249 :rtype: str
250 250 :returns: str object
251 251 """
252 252
253 253 # if it's not basestr cast to str
254 254 if not isinstance(unicode_, basestring):
255 255 return str(unicode_)
256 256
257 257 if isinstance(unicode_, str):
258 258 return unicode_
259 259
260 260 if not to_encoding:
261 261 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
262 262 'utf8'), sep=',')
263 263 to_encoding = DEFAULT_ENCODINGS
264 264
265 265 if not isinstance(to_encoding, (list, tuple)):
266 266 to_encoding = [to_encoding]
267 267
268 268 for enc in to_encoding:
269 269 try:
270 270 return unicode_.encode(enc)
271 271 except UnicodeEncodeError:
272 272 pass
273 273
274 274 try:
275 275 import chardet
276 276 encoding = chardet.detect(unicode_)['encoding']
277 277 if encoding is None:
278 278 raise UnicodeEncodeError()
279 279
280 280 return unicode_.encode(encoding)
281 281 except (ImportError, UnicodeEncodeError):
282 282 return unicode_.encode(to_encoding[0], 'replace')
283 283
284 284
285 285 def remove_suffix(s, suffix):
286 286 if s.endswith(suffix):
287 287 s = s[:-1 * len(suffix)]
288 288 return s
289 289
290 290
291 291 def remove_prefix(s, prefix):
292 292 if s.startswith(prefix):
293 293 s = s[len(prefix):]
294 294 return s
295 295
296 296
297 297 def find_calling_context(ignore_modules=None):
298 298 """
299 299 Look through the calling stack and return the frame which called
300 300 this function and is part of core module ( ie. rhodecode.* )
301 301
302 302 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
303 303 """
304 304
305 305 ignore_modules = ignore_modules or []
306 306
307 307 f = sys._getframe(2)
308 308 while f.f_back is not None:
309 309 name = f.f_globals.get('__name__')
310 310 if name and name.startswith(__name__.split('.')[0]):
311 311 if name not in ignore_modules:
312 312 return f
313 313 f = f.f_back
314 314 return None
315 315
316 316
317 317 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
318 318 """Custom engine_from_config functions."""
319 319 log = logging.getLogger('sqlalchemy.engine')
320 320 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
321 321
322 322 def color_sql(sql):
323 323 color_seq = '\033[1;33m' # This is yellow: code 33
324 324 normal = '\x1b[0m'
325 325 return ''.join([color_seq, sql, normal])
326 326
327 327 if configuration['debug']:
328 328 # attach events only for debug configuration
329 329
330 330 def before_cursor_execute(conn, cursor, statement,
331 331 parameters, context, executemany):
332 332 setattr(conn, 'query_start_time', time.time())
333 333 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
334 334 calling_context = find_calling_context(ignore_modules=[
335 335 'rhodecode.lib.caching_query',
336 336 'rhodecode.model.settings',
337 337 ])
338 338 if calling_context:
339 339 log.info(color_sql('call context %s:%s' % (
340 340 calling_context.f_code.co_filename,
341 341 calling_context.f_lineno,
342 342 )))
343 343
344 344 def after_cursor_execute(conn, cursor, statement,
345 345 parameters, context, executemany):
346 346 delattr(conn, 'query_start_time')
347 347
348 348 sqlalchemy.event.listen(engine, "before_cursor_execute",
349 349 before_cursor_execute)
350 350 sqlalchemy.event.listen(engine, "after_cursor_execute",
351 351 after_cursor_execute)
352 352
353 353 return engine
354 354
355 355
356 356 def get_encryption_key(config):
357 357 secret = config.get('rhodecode.encrypted_values.secret')
358 358 default = config['beaker.session.secret']
359 359 return secret or default
360 360
361 361
362 362 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
363 363 short_format=False):
364 364 """
365 365 Turns a datetime into an age string.
366 366 If show_short_version is True, this generates a shorter string with
367 367 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
368 368
369 369 * IMPORTANT*
370 370 Code of this function is written in special way so it's easier to
371 371 backport it to javascript. If you mean to update it, please also update
372 372 `jquery.timeago-extension.js` file
373 373
374 374 :param prevdate: datetime object
375 375 :param now: get current time, if not define we use
376 376 `datetime.datetime.now()`
377 377 :param show_short_version: if it should approximate the date and
378 378 return a shorter string
379 379 :param show_suffix:
380 380 :param short_format: show short format, eg 2D instead of 2 days
381 381 :rtype: unicode
382 382 :returns: unicode words describing age
383 383 """
384 384
385 385 def _get_relative_delta(now, prevdate):
386 386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 387 return {
388 388 'year': base.years,
389 389 'month': base.months,
390 390 'day': base.days,
391 391 'hour': base.hours,
392 392 'minute': base.minutes,
393 393 'second': base.seconds,
394 394 }
395 395
396 396 def _is_leap_year(year):
397 397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398 398
399 399 def get_month(prevdate):
400 400 return prevdate.month
401 401
402 402 def get_year(prevdate):
403 403 return prevdate.year
404 404
405 405 now = now or datetime.datetime.now()
406 406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 407 deltas = {}
408 408 future = False
409 409
410 410 if prevdate > now:
411 411 now_old = now
412 412 now = prevdate
413 413 prevdate = now_old
414 414 future = True
415 415 if future:
416 416 prevdate = prevdate.replace(microsecond=0)
417 417 # Get date parts deltas
418 418 for part in order:
419 419 rel_delta = _get_relative_delta(now, prevdate)
420 420 deltas[part] = rel_delta[part]
421 421
422 422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 423 # not 1 hour, -59 minutes and -59 seconds)
424 424 offsets = [[5, 60], [4, 60], [3, 24]]
425 425 for element in offsets: # seconds, minutes, hours
426 426 num = element[0]
427 427 length = element[1]
428 428
429 429 part = order[num]
430 430 carry_part = order[num - 1]
431 431
432 432 if deltas[part] < 0:
433 433 deltas[part] += length
434 434 deltas[carry_part] -= 1
435 435
436 436 # Same thing for days except that the increment depends on the (variable)
437 437 # number of days in the month
438 438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 439 if deltas['day'] < 0:
440 440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 441 deltas['day'] += 29
442 442 else:
443 443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444 444
445 445 deltas['month'] -= 1
446 446
447 447 if deltas['month'] < 0:
448 448 deltas['month'] += 12
449 449 deltas['year'] -= 1
450 450
451 451 # Format the result
452 452 if short_format:
453 453 fmt_funcs = {
454 454 'year': lambda d: u'%dy' % d,
455 455 'month': lambda d: u'%dm' % d,
456 456 'day': lambda d: u'%dd' % d,
457 457 'hour': lambda d: u'%dh' % d,
458 458 'minute': lambda d: u'%dmin' % d,
459 459 'second': lambda d: u'%dsec' % d,
460 460 }
461 461 else:
462 462 fmt_funcs = {
463 463 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
464 464 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
465 465 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
466 466 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
467 467 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
468 468 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
469 469 }
470 470
471 471 i = 0
472 472 for part in order:
473 473 value = deltas[part]
474 474 if value != 0:
475 475
476 476 if i < 5:
477 477 sub_part = order[i + 1]
478 478 sub_value = deltas[sub_part]
479 479 else:
480 480 sub_value = 0
481 481
482 482 if sub_value == 0 or show_short_version:
483 483 _val = fmt_funcs[part](value)
484 484 if future:
485 485 if show_suffix:
486 486 return _(u'in ${ago}', mapping={'ago': _val})
487 487 else:
488 488 return _(_val)
489 489
490 490 else:
491 491 if show_suffix:
492 492 return _(u'${ago} ago', mapping={'ago': _val})
493 493 else:
494 494 return _(_val)
495 495
496 496 val = fmt_funcs[part](value)
497 497 val_detail = fmt_funcs[sub_part](sub_value)
498 498 mapping = {'val': val, 'detail': val_detail}
499 499
500 500 if short_format:
501 501 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
502 502 if show_suffix:
503 503 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
504 504 if future:
505 505 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
506 506 else:
507 507 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
508 508 if show_suffix:
509 509 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
510 510 if future:
511 511 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
512 512
513 513 return datetime_tmpl
514 514 i += 1
515 515 return _(u'just now')
516 516
517 517
518 518 def cleaned_uri(uri):
519 519 """
520 520 Quotes '[' and ']' from uri if there is only one of them.
521 521 according to RFC3986 we cannot use such chars in uri
522 522 :param uri:
523 523 :return: uri without this chars
524 524 """
525 525 return urllib.quote(uri, safe='@$:/')
526 526
527 527
528 528 def uri_filter(uri):
529 529 """
530 530 Removes user:password from given url string
531 531
532 532 :param uri:
533 533 :rtype: unicode
534 534 :returns: filtered list of strings
535 535 """
536 536 if not uri:
537 537 return ''
538 538
539 539 proto = ''
540 540
541 541 for pat in ('https://', 'http://'):
542 542 if uri.startswith(pat):
543 543 uri = uri[len(pat):]
544 544 proto = pat
545 545 break
546 546
547 547 # remove passwords and username
548 548 uri = uri[uri.find('@') + 1:]
549 549
550 550 # get the port
551 551 cred_pos = uri.find(':')
552 552 if cred_pos == -1:
553 553 host, port = uri, None
554 554 else:
555 555 host, port = uri[:cred_pos], uri[cred_pos + 1:]
556 556
557 557 return filter(None, [proto, host, port])
558 558
559 559
560 560 def credentials_filter(uri):
561 561 """
562 562 Returns a url with removed credentials
563 563
564 564 :param uri:
565 565 """
566 566
567 567 uri = uri_filter(uri)
568 568 # check if we have port
569 569 if len(uri) > 2 and uri[2]:
570 570 uri[2] = ':' + uri[2]
571 571
572 572 return ''.join(uri)
573 573
574 574
575 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
575 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
576 qualifed_home_url = request.route_url('home')
576 577 parsed_url = urlobject.URLObject(qualifed_home_url)
577 578 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
578 579 args = {
579 580 'scheme': parsed_url.scheme,
580 581 'user': '',
581 582 # path if we use proxy-prefix
582 583 'netloc': parsed_url.netloc+decoded_path,
583 584 'prefix': decoded_path,
584 585 'repo': repo_name,
585 586 'repoid': str(repo_id)
586 587 }
587 588 args.update(override)
588 589 args['user'] = urllib.quote(safe_str(args['user']))
589 590
590 591 for k, v in args.items():
591 592 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
592 593
593 594 # remove leading @ sign if it's present. Case of empty user
594 595 url_obj = urlobject.URLObject(uri_tmpl)
595 596 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
596 597
597 598 return safe_unicode(url)
598 599
599 600
600 601 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
601 602 """
602 603 Safe version of get_commit if this commit doesn't exists for a
603 604 repository it returns a Dummy one instead
604 605
605 606 :param repo: repository instance
606 607 :param commit_id: commit id as str
607 608 :param pre_load: optional list of commit attributes to load
608 609 """
609 610 # TODO(skreft): remove these circular imports
610 611 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
611 612 from rhodecode.lib.vcs.exceptions import RepositoryError
612 613 if not isinstance(repo, BaseRepository):
613 614 raise Exception('You must pass an Repository '
614 615 'object as first argument got %s', type(repo))
615 616
616 617 try:
617 618 commit = repo.get_commit(
618 619 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
619 620 except (RepositoryError, LookupError):
620 621 commit = EmptyCommit()
621 622 return commit
622 623
623 624
624 625 def datetime_to_time(dt):
625 626 if dt:
626 627 return time.mktime(dt.timetuple())
627 628
628 629
629 630 def time_to_datetime(tm):
630 631 if tm:
631 632 if isinstance(tm, basestring):
632 633 try:
633 634 tm = float(tm)
634 635 except ValueError:
635 636 return
636 637 return datetime.datetime.fromtimestamp(tm)
637 638
638 639
639 640 def time_to_utcdatetime(tm):
640 641 if tm:
641 642 if isinstance(tm, basestring):
642 643 try:
643 644 tm = float(tm)
644 645 except ValueError:
645 646 return
646 647 return datetime.datetime.utcfromtimestamp(tm)
647 648
648 649
649 650 MENTIONS_REGEX = re.compile(
650 651 # ^@ or @ without any special chars in front
651 652 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
652 653 # main body starts with letter, then can be . - _
653 654 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
654 655 re.VERBOSE | re.MULTILINE)
655 656
656 657
657 658 def extract_mentioned_users(s):
658 659 """
659 660 Returns unique usernames from given string s that have @mention
660 661
661 662 :param s: string to get mentions
662 663 """
663 664 usrs = set()
664 665 for username in MENTIONS_REGEX.findall(s):
665 666 usrs.add(username)
666 667
667 668 return sorted(list(usrs), key=lambda k: k.lower())
668 669
669 670
670 671 class StrictAttributeDict(dict):
671 672 """
672 673 Strict Version of Attribute dict which raises an Attribute error when
673 674 requested attribute is not set
674 675 """
675 676 def __getattr__(self, attr):
676 677 try:
677 678 return self[attr]
678 679 except KeyError:
679 680 raise AttributeError('%s object has no attribute %s' % (
680 681 self.__class__, attr))
681 682 __setattr__ = dict.__setitem__
682 683 __delattr__ = dict.__delitem__
683 684
684 685
685 686 class AttributeDict(dict):
686 687 def __getattr__(self, attr):
687 688 return self.get(attr, None)
688 689 __setattr__ = dict.__setitem__
689 690 __delattr__ = dict.__delitem__
690 691
691 692
692 693 def fix_PATH(os_=None):
693 694 """
694 695 Get current active python path, and append it to PATH variable to fix
695 696 issues of subprocess calls and different python versions
696 697 """
697 698 if os_ is None:
698 699 import os
699 700 else:
700 701 os = os_
701 702
702 703 cur_path = os.path.split(sys.executable)[0]
703 704 if not os.environ['PATH'].startswith(cur_path):
704 705 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
705 706
706 707
707 708 def obfuscate_url_pw(engine):
708 709 _url = engine or ''
709 710 try:
710 711 _url = sqlalchemy.engine.url.make_url(engine)
711 712 if _url.password:
712 713 _url.password = 'XXXXX'
713 714 except Exception:
714 715 pass
715 716 return unicode(_url)
716 717
717 718
718 719 def get_server_url(environ):
719 720 req = webob.Request(environ)
720 721 return req.host_url + req.script_name
721 722
722 723
723 724 def unique_id(hexlen=32):
724 725 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
725 726 return suuid(truncate_to=hexlen, alphabet=alphabet)
726 727
727 728
728 729 def suuid(url=None, truncate_to=22, alphabet=None):
729 730 """
730 731 Generate and return a short URL safe UUID.
731 732
732 733 If the url parameter is provided, set the namespace to the provided
733 734 URL and generate a UUID.
734 735
735 736 :param url to get the uuid for
736 737 :truncate_to: truncate the basic 22 UUID to shorter version
737 738
738 739 The IDs won't be universally unique any longer, but the probability of
739 740 a collision will still be very low.
740 741 """
741 742 # Define our alphabet.
742 743 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
743 744
744 745 # If no URL is given, generate a random UUID.
745 746 if url is None:
746 747 unique_id = uuid.uuid4().int
747 748 else:
748 749 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
749 750
750 751 alphabet_length = len(_ALPHABET)
751 752 output = []
752 753 while unique_id > 0:
753 754 digit = unique_id % alphabet_length
754 755 output.append(_ALPHABET[digit])
755 756 unique_id = int(unique_id / alphabet_length)
756 757 return "".join(output)[:truncate_to]
757 758
758 759
759 760 def get_current_rhodecode_user():
760 761 """
761 762 Gets rhodecode user from threadlocal tmpl_context variable if it's
762 763 defined, else returns None.
763 764 """
764 765 from pylons import tmpl_context as c
765 766 if hasattr(c, 'rhodecode_user'):
766 767 return c.rhodecode_user
767 768
768 769 return None
769 770
770 771
771 772 def action_logger_generic(action, namespace=''):
772 773 """
773 774 A generic logger for actions useful to the system overview, tries to find
774 775 an acting user for the context of the call otherwise reports unknown user
775 776
776 777 :param action: logging message eg 'comment 5 deleted'
777 778 :param type: string
778 779
779 780 :param namespace: namespace of the logging message eg. 'repo.comments'
780 781 :param type: string
781 782
782 783 """
783 784
784 785 logger_name = 'rhodecode.actions'
785 786
786 787 if namespace:
787 788 logger_name += '.' + namespace
788 789
789 790 log = logging.getLogger(logger_name)
790 791
791 792 # get a user if we can
792 793 user = get_current_rhodecode_user()
793 794
794 795 logfunc = log.info
795 796
796 797 if not user:
797 798 user = '<unknown user>'
798 799 logfunc = log.warning
799 800
800 801 logfunc('Logging action by {}: {}'.format(user, action))
801 802
802 803
803 804 def escape_split(text, sep=',', maxsplit=-1):
804 805 r"""
805 806 Allows for escaping of the separator: e.g. arg='foo\, bar'
806 807
807 808 It should be noted that the way bash et. al. do command line parsing, those
808 809 single quotes are required.
809 810 """
810 811 escaped_sep = r'\%s' % sep
811 812
812 813 if escaped_sep not in text:
813 814 return text.split(sep, maxsplit)
814 815
815 816 before, _mid, after = text.partition(escaped_sep)
816 817 startlist = before.split(sep, maxsplit) # a regular split is fine here
817 818 unfinished = startlist[-1]
818 819 startlist = startlist[:-1]
819 820
820 821 # recurse because there may be more escaped separators
821 822 endlist = escape_split(after, sep, maxsplit)
822 823
823 824 # finish building the escaped value. we use endlist[0] becaue the first
824 825 # part of the string sent in recursion is the rest of the escaped value.
825 826 unfinished += sep + endlist[0]
826 827
827 828 return startlist + [unfinished] + endlist[1:] # put together all the parts
828 829
829 830
830 831 class OptionalAttr(object):
831 832 """
832 833 Special Optional Option that defines other attribute. Example::
833 834
834 835 def test(apiuser, userid=Optional(OAttr('apiuser')):
835 836 user = Optional.extract(userid)
836 837 # calls
837 838
838 839 """
839 840
840 841 def __init__(self, attr_name):
841 842 self.attr_name = attr_name
842 843
843 844 def __repr__(self):
844 845 return '<OptionalAttr:%s>' % self.attr_name
845 846
846 847 def __call__(self):
847 848 return self
848 849
849 850
850 851 # alias
851 852 OAttr = OptionalAttr
852 853
853 854
854 855 class Optional(object):
855 856 """
856 857 Defines an optional parameter::
857 858
858 859 param = param.getval() if isinstance(param, Optional) else param
859 860 param = param() if isinstance(param, Optional) else param
860 861
861 862 is equivalent of::
862 863
863 864 param = Optional.extract(param)
864 865
865 866 """
866 867
867 868 def __init__(self, type_):
868 869 self.type_ = type_
869 870
870 871 def __repr__(self):
871 872 return '<Optional:%s>' % self.type_.__repr__()
872 873
873 874 def __call__(self):
874 875 return self.getval()
875 876
876 877 def getval(self):
877 878 """
878 879 returns value from this Optional instance
879 880 """
880 881 if isinstance(self.type_, OAttr):
881 882 # use params name
882 883 return self.type_.attr_name
883 884 return self.type_
884 885
885 886 @classmethod
886 887 def extract(cls, val):
887 888 """
888 889 Extracts value from Optional() instance
889 890
890 891 :param val:
891 892 :return: original value if it's not Optional instance else
892 893 value of instance
893 894 """
894 895 if isinstance(val, cls):
895 896 return val.getval()
896 897 return val
897 898
898 899
899 900 def get_routes_generator_for_server_url(server_url):
900 901 parsed_url = urlobject.URLObject(server_url)
901 902 netloc = safe_str(parsed_url.netloc)
902 903 script_name = safe_str(parsed_url.path)
903 904
904 905 if ':' in netloc:
905 906 server_name, server_port = netloc.split(':')
906 907 else:
907 908 server_name = netloc
908 909 server_port = (parsed_url.scheme == 'https' and '443' or '80')
909 910
910 911 environ = {
911 912 'REQUEST_METHOD': 'GET',
912 913 'PATH_INFO': '/',
913 914 'SERVER_NAME': server_name,
914 915 'SERVER_PORT': server_port,
915 916 'SCRIPT_NAME': script_name,
916 917 }
917 918 if parsed_url.scheme == 'https':
918 919 environ['HTTPS'] = 'on'
919 920 environ['wsgi.url_scheme'] = 'https'
920 921
921 922 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
922 923
923 924
924 925 def glob2re(pat):
925 926 """
926 927 Translate a shell PATTERN to a regular expression.
927 928
928 929 There is no way to quote meta-characters.
929 930 """
930 931
931 932 i, n = 0, len(pat)
932 933 res = ''
933 934 while i < n:
934 935 c = pat[i]
935 936 i = i+1
936 937 if c == '*':
937 938 #res = res + '.*'
938 939 res = res + '[^/]*'
939 940 elif c == '?':
940 941 #res = res + '.'
941 942 res = res + '[^/]'
942 943 elif c == '[':
943 944 j = i
944 945 if j < n and pat[j] == '!':
945 946 j = j+1
946 947 if j < n and pat[j] == ']':
947 948 j = j+1
948 949 while j < n and pat[j] != ']':
949 950 j = j+1
950 951 if j >= n:
951 952 res = res + '\\['
952 953 else:
953 954 stuff = pat[i:j].replace('\\','\\\\')
954 955 i = j+1
955 956 if stuff[0] == '!':
956 957 stuff = '^' + stuff[1:]
957 958 elif stuff[0] == '^':
958 959 stuff = '\\' + stuff
959 960 res = '%s[%s]' % (res, stuff)
960 961 else:
961 962 res = res + re.escape(c)
962 963 return res + '\Z(?ms)'
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now