##// 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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.db import Gist
24 from rhodecode.model.db import Gist
25 from rhodecode.api.tests.utils import (
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_error, assert_ok)
26 build_data, api_call, assert_error, assert_ok)
27
27
28
28
29 @pytest.mark.usefixtures("testuser_api", "app")
29 @pytest.mark.usefixtures("testuser_api", "app")
30 class TestApiGetGist(object):
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 gist = gist_util.create_gist()
32 gist = gist_util.create_gist()
33 gist_id = gist.gist_access_id
33 gist_id = gist.gist_access_id
34 gist_created_on = gist.created_on
34 gist_created_on = gist.created_on
35 gist_modified_at = gist.modified_at
35 gist_modified_at = gist.modified_at
36 id_, params = build_data(
36 id_, params = build_data(
37 self.apikey, 'get_gist', gistid=gist_id, )
37 self.apikey, 'get_gist', gistid=gist_id, )
38 response = api_call(self.app, params)
38 response = api_call(self.app, params)
39
39
40 expected = {
40 expected = {
41 'access_id': gist_id,
41 'access_id': gist_id,
42 'created_on': gist_created_on,
42 'created_on': gist_created_on,
43 'modified_at': gist_modified_at,
43 'modified_at': gist_modified_at,
44 'description': 'new-gist',
44 'description': 'new-gist',
45 'expires': -1.0,
45 'expires': -1.0,
46 'gist_id': int(gist_id),
46 'gist_id': int(gist_id),
47 'type': 'public',
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 'acl_level': Gist.ACL_LEVEL_PUBLIC,
49 'acl_level': Gist.ACL_LEVEL_PUBLIC,
50 'content': None,
50 'content': None,
51 }
51 }
52
52
53 assert_ok(id_, expected, given=response.body)
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 mapping = {
56 mapping = {
57 u'filename1.txt': {'content': u'hello world'},
57 u'filename1.txt': {'content': u'hello world'},
58 u'filename1ą.txt': {'content': u'hello worldę'}
58 u'filename1ą.txt': {'content': u'hello worldę'}
59 }
59 }
60 gist = gist_util.create_gist(gist_mapping=mapping)
60 gist = gist_util.create_gist(gist_mapping=mapping)
61 gist_id = gist.gist_access_id
61 gist_id = gist.gist_access_id
62 gist_created_on = gist.created_on
62 gist_created_on = gist.created_on
63 gist_modified_at = gist.modified_at
63 gist_modified_at = gist.modified_at
64 id_, params = build_data(
64 id_, params = build_data(
65 self.apikey, 'get_gist', gistid=gist_id, content=True)
65 self.apikey, 'get_gist', gistid=gist_id, content=True)
66 response = api_call(self.app, params)
66 response = api_call(self.app, params)
67
67
68 expected = {
68 expected = {
69 'access_id': gist_id,
69 'access_id': gist_id,
70 'created_on': gist_created_on,
70 'created_on': gist_created_on,
71 'modified_at': gist_modified_at,
71 'modified_at': gist_modified_at,
72 'description': 'new-gist',
72 'description': 'new-gist',
73 'expires': -1.0,
73 'expires': -1.0,
74 'gist_id': int(gist_id),
74 'gist_id': int(gist_id),
75 'type': 'public',
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 'acl_level': Gist.ACL_LEVEL_PUBLIC,
77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
78 'content': {
78 'content': {
79 u'filename1.txt': u'hello world',
79 u'filename1.txt': u'hello world',
80 u'filename1ą.txt': u'hello worldę'
80 u'filename1ą.txt': u'hello worldę'
81 },
81 },
82 }
82 }
83
83
84 assert_ok(id_, expected, given=response.body)
84 assert_ok(id_, expected, given=response.body)
85
85
86 def test_api_get_gist_not_existing(self):
86 def test_api_get_gist_not_existing(self):
87 id_, params = build_data(
87 id_, params = build_data(
88 self.apikey_regular, 'get_gist', gistid='12345', )
88 self.apikey_regular, 'get_gist', gistid='12345', )
89 response = api_call(self.app, params)
89 response = api_call(self.app, params)
90 expected = 'gist `%s` does not exist' % ('12345',)
90 expected = 'gist `%s` does not exist' % ('12345',)
91 assert_error(id_, expected, given=response.body)
91 assert_error(id_, expected, given=response.body)
92
92
93 def test_api_get_gist_private_gist_without_permission(self, gist_util):
93 def test_api_get_gist_private_gist_without_permission(self, gist_util):
94 gist = gist_util.create_gist()
94 gist = gist_util.create_gist()
95 gist_id = gist.gist_access_id
95 gist_id = gist.gist_access_id
96 id_, params = build_data(
96 id_, params = build_data(
97 self.apikey_regular, 'get_gist', gistid=gist_id, )
97 self.apikey_regular, 'get_gist', gistid=gist_id, )
98 response = api_call(self.app, params)
98 response = api_call(self.app, params)
99
99
100 expected = 'gist `%s` does not exist' % (gist_id,)
100 expected = 'gist `%s` does not exist' % (gist_id,)
101 assert_error(id_, expected, given=response.body)
101 assert_error(id_, expected, given=response.body)
@@ -1,134 +1,134 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
23 import pytest
22 import pytest
24 import urlobject
23 import urlobject
25 from pylons import url
24 from pylons import url
26
25
27 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28 from rhodecode.lib.utils2 import safe_unicode
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
32
32
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
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 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'get_pull_request',
40 self.apikey, 'get_pull_request',
41 repoid=pull_request.target_repo.repo_name,
41 repoid=pull_request.target_repo.repo_name,
42 pullrequestid=pull_request.pull_request_id)
42 pullrequestid=pull_request.pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 assert response.status == '200 OK'
46 assert response.status == '200 OK'
47
47
48 url_obj = urlobject.URLObject(
48 url_obj = urlobject.URLObject(
49 url(
49 url(
50 'pullrequest_show',
50 'pullrequest_show',
51 repo_name=pull_request.target_repo.repo_name,
51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 pr_url = unicode(
53
54 url_obj.with_netloc('test.example.com:80'))
54 pr_url = safe_unicode(
55 source_url = unicode(
55 url_obj.with_netloc(http_host_stub))
56 pull_request.source_repo.clone_url()
56 source_url = safe_unicode(
57 .with_netloc('test.example.com:80'))
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = unicode(
58 target_url = safe_unicode(
59 pull_request.target_repo.clone_url()
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 .with_netloc('test.example.com:80'))
60 shadow_url = safe_unicode(
61 shadow_url = unicode(
62 PullRequestModel().get_shadow_clone_url(pull_request))
61 PullRequestModel().get_shadow_clone_url(pull_request))
62
63 expected = {
63 expected = {
64 'pull_request_id': pull_request.pull_request_id,
64 'pull_request_id': pull_request.pull_request_id,
65 'url': pr_url,
65 'url': pr_url,
66 'title': pull_request.title,
66 'title': pull_request.title,
67 'description': pull_request.description,
67 'description': pull_request.description,
68 'status': pull_request.status,
68 'status': pull_request.status,
69 'created_on': pull_request.created_on,
69 'created_on': pull_request.created_on,
70 'updated_on': pull_request.updated_on,
70 'updated_on': pull_request.updated_on,
71 'commit_ids': pull_request.revisions,
71 'commit_ids': pull_request.revisions,
72 'review_status': pull_request.calculated_review_status(),
72 'review_status': pull_request.calculated_review_status(),
73 'mergeable': {
73 'mergeable': {
74 'status': True,
74 'status': True,
75 'message': 'This pull request can be automatically merged.',
75 'message': 'This pull request can be automatically merged.',
76 },
76 },
77 'source': {
77 'source': {
78 'clone_url': source_url,
78 'clone_url': source_url,
79 'repository': pull_request.source_repo.repo_name,
79 'repository': pull_request.source_repo.repo_name,
80 'reference': {
80 'reference': {
81 'name': pull_request.source_ref_parts.name,
81 'name': pull_request.source_ref_parts.name,
82 'type': pull_request.source_ref_parts.type,
82 'type': pull_request.source_ref_parts.type,
83 'commit_id': pull_request.source_ref_parts.commit_id,
83 'commit_id': pull_request.source_ref_parts.commit_id,
84 },
84 },
85 },
85 },
86 'target': {
86 'target': {
87 'clone_url': target_url,
87 'clone_url': target_url,
88 'repository': pull_request.target_repo.repo_name,
88 'repository': pull_request.target_repo.repo_name,
89 'reference': {
89 'reference': {
90 'name': pull_request.target_ref_parts.name,
90 'name': pull_request.target_ref_parts.name,
91 'type': pull_request.target_ref_parts.type,
91 'type': pull_request.target_ref_parts.type,
92 'commit_id': pull_request.target_ref_parts.commit_id,
92 'commit_id': pull_request.target_ref_parts.commit_id,
93 },
93 },
94 },
94 },
95 'merge': {
95 'merge': {
96 'clone_url': shadow_url,
96 'clone_url': shadow_url,
97 'reference': {
97 'reference': {
98 'name': pull_request.shadow_merge_ref.name,
98 'name': pull_request.shadow_merge_ref.name,
99 'type': pull_request.shadow_merge_ref.type,
99 'type': pull_request.shadow_merge_ref.type,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
100 'commit_id': pull_request.shadow_merge_ref.commit_id,
101 },
101 },
102 },
102 },
103 'author': pull_request.author.get_api_data(include_secrets=False,
103 'author': pull_request.author.get_api_data(include_secrets=False,
104 details='basic'),
104 details='basic'),
105 'reviewers': [
105 'reviewers': [
106 {
106 {
107 'user': reviewer.get_api_data(include_secrets=False,
107 'user': reviewer.get_api_data(include_secrets=False,
108 details='basic'),
108 details='basic'),
109 'reasons': reasons,
109 'reasons': reasons,
110 'review_status': st[0][1].status if st else 'not_reviewed',
110 'review_status': st[0][1].status if st else 'not_reviewed',
111 }
111 }
112 for reviewer, reasons, mandatory, st in
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 pull_request.reviewers_statuses()
114 ]
114 ]
115 }
115 }
116 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
117
117
118 def test_api_get_pull_request_repo_error(self):
118 def test_api_get_pull_request_repo_error(self):
119 id_, params = build_data(
119 id_, params = build_data(
120 self.apikey, 'get_pull_request',
120 self.apikey, 'get_pull_request',
121 repoid=666, pullrequestid=1)
121 repoid=666, pullrequestid=1)
122 response = api_call(self.app, params)
122 response = api_call(self.app, params)
123
123
124 expected = 'repository `666` does not exist'
124 expected = 'repository `666` does not exist'
125 assert_error(id_, expected, given=response.body)
125 assert_error(id_, expected, given=response.body)
126
126
127 def test_api_get_pull_request_pull_request_error(self):
127 def test_api_get_pull_request_pull_request_error(self):
128 id_, params = build_data(
128 id_, params = build_data(
129 self.apikey, 'get_pull_request',
129 self.apikey, 'get_pull_request',
130 repoid=1, pullrequestid=666)
130 repoid=1, pullrequestid=666)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132
132
133 expected = 'pull request `666` does not exist'
133 expected = 'pull request `666` does not exist'
134 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
@@ -1,189 +1,190 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import mock
21 import mock
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.repo import RepoModel
24 from rhodecode.model.repo import RepoModel
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29 from rhodecode.tests.plugin import http_host_stub, http_host_only_stub
30
30
31 fixture = Fixture()
31 fixture = Fixture()
32
32
33 UPDATE_REPO_NAME = 'api_update_me'
33 UPDATE_REPO_NAME = 'api_update_me'
34
34
35
35
36 class SAME_AS_UPDATES(object):
36 class SAME_AS_UPDATES(object):
37 """ Constant used for tests below """
37 """ Constant used for tests below """
38
38
39
39
40 @pytest.mark.usefixtures("testuser_api", "app")
40 @pytest.mark.usefixtures("testuser_api", "app")
41 class TestApiUpdateRepo(object):
41 class TestApiUpdateRepo(object):
42
42
43 @pytest.mark.parametrize("updates, expected", [
43 @pytest.mark.parametrize("updates, expected", [
44 ({'owner': TEST_USER_REGULAR_LOGIN},
44 ({'owner': TEST_USER_REGULAR_LOGIN},
45 SAME_AS_UPDATES),
45 SAME_AS_UPDATES),
46
46
47 ({'description': 'new description'},
47 ({'description': 'new description'},
48 SAME_AS_UPDATES),
48 SAME_AS_UPDATES),
49
49
50 ({'clone_uri': 'http://foo.com/repo'},
50 ({'clone_uri': 'http://foo.com/repo'},
51 SAME_AS_UPDATES),
51 SAME_AS_UPDATES),
52
52
53 ({'clone_uri': None},
53 ({'clone_uri': None},
54 {'clone_uri': ''}),
54 {'clone_uri': ''}),
55
55
56 ({'clone_uri': ''},
56 ({'clone_uri': ''},
57 {'clone_uri': ''}),
57 {'clone_uri': ''}),
58
58
59 ({'landing_rev': 'rev:tip'},
59 ({'landing_rev': 'rev:tip'},
60 {'landing_rev': ['rev', 'tip']}),
60 {'landing_rev': ['rev', 'tip']}),
61
61
62 ({'enable_statistics': True},
62 ({'enable_statistics': True},
63 SAME_AS_UPDATES),
63 SAME_AS_UPDATES),
64
64
65 ({'enable_locking': True},
65 ({'enable_locking': True},
66 SAME_AS_UPDATES),
66 SAME_AS_UPDATES),
67
67
68 ({'enable_downloads': True},
68 ({'enable_downloads': True},
69 SAME_AS_UPDATES),
69 SAME_AS_UPDATES),
70
70
71 ({'repo_name': 'new_repo_name'},
71 ({'repo_name': 'new_repo_name'},
72 {
72 {
73 'repo_name': 'new_repo_name',
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 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
77 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
78 '_group': 'test_group_for_update'},
78 '_group': 'test_group_for_update'},
79 {
79 {
80 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
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 def test_api_update_repo(self, updates, expected, backend):
85 def test_api_update_repo(self, updates, expected, backend):
85 repo_name = UPDATE_REPO_NAME
86 repo_name = UPDATE_REPO_NAME
86 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
87 repo = fixture.create_repo(repo_name, repo_type=backend.alias)
87 if updates.get('_group'):
88 if updates.get('_group'):
88 fixture.create_repo_group(updates['_group'])
89 fixture.create_repo_group(updates['_group'])
89
90
90 expected_api_data = repo.get_api_data(include_secrets=True)
91 expected_api_data = repo.get_api_data(include_secrets=True)
91 if expected is SAME_AS_UPDATES:
92 if expected is SAME_AS_UPDATES:
92 expected_api_data.update(updates)
93 expected_api_data.update(updates)
93 else:
94 else:
94 expected_api_data.update(expected)
95 expected_api_data.update(expected)
95
96
96 id_, params = build_data(
97 id_, params = build_data(
97 self.apikey, 'update_repo', repoid=repo_name, **updates)
98 self.apikey, 'update_repo', repoid=repo_name, **updates)
98 response = api_call(self.app, params)
99 response = api_call(self.app, params)
99
100
100 if updates.get('repo_name'):
101 if updates.get('repo_name'):
101 repo_name = updates['repo_name']
102 repo_name = updates['repo_name']
102
103
103 try:
104 try:
104 expected = {
105 expected = {
105 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
106 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
106 'repository': jsonify(expected_api_data)
107 'repository': jsonify(expected_api_data)
107 }
108 }
108 assert_ok(id_, expected, given=response.body)
109 assert_ok(id_, expected, given=response.body)
109 finally:
110 finally:
110 fixture.destroy_repo(repo_name)
111 fixture.destroy_repo(repo_name)
111 if updates.get('_group'):
112 if updates.get('_group'):
112 fixture.destroy_repo_group(updates['_group'])
113 fixture.destroy_repo_group(updates['_group'])
113
114
114 def test_api_update_repo_fork_of_field(self, backend):
115 def test_api_update_repo_fork_of_field(self, backend):
115 master_repo = backend.create_repo()
116 master_repo = backend.create_repo()
116 repo = backend.create_repo()
117 repo = backend.create_repo()
117 updates = {
118 updates = {
118 'fork_of': master_repo.repo_name
119 'fork_of': master_repo.repo_name
119 }
120 }
120 expected_api_data = repo.get_api_data(include_secrets=True)
121 expected_api_data = repo.get_api_data(include_secrets=True)
121 expected_api_data.update(updates)
122 expected_api_data.update(updates)
122
123
123 id_, params = build_data(
124 id_, params = build_data(
124 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
125 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
125 response = api_call(self.app, params)
126 response = api_call(self.app, params)
126 expected = {
127 expected = {
127 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
128 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
128 'repository': jsonify(expected_api_data)
129 'repository': jsonify(expected_api_data)
129 }
130 }
130 assert_ok(id_, expected, given=response.body)
131 assert_ok(id_, expected, given=response.body)
131 result = response.json['result']['repository']
132 result = response.json['result']['repository']
132 assert result['fork_of'] == master_repo.repo_name
133 assert result['fork_of'] == master_repo.repo_name
133
134
134 def test_api_update_repo_fork_of_not_found(self, backend):
135 def test_api_update_repo_fork_of_not_found(self, backend):
135 master_repo_name = 'fake-parent-repo'
136 master_repo_name = 'fake-parent-repo'
136 repo = backend.create_repo()
137 repo = backend.create_repo()
137 updates = {
138 updates = {
138 'fork_of': master_repo_name
139 'fork_of': master_repo_name
139 }
140 }
140 id_, params = build_data(
141 id_, params = build_data(
141 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
142 self.apikey, 'update_repo', repoid=repo.repo_name, **updates)
142 response = api_call(self.app, params)
143 response = api_call(self.app, params)
143 expected = {
144 expected = {
144 'repo_fork_of': 'Fork with id `{}` does not exists'.format(
145 'repo_fork_of': 'Fork with id `{}` does not exists'.format(
145 master_repo_name)}
146 master_repo_name)}
146 assert_error(id_, expected, given=response.body)
147 assert_error(id_, expected, given=response.body)
147
148
148 def test_api_update_repo_with_repo_group_not_existing(self):
149 def test_api_update_repo_with_repo_group_not_existing(self):
149 repo_name = 'admin_owned'
150 repo_name = 'admin_owned'
150 fake_repo_group = 'test_group_for_update'
151 fake_repo_group = 'test_group_for_update'
151 fixture.create_repo(repo_name)
152 fixture.create_repo(repo_name)
152 updates = {'repo_name': '{}/{}'.format(fake_repo_group, repo_name)}
153 updates = {'repo_name': '{}/{}'.format(fake_repo_group, repo_name)}
153 id_, params = build_data(
154 id_, params = build_data(
154 self.apikey, 'update_repo', repoid=repo_name, **updates)
155 self.apikey, 'update_repo', repoid=repo_name, **updates)
155 response = api_call(self.app, params)
156 response = api_call(self.app, params)
156 try:
157 try:
157 expected = {
158 expected = {
158 'repo_group': 'Repository group `{}` does not exist'.format(fake_repo_group)
159 'repo_group': 'Repository group `{}` does not exist'.format(fake_repo_group)
159 }
160 }
160 assert_error(id_, expected, given=response.body)
161 assert_error(id_, expected, given=response.body)
161 finally:
162 finally:
162 fixture.destroy_repo(repo_name)
163 fixture.destroy_repo(repo_name)
163
164
164 def test_api_update_repo_regular_user_not_allowed(self):
165 def test_api_update_repo_regular_user_not_allowed(self):
165 repo_name = 'admin_owned'
166 repo_name = 'admin_owned'
166 fixture.create_repo(repo_name)
167 fixture.create_repo(repo_name)
167 updates = {'active': False}
168 updates = {'active': False}
168 id_, params = build_data(
169 id_, params = build_data(
169 self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
170 self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
170 response = api_call(self.app, params)
171 response = api_call(self.app, params)
171 try:
172 try:
172 expected = 'repository `%s` does not exist' % (repo_name,)
173 expected = 'repository `%s` does not exist' % (repo_name,)
173 assert_error(id_, expected, given=response.body)
174 assert_error(id_, expected, given=response.body)
174 finally:
175 finally:
175 fixture.destroy_repo(repo_name)
176 fixture.destroy_repo(repo_name)
176
177
177 @mock.patch.object(RepoModel, 'update', crash)
178 @mock.patch.object(RepoModel, 'update', crash)
178 def test_api_update_repo_exception_occurred(self, backend):
179 def test_api_update_repo_exception_occurred(self, backend):
179 repo_name = UPDATE_REPO_NAME
180 repo_name = UPDATE_REPO_NAME
180 fixture.create_repo(repo_name, repo_type=backend.alias)
181 fixture.create_repo(repo_name, repo_type=backend.alias)
181 id_, params = build_data(
182 id_, params = build_data(
182 self.apikey, 'update_repo', repoid=repo_name,
183 self.apikey, 'update_repo', repoid=repo_name,
183 owner=TEST_USER_ADMIN_LOGIN,)
184 owner=TEST_USER_ADMIN_LOGIN,)
184 response = api_call(self.app, params)
185 response = api_call(self.app, params)
185 try:
186 try:
186 expected = 'failed to update repo `%s`' % (repo_name,)
187 expected = 'failed to update repo `%s`' % (repo_name,)
187 assert_error(id_, expected, given=response.body)
188 assert_error(id_, expected, given=response.body)
188 finally:
189 finally:
189 fixture.destroy_repo(repo_name)
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 from pylons import tmpl_context as c
23 from pylons import tmpl_context as c
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils import PartialRenderer
27 from rhodecode.lib.utils import PartialRenderer
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.model import repo
31 from rhodecode.model import repo
32 from rhodecode.model import repo_group
32 from rhodecode.model.db import User
33 from rhodecode.model.db import User
33 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.scm import ScmModel
34
35
35 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
36
37
37
38
38 ADMIN_PREFIX = '/_admin'
39 ADMIN_PREFIX = '/_admin'
39 STATIC_FILE_PREFIX = '/_static'
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 def get_format_ref_id(repo):
49 def get_format_ref_id(repo):
43 """Returns a `repo` specific reference formatter function"""
50 """Returns a `repo` specific reference formatter function"""
44 if h.is_svn(repo):
51 if h.is_svn(repo):
45 return _format_ref_id_svn
52 return _format_ref_id_svn
46 else:
53 else:
47 return _format_ref_id
54 return _format_ref_id
48
55
49
56
50 def _format_ref_id(name, raw_id):
57 def _format_ref_id(name, raw_id):
51 """Default formatting of a given reference `name`"""
58 """Default formatting of a given reference `name`"""
52 return name
59 return name
53
60
54
61
55 def _format_ref_id_svn(name, raw_id):
62 def _format_ref_id_svn(name, raw_id):
56 """Special way of formatting a reference for Subversion including path"""
63 """Special way of formatting a reference for Subversion including path"""
57 return '%s@%s' % (name, raw_id)
64 return '%s@%s' % (name, raw_id)
58
65
59
66
60 class TemplateArgs(StrictAttributeDict):
67 class TemplateArgs(StrictAttributeDict):
61 pass
68 pass
62
69
63
70
64 class BaseAppView(object):
71 class BaseAppView(object):
65
72
66 def __init__(self, context, request):
73 def __init__(self, context, request):
67 self.request = request
74 self.request = request
68 self.context = context
75 self.context = context
69 self.session = request.session
76 self.session = request.session
70 self._rhodecode_user = request.user # auth user
77 self._rhodecode_user = request.user # auth user
71 self._rhodecode_db_user = self._rhodecode_user.get_instance()
78 self._rhodecode_db_user = self._rhodecode_user.get_instance()
72 self._maybe_needs_password_change(
79 self._maybe_needs_password_change(
73 request.matched_route.name, self._rhodecode_db_user)
80 request.matched_route.name, self._rhodecode_db_user)
74
81
75 def _maybe_needs_password_change(self, view_name, user_obj):
82 def _maybe_needs_password_change(self, view_name, user_obj):
76 log.debug('Checking if user %s needs password change on view %s',
83 log.debug('Checking if user %s needs password change on view %s',
77 user_obj, view_name)
84 user_obj, view_name)
78 skip_user_views = [
85 skip_user_views = [
79 'logout', 'login',
86 'logout', 'login',
80 'my_account_password', 'my_account_password_update'
87 'my_account_password', 'my_account_password_update'
81 ]
88 ]
82
89
83 if not user_obj:
90 if not user_obj:
84 return
91 return
85
92
86 if user_obj.username == User.DEFAULT_USER:
93 if user_obj.username == User.DEFAULT_USER:
87 return
94 return
88
95
89 now = time.time()
96 now = time.time()
90 should_change = user_obj.user_data.get('force_password_change')
97 should_change = user_obj.user_data.get('force_password_change')
91 change_after = safe_int(should_change) or 0
98 change_after = safe_int(should_change) or 0
92 if should_change and now > change_after:
99 if should_change and now > change_after:
93 log.debug('User %s requires password change', user_obj)
100 log.debug('User %s requires password change', user_obj)
94 h.flash('You are required to change your password', 'warning',
101 h.flash('You are required to change your password', 'warning',
95 ignore_duplicate=True)
102 ignore_duplicate=True)
96
103
97 if view_name not in skip_user_views:
104 if view_name not in skip_user_views:
98 raise HTTPFound(
105 raise HTTPFound(
99 self.request.route_path('my_account_password'))
106 self.request.route_path('my_account_password'))
100
107
101 def _get_local_tmpl_context(self):
108 def _get_local_tmpl_context(self):
102 c = TemplateArgs()
109 c = TemplateArgs()
103 c.auth_user = self.request.user
110 c.auth_user = self.request.user
104 return c
111 return c
105
112
106 def _register_global_c(self, tmpl_args):
113 def _register_global_c(self, tmpl_args):
107 """
114 """
108 Registers attributes to pylons global `c`
115 Registers attributes to pylons global `c`
109 """
116 """
110 # TODO(marcink): remove once pyramid migration is finished
117 # TODO(marcink): remove once pyramid migration is finished
111 for k, v in tmpl_args.items():
118 for k, v in tmpl_args.items():
112 setattr(c, k, v)
119 setattr(c, k, v)
113
120
114 def _get_template_context(self, tmpl_args):
121 def _get_template_context(self, tmpl_args):
115 self._register_global_c(tmpl_args)
122 self._register_global_c(tmpl_args)
116
123
117 local_tmpl_args = {
124 local_tmpl_args = {
118 'defaults': {},
125 'defaults': {},
119 'errors': {},
126 'errors': {},
120 }
127 }
121 local_tmpl_args.update(tmpl_args)
128 local_tmpl_args.update(tmpl_args)
122 return local_tmpl_args
129 return local_tmpl_args
123
130
124 def load_default_context(self):
131 def load_default_context(self):
125 """
132 """
126 example:
133 example:
127
134
128 def load_default_context(self):
135 def load_default_context(self):
129 c = self._get_local_tmpl_context()
136 c = self._get_local_tmpl_context()
130 c.custom_var = 'foobar'
137 c.custom_var = 'foobar'
131 self._register_global_c(c)
138 self._register_global_c(c)
132 return c
139 return c
133 """
140 """
134 raise NotImplementedError('Needs implementation in view class')
141 raise NotImplementedError('Needs implementation in view class')
135
142
136
143
137 class RepoAppView(BaseAppView):
144 class RepoAppView(BaseAppView):
138
145
139 def __init__(self, context, request):
146 def __init__(self, context, request):
140 super(RepoAppView, self).__init__(context, request)
147 super(RepoAppView, self).__init__(context, request)
141 self.db_repo = request.db_repo
148 self.db_repo = request.db_repo
142 self.db_repo_name = self.db_repo.repo_name
149 self.db_repo_name = self.db_repo.repo_name
143 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
150 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
144
151
145 def _handle_missing_requirements(self, error):
152 def _handle_missing_requirements(self, error):
146 log.error(
153 log.error(
147 'Requirements are missing for repository %s: %s',
154 'Requirements are missing for repository %s: %s',
148 self.db_repo_name, error.message)
155 self.db_repo_name, error.message)
149
156
150 def _get_local_tmpl_context(self):
157 def _get_local_tmpl_context(self):
151 c = super(RepoAppView, self)._get_local_tmpl_context()
158 c = super(RepoAppView, self)._get_local_tmpl_context()
152 # register common vars for this type of view
159 # register common vars for this type of view
153 c.rhodecode_db_repo = self.db_repo
160 c.rhodecode_db_repo = self.db_repo
154 c.repo_name = self.db_repo_name
161 c.repo_name = self.db_repo_name
155 c.repository_pull_requests = self.db_repo_pull_requests
162 c.repository_pull_requests = self.db_repo_pull_requests
156
163
157 c.repository_requirements_missing = False
164 c.repository_requirements_missing = False
158 try:
165 try:
159 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
166 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
160 except RepositoryRequirementError as e:
167 except RepositoryRequirementError as e:
161 c.repository_requirements_missing = True
168 c.repository_requirements_missing = True
162 self._handle_missing_requirements(e)
169 self._handle_missing_requirements(e)
163
170
164 return c
171 return c
165
172
166
173
167 class DataGridAppView(object):
174 class DataGridAppView(object):
168 """
175 """
169 Common class to have re-usable grid rendering components
176 Common class to have re-usable grid rendering components
170 """
177 """
171
178
172 def _extract_ordering(self, request, column_map=None):
179 def _extract_ordering(self, request, column_map=None):
173 column_map = column_map or {}
180 column_map = column_map or {}
174 column_index = safe_int(request.GET.get('order[0][column]'))
181 column_index = safe_int(request.GET.get('order[0][column]'))
175 order_dir = request.GET.get(
182 order_dir = request.GET.get(
176 'order[0][dir]', 'desc')
183 'order[0][dir]', 'desc')
177 order_by = request.GET.get(
184 order_by = request.GET.get(
178 'columns[%s][data][sort]' % column_index, 'name_raw')
185 'columns[%s][data][sort]' % column_index, 'name_raw')
179
186
180 # translate datatable to DB columns
187 # translate datatable to DB columns
181 order_by = column_map.get(order_by) or order_by
188 order_by = column_map.get(order_by) or order_by
182
189
183 search_q = request.GET.get('search[value]')
190 search_q = request.GET.get('search[value]')
184 return search_q, order_by, order_dir
191 return search_q, order_by, order_dir
185
192
186 def _extract_chunk(self, request):
193 def _extract_chunk(self, request):
187 start = safe_int(request.GET.get('start'), 0)
194 start = safe_int(request.GET.get('start'), 0)
188 length = safe_int(request.GET.get('length'), 25)
195 length = safe_int(request.GET.get('length'), 25)
189 draw = safe_int(request.GET.get('draw'))
196 draw = safe_int(request.GET.get('draw'))
190 return draw, start, length
197 return draw, start, length
191
198
192
199
193 class BaseReferencesView(RepoAppView):
200 class BaseReferencesView(RepoAppView):
194 """
201 """
195 Base for reference view for branches, tags and bookmarks.
202 Base for reference view for branches, tags and bookmarks.
196 """
203 """
197 def load_default_context(self):
204 def load_default_context(self):
198 c = self._get_local_tmpl_context()
205 c = self._get_local_tmpl_context()
199
206
200 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
207 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
201 c.repo_info = self.db_repo
208 c.repo_info = self.db_repo
202
209
203 self._register_global_c(c)
210 self._register_global_c(c)
204 return c
211 return c
205
212
206 def load_refs_context(self, ref_items, partials_template):
213 def load_refs_context(self, ref_items, partials_template):
207 _render = PartialRenderer(partials_template)
214 _render = PartialRenderer(partials_template)
208 _data = []
215 _data = []
209 pre_load = ["author", "date", "message"]
216 pre_load = ["author", "date", "message"]
210
217
211 is_svn = h.is_svn(self.rhodecode_vcs_repo)
218 is_svn = h.is_svn(self.rhodecode_vcs_repo)
212 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
219 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
213
220
214 for ref_name, commit_id in ref_items:
221 for ref_name, commit_id in ref_items:
215 commit = self.rhodecode_vcs_repo.get_commit(
222 commit = self.rhodecode_vcs_repo.get_commit(
216 commit_id=commit_id, pre_load=pre_load)
223 commit_id=commit_id, pre_load=pre_load)
217
224
218 # TODO: johbo: Unify generation of reference links
225 # TODO: johbo: Unify generation of reference links
219 use_commit_id = '/' in ref_name or is_svn
226 use_commit_id = '/' in ref_name or is_svn
220 files_url = h.url(
227 files_url = h.url(
221 'files_home',
228 'files_home',
222 repo_name=c.repo_name,
229 repo_name=c.repo_name,
223 f_path=ref_name if is_svn else '',
230 f_path=ref_name if is_svn else '',
224 revision=commit_id if use_commit_id else ref_name,
231 revision=commit_id if use_commit_id else ref_name,
225 at=ref_name)
232 at=ref_name)
226
233
227 _data.append({
234 _data.append({
228 "name": _render('name', ref_name, files_url),
235 "name": _render('name', ref_name, files_url),
229 "name_raw": ref_name,
236 "name_raw": ref_name,
230 "date": _render('date', commit.date),
237 "date": _render('date', commit.date),
231 "date_raw": datetime_to_time(commit.date),
238 "date_raw": datetime_to_time(commit.date),
232 "author": _render('author', commit.author),
239 "author": _render('author', commit.author),
233 "commit": _render(
240 "commit": _render(
234 'commit', commit.message, commit.raw_id, commit.idx),
241 'commit', commit.message, commit.raw_id, commit.idx),
235 "commit_raw": commit.idx,
242 "commit_raw": commit.idx,
236 "compare": _render(
243 "compare": _render(
237 'compare', format_ref_id(ref_name, commit.raw_id)),
244 'compare', format_ref_id(ref_name, commit.raw_id)),
238 })
245 })
239 c.has_references = bool(_data)
246 c.has_references = bool(_data)
240 c.data = json.dumps(_data)
247 c.data = json.dumps(_data)
241
248
242
249
243 class RepoRoutePredicate(object):
250 class RepoRoutePredicate(object):
244 def __init__(self, val, config):
251 def __init__(self, val, config):
245 self.val = val
252 self.val = val
246
253
247 def text(self):
254 def text(self):
248 return 'repo_route = %s' % self.val
255 return 'repo_route = %s' % self.val
249
256
250 phash = text
257 phash = text
251
258
252 def __call__(self, info, request):
259 def __call__(self, info, request):
253 repo_name = info['match']['repo_name']
260 repo_name = info['match']['repo_name']
254 repo_model = repo.RepoModel()
261 repo_model = repo.RepoModel()
255 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
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,
263
257 # and validate repo based on the type.
258 if by_name_match:
264 if by_name_match:
259 # register this as request object we can re-use later
265 # register this as request object we can re-use later
260 request.db_repo = by_name_match
266 request.db_repo = by_name_match
261 return True
267 return True
262
268
263 by_id_match = repo_model.get_repo_by_id(repo_name)
269 by_id_match = repo_model.get_repo_by_id(repo_name)
264 if by_id_match:
270 if by_id_match:
265 request.db_repo = by_id_match
271 request.db_repo = by_id_match
266 return True
272 return True
267
273
268 return False
274 return False
269
275
270
276
271 class RepoTypeRoutePredicate(object):
277 class RepoTypeRoutePredicate(object):
272 def __init__(self, val, config):
278 def __init__(self, val, config):
273 self.val = val or ['hg', 'git', 'svn']
279 self.val = val or ['hg', 'git', 'svn']
274
280
275 def text(self):
281 def text(self):
276 return 'repo_accepted_type = %s' % self.val
282 return 'repo_accepted_type = %s' % self.val
277
283
278 phash = text
284 phash = text
279
285
280 def __call__(self, info, request):
286 def __call__(self, info, request):
281
287
282 rhodecode_db_repo = request.db_repo
288 rhodecode_db_repo = request.db_repo
283
289
284 log.debug(
290 log.debug(
285 '%s checking repo type for %s in %s',
291 '%s checking repo type for %s in %s',
286 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
292 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
287
293
288 if rhodecode_db_repo.repo_type in self.val:
294 if rhodecode_db_repo.repo_type in self.val:
289 return True
295 return True
290 else:
296 else:
291 log.warning('Current view is not supported for repo type:%s',
297 log.warning('Current view is not supported for repo type:%s',
292 rhodecode_db_repo.repo_type)
298 rhodecode_db_repo.repo_type)
293 #
299 #
294 # h.flash(h.literal(
300 # h.flash(h.literal(
295 # _('Action not supported for %s.' % rhodecode_repo.alias)),
301 # _('Action not supported for %s.' % rhodecode_repo.alias)),
296 # category='warning')
302 # category='warning')
297 # return redirect(
303 # return redirect(
298 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
304 # url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
299
305
300 return False
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 def includeme(config):
332 def includeme(config):
305 config.add_route_predicate(
333 config.add_route_predicate(
306 'repo_route', RepoRoutePredicate)
334 'repo_route', RepoRoutePredicate)
307 config.add_route_predicate(
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 from pylons import tmpl_context as c
23 from pylons import tmpl_context as c
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.model.db import Repository, User
26 from rhodecode.model.db import Repository, User
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.repo import RepoModel
28 from rhodecode.model.repo import RepoModel
29 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.model.repo_group import RepoGroupModel
30 from rhodecode.model.settings import SettingsModel
30 from rhodecode.model.settings import SettingsModel
31 from rhodecode.tests import TestController, url
31 from rhodecode.tests import TestController
32 from rhodecode.tests.fixture import Fixture
32 from rhodecode.tests.fixture import Fixture
33
33
34
34
35 fixture = Fixture()
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 class TestHomeController(TestController):
45 class TestHomeController(TestController):
39
46
40 def test_index(self):
47 def test_index(self):
41 self.log_user()
48 self.log_user()
42 response = self.app.get(url(controller='home', action='index'))
49 response = self.app.get(route_path('home'))
43 # if global permission is set
50 # if global permission is set
44 response.mustcontain('Add Repository')
51 response.mustcontain('Add Repository')
45
52
46 # search for objects inside the JavaScript JSON
53 # search for objects inside the JavaScript JSON
47 for repo in Repository.getAll():
54 for repo in Repository.getAll():
48 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
55 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
49
56
50 def test_index_contains_statics_with_ver(self):
57 def test_index_contains_statics_with_ver(self):
51 self.log_user()
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 rhodecode_version_hash = c.rhodecode_version_hash
61 rhodecode_version_hash = c.rhodecode_version_hash
55 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
62 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
56 response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash))
63 response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash))
57
64
58 def test_index_contains_backend_specific_details(self, backend):
65 def test_index_contains_backend_specific_details(self, backend):
59 self.log_user()
66 self.log_user()
60 response = self.app.get(url(controller='home', action='index'))
67 response = self.app.get(route_path('home'))
61 tip = backend.repo.get_commit().raw_id
68 tip = backend.repo.get_commit().raw_id
62
69
63 # html in javascript variable:
70 # html in javascript variable:
64 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
71 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
65 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
72 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
66
73
67 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
74 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
68 response.mustcontain("""Added a symlink""")
75 response.mustcontain("""Added a symlink""")
69
76
70 def test_index_with_anonymous_access_disabled(self):
77 def test_index_with_anonymous_access_disabled(self):
71 with fixture.anon_access(False):
78 with fixture.anon_access(False):
72 response = self.app.get(url(controller='home', action='index'),
79 response = self.app.get(route_path('home'), status=302)
73 status=302)
74 assert 'login' in response.location
80 assert 'login' in response.location
75
81
76 def test_index_page_on_groups(self, autologin_user, repo_group):
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 response.mustcontain("gr1/repo_in_group")
84 response.mustcontain("gr1/repo_in_group")
79
85
80 def test_index_page_on_group_with_trailing_slash(
86 def test_index_page_on_group_with_trailing_slash(
81 self, autologin_user, repo_group):
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 response.mustcontain("gr1/repo_in_group")
89 response.mustcontain("gr1/repo_in_group")
84
90
85 @pytest.fixture(scope='class')
91 @pytest.fixture(scope='class')
86 def repo_group(self, request):
92 def repo_group(self, request):
87 gr = fixture.create_repo_group('gr1')
93 gr = fixture.create_repo_group('gr1')
88 fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
94 fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
89
95
90 @request.addfinalizer
96 @request.addfinalizer
91 def cleanup():
97 def cleanup():
92 RepoModel().delete('gr1/repo_in_group')
98 RepoModel().delete('gr1/repo_in_group')
93 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
99 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
94 Session().commit()
100 Session().commit()
95
101
96 def test_index_with_name_with_tags(self, autologin_user):
102 def test_index_with_name_with_tags(self, user_util, autologin_user):
97 user = User.get_by_username('test_admin')
103 user = user_util.create_user()
104 username = user.username
98 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
105 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
99 user.lastname = (
106 user.lastname = (
100 '<img src="/image2" onload="alert(\'Hello, World!\');">')
107 '<img src="/image2" onload="alert(\'Hello, World!\');">')
101 Session().add(user)
108 Session().add(user)
102 Session().commit()
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 response.mustcontain(
113 response.mustcontain(
106 '&lt;img src=&#34;/image1&#34; onload=&#34;'
114 '&lt;img src=&#34;/image1&#34; onload=&#34;'
107 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
115 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
108 response.mustcontain(
116 response.mustcontain(
109 '&lt;img src=&#34;/image2&#34; onload=&#34;'
117 '&lt;img src=&#34;/image2&#34; onload=&#34;'
110 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
118 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
111
119
112 @pytest.mark.parametrize("name, state", [
120 @pytest.mark.parametrize("name, state", [
113 ('Disabled', False),
121 ('Disabled', False),
114 ('Enabled', True),
122 ('Enabled', True),
115 ])
123 ])
116 def test_index_show_version(self, autologin_user, name, state):
124 def test_index_show_version(self, autologin_user, name, state):
117 version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
125 version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
118
126
119 sett = SettingsModel().create_or_update_setting(
127 sett = SettingsModel().create_or_update_setting(
120 'show_version', state, 'bool')
128 'show_version', state, 'bool')
121 Session().add(sett)
129 Session().add(sett)
122 Session().commit()
130 Session().commit()
123 SettingsModel().invalidate_settings_cache()
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 if state is True:
134 if state is True:
127 response.mustcontain(version_string)
135 response.mustcontain(version_string)
128 if state is False:
136 if state is False:
129 response.mustcontain(no=[version_string])
137 response.mustcontain(no=[version_string])
@@ -1,248 +1,304 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23
23
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib import helpers as h
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 from rhodecode.lib.index import searcher_from_config
30 from rhodecode.lib.index import searcher_from_config
30 from rhodecode.lib.utils2 import safe_unicode, str2bool
31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 from rhodecode.lib.ext_json import json
31 from rhodecode.model.db import func, Repository, RepoGroup
33 from rhodecode.model.db import func, Repository, RepoGroup
32 from rhodecode.model.repo import RepoModel
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 from rhodecode.model.user import UserModel
37 from rhodecode.model.user import UserModel
35 from rhodecode.model.user_group import UserGroupModel
38 from rhodecode.model.user_group import UserGroupModel
36
39
37 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
38
41
39
42
40 class HomeView(BaseAppView):
43 class HomeView(BaseAppView):
41
44
42 def load_default_context(self):
45 def load_default_context(self):
43 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
44 c.user = c.auth_user.get_instance()
47 c.user = c.auth_user.get_instance()
45 self._register_global_c(c)
48 self._register_global_c(c)
46 return c
49 return c
47
50
48 @LoginRequired()
51 @LoginRequired()
49 @view_config(
52 @view_config(
50 route_name='user_autocomplete_data', request_method='GET',
53 route_name='user_autocomplete_data', request_method='GET',
51 renderer='json_ext', xhr=True)
54 renderer='json_ext', xhr=True)
52 def user_autocomplete_data(self):
55 def user_autocomplete_data(self):
53 query = self.request.GET.get('query')
56 query = self.request.GET.get('query')
54 active = str2bool(self.request.GET.get('active') or True)
57 active = str2bool(self.request.GET.get('active') or True)
55 include_groups = str2bool(self.request.GET.get('user_groups'))
58 include_groups = str2bool(self.request.GET.get('user_groups'))
56 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
59 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
57 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
60 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
58
61
59 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
62 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
60 query, active, include_groups)
63 query, active, include_groups)
61
64
62 _users = UserModel().get_users(
65 _users = UserModel().get_users(
63 name_contains=query, only_active=active)
66 name_contains=query, only_active=active)
64
67
65 def maybe_skip_default_user(usr):
68 def maybe_skip_default_user(usr):
66 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
69 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
67 return False
70 return False
68 return True
71 return True
69 _users = filter(maybe_skip_default_user, _users)
72 _users = filter(maybe_skip_default_user, _users)
70
73
71 if include_groups:
74 if include_groups:
72 # extend with user groups
75 # extend with user groups
73 _user_groups = UserGroupModel().get_user_groups(
76 _user_groups = UserGroupModel().get_user_groups(
74 name_contains=query, only_active=active,
77 name_contains=query, only_active=active,
75 expand_groups=expand_groups)
78 expand_groups=expand_groups)
76 _users = _users + _user_groups
79 _users = _users + _user_groups
77
80
78 return {'suggestions': _users}
81 return {'suggestions': _users}
79
82
80 @LoginRequired()
83 @LoginRequired()
81 @NotAnonymous()
84 @NotAnonymous()
82 @view_config(
85 @view_config(
83 route_name='user_group_autocomplete_data', request_method='GET',
86 route_name='user_group_autocomplete_data', request_method='GET',
84 renderer='json_ext', xhr=True)
87 renderer='json_ext', xhr=True)
85 def user_group_autocomplete_data(self):
88 def user_group_autocomplete_data(self):
86 query = self.request.GET.get('query')
89 query = self.request.GET.get('query')
87 active = str2bool(self.request.GET.get('active') or True)
90 active = str2bool(self.request.GET.get('active') or True)
88 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
91 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
89
92
90 log.debug('generating user group list, query:%s, active:%s',
93 log.debug('generating user group list, query:%s, active:%s',
91 query, active)
94 query, active)
92
95
93 _user_groups = UserGroupModel().get_user_groups(
96 _user_groups = UserGroupModel().get_user_groups(
94 name_contains=query, only_active=active,
97 name_contains=query, only_active=active,
95 expand_groups=expand_groups)
98 expand_groups=expand_groups)
96 _user_groups = _user_groups
99 _user_groups = _user_groups
97
100
98 return {'suggestions': _user_groups}
101 return {'suggestions': _user_groups}
99
102
100 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
103 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
101 query = Repository.query()\
104 query = Repository.query()\
102 .order_by(func.length(Repository.repo_name))\
105 .order_by(func.length(Repository.repo_name))\
103 .order_by(Repository.repo_name)
106 .order_by(Repository.repo_name)
104
107
105 if repo_type:
108 if repo_type:
106 query = query.filter(Repository.repo_type == repo_type)
109 query = query.filter(Repository.repo_type == repo_type)
107
110
108 if name_contains:
111 if name_contains:
109 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
112 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
110 query = query.filter(
113 query = query.filter(
111 Repository.repo_name.ilike(ilike_expression))
114 Repository.repo_name.ilike(ilike_expression))
112 query = query.limit(limit)
115 query = query.limit(limit)
113
116
114 all_repos = query.all()
117 all_repos = query.all()
115 # permission checks are inside this function
118 # permission checks are inside this function
116 repo_iter = ScmModel().get_repos(all_repos)
119 repo_iter = ScmModel().get_repos(all_repos)
117 return [
120 return [
118 {
121 {
119 'id': obj['name'],
122 'id': obj['name'],
120 'text': obj['name'],
123 'text': obj['name'],
121 'type': 'repo',
124 'type': 'repo',
122 'obj': obj['dbrepo'],
125 'obj': obj['dbrepo'],
123 'url': h.url('summary_home', repo_name=obj['name'])
126 'url': h.url('summary_home', repo_name=obj['name'])
124 }
127 }
125 for obj in repo_iter]
128 for obj in repo_iter]
126
129
127 def _get_repo_group_list(self, name_contains=None, limit=20):
130 def _get_repo_group_list(self, name_contains=None, limit=20):
128 query = RepoGroup.query()\
131 query = RepoGroup.query()\
129 .order_by(func.length(RepoGroup.group_name))\
132 .order_by(func.length(RepoGroup.group_name))\
130 .order_by(RepoGroup.group_name)
133 .order_by(RepoGroup.group_name)
131
134
132 if name_contains:
135 if name_contains:
133 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
136 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
134 query = query.filter(
137 query = query.filter(
135 RepoGroup.group_name.ilike(ilike_expression))
138 RepoGroup.group_name.ilike(ilike_expression))
136 query = query.limit(limit)
139 query = query.limit(limit)
137
140
138 all_groups = query.all()
141 all_groups = query.all()
139 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
142 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
140 return [
143 return [
141 {
144 {
142 'id': obj.group_name,
145 'id': obj.group_name,
143 'text': obj.group_name,
146 'text': obj.group_name,
144 'type': 'group',
147 'type': 'group',
145 'obj': {},
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 for obj in repo_groups_iter]
151 for obj in repo_groups_iter]
149
152
150 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
153 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
151 if not hash_starts_with or len(hash_starts_with) < 3:
154 if not hash_starts_with or len(hash_starts_with) < 3:
152 return []
155 return []
153
156
154 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
157 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
155
158
156 if len(commit_hashes) != 1:
159 if len(commit_hashes) != 1:
157 return []
160 return []
158
161
159 commit_hash_prefix = commit_hashes[0]
162 commit_hash_prefix = commit_hashes[0]
160
163
161 searcher = searcher_from_config(self.request.registry.settings)
164 searcher = searcher_from_config(self.request.registry.settings)
162 result = searcher.search(
165 result = searcher.search(
163 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
166 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
164 raise_on_exc=False)
167 raise_on_exc=False)
165
168
166 return [
169 return [
167 {
170 {
168 'id': entry['commit_id'],
171 'id': entry['commit_id'],
169 'text': entry['commit_id'],
172 'text': entry['commit_id'],
170 'type': 'commit',
173 'type': 'commit',
171 'obj': {'repo': entry['repository']},
174 'obj': {'repo': entry['repository']},
172 'url': h.url('changeset_home',
175 'url': h.url('changeset_home',
173 repo_name=entry['repository'],
176 repo_name=entry['repository'],
174 revision=entry['commit_id'])
177 revision=entry['commit_id'])
175 }
178 }
176 for entry in result['results']]
179 for entry in result['results']]
177
180
178 @LoginRequired()
181 @LoginRequired()
179 @view_config(
182 @view_config(
180 route_name='repo_list_data', request_method='GET',
183 route_name='repo_list_data', request_method='GET',
181 renderer='json_ext', xhr=True)
184 renderer='json_ext', xhr=True)
182 def repo_list_data(self):
185 def repo_list_data(self):
183 _ = self.request.translate
186 _ = self.request.translate
184
187
185 query = self.request.GET.get('query')
188 query = self.request.GET.get('query')
186 repo_type = self.request.GET.get('repo_type')
189 repo_type = self.request.GET.get('repo_type')
187 log.debug('generating repo list, query:%s, repo_type:%s',
190 log.debug('generating repo list, query:%s, repo_type:%s',
188 query, repo_type)
191 query, repo_type)
189
192
190 res = []
193 res = []
191 repos = self._get_repo_list(query, repo_type=repo_type)
194 repos = self._get_repo_list(query, repo_type=repo_type)
192 if repos:
195 if repos:
193 res.append({
196 res.append({
194 'text': _('Repositories'),
197 'text': _('Repositories'),
195 'children': repos
198 'children': repos
196 })
199 })
197
200
198 data = {
201 data = {
199 'more': False,
202 'more': False,
200 'results': res
203 'results': res
201 }
204 }
202 return data
205 return data
203
206
204 @LoginRequired()
207 @LoginRequired()
205 @view_config(
208 @view_config(
206 route_name='goto_switcher_data', request_method='GET',
209 route_name='goto_switcher_data', request_method='GET',
207 renderer='json_ext', xhr=True)
210 renderer='json_ext', xhr=True)
208 def goto_switcher_data(self):
211 def goto_switcher_data(self):
209 c = self.load_default_context()
212 c = self.load_default_context()
210
213
211 _ = self.request.translate
214 _ = self.request.translate
212
215
213 query = self.request.GET.get('query')
216 query = self.request.GET.get('query')
214 log.debug('generating goto switcher list, query %s', query)
217 log.debug('generating goto switcher list, query %s', query)
215
218
216 res = []
219 res = []
217 repo_groups = self._get_repo_group_list(query)
220 repo_groups = self._get_repo_group_list(query)
218 if repo_groups:
221 if repo_groups:
219 res.append({
222 res.append({
220 'text': _('Groups'),
223 'text': _('Groups'),
221 'children': repo_groups
224 'children': repo_groups
222 })
225 })
223
226
224 repos = self._get_repo_list(query)
227 repos = self._get_repo_list(query)
225 if repos:
228 if repos:
226 res.append({
229 res.append({
227 'text': _('Repositories'),
230 'text': _('Repositories'),
228 'children': repos
231 'children': repos
229 })
232 })
230
233
231 commits = self._get_hash_commit_list(c.auth_user, query)
234 commits = self._get_hash_commit_list(c.auth_user, query)
232 if commits:
235 if commits:
233 unique_repos = {}
236 unique_repos = {}
234 for commit in commits:
237 for commit in commits:
235 unique_repos.setdefault(commit['obj']['repo'], []
238 unique_repos.setdefault(commit['obj']['repo'], []
236 ).append(commit)
239 ).append(commit)
237
240
238 for repo in unique_repos:
241 for repo in unique_repos:
239 res.append({
242 res.append({
240 'text': _('Commits in %(repo)s') % {'repo': repo},
243 'text': _('Commits in %(repo)s') % {'repo': repo},
241 'children': unique_repos[repo]
244 'children': unique_repos[repo]
242 })
245 })
243
246
244 data = {
247 data = {
245 'more': False,
248 'more': False,
246 'results': res
249 'results': res
247 }
250 }
248 return data
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 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import collections
22 import collections
23 import datetime
23 import datetime
24 import formencode
24 import formencode
25 import logging
25 import logging
26 import urlparse
26 import urlparse
27
27
28 from pylons import url
29 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
30 from pyramid.view import view_config
29 from pyramid.view import view_config
31 from recaptcha.client.captcha import submit
30 from recaptcha.client.captcha import submit
32
31
33 from rhodecode.apps._base import BaseAppView
32 from rhodecode.apps._base import BaseAppView
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.events import UserRegistered
34 from rhodecode.events import UserRegistered
36 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
37 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
38 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
39 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
40 from rhodecode.lib.base import get_ip_addr
39 from rhodecode.lib.base import get_ip_addr
41 from rhodecode.lib.exceptions import UserCreationError
40 from rhodecode.lib.exceptions import UserCreationError
42 from rhodecode.lib.utils2 import safe_str
41 from rhodecode.lib.utils2 import safe_str
43 from rhodecode.model.db import User, UserApiKeys
42 from rhodecode.model.db import User, UserApiKeys
44 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
43 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
45 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
46 from rhodecode.model.auth_token import AuthTokenModel
45 from rhodecode.model.auth_token import AuthTokenModel
47 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
48 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
49 from rhodecode.translation import _
48 from rhodecode.translation import _
50
49
51
50
52 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
53
52
54 CaptchaData = collections.namedtuple(
53 CaptchaData = collections.namedtuple(
55 'CaptchaData', 'active, private_key, public_key')
54 'CaptchaData', 'active, private_key, public_key')
56
55
57
56
58 def _store_user_in_session(session, username, remember=False):
57 def _store_user_in_session(session, username, remember=False):
59 user = User.get_by_username(username, case_insensitive=True)
58 user = User.get_by_username(username, case_insensitive=True)
60 auth_user = AuthUser(user.user_id)
59 auth_user = AuthUser(user.user_id)
61 auth_user.set_authenticated()
60 auth_user.set_authenticated()
62 cs = auth_user.get_cookie_store()
61 cs = auth_user.get_cookie_store()
63 session['rhodecode_user'] = cs
62 session['rhodecode_user'] = cs
64 user.update_lastlogin()
63 user.update_lastlogin()
65 Session().commit()
64 Session().commit()
66
65
67 # If they want to be remembered, update the cookie
66 # If they want to be remembered, update the cookie
68 if remember:
67 if remember:
69 _year = (datetime.datetime.now() +
68 _year = (datetime.datetime.now() +
70 datetime.timedelta(seconds=60 * 60 * 24 * 365))
69 datetime.timedelta(seconds=60 * 60 * 24 * 365))
71 session._set_cookie_expires(_year)
70 session._set_cookie_expires(_year)
72
71
73 session.save()
72 session.save()
74
73
75 safe_cs = cs.copy()
74 safe_cs = cs.copy()
76 safe_cs['password'] = '****'
75 safe_cs['password'] = '****'
77 log.info('user %s is now authenticated and stored in '
76 log.info('user %s is now authenticated and stored in '
78 'session, session attrs %s', username, safe_cs)
77 'session, session attrs %s', username, safe_cs)
79
78
80 # dumps session attrs back to cookie
79 # dumps session attrs back to cookie
81 session._update_cookie_out()
80 session._update_cookie_out()
82 # we set new cookie
81 # we set new cookie
83 headers = None
82 headers = None
84 if session.request['set_cookie']:
83 if session.request['set_cookie']:
85 # send set-cookie headers back to response to update cookie
84 # send set-cookie headers back to response to update cookie
86 headers = [('Set-Cookie', session.request['cookie_out'])]
85 headers = [('Set-Cookie', session.request['cookie_out'])]
87 return headers
86 return headers
88
87
89
88
90 def get_came_from(request):
89 def get_came_from(request):
91 came_from = safe_str(request.GET.get('came_from', ''))
90 came_from = safe_str(request.GET.get('came_from', ''))
92 parsed = urlparse.urlparse(came_from)
91 parsed = urlparse.urlparse(came_from)
93 allowed_schemes = ['http', 'https']
92 allowed_schemes = ['http', 'https']
93 default_came_from = h.route_path('home')
94 if parsed.scheme and parsed.scheme not in allowed_schemes:
94 if parsed.scheme and parsed.scheme not in allowed_schemes:
95 log.error('Suspicious URL scheme detected %s for url %s' %
95 log.error('Suspicious URL scheme detected %s for url %s' %
96 (parsed.scheme, parsed))
96 (parsed.scheme, parsed))
97 came_from = url('home')
97 came_from = default_came_from
98 elif parsed.netloc and request.host != parsed.netloc:
98 elif parsed.netloc and request.host != parsed.netloc:
99 log.error('Suspicious NETLOC detected %s for url %s server url '
99 log.error('Suspicious NETLOC detected %s for url %s server url '
100 'is: %s' % (parsed.netloc, parsed, request.host))
100 'is: %s' % (parsed.netloc, parsed, request.host))
101 came_from = url('home')
101 came_from = default_came_from
102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
103 log.error('Header injection detected `%s` for url %s server url ' %
103 log.error('Header injection detected `%s` for url %s server url ' %
104 (parsed.path, parsed))
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 class LoginView(BaseAppView):
110 class LoginView(BaseAppView):
111
111
112 def load_default_context(self):
112 def load_default_context(self):
113 c = self._get_local_tmpl_context()
113 c = self._get_local_tmpl_context()
114 c.came_from = get_came_from(self.request)
114 c.came_from = get_came_from(self.request)
115 self._register_global_c(c)
115 self._register_global_c(c)
116 return c
116 return c
117
117
118 def _get_captcha_data(self):
118 def _get_captcha_data(self):
119 settings = SettingsModel().get_all_settings()
119 settings = SettingsModel().get_all_settings()
120 private_key = settings.get('rhodecode_captcha_private_key')
120 private_key = settings.get('rhodecode_captcha_private_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
121 public_key = settings.get('rhodecode_captcha_public_key')
122 active = bool(private_key)
122 active = bool(private_key)
123 return CaptchaData(
123 return CaptchaData(
124 active=active, private_key=private_key, public_key=public_key)
124 active=active, private_key=private_key, public_key=public_key)
125
125
126 @view_config(
126 @view_config(
127 route_name='login', request_method='GET',
127 route_name='login', request_method='GET',
128 renderer='rhodecode:templates/login.mako')
128 renderer='rhodecode:templates/login.mako')
129 def login(self):
129 def login(self):
130 c = self.load_default_context()
130 c = self.load_default_context()
131 auth_user = self._rhodecode_user
131 auth_user = self._rhodecode_user
132
132
133 # redirect if already logged in
133 # redirect if already logged in
134 if (auth_user.is_authenticated and
134 if (auth_user.is_authenticated and
135 not auth_user.is_default and auth_user.ip_allowed):
135 not auth_user.is_default and auth_user.ip_allowed):
136 raise HTTPFound(c.came_from)
136 raise HTTPFound(c.came_from)
137
137
138 # check if we use headers plugin, and try to login using it.
138 # check if we use headers plugin, and try to login using it.
139 try:
139 try:
140 log.debug('Running PRE-AUTH for headers based authentication')
140 log.debug('Running PRE-AUTH for headers based authentication')
141 auth_info = authenticate(
141 auth_info = authenticate(
142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
142 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
143 if auth_info:
143 if auth_info:
144 headers = _store_user_in_session(
144 headers = _store_user_in_session(
145 self.session, auth_info.get('username'))
145 self.session, auth_info.get('username'))
146 raise HTTPFound(c.came_from, headers=headers)
146 raise HTTPFound(c.came_from, headers=headers)
147 except UserCreationError as e:
147 except UserCreationError as e:
148 log.error(e)
148 log.error(e)
149 self.session.flash(e, queue='error')
149 self.session.flash(e, queue='error')
150
150
151 return self._get_template_context(c)
151 return self._get_template_context(c)
152
152
153 @view_config(
153 @view_config(
154 route_name='login', request_method='POST',
154 route_name='login', request_method='POST',
155 renderer='rhodecode:templates/login.mako')
155 renderer='rhodecode:templates/login.mako')
156 def login_post(self):
156 def login_post(self):
157 c = self.load_default_context()
157 c = self.load_default_context()
158
158
159 login_form = LoginForm()()
159 login_form = LoginForm()()
160
160
161 try:
161 try:
162 self.session.invalidate()
162 self.session.invalidate()
163 form_result = login_form.to_python(self.request.params)
163 form_result = login_form.to_python(self.request.params)
164 # form checks for username/password, now we're authenticated
164 # form checks for username/password, now we're authenticated
165 headers = _store_user_in_session(
165 headers = _store_user_in_session(
166 self.session,
166 self.session,
167 username=form_result['username'],
167 username=form_result['username'],
168 remember=form_result['remember'])
168 remember=form_result['remember'])
169 log.debug('Redirecting to "%s" after login.', c.came_from)
169 log.debug('Redirecting to "%s" after login.', c.came_from)
170
170
171 audit_user = audit_logger.UserWrap(
171 audit_user = audit_logger.UserWrap(
172 username=self.request.params.get('username'),
172 username=self.request.params.get('username'),
173 ip_addr=self.request.remote_addr)
173 ip_addr=self.request.remote_addr)
174 action_data = {'user_agent': self.request.user_agent}
174 action_data = {'user_agent': self.request.user_agent}
175 audit_logger.store(
175 audit_logger.store(
176 action='user.login.success', action_data=action_data,
176 action='user.login.success', action_data=action_data,
177 user=audit_user, commit=True)
177 user=audit_user, commit=True)
178
178
179 raise HTTPFound(c.came_from, headers=headers)
179 raise HTTPFound(c.came_from, headers=headers)
180 except formencode.Invalid as errors:
180 except formencode.Invalid as errors:
181 defaults = errors.value
181 defaults = errors.value
182 # remove password from filling in form again
182 # remove password from filling in form again
183 defaults.pop('password', None)
183 defaults.pop('password', None)
184 render_ctx = self._get_template_context(c)
184 render_ctx = self._get_template_context(c)
185 render_ctx.update({
185 render_ctx.update({
186 'errors': errors.error_dict,
186 'errors': errors.error_dict,
187 'defaults': defaults,
187 'defaults': defaults,
188 })
188 })
189
189
190 audit_user = audit_logger.UserWrap(
190 audit_user = audit_logger.UserWrap(
191 username=self.request.params.get('username'),
191 username=self.request.params.get('username'),
192 ip_addr=self.request.remote_addr)
192 ip_addr=self.request.remote_addr)
193 action_data = {'user_agent': self.request.user_agent}
193 action_data = {'user_agent': self.request.user_agent}
194 audit_logger.store(
194 audit_logger.store(
195 action='user.login.failure', action_data=action_data,
195 action='user.login.failure', action_data=action_data,
196 user=audit_user, commit=True)
196 user=audit_user, commit=True)
197 return render_ctx
197 return render_ctx
198
198
199 except UserCreationError as e:
199 except UserCreationError as e:
200 # headers auth or other auth functions that create users on
200 # headers auth or other auth functions that create users on
201 # the fly can throw this exception signaling that there's issue
201 # the fly can throw this exception signaling that there's issue
202 # with user creation, explanation should be provided in
202 # with user creation, explanation should be provided in
203 # Exception itself
203 # Exception itself
204 self.session.flash(e, queue='error')
204 self.session.flash(e, queue='error')
205 return self._get_template_context(c)
205 return self._get_template_context(c)
206
206
207 @CSRFRequired()
207 @CSRFRequired()
208 @view_config(route_name='logout', request_method='POST')
208 @view_config(route_name='logout', request_method='POST')
209 def logout(self):
209 def logout(self):
210 auth_user = self._rhodecode_user
210 auth_user = self._rhodecode_user
211 log.info('Deleting session for user: `%s`', auth_user)
211 log.info('Deleting session for user: `%s`', auth_user)
212
212
213 action_data = {'user_agent': self.request.user_agent}
213 action_data = {'user_agent': self.request.user_agent}
214 audit_logger.store(
214 audit_logger.store(
215 action='user.logout', action_data=action_data,
215 action='user.logout', action_data=action_data,
216 user=auth_user, commit=True)
216 user=auth_user, commit=True)
217 self.session.delete()
217 self.session.delete()
218 return HTTPFound(url('home'))
218 return HTTPFound(h.route_path('home'))
219
219
220 @HasPermissionAnyDecorator(
220 @HasPermissionAnyDecorator(
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
222 @view_config(
222 @view_config(
223 route_name='register', request_method='GET',
223 route_name='register', request_method='GET',
224 renderer='rhodecode:templates/register.mako',)
224 renderer='rhodecode:templates/register.mako',)
225 def register(self, defaults=None, errors=None):
225 def register(self, defaults=None, errors=None):
226 c = self.load_default_context()
226 c = self.load_default_context()
227 defaults = defaults or {}
227 defaults = defaults or {}
228 errors = errors or {}
228 errors = errors or {}
229
229
230 settings = SettingsModel().get_all_settings()
230 settings = SettingsModel().get_all_settings()
231 register_message = settings.get('rhodecode_register_message') or ''
231 register_message = settings.get('rhodecode_register_message') or ''
232 captcha = self._get_captcha_data()
232 captcha = self._get_captcha_data()
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
233 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
234 .AuthUser.permissions['global']
234 .AuthUser.permissions['global']
235
235
236 render_ctx = self._get_template_context(c)
236 render_ctx = self._get_template_context(c)
237 render_ctx.update({
237 render_ctx.update({
238 'defaults': defaults,
238 'defaults': defaults,
239 'errors': errors,
239 'errors': errors,
240 'auto_active': auto_active,
240 'auto_active': auto_active,
241 'captcha_active': captcha.active,
241 'captcha_active': captcha.active,
242 'captcha_public_key': captcha.public_key,
242 'captcha_public_key': captcha.public_key,
243 'register_message': register_message,
243 'register_message': register_message,
244 })
244 })
245 return render_ctx
245 return render_ctx
246
246
247 @HasPermissionAnyDecorator(
247 @HasPermissionAnyDecorator(
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
248 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
249 @view_config(
249 @view_config(
250 route_name='register', request_method='POST',
250 route_name='register', request_method='POST',
251 renderer='rhodecode:templates/register.mako')
251 renderer='rhodecode:templates/register.mako')
252 def register_post(self):
252 def register_post(self):
253 captcha = self._get_captcha_data()
253 captcha = self._get_captcha_data()
254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
254 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
255 .AuthUser.permissions['global']
255 .AuthUser.permissions['global']
256
256
257 register_form = RegisterForm()()
257 register_form = RegisterForm()()
258 try:
258 try:
259 form_result = register_form.to_python(self.request.params)
259 form_result = register_form.to_python(self.request.params)
260 form_result['active'] = auto_active
260 form_result['active'] = auto_active
261
261
262 if captcha.active:
262 if captcha.active:
263 response = submit(
263 response = submit(
264 self.request.params.get('recaptcha_challenge_field'),
264 self.request.params.get('recaptcha_challenge_field'),
265 self.request.params.get('recaptcha_response_field'),
265 self.request.params.get('recaptcha_response_field'),
266 private_key=captcha.private_key,
266 private_key=captcha.private_key,
267 remoteip=get_ip_addr(self.request.environ))
267 remoteip=get_ip_addr(self.request.environ))
268 if not response.is_valid:
268 if not response.is_valid:
269 _value = form_result
269 _value = form_result
270 _msg = _('Bad captcha')
270 _msg = _('Bad captcha')
271 error_dict = {'recaptcha_field': _msg}
271 error_dict = {'recaptcha_field': _msg}
272 raise formencode.Invalid(_msg, _value, None,
272 raise formencode.Invalid(_msg, _value, None,
273 error_dict=error_dict)
273 error_dict=error_dict)
274
274
275 new_user = UserModel().create_registration(form_result)
275 new_user = UserModel().create_registration(form_result)
276 event = UserRegistered(user=new_user, session=self.session)
276 event = UserRegistered(user=new_user, session=self.session)
277 self.request.registry.notify(event)
277 self.request.registry.notify(event)
278 self.session.flash(
278 self.session.flash(
279 _('You have successfully registered with RhodeCode'),
279 _('You have successfully registered with RhodeCode'),
280 queue='success')
280 queue='success')
281 Session().commit()
281 Session().commit()
282
282
283 redirect_ro = self.request.route_path('login')
283 redirect_ro = self.request.route_path('login')
284 raise HTTPFound(redirect_ro)
284 raise HTTPFound(redirect_ro)
285
285
286 except formencode.Invalid as errors:
286 except formencode.Invalid as errors:
287 errors.value.pop('password', None)
287 errors.value.pop('password', None)
288 errors.value.pop('password_confirmation', None)
288 errors.value.pop('password_confirmation', None)
289 return self.register(
289 return self.register(
290 defaults=errors.value, errors=errors.error_dict)
290 defaults=errors.value, errors=errors.error_dict)
291
291
292 except UserCreationError as e:
292 except UserCreationError as e:
293 # container auth or other auth functions that create users on
293 # container auth or other auth functions that create users on
294 # the fly can throw this exception signaling that there's issue
294 # the fly can throw this exception signaling that there's issue
295 # with user creation, explanation should be provided in
295 # with user creation, explanation should be provided in
296 # Exception itself
296 # Exception itself
297 self.session.flash(e, queue='error')
297 self.session.flash(e, queue='error')
298 return self.register()
298 return self.register()
299
299
300 @view_config(
300 @view_config(
301 route_name='reset_password', request_method=('GET', 'POST'),
301 route_name='reset_password', request_method=('GET', 'POST'),
302 renderer='rhodecode:templates/password_reset.mako')
302 renderer='rhodecode:templates/password_reset.mako')
303 def password_reset(self):
303 def password_reset(self):
304 captcha = self._get_captcha_data()
304 captcha = self._get_captcha_data()
305
305
306 render_ctx = {
306 render_ctx = {
307 'captcha_active': captcha.active,
307 'captcha_active': captcha.active,
308 'captcha_public_key': captcha.public_key,
308 'captcha_public_key': captcha.public_key,
309 'defaults': {},
309 'defaults': {},
310 'errors': {},
310 'errors': {},
311 }
311 }
312
312
313 # always send implicit message to prevent from discovery of
313 # always send implicit message to prevent from discovery of
314 # matching emails
314 # matching emails
315 msg = _('If such email exists, a password reset link was sent to it.')
315 msg = _('If such email exists, a password reset link was sent to it.')
316
316
317 if self.request.POST:
317 if self.request.POST:
318 if h.HasPermissionAny('hg.password_reset.disabled')():
318 if h.HasPermissionAny('hg.password_reset.disabled')():
319 _email = self.request.POST.get('email', '')
319 _email = self.request.POST.get('email', '')
320 log.error('Failed attempt to reset password for `%s`.', _email)
320 log.error('Failed attempt to reset password for `%s`.', _email)
321 self.session.flash(_('Password reset has been disabled.'),
321 self.session.flash(_('Password reset has been disabled.'),
322 queue='error')
322 queue='error')
323 return HTTPFound(self.request.route_path('reset_password'))
323 return HTTPFound(self.request.route_path('reset_password'))
324
324
325 password_reset_form = PasswordResetForm()()
325 password_reset_form = PasswordResetForm()()
326 try:
326 try:
327 form_result = password_reset_form.to_python(
327 form_result = password_reset_form.to_python(
328 self.request.params)
328 self.request.params)
329 user_email = form_result['email']
329 user_email = form_result['email']
330
330
331 if captcha.active:
331 if captcha.active:
332 response = submit(
332 response = submit(
333 self.request.params.get('recaptcha_challenge_field'),
333 self.request.params.get('recaptcha_challenge_field'),
334 self.request.params.get('recaptcha_response_field'),
334 self.request.params.get('recaptcha_response_field'),
335 private_key=captcha.private_key,
335 private_key=captcha.private_key,
336 remoteip=get_ip_addr(self.request.environ))
336 remoteip=get_ip_addr(self.request.environ))
337 if not response.is_valid:
337 if not response.is_valid:
338 _value = form_result
338 _value = form_result
339 _msg = _('Bad captcha')
339 _msg = _('Bad captcha')
340 error_dict = {'recaptcha_field': _msg}
340 error_dict = {'recaptcha_field': _msg}
341 raise formencode.Invalid(
341 raise formencode.Invalid(
342 _msg, _value, None, error_dict=error_dict)
342 _msg, _value, None, error_dict=error_dict)
343
343
344 # Generate reset URL and send mail.
344 # Generate reset URL and send mail.
345 user = User.get_by_email(user_email)
345 user = User.get_by_email(user_email)
346
346
347 # generate password reset token that expires in 10minutes
347 # generate password reset token that expires in 10minutes
348 desc = 'Generated token for password reset from {}'.format(
348 desc = 'Generated token for password reset from {}'.format(
349 datetime.datetime.now().isoformat())
349 datetime.datetime.now().isoformat())
350 reset_token = AuthTokenModel().create(
350 reset_token = AuthTokenModel().create(
351 user, lifetime=10,
351 user, lifetime=10,
352 description=desc,
352 description=desc,
353 role=UserApiKeys.ROLE_PASSWORD_RESET)
353 role=UserApiKeys.ROLE_PASSWORD_RESET)
354 Session().commit()
354 Session().commit()
355
355
356 log.debug('Successfully created password recovery token')
356 log.debug('Successfully created password recovery token')
357 password_reset_url = self.request.route_url(
357 password_reset_url = self.request.route_url(
358 'reset_password_confirmation',
358 'reset_password_confirmation',
359 _query={'key': reset_token.api_key})
359 _query={'key': reset_token.api_key})
360 UserModel().reset_password_link(
360 UserModel().reset_password_link(
361 form_result, password_reset_url)
361 form_result, password_reset_url)
362 # Display success message and redirect.
362 # Display success message and redirect.
363 self.session.flash(msg, queue='success')
363 self.session.flash(msg, queue='success')
364
364
365 action_data = {'email': user_email,
365 action_data = {'email': user_email,
366 'user_agent': self.request.user_agent}
366 'user_agent': self.request.user_agent}
367 audit_logger.store(action='user.password.reset_request',
367 audit_logger.store(action='user.password.reset_request',
368 action_data=action_data,
368 action_data=action_data,
369 user=self._rhodecode_user, commit=True)
369 user=self._rhodecode_user, commit=True)
370 return HTTPFound(self.request.route_path('reset_password'))
370 return HTTPFound(self.request.route_path('reset_password'))
371
371
372 except formencode.Invalid as errors:
372 except formencode.Invalid as errors:
373 render_ctx.update({
373 render_ctx.update({
374 'defaults': errors.value,
374 'defaults': errors.value,
375 'errors': errors.error_dict,
375 'errors': errors.error_dict,
376 })
376 })
377 if not self.request.params.get('email'):
377 if not self.request.params.get('email'):
378 # case of empty email, we want to report that
378 # case of empty email, we want to report that
379 return render_ctx
379 return render_ctx
380
380
381 if 'recaptcha_field' in errors.error_dict:
381 if 'recaptcha_field' in errors.error_dict:
382 # case of failed captcha
382 # case of failed captcha
383 return render_ctx
383 return render_ctx
384
384
385 log.debug('faking response on invalid password reset')
385 log.debug('faking response on invalid password reset')
386 # make this take 2s, to prevent brute forcing.
386 # make this take 2s, to prevent brute forcing.
387 time.sleep(2)
387 time.sleep(2)
388 self.session.flash(msg, queue='success')
388 self.session.flash(msg, queue='success')
389 return HTTPFound(self.request.route_path('reset_password'))
389 return HTTPFound(self.request.route_path('reset_password'))
390
390
391 return render_ctx
391 return render_ctx
392
392
393 @view_config(route_name='reset_password_confirmation',
393 @view_config(route_name='reset_password_confirmation',
394 request_method='GET')
394 request_method='GET')
395 def password_reset_confirmation(self):
395 def password_reset_confirmation(self):
396
396
397 if self.request.GET and self.request.GET.get('key'):
397 if self.request.GET and self.request.GET.get('key'):
398 # make this take 2s, to prevent brute forcing.
398 # make this take 2s, to prevent brute forcing.
399 time.sleep(2)
399 time.sleep(2)
400
400
401 token = AuthTokenModel().get_auth_token(
401 token = AuthTokenModel().get_auth_token(
402 self.request.GET.get('key'))
402 self.request.GET.get('key'))
403
403
404 # verify token is the correct role
404 # verify token is the correct role
405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
405 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
406 log.debug('Got token with role:%s expected is %s',
406 log.debug('Got token with role:%s expected is %s',
407 getattr(token, 'role', 'EMPTY_TOKEN'),
407 getattr(token, 'role', 'EMPTY_TOKEN'),
408 UserApiKeys.ROLE_PASSWORD_RESET)
408 UserApiKeys.ROLE_PASSWORD_RESET)
409 self.session.flash(
409 self.session.flash(
410 _('Given reset token is invalid'), queue='error')
410 _('Given reset token is invalid'), queue='error')
411 return HTTPFound(self.request.route_path('reset_password'))
411 return HTTPFound(self.request.route_path('reset_password'))
412
412
413 try:
413 try:
414 owner = token.user
414 owner = token.user
415 data = {'email': owner.email, 'token': token.api_key}
415 data = {'email': owner.email, 'token': token.api_key}
416 UserModel().reset_password(data)
416 UserModel().reset_password(data)
417 self.session.flash(
417 self.session.flash(
418 _('Your password reset was successful, '
418 _('Your password reset was successful, '
419 'a new password has been sent to your email'),
419 'a new password has been sent to your email'),
420 queue='success')
420 queue='success')
421 except Exception as e:
421 except Exception as e:
422 log.error(e)
422 log.error(e)
423 return HTTPFound(self.request.route_path('reset_password'))
423 return HTTPFound(self.request.route_path('reset_password'))
424
424
425 return HTTPFound(self.request.route_path('login'))
425 return HTTPFound(self.request.route_path('login'))
@@ -1,137 +1,138 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 import mock
22 import mock
23
23
24 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib import helpers as h
25 from rhodecode.lib import helpers as h
26 from rhodecode.lib.auth import check_password
26 from rhodecode.lib.auth import check_password
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
28 from rhodecode.model.user import UserModel
29 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests import assert_session_flash
30 from rhodecode.tests.fixture import Fixture, TestController, error_function
30 from rhodecode.tests.fixture import Fixture, TestController, error_function
31
31
32 fixture = Fixture()
32 fixture = Fixture()
33
33
34
34
35 def route_path(name, **kwargs):
35 def route_path(name, **kwargs):
36 return {
36 return {
37 'home': '/',
37 'home': '/',
38 'my_account_password':
38 'my_account_password':
39 ADMIN_PREFIX + '/my_account/password',
39 ADMIN_PREFIX + '/my_account/password',
40 }[name].format(**kwargs)
40 }[name].format(**kwargs)
41
41
42
42
43 test_user_1 = 'testme'
43 test_user_1 = 'testme'
44 test_user_1_password = '0jd83nHNS/d23n'
44 test_user_1_password = '0jd83nHNS/d23n'
45
45
46
46
47 class TestMyAccountPassword(TestController):
47 class TestMyAccountPassword(TestController):
48 def test_valid_change_password(self, user_util):
48 def test_valid_change_password(self, user_util):
49 new_password = 'my_new_valid_password'
49 new_password = 'my_new_valid_password'
50 user = user_util.create_user(password=test_user_1_password)
50 user = user_util.create_user(password=test_user_1_password)
51 self.log_user(user.username, test_user_1_password)
51 self.log_user(user.username, test_user_1_password)
52
52
53 form_data = [
53 form_data = [
54 ('current_password', test_user_1_password),
54 ('current_password', test_user_1_password),
55 ('__start__', 'new_password:mapping'),
55 ('__start__', 'new_password:mapping'),
56 ('new_password', new_password),
56 ('new_password', new_password),
57 ('new_password-confirm', new_password),
57 ('new_password-confirm', new_password),
58 ('__end__', 'new_password:mapping'),
58 ('__end__', 'new_password:mapping'),
59 ('csrf_token', self.csrf_token),
59 ('csrf_token', self.csrf_token),
60 ]
60 ]
61 response = self.app.post(route_path('my_account_password'), form_data).follow()
61 response = self.app.post(route_path('my_account_password'), form_data).follow()
62 assert 'Successfully updated password' in response
62 assert 'Successfully updated password' in response
63
63
64 # check_password depends on user being in session
64 # check_password depends on user being in session
65 Session().add(user)
65 Session().add(user)
66 try:
66 try:
67 assert check_password(new_password, user.password)
67 assert check_password(new_password, user.password)
68 finally:
68 finally:
69 Session().expunge(user)
69 Session().expunge(user)
70
70
71 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
71 @pytest.mark.parametrize('current_pw, new_pw, confirm_pw', [
72 ('', 'abcdef123', 'abcdef123'),
72 ('', 'abcdef123', 'abcdef123'),
73 ('wrong_pw', 'abcdef123', 'abcdef123'),
73 ('wrong_pw', 'abcdef123', 'abcdef123'),
74 (test_user_1_password, test_user_1_password, test_user_1_password),
74 (test_user_1_password, test_user_1_password, test_user_1_password),
75 (test_user_1_password, '', ''),
75 (test_user_1_password, '', ''),
76 (test_user_1_password, 'abcdef123', ''),
76 (test_user_1_password, 'abcdef123', ''),
77 (test_user_1_password, '', 'abcdef123'),
77 (test_user_1_password, '', 'abcdef123'),
78 (test_user_1_password, 'not_the', 'same_pw'),
78 (test_user_1_password, 'not_the', 'same_pw'),
79 (test_user_1_password, 'short', 'short'),
79 (test_user_1_password, 'short', 'short'),
80 ])
80 ])
81 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
81 def test_invalid_change_password(self, current_pw, new_pw, confirm_pw,
82 user_util):
82 user_util):
83 user = user_util.create_user(password=test_user_1_password)
83 user = user_util.create_user(password=test_user_1_password)
84 self.log_user(user.username, test_user_1_password)
84 self.log_user(user.username, test_user_1_password)
85
85
86 form_data = [
86 form_data = [
87 ('current_password', current_pw),
87 ('current_password', current_pw),
88 ('__start__', 'new_password:mapping'),
88 ('__start__', 'new_password:mapping'),
89 ('new_password', new_pw),
89 ('new_password', new_pw),
90 ('new_password-confirm', confirm_pw),
90 ('new_password-confirm', confirm_pw),
91 ('__end__', 'new_password:mapping'),
91 ('__end__', 'new_password:mapping'),
92 ('csrf_token', self.csrf_token),
92 ('csrf_token', self.csrf_token),
93 ]
93 ]
94 response = self.app.post(route_path('my_account_password'), form_data)
94 response = self.app.post(route_path('my_account_password'), form_data)
95
95
96 assert_response = response.assert_response()
96 assert_response = response.assert_response()
97 assert assert_response.get_elements('.error-block')
97 assert assert_response.get_elements('.error-block')
98
98
99 @mock.patch.object(UserModel, 'update_user', error_function)
99 @mock.patch.object(UserModel, 'update_user', error_function)
100 def test_invalid_change_password_exception(self, user_util):
100 def test_invalid_change_password_exception(self, user_util):
101 user = user_util.create_user(password=test_user_1_password)
101 user = user_util.create_user(password=test_user_1_password)
102 self.log_user(user.username, test_user_1_password)
102 self.log_user(user.username, test_user_1_password)
103
103
104 form_data = [
104 form_data = [
105 ('current_password', test_user_1_password),
105 ('current_password', test_user_1_password),
106 ('__start__', 'new_password:mapping'),
106 ('__start__', 'new_password:mapping'),
107 ('new_password', '123456'),
107 ('new_password', '123456'),
108 ('new_password-confirm', '123456'),
108 ('new_password-confirm', '123456'),
109 ('__end__', 'new_password:mapping'),
109 ('__end__', 'new_password:mapping'),
110 ('csrf_token', self.csrf_token),
110 ('csrf_token', self.csrf_token),
111 ]
111 ]
112 response = self.app.post(route_path('my_account_password'), form_data)
112 response = self.app.post(route_path('my_account_password'), form_data)
113 assert_session_flash(
113 assert_session_flash(
114 response, 'Error occurred during update of user password')
114 response, 'Error occurred during update of user password')
115
115
116 def test_password_is_updated_in_session_on_password_change(self, user_util):
116 def test_password_is_updated_in_session_on_password_change(self, user_util):
117 old_password = 'abcdef123'
117 old_password = 'abcdef123'
118 new_password = 'abcdef124'
118 new_password = 'abcdef124'
119
119
120 user = user_util.create_user(password=old_password)
120 user = user_util.create_user(password=old_password)
121 session = self.log_user(user.username, old_password)
121 session = self.log_user(user.username, old_password)
122 old_password_hash = session['password']
122 old_password_hash = session['password']
123
123
124 form_data = [
124 form_data = [
125 ('current_password', old_password),
125 ('current_password', old_password),
126 ('__start__', 'new_password:mapping'),
126 ('__start__', 'new_password:mapping'),
127 ('new_password', new_password),
127 ('new_password', new_password),
128 ('new_password-confirm', new_password),
128 ('new_password-confirm', new_password),
129 ('__end__', 'new_password:mapping'),
129 ('__end__', 'new_password:mapping'),
130 ('csrf_token', self.csrf_token),
130 ('csrf_token', self.csrf_token),
131 ]
131 ]
132 self.app.post(route_path('my_account_password'), form_data)
132 self.app.post(route_path('my_account_password'), form_data)
133
133
134 response = self.app.get(route_path('home'))
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 assert old_password_hash != new_password_hash No newline at end of file
138 assert old_password_hash != new_password_hash
@@ -1,129 +1,133 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 def includeme(config):
22 def includeme(config):
23
23
24 # Summary
24 # Summary
25 config.add_route(
25 config.add_route(
26 name='repo_summary',
27 pattern='/{repo_name:.*?[^/]}', repo_route=True)
28
29 config.add_route(
26 name='repo_summary_explicit',
30 name='repo_summary_explicit',
27 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
28
32
29 # Tags
33 # Tags
30 config.add_route(
34 config.add_route(
31 name='tags_home',
35 name='tags_home',
32 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
36 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
33
37
34 # Branches
38 # Branches
35 config.add_route(
39 config.add_route(
36 name='branches_home',
40 name='branches_home',
37 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
41 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
38
42
39 # Bookmarks
43 # Bookmarks
40 config.add_route(
44 config.add_route(
41 name='bookmarks_home',
45 name='bookmarks_home',
42 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
46 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
43
47
44 # Pull Requests
48 # Pull Requests
45 config.add_route(
49 config.add_route(
46 name='pullrequest_show',
50 name='pullrequest_show',
47 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
51 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
48 repo_route=True)
52 repo_route=True)
49
53
50 config.add_route(
54 config.add_route(
51 name='pullrequest_show_all',
55 name='pullrequest_show_all',
52 pattern='/{repo_name:.*?[^/]}/pull-request',
56 pattern='/{repo_name:.*?[^/]}/pull-request',
53 repo_route=True, repo_accepted_types=['hg', 'git'])
57 repo_route=True, repo_accepted_types=['hg', 'git'])
54
58
55 config.add_route(
59 config.add_route(
56 name='pullrequest_show_all_data',
60 name='pullrequest_show_all_data',
57 pattern='/{repo_name:.*?[^/]}/pull-request-data',
61 pattern='/{repo_name:.*?[^/]}/pull-request-data',
58 repo_route=True, repo_accepted_types=['hg', 'git'])
62 repo_route=True, repo_accepted_types=['hg', 'git'])
59
63
60 # Settings
64 # Settings
61 config.add_route(
65 config.add_route(
62 name='edit_repo',
66 name='edit_repo',
63 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
67 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
64
68
65 # Settings advanced
69 # Settings advanced
66 config.add_route(
70 config.add_route(
67 name='edit_repo_advanced',
71 name='edit_repo_advanced',
68 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
72 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
69 config.add_route(
73 config.add_route(
70 name='edit_repo_advanced_delete',
74 name='edit_repo_advanced_delete',
71 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
75 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
72 config.add_route(
76 config.add_route(
73 name='edit_repo_advanced_locking',
77 name='edit_repo_advanced_locking',
74 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
78 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
75 config.add_route(
79 config.add_route(
76 name='edit_repo_advanced_journal',
80 name='edit_repo_advanced_journal',
77 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
81 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
78 config.add_route(
82 config.add_route(
79 name='edit_repo_advanced_fork',
83 name='edit_repo_advanced_fork',
80 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
84 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
81
85
82 # Caches
86 # Caches
83 config.add_route(
87 config.add_route(
84 name='edit_repo_caches',
88 name='edit_repo_caches',
85 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
89 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
86
90
87 # Permissions
91 # Permissions
88 config.add_route(
92 config.add_route(
89 name='edit_repo_perms',
93 name='edit_repo_perms',
90 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
94 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
91
95
92 # Repo Review Rules
96 # Repo Review Rules
93 config.add_route(
97 config.add_route(
94 name='repo_reviewers',
98 name='repo_reviewers',
95 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
99 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
96
100
97 config.add_route(
101 config.add_route(
98 name='repo_default_reviewers_data',
102 name='repo_default_reviewers_data',
99 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
103 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
100
104
101 # Maintenance
105 # Maintenance
102 config.add_route(
106 config.add_route(
103 name='repo_maintenance',
107 name='repo_maintenance',
104 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
108 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
105
109
106 config.add_route(
110 config.add_route(
107 name='repo_maintenance_execute',
111 name='repo_maintenance_execute',
108 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
112 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
109
113
110 # Strip
114 # Strip
111 config.add_route(
115 config.add_route(
112 name='strip',
116 name='strip',
113 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
117 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
114
118
115 config.add_route(
119 config.add_route(
116 name='strip_check',
120 name='strip_check',
117 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
121 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
118
122
119 config.add_route(
123 config.add_route(
120 name='strip_execute',
124 name='strip_execute',
121 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
125 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
122
126
123 # NOTE(marcink): needs to be at the end for catch-all
127 # NOTE(marcink): needs to be at the end for catch-all
124 # config.add_route(
128 # config.add_route(
125 # name='repo_summary',
129 # name='repo_summary',
126 # pattern='/{repo_name:.*?[^/]}', repo_route=True)
130 # pattern='/{repo_name:.*?[^/]}', repo_route=True)
127
131
128 # Scan module for configuration decorators.
132 # Scan module for configuration decorators.
129 config.scan()
133 config.scan()
@@ -1,515 +1,516 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25 from collections import OrderedDict
25 from collections import OrderedDict
26
26
27 from paste.registry import RegistryManager
27 from paste.registry import RegistryManager
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 from pylons.wsgiapp import PylonsApp
29 from pylons.wsgiapp import PylonsApp
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.config import Configurator
31 from pyramid.config import Configurator
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 from pyramid.events import ApplicationCreated
36 from pyramid.events import ApplicationCreated
37 from pyramid.renderers import render_to_response
37 from pyramid.renderers import render_to_response
38 from routes.middleware import RoutesMiddleware
38 from routes.middleware import RoutesMiddleware
39 import routes.util
39 import routes.util
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.model import meta
42 from rhodecode.model import meta
43 from rhodecode.config import patches
43 from rhodecode.config import patches
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 from rhodecode.config.environment import (
45 from rhodecode.config.environment import (
46 load_environment, load_pyramid_environment)
46 load_environment, load_pyramid_environment)
47 from rhodecode.lib.middleware import csrf
47 from rhodecode.lib.middleware import csrf
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.error_handling import (
49 from rhodecode.lib.middleware.error_handling import (
50 PylonsErrorHandlingMiddleware)
50 PylonsErrorHandlingMiddleware)
51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 from rhodecode.lib.middleware.vcs import VCSMiddleware
52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 from rhodecode.subscribers import (
55 from rhodecode.subscribers import (
56 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 scan_repositories_if_enabled, write_js_routes_if_enabled,
57 write_metadata_if_needed)
57 write_metadata_if_needed)
58
58
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 # this is used to avoid avoid the route lookup overhead in routesmiddleware
63 # this is used to avoid avoid the route lookup overhead in routesmiddleware
64 # for certain routes which won't go to pylons to - eg. static files, debugger
64 # for certain routes which won't go to pylons to - eg. static files, debugger
65 # it is only needed for the pylons migration and can be removed once complete
65 # it is only needed for the pylons migration and can be removed once complete
66 class SkippableRoutesMiddleware(RoutesMiddleware):
66 class SkippableRoutesMiddleware(RoutesMiddleware):
67 """ Routes middleware that allows you to skip prefixes """
67 """ Routes middleware that allows you to skip prefixes """
68
68
69 def __init__(self, *args, **kw):
69 def __init__(self, *args, **kw):
70 self.skip_prefixes = kw.pop('skip_prefixes', [])
70 self.skip_prefixes = kw.pop('skip_prefixes', [])
71 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
71 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
72
72
73 def __call__(self, environ, start_response):
73 def __call__(self, environ, start_response):
74 for prefix in self.skip_prefixes:
74 for prefix in self.skip_prefixes:
75 if environ['PATH_INFO'].startswith(prefix):
75 if environ['PATH_INFO'].startswith(prefix):
76 # added to avoid the case when a missing /_static route falls
76 # added to avoid the case when a missing /_static route falls
77 # through to pylons and causes an exception as pylons is
77 # through to pylons and causes an exception as pylons is
78 # expecting wsgiorg.routingargs to be set in the environ
78 # expecting wsgiorg.routingargs to be set in the environ
79 # by RoutesMiddleware.
79 # by RoutesMiddleware.
80 if 'wsgiorg.routing_args' not in environ:
80 if 'wsgiorg.routing_args' not in environ:
81 environ['wsgiorg.routing_args'] = (None, {})
81 environ['wsgiorg.routing_args'] = (None, {})
82 return self.app(environ, start_response)
82 return self.app(environ, start_response)
83
83
84 return super(SkippableRoutesMiddleware, self).__call__(
84 return super(SkippableRoutesMiddleware, self).__call__(
85 environ, start_response)
85 environ, start_response)
86
86
87
87
88 def make_app(global_conf, static_files=True, **app_conf):
88 def make_app(global_conf, static_files=True, **app_conf):
89 """Create a Pylons WSGI application and return it
89 """Create a Pylons WSGI application and return it
90
90
91 ``global_conf``
91 ``global_conf``
92 The inherited configuration for this application. Normally from
92 The inherited configuration for this application. Normally from
93 the [DEFAULT] section of the Paste ini file.
93 the [DEFAULT] section of the Paste ini file.
94
94
95 ``app_conf``
95 ``app_conf``
96 The application's local configuration. Normally specified in
96 The application's local configuration. Normally specified in
97 the [app:<name>] section of the Paste ini file (where <name>
97 the [app:<name>] section of the Paste ini file (where <name>
98 defaults to main).
98 defaults to main).
99
99
100 """
100 """
101 # Apply compatibility patches
101 # Apply compatibility patches
102 patches.kombu_1_5_1_python_2_7_11()
102 patches.kombu_1_5_1_python_2_7_11()
103 patches.inspect_getargspec()
103 patches.inspect_getargspec()
104
104
105 # Configure the Pylons environment
105 # Configure the Pylons environment
106 config = load_environment(global_conf, app_conf)
106 config = load_environment(global_conf, app_conf)
107
107
108 # The Pylons WSGI app
108 # The Pylons WSGI app
109 app = PylonsApp(config=config)
109 app = PylonsApp(config=config)
110 if rhodecode.is_test:
110 if rhodecode.is_test:
111 app = csrf.CSRFDetector(app)
111 app = csrf.CSRFDetector(app)
112
112
113 expected_origin = config.get('expected_origin')
113 expected_origin = config.get('expected_origin')
114 if expected_origin:
114 if expected_origin:
115 # The API can be accessed from other Origins.
115 # The API can be accessed from other Origins.
116 app = csrf.OriginChecker(app, expected_origin,
116 app = csrf.OriginChecker(app, expected_origin,
117 skip_urls=[routes.util.url_for('api')])
117 skip_urls=[routes.util.url_for('api')])
118
118
119 # Establish the Registry for this application
119 # Establish the Registry for this application
120 app = RegistryManager(app)
120 app = RegistryManager(app)
121
121
122 app.config = config
122 app.config = config
123
123
124 return app
124 return app
125
125
126
126
127 def make_pyramid_app(global_config, **settings):
127 def make_pyramid_app(global_config, **settings):
128 """
128 """
129 Constructs the WSGI application based on Pyramid and wraps the Pylons based
129 Constructs the WSGI application based on Pyramid and wraps the Pylons based
130 application.
130 application.
131
131
132 Specials:
132 Specials:
133
133
134 * We migrate from Pylons to Pyramid. While doing this, we keep both
134 * We migrate from Pylons to Pyramid. While doing this, we keep both
135 frameworks functional. This involves moving some WSGI middlewares around
135 frameworks functional. This involves moving some WSGI middlewares around
136 and providing access to some data internals, so that the old code is
136 and providing access to some data internals, so that the old code is
137 still functional.
137 still functional.
138
138
139 * The application can also be integrated like a plugin via the call to
139 * The application can also be integrated like a plugin via the call to
140 `includeme`. This is accompanied with the other utility functions which
140 `includeme`. This is accompanied with the other utility functions which
141 are called. Changing this should be done with great care to not break
141 are called. Changing this should be done with great care to not break
142 cases when these fragments are assembled from another place.
142 cases when these fragments are assembled from another place.
143
143
144 """
144 """
145 # The edition string should be available in pylons too, so we add it here
145 # The edition string should be available in pylons too, so we add it here
146 # before copying the settings.
146 # before copying the settings.
147 settings.setdefault('rhodecode.edition', 'Community Edition')
147 settings.setdefault('rhodecode.edition', 'Community Edition')
148
148
149 # As long as our Pylons application does expect "unprepared" settings, make
149 # As long as our Pylons application does expect "unprepared" settings, make
150 # sure that we keep an unmodified copy. This avoids unintentional change of
150 # sure that we keep an unmodified copy. This avoids unintentional change of
151 # behavior in the old application.
151 # behavior in the old application.
152 settings_pylons = settings.copy()
152 settings_pylons = settings.copy()
153
153
154 sanitize_settings_and_apply_defaults(settings)
154 sanitize_settings_and_apply_defaults(settings)
155 config = Configurator(settings=settings)
155 config = Configurator(settings=settings)
156 add_pylons_compat_data(config.registry, global_config, settings_pylons)
156 add_pylons_compat_data(config.registry, global_config, settings_pylons)
157
157
158 load_pyramid_environment(global_config, settings)
158 load_pyramid_environment(global_config, settings)
159
159
160 includeme_first(config)
160 includeme_first(config)
161 includeme(config)
161 includeme(config)
162 pyramid_app = config.make_wsgi_app()
162 pyramid_app = config.make_wsgi_app()
163 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
163 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
164 pyramid_app.config = config
164 pyramid_app.config = config
165
165
166 # creating the app uses a connection - return it after we are done
166 # creating the app uses a connection - return it after we are done
167 meta.Session.remove()
167 meta.Session.remove()
168
168
169 return pyramid_app
169 return pyramid_app
170
170
171
171
172 def make_not_found_view(config):
172 def make_not_found_view(config):
173 """
173 """
174 This creates the view which should be registered as not-found-view to
174 This creates the view which should be registered as not-found-view to
175 pyramid. Basically it contains of the old pylons app, converted to a view.
175 pyramid. Basically it contains of the old pylons app, converted to a view.
176 Additionally it is wrapped by some other middlewares.
176 Additionally it is wrapped by some other middlewares.
177 """
177 """
178 settings = config.registry.settings
178 settings = config.registry.settings
179 vcs_server_enabled = settings['vcs.server.enable']
179 vcs_server_enabled = settings['vcs.server.enable']
180
180
181 # Make pylons app from unprepared settings.
181 # Make pylons app from unprepared settings.
182 pylons_app = make_app(
182 pylons_app = make_app(
183 config.registry._pylons_compat_global_config,
183 config.registry._pylons_compat_global_config,
184 **config.registry._pylons_compat_settings)
184 **config.registry._pylons_compat_settings)
185 config.registry._pylons_compat_config = pylons_app.config
185 config.registry._pylons_compat_config = pylons_app.config
186
186
187 # Appenlight monitoring.
187 # Appenlight monitoring.
188 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
188 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
189 pylons_app, settings)
189 pylons_app, settings)
190
190
191 # The pylons app is executed inside of the pyramid 404 exception handler.
191 # The pylons app is executed inside of the pyramid 404 exception handler.
192 # Exceptions which are raised inside of it are not handled by pyramid
192 # Exceptions which are raised inside of it are not handled by pyramid
193 # again. Therefore we add a middleware that invokes the error handler in
193 # again. Therefore we add a middleware that invokes the error handler in
194 # case of an exception or error response. This way we return proper error
194 # case of an exception or error response. This way we return proper error
195 # HTML pages in case of an error.
195 # HTML pages in case of an error.
196 reraise = (settings.get('debugtoolbar.enabled', False) or
196 reraise = (settings.get('debugtoolbar.enabled', False) or
197 rhodecode.disable_error_handler)
197 rhodecode.disable_error_handler)
198 pylons_app = PylonsErrorHandlingMiddleware(
198 pylons_app = PylonsErrorHandlingMiddleware(
199 pylons_app, error_handler, reraise)
199 pylons_app, error_handler, reraise)
200
200
201 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
201 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
202 # view to handle the request. Therefore it is wrapped around the pylons
202 # view to handle the request. Therefore it is wrapped around the pylons
203 # app. It has to be outside of the error handling otherwise error responses
203 # app. It has to be outside of the error handling otherwise error responses
204 # from the vcsserver are converted to HTML error pages. This confuses the
204 # from the vcsserver are converted to HTML error pages. This confuses the
205 # command line tools and the user won't get a meaningful error message.
205 # command line tools and the user won't get a meaningful error message.
206 if vcs_server_enabled:
206 if vcs_server_enabled:
207 pylons_app = VCSMiddleware(
207 pylons_app = VCSMiddleware(
208 pylons_app, settings, appenlight_client, registry=config.registry)
208 pylons_app, settings, appenlight_client, registry=config.registry)
209
209
210 # Convert WSGI app to pyramid view and return it.
210 # Convert WSGI app to pyramid view and return it.
211 return wsgiapp(pylons_app)
211 return wsgiapp(pylons_app)
212
212
213
213
214 def add_pylons_compat_data(registry, global_config, settings):
214 def add_pylons_compat_data(registry, global_config, settings):
215 """
215 """
216 Attach data to the registry to support the Pylons integration.
216 Attach data to the registry to support the Pylons integration.
217 """
217 """
218 registry._pylons_compat_global_config = global_config
218 registry._pylons_compat_global_config = global_config
219 registry._pylons_compat_settings = settings
219 registry._pylons_compat_settings = settings
220
220
221
221
222 def error_handler(exception, request):
222 def error_handler(exception, request):
223 import rhodecode
223 import rhodecode
224 from rhodecode.lib.utils2 import AttributeDict
224 from rhodecode.lib.utils2 import AttributeDict
225 from rhodecode.lib import helpers
225 from rhodecode.lib import helpers
226
226
227 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
227 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
228
228
229 base_response = HTTPInternalServerError()
229 base_response = HTTPInternalServerError()
230 # prefer original exception for the response since it may have headers set
230 # prefer original exception for the response since it may have headers set
231 if isinstance(exception, HTTPException):
231 if isinstance(exception, HTTPException):
232 base_response = exception
232 base_response = exception
233
233
234 def is_http_error(response):
234 def is_http_error(response):
235 # error which should have traceback
235 # error which should have traceback
236 return response.status_code > 499
236 return response.status_code > 499
237
237
238 if is_http_error(base_response):
238 if is_http_error(base_response):
239 log.exception(
239 log.exception(
240 'error occurred handling this request for path: %s', request.path)
240 'error occurred handling this request for path: %s', request.path)
241
241
242 c = AttributeDict()
242 c = AttributeDict()
243 c.error_message = base_response.status
243 c.error_message = base_response.status
244 c.error_explanation = base_response.explanation or str(base_response)
244 c.error_explanation = base_response.explanation or str(base_response)
245 c.visual = AttributeDict()
245 c.visual = AttributeDict()
246
246
247 c.visual.rhodecode_support_url = (
247 c.visual.rhodecode_support_url = (
248 request.registry.settings.get('rhodecode_support_url') or
248 request.registry.settings.get('rhodecode_support_url') or
249 request.route_url('rhodecode_support')
249 request.route_url('rhodecode_support')
250 )
250 )
251 c.redirect_time = 0
251 c.redirect_time = 0
252 c.rhodecode_name = rhodecode_title
252 c.rhodecode_name = rhodecode_title
253 if not c.rhodecode_name:
253 if not c.rhodecode_name:
254 c.rhodecode_name = 'Rhodecode'
254 c.rhodecode_name = 'Rhodecode'
255
255
256 c.causes = []
256 c.causes = []
257 if hasattr(base_response, 'causes'):
257 if hasattr(base_response, 'causes'):
258 c.causes = base_response.causes
258 c.causes = base_response.causes
259 c.messages = helpers.flash.pop_messages()
259 c.messages = helpers.flash.pop_messages()
260 response = render_to_response(
260 response = render_to_response(
261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
262 response=base_response)
262 response=base_response)
263
263
264 return response
264 return response
265
265
266
266
267 def includeme(config):
267 def includeme(config):
268 settings = config.registry.settings
268 settings = config.registry.settings
269
269
270 # plugin information
270 # plugin information
271 config.registry.rhodecode_plugins = OrderedDict()
271 config.registry.rhodecode_plugins = OrderedDict()
272
272
273 config.add_directive(
273 config.add_directive(
274 'register_rhodecode_plugin', register_rhodecode_plugin)
274 'register_rhodecode_plugin', register_rhodecode_plugin)
275
275
276 if asbool(settings.get('appenlight', 'false')):
276 if asbool(settings.get('appenlight', 'false')):
277 config.include('appenlight_client.ext.pyramid_tween')
277 config.include('appenlight_client.ext.pyramid_tween')
278
278
279 # Includes which are required. The application would fail without them.
279 # Includes which are required. The application would fail without them.
280 config.include('pyramid_mako')
280 config.include('pyramid_mako')
281 config.include('pyramid_beaker')
281 config.include('pyramid_beaker')
282
282
283 config.include('rhodecode.authentication')
283 config.include('rhodecode.authentication')
284 config.include('rhodecode.integrations')
284 config.include('rhodecode.integrations')
285
285
286 # apps
286 # apps
287 config.include('rhodecode.apps._base')
287 config.include('rhodecode.apps._base')
288 config.include('rhodecode.apps.ops')
288 config.include('rhodecode.apps.ops')
289
289
290 config.include('rhodecode.apps.admin')
290 config.include('rhodecode.apps.admin')
291 config.include('rhodecode.apps.channelstream')
291 config.include('rhodecode.apps.channelstream')
292 config.include('rhodecode.apps.login')
292 config.include('rhodecode.apps.login')
293 config.include('rhodecode.apps.home')
293 config.include('rhodecode.apps.home')
294 config.include('rhodecode.apps.repository')
294 config.include('rhodecode.apps.repository')
295 config.include('rhodecode.apps.repo_group')
295 config.include('rhodecode.apps.search')
296 config.include('rhodecode.apps.search')
296 config.include('rhodecode.apps.user_profile')
297 config.include('rhodecode.apps.user_profile')
297 config.include('rhodecode.apps.my_account')
298 config.include('rhodecode.apps.my_account')
298 config.include('rhodecode.apps.svn_support')
299 config.include('rhodecode.apps.svn_support')
299
300
300 config.include('rhodecode.tweens')
301 config.include('rhodecode.tweens')
301 config.include('rhodecode.api')
302 config.include('rhodecode.api')
302
303
303 config.add_route(
304 config.add_route(
304 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
305 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
305
306
306 config.add_translation_dirs('rhodecode:i18n/')
307 config.add_translation_dirs('rhodecode:i18n/')
307 settings['default_locale_name'] = settings.get('lang', 'en')
308 settings['default_locale_name'] = settings.get('lang', 'en')
308
309
309 # Add subscribers.
310 # Add subscribers.
310 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
312 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
312 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
313 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
313
314
314 # Set the authorization policy.
315 # Set the authorization policy.
315 authz_policy = ACLAuthorizationPolicy()
316 authz_policy = ACLAuthorizationPolicy()
316 config.set_authorization_policy(authz_policy)
317 config.set_authorization_policy(authz_policy)
317
318
318 # Set the default renderer for HTML templates to mako.
319 # Set the default renderer for HTML templates to mako.
319 config.add_mako_renderer('.html')
320 config.add_mako_renderer('.html')
320
321
321 config.add_renderer(
322 config.add_renderer(
322 name='json_ext',
323 name='json_ext',
323 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
324 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
324
325
325 # include RhodeCode plugins
326 # include RhodeCode plugins
326 includes = aslist(settings.get('rhodecode.includes', []))
327 includes = aslist(settings.get('rhodecode.includes', []))
327 for inc in includes:
328 for inc in includes:
328 config.include(inc)
329 config.include(inc)
329
330
330 # This is the glue which allows us to migrate in chunks. By registering the
331 # This is the glue which allows us to migrate in chunks. By registering the
331 # pylons based application as the "Not Found" view in Pyramid, we will
332 # pylons based application as the "Not Found" view in Pyramid, we will
332 # fallback to the old application each time the new one does not yet know
333 # fallback to the old application each time the new one does not yet know
333 # how to handle a request.
334 # how to handle a request.
334 config.add_notfound_view(make_not_found_view(config))
335 config.add_notfound_view(make_not_found_view(config))
335
336
336 if not settings.get('debugtoolbar.enabled', False):
337 if not settings.get('debugtoolbar.enabled', False):
337 # if no toolbar, then any exception gets caught and rendered
338 # if no toolbar, then any exception gets caught and rendered
338 config.add_view(error_handler, context=Exception)
339 config.add_view(error_handler, context=Exception)
339
340
340 config.add_view(error_handler, context=HTTPError)
341 config.add_view(error_handler, context=HTTPError)
341
342
342
343
343 def includeme_first(config):
344 def includeme_first(config):
344 # redirect automatic browser favicon.ico requests to correct place
345 # redirect automatic browser favicon.ico requests to correct place
345 def favicon_redirect(context, request):
346 def favicon_redirect(context, request):
346 return HTTPFound(
347 return HTTPFound(
347 request.static_path('rhodecode:public/images/favicon.ico'))
348 request.static_path('rhodecode:public/images/favicon.ico'))
348
349
349 config.add_view(favicon_redirect, route_name='favicon')
350 config.add_view(favicon_redirect, route_name='favicon')
350 config.add_route('favicon', '/favicon.ico')
351 config.add_route('favicon', '/favicon.ico')
351
352
352 def robots_redirect(context, request):
353 def robots_redirect(context, request):
353 return HTTPFound(
354 return HTTPFound(
354 request.static_path('rhodecode:public/robots.txt'))
355 request.static_path('rhodecode:public/robots.txt'))
355
356
356 config.add_view(robots_redirect, route_name='robots')
357 config.add_view(robots_redirect, route_name='robots')
357 config.add_route('robots', '/robots.txt')
358 config.add_route('robots', '/robots.txt')
358
359
359 config.add_static_view(
360 config.add_static_view(
360 '_static/deform', 'deform:static')
361 '_static/deform', 'deform:static')
361 config.add_static_view(
362 config.add_static_view(
362 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
363 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
363
364
364
365
365 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
366 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
366 """
367 """
367 Apply outer WSGI middlewares around the application.
368 Apply outer WSGI middlewares around the application.
368
369
369 Part of this has been moved up from the Pylons layer, so that the
370 Part of this has been moved up from the Pylons layer, so that the
370 data is also available if old Pylons code is hit through an already ported
371 data is also available if old Pylons code is hit through an already ported
371 view.
372 view.
372 """
373 """
373 settings = config.registry.settings
374 settings = config.registry.settings
374
375
375 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
376 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
376 pyramid_app = HttpsFixup(pyramid_app, settings)
377 pyramid_app = HttpsFixup(pyramid_app, settings)
377
378
378 # Add RoutesMiddleware to support the pylons compatibility tween during
379 # Add RoutesMiddleware to support the pylons compatibility tween during
379 # migration to pyramid.
380 # migration to pyramid.
380 pyramid_app = SkippableRoutesMiddleware(
381 pyramid_app = SkippableRoutesMiddleware(
381 pyramid_app, config.registry._pylons_compat_config['routes.map'],
382 pyramid_app, config.registry._pylons_compat_config['routes.map'],
382 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
383 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
383
384
384 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
385 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
385
386
386 if settings['gzip_responses']:
387 if settings['gzip_responses']:
387 pyramid_app = make_gzip_middleware(
388 pyramid_app = make_gzip_middleware(
388 pyramid_app, settings, compress_level=1)
389 pyramid_app, settings, compress_level=1)
389
390
390 # this should be the outer most middleware in the wsgi stack since
391 # this should be the outer most middleware in the wsgi stack since
391 # middleware like Routes make database calls
392 # middleware like Routes make database calls
392 def pyramid_app_with_cleanup(environ, start_response):
393 def pyramid_app_with_cleanup(environ, start_response):
393 try:
394 try:
394 return pyramid_app(environ, start_response)
395 return pyramid_app(environ, start_response)
395 finally:
396 finally:
396 # Dispose current database session and rollback uncommitted
397 # Dispose current database session and rollback uncommitted
397 # transactions.
398 # transactions.
398 meta.Session.remove()
399 meta.Session.remove()
399
400
400 # In a single threaded mode server, on non sqlite db we should have
401 # In a single threaded mode server, on non sqlite db we should have
401 # '0 Current Checked out connections' at the end of a request,
402 # '0 Current Checked out connections' at the end of a request,
402 # if not, then something, somewhere is leaving a connection open
403 # if not, then something, somewhere is leaving a connection open
403 pool = meta.Base.metadata.bind.engine.pool
404 pool = meta.Base.metadata.bind.engine.pool
404 log.debug('sa pool status: %s', pool.status())
405 log.debug('sa pool status: %s', pool.status())
405
406
406
407
407 return pyramid_app_with_cleanup
408 return pyramid_app_with_cleanup
408
409
409
410
410 def sanitize_settings_and_apply_defaults(settings):
411 def sanitize_settings_and_apply_defaults(settings):
411 """
412 """
412 Applies settings defaults and does all type conversion.
413 Applies settings defaults and does all type conversion.
413
414
414 We would move all settings parsing and preparation into this place, so that
415 We would move all settings parsing and preparation into this place, so that
415 we have only one place left which deals with this part. The remaining parts
416 we have only one place left which deals with this part. The remaining parts
416 of the application would start to rely fully on well prepared settings.
417 of the application would start to rely fully on well prepared settings.
417
418
418 This piece would later be split up per topic to avoid a big fat monster
419 This piece would later be split up per topic to avoid a big fat monster
419 function.
420 function.
420 """
421 """
421
422
422 # Pyramid's mako renderer has to search in the templates folder so that the
423 # Pyramid's mako renderer has to search in the templates folder so that the
423 # old templates still work. Ported and new templates are expected to use
424 # old templates still work. Ported and new templates are expected to use
424 # real asset specifications for the includes.
425 # real asset specifications for the includes.
425 mako_directories = settings.setdefault('mako.directories', [
426 mako_directories = settings.setdefault('mako.directories', [
426 # Base templates of the original Pylons application
427 # Base templates of the original Pylons application
427 'rhodecode:templates',
428 'rhodecode:templates',
428 ])
429 ])
429 log.debug(
430 log.debug(
430 "Using the following Mako template directories: %s",
431 "Using the following Mako template directories: %s",
431 mako_directories)
432 mako_directories)
432
433
433 # Default includes, possible to change as a user
434 # Default includes, possible to change as a user
434 pyramid_includes = settings.setdefault('pyramid.includes', [
435 pyramid_includes = settings.setdefault('pyramid.includes', [
435 'rhodecode.lib.middleware.request_wrapper',
436 'rhodecode.lib.middleware.request_wrapper',
436 ])
437 ])
437 log.debug(
438 log.debug(
438 "Using the following pyramid.includes: %s",
439 "Using the following pyramid.includes: %s",
439 pyramid_includes)
440 pyramid_includes)
440
441
441 # TODO: johbo: Re-think this, usually the call to config.include
442 # TODO: johbo: Re-think this, usually the call to config.include
442 # should allow to pass in a prefix.
443 # should allow to pass in a prefix.
443 settings.setdefault('rhodecode.api.url', '/_admin/api')
444 settings.setdefault('rhodecode.api.url', '/_admin/api')
444
445
445 # Sanitize generic settings.
446 # Sanitize generic settings.
446 _list_setting(settings, 'default_encoding', 'UTF-8')
447 _list_setting(settings, 'default_encoding', 'UTF-8')
447 _bool_setting(settings, 'is_test', 'false')
448 _bool_setting(settings, 'is_test', 'false')
448 _bool_setting(settings, 'gzip_responses', 'false')
449 _bool_setting(settings, 'gzip_responses', 'false')
449
450
450 # Call split out functions that sanitize settings for each topic.
451 # Call split out functions that sanitize settings for each topic.
451 _sanitize_appenlight_settings(settings)
452 _sanitize_appenlight_settings(settings)
452 _sanitize_vcs_settings(settings)
453 _sanitize_vcs_settings(settings)
453
454
454 return settings
455 return settings
455
456
456
457
457 def _sanitize_appenlight_settings(settings):
458 def _sanitize_appenlight_settings(settings):
458 _bool_setting(settings, 'appenlight', 'false')
459 _bool_setting(settings, 'appenlight', 'false')
459
460
460
461
461 def _sanitize_vcs_settings(settings):
462 def _sanitize_vcs_settings(settings):
462 """
463 """
463 Applies settings defaults and does type conversion for all VCS related
464 Applies settings defaults and does type conversion for all VCS related
464 settings.
465 settings.
465 """
466 """
466 _string_setting(settings, 'vcs.svn.compatible_version', '')
467 _string_setting(settings, 'vcs.svn.compatible_version', '')
467 _string_setting(settings, 'git_rev_filter', '--all')
468 _string_setting(settings, 'git_rev_filter', '--all')
468 _string_setting(settings, 'vcs.hooks.protocol', 'http')
469 _string_setting(settings, 'vcs.hooks.protocol', 'http')
469 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
470 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
470 _string_setting(settings, 'vcs.server', '')
471 _string_setting(settings, 'vcs.server', '')
471 _string_setting(settings, 'vcs.server.log_level', 'debug')
472 _string_setting(settings, 'vcs.server.log_level', 'debug')
472 _string_setting(settings, 'vcs.server.protocol', 'http')
473 _string_setting(settings, 'vcs.server.protocol', 'http')
473 _bool_setting(settings, 'startup.import_repos', 'false')
474 _bool_setting(settings, 'startup.import_repos', 'false')
474 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
475 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
475 _bool_setting(settings, 'vcs.server.enable', 'true')
476 _bool_setting(settings, 'vcs.server.enable', 'true')
476 _bool_setting(settings, 'vcs.start_server', 'false')
477 _bool_setting(settings, 'vcs.start_server', 'false')
477 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
478 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
478 _int_setting(settings, 'vcs.connection_timeout', 3600)
479 _int_setting(settings, 'vcs.connection_timeout', 3600)
479
480
480 # Support legacy values of vcs.scm_app_implementation. Legacy
481 # Support legacy values of vcs.scm_app_implementation. Legacy
481 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
482 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
482 # which is now mapped to 'http'.
483 # which is now mapped to 'http'.
483 scm_app_impl = settings['vcs.scm_app_implementation']
484 scm_app_impl = settings['vcs.scm_app_implementation']
484 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
485 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
485 settings['vcs.scm_app_implementation'] = 'http'
486 settings['vcs.scm_app_implementation'] = 'http'
486
487
487
488
488 def _int_setting(settings, name, default):
489 def _int_setting(settings, name, default):
489 settings[name] = int(settings.get(name, default))
490 settings[name] = int(settings.get(name, default))
490
491
491
492
492 def _bool_setting(settings, name, default):
493 def _bool_setting(settings, name, default):
493 input = settings.get(name, default)
494 input = settings.get(name, default)
494 if isinstance(input, unicode):
495 if isinstance(input, unicode):
495 input = input.encode('utf8')
496 input = input.encode('utf8')
496 settings[name] = asbool(input)
497 settings[name] = asbool(input)
497
498
498
499
499 def _list_setting(settings, name, default):
500 def _list_setting(settings, name, default):
500 raw_value = settings.get(name, default)
501 raw_value = settings.get(name, default)
501
502
502 old_separator = ','
503 old_separator = ','
503 if old_separator in raw_value:
504 if old_separator in raw_value:
504 # If we get a comma separated list, pass it to our own function.
505 # If we get a comma separated list, pass it to our own function.
505 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
506 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
506 else:
507 else:
507 # Otherwise we assume it uses pyramids space/newline separation.
508 # Otherwise we assume it uses pyramids space/newline separation.
508 settings[name] = aslist(raw_value)
509 settings[name] = aslist(raw_value)
509
510
510
511
511 def _string_setting(settings, name, default, lower=True):
512 def _string_setting(settings, name, default, lower=True):
512 value = settings.get(name, default)
513 value = settings.get(name, default)
513 if lower:
514 if lower:
514 value = value.lower()
515 value = value.lower()
515 settings[name] = value
516 settings[name] = value
@@ -1,1027 +1,1017 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Routes configuration
22 Routes configuration
23
23
24 The more specific and detailed routes should be defined first so they
24 The more specific and detailed routes should be defined first so they
25 may take precedent over the more generic routes. For more information
25 may take precedent over the more generic routes. For more information
26 refer to the routes manual at http://routes.groovie.org/docs/
26 refer to the routes manual at http://routes.groovie.org/docs/
27
27
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 and _route_name variable which uses some of stored naming here to do redirects.
29 and _route_name variable which uses some of stored naming here to do redirects.
30 """
30 """
31 import os
31 import os
32 import re
32 import re
33 from routes import Mapper
33 from routes import Mapper
34
34
35 # prefix for non repository related links needs to be prefixed with `/`
35 # prefix for non repository related links needs to be prefixed with `/`
36 ADMIN_PREFIX = '/_admin'
36 ADMIN_PREFIX = '/_admin'
37 STATIC_FILE_PREFIX = '/_static'
37 STATIC_FILE_PREFIX = '/_static'
38
38
39 # Default requirements for URL parts
39 # Default requirements for URL parts
40 URL_NAME_REQUIREMENTS = {
40 URL_NAME_REQUIREMENTS = {
41 # group name can have a slash in them, but they must not end with a slash
41 # group name can have a slash in them, but they must not end with a slash
42 'group_name': r'.*?[^/]',
42 'group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
44 # repo names can have a slash in them, but they must not end with a slash
44 # repo names can have a slash in them, but they must not end with a slash
45 'repo_name': r'.*?[^/]',
45 'repo_name': r'.*?[^/]',
46 # file path eats up everything at the end
46 # file path eats up everything at the end
47 'f_path': r'.*',
47 'f_path': r'.*',
48 # reference types
48 # reference types
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 }
51 }
52
52
53
53
54 def add_route_requirements(route_path, requirements):
54 def add_route_requirements(route_path, requirements):
55 """
55 """
56 Adds regex requirements to pyramid routes using a mapping dict
56 Adds regex requirements to pyramid routes using a mapping dict
57
57
58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
58 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
59 '/{action}/{id:\d+}'
59 '/{action}/{id:\d+}'
60
60
61 """
61 """
62 for key, regex in requirements.items():
62 for key, regex in requirements.items():
63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
63 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
64 return route_path
64 return route_path
65
65
66
66
67 class JSRoutesMapper(Mapper):
67 class JSRoutesMapper(Mapper):
68 """
68 """
69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
69 Wrapper for routes.Mapper to make pyroutes compatible url definitions
70 """
70 """
71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
71 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
72 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
73 def __init__(self, *args, **kw):
73 def __init__(self, *args, **kw):
74 super(JSRoutesMapper, self).__init__(*args, **kw)
74 super(JSRoutesMapper, self).__init__(*args, **kw)
75 self._jsroutes = []
75 self._jsroutes = []
76
76
77 def connect(self, *args, **kw):
77 def connect(self, *args, **kw):
78 """
78 """
79 Wrapper for connect to take an extra argument jsroute=True
79 Wrapper for connect to take an extra argument jsroute=True
80
80
81 :param jsroute: boolean, if True will add the route to the pyroutes list
81 :param jsroute: boolean, if True will add the route to the pyroutes list
82 """
82 """
83 if kw.pop('jsroute', False):
83 if kw.pop('jsroute', False):
84 if not self._named_route_regex.match(args[0]):
84 if not self._named_route_regex.match(args[0]):
85 raise Exception('only named routes can be added to pyroutes')
85 raise Exception('only named routes can be added to pyroutes')
86 self._jsroutes.append(args[0])
86 self._jsroutes.append(args[0])
87
87
88 super(JSRoutesMapper, self).connect(*args, **kw)
88 super(JSRoutesMapper, self).connect(*args, **kw)
89
89
90 def _extract_route_information(self, route):
90 def _extract_route_information(self, route):
91 """
91 """
92 Convert a route into tuple(name, path, args), eg:
92 Convert a route into tuple(name, path, args), eg:
93 ('show_user', '/profile/%(username)s', ['username'])
93 ('show_user', '/profile/%(username)s', ['username'])
94 """
94 """
95 routepath = route.routepath
95 routepath = route.routepath
96 def replace(matchobj):
96 def replace(matchobj):
97 if matchobj.group(1):
97 if matchobj.group(1):
98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
98 return "%%(%s)s" % matchobj.group(1).split(':')[0]
99 else:
99 else:
100 return "%%(%s)s" % matchobj.group(2)
100 return "%%(%s)s" % matchobj.group(2)
101
101
102 routepath = self._argument_prog.sub(replace, routepath)
102 routepath = self._argument_prog.sub(replace, routepath)
103 return (
103 return (
104 route.name,
104 route.name,
105 routepath,
105 routepath,
106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
106 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
107 for arg in self._argument_prog.findall(route.routepath)]
107 for arg in self._argument_prog.findall(route.routepath)]
108 )
108 )
109
109
110 def jsroutes(self):
110 def jsroutes(self):
111 """
111 """
112 Return a list of pyroutes.js compatible routes
112 Return a list of pyroutes.js compatible routes
113 """
113 """
114 for route_name in self._jsroutes:
114 for route_name in self._jsroutes:
115 yield self._extract_route_information(self._routenames[route_name])
115 yield self._extract_route_information(self._routenames[route_name])
116
116
117
117
118 def make_map(config):
118 def make_map(config):
119 """Create, configure and return the routes Mapper"""
119 """Create, configure and return the routes Mapper"""
120 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
120 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
121 always_scan=config['debug'])
121 always_scan=config['debug'])
122 rmap.minimization = False
122 rmap.minimization = False
123 rmap.explicit = False
123 rmap.explicit = False
124
124
125 from rhodecode.lib.utils2 import str2bool
125 from rhodecode.lib.utils2 import str2bool
126 from rhodecode.model import repo, repo_group
126 from rhodecode.model import repo, repo_group
127
127
128 def check_repo(environ, match_dict):
128 def check_repo(environ, match_dict):
129 """
129 """
130 check for valid repository for proper 404 handling
130 check for valid repository for proper 404 handling
131
131
132 :param environ:
132 :param environ:
133 :param match_dict:
133 :param match_dict:
134 """
134 """
135 repo_name = match_dict.get('repo_name')
135 repo_name = match_dict.get('repo_name')
136
136
137 if match_dict.get('f_path'):
137 if match_dict.get('f_path'):
138 # fix for multiple initial slashes that causes errors
138 # fix for multiple initial slashes that causes errors
139 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
139 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
140 repo_model = repo.RepoModel()
140 repo_model = repo.RepoModel()
141 by_name_match = repo_model.get_by_repo_name(repo_name)
141 by_name_match = repo_model.get_by_repo_name(repo_name)
142 # if we match quickly from database, short circuit the operation,
142 # if we match quickly from database, short circuit the operation,
143 # and validate repo based on the type.
143 # and validate repo based on the type.
144 if by_name_match:
144 if by_name_match:
145 return True
145 return True
146
146
147 by_id_match = repo_model.get_repo_by_id(repo_name)
147 by_id_match = repo_model.get_repo_by_id(repo_name)
148 if by_id_match:
148 if by_id_match:
149 repo_name = by_id_match.repo_name
149 repo_name = by_id_match.repo_name
150 match_dict['repo_name'] = repo_name
150 match_dict['repo_name'] = repo_name
151 return True
151 return True
152
152
153 return False
153 return False
154
154
155 def check_group(environ, match_dict):
155 def check_group(environ, match_dict):
156 """
156 """
157 check for valid repository group path for proper 404 handling
157 check for valid repository group path for proper 404 handling
158
158
159 :param environ:
159 :param environ:
160 :param match_dict:
160 :param match_dict:
161 """
161 """
162 repo_group_name = match_dict.get('group_name')
162 repo_group_name = match_dict.get('group_name')
163 repo_group_model = repo_group.RepoGroupModel()
163 repo_group_model = repo_group.RepoGroupModel()
164 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
164 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
165 if by_name_match:
165 if by_name_match:
166 return True
166 return True
167
167
168 return False
168 return False
169
169
170 def check_user_group(environ, match_dict):
170 def check_user_group(environ, match_dict):
171 """
171 """
172 check for valid user group for proper 404 handling
172 check for valid user group for proper 404 handling
173
173
174 :param environ:
174 :param environ:
175 :param match_dict:
175 :param match_dict:
176 """
176 """
177 return True
177 return True
178
178
179 def check_int(environ, match_dict):
179 def check_int(environ, match_dict):
180 return match_dict.get('id').isdigit()
180 return match_dict.get('id').isdigit()
181
181
182
182
183 #==========================================================================
183 #==========================================================================
184 # CUSTOM ROUTES HERE
184 # CUSTOM ROUTES HERE
185 #==========================================================================
185 #==========================================================================
186
186
187 # MAIN PAGE
188 rmap.connect('home', '/', controller='home', action='index')
189
190 # ping and pylons error test
187 # ping and pylons error test
191 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
188 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
192 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
189 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
193
190
194 # ADMIN REPOSITORY ROUTES
191 # ADMIN REPOSITORY ROUTES
195 with rmap.submapper(path_prefix=ADMIN_PREFIX,
192 with rmap.submapper(path_prefix=ADMIN_PREFIX,
196 controller='admin/repos') as m:
193 controller='admin/repos') as m:
197 m.connect('repos', '/repos',
194 m.connect('repos', '/repos',
198 action='create', conditions={'method': ['POST']})
195 action='create', conditions={'method': ['POST']})
199 m.connect('repos', '/repos',
196 m.connect('repos', '/repos',
200 action='index', conditions={'method': ['GET']})
197 action='index', conditions={'method': ['GET']})
201 m.connect('new_repo', '/create_repository', jsroute=True,
198 m.connect('new_repo', '/create_repository', jsroute=True,
202 action='create_repository', conditions={'method': ['GET']})
199 action='create_repository', conditions={'method': ['GET']})
203 m.connect('delete_repo', '/repos/{repo_name}',
200 m.connect('delete_repo', '/repos/{repo_name}',
204 action='delete', conditions={'method': ['DELETE']},
201 action='delete', conditions={'method': ['DELETE']},
205 requirements=URL_NAME_REQUIREMENTS)
202 requirements=URL_NAME_REQUIREMENTS)
206 m.connect('repo', '/repos/{repo_name}',
203 m.connect('repo', '/repos/{repo_name}',
207 action='show', conditions={'method': ['GET'],
204 action='show', conditions={'method': ['GET'],
208 'function': check_repo},
205 'function': check_repo},
209 requirements=URL_NAME_REQUIREMENTS)
206 requirements=URL_NAME_REQUIREMENTS)
210
207
211 # ADMIN REPOSITORY GROUPS ROUTES
208 # ADMIN REPOSITORY GROUPS ROUTES
212 with rmap.submapper(path_prefix=ADMIN_PREFIX,
209 with rmap.submapper(path_prefix=ADMIN_PREFIX,
213 controller='admin/repo_groups') as m:
210 controller='admin/repo_groups') as m:
214 m.connect('repo_groups', '/repo_groups',
211 m.connect('repo_groups', '/repo_groups',
215 action='create', conditions={'method': ['POST']})
212 action='create', conditions={'method': ['POST']})
216 m.connect('repo_groups', '/repo_groups',
213 m.connect('repo_groups', '/repo_groups',
217 action='index', conditions={'method': ['GET']})
214 action='index', conditions={'method': ['GET']})
218 m.connect('new_repo_group', '/repo_groups/new',
215 m.connect('new_repo_group', '/repo_groups/new',
219 action='new', conditions={'method': ['GET']})
216 action='new', conditions={'method': ['GET']})
220 m.connect('update_repo_group', '/repo_groups/{group_name}',
217 m.connect('update_repo_group', '/repo_groups/{group_name}',
221 action='update', conditions={'method': ['PUT'],
218 action='update', conditions={'method': ['PUT'],
222 'function': check_group},
219 'function': check_group},
223 requirements=URL_NAME_REQUIREMENTS)
220 requirements=URL_NAME_REQUIREMENTS)
224
221
225 # EXTRAS REPO GROUP ROUTES
222 # EXTRAS REPO GROUP ROUTES
226 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
223 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
227 action='edit',
224 action='edit',
228 conditions={'method': ['GET'], 'function': check_group},
225 conditions={'method': ['GET'], 'function': check_group},
229 requirements=URL_NAME_REQUIREMENTS)
226 requirements=URL_NAME_REQUIREMENTS)
230 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
227 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
231 action='edit',
228 action='edit',
232 conditions={'method': ['PUT'], 'function': check_group},
229 conditions={'method': ['PUT'], 'function': check_group},
233 requirements=URL_NAME_REQUIREMENTS)
230 requirements=URL_NAME_REQUIREMENTS)
234
231
235 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
232 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
236 action='edit_repo_group_advanced',
233 action='edit_repo_group_advanced',
237 conditions={'method': ['GET'], 'function': check_group},
234 conditions={'method': ['GET'], 'function': check_group},
238 requirements=URL_NAME_REQUIREMENTS)
235 requirements=URL_NAME_REQUIREMENTS)
239 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
236 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
240 action='edit_repo_group_advanced',
237 action='edit_repo_group_advanced',
241 conditions={'method': ['PUT'], 'function': check_group},
238 conditions={'method': ['PUT'], 'function': check_group},
242 requirements=URL_NAME_REQUIREMENTS)
239 requirements=URL_NAME_REQUIREMENTS)
243
240
244 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
241 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
245 action='edit_repo_group_perms',
242 action='edit_repo_group_perms',
246 conditions={'method': ['GET'], 'function': check_group},
243 conditions={'method': ['GET'], 'function': check_group},
247 requirements=URL_NAME_REQUIREMENTS)
244 requirements=URL_NAME_REQUIREMENTS)
248 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
245 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
249 action='update_perms',
246 action='update_perms',
250 conditions={'method': ['PUT'], 'function': check_group},
247 conditions={'method': ['PUT'], 'function': check_group},
251 requirements=URL_NAME_REQUIREMENTS)
248 requirements=URL_NAME_REQUIREMENTS)
252
249
253 m.connect('delete_repo_group', '/repo_groups/{group_name}',
250 m.connect('delete_repo_group', '/repo_groups/{group_name}',
254 action='delete', conditions={'method': ['DELETE'],
251 action='delete', conditions={'method': ['DELETE'],
255 'function': check_group},
252 'function': check_group},
256 requirements=URL_NAME_REQUIREMENTS)
253 requirements=URL_NAME_REQUIREMENTS)
257
254
258 # ADMIN USER ROUTES
255 # ADMIN USER ROUTES
259 with rmap.submapper(path_prefix=ADMIN_PREFIX,
256 with rmap.submapper(path_prefix=ADMIN_PREFIX,
260 controller='admin/users') as m:
257 controller='admin/users') as m:
261 m.connect('users', '/users',
258 m.connect('users', '/users',
262 action='create', conditions={'method': ['POST']})
259 action='create', conditions={'method': ['POST']})
263 m.connect('new_user', '/users/new',
260 m.connect('new_user', '/users/new',
264 action='new', conditions={'method': ['GET']})
261 action='new', conditions={'method': ['GET']})
265 m.connect('update_user', '/users/{user_id}',
262 m.connect('update_user', '/users/{user_id}',
266 action='update', conditions={'method': ['PUT']})
263 action='update', conditions={'method': ['PUT']})
267 m.connect('delete_user', '/users/{user_id}',
264 m.connect('delete_user', '/users/{user_id}',
268 action='delete', conditions={'method': ['DELETE']})
265 action='delete', conditions={'method': ['DELETE']})
269 m.connect('edit_user', '/users/{user_id}/edit',
266 m.connect('edit_user', '/users/{user_id}/edit',
270 action='edit', conditions={'method': ['GET']}, jsroute=True)
267 action='edit', conditions={'method': ['GET']}, jsroute=True)
271 m.connect('user', '/users/{user_id}',
268 m.connect('user', '/users/{user_id}',
272 action='show', conditions={'method': ['GET']})
269 action='show', conditions={'method': ['GET']})
273 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
270 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
274 action='reset_password', conditions={'method': ['POST']})
271 action='reset_password', conditions={'method': ['POST']})
275 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
272 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
276 action='create_personal_repo_group', conditions={'method': ['POST']})
273 action='create_personal_repo_group', conditions={'method': ['POST']})
277
274
278 # EXTRAS USER ROUTES
275 # EXTRAS USER ROUTES
279 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
276 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
280 action='edit_advanced', conditions={'method': ['GET']})
277 action='edit_advanced', conditions={'method': ['GET']})
281 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
278 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
282 action='update_advanced', conditions={'method': ['PUT']})
279 action='update_advanced', conditions={'method': ['PUT']})
283
280
284 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
281 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
285 action='edit_global_perms', conditions={'method': ['GET']})
282 action='edit_global_perms', conditions={'method': ['GET']})
286 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
283 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
287 action='update_global_perms', conditions={'method': ['PUT']})
284 action='update_global_perms', conditions={'method': ['PUT']})
288
285
289 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
286 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
290 action='edit_perms_summary', conditions={'method': ['GET']})
287 action='edit_perms_summary', conditions={'method': ['GET']})
291
288
292 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
289 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
293 action='edit_emails', conditions={'method': ['GET']})
290 action='edit_emails', conditions={'method': ['GET']})
294 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
291 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
295 action='add_email', conditions={'method': ['PUT']})
292 action='add_email', conditions={'method': ['PUT']})
296 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
293 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
297 action='delete_email', conditions={'method': ['DELETE']})
294 action='delete_email', conditions={'method': ['DELETE']})
298
295
299 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
296 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
300 action='edit_ips', conditions={'method': ['GET']})
297 action='edit_ips', conditions={'method': ['GET']})
301 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
298 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
302 action='add_ip', conditions={'method': ['PUT']})
299 action='add_ip', conditions={'method': ['PUT']})
303 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
300 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
304 action='delete_ip', conditions={'method': ['DELETE']})
301 action='delete_ip', conditions={'method': ['DELETE']})
305
302
306 # ADMIN USER GROUPS REST ROUTES
303 # ADMIN USER GROUPS REST ROUTES
307 with rmap.submapper(path_prefix=ADMIN_PREFIX,
304 with rmap.submapper(path_prefix=ADMIN_PREFIX,
308 controller='admin/user_groups') as m:
305 controller='admin/user_groups') as m:
309 m.connect('users_groups', '/user_groups',
306 m.connect('users_groups', '/user_groups',
310 action='create', conditions={'method': ['POST']})
307 action='create', conditions={'method': ['POST']})
311 m.connect('users_groups', '/user_groups',
308 m.connect('users_groups', '/user_groups',
312 action='index', conditions={'method': ['GET']})
309 action='index', conditions={'method': ['GET']})
313 m.connect('new_users_group', '/user_groups/new',
310 m.connect('new_users_group', '/user_groups/new',
314 action='new', conditions={'method': ['GET']})
311 action='new', conditions={'method': ['GET']})
315 m.connect('update_users_group', '/user_groups/{user_group_id}',
312 m.connect('update_users_group', '/user_groups/{user_group_id}',
316 action='update', conditions={'method': ['PUT']})
313 action='update', conditions={'method': ['PUT']})
317 m.connect('delete_users_group', '/user_groups/{user_group_id}',
314 m.connect('delete_users_group', '/user_groups/{user_group_id}',
318 action='delete', conditions={'method': ['DELETE']})
315 action='delete', conditions={'method': ['DELETE']})
319 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
316 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
320 action='edit', conditions={'method': ['GET']},
317 action='edit', conditions={'method': ['GET']},
321 function=check_user_group)
318 function=check_user_group)
322
319
323 # EXTRAS USER GROUP ROUTES
320 # EXTRAS USER GROUP ROUTES
324 m.connect('edit_user_group_global_perms',
321 m.connect('edit_user_group_global_perms',
325 '/user_groups/{user_group_id}/edit/global_permissions',
322 '/user_groups/{user_group_id}/edit/global_permissions',
326 action='edit_global_perms', conditions={'method': ['GET']})
323 action='edit_global_perms', conditions={'method': ['GET']})
327 m.connect('edit_user_group_global_perms',
324 m.connect('edit_user_group_global_perms',
328 '/user_groups/{user_group_id}/edit/global_permissions',
325 '/user_groups/{user_group_id}/edit/global_permissions',
329 action='update_global_perms', conditions={'method': ['PUT']})
326 action='update_global_perms', conditions={'method': ['PUT']})
330 m.connect('edit_user_group_perms_summary',
327 m.connect('edit_user_group_perms_summary',
331 '/user_groups/{user_group_id}/edit/permissions_summary',
328 '/user_groups/{user_group_id}/edit/permissions_summary',
332 action='edit_perms_summary', conditions={'method': ['GET']})
329 action='edit_perms_summary', conditions={'method': ['GET']})
333
330
334 m.connect('edit_user_group_perms',
331 m.connect('edit_user_group_perms',
335 '/user_groups/{user_group_id}/edit/permissions',
332 '/user_groups/{user_group_id}/edit/permissions',
336 action='edit_perms', conditions={'method': ['GET']})
333 action='edit_perms', conditions={'method': ['GET']})
337 m.connect('edit_user_group_perms',
334 m.connect('edit_user_group_perms',
338 '/user_groups/{user_group_id}/edit/permissions',
335 '/user_groups/{user_group_id}/edit/permissions',
339 action='update_perms', conditions={'method': ['PUT']})
336 action='update_perms', conditions={'method': ['PUT']})
340
337
341 m.connect('edit_user_group_advanced',
338 m.connect('edit_user_group_advanced',
342 '/user_groups/{user_group_id}/edit/advanced',
339 '/user_groups/{user_group_id}/edit/advanced',
343 action='edit_advanced', conditions={'method': ['GET']})
340 action='edit_advanced', conditions={'method': ['GET']})
344
341
345 m.connect('edit_user_group_advanced_sync',
342 m.connect('edit_user_group_advanced_sync',
346 '/user_groups/{user_group_id}/edit/advanced/sync',
343 '/user_groups/{user_group_id}/edit/advanced/sync',
347 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
344 action='edit_advanced_set_synchronization', conditions={'method': ['POST']})
348
345
349 m.connect('edit_user_group_members',
346 m.connect('edit_user_group_members',
350 '/user_groups/{user_group_id}/edit/members', jsroute=True,
347 '/user_groups/{user_group_id}/edit/members', jsroute=True,
351 action='user_group_members', conditions={'method': ['GET']})
348 action='user_group_members', conditions={'method': ['GET']})
352
349
353 # ADMIN PERMISSIONS ROUTES
350 # ADMIN PERMISSIONS ROUTES
354 with rmap.submapper(path_prefix=ADMIN_PREFIX,
351 with rmap.submapper(path_prefix=ADMIN_PREFIX,
355 controller='admin/permissions') as m:
352 controller='admin/permissions') as m:
356 m.connect('admin_permissions_application', '/permissions/application',
353 m.connect('admin_permissions_application', '/permissions/application',
357 action='permission_application_update', conditions={'method': ['POST']})
354 action='permission_application_update', conditions={'method': ['POST']})
358 m.connect('admin_permissions_application', '/permissions/application',
355 m.connect('admin_permissions_application', '/permissions/application',
359 action='permission_application', conditions={'method': ['GET']})
356 action='permission_application', conditions={'method': ['GET']})
360
357
361 m.connect('admin_permissions_global', '/permissions/global',
358 m.connect('admin_permissions_global', '/permissions/global',
362 action='permission_global_update', conditions={'method': ['POST']})
359 action='permission_global_update', conditions={'method': ['POST']})
363 m.connect('admin_permissions_global', '/permissions/global',
360 m.connect('admin_permissions_global', '/permissions/global',
364 action='permission_global', conditions={'method': ['GET']})
361 action='permission_global', conditions={'method': ['GET']})
365
362
366 m.connect('admin_permissions_object', '/permissions/object',
363 m.connect('admin_permissions_object', '/permissions/object',
367 action='permission_objects_update', conditions={'method': ['POST']})
364 action='permission_objects_update', conditions={'method': ['POST']})
368 m.connect('admin_permissions_object', '/permissions/object',
365 m.connect('admin_permissions_object', '/permissions/object',
369 action='permission_objects', conditions={'method': ['GET']})
366 action='permission_objects', conditions={'method': ['GET']})
370
367
371 m.connect('admin_permissions_ips', '/permissions/ips',
368 m.connect('admin_permissions_ips', '/permissions/ips',
372 action='permission_ips', conditions={'method': ['POST']})
369 action='permission_ips', conditions={'method': ['POST']})
373 m.connect('admin_permissions_ips', '/permissions/ips',
370 m.connect('admin_permissions_ips', '/permissions/ips',
374 action='permission_ips', conditions={'method': ['GET']})
371 action='permission_ips', conditions={'method': ['GET']})
375
372
376 m.connect('admin_permissions_overview', '/permissions/overview',
373 m.connect('admin_permissions_overview', '/permissions/overview',
377 action='permission_perms', conditions={'method': ['GET']})
374 action='permission_perms', conditions={'method': ['GET']})
378
375
379 # ADMIN DEFAULTS REST ROUTES
376 # ADMIN DEFAULTS REST ROUTES
380 with rmap.submapper(path_prefix=ADMIN_PREFIX,
377 with rmap.submapper(path_prefix=ADMIN_PREFIX,
381 controller='admin/defaults') as m:
378 controller='admin/defaults') as m:
382 m.connect('admin_defaults_repositories', '/defaults/repositories',
379 m.connect('admin_defaults_repositories', '/defaults/repositories',
383 action='update_repository_defaults', conditions={'method': ['POST']})
380 action='update_repository_defaults', conditions={'method': ['POST']})
384 m.connect('admin_defaults_repositories', '/defaults/repositories',
381 m.connect('admin_defaults_repositories', '/defaults/repositories',
385 action='index', conditions={'method': ['GET']})
382 action='index', conditions={'method': ['GET']})
386
383
387 # ADMIN DEBUG STYLE ROUTES
384 # ADMIN DEBUG STYLE ROUTES
388 if str2bool(config.get('debug_style')):
385 if str2bool(config.get('debug_style')):
389 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
386 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
390 controller='debug_style') as m:
387 controller='debug_style') as m:
391 m.connect('debug_style_home', '',
388 m.connect('debug_style_home', '',
392 action='index', conditions={'method': ['GET']})
389 action='index', conditions={'method': ['GET']})
393 m.connect('debug_style_template', '/t/{t_path}',
390 m.connect('debug_style_template', '/t/{t_path}',
394 action='template', conditions={'method': ['GET']})
391 action='template', conditions={'method': ['GET']})
395
392
396 # ADMIN SETTINGS ROUTES
393 # ADMIN SETTINGS ROUTES
397 with rmap.submapper(path_prefix=ADMIN_PREFIX,
394 with rmap.submapper(path_prefix=ADMIN_PREFIX,
398 controller='admin/settings') as m:
395 controller='admin/settings') as m:
399
396
400 # default
397 # default
401 m.connect('admin_settings', '/settings',
398 m.connect('admin_settings', '/settings',
402 action='settings_global_update',
399 action='settings_global_update',
403 conditions={'method': ['POST']})
400 conditions={'method': ['POST']})
404 m.connect('admin_settings', '/settings',
401 m.connect('admin_settings', '/settings',
405 action='settings_global', conditions={'method': ['GET']})
402 action='settings_global', conditions={'method': ['GET']})
406
403
407 m.connect('admin_settings_vcs', '/settings/vcs',
404 m.connect('admin_settings_vcs', '/settings/vcs',
408 action='settings_vcs_update',
405 action='settings_vcs_update',
409 conditions={'method': ['POST']})
406 conditions={'method': ['POST']})
410 m.connect('admin_settings_vcs', '/settings/vcs',
407 m.connect('admin_settings_vcs', '/settings/vcs',
411 action='settings_vcs',
408 action='settings_vcs',
412 conditions={'method': ['GET']})
409 conditions={'method': ['GET']})
413 m.connect('admin_settings_vcs', '/settings/vcs',
410 m.connect('admin_settings_vcs', '/settings/vcs',
414 action='delete_svn_pattern',
411 action='delete_svn_pattern',
415 conditions={'method': ['DELETE']})
412 conditions={'method': ['DELETE']})
416
413
417 m.connect('admin_settings_mapping', '/settings/mapping',
414 m.connect('admin_settings_mapping', '/settings/mapping',
418 action='settings_mapping_update',
415 action='settings_mapping_update',
419 conditions={'method': ['POST']})
416 conditions={'method': ['POST']})
420 m.connect('admin_settings_mapping', '/settings/mapping',
417 m.connect('admin_settings_mapping', '/settings/mapping',
421 action='settings_mapping', conditions={'method': ['GET']})
418 action='settings_mapping', conditions={'method': ['GET']})
422
419
423 m.connect('admin_settings_global', '/settings/global',
420 m.connect('admin_settings_global', '/settings/global',
424 action='settings_global_update',
421 action='settings_global_update',
425 conditions={'method': ['POST']})
422 conditions={'method': ['POST']})
426 m.connect('admin_settings_global', '/settings/global',
423 m.connect('admin_settings_global', '/settings/global',
427 action='settings_global', conditions={'method': ['GET']})
424 action='settings_global', conditions={'method': ['GET']})
428
425
429 m.connect('admin_settings_visual', '/settings/visual',
426 m.connect('admin_settings_visual', '/settings/visual',
430 action='settings_visual_update',
427 action='settings_visual_update',
431 conditions={'method': ['POST']})
428 conditions={'method': ['POST']})
432 m.connect('admin_settings_visual', '/settings/visual',
429 m.connect('admin_settings_visual', '/settings/visual',
433 action='settings_visual', conditions={'method': ['GET']})
430 action='settings_visual', conditions={'method': ['GET']})
434
431
435 m.connect('admin_settings_issuetracker',
432 m.connect('admin_settings_issuetracker',
436 '/settings/issue-tracker', action='settings_issuetracker',
433 '/settings/issue-tracker', action='settings_issuetracker',
437 conditions={'method': ['GET']})
434 conditions={'method': ['GET']})
438 m.connect('admin_settings_issuetracker_save',
435 m.connect('admin_settings_issuetracker_save',
439 '/settings/issue-tracker/save',
436 '/settings/issue-tracker/save',
440 action='settings_issuetracker_save',
437 action='settings_issuetracker_save',
441 conditions={'method': ['POST']})
438 conditions={'method': ['POST']})
442 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
439 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
443 action='settings_issuetracker_test',
440 action='settings_issuetracker_test',
444 conditions={'method': ['POST']})
441 conditions={'method': ['POST']})
445 m.connect('admin_issuetracker_delete',
442 m.connect('admin_issuetracker_delete',
446 '/settings/issue-tracker/delete',
443 '/settings/issue-tracker/delete',
447 action='settings_issuetracker_delete',
444 action='settings_issuetracker_delete',
448 conditions={'method': ['DELETE']})
445 conditions={'method': ['DELETE']})
449
446
450 m.connect('admin_settings_email', '/settings/email',
447 m.connect('admin_settings_email', '/settings/email',
451 action='settings_email_update',
448 action='settings_email_update',
452 conditions={'method': ['POST']})
449 conditions={'method': ['POST']})
453 m.connect('admin_settings_email', '/settings/email',
450 m.connect('admin_settings_email', '/settings/email',
454 action='settings_email', conditions={'method': ['GET']})
451 action='settings_email', conditions={'method': ['GET']})
455
452
456 m.connect('admin_settings_hooks', '/settings/hooks',
453 m.connect('admin_settings_hooks', '/settings/hooks',
457 action='settings_hooks_update',
454 action='settings_hooks_update',
458 conditions={'method': ['POST', 'DELETE']})
455 conditions={'method': ['POST', 'DELETE']})
459 m.connect('admin_settings_hooks', '/settings/hooks',
456 m.connect('admin_settings_hooks', '/settings/hooks',
460 action='settings_hooks', conditions={'method': ['GET']})
457 action='settings_hooks', conditions={'method': ['GET']})
461
458
462 m.connect('admin_settings_search', '/settings/search',
459 m.connect('admin_settings_search', '/settings/search',
463 action='settings_search', conditions={'method': ['GET']})
460 action='settings_search', conditions={'method': ['GET']})
464
461
465 m.connect('admin_settings_supervisor', '/settings/supervisor',
462 m.connect('admin_settings_supervisor', '/settings/supervisor',
466 action='settings_supervisor', conditions={'method': ['GET']})
463 action='settings_supervisor', conditions={'method': ['GET']})
467 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
464 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
468 action='settings_supervisor_log', conditions={'method': ['GET']})
465 action='settings_supervisor_log', conditions={'method': ['GET']})
469
466
470 m.connect('admin_settings_labs', '/settings/labs',
467 m.connect('admin_settings_labs', '/settings/labs',
471 action='settings_labs_update',
468 action='settings_labs_update',
472 conditions={'method': ['POST']})
469 conditions={'method': ['POST']})
473 m.connect('admin_settings_labs', '/settings/labs',
470 m.connect('admin_settings_labs', '/settings/labs',
474 action='settings_labs', conditions={'method': ['GET']})
471 action='settings_labs', conditions={'method': ['GET']})
475
472
476 # ADMIN MY ACCOUNT
473 # ADMIN MY ACCOUNT
477 with rmap.submapper(path_prefix=ADMIN_PREFIX,
474 with rmap.submapper(path_prefix=ADMIN_PREFIX,
478 controller='admin/my_account') as m:
475 controller='admin/my_account') as m:
479
476
480 m.connect('my_account_edit', '/my_account/edit',
477 m.connect('my_account_edit', '/my_account/edit',
481 action='my_account_edit', conditions={'method': ['GET']})
478 action='my_account_edit', conditions={'method': ['GET']})
482 m.connect('my_account', '/my_account/update',
479 m.connect('my_account', '/my_account/update',
483 action='my_account_update', conditions={'method': ['POST']})
480 action='my_account_update', conditions={'method': ['POST']})
484
481
485 # NOTE(marcink): this needs to be kept for password force flag to be
482 # NOTE(marcink): this needs to be kept for password force flag to be
486 # handler, remove after migration to pyramid
483 # handler, remove after migration to pyramid
487 m.connect('my_account_password', '/my_account/password',
484 m.connect('my_account_password', '/my_account/password',
488 action='my_account_password', conditions={'method': ['GET']})
485 action='my_account_password', conditions={'method': ['GET']})
489
486
490 m.connect('my_account_repos', '/my_account/repos',
487 m.connect('my_account_repos', '/my_account/repos',
491 action='my_account_repos', conditions={'method': ['GET']})
488 action='my_account_repos', conditions={'method': ['GET']})
492
489
493 m.connect('my_account_watched', '/my_account/watched',
490 m.connect('my_account_watched', '/my_account/watched',
494 action='my_account_watched', conditions={'method': ['GET']})
491 action='my_account_watched', conditions={'method': ['GET']})
495
492
496 m.connect('my_account_pullrequests', '/my_account/pull_requests',
493 m.connect('my_account_pullrequests', '/my_account/pull_requests',
497 action='my_account_pullrequests', conditions={'method': ['GET']})
494 action='my_account_pullrequests', conditions={'method': ['GET']})
498
495
499 m.connect('my_account_perms', '/my_account/perms',
496 m.connect('my_account_perms', '/my_account/perms',
500 action='my_account_perms', conditions={'method': ['GET']})
497 action='my_account_perms', conditions={'method': ['GET']})
501
498
502 m.connect('my_account_emails', '/my_account/emails',
499 m.connect('my_account_emails', '/my_account/emails',
503 action='my_account_emails', conditions={'method': ['GET']})
500 action='my_account_emails', conditions={'method': ['GET']})
504 m.connect('my_account_emails', '/my_account/emails',
501 m.connect('my_account_emails', '/my_account/emails',
505 action='my_account_emails_add', conditions={'method': ['POST']})
502 action='my_account_emails_add', conditions={'method': ['POST']})
506 m.connect('my_account_emails', '/my_account/emails',
503 m.connect('my_account_emails', '/my_account/emails',
507 action='my_account_emails_delete', conditions={'method': ['DELETE']})
504 action='my_account_emails_delete', conditions={'method': ['DELETE']})
508
505
509 m.connect('my_account_notifications', '/my_account/notifications',
506 m.connect('my_account_notifications', '/my_account/notifications',
510 action='my_notifications',
507 action='my_notifications',
511 conditions={'method': ['GET']})
508 conditions={'method': ['GET']})
512 m.connect('my_account_notifications_toggle_visibility',
509 m.connect('my_account_notifications_toggle_visibility',
513 '/my_account/toggle_visibility',
510 '/my_account/toggle_visibility',
514 action='my_notifications_toggle_visibility',
511 action='my_notifications_toggle_visibility',
515 conditions={'method': ['POST']})
512 conditions={'method': ['POST']})
516
513
517 # NOTIFICATION REST ROUTES
514 # NOTIFICATION REST ROUTES
518 with rmap.submapper(path_prefix=ADMIN_PREFIX,
515 with rmap.submapper(path_prefix=ADMIN_PREFIX,
519 controller='admin/notifications') as m:
516 controller='admin/notifications') as m:
520 m.connect('notifications', '/notifications',
517 m.connect('notifications', '/notifications',
521 action='index', conditions={'method': ['GET']})
518 action='index', conditions={'method': ['GET']})
522 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
519 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
523 action='mark_all_read', conditions={'method': ['POST']})
520 action='mark_all_read', conditions={'method': ['POST']})
524 m.connect('/notifications/{notification_id}',
521 m.connect('/notifications/{notification_id}',
525 action='update', conditions={'method': ['PUT']})
522 action='update', conditions={'method': ['PUT']})
526 m.connect('/notifications/{notification_id}',
523 m.connect('/notifications/{notification_id}',
527 action='delete', conditions={'method': ['DELETE']})
524 action='delete', conditions={'method': ['DELETE']})
528 m.connect('notification', '/notifications/{notification_id}',
525 m.connect('notification', '/notifications/{notification_id}',
529 action='show', conditions={'method': ['GET']})
526 action='show', conditions={'method': ['GET']})
530
527
531 # ADMIN GIST
528 # ADMIN GIST
532 with rmap.submapper(path_prefix=ADMIN_PREFIX,
529 with rmap.submapper(path_prefix=ADMIN_PREFIX,
533 controller='admin/gists') as m:
530 controller='admin/gists') as m:
534 m.connect('gists', '/gists',
531 m.connect('gists', '/gists',
535 action='create', conditions={'method': ['POST']})
532 action='create', conditions={'method': ['POST']})
536 m.connect('gists', '/gists', jsroute=True,
533 m.connect('gists', '/gists', jsroute=True,
537 action='index', conditions={'method': ['GET']})
534 action='index', conditions={'method': ['GET']})
538 m.connect('new_gist', '/gists/new', jsroute=True,
535 m.connect('new_gist', '/gists/new', jsroute=True,
539 action='new', conditions={'method': ['GET']})
536 action='new', conditions={'method': ['GET']})
540
537
541 m.connect('/gists/{gist_id}',
538 m.connect('/gists/{gist_id}',
542 action='delete', conditions={'method': ['DELETE']})
539 action='delete', conditions={'method': ['DELETE']})
543 m.connect('edit_gist', '/gists/{gist_id}/edit',
540 m.connect('edit_gist', '/gists/{gist_id}/edit',
544 action='edit_form', conditions={'method': ['GET']})
541 action='edit_form', conditions={'method': ['GET']})
545 m.connect('edit_gist', '/gists/{gist_id}/edit',
542 m.connect('edit_gist', '/gists/{gist_id}/edit',
546 action='edit', conditions={'method': ['POST']})
543 action='edit', conditions={'method': ['POST']})
547 m.connect(
544 m.connect(
548 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
545 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
549 action='check_revision', conditions={'method': ['GET']})
546 action='check_revision', conditions={'method': ['GET']})
550
547
551 m.connect('gist', '/gists/{gist_id}',
548 m.connect('gist', '/gists/{gist_id}',
552 action='show', conditions={'method': ['GET']})
549 action='show', conditions={'method': ['GET']})
553 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
550 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
554 revision='tip',
551 revision='tip',
555 action='show', conditions={'method': ['GET']})
552 action='show', conditions={'method': ['GET']})
556 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
553 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
557 revision='tip',
554 revision='tip',
558 action='show', conditions={'method': ['GET']})
555 action='show', conditions={'method': ['GET']})
559 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
556 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
560 revision='tip',
557 revision='tip',
561 action='show', conditions={'method': ['GET']},
558 action='show', conditions={'method': ['GET']},
562 requirements=URL_NAME_REQUIREMENTS)
559 requirements=URL_NAME_REQUIREMENTS)
563
560
564 # USER JOURNAL
561 # USER JOURNAL
565 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
562 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
566 controller='journal', action='index')
563 controller='journal', action='index')
567 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
564 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
568 controller='journal', action='journal_rss')
565 controller='journal', action='journal_rss')
569 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
566 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
570 controller='journal', action='journal_atom')
567 controller='journal', action='journal_atom')
571
568
572 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
569 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
573 controller='journal', action='public_journal')
570 controller='journal', action='public_journal')
574
571
575 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
572 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
576 controller='journal', action='public_journal_rss')
573 controller='journal', action='public_journal_rss')
577
574
578 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
575 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
579 controller='journal', action='public_journal_rss')
576 controller='journal', action='public_journal_rss')
580
577
581 rmap.connect('public_journal_atom',
578 rmap.connect('public_journal_atom',
582 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
579 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
583 action='public_journal_atom')
580 action='public_journal_atom')
584
581
585 rmap.connect('public_journal_atom_old',
582 rmap.connect('public_journal_atom_old',
586 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
583 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
587 action='public_journal_atom')
584 action='public_journal_atom')
588
585
589 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
586 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
590 controller='journal', action='toggle_following', jsroute=True,
587 controller='journal', action='toggle_following', jsroute=True,
591 conditions={'method': ['POST']})
588 conditions={'method': ['POST']})
592
589
593 # FEEDS
590 # FEEDS
594 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
591 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
595 controller='feed', action='rss',
592 controller='feed', action='rss',
596 conditions={'function': check_repo},
593 conditions={'function': check_repo},
597 requirements=URL_NAME_REQUIREMENTS)
594 requirements=URL_NAME_REQUIREMENTS)
598
595
599 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
596 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
600 controller='feed', action='atom',
597 controller='feed', action='atom',
601 conditions={'function': check_repo},
598 conditions={'function': check_repo},
602 requirements=URL_NAME_REQUIREMENTS)
599 requirements=URL_NAME_REQUIREMENTS)
603
600
604 #==========================================================================
601 #==========================================================================
605 # REPOSITORY ROUTES
602 # REPOSITORY ROUTES
606 #==========================================================================
603 #==========================================================================
607
604
608 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
605 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
609 controller='admin/repos', action='repo_creating',
606 controller='admin/repos', action='repo_creating',
610 requirements=URL_NAME_REQUIREMENTS)
607 requirements=URL_NAME_REQUIREMENTS)
611 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
608 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
612 controller='admin/repos', action='repo_check',
609 controller='admin/repos', action='repo_check',
613 requirements=URL_NAME_REQUIREMENTS)
610 requirements=URL_NAME_REQUIREMENTS)
614
611
615 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
612 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
616 controller='summary', action='repo_stats',
613 controller='summary', action='repo_stats',
617 conditions={'function': check_repo},
614 conditions={'function': check_repo},
618 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
615 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
619
616
620 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
617 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
621 controller='summary', action='repo_refs_data',
618 controller='summary', action='repo_refs_data',
622 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
619 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
623 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
620 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
624 controller='summary', action='repo_refs_changelog_data',
621 controller='summary', action='repo_refs_changelog_data',
625 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
622 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
626
623
627 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
624 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
628 controller='changeset', revision='tip',
625 controller='changeset', revision='tip',
629 conditions={'function': check_repo},
626 conditions={'function': check_repo},
630 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
627 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
631 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
628 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
632 controller='changeset', revision='tip', action='changeset_children',
629 controller='changeset', revision='tip', action='changeset_children',
633 conditions={'function': check_repo},
630 conditions={'function': check_repo},
634 requirements=URL_NAME_REQUIREMENTS)
631 requirements=URL_NAME_REQUIREMENTS)
635 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
632 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
636 controller='changeset', revision='tip', action='changeset_parents',
633 controller='changeset', revision='tip', action='changeset_parents',
637 conditions={'function': check_repo},
634 conditions={'function': check_repo},
638 requirements=URL_NAME_REQUIREMENTS)
635 requirements=URL_NAME_REQUIREMENTS)
639
636
640 # repo edit options
637 # repo edit options
641 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
638 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
642 controller='admin/repos', action='edit_fields',
639 controller='admin/repos', action='edit_fields',
643 conditions={'method': ['GET'], 'function': check_repo},
640 conditions={'method': ['GET'], 'function': check_repo},
644 requirements=URL_NAME_REQUIREMENTS)
641 requirements=URL_NAME_REQUIREMENTS)
645 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
642 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
646 controller='admin/repos', action='create_repo_field',
643 controller='admin/repos', action='create_repo_field',
647 conditions={'method': ['PUT'], 'function': check_repo},
644 conditions={'method': ['PUT'], 'function': check_repo},
648 requirements=URL_NAME_REQUIREMENTS)
645 requirements=URL_NAME_REQUIREMENTS)
649 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
646 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
650 controller='admin/repos', action='delete_repo_field',
647 controller='admin/repos', action='delete_repo_field',
651 conditions={'method': ['DELETE'], 'function': check_repo},
648 conditions={'method': ['DELETE'], 'function': check_repo},
652 requirements=URL_NAME_REQUIREMENTS)
649 requirements=URL_NAME_REQUIREMENTS)
653
650
654 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
651 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
655 controller='admin/repos', action='toggle_locking',
652 controller='admin/repos', action='toggle_locking',
656 conditions={'method': ['GET'], 'function': check_repo},
653 conditions={'method': ['GET'], 'function': check_repo},
657 requirements=URL_NAME_REQUIREMENTS)
654 requirements=URL_NAME_REQUIREMENTS)
658
655
659 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
656 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
660 controller='admin/repos', action='edit_remote_form',
657 controller='admin/repos', action='edit_remote_form',
661 conditions={'method': ['GET'], 'function': check_repo},
658 conditions={'method': ['GET'], 'function': check_repo},
662 requirements=URL_NAME_REQUIREMENTS)
659 requirements=URL_NAME_REQUIREMENTS)
663 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
660 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
664 controller='admin/repos', action='edit_remote',
661 controller='admin/repos', action='edit_remote',
665 conditions={'method': ['PUT'], 'function': check_repo},
662 conditions={'method': ['PUT'], 'function': check_repo},
666 requirements=URL_NAME_REQUIREMENTS)
663 requirements=URL_NAME_REQUIREMENTS)
667
664
668 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
665 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
669 controller='admin/repos', action='edit_statistics_form',
666 controller='admin/repos', action='edit_statistics_form',
670 conditions={'method': ['GET'], 'function': check_repo},
667 conditions={'method': ['GET'], 'function': check_repo},
671 requirements=URL_NAME_REQUIREMENTS)
668 requirements=URL_NAME_REQUIREMENTS)
672 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
669 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
673 controller='admin/repos', action='edit_statistics',
670 controller='admin/repos', action='edit_statistics',
674 conditions={'method': ['PUT'], 'function': check_repo},
671 conditions={'method': ['PUT'], 'function': check_repo},
675 requirements=URL_NAME_REQUIREMENTS)
672 requirements=URL_NAME_REQUIREMENTS)
676 rmap.connect('repo_settings_issuetracker',
673 rmap.connect('repo_settings_issuetracker',
677 '/{repo_name}/settings/issue-tracker',
674 '/{repo_name}/settings/issue-tracker',
678 controller='admin/repos', action='repo_issuetracker',
675 controller='admin/repos', action='repo_issuetracker',
679 conditions={'method': ['GET'], 'function': check_repo},
676 conditions={'method': ['GET'], 'function': check_repo},
680 requirements=URL_NAME_REQUIREMENTS)
677 requirements=URL_NAME_REQUIREMENTS)
681 rmap.connect('repo_issuetracker_test',
678 rmap.connect('repo_issuetracker_test',
682 '/{repo_name}/settings/issue-tracker/test',
679 '/{repo_name}/settings/issue-tracker/test',
683 controller='admin/repos', action='repo_issuetracker_test',
680 controller='admin/repos', action='repo_issuetracker_test',
684 conditions={'method': ['POST'], 'function': check_repo},
681 conditions={'method': ['POST'], 'function': check_repo},
685 requirements=URL_NAME_REQUIREMENTS)
682 requirements=URL_NAME_REQUIREMENTS)
686 rmap.connect('repo_issuetracker_delete',
683 rmap.connect('repo_issuetracker_delete',
687 '/{repo_name}/settings/issue-tracker/delete',
684 '/{repo_name}/settings/issue-tracker/delete',
688 controller='admin/repos', action='repo_issuetracker_delete',
685 controller='admin/repos', action='repo_issuetracker_delete',
689 conditions={'method': ['DELETE'], 'function': check_repo},
686 conditions={'method': ['DELETE'], 'function': check_repo},
690 requirements=URL_NAME_REQUIREMENTS)
687 requirements=URL_NAME_REQUIREMENTS)
691 rmap.connect('repo_issuetracker_save',
688 rmap.connect('repo_issuetracker_save',
692 '/{repo_name}/settings/issue-tracker/save',
689 '/{repo_name}/settings/issue-tracker/save',
693 controller='admin/repos', action='repo_issuetracker_save',
690 controller='admin/repos', action='repo_issuetracker_save',
694 conditions={'method': ['POST'], 'function': check_repo},
691 conditions={'method': ['POST'], 'function': check_repo},
695 requirements=URL_NAME_REQUIREMENTS)
692 requirements=URL_NAME_REQUIREMENTS)
696 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
693 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
697 controller='admin/repos', action='repo_settings_vcs_update',
694 controller='admin/repos', action='repo_settings_vcs_update',
698 conditions={'method': ['POST'], 'function': check_repo},
695 conditions={'method': ['POST'], 'function': check_repo},
699 requirements=URL_NAME_REQUIREMENTS)
696 requirements=URL_NAME_REQUIREMENTS)
700 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
697 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
701 controller='admin/repos', action='repo_settings_vcs',
698 controller='admin/repos', action='repo_settings_vcs',
702 conditions={'method': ['GET'], 'function': check_repo},
699 conditions={'method': ['GET'], 'function': check_repo},
703 requirements=URL_NAME_REQUIREMENTS)
700 requirements=URL_NAME_REQUIREMENTS)
704 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
701 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
705 controller='admin/repos', action='repo_delete_svn_pattern',
702 controller='admin/repos', action='repo_delete_svn_pattern',
706 conditions={'method': ['DELETE'], 'function': check_repo},
703 conditions={'method': ['DELETE'], 'function': check_repo},
707 requirements=URL_NAME_REQUIREMENTS)
704 requirements=URL_NAME_REQUIREMENTS)
708 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
705 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
709 controller='admin/repos', action='repo_settings_pullrequest',
706 controller='admin/repos', action='repo_settings_pullrequest',
710 conditions={'method': ['GET', 'POST'], 'function': check_repo},
707 conditions={'method': ['GET', 'POST'], 'function': check_repo},
711 requirements=URL_NAME_REQUIREMENTS)
708 requirements=URL_NAME_REQUIREMENTS)
712
709
713 # still working url for backward compat.
710 # still working url for backward compat.
714 rmap.connect('raw_changeset_home_depraced',
711 rmap.connect('raw_changeset_home_depraced',
715 '/{repo_name}/raw-changeset/{revision}',
712 '/{repo_name}/raw-changeset/{revision}',
716 controller='changeset', action='changeset_raw',
713 controller='changeset', action='changeset_raw',
717 revision='tip', conditions={'function': check_repo},
714 revision='tip', conditions={'function': check_repo},
718 requirements=URL_NAME_REQUIREMENTS)
715 requirements=URL_NAME_REQUIREMENTS)
719
716
720 # new URLs
717 # new URLs
721 rmap.connect('changeset_raw_home',
718 rmap.connect('changeset_raw_home',
722 '/{repo_name}/changeset-diff/{revision}',
719 '/{repo_name}/changeset-diff/{revision}',
723 controller='changeset', action='changeset_raw',
720 controller='changeset', action='changeset_raw',
724 revision='tip', conditions={'function': check_repo},
721 revision='tip', conditions={'function': check_repo},
725 requirements=URL_NAME_REQUIREMENTS)
722 requirements=URL_NAME_REQUIREMENTS)
726
723
727 rmap.connect('changeset_patch_home',
724 rmap.connect('changeset_patch_home',
728 '/{repo_name}/changeset-patch/{revision}',
725 '/{repo_name}/changeset-patch/{revision}',
729 controller='changeset', action='changeset_patch',
726 controller='changeset', action='changeset_patch',
730 revision='tip', conditions={'function': check_repo},
727 revision='tip', conditions={'function': check_repo},
731 requirements=URL_NAME_REQUIREMENTS)
728 requirements=URL_NAME_REQUIREMENTS)
732
729
733 rmap.connect('changeset_download_home',
730 rmap.connect('changeset_download_home',
734 '/{repo_name}/changeset-download/{revision}',
731 '/{repo_name}/changeset-download/{revision}',
735 controller='changeset', action='changeset_download',
732 controller='changeset', action='changeset_download',
736 revision='tip', conditions={'function': check_repo},
733 revision='tip', conditions={'function': check_repo},
737 requirements=URL_NAME_REQUIREMENTS)
734 requirements=URL_NAME_REQUIREMENTS)
738
735
739 rmap.connect('changeset_comment',
736 rmap.connect('changeset_comment',
740 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
737 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
741 controller='changeset', revision='tip', action='comment',
738 controller='changeset', revision='tip', action='comment',
742 conditions={'function': check_repo},
739 conditions={'function': check_repo},
743 requirements=URL_NAME_REQUIREMENTS)
740 requirements=URL_NAME_REQUIREMENTS)
744
741
745 rmap.connect('changeset_comment_preview',
742 rmap.connect('changeset_comment_preview',
746 '/{repo_name}/changeset/comment/preview', jsroute=True,
743 '/{repo_name}/changeset/comment/preview', jsroute=True,
747 controller='changeset', action='preview_comment',
744 controller='changeset', action='preview_comment',
748 conditions={'function': check_repo, 'method': ['POST']},
745 conditions={'function': check_repo, 'method': ['POST']},
749 requirements=URL_NAME_REQUIREMENTS)
746 requirements=URL_NAME_REQUIREMENTS)
750
747
751 rmap.connect('changeset_comment_delete',
748 rmap.connect('changeset_comment_delete',
752 '/{repo_name}/changeset/comment/{comment_id}/delete',
749 '/{repo_name}/changeset/comment/{comment_id}/delete',
753 controller='changeset', action='delete_comment',
750 controller='changeset', action='delete_comment',
754 conditions={'function': check_repo, 'method': ['DELETE']},
751 conditions={'function': check_repo, 'method': ['DELETE']},
755 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
752 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
756
753
757 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
754 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
758 controller='changeset', action='changeset_info',
755 controller='changeset', action='changeset_info',
759 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
756 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
760
757
761 rmap.connect('compare_home',
758 rmap.connect('compare_home',
762 '/{repo_name}/compare',
759 '/{repo_name}/compare',
763 controller='compare', action='index',
760 controller='compare', action='index',
764 conditions={'function': check_repo},
761 conditions={'function': check_repo},
765 requirements=URL_NAME_REQUIREMENTS)
762 requirements=URL_NAME_REQUIREMENTS)
766
763
767 rmap.connect('compare_url',
764 rmap.connect('compare_url',
768 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
765 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
769 controller='compare', action='compare',
766 controller='compare', action='compare',
770 conditions={'function': check_repo},
767 conditions={'function': check_repo},
771 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
768 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
772
769
773 rmap.connect('pullrequest_home',
770 rmap.connect('pullrequest_home',
774 '/{repo_name}/pull-request/new', controller='pullrequests',
771 '/{repo_name}/pull-request/new', controller='pullrequests',
775 action='index', conditions={'function': check_repo,
772 action='index', conditions={'function': check_repo,
776 'method': ['GET']},
773 'method': ['GET']},
777 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
774 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
778
775
779 rmap.connect('pullrequest',
776 rmap.connect('pullrequest',
780 '/{repo_name}/pull-request/new', controller='pullrequests',
777 '/{repo_name}/pull-request/new', controller='pullrequests',
781 action='create', conditions={'function': check_repo,
778 action='create', conditions={'function': check_repo,
782 'method': ['POST']},
779 'method': ['POST']},
783 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
780 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
784
781
785 rmap.connect('pullrequest_repo_refs',
782 rmap.connect('pullrequest_repo_refs',
786 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
783 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
787 controller='pullrequests',
784 controller='pullrequests',
788 action='get_repo_refs',
785 action='get_repo_refs',
789 conditions={'function': check_repo, 'method': ['GET']},
786 conditions={'function': check_repo, 'method': ['GET']},
790 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
787 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
791
788
792 rmap.connect('pullrequest_repo_destinations',
789 rmap.connect('pullrequest_repo_destinations',
793 '/{repo_name}/pull-request/repo-destinations',
790 '/{repo_name}/pull-request/repo-destinations',
794 controller='pullrequests',
791 controller='pullrequests',
795 action='get_repo_destinations',
792 action='get_repo_destinations',
796 conditions={'function': check_repo, 'method': ['GET']},
793 conditions={'function': check_repo, 'method': ['GET']},
797 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
794 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
798
795
799 rmap.connect('pullrequest_show',
796 rmap.connect('pullrequest_show',
800 '/{repo_name}/pull-request/{pull_request_id}',
797 '/{repo_name}/pull-request/{pull_request_id}',
801 controller='pullrequests',
798 controller='pullrequests',
802 action='show', conditions={'function': check_repo,
799 action='show', conditions={'function': check_repo,
803 'method': ['GET']},
800 'method': ['GET']},
804 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
801 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
805
802
806 rmap.connect('pullrequest_update',
803 rmap.connect('pullrequest_update',
807 '/{repo_name}/pull-request/{pull_request_id}',
804 '/{repo_name}/pull-request/{pull_request_id}',
808 controller='pullrequests',
805 controller='pullrequests',
809 action='update', conditions={'function': check_repo,
806 action='update', conditions={'function': check_repo,
810 'method': ['PUT']},
807 'method': ['PUT']},
811 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
808 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
812
809
813 rmap.connect('pullrequest_merge',
810 rmap.connect('pullrequest_merge',
814 '/{repo_name}/pull-request/{pull_request_id}',
811 '/{repo_name}/pull-request/{pull_request_id}',
815 controller='pullrequests',
812 controller='pullrequests',
816 action='merge', conditions={'function': check_repo,
813 action='merge', conditions={'function': check_repo,
817 'method': ['POST']},
814 'method': ['POST']},
818 requirements=URL_NAME_REQUIREMENTS)
815 requirements=URL_NAME_REQUIREMENTS)
819
816
820 rmap.connect('pullrequest_delete',
817 rmap.connect('pullrequest_delete',
821 '/{repo_name}/pull-request/{pull_request_id}',
818 '/{repo_name}/pull-request/{pull_request_id}',
822 controller='pullrequests',
819 controller='pullrequests',
823 action='delete', conditions={'function': check_repo,
820 action='delete', conditions={'function': check_repo,
824 'method': ['DELETE']},
821 'method': ['DELETE']},
825 requirements=URL_NAME_REQUIREMENTS)
822 requirements=URL_NAME_REQUIREMENTS)
826
823
827 rmap.connect('pullrequest_comment',
824 rmap.connect('pullrequest_comment',
828 '/{repo_name}/pull-request-comment/{pull_request_id}',
825 '/{repo_name}/pull-request-comment/{pull_request_id}',
829 controller='pullrequests',
826 controller='pullrequests',
830 action='comment', conditions={'function': check_repo,
827 action='comment', conditions={'function': check_repo,
831 'method': ['POST']},
828 'method': ['POST']},
832 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
829 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
833
830
834 rmap.connect('pullrequest_comment_delete',
831 rmap.connect('pullrequest_comment_delete',
835 '/{repo_name}/pull-request-comment/{comment_id}/delete',
832 '/{repo_name}/pull-request-comment/{comment_id}/delete',
836 controller='pullrequests', action='delete_comment',
833 controller='pullrequests', action='delete_comment',
837 conditions={'function': check_repo, 'method': ['DELETE']},
834 conditions={'function': check_repo, 'method': ['DELETE']},
838 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
835 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
839
836
840 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
837 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
841 controller='summary', conditions={'function': check_repo},
838 controller='summary', conditions={'function': check_repo},
842 requirements=URL_NAME_REQUIREMENTS)
839 requirements=URL_NAME_REQUIREMENTS)
843
840
844 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
841 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
845 controller='changelog', conditions={'function': check_repo},
842 controller='changelog', conditions={'function': check_repo},
846 requirements=URL_NAME_REQUIREMENTS)
843 requirements=URL_NAME_REQUIREMENTS)
847
844
848 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
845 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
849 controller='changelog', action='changelog_summary',
846 controller='changelog', action='changelog_summary',
850 conditions={'function': check_repo},
847 conditions={'function': check_repo},
851 requirements=URL_NAME_REQUIREMENTS)
848 requirements=URL_NAME_REQUIREMENTS)
852
849
853 rmap.connect('changelog_file_home',
850 rmap.connect('changelog_file_home',
854 '/{repo_name}/changelog/{revision}/{f_path}',
851 '/{repo_name}/changelog/{revision}/{f_path}',
855 controller='changelog', f_path=None,
852 controller='changelog', f_path=None,
856 conditions={'function': check_repo},
853 conditions={'function': check_repo},
857 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
854 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
858
855
859 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
856 rmap.connect('changelog_elements', '/{repo_name}/changelog_details',
860 controller='changelog', action='changelog_elements',
857 controller='changelog', action='changelog_elements',
861 conditions={'function': check_repo},
858 conditions={'function': check_repo},
862 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
859 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
863
860
864 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
861 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
865 controller='files', revision='tip', f_path='',
862 controller='files', revision='tip', f_path='',
866 conditions={'function': check_repo},
863 conditions={'function': check_repo},
867 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
864 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
868
865
869 rmap.connect('files_home_simple_catchrev',
866 rmap.connect('files_home_simple_catchrev',
870 '/{repo_name}/files/{revision}',
867 '/{repo_name}/files/{revision}',
871 controller='files', revision='tip', f_path='',
868 controller='files', revision='tip', f_path='',
872 conditions={'function': check_repo},
869 conditions={'function': check_repo},
873 requirements=URL_NAME_REQUIREMENTS)
870 requirements=URL_NAME_REQUIREMENTS)
874
871
875 rmap.connect('files_home_simple_catchall',
872 rmap.connect('files_home_simple_catchall',
876 '/{repo_name}/files',
873 '/{repo_name}/files',
877 controller='files', revision='tip', f_path='',
874 controller='files', revision='tip', f_path='',
878 conditions={'function': check_repo},
875 conditions={'function': check_repo},
879 requirements=URL_NAME_REQUIREMENTS)
876 requirements=URL_NAME_REQUIREMENTS)
880
877
881 rmap.connect('files_history_home',
878 rmap.connect('files_history_home',
882 '/{repo_name}/history/{revision}/{f_path}',
879 '/{repo_name}/history/{revision}/{f_path}',
883 controller='files', action='history', revision='tip', f_path='',
880 controller='files', action='history', revision='tip', f_path='',
884 conditions={'function': check_repo},
881 conditions={'function': check_repo},
885 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
882 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
886
883
887 rmap.connect('files_authors_home',
884 rmap.connect('files_authors_home',
888 '/{repo_name}/authors/{revision}/{f_path}',
885 '/{repo_name}/authors/{revision}/{f_path}',
889 controller='files', action='authors', revision='tip', f_path='',
886 controller='files', action='authors', revision='tip', f_path='',
890 conditions={'function': check_repo},
887 conditions={'function': check_repo},
891 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
888 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
892
889
893 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
890 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
894 controller='files', action='diff', f_path='',
891 controller='files', action='diff', f_path='',
895 conditions={'function': check_repo},
892 conditions={'function': check_repo},
896 requirements=URL_NAME_REQUIREMENTS)
893 requirements=URL_NAME_REQUIREMENTS)
897
894
898 rmap.connect('files_diff_2way_home',
895 rmap.connect('files_diff_2way_home',
899 '/{repo_name}/diff-2way/{f_path}',
896 '/{repo_name}/diff-2way/{f_path}',
900 controller='files', action='diff_2way', f_path='',
897 controller='files', action='diff_2way', f_path='',
901 conditions={'function': check_repo},
898 conditions={'function': check_repo},
902 requirements=URL_NAME_REQUIREMENTS)
899 requirements=URL_NAME_REQUIREMENTS)
903
900
904 rmap.connect('files_rawfile_home',
901 rmap.connect('files_rawfile_home',
905 '/{repo_name}/rawfile/{revision}/{f_path}',
902 '/{repo_name}/rawfile/{revision}/{f_path}',
906 controller='files', action='rawfile', revision='tip',
903 controller='files', action='rawfile', revision='tip',
907 f_path='', conditions={'function': check_repo},
904 f_path='', conditions={'function': check_repo},
908 requirements=URL_NAME_REQUIREMENTS)
905 requirements=URL_NAME_REQUIREMENTS)
909
906
910 rmap.connect('files_raw_home',
907 rmap.connect('files_raw_home',
911 '/{repo_name}/raw/{revision}/{f_path}',
908 '/{repo_name}/raw/{revision}/{f_path}',
912 controller='files', action='raw', revision='tip', f_path='',
909 controller='files', action='raw', revision='tip', f_path='',
913 conditions={'function': check_repo},
910 conditions={'function': check_repo},
914 requirements=URL_NAME_REQUIREMENTS)
911 requirements=URL_NAME_REQUIREMENTS)
915
912
916 rmap.connect('files_render_home',
913 rmap.connect('files_render_home',
917 '/{repo_name}/render/{revision}/{f_path}',
914 '/{repo_name}/render/{revision}/{f_path}',
918 controller='files', action='index', revision='tip', f_path='',
915 controller='files', action='index', revision='tip', f_path='',
919 rendered=True, conditions={'function': check_repo},
916 rendered=True, conditions={'function': check_repo},
920 requirements=URL_NAME_REQUIREMENTS)
917 requirements=URL_NAME_REQUIREMENTS)
921
918
922 rmap.connect('files_annotate_home',
919 rmap.connect('files_annotate_home',
923 '/{repo_name}/annotate/{revision}/{f_path}',
920 '/{repo_name}/annotate/{revision}/{f_path}',
924 controller='files', action='index', revision='tip',
921 controller='files', action='index', revision='tip',
925 f_path='', annotate=True, conditions={'function': check_repo},
922 f_path='', annotate=True, conditions={'function': check_repo},
926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
923 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927
924
928 rmap.connect('files_annotate_previous',
925 rmap.connect('files_annotate_previous',
929 '/{repo_name}/annotate-previous/{revision}/{f_path}',
926 '/{repo_name}/annotate-previous/{revision}/{f_path}',
930 controller='files', action='annotate_previous', revision='tip',
927 controller='files', action='annotate_previous', revision='tip',
931 f_path='', annotate=True, conditions={'function': check_repo},
928 f_path='', annotate=True, conditions={'function': check_repo},
932 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
929 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
933
930
934 rmap.connect('files_edit',
931 rmap.connect('files_edit',
935 '/{repo_name}/edit/{revision}/{f_path}',
932 '/{repo_name}/edit/{revision}/{f_path}',
936 controller='files', action='edit', revision='tip',
933 controller='files', action='edit', revision='tip',
937 f_path='',
934 f_path='',
938 conditions={'function': check_repo, 'method': ['POST']},
935 conditions={'function': check_repo, 'method': ['POST']},
939 requirements=URL_NAME_REQUIREMENTS)
936 requirements=URL_NAME_REQUIREMENTS)
940
937
941 rmap.connect('files_edit_home',
938 rmap.connect('files_edit_home',
942 '/{repo_name}/edit/{revision}/{f_path}',
939 '/{repo_name}/edit/{revision}/{f_path}',
943 controller='files', action='edit_home', revision='tip',
940 controller='files', action='edit_home', revision='tip',
944 f_path='', conditions={'function': check_repo},
941 f_path='', conditions={'function': check_repo},
945 requirements=URL_NAME_REQUIREMENTS)
942 requirements=URL_NAME_REQUIREMENTS)
946
943
947 rmap.connect('files_add',
944 rmap.connect('files_add',
948 '/{repo_name}/add/{revision}/{f_path}',
945 '/{repo_name}/add/{revision}/{f_path}',
949 controller='files', action='add', revision='tip',
946 controller='files', action='add', revision='tip',
950 f_path='',
947 f_path='',
951 conditions={'function': check_repo, 'method': ['POST']},
948 conditions={'function': check_repo, 'method': ['POST']},
952 requirements=URL_NAME_REQUIREMENTS)
949 requirements=URL_NAME_REQUIREMENTS)
953
950
954 rmap.connect('files_add_home',
951 rmap.connect('files_add_home',
955 '/{repo_name}/add/{revision}/{f_path}',
952 '/{repo_name}/add/{revision}/{f_path}',
956 controller='files', action='add_home', revision='tip',
953 controller='files', action='add_home', revision='tip',
957 f_path='', conditions={'function': check_repo},
954 f_path='', conditions={'function': check_repo},
958 requirements=URL_NAME_REQUIREMENTS)
955 requirements=URL_NAME_REQUIREMENTS)
959
956
960 rmap.connect('files_delete',
957 rmap.connect('files_delete',
961 '/{repo_name}/delete/{revision}/{f_path}',
958 '/{repo_name}/delete/{revision}/{f_path}',
962 controller='files', action='delete', revision='tip',
959 controller='files', action='delete', revision='tip',
963 f_path='',
960 f_path='',
964 conditions={'function': check_repo, 'method': ['POST']},
961 conditions={'function': check_repo, 'method': ['POST']},
965 requirements=URL_NAME_REQUIREMENTS)
962 requirements=URL_NAME_REQUIREMENTS)
966
963
967 rmap.connect('files_delete_home',
964 rmap.connect('files_delete_home',
968 '/{repo_name}/delete/{revision}/{f_path}',
965 '/{repo_name}/delete/{revision}/{f_path}',
969 controller='files', action='delete_home', revision='tip',
966 controller='files', action='delete_home', revision='tip',
970 f_path='', conditions={'function': check_repo},
967 f_path='', conditions={'function': check_repo},
971 requirements=URL_NAME_REQUIREMENTS)
968 requirements=URL_NAME_REQUIREMENTS)
972
969
973 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
970 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
974 controller='files', action='archivefile',
971 controller='files', action='archivefile',
975 conditions={'function': check_repo},
972 conditions={'function': check_repo},
976 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
973 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
977
974
978 rmap.connect('files_nodelist_home',
975 rmap.connect('files_nodelist_home',
979 '/{repo_name}/nodelist/{revision}/{f_path}',
976 '/{repo_name}/nodelist/{revision}/{f_path}',
980 controller='files', action='nodelist',
977 controller='files', action='nodelist',
981 conditions={'function': check_repo},
978 conditions={'function': check_repo},
982 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
979 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
983
980
984 rmap.connect('files_nodetree_full',
981 rmap.connect('files_nodetree_full',
985 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
982 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
986 controller='files', action='nodetree_full',
983 controller='files', action='nodetree_full',
987 conditions={'function': check_repo},
984 conditions={'function': check_repo},
988 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
985 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
989
986
990 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
987 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
991 controller='forks', action='fork_create',
988 controller='forks', action='fork_create',
992 conditions={'function': check_repo, 'method': ['POST']},
989 conditions={'function': check_repo, 'method': ['POST']},
993 requirements=URL_NAME_REQUIREMENTS)
990 requirements=URL_NAME_REQUIREMENTS)
994
991
995 rmap.connect('repo_fork_home', '/{repo_name}/fork',
992 rmap.connect('repo_fork_home', '/{repo_name}/fork',
996 controller='forks', action='fork',
993 controller='forks', action='fork',
997 conditions={'function': check_repo},
994 conditions={'function': check_repo},
998 requirements=URL_NAME_REQUIREMENTS)
995 requirements=URL_NAME_REQUIREMENTS)
999
996
1000 rmap.connect('repo_forks_home', '/{repo_name}/forks',
997 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1001 controller='forks', action='forks',
998 controller='forks', action='forks',
1002 conditions={'function': check_repo},
999 conditions={'function': check_repo},
1003 requirements=URL_NAME_REQUIREMENTS)
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 # catch all, at the end
1002 # catch all, at the end
1013 _connect_with_slash(
1003 _connect_with_slash(
1014 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1004 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1015 controller='summary', action='index',
1005 controller='summary', action='index',
1016 conditions={'function': check_repo},
1006 conditions={'function': check_repo},
1017 requirements=URL_NAME_REQUIREMENTS)
1007 requirements=URL_NAME_REQUIREMENTS)
1018
1008
1019 return rmap
1009 return rmap
1020
1010
1021
1011
1022 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1012 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1023 """
1013 """
1024 Connect a route with an optional trailing slash in `path`.
1014 Connect a route with an optional trailing slash in `path`.
1025 """
1015 """
1026 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1016 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1027 mapper.connect(name, path, *args, **kwargs)
1017 mapper.connect(name, path, *args, **kwargs)
@@ -1,406 +1,404 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Repository groups controller for RhodeCode
23 Repository groups controller for RhodeCode
24 """
24 """
25
25
26 import logging
26 import logging
27 import formencode
27 import formencode
28
28
29 from formencode import htmlfill
29 from formencode import htmlfill
30
30
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import abort, redirect
32 from pylons.controllers.util import abort, redirect
33 from pylons.i18n.translation import _, ungettext
33 from pylons.i18n.translation import _, ungettext
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, NotAnonymous, HasPermissionAll,
39 LoginRequired, NotAnonymous, HasPermissionAll,
40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 from rhodecode.lib.base import BaseController, render
41 from rhodecode.lib.base import BaseController, render
42 from rhodecode.model.db import RepoGroup, User
42 from rhodecode.model.db import RepoGroup, User
43 from rhodecode.model.scm import RepoGroupList
43 from rhodecode.model.scm import RepoGroupList
44 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.lib.utils2 import safe_int
47 from rhodecode.lib.utils2 import safe_int
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class RepoGroupsController(BaseController):
53 class RepoGroupsController(BaseController):
54 """REST Controller styled on the Atom Publishing Protocol"""
54 """REST Controller styled on the Atom Publishing Protocol"""
55
55
56 @LoginRequired()
56 @LoginRequired()
57 def __before__(self):
57 def __before__(self):
58 super(RepoGroupsController, self).__before__()
58 super(RepoGroupsController, self).__before__()
59
59
60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
60 def __load_defaults(self, allow_empty_group=False, repo_group=None):
61 if self._can_create_repo_group():
61 if self._can_create_repo_group():
62 # we're global admin, we're ok and we can create TOP level groups
62 # we're global admin, we're ok and we can create TOP level groups
63 allow_empty_group = True
63 allow_empty_group = True
64
64
65 # override the choices for this form, we need to filter choices
65 # override the choices for this form, we need to filter choices
66 # and display only those we have ADMIN right
66 # and display only those we have ADMIN right
67 groups_with_admin_rights = RepoGroupList(
67 groups_with_admin_rights = RepoGroupList(
68 RepoGroup.query().all(),
68 RepoGroup.query().all(),
69 perm_set=['group.admin'])
69 perm_set=['group.admin'])
70 c.repo_groups = RepoGroup.groups_choices(
70 c.repo_groups = RepoGroup.groups_choices(
71 groups=groups_with_admin_rights,
71 groups=groups_with_admin_rights,
72 show_empty_group=allow_empty_group)
72 show_empty_group=allow_empty_group)
73
73
74 if repo_group:
74 if repo_group:
75 # exclude filtered ids
75 # exclude filtered ids
76 exclude_group_ids = [repo_group.group_id]
76 exclude_group_ids = [repo_group.group_id]
77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
77 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
78 c.repo_groups)
78 c.repo_groups)
79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
79 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
80 parent_group = repo_group.parent_group
80 parent_group = repo_group.parent_group
81
81
82 add_parent_group = (parent_group and (
82 add_parent_group = (parent_group and (
83 unicode(parent_group.group_id) not in c.repo_groups_choices))
83 unicode(parent_group.group_id) not in c.repo_groups_choices))
84 if add_parent_group:
84 if add_parent_group:
85 c.repo_groups_choices.append(unicode(parent_group.group_id))
85 c.repo_groups_choices.append(unicode(parent_group.group_id))
86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
86 c.repo_groups.append(RepoGroup._generate_choice(parent_group))
87
87
88 def __load_data(self, group_id):
88 def __load_data(self, group_id):
89 """
89 """
90 Load defaults settings for edit, and update
90 Load defaults settings for edit, and update
91
91
92 :param group_id:
92 :param group_id:
93 """
93 """
94 repo_group = RepoGroup.get_or_404(group_id)
94 repo_group = RepoGroup.get_or_404(group_id)
95 data = repo_group.get_dict()
95 data = repo_group.get_dict()
96 data['group_name'] = repo_group.name
96 data['group_name'] = repo_group.name
97
97
98 # fill owner
98 # fill owner
99 if repo_group.user:
99 if repo_group.user:
100 data.update({'user': repo_group.user.username})
100 data.update({'user': repo_group.user.username})
101 else:
101 else:
102 replacement_user = User.get_first_super_admin().username
102 replacement_user = User.get_first_super_admin().username
103 data.update({'user': replacement_user})
103 data.update({'user': replacement_user})
104
104
105 # fill repository group users
105 # fill repository group users
106 for p in repo_group.repo_group_to_perm:
106 for p in repo_group.repo_group_to_perm:
107 data.update({
107 data.update({
108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
108 'u_perm_%s' % p.user.user_id: p.permission.permission_name})
109
109
110 # fill repository group user groups
110 # fill repository group user groups
111 for p in repo_group.users_group_to_perm:
111 for p in repo_group.users_group_to_perm:
112 data.update({
112 data.update({
113 'g_perm_%s' % p.users_group.users_group_id:
113 'g_perm_%s' % p.users_group.users_group_id:
114 p.permission.permission_name})
114 p.permission.permission_name})
115 # html and form expects -1 as empty parent group
115 # html and form expects -1 as empty parent group
116 data['group_parent_id'] = data['group_parent_id'] or -1
116 data['group_parent_id'] = data['group_parent_id'] or -1
117 return data
117 return data
118
118
119 def _revoke_perms_on_yourself(self, form_result):
119 def _revoke_perms_on_yourself(self, form_result):
120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
120 _updates = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
121 form_result['perm_updates'])
121 form_result['perm_updates'])
122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
122 _additions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
123 form_result['perm_additions'])
123 form_result['perm_additions'])
124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
124 _deletions = filter(lambda u: c.rhodecode_user.user_id == int(u[0]),
125 form_result['perm_deletions'])
125 form_result['perm_deletions'])
126 admin_perm = 'group.admin'
126 admin_perm = 'group.admin'
127 if _updates and _updates[0][1] != admin_perm or \
127 if _updates and _updates[0][1] != admin_perm or \
128 _additions and _additions[0][1] != admin_perm or \
128 _additions and _additions[0][1] != admin_perm or \
129 _deletions and _deletions[0][1] != admin_perm:
129 _deletions and _deletions[0][1] != admin_perm:
130 return True
130 return True
131 return False
131 return False
132
132
133 def _can_create_repo_group(self, parent_group_id=None):
133 def _can_create_repo_group(self, parent_group_id=None):
134 is_admin = HasPermissionAll('hg.admin')('group create controller')
134 is_admin = HasPermissionAll('hg.admin')('group create controller')
135 create_repo_group = HasPermissionAll(
135 create_repo_group = HasPermissionAll(
136 'hg.repogroup.create.true')('group create controller')
136 'hg.repogroup.create.true')('group create controller')
137 if is_admin or (create_repo_group and not parent_group_id):
137 if is_admin or (create_repo_group and not parent_group_id):
138 # we're global admin, or we have global repo group create
138 # we're global admin, or we have global repo group create
139 # permission
139 # permission
140 # we're ok and we can create TOP level groups
140 # we're ok and we can create TOP level groups
141 return True
141 return True
142 elif parent_group_id:
142 elif parent_group_id:
143 # we check the permission if we can write to parent group
143 # we check the permission if we can write to parent group
144 group = RepoGroup.get(parent_group_id)
144 group = RepoGroup.get(parent_group_id)
145 group_name = group.group_name if group else None
145 group_name = group.group_name if group else None
146 if HasRepoGroupPermissionAll('group.admin')(
146 if HasRepoGroupPermissionAll('group.admin')(
147 group_name, 'check if user is an admin of group'):
147 group_name, 'check if user is an admin of group'):
148 # we're an admin of passed in group, we're ok.
148 # we're an admin of passed in group, we're ok.
149 return True
149 return True
150 else:
150 else:
151 return False
151 return False
152 return False
152 return False
153
153
154 @NotAnonymous()
154 @NotAnonymous()
155 def index(self):
155 def index(self):
156 """GET /repo_groups: All items in the collection"""
156 """GET /repo_groups: All items in the collection"""
157 # url('repo_groups')
157 # url('repo_groups')
158
158
159 repo_group_list = RepoGroup.get_all_repo_groups()
159 repo_group_list = RepoGroup.get_all_repo_groups()
160 _perms = ['group.admin']
160 _perms = ['group.admin']
161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
162 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
163 repo_group_list=repo_group_list_acl, admin=True)
163 repo_group_list=repo_group_list_acl, admin=True)
164 c.data = json.dumps(repo_group_data)
164 c.data = json.dumps(repo_group_data)
165 return render('admin/repo_groups/repo_groups.mako')
165 return render('admin/repo_groups/repo_groups.mako')
166
166
167 # perm checks inside
167 # perm checks inside
168 @NotAnonymous()
168 @NotAnonymous()
169 @auth.CSRFRequired()
169 @auth.CSRFRequired()
170 def create(self):
170 def create(self):
171 """POST /repo_groups: Create a new item"""
171 """POST /repo_groups: Create a new item"""
172 # url('repo_groups')
172 # url('repo_groups')
173
173
174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
175 can_create = self._can_create_repo_group(parent_group_id)
175 can_create = self._can_create_repo_group(parent_group_id)
176
176
177 self.__load_defaults()
177 self.__load_defaults()
178 # permissions for can create group based on parent_id are checked
178 # permissions for can create group based on parent_id are checked
179 # here in the Form
179 # here in the Form
180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
180 available_groups = map(lambda k: unicode(k[0]), c.repo_groups)
181 repo_group_form = RepoGroupForm(available_groups=available_groups,
181 repo_group_form = RepoGroupForm(available_groups=available_groups,
182 can_create_in_root=can_create)()
182 can_create_in_root=can_create)()
183 try:
183 try:
184 owner = c.rhodecode_user
184 owner = c.rhodecode_user
185 form_result = repo_group_form.to_python(dict(request.POST))
185 form_result = repo_group_form.to_python(dict(request.POST))
186 RepoGroupModel().create(
186 RepoGroupModel().create(
187 group_name=form_result['group_name_full'],
187 group_name=form_result['group_name_full'],
188 group_description=form_result['group_description'],
188 group_description=form_result['group_description'],
189 owner=owner.user_id,
189 owner=owner.user_id,
190 copy_permissions=form_result['group_copy_permissions']
190 copy_permissions=form_result['group_copy_permissions']
191 )
191 )
192 Session().commit()
192 Session().commit()
193 _new_group_name = form_result['group_name_full']
193 _new_group_name = form_result['group_name_full']
194 repo_group_url = h.link_to(
194 repo_group_url = h.link_to(
195 _new_group_name,
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 h.flash(h.literal(_('Created repository group %s')
197 h.flash(h.literal(_('Created repository group %s')
198 % repo_group_url), category='success')
198 % repo_group_url), category='success')
199 # TODO: in futureaction_logger(, '', '', '', self.sa)
199 # TODO: in future action_logger(, '', '', '', self.sa)
200 except formencode.Invalid as errors:
200 except formencode.Invalid as errors:
201 return htmlfill.render(
201 return htmlfill.render(
202 render('admin/repo_groups/repo_group_add.mako'),
202 render('admin/repo_groups/repo_group_add.mako'),
203 defaults=errors.value,
203 defaults=errors.value,
204 errors=errors.error_dict or {},
204 errors=errors.error_dict or {},
205 prefix_error=False,
205 prefix_error=False,
206 encoding="UTF-8",
206 encoding="UTF-8",
207 force_defaults=False)
207 force_defaults=False)
208 except Exception:
208 except Exception:
209 log.exception("Exception during creation of repository group")
209 log.exception("Exception during creation of repository group")
210 h.flash(_('Error occurred during creation of repository group %s')
210 h.flash(_('Error occurred during creation of repository group %s')
211 % request.POST.get('group_name'), category='error')
211 % request.POST.get('group_name'), category='error')
212
212
213 # TODO: maybe we should get back to the main view, not the admin one
213 # TODO: maybe we should get back to the main view, not the admin one
214 return redirect(url('repo_groups', parent_group=parent_group_id))
214 return redirect(url('repo_groups', parent_group=parent_group_id))
215
215
216 # perm checks inside
216 # perm checks inside
217 @NotAnonymous()
217 @NotAnonymous()
218 def new(self):
218 def new(self):
219 """GET /repo_groups/new: Form to create a new item"""
219 """GET /repo_groups/new: Form to create a new item"""
220 # url('new_repo_group')
220 # url('new_repo_group')
221 # perm check for admin, create_group perm or admin of parent_group
221 # perm check for admin, create_group perm or admin of parent_group
222 parent_group_id = safe_int(request.GET.get('parent_group'))
222 parent_group_id = safe_int(request.GET.get('parent_group'))
223 if not self._can_create_repo_group(parent_group_id):
223 if not self._can_create_repo_group(parent_group_id):
224 return abort(403)
224 return abort(403)
225
225
226 self.__load_defaults()
226 self.__load_defaults()
227 return render('admin/repo_groups/repo_group_add.mako')
227 return render('admin/repo_groups/repo_group_add.mako')
228
228
229 @HasRepoGroupPermissionAnyDecorator('group.admin')
229 @HasRepoGroupPermissionAnyDecorator('group.admin')
230 @auth.CSRFRequired()
230 @auth.CSRFRequired()
231 def update(self, group_name):
231 def update(self, group_name):
232 """PUT /repo_groups/group_name: Update an existing item"""
232 """PUT /repo_groups/group_name: Update an existing item"""
233 # Forms posted to this method should contain a hidden field:
233 # Forms posted to this method should contain a hidden field:
234 # <input type="hidden" name="_method" value="PUT" />
234 # <input type="hidden" name="_method" value="PUT" />
235 # Or using helpers:
235 # Or using helpers:
236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
237 # url('repo_group_home', group_name=GROUP_NAME)
238
237
239 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
238 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
240 can_create_in_root = self._can_create_repo_group()
239 can_create_in_root = self._can_create_repo_group()
241 show_root_location = can_create_in_root
240 show_root_location = can_create_in_root
242 if not c.repo_group.parent_group:
241 if not c.repo_group.parent_group:
243 # this group don't have a parrent so we should show empty value
242 # this group don't have a parrent so we should show empty value
244 show_root_location = True
243 show_root_location = True
245 self.__load_defaults(allow_empty_group=show_root_location,
244 self.__load_defaults(allow_empty_group=show_root_location,
246 repo_group=c.repo_group)
245 repo_group=c.repo_group)
247
246
248 repo_group_form = RepoGroupForm(
247 repo_group_form = RepoGroupForm(
249 edit=True, old_data=c.repo_group.get_dict(),
248 edit=True, old_data=c.repo_group.get_dict(),
250 available_groups=c.repo_groups_choices,
249 available_groups=c.repo_groups_choices,
251 can_create_in_root=can_create_in_root, allow_disabled=True)()
250 can_create_in_root=can_create_in_root, allow_disabled=True)()
252
251
253 try:
252 try:
254 form_result = repo_group_form.to_python(dict(request.POST))
253 form_result = repo_group_form.to_python(dict(request.POST))
255 gr_name = form_result['group_name']
254 gr_name = form_result['group_name']
256 new_gr = RepoGroupModel().update(group_name, form_result)
255 new_gr = RepoGroupModel().update(group_name, form_result)
257 Session().commit()
256 Session().commit()
258 h.flash(_('Updated repository group %s') % (gr_name,),
257 h.flash(_('Updated repository group %s') % (gr_name,),
259 category='success')
258 category='success')
260 # we now have new name !
259 # we now have new name !
261 group_name = new_gr.group_name
260 group_name = new_gr.group_name
262 # TODO: in future action_logger(, '', '', '', self.sa)
261 # TODO: in future action_logger(, '', '', '', self.sa)
263 except formencode.Invalid as errors:
262 except formencode.Invalid as errors:
264 c.active = 'settings'
263 c.active = 'settings'
265 return htmlfill.render(
264 return htmlfill.render(
266 render('admin/repo_groups/repo_group_edit.mako'),
265 render('admin/repo_groups/repo_group_edit.mako'),
267 defaults=errors.value,
266 defaults=errors.value,
268 errors=errors.error_dict or {},
267 errors=errors.error_dict or {},
269 prefix_error=False,
268 prefix_error=False,
270 encoding="UTF-8",
269 encoding="UTF-8",
271 force_defaults=False)
270 force_defaults=False)
272 except Exception:
271 except Exception:
273 log.exception("Exception during update or repository group")
272 log.exception("Exception during update or repository group")
274 h.flash(_('Error occurred during update of repository group %s')
273 h.flash(_('Error occurred during update of repository group %s')
275 % request.POST.get('group_name'), category='error')
274 % request.POST.get('group_name'), category='error')
276
275
277 return redirect(url('edit_repo_group', group_name=group_name))
276 return redirect(url('edit_repo_group', group_name=group_name))
278
277
279 @HasRepoGroupPermissionAnyDecorator('group.admin')
278 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 @auth.CSRFRequired()
279 @auth.CSRFRequired()
281 def delete(self, group_name):
280 def delete(self, group_name):
282 """DELETE /repo_groups/group_name: Delete an existing item"""
281 """DELETE /repo_groups/group_name: Delete an existing item"""
283 # Forms posted to this method should contain a hidden field:
282 # Forms posted to this method should contain a hidden field:
284 # <input type="hidden" name="_method" value="DELETE" />
283 # <input type="hidden" name="_method" value="DELETE" />
285 # Or using helpers:
284 # Or using helpers:
286 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
285 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
287 # url('repo_group_home', group_name=GROUP_NAME)
288
286
289 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
287 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
290 repos = gr.repositories.all()
288 repos = gr.repositories.all()
291 if repos:
289 if repos:
292 msg = ungettext(
290 msg = ungettext(
293 'This group contains %(num)d repository and cannot be deleted',
291 'This group contains %(num)d repository and cannot be deleted',
294 'This group contains %(num)d repositories and cannot be'
292 'This group contains %(num)d repositories and cannot be'
295 ' deleted',
293 ' deleted',
296 len(repos)) % {'num': len(repos)}
294 len(repos)) % {'num': len(repos)}
297 h.flash(msg, category='warning')
295 h.flash(msg, category='warning')
298 return redirect(url('repo_groups'))
296 return redirect(url('repo_groups'))
299
297
300 children = gr.children.all()
298 children = gr.children.all()
301 if children:
299 if children:
302 msg = ungettext(
300 msg = ungettext(
303 'This group contains %(num)d subgroup and cannot be deleted',
301 'This group contains %(num)d subgroup and cannot be deleted',
304 'This group contains %(num)d subgroups and cannot be deleted',
302 'This group contains %(num)d subgroups and cannot be deleted',
305 len(children)) % {'num': len(children)}
303 len(children)) % {'num': len(children)}
306 h.flash(msg, category='warning')
304 h.flash(msg, category='warning')
307 return redirect(url('repo_groups'))
305 return redirect(url('repo_groups'))
308
306
309 try:
307 try:
310 RepoGroupModel().delete(group_name)
308 RepoGroupModel().delete(group_name)
311 Session().commit()
309 Session().commit()
312 h.flash(_('Removed repository group %s') % group_name,
310 h.flash(_('Removed repository group %s') % group_name,
313 category='success')
311 category='success')
314 # TODO: in future action_logger(, '', '', '', self.sa)
312 # TODO: in future action_logger(, '', '', '', self.sa)
315 except Exception:
313 except Exception:
316 log.exception("Exception during deletion of repository group")
314 log.exception("Exception during deletion of repository group")
317 h.flash(_('Error occurred during deletion of repository group %s')
315 h.flash(_('Error occurred during deletion of repository group %s')
318 % group_name, category='error')
316 % group_name, category='error')
319
317
320 return redirect(url('repo_groups'))
318 return redirect(url('repo_groups'))
321
319
322 @HasRepoGroupPermissionAnyDecorator('group.admin')
320 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 def edit(self, group_name):
321 def edit(self, group_name):
324 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
322 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
325 # url('edit_repo_group', group_name=GROUP_NAME)
323 # url('edit_repo_group', group_name=GROUP_NAME)
326 c.active = 'settings'
324 c.active = 'settings'
327
325
328 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
326 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
329 # we can only allow moving empty group if it's already a top-level
327 # we can only allow moving empty group if it's already a top-level
330 # group, ie has no parents, or we're admin
328 # group, ie has no parents, or we're admin
331 can_create_in_root = self._can_create_repo_group()
329 can_create_in_root = self._can_create_repo_group()
332 show_root_location = can_create_in_root
330 show_root_location = can_create_in_root
333 if not c.repo_group.parent_group:
331 if not c.repo_group.parent_group:
334 # this group don't have a parrent so we should show empty value
332 # this group don't have a parrent so we should show empty value
335 show_root_location = True
333 show_root_location = True
336 self.__load_defaults(allow_empty_group=show_root_location,
334 self.__load_defaults(allow_empty_group=show_root_location,
337 repo_group=c.repo_group)
335 repo_group=c.repo_group)
338 defaults = self.__load_data(c.repo_group.group_id)
336 defaults = self.__load_data(c.repo_group.group_id)
339
337
340 return htmlfill.render(
338 return htmlfill.render(
341 render('admin/repo_groups/repo_group_edit.mako'),
339 render('admin/repo_groups/repo_group_edit.mako'),
342 defaults=defaults,
340 defaults=defaults,
343 encoding="UTF-8",
341 encoding="UTF-8",
344 force_defaults=False
342 force_defaults=False
345 )
343 )
346
344
347 @HasRepoGroupPermissionAnyDecorator('group.admin')
345 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 def edit_repo_group_advanced(self, group_name):
346 def edit_repo_group_advanced(self, group_name):
349 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
347 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 # url('edit_repo_group', group_name=GROUP_NAME)
348 # url('edit_repo_group', group_name=GROUP_NAME)
351 c.active = 'advanced'
349 c.active = 'advanced'
352 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
350 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
353
351
354 return render('admin/repo_groups/repo_group_edit.mako')
352 return render('admin/repo_groups/repo_group_edit.mako')
355
353
356 @HasRepoGroupPermissionAnyDecorator('group.admin')
354 @HasRepoGroupPermissionAnyDecorator('group.admin')
357 def edit_repo_group_perms(self, group_name):
355 def edit_repo_group_perms(self, group_name):
358 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
356 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
359 # url('edit_repo_group', group_name=GROUP_NAME)
357 # url('edit_repo_group', group_name=GROUP_NAME)
360 c.active = 'perms'
358 c.active = 'perms'
361 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
359 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
362 self.__load_defaults()
360 self.__load_defaults()
363 defaults = self.__load_data(c.repo_group.group_id)
361 defaults = self.__load_data(c.repo_group.group_id)
364
362
365 return htmlfill.render(
363 return htmlfill.render(
366 render('admin/repo_groups/repo_group_edit.mako'),
364 render('admin/repo_groups/repo_group_edit.mako'),
367 defaults=defaults,
365 defaults=defaults,
368 encoding="UTF-8",
366 encoding="UTF-8",
369 force_defaults=False
367 force_defaults=False
370 )
368 )
371
369
372 @HasRepoGroupPermissionAnyDecorator('group.admin')
370 @HasRepoGroupPermissionAnyDecorator('group.admin')
373 @auth.CSRFRequired()
371 @auth.CSRFRequired()
374 def update_perms(self, group_name):
372 def update_perms(self, group_name):
375 """
373 """
376 Update permissions for given repository group
374 Update permissions for given repository group
377
375
378 :param group_name:
376 :param group_name:
379 """
377 """
380
378
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
379 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
380 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
383 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
381 form = RepoGroupPermsForm(valid_recursive_choices)().to_python(
384 request.POST)
382 request.POST)
385
383
386 if not c.rhodecode_user.is_admin:
384 if not c.rhodecode_user.is_admin:
387 if self._revoke_perms_on_yourself(form):
385 if self._revoke_perms_on_yourself(form):
388 msg = _('Cannot change permission for yourself as admin')
386 msg = _('Cannot change permission for yourself as admin')
389 h.flash(msg, category='warning')
387 h.flash(msg, category='warning')
390 return redirect(
388 return redirect(
391 url('edit_repo_group_perms', group_name=group_name))
389 url('edit_repo_group_perms', group_name=group_name))
392
390
393 # iterate over all members(if in recursive mode) of this groups and
391 # iterate over all members(if in recursive mode) of this groups and
394 # set the permissions !
392 # set the permissions !
395 # this can be potentially heavy operation
393 # this can be potentially heavy operation
396 RepoGroupModel().update_permissions(
394 RepoGroupModel().update_permissions(
397 c.repo_group,
395 c.repo_group,
398 form['perm_additions'], form['perm_updates'],
396 form['perm_additions'], form['perm_updates'],
399 form['perm_deletions'], form['recursive'])
397 form['perm_deletions'], form['recursive'])
400
398
401 # TODO: implement this
399 # TODO: implement this
402 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
400 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
403 # repo_name, self.ip_addr, self.sa)
401 # repo_name, self.ip_addr, self.sa)
404 Session().commit()
402 Session().commit()
405 h.flash(_('Repository Group permissions updated'), category='success')
403 h.flash(_('Repository Group permissions updated'), category='success')
406 return redirect(url('edit_repo_group_perms', group_name=group_name))
404 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -1,610 +1,610 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Repositories controller for RhodeCode
23 Repositories controller for RhodeCode
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import formencode
29 import formencode
30 from formencode import htmlfill
30 from formencode import htmlfill
31 from pylons import request, tmpl_context as c, url
31 from pylons import request, tmpl_context as c, url
32 from pylons.controllers.util import redirect
32 from pylons.controllers.util import redirect
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
34 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest
35
35
36 import rhodecode
36 import rhodecode
37 from rhodecode.lib import auth, helpers as h
37 from rhodecode.lib import auth, helpers as h
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator,
39 LoginRequired, HasPermissionAllDecorator,
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
40 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny,
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.exceptions import AttachedForksError
44 from rhodecode.lib.exceptions import AttachedForksError
45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
46 from rhodecode.lib.utils2 import safe_int, str2bool
46 from rhodecode.lib.utils2 import safe_int, str2bool
47 from rhodecode.lib.vcs import RepositoryError
47 from rhodecode.lib.vcs import RepositoryError
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
52 IssueTrackerPatternsForm)
52 IssueTrackerPatternsForm)
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.repo import RepoModel
54 from rhodecode.model.repo import RepoModel
55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
56 from rhodecode.model.settings import (
56 from rhodecode.model.settings import (
57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
57 SettingsModel, IssueTrackerSettingsModel, VcsSettingsModel,
58 SettingNotFound)
58 SettingNotFound)
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class ReposController(BaseRepoController):
63 class ReposController(BaseRepoController):
64 """
64 """
65 REST Controller styled on the Atom Publishing Protocol"""
65 REST Controller styled on the Atom Publishing Protocol"""
66 # To properly map this controller, ensure your config/routing.py
66 # To properly map this controller, ensure your config/routing.py
67 # file has a resource setup:
67 # file has a resource setup:
68 # map.resource('repo', 'repos')
68 # map.resource('repo', 'repos')
69
69
70 @LoginRequired()
70 @LoginRequired()
71 def __before__(self):
71 def __before__(self):
72 super(ReposController, self).__before__()
72 super(ReposController, self).__before__()
73
73
74 def _load_repo(self, repo_name):
74 def _load_repo(self, repo_name):
75 repo_obj = Repository.get_by_repo_name(repo_name)
75 repo_obj = Repository.get_by_repo_name(repo_name)
76
76
77 if repo_obj is None:
77 if repo_obj is None:
78 h.not_mapped_error(repo_name)
78 h.not_mapped_error(repo_name)
79 return redirect(url('repos'))
79 return redirect(url('repos'))
80
80
81 return repo_obj
81 return repo_obj
82
82
83 def __load_defaults(self, repo=None):
83 def __load_defaults(self, repo=None):
84 acl_groups = RepoGroupList(RepoGroup.query().all(),
84 acl_groups = RepoGroupList(RepoGroup.query().all(),
85 perm_set=['group.write', 'group.admin'])
85 perm_set=['group.write', 'group.admin'])
86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
86 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
87 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
88
88
89 # in case someone no longer have a group.write access to a repository
89 # in case someone no longer have a group.write access to a repository
90 # pre fill the list with this entry, we don't care if this is the same
90 # pre fill the list with this entry, we don't care if this is the same
91 # but it will allow saving repo data properly.
91 # but it will allow saving repo data properly.
92
92
93 repo_group = None
93 repo_group = None
94 if repo:
94 if repo:
95 repo_group = repo.group
95 repo_group = repo.group
96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
96 if repo_group and unicode(repo_group.group_id) not in c.repo_groups_choices:
97 c.repo_groups_choices.append(unicode(repo_group.group_id))
97 c.repo_groups_choices.append(unicode(repo_group.group_id))
98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
98 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
99
99
100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
100 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
101 c.landing_revs_choices = choices
101 c.landing_revs_choices = choices
102
102
103 def __load_data(self, repo_name=None):
103 def __load_data(self, repo_name=None):
104 """
104 """
105 Load defaults settings for edit, and update
105 Load defaults settings for edit, and update
106
106
107 :param repo_name:
107 :param repo_name:
108 """
108 """
109 c.repo_info = self._load_repo(repo_name)
109 c.repo_info = self._load_repo(repo_name)
110 self.__load_defaults(c.repo_info)
110 self.__load_defaults(c.repo_info)
111
111
112 # override defaults for exact repo info here git/hg etc
112 # override defaults for exact repo info here git/hg etc
113 if not c.repository_requirements_missing:
113 if not c.repository_requirements_missing:
114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
114 choices, c.landing_revs = ScmModel().get_repo_landing_revs(
115 c.repo_info)
115 c.repo_info)
116 c.landing_revs_choices = choices
116 c.landing_revs_choices = choices
117 defaults = RepoModel()._get_defaults(repo_name)
117 defaults = RepoModel()._get_defaults(repo_name)
118
118
119 return defaults
119 return defaults
120
120
121 def _log_creation_exception(self, e, repo_name):
121 def _log_creation_exception(self, e, repo_name):
122 reason = None
122 reason = None
123 if len(e.args) == 2:
123 if len(e.args) == 2:
124 reason = e.args[1]
124 reason = e.args[1]
125
125
126 if reason == 'INVALID_CERTIFICATE':
126 if reason == 'INVALID_CERTIFICATE':
127 log.exception(
127 log.exception(
128 'Exception creating a repository: invalid certificate')
128 'Exception creating a repository: invalid certificate')
129 msg = (_('Error creating repository %s: invalid certificate')
129 msg = (_('Error creating repository %s: invalid certificate')
130 % repo_name)
130 % repo_name)
131 else:
131 else:
132 log.exception("Exception creating a repository")
132 log.exception("Exception creating a repository")
133 msg = (_('Error creating repository %s')
133 msg = (_('Error creating repository %s')
134 % repo_name)
134 % repo_name)
135
135
136 return msg
136 return msg
137
137
138 @NotAnonymous()
138 @NotAnonymous()
139 def index(self, format='html'):
139 def index(self, format='html'):
140 """GET /repos: All items in the collection"""
140 """GET /repos: All items in the collection"""
141 # url('repos')
141 # url('repos')
142
142
143 repo_list = Repository.get_all_repos()
143 repo_list = Repository.get_all_repos()
144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
144 c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
145 repos_data = RepoModel().get_repos_as_dict(
145 repos_data = RepoModel().get_repos_as_dict(
146 repo_list=c.repo_list, admin=True, super_user_actions=True)
146 repo_list=c.repo_list, admin=True, super_user_actions=True)
147 # json used to render the grid
147 # json used to render the grid
148 c.data = json.dumps(repos_data)
148 c.data = json.dumps(repos_data)
149
149
150 return render('admin/repos/repos.mako')
150 return render('admin/repos/repos.mako')
151
151
152 # perms check inside
152 # perms check inside
153 @NotAnonymous()
153 @NotAnonymous()
154 @auth.CSRFRequired()
154 @auth.CSRFRequired()
155 def create(self):
155 def create(self):
156 """
156 """
157 POST /repos: Create a new item"""
157 POST /repos: Create a new item"""
158 # url('repos')
158 # url('repos')
159
159
160 self.__load_defaults()
160 self.__load_defaults()
161 form_result = {}
161 form_result = {}
162 task_id = None
162 task_id = None
163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
163 c.personal_repo_group = c.rhodecode_user.personal_repo_group
164 try:
164 try:
165 # CanWriteToGroup validators checks permissions of this POST
165 # CanWriteToGroup validators checks permissions of this POST
166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
166 form_result = RepoForm(repo_groups=c.repo_groups_choices,
167 landing_revs=c.landing_revs_choices)()\
167 landing_revs=c.landing_revs_choices)()\
168 .to_python(dict(request.POST))
168 .to_python(dict(request.POST))
169
169
170 # create is done sometimes async on celery, db transaction
170 # create is done sometimes async on celery, db transaction
171 # management is handled there.
171 # management is handled there.
172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
172 task = RepoModel().create(form_result, c.rhodecode_user.user_id)
173 from celery.result import BaseAsyncResult
173 from celery.result import BaseAsyncResult
174 if isinstance(task, BaseAsyncResult):
174 if isinstance(task, BaseAsyncResult):
175 task_id = task.task_id
175 task_id = task.task_id
176 except formencode.Invalid as errors:
176 except formencode.Invalid as errors:
177 return htmlfill.render(
177 return htmlfill.render(
178 render('admin/repos/repo_add.mako'),
178 render('admin/repos/repo_add.mako'),
179 defaults=errors.value,
179 defaults=errors.value,
180 errors=errors.error_dict or {},
180 errors=errors.error_dict or {},
181 prefix_error=False,
181 prefix_error=False,
182 encoding="UTF-8",
182 encoding="UTF-8",
183 force_defaults=False)
183 force_defaults=False)
184
184
185 except Exception as e:
185 except Exception as e:
186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
187 h.flash(msg, category='error')
187 h.flash(msg, category='error')
188 return redirect(url('home'))
188 return redirect(h.route_path('home'))
189
189
190 return redirect(h.url('repo_creating_home',
190 return redirect(h.url('repo_creating_home',
191 repo_name=form_result['repo_name_full'],
191 repo_name=form_result['repo_name_full'],
192 task_id=task_id))
192 task_id=task_id))
193
193
194 # perms check inside
194 # perms check inside
195 @NotAnonymous()
195 @NotAnonymous()
196 def create_repository(self):
196 def create_repository(self):
197 """GET /_admin/create_repository: Form to create a new item"""
197 """GET /_admin/create_repository: Form to create a new item"""
198 new_repo = request.GET.get('repo', '')
198 new_repo = request.GET.get('repo', '')
199 parent_group = safe_int(request.GET.get('parent_group'))
199 parent_group = safe_int(request.GET.get('parent_group'))
200 _gr = RepoGroup.get(parent_group)
200 _gr = RepoGroup.get(parent_group)
201
201
202 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
202 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
203 # you're not super admin nor have global create permissions,
203 # you're not super admin nor have global create permissions,
204 # but maybe you have at least write permission to a parent group ?
204 # but maybe you have at least write permission to a parent group ?
205
205
206 gr_name = _gr.group_name if _gr else None
206 gr_name = _gr.group_name if _gr else None
207 # create repositories with write permission on group is set to true
207 # create repositories with write permission on group is set to true
208 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
208 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
209 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
209 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
210 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
210 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
211 if not (group_admin or (group_write and create_on_write)):
211 if not (group_admin or (group_write and create_on_write)):
212 raise HTTPForbidden
212 raise HTTPForbidden
213
213
214 acl_groups = RepoGroupList(RepoGroup.query().all(),
214 acl_groups = RepoGroupList(RepoGroup.query().all(),
215 perm_set=['group.write', 'group.admin'])
215 perm_set=['group.write', 'group.admin'])
216 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
216 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
217 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
217 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
218 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
218 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
219 c.personal_repo_group = c.rhodecode_user.personal_repo_group
219 c.personal_repo_group = c.rhodecode_user.personal_repo_group
220 c.new_repo = repo_name_slug(new_repo)
220 c.new_repo = repo_name_slug(new_repo)
221
221
222 # apply the defaults from defaults page
222 # apply the defaults from defaults page
223 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
223 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
224 # set checkbox to autochecked
224 # set checkbox to autochecked
225 defaults['repo_copy_permissions'] = True
225 defaults['repo_copy_permissions'] = True
226
226
227 parent_group_choice = '-1'
227 parent_group_choice = '-1'
228 if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group:
228 if not c.rhodecode_user.is_admin and c.rhodecode_user.personal_repo_group:
229 parent_group_choice = c.rhodecode_user.personal_repo_group
229 parent_group_choice = c.rhodecode_user.personal_repo_group
230
230
231 if parent_group and _gr:
231 if parent_group and _gr:
232 if parent_group in [x[0] for x in c.repo_groups]:
232 if parent_group in [x[0] for x in c.repo_groups]:
233 parent_group_choice = unicode(parent_group)
233 parent_group_choice = unicode(parent_group)
234
234
235 defaults.update({'repo_group': parent_group_choice})
235 defaults.update({'repo_group': parent_group_choice})
236
236
237 return htmlfill.render(
237 return htmlfill.render(
238 render('admin/repos/repo_add.mako'),
238 render('admin/repos/repo_add.mako'),
239 defaults=defaults,
239 defaults=defaults,
240 errors={},
240 errors={},
241 prefix_error=False,
241 prefix_error=False,
242 encoding="UTF-8",
242 encoding="UTF-8",
243 force_defaults=False
243 force_defaults=False
244 )
244 )
245
245
246 @NotAnonymous()
246 @NotAnonymous()
247 def repo_creating(self, repo_name):
247 def repo_creating(self, repo_name):
248 c.repo = repo_name
248 c.repo = repo_name
249 c.task_id = request.GET.get('task_id')
249 c.task_id = request.GET.get('task_id')
250 if not c.repo:
250 if not c.repo:
251 raise HTTPNotFound()
251 raise HTTPNotFound()
252 return render('admin/repos/repo_creating.mako')
252 return render('admin/repos/repo_creating.mako')
253
253
254 @NotAnonymous()
254 @NotAnonymous()
255 @jsonify
255 @jsonify
256 def repo_check(self, repo_name):
256 def repo_check(self, repo_name):
257 c.repo = repo_name
257 c.repo = repo_name
258 task_id = request.GET.get('task_id')
258 task_id = request.GET.get('task_id')
259
259
260 if task_id and task_id not in ['None']:
260 if task_id and task_id not in ['None']:
261 import rhodecode
261 import rhodecode
262 from celery.result import AsyncResult
262 from celery.result import AsyncResult
263 if rhodecode.CELERY_ENABLED:
263 if rhodecode.CELERY_ENABLED:
264 task = AsyncResult(task_id)
264 task = AsyncResult(task_id)
265 if task.failed():
265 if task.failed():
266 msg = self._log_creation_exception(task.result, c.repo)
266 msg = self._log_creation_exception(task.result, c.repo)
267 h.flash(msg, category='error')
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 repo = Repository.get_by_repo_name(repo_name)
270 repo = Repository.get_by_repo_name(repo_name)
271 if repo and repo.repo_state == Repository.STATE_CREATED:
271 if repo and repo.repo_state == Repository.STATE_CREATED:
272 if repo.clone_uri:
272 if repo.clone_uri:
273 clone_uri = repo.clone_uri_hidden
273 clone_uri = repo.clone_uri_hidden
274 h.flash(_('Created repository %s from %s')
274 h.flash(_('Created repository %s from %s')
275 % (repo.repo_name, clone_uri), category='success')
275 % (repo.repo_name, clone_uri), category='success')
276 else:
276 else:
277 repo_url = h.link_to(repo.repo_name,
277 repo_url = h.link_to(repo.repo_name,
278 h.url('summary_home',
278 h.url('summary_home',
279 repo_name=repo.repo_name))
279 repo_name=repo.repo_name))
280 fork = repo.fork
280 fork = repo.fork
281 if fork:
281 if fork:
282 fork_name = fork.repo_name
282 fork_name = fork.repo_name
283 h.flash(h.literal(_('Forked repository %s as %s')
283 h.flash(h.literal(_('Forked repository %s as %s')
284 % (fork_name, repo_url)), category='success')
284 % (fork_name, repo_url)), category='success')
285 else:
285 else:
286 h.flash(h.literal(_('Created repository %s') % repo_url),
286 h.flash(h.literal(_('Created repository %s') % repo_url),
287 category='success')
287 category='success')
288 return {'result': True}
288 return {'result': True}
289 return {'result': False}
289 return {'result': False}
290
290
291 @HasPermissionAllDecorator('hg.admin')
291 @HasPermissionAllDecorator('hg.admin')
292 def show(self, repo_name, format='html'):
292 def show(self, repo_name, format='html'):
293 """GET /repos/repo_name: Show a specific item"""
293 """GET /repos/repo_name: Show a specific item"""
294 # url('repo', repo_name=ID)
294 # url('repo', repo_name=ID)
295
295
296 @HasRepoPermissionAllDecorator('repository.admin')
296 @HasRepoPermissionAllDecorator('repository.admin')
297 def edit_fields(self, repo_name):
297 def edit_fields(self, repo_name):
298 """GET /repo_name/settings: Form to edit an existing item"""
298 """GET /repo_name/settings: Form to edit an existing item"""
299 c.repo_info = self._load_repo(repo_name)
299 c.repo_info = self._load_repo(repo_name)
300 c.repo_fields = RepositoryField.query()\
300 c.repo_fields = RepositoryField.query()\
301 .filter(RepositoryField.repository == c.repo_info).all()
301 .filter(RepositoryField.repository == c.repo_info).all()
302 c.active = 'fields'
302 c.active = 'fields'
303 if request.POST:
303 if request.POST:
304
304
305 return redirect(url('repo_edit_fields'))
305 return redirect(url('repo_edit_fields'))
306 return render('admin/repos/repo_edit.mako')
306 return render('admin/repos/repo_edit.mako')
307
307
308 @HasRepoPermissionAllDecorator('repository.admin')
308 @HasRepoPermissionAllDecorator('repository.admin')
309 @auth.CSRFRequired()
309 @auth.CSRFRequired()
310 def create_repo_field(self, repo_name):
310 def create_repo_field(self, repo_name):
311 try:
311 try:
312 form_result = RepoFieldForm()().to_python(dict(request.POST))
312 form_result = RepoFieldForm()().to_python(dict(request.POST))
313 RepoModel().add_repo_field(
313 RepoModel().add_repo_field(
314 repo_name, form_result['new_field_key'],
314 repo_name, form_result['new_field_key'],
315 field_type=form_result['new_field_type'],
315 field_type=form_result['new_field_type'],
316 field_value=form_result['new_field_value'],
316 field_value=form_result['new_field_value'],
317 field_label=form_result['new_field_label'],
317 field_label=form_result['new_field_label'],
318 field_desc=form_result['new_field_desc'])
318 field_desc=form_result['new_field_desc'])
319
319
320 Session().commit()
320 Session().commit()
321 except Exception as e:
321 except Exception as e:
322 log.exception("Exception creating field")
322 log.exception("Exception creating field")
323 msg = _('An error occurred during creation of field')
323 msg = _('An error occurred during creation of field')
324 if isinstance(e, formencode.Invalid):
324 if isinstance(e, formencode.Invalid):
325 msg += ". " + e.msg
325 msg += ". " + e.msg
326 h.flash(msg, category='error')
326 h.flash(msg, category='error')
327 return redirect(url('edit_repo_fields', repo_name=repo_name))
327 return redirect(url('edit_repo_fields', repo_name=repo_name))
328
328
329 @HasRepoPermissionAllDecorator('repository.admin')
329 @HasRepoPermissionAllDecorator('repository.admin')
330 @auth.CSRFRequired()
330 @auth.CSRFRequired()
331 def delete_repo_field(self, repo_name, field_id):
331 def delete_repo_field(self, repo_name, field_id):
332 field = RepositoryField.get_or_404(field_id)
332 field = RepositoryField.get_or_404(field_id)
333 try:
333 try:
334 RepoModel().delete_repo_field(repo_name, field.field_key)
334 RepoModel().delete_repo_field(repo_name, field.field_key)
335 Session().commit()
335 Session().commit()
336 except Exception as e:
336 except Exception as e:
337 log.exception("Exception during removal of field")
337 log.exception("Exception during removal of field")
338 msg = _('An error occurred during removal of field')
338 msg = _('An error occurred during removal of field')
339 h.flash(msg, category='error')
339 h.flash(msg, category='error')
340 return redirect(url('edit_repo_fields', repo_name=repo_name))
340 return redirect(url('edit_repo_fields', repo_name=repo_name))
341
341
342 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
342 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
343 @auth.CSRFRequired()
343 @auth.CSRFRequired()
344 def toggle_locking(self, repo_name):
344 def toggle_locking(self, repo_name):
345 """
345 """
346 Toggle locking of repository by simple GET call to url
346 Toggle locking of repository by simple GET call to url
347
347
348 :param repo_name:
348 :param repo_name:
349 """
349 """
350
350
351 try:
351 try:
352 repo = Repository.get_by_repo_name(repo_name)
352 repo = Repository.get_by_repo_name(repo_name)
353
353
354 if repo.enable_locking:
354 if repo.enable_locking:
355 if repo.locked[0]:
355 if repo.locked[0]:
356 Repository.unlock(repo)
356 Repository.unlock(repo)
357 action = _('Unlocked')
357 action = _('Unlocked')
358 else:
358 else:
359 Repository.lock(repo, c.rhodecode_user.user_id,
359 Repository.lock(repo, c.rhodecode_user.user_id,
360 lock_reason=Repository.LOCK_WEB)
360 lock_reason=Repository.LOCK_WEB)
361 action = _('Locked')
361 action = _('Locked')
362
362
363 h.flash(_('Repository has been %s') % action,
363 h.flash(_('Repository has been %s') % action,
364 category='success')
364 category='success')
365 except Exception:
365 except Exception:
366 log.exception("Exception during unlocking")
366 log.exception("Exception during unlocking")
367 h.flash(_('An error occurred during unlocking'),
367 h.flash(_('An error occurred during unlocking'),
368 category='error')
368 category='error')
369 return redirect(url('summary_home', repo_name=repo_name))
369 return redirect(url('summary_home', repo_name=repo_name))
370
370
371 @HasRepoPermissionAllDecorator('repository.admin')
371 @HasRepoPermissionAllDecorator('repository.admin')
372 @auth.CSRFRequired()
372 @auth.CSRFRequired()
373 def edit_remote(self, repo_name):
373 def edit_remote(self, repo_name):
374 """PUT /{repo_name}/settings/remote: edit the repo remote."""
374 """PUT /{repo_name}/settings/remote: edit the repo remote."""
375 try:
375 try:
376 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
376 ScmModel().pull_changes(repo_name, c.rhodecode_user.username)
377 h.flash(_('Pulled from remote location'), category='success')
377 h.flash(_('Pulled from remote location'), category='success')
378 except Exception:
378 except Exception:
379 log.exception("Exception during pull from remote")
379 log.exception("Exception during pull from remote")
380 h.flash(_('An error occurred during pull from remote location'),
380 h.flash(_('An error occurred during pull from remote location'),
381 category='error')
381 category='error')
382 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
382 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
383
383
384 @HasRepoPermissionAllDecorator('repository.admin')
384 @HasRepoPermissionAllDecorator('repository.admin')
385 def edit_remote_form(self, repo_name):
385 def edit_remote_form(self, repo_name):
386 """GET /repo_name/settings: Form to edit an existing item"""
386 """GET /repo_name/settings: Form to edit an existing item"""
387 c.repo_info = self._load_repo(repo_name)
387 c.repo_info = self._load_repo(repo_name)
388 c.active = 'remote'
388 c.active = 'remote'
389
389
390 return render('admin/repos/repo_edit.mako')
390 return render('admin/repos/repo_edit.mako')
391
391
392 @HasRepoPermissionAllDecorator('repository.admin')
392 @HasRepoPermissionAllDecorator('repository.admin')
393 @auth.CSRFRequired()
393 @auth.CSRFRequired()
394 def edit_statistics(self, repo_name):
394 def edit_statistics(self, repo_name):
395 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
395 """PUT /{repo_name}/settings/statistics: reset the repo statistics."""
396 try:
396 try:
397 RepoModel().delete_stats(repo_name)
397 RepoModel().delete_stats(repo_name)
398 Session().commit()
398 Session().commit()
399 except Exception as e:
399 except Exception as e:
400 log.error(traceback.format_exc())
400 log.error(traceback.format_exc())
401 h.flash(_('An error occurred during deletion of repository stats'),
401 h.flash(_('An error occurred during deletion of repository stats'),
402 category='error')
402 category='error')
403 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
403 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
404
404
405 @HasRepoPermissionAllDecorator('repository.admin')
405 @HasRepoPermissionAllDecorator('repository.admin')
406 def edit_statistics_form(self, repo_name):
406 def edit_statistics_form(self, repo_name):
407 """GET /repo_name/settings: Form to edit an existing item"""
407 """GET /repo_name/settings: Form to edit an existing item"""
408 c.repo_info = self._load_repo(repo_name)
408 c.repo_info = self._load_repo(repo_name)
409 repo = c.repo_info.scm_instance()
409 repo = c.repo_info.scm_instance()
410
410
411 if c.repo_info.stats:
411 if c.repo_info.stats:
412 # this is on what revision we ended up so we add +1 for count
412 # this is on what revision we ended up so we add +1 for count
413 last_rev = c.repo_info.stats.stat_on_revision + 1
413 last_rev = c.repo_info.stats.stat_on_revision + 1
414 else:
414 else:
415 last_rev = 0
415 last_rev = 0
416 c.stats_revision = last_rev
416 c.stats_revision = last_rev
417
417
418 c.repo_last_rev = repo.count()
418 c.repo_last_rev = repo.count()
419
419
420 if last_rev == 0 or c.repo_last_rev == 0:
420 if last_rev == 0 or c.repo_last_rev == 0:
421 c.stats_percentage = 0
421 c.stats_percentage = 0
422 else:
422 else:
423 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
423 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
424
424
425 c.active = 'statistics'
425 c.active = 'statistics'
426
426
427 return render('admin/repos/repo_edit.mako')
427 return render('admin/repos/repo_edit.mako')
428
428
429 @HasRepoPermissionAllDecorator('repository.admin')
429 @HasRepoPermissionAllDecorator('repository.admin')
430 @auth.CSRFRequired()
430 @auth.CSRFRequired()
431 def repo_issuetracker_test(self, repo_name):
431 def repo_issuetracker_test(self, repo_name):
432 if request.is_xhr:
432 if request.is_xhr:
433 return h.urlify_commit_message(
433 return h.urlify_commit_message(
434 request.POST.get('test_text', ''),
434 request.POST.get('test_text', ''),
435 repo_name)
435 repo_name)
436 else:
436 else:
437 raise HTTPBadRequest()
437 raise HTTPBadRequest()
438
438
439 @HasRepoPermissionAllDecorator('repository.admin')
439 @HasRepoPermissionAllDecorator('repository.admin')
440 @auth.CSRFRequired()
440 @auth.CSRFRequired()
441 def repo_issuetracker_delete(self, repo_name):
441 def repo_issuetracker_delete(self, repo_name):
442 uid = request.POST.get('uid')
442 uid = request.POST.get('uid')
443 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
443 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
444 try:
444 try:
445 repo_settings.delete_entries(uid)
445 repo_settings.delete_entries(uid)
446 except Exception:
446 except Exception:
447 h.flash(_('Error occurred during deleting issue tracker entry'),
447 h.flash(_('Error occurred during deleting issue tracker entry'),
448 category='error')
448 category='error')
449 else:
449 else:
450 h.flash(_('Removed issue tracker entry'), category='success')
450 h.flash(_('Removed issue tracker entry'), category='success')
451 return redirect(url('repo_settings_issuetracker',
451 return redirect(url('repo_settings_issuetracker',
452 repo_name=repo_name))
452 repo_name=repo_name))
453
453
454 def _update_patterns(self, form, repo_settings):
454 def _update_patterns(self, form, repo_settings):
455 for uid in form['delete_patterns']:
455 for uid in form['delete_patterns']:
456 repo_settings.delete_entries(uid)
456 repo_settings.delete_entries(uid)
457
457
458 for pattern in form['patterns']:
458 for pattern in form['patterns']:
459 for setting, value, type_ in pattern:
459 for setting, value, type_ in pattern:
460 sett = repo_settings.create_or_update_setting(
460 sett = repo_settings.create_or_update_setting(
461 setting, value, type_)
461 setting, value, type_)
462 Session().add(sett)
462 Session().add(sett)
463
463
464 Session().commit()
464 Session().commit()
465
465
466 @HasRepoPermissionAllDecorator('repository.admin')
466 @HasRepoPermissionAllDecorator('repository.admin')
467 @auth.CSRFRequired()
467 @auth.CSRFRequired()
468 def repo_issuetracker_save(self, repo_name):
468 def repo_issuetracker_save(self, repo_name):
469 # Save inheritance
469 # Save inheritance
470 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
470 repo_settings = IssueTrackerSettingsModel(repo=repo_name)
471 inherited = (request.POST.get('inherit_global_issuetracker')
471 inherited = (request.POST.get('inherit_global_issuetracker')
472 == "inherited")
472 == "inherited")
473 repo_settings.inherit_global_settings = inherited
473 repo_settings.inherit_global_settings = inherited
474 Session().commit()
474 Session().commit()
475
475
476 form = IssueTrackerPatternsForm()().to_python(request.POST)
476 form = IssueTrackerPatternsForm()().to_python(request.POST)
477 if form:
477 if form:
478 self._update_patterns(form, repo_settings)
478 self._update_patterns(form, repo_settings)
479
479
480 h.flash(_('Updated issue tracker entries'), category='success')
480 h.flash(_('Updated issue tracker entries'), category='success')
481 return redirect(url('repo_settings_issuetracker',
481 return redirect(url('repo_settings_issuetracker',
482 repo_name=repo_name))
482 repo_name=repo_name))
483
483
484 @HasRepoPermissionAllDecorator('repository.admin')
484 @HasRepoPermissionAllDecorator('repository.admin')
485 def repo_issuetracker(self, repo_name):
485 def repo_issuetracker(self, repo_name):
486 """GET /admin/settings/issue-tracker: All items in the collection"""
486 """GET /admin/settings/issue-tracker: All items in the collection"""
487 c.active = 'issuetracker'
487 c.active = 'issuetracker'
488 c.data = 'data'
488 c.data = 'data'
489 c.repo_info = self._load_repo(repo_name)
489 c.repo_info = self._load_repo(repo_name)
490
490
491 repo = Repository.get_by_repo_name(repo_name)
491 repo = Repository.get_by_repo_name(repo_name)
492 c.settings_model = IssueTrackerSettingsModel(repo=repo)
492 c.settings_model = IssueTrackerSettingsModel(repo=repo)
493 c.global_patterns = c.settings_model.get_global_settings()
493 c.global_patterns = c.settings_model.get_global_settings()
494 c.repo_patterns = c.settings_model.get_repo_settings()
494 c.repo_patterns = c.settings_model.get_repo_settings()
495
495
496 return render('admin/repos/repo_edit.mako')
496 return render('admin/repos/repo_edit.mako')
497
497
498 @HasRepoPermissionAllDecorator('repository.admin')
498 @HasRepoPermissionAllDecorator('repository.admin')
499 def repo_settings_vcs(self, repo_name):
499 def repo_settings_vcs(self, repo_name):
500 """GET /{repo_name}/settings/vcs/: All items in the collection"""
500 """GET /{repo_name}/settings/vcs/: All items in the collection"""
501
501
502 model = VcsSettingsModel(repo=repo_name)
502 model = VcsSettingsModel(repo=repo_name)
503
503
504 c.active = 'vcs'
504 c.active = 'vcs'
505 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
505 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
506 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
506 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
507 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
507 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
508 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
508 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
509 c.repo_info = self._load_repo(repo_name)
509 c.repo_info = self._load_repo(repo_name)
510 defaults = self._vcs_form_defaults(repo_name)
510 defaults = self._vcs_form_defaults(repo_name)
511 c.inherit_global_settings = defaults['inherit_global_settings']
511 c.inherit_global_settings = defaults['inherit_global_settings']
512 c.labs_active = str2bool(
512 c.labs_active = str2bool(
513 rhodecode.CONFIG.get('labs_settings_active', 'true'))
513 rhodecode.CONFIG.get('labs_settings_active', 'true'))
514
514
515 return htmlfill.render(
515 return htmlfill.render(
516 render('admin/repos/repo_edit.mako'),
516 render('admin/repos/repo_edit.mako'),
517 defaults=defaults,
517 defaults=defaults,
518 encoding="UTF-8",
518 encoding="UTF-8",
519 force_defaults=False)
519 force_defaults=False)
520
520
521 @HasRepoPermissionAllDecorator('repository.admin')
521 @HasRepoPermissionAllDecorator('repository.admin')
522 @auth.CSRFRequired()
522 @auth.CSRFRequired()
523 def repo_settings_vcs_update(self, repo_name):
523 def repo_settings_vcs_update(self, repo_name):
524 """POST /{repo_name}/settings/vcs/: All items in the collection"""
524 """POST /{repo_name}/settings/vcs/: All items in the collection"""
525 c.active = 'vcs'
525 c.active = 'vcs'
526
526
527 model = VcsSettingsModel(repo=repo_name)
527 model = VcsSettingsModel(repo=repo_name)
528 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
528 c.global_svn_branch_patterns = model.get_global_svn_branch_patterns()
529 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
529 c.global_svn_tag_patterns = model.get_global_svn_tag_patterns()
530 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
530 c.svn_branch_patterns = model.get_repo_svn_branch_patterns()
531 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
531 c.svn_tag_patterns = model.get_repo_svn_tag_patterns()
532 c.repo_info = self._load_repo(repo_name)
532 c.repo_info = self._load_repo(repo_name)
533 defaults = self._vcs_form_defaults(repo_name)
533 defaults = self._vcs_form_defaults(repo_name)
534 c.inherit_global_settings = defaults['inherit_global_settings']
534 c.inherit_global_settings = defaults['inherit_global_settings']
535
535
536 application_form = RepoVcsSettingsForm(repo_name)()
536 application_form = RepoVcsSettingsForm(repo_name)()
537 try:
537 try:
538 form_result = application_form.to_python(dict(request.POST))
538 form_result = application_form.to_python(dict(request.POST))
539 except formencode.Invalid as errors:
539 except formencode.Invalid as errors:
540 h.flash(
540 h.flash(
541 _("Some form inputs contain invalid data."),
541 _("Some form inputs contain invalid data."),
542 category='error')
542 category='error')
543 return htmlfill.render(
543 return htmlfill.render(
544 render('admin/repos/repo_edit.mako'),
544 render('admin/repos/repo_edit.mako'),
545 defaults=errors.value,
545 defaults=errors.value,
546 errors=errors.error_dict or {},
546 errors=errors.error_dict or {},
547 prefix_error=False,
547 prefix_error=False,
548 encoding="UTF-8",
548 encoding="UTF-8",
549 force_defaults=False
549 force_defaults=False
550 )
550 )
551
551
552 try:
552 try:
553 inherit_global_settings = form_result['inherit_global_settings']
553 inherit_global_settings = form_result['inherit_global_settings']
554 model.create_or_update_repo_settings(
554 model.create_or_update_repo_settings(
555 form_result, inherit_global_settings=inherit_global_settings)
555 form_result, inherit_global_settings=inherit_global_settings)
556 except Exception:
556 except Exception:
557 log.exception("Exception while updating settings")
557 log.exception("Exception while updating settings")
558 h.flash(
558 h.flash(
559 _('Error occurred during updating repository VCS settings'),
559 _('Error occurred during updating repository VCS settings'),
560 category='error')
560 category='error')
561 else:
561 else:
562 Session().commit()
562 Session().commit()
563 h.flash(_('Updated VCS settings'), category='success')
563 h.flash(_('Updated VCS settings'), category='success')
564 return redirect(url('repo_vcs_settings', repo_name=repo_name))
564 return redirect(url('repo_vcs_settings', repo_name=repo_name))
565
565
566 return htmlfill.render(
566 return htmlfill.render(
567 render('admin/repos/repo_edit.mako'),
567 render('admin/repos/repo_edit.mako'),
568 defaults=self._vcs_form_defaults(repo_name),
568 defaults=self._vcs_form_defaults(repo_name),
569 encoding="UTF-8",
569 encoding="UTF-8",
570 force_defaults=False)
570 force_defaults=False)
571
571
572 @HasRepoPermissionAllDecorator('repository.admin')
572 @HasRepoPermissionAllDecorator('repository.admin')
573 @auth.CSRFRequired()
573 @auth.CSRFRequired()
574 @jsonify
574 @jsonify
575 def repo_delete_svn_pattern(self, repo_name):
575 def repo_delete_svn_pattern(self, repo_name):
576 if not request.is_xhr:
576 if not request.is_xhr:
577 return False
577 return False
578
578
579 delete_pattern_id = request.POST.get('delete_svn_pattern')
579 delete_pattern_id = request.POST.get('delete_svn_pattern')
580 model = VcsSettingsModel(repo=repo_name)
580 model = VcsSettingsModel(repo=repo_name)
581 try:
581 try:
582 model.delete_repo_svn_pattern(delete_pattern_id)
582 model.delete_repo_svn_pattern(delete_pattern_id)
583 except SettingNotFound:
583 except SettingNotFound:
584 raise HTTPBadRequest()
584 raise HTTPBadRequest()
585
585
586 Session().commit()
586 Session().commit()
587 return True
587 return True
588
588
589 def _vcs_form_defaults(self, repo_name):
589 def _vcs_form_defaults(self, repo_name):
590 model = VcsSettingsModel(repo=repo_name)
590 model = VcsSettingsModel(repo=repo_name)
591 global_defaults = model.get_global_settings()
591 global_defaults = model.get_global_settings()
592
592
593 repo_defaults = {}
593 repo_defaults = {}
594 repo_defaults.update(global_defaults)
594 repo_defaults.update(global_defaults)
595 repo_defaults.update(model.get_repo_settings())
595 repo_defaults.update(model.get_repo_settings())
596
596
597 global_defaults = {
597 global_defaults = {
598 '{}_inherited'.format(k): global_defaults[k]
598 '{}_inherited'.format(k): global_defaults[k]
599 for k in global_defaults}
599 for k in global_defaults}
600
600
601 defaults = {
601 defaults = {
602 'inherit_global_settings': model.inherit_global_settings
602 'inherit_global_settings': model.inherit_global_settings
603 }
603 }
604 defaults.update(global_defaults)
604 defaults.update(global_defaults)
605 defaults.update(repo_defaults)
605 defaults.update(repo_defaults)
606 defaults.update({
606 defaults.update({
607 'new_svn_branch': '',
607 'new_svn_branch': '',
608 'new_svn_tag': '',
608 'new_svn_tag': '',
609 })
609 })
610 return defaults
610 return defaults
@@ -1,196 +1,196 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 forks controller for rhodecode
22 forks controller for rhodecode
23 """
23 """
24
24
25 import formencode
25 import formencode
26 import logging
26 import logging
27 from formencode import htmlfill
27 from formencode import htmlfill
28
28
29 from pylons import tmpl_context as c, request, url
29 from pylons import tmpl_context as c, request, url
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 import rhodecode.lib.helpers as h
33 import rhodecode.lib.helpers as h
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib.helpers import Page
36 from rhodecode.lib.helpers import Page
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
39 HasRepoPermissionAny, HasPermissionAnyDecorator, HasAcceptedRepoType)
40 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.forms import RepoForkForm
43 from rhodecode.model.forms import RepoForkForm
44 from rhodecode.model.scm import ScmModel, RepoGroupList
44 from rhodecode.model.scm import ScmModel, RepoGroupList
45 from rhodecode.lib.utils2 import safe_int
45 from rhodecode.lib.utils2 import safe_int
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class ForksController(BaseRepoController):
50 class ForksController(BaseRepoController):
51
51
52 def __before__(self):
52 def __before__(self):
53 super(ForksController, self).__before__()
53 super(ForksController, self).__before__()
54
54
55 def __load_defaults(self):
55 def __load_defaults(self):
56 acl_groups = RepoGroupList(
56 acl_groups = RepoGroupList(
57 RepoGroup.query().all(),
57 RepoGroup.query().all(),
58 perm_set=['group.write', 'group.admin'])
58 perm_set=['group.write', 'group.admin'])
59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
59 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
60 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
61 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
62 c.landing_revs_choices = choices
62 c.landing_revs_choices = choices
63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
63 c.personal_repo_group = c.rhodecode_user.personal_repo_group
64
64
65 def __load_data(self, repo_name=None):
65 def __load_data(self, repo_name=None):
66 """
66 """
67 Load defaults settings for edit, and update
67 Load defaults settings for edit, and update
68
68
69 :param repo_name:
69 :param repo_name:
70 """
70 """
71 self.__load_defaults()
71 self.__load_defaults()
72
72
73 c.repo_info = Repository.get_by_repo_name(repo_name)
73 c.repo_info = Repository.get_by_repo_name(repo_name)
74 repo = c.repo_info.scm_instance()
74 repo = c.repo_info.scm_instance()
75
75
76 if c.repo_info is None:
76 if c.repo_info is None:
77 h.not_mapped_error(repo_name)
77 h.not_mapped_error(repo_name)
78 return redirect(url('repos'))
78 return redirect(url('repos'))
79
79
80 c.default_user_id = User.get_default_user().user_id
80 c.default_user_id = User.get_default_user().user_id
81 c.in_public_journal = UserFollowing.query()\
81 c.in_public_journal = UserFollowing.query()\
82 .filter(UserFollowing.user_id == c.default_user_id)\
82 .filter(UserFollowing.user_id == c.default_user_id)\
83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
84
84
85 if c.repo_info.stats:
85 if c.repo_info.stats:
86 last_rev = c.repo_info.stats.stat_on_revision+1
86 last_rev = c.repo_info.stats.stat_on_revision+1
87 else:
87 else:
88 last_rev = 0
88 last_rev = 0
89 c.stats_revision = last_rev
89 c.stats_revision = last_rev
90
90
91 c.repo_last_rev = repo.count()
91 c.repo_last_rev = repo.count()
92
92
93 if last_rev == 0 or c.repo_last_rev == 0:
93 if last_rev == 0 or c.repo_last_rev == 0:
94 c.stats_percentage = 0
94 c.stats_percentage = 0
95 else:
95 else:
96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
97 c.repo_last_rev) * 100)
97 c.repo_last_rev) * 100)
98
98
99 defaults = RepoModel()._get_defaults(repo_name)
99 defaults = RepoModel()._get_defaults(repo_name)
100 # alter the description to indicate a fork
100 # alter the description to indicate a fork
101 defaults['description'] = ('fork of repository: %s \n%s'
101 defaults['description'] = ('fork of repository: %s \n%s'
102 % (defaults['repo_name'],
102 % (defaults['repo_name'],
103 defaults['description']))
103 defaults['description']))
104 # add suffix to fork
104 # add suffix to fork
105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
105 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
106
106
107 return defaults
107 return defaults
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
111 'repository.admin')
111 'repository.admin')
112 @HasAcceptedRepoType('git', 'hg')
112 @HasAcceptedRepoType('git', 'hg')
113 def forks(self, repo_name):
113 def forks(self, repo_name):
114 p = safe_int(request.GET.get('page', 1), 1)
114 p = safe_int(request.GET.get('page', 1), 1)
115 repo_id = c.rhodecode_db_repo.repo_id
115 repo_id = c.rhodecode_db_repo.repo_id
116 d = []
116 d = []
117 for r in Repository.get_repo_forks(repo_id):
117 for r in Repository.get_repo_forks(repo_id):
118 if not HasRepoPermissionAny(
118 if not HasRepoPermissionAny(
119 'repository.read', 'repository.write', 'repository.admin'
119 'repository.read', 'repository.write', 'repository.admin'
120 )(r.repo_name, 'get forks check'):
120 )(r.repo_name, 'get forks check'):
121 continue
121 continue
122 d.append(r)
122 d.append(r)
123 c.forks_pager = Page(d, page=p, items_per_page=20)
123 c.forks_pager = Page(d, page=p, items_per_page=20)
124
124
125 c.forks_data = render('/forks/forks_data.mako')
125 c.forks_data = render('/forks/forks_data.mako')
126
126
127 if request.environ.get('HTTP_X_PJAX'):
127 if request.environ.get('HTTP_X_PJAX'):
128 return c.forks_data
128 return c.forks_data
129
129
130 return render('/forks/forks.mako')
130 return render('/forks/forks.mako')
131
131
132 @LoginRequired()
132 @LoginRequired()
133 @NotAnonymous()
133 @NotAnonymous()
134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 'repository.admin')
136 'repository.admin')
137 @HasAcceptedRepoType('git', 'hg')
137 @HasAcceptedRepoType('git', 'hg')
138 def fork(self, repo_name):
138 def fork(self, repo_name):
139 c.repo_info = Repository.get_by_repo_name(repo_name)
139 c.repo_info = Repository.get_by_repo_name(repo_name)
140 if not c.repo_info:
140 if not c.repo_info:
141 h.not_mapped_error(repo_name)
141 h.not_mapped_error(repo_name)
142 return redirect(url('home'))
142 return redirect(h.route_path('home'))
143
143
144 defaults = self.__load_data(repo_name)
144 defaults = self.__load_data(repo_name)
145
145
146 return htmlfill.render(
146 return htmlfill.render(
147 render('forks/fork.mako'),
147 render('forks/fork.mako'),
148 defaults=defaults,
148 defaults=defaults,
149 encoding="UTF-8",
149 encoding="UTF-8",
150 force_defaults=False
150 force_defaults=False
151 )
151 )
152
152
153 @LoginRequired()
153 @LoginRequired()
154 @NotAnonymous()
154 @NotAnonymous()
155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
155 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
156 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
157 'repository.admin')
157 'repository.admin')
158 @HasAcceptedRepoType('git', 'hg')
158 @HasAcceptedRepoType('git', 'hg')
159 @auth.CSRFRequired()
159 @auth.CSRFRequired()
160 def fork_create(self, repo_name):
160 def fork_create(self, repo_name):
161 self.__load_defaults()
161 self.__load_defaults()
162 c.repo_info = Repository.get_by_repo_name(repo_name)
162 c.repo_info = Repository.get_by_repo_name(repo_name)
163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
163 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
164 repo_groups=c.repo_groups_choices,
164 repo_groups=c.repo_groups_choices,
165 landing_revs=c.landing_revs_choices)()
165 landing_revs=c.landing_revs_choices)()
166 form_result = {}
166 form_result = {}
167 task_id = None
167 task_id = None
168 try:
168 try:
169 form_result = _form.to_python(dict(request.POST))
169 form_result = _form.to_python(dict(request.POST))
170 # create fork is done sometimes async on celery, db transaction
170 # create fork is done sometimes async on celery, db transaction
171 # management is handled there.
171 # management is handled there.
172 task = RepoModel().create_fork(
172 task = RepoModel().create_fork(
173 form_result, c.rhodecode_user.user_id)
173 form_result, c.rhodecode_user.user_id)
174 from celery.result import BaseAsyncResult
174 from celery.result import BaseAsyncResult
175 if isinstance(task, BaseAsyncResult):
175 if isinstance(task, BaseAsyncResult):
176 task_id = task.task_id
176 task_id = task.task_id
177 except formencode.Invalid as errors:
177 except formencode.Invalid as errors:
178 c.new_repo = errors.value['repo_name']
178 c.new_repo = errors.value['repo_name']
179 return htmlfill.render(
179 return htmlfill.render(
180 render('forks/fork.mako'),
180 render('forks/fork.mako'),
181 defaults=errors.value,
181 defaults=errors.value,
182 errors=errors.error_dict or {},
182 errors=errors.error_dict or {},
183 prefix_error=False,
183 prefix_error=False,
184 encoding="UTF-8",
184 encoding="UTF-8",
185 force_defaults=False)
185 force_defaults=False)
186 except Exception:
186 except Exception:
187 log.exception(
187 log.exception(
188 u'Exception while trying to fork the repository %s', repo_name)
188 u'Exception while trying to fork the repository %s', repo_name)
189 msg = (
189 msg = (
190 _('An error occurred during repository forking %s') %
190 _('An error occurred during repository forking %s') %
191 (repo_name, ))
191 (repo_name, ))
192 h.flash(msg, category='error')
192 h.flash(msg, category='error')
193
193
194 return redirect(h.url('repo_creating_home',
194 return redirect(h.url('repo_creating_home',
195 repo_name=form_result['repo_name_full'],
195 repo_name=form_result['repo_name_full'],
196 task_id=task_id))
196 task_id=task_id))
@@ -1,110 +1,67 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Home controller for RhodeCode Enterprise
22 Home controller for RhodeCode Enterprise
23 """
23 """
24
24
25 import logging
25 import logging
26 import time
26 import time
27
27
28 from pylons import tmpl_context as c
28 from pylons import tmpl_context as c
29
29
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, HasPermissionAllDecorator,
31 LoginRequired, HasPermissionAllDecorator,
32 HasRepoGroupPermissionAnyDecorator)
32 HasRepoGroupPermissionAnyDecorator)
33 from rhodecode.lib.base import BaseController, render
33 from rhodecode.lib.base import BaseController, render
34
34
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.model.db import Repository, RepoGroup
36 from rhodecode.model.db import Repository, RepoGroup
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.repo_group import RepoGroupModel
38 from rhodecode.model.repo_group import RepoGroupModel
39 from rhodecode.model.scm import RepoList, RepoGroupList
39 from rhodecode.model.scm import RepoList, RepoGroupList
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class HomeController(BaseController):
45 class HomeController(BaseController):
46 def __before__(self):
46 def __before__(self):
47 super(HomeController, self).__before__()
47 super(HomeController, self).__before__()
48
48
49 def ping(self):
49 def ping(self):
50 """
50 """
51 Ping, doesn't require login, good for checking out the platform
51 Ping, doesn't require login, good for checking out the platform
52 """
52 """
53 instance_id = getattr(c, 'rhodecode_instanceid', '')
53 instance_id = getattr(c, 'rhodecode_instanceid', '')
54 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
54 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
55
55
56 @LoginRequired()
56 @LoginRequired()
57 @HasPermissionAllDecorator('hg.admin')
57 @HasPermissionAllDecorator('hg.admin')
58 def error_test(self):
58 def error_test(self):
59 """
59 """
60 Test exception handling and emails on errors
60 Test exception handling and emails on errors
61 """
61 """
62 class TestException(Exception):
62 class TestException(Exception):
63 pass
63 pass
64
64
65 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
65 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
66 % (c.rhodecode_name, time.time()))
66 % (c.rhodecode_name, time.time()))
67 raise TestException(msg)
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 # Copyright (C) 2016-2017 RhodeCode GmbH
1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import logging
18 import logging
19
19
20 from datetime import datetime
20 from datetime import datetime
21 from pyramid.threadlocal import get_current_request
21 from pyramid.threadlocal import get_current_request
22 from rhodecode.lib.utils2 import AttributeDict
22 from rhodecode.lib.utils2 import AttributeDict
23
23
24
24
25 # this is a user object to be used for events caused by the system (eg. shell)
25 # this is a user object to be used for events caused by the system (eg. shell)
26 SYSTEM_USER = AttributeDict(dict(
26 SYSTEM_USER = AttributeDict(dict(
27 username='__SYSTEM__',
27 username='__SYSTEM__',
28 user_id='__SYSTEM_ID__'
28 user_id='__SYSTEM_ID__'
29 ))
29 ))
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class RhodecodeEvent(object):
34 class RhodecodeEvent(object):
35 """
35 """
36 Base event class for all Rhodecode events
36 Base event class for all Rhodecode events
37 """
37 """
38 name = "RhodeCodeEvent"
38 name = "RhodeCodeEvent"
39
39
40 def __init__(self):
40 def __init__(self):
41 self.request = get_current_request()
41 self.request = get_current_request()
42 self.utc_timestamp = datetime.utcnow()
42 self.utc_timestamp = datetime.utcnow()
43
43
44 @property
44 @property
45 def auth_user(self):
45 def auth_user(self):
46 if not self.request:
46 if not self.request:
47 return
47 return
48
48
49 user = getattr(self.request, 'user', None)
49 user = getattr(self.request, 'user', None)
50 if user:
50 if user:
51 return user
51 return user
52
52
53 api_user = getattr(self.request, 'rpc_user', None)
53 api_user = getattr(self.request, 'rpc_user', None)
54 if api_user:
54 if api_user:
55 return api_user
55 return api_user
56
56
57 @property
57 @property
58 def actor(self):
58 def actor(self):
59 auth_user = self.auth_user
59 auth_user = self.auth_user
60
60
61 if auth_user:
61 if auth_user:
62 instance = auth_user.get_instance()
62 instance = auth_user.get_instance()
63 if not instance:
63 if not instance:
64 return AttributeDict(dict(
64 return AttributeDict(dict(
65 username=auth_user.username,
65 username=auth_user.username,
66 user_id=auth_user.user_id,
66 user_id=auth_user.user_id,
67 ))
67 ))
68 return instance
68 return instance
69
69
70 return SYSTEM_USER
70 return SYSTEM_USER
71
71
72 @property
72 @property
73 def actor_ip(self):
73 def actor_ip(self):
74 auth_user = self.auth_user
74 auth_user = self.auth_user
75 if auth_user:
75 if auth_user:
76 return auth_user.ip_addr
76 return auth_user.ip_addr
77 return '<no ip available>'
77 return '<no ip available>'
78
78
79 @property
79 @property
80 def server_url(self):
80 def server_url(self):
81 default = '<no server_url available>'
81 default = '<no server_url available>'
82 if self.request:
82 if self.request:
83 from rhodecode.lib import helpers as h
83 from rhodecode.lib import helpers as h
84 try:
84 try:
85 return h.url('home', qualified=True)
85 return h.route_url('home')
86 except Exception:
86 except Exception:
87 log.exception('Failed to fetch URL for server')
87 log.exception('Failed to fetch URL for server')
88 return default
88 return default
89
89
90 return default
90 return default
91
91
92 def as_dict(self):
92 def as_dict(self):
93 data = {
93 data = {
94 'name': self.name,
94 'name': self.name,
95 'utc_timestamp': self.utc_timestamp,
95 'utc_timestamp': self.utc_timestamp,
96 'actor_ip': self.actor_ip,
96 'actor_ip': self.actor_ip,
97 'actor': {
97 'actor': {
98 'username': self.actor.username,
98 'username': self.actor.username,
99 'user_id': self.actor.user_id
99 'user_id': self.actor.user_id
100 },
100 },
101 'server_url': self.server_url
101 'server_url': self.server_url
102 }
102 }
103 return data
103 return data
@@ -1,1973 +1,1993 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 authentication and permission libraries
22 authentication and permission libraries
23 """
23 """
24
24
25 import os
25 import os
26 import inspect
26 import inspect
27 import collections
27 import collections
28 import fnmatch
28 import fnmatch
29 import hashlib
29 import hashlib
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import random
32 import random
33 import traceback
33 import traceback
34 from functools import wraps
34 from functools import wraps
35
35
36 import ipaddress
36 import ipaddress
37 from pyramid.httpexceptions import HTTPForbidden, HTTPFound
37 from pyramid.httpexceptions import HTTPForbidden, HTTPFound
38 from pylons import url, request
38 from pylons import url, request
39 from pylons.controllers.util import abort, redirect
39 from pylons.controllers.util import abort, redirect
40 from pylons.i18n.translation import _
40 from pylons.i18n.translation import _
41 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm.exc import ObjectDeletedError
42 from sqlalchemy.orm import joinedload
42 from sqlalchemy.orm import joinedload
43 from zope.cachedescriptors.property import Lazy as LazyProperty
43 from zope.cachedescriptors.property import Lazy as LazyProperty
44
44
45 import rhodecode
45 import rhodecode
46 from rhodecode.model import meta
46 from rhodecode.model import meta
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
50 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
51 UserIpMap, UserApiKeys, RepoGroup)
51 UserIpMap, UserApiKeys, RepoGroup)
52 from rhodecode.lib import caches
52 from rhodecode.lib import caches
53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
54 from rhodecode.lib.utils import (
54 from rhodecode.lib.utils import (
55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
56 from rhodecode.lib.caching_query import FromCache
56 from rhodecode.lib.caching_query import FromCache
57
57
58
58
59 if rhodecode.is_unix:
59 if rhodecode.is_unix:
60 import bcrypt
60 import bcrypt
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64 csrf_token_key = "csrf_token"
64 csrf_token_key = "csrf_token"
65
65
66
66
67 class PasswordGenerator(object):
67 class PasswordGenerator(object):
68 """
68 """
69 This is a simple class for generating password from different sets of
69 This is a simple class for generating password from different sets of
70 characters
70 characters
71 usage::
71 usage::
72
72
73 passwd_gen = PasswordGenerator()
73 passwd_gen = PasswordGenerator()
74 #print 8-letter password containing only big and small letters
74 #print 8-letter password containing only big and small letters
75 of alphabet
75 of alphabet
76 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
76 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
77 """
77 """
78 ALPHABETS_NUM = r'''1234567890'''
78 ALPHABETS_NUM = r'''1234567890'''
79 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
79 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
80 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
80 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
81 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
81 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
82 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
82 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
83 + ALPHABETS_NUM + ALPHABETS_SPECIAL
83 + ALPHABETS_NUM + ALPHABETS_SPECIAL
84 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
84 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
85 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
85 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
86 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
86 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
87 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
87 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
88
88
89 def __init__(self, passwd=''):
89 def __init__(self, passwd=''):
90 self.passwd = passwd
90 self.passwd = passwd
91
91
92 def gen_password(self, length, type_=None):
92 def gen_password(self, length, type_=None):
93 if type_ is None:
93 if type_ is None:
94 type_ = self.ALPHABETS_FULL
94 type_ = self.ALPHABETS_FULL
95 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
95 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
96 return self.passwd
96 return self.passwd
97
97
98
98
99 class _RhodeCodeCryptoBase(object):
99 class _RhodeCodeCryptoBase(object):
100 ENC_PREF = None
100 ENC_PREF = None
101
101
102 def hash_create(self, str_):
102 def hash_create(self, str_):
103 """
103 """
104 hash the string using
104 hash the string using
105
105
106 :param str_: password to hash
106 :param str_: password to hash
107 """
107 """
108 raise NotImplementedError
108 raise NotImplementedError
109
109
110 def hash_check_with_upgrade(self, password, hashed):
110 def hash_check_with_upgrade(self, password, hashed):
111 """
111 """
112 Returns tuple in which first element is boolean that states that
112 Returns tuple in which first element is boolean that states that
113 given password matches it's hashed version, and the second is new hash
113 given password matches it's hashed version, and the second is new hash
114 of the password, in case this password should be migrated to new
114 of the password, in case this password should be migrated to new
115 cipher.
115 cipher.
116 """
116 """
117 checked_hash = self.hash_check(password, hashed)
117 checked_hash = self.hash_check(password, hashed)
118 return checked_hash, None
118 return checked_hash, None
119
119
120 def hash_check(self, password, hashed):
120 def hash_check(self, password, hashed):
121 """
121 """
122 Checks matching password with it's hashed value.
122 Checks matching password with it's hashed value.
123
123
124 :param password: password
124 :param password: password
125 :param hashed: password in hashed form
125 :param hashed: password in hashed form
126 """
126 """
127 raise NotImplementedError
127 raise NotImplementedError
128
128
129 def _assert_bytes(self, value):
129 def _assert_bytes(self, value):
130 """
130 """
131 Passing in an `unicode` object can lead to hard to detect issues
131 Passing in an `unicode` object can lead to hard to detect issues
132 if passwords contain non-ascii characters. Doing a type check
132 if passwords contain non-ascii characters. Doing a type check
133 during runtime, so that such mistakes are detected early on.
133 during runtime, so that such mistakes are detected early on.
134 """
134 """
135 if not isinstance(value, str):
135 if not isinstance(value, str):
136 raise TypeError(
136 raise TypeError(
137 "Bytestring required as input, got %r." % (value, ))
137 "Bytestring required as input, got %r." % (value, ))
138
138
139
139
140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
140 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
141 ENC_PREF = ('$2a$10', '$2b$10')
141 ENC_PREF = ('$2a$10', '$2b$10')
142
142
143 def hash_create(self, str_):
143 def hash_create(self, str_):
144 self._assert_bytes(str_)
144 self._assert_bytes(str_)
145 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
145 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
146
146
147 def hash_check_with_upgrade(self, password, hashed):
147 def hash_check_with_upgrade(self, password, hashed):
148 """
148 """
149 Returns tuple in which first element is boolean that states that
149 Returns tuple in which first element is boolean that states that
150 given password matches it's hashed version, and the second is new hash
150 given password matches it's hashed version, and the second is new hash
151 of the password, in case this password should be migrated to new
151 of the password, in case this password should be migrated to new
152 cipher.
152 cipher.
153
153
154 This implements special upgrade logic which works like that:
154 This implements special upgrade logic which works like that:
155 - check if the given password == bcrypted hash, if yes then we
155 - check if the given password == bcrypted hash, if yes then we
156 properly used password and it was already in bcrypt. Proceed
156 properly used password and it was already in bcrypt. Proceed
157 without any changes
157 without any changes
158 - if bcrypt hash check is not working try with sha256. If hash compare
158 - if bcrypt hash check is not working try with sha256. If hash compare
159 is ok, it means we using correct but old hashed password. indicate
159 is ok, it means we using correct but old hashed password. indicate
160 hash change and proceed
160 hash change and proceed
161 """
161 """
162
162
163 new_hash = None
163 new_hash = None
164
164
165 # regular pw check
165 # regular pw check
166 password_match_bcrypt = self.hash_check(password, hashed)
166 password_match_bcrypt = self.hash_check(password, hashed)
167
167
168 # now we want to know if the password was maybe from sha256
168 # now we want to know if the password was maybe from sha256
169 # basically calling _RhodeCodeCryptoSha256().hash_check()
169 # basically calling _RhodeCodeCryptoSha256().hash_check()
170 if not password_match_bcrypt:
170 if not password_match_bcrypt:
171 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
171 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
172 new_hash = self.hash_create(password) # make new bcrypt hash
172 new_hash = self.hash_create(password) # make new bcrypt hash
173 password_match_bcrypt = True
173 password_match_bcrypt = True
174
174
175 return password_match_bcrypt, new_hash
175 return password_match_bcrypt, new_hash
176
176
177 def hash_check(self, password, hashed):
177 def hash_check(self, password, hashed):
178 """
178 """
179 Checks matching password with it's hashed value.
179 Checks matching password with it's hashed value.
180
180
181 :param password: password
181 :param password: password
182 :param hashed: password in hashed form
182 :param hashed: password in hashed form
183 """
183 """
184 self._assert_bytes(password)
184 self._assert_bytes(password)
185 try:
185 try:
186 return bcrypt.hashpw(password, hashed) == hashed
186 return bcrypt.hashpw(password, hashed) == hashed
187 except ValueError as e:
187 except ValueError as e:
188 # we're having a invalid salt here probably, we should not crash
188 # we're having a invalid salt here probably, we should not crash
189 # just return with False as it would be a wrong password.
189 # just return with False as it would be a wrong password.
190 log.debug('Failed to check password hash using bcrypt %s',
190 log.debug('Failed to check password hash using bcrypt %s',
191 safe_str(e))
191 safe_str(e))
192
192
193 return False
193 return False
194
194
195
195
196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
197 ENC_PREF = '_'
197 ENC_PREF = '_'
198
198
199 def hash_create(self, str_):
199 def hash_create(self, str_):
200 self._assert_bytes(str_)
200 self._assert_bytes(str_)
201 return hashlib.sha256(str_).hexdigest()
201 return hashlib.sha256(str_).hexdigest()
202
202
203 def hash_check(self, password, hashed):
203 def hash_check(self, password, hashed):
204 """
204 """
205 Checks matching password with it's hashed value.
205 Checks matching password with it's hashed value.
206
206
207 :param password: password
207 :param password: password
208 :param hashed: password in hashed form
208 :param hashed: password in hashed form
209 """
209 """
210 self._assert_bytes(password)
210 self._assert_bytes(password)
211 return hashlib.sha256(password).hexdigest() == hashed
211 return hashlib.sha256(password).hexdigest() == hashed
212
212
213
213
214 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
214 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
215 ENC_PREF = '_'
215 ENC_PREF = '_'
216
216
217 def hash_create(self, str_):
217 def hash_create(self, str_):
218 self._assert_bytes(str_)
218 self._assert_bytes(str_)
219 return hashlib.md5(str_).hexdigest()
219 return hashlib.md5(str_).hexdigest()
220
220
221 def hash_check(self, password, hashed):
221 def hash_check(self, password, hashed):
222 """
222 """
223 Checks matching password with it's hashed value.
223 Checks matching password with it's hashed value.
224
224
225 :param password: password
225 :param password: password
226 :param hashed: password in hashed form
226 :param hashed: password in hashed form
227 """
227 """
228 self._assert_bytes(password)
228 self._assert_bytes(password)
229 return hashlib.md5(password).hexdigest() == hashed
229 return hashlib.md5(password).hexdigest() == hashed
230
230
231
231
232 def crypto_backend():
232 def crypto_backend():
233 """
233 """
234 Return the matching crypto backend.
234 Return the matching crypto backend.
235
235
236 Selection is based on if we run tests or not, we pick md5 backend to run
236 Selection is based on if we run tests or not, we pick md5 backend to run
237 tests faster since BCRYPT is expensive to calculate
237 tests faster since BCRYPT is expensive to calculate
238 """
238 """
239 if rhodecode.is_test:
239 if rhodecode.is_test:
240 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
240 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
241 else:
241 else:
242 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
242 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
243
243
244 return RhodeCodeCrypto
244 return RhodeCodeCrypto
245
245
246
246
247 def get_crypt_password(password):
247 def get_crypt_password(password):
248 """
248 """
249 Create the hash of `password` with the active crypto backend.
249 Create the hash of `password` with the active crypto backend.
250
250
251 :param password: The cleartext password.
251 :param password: The cleartext password.
252 :type password: unicode
252 :type password: unicode
253 """
253 """
254 password = safe_str(password)
254 password = safe_str(password)
255 return crypto_backend().hash_create(password)
255 return crypto_backend().hash_create(password)
256
256
257
257
258 def check_password(password, hashed):
258 def check_password(password, hashed):
259 """
259 """
260 Check if the value in `password` matches the hash in `hashed`.
260 Check if the value in `password` matches the hash in `hashed`.
261
261
262 :param password: The cleartext password.
262 :param password: The cleartext password.
263 :type password: unicode
263 :type password: unicode
264
264
265 :param hashed: The expected hashed version of the password.
265 :param hashed: The expected hashed version of the password.
266 :type hashed: The hash has to be passed in in text representation.
266 :type hashed: The hash has to be passed in in text representation.
267 """
267 """
268 password = safe_str(password)
268 password = safe_str(password)
269 return crypto_backend().hash_check(password, hashed)
269 return crypto_backend().hash_check(password, hashed)
270
270
271
271
272 def generate_auth_token(data, salt=None):
272 def generate_auth_token(data, salt=None):
273 """
273 """
274 Generates API KEY from given string
274 Generates API KEY from given string
275 """
275 """
276
276
277 if salt is None:
277 if salt is None:
278 salt = os.urandom(16)
278 salt = os.urandom(16)
279 return hashlib.sha1(safe_str(data) + salt).hexdigest()
279 return hashlib.sha1(safe_str(data) + salt).hexdigest()
280
280
281
281
282 class CookieStoreWrapper(object):
282 class CookieStoreWrapper(object):
283
283
284 def __init__(self, cookie_store):
284 def __init__(self, cookie_store):
285 self.cookie_store = cookie_store
285 self.cookie_store = cookie_store
286
286
287 def __repr__(self):
287 def __repr__(self):
288 return 'CookieStore<%s>' % (self.cookie_store)
288 return 'CookieStore<%s>' % (self.cookie_store)
289
289
290 def get(self, key, other=None):
290 def get(self, key, other=None):
291 if isinstance(self.cookie_store, dict):
291 if isinstance(self.cookie_store, dict):
292 return self.cookie_store.get(key, other)
292 return self.cookie_store.get(key, other)
293 elif isinstance(self.cookie_store, AuthUser):
293 elif isinstance(self.cookie_store, AuthUser):
294 return self.cookie_store.__dict__.get(key, other)
294 return self.cookie_store.__dict__.get(key, other)
295
295
296
296
297 def _cached_perms_data(user_id, scope, user_is_admin,
297 def _cached_perms_data(user_id, scope, user_is_admin,
298 user_inherit_default_permissions, explicit, algo):
298 user_inherit_default_permissions, explicit, algo):
299
299
300 permissions = PermissionCalculator(
300 permissions = PermissionCalculator(
301 user_id, scope, user_is_admin, user_inherit_default_permissions,
301 user_id, scope, user_is_admin, user_inherit_default_permissions,
302 explicit, algo)
302 explicit, algo)
303 return permissions.calculate()
303 return permissions.calculate()
304
304
305 class PermOrigin:
305 class PermOrigin:
306 ADMIN = 'superadmin'
306 ADMIN = 'superadmin'
307
307
308 REPO_USER = 'user:%s'
308 REPO_USER = 'user:%s'
309 REPO_USERGROUP = 'usergroup:%s'
309 REPO_USERGROUP = 'usergroup:%s'
310 REPO_OWNER = 'repo.owner'
310 REPO_OWNER = 'repo.owner'
311 REPO_DEFAULT = 'repo.default'
311 REPO_DEFAULT = 'repo.default'
312 REPO_PRIVATE = 'repo.private'
312 REPO_PRIVATE = 'repo.private'
313
313
314 REPOGROUP_USER = 'user:%s'
314 REPOGROUP_USER = 'user:%s'
315 REPOGROUP_USERGROUP = 'usergroup:%s'
315 REPOGROUP_USERGROUP = 'usergroup:%s'
316 REPOGROUP_OWNER = 'group.owner'
316 REPOGROUP_OWNER = 'group.owner'
317 REPOGROUP_DEFAULT = 'group.default'
317 REPOGROUP_DEFAULT = 'group.default'
318
318
319 USERGROUP_USER = 'user:%s'
319 USERGROUP_USER = 'user:%s'
320 USERGROUP_USERGROUP = 'usergroup:%s'
320 USERGROUP_USERGROUP = 'usergroup:%s'
321 USERGROUP_OWNER = 'usergroup.owner'
321 USERGROUP_OWNER = 'usergroup.owner'
322 USERGROUP_DEFAULT = 'usergroup.default'
322 USERGROUP_DEFAULT = 'usergroup.default'
323
323
324
324
325 class PermOriginDict(dict):
325 class PermOriginDict(dict):
326 """
326 """
327 A special dict used for tracking permissions along with their origins.
327 A special dict used for tracking permissions along with their origins.
328
328
329 `__setitem__` has been overridden to expect a tuple(perm, origin)
329 `__setitem__` has been overridden to expect a tuple(perm, origin)
330 `__getitem__` will return only the perm
330 `__getitem__` will return only the perm
331 `.perm_origin_stack` will return the stack of (perm, origin) set per key
331 `.perm_origin_stack` will return the stack of (perm, origin) set per key
332
332
333 >>> perms = PermOriginDict()
333 >>> perms = PermOriginDict()
334 >>> perms['resource'] = 'read', 'default'
334 >>> perms['resource'] = 'read', 'default'
335 >>> perms['resource']
335 >>> perms['resource']
336 'read'
336 'read'
337 >>> perms['resource'] = 'write', 'admin'
337 >>> perms['resource'] = 'write', 'admin'
338 >>> perms['resource']
338 >>> perms['resource']
339 'write'
339 'write'
340 >>> perms.perm_origin_stack
340 >>> perms.perm_origin_stack
341 {'resource': [('read', 'default'), ('write', 'admin')]}
341 {'resource': [('read', 'default'), ('write', 'admin')]}
342 """
342 """
343
343
344
344
345 def __init__(self, *args, **kw):
345 def __init__(self, *args, **kw):
346 dict.__init__(self, *args, **kw)
346 dict.__init__(self, *args, **kw)
347 self.perm_origin_stack = {}
347 self.perm_origin_stack = {}
348
348
349 def __setitem__(self, key, (perm, origin)):
349 def __setitem__(self, key, (perm, origin)):
350 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
350 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
351 dict.__setitem__(self, key, perm)
351 dict.__setitem__(self, key, perm)
352
352
353
353
354 class PermissionCalculator(object):
354 class PermissionCalculator(object):
355
355
356 def __init__(
356 def __init__(
357 self, user_id, scope, user_is_admin,
357 self, user_id, scope, user_is_admin,
358 user_inherit_default_permissions, explicit, algo):
358 user_inherit_default_permissions, explicit, algo):
359 self.user_id = user_id
359 self.user_id = user_id
360 self.user_is_admin = user_is_admin
360 self.user_is_admin = user_is_admin
361 self.inherit_default_permissions = user_inherit_default_permissions
361 self.inherit_default_permissions = user_inherit_default_permissions
362 self.explicit = explicit
362 self.explicit = explicit
363 self.algo = algo
363 self.algo = algo
364
364
365 scope = scope or {}
365 scope = scope or {}
366 self.scope_repo_id = scope.get('repo_id')
366 self.scope_repo_id = scope.get('repo_id')
367 self.scope_repo_group_id = scope.get('repo_group_id')
367 self.scope_repo_group_id = scope.get('repo_group_id')
368 self.scope_user_group_id = scope.get('user_group_id')
368 self.scope_user_group_id = scope.get('user_group_id')
369
369
370 self.default_user_id = User.get_default_user(cache=True).user_id
370 self.default_user_id = User.get_default_user(cache=True).user_id
371
371
372 self.permissions_repositories = PermOriginDict()
372 self.permissions_repositories = PermOriginDict()
373 self.permissions_repository_groups = PermOriginDict()
373 self.permissions_repository_groups = PermOriginDict()
374 self.permissions_user_groups = PermOriginDict()
374 self.permissions_user_groups = PermOriginDict()
375 self.permissions_global = set()
375 self.permissions_global = set()
376
376
377 self.default_repo_perms = Permission.get_default_repo_perms(
377 self.default_repo_perms = Permission.get_default_repo_perms(
378 self.default_user_id, self.scope_repo_id)
378 self.default_user_id, self.scope_repo_id)
379 self.default_repo_groups_perms = Permission.get_default_group_perms(
379 self.default_repo_groups_perms = Permission.get_default_group_perms(
380 self.default_user_id, self.scope_repo_group_id)
380 self.default_user_id, self.scope_repo_group_id)
381 self.default_user_group_perms = \
381 self.default_user_group_perms = \
382 Permission.get_default_user_group_perms(
382 Permission.get_default_user_group_perms(
383 self.default_user_id, self.scope_user_group_id)
383 self.default_user_id, self.scope_user_group_id)
384
384
385 def calculate(self):
385 def calculate(self):
386 if self.user_is_admin:
386 if self.user_is_admin:
387 return self._admin_permissions()
387 return self._admin_permissions()
388
388
389 self._calculate_global_default_permissions()
389 self._calculate_global_default_permissions()
390 self._calculate_global_permissions()
390 self._calculate_global_permissions()
391 self._calculate_default_permissions()
391 self._calculate_default_permissions()
392 self._calculate_repository_permissions()
392 self._calculate_repository_permissions()
393 self._calculate_repository_group_permissions()
393 self._calculate_repository_group_permissions()
394 self._calculate_user_group_permissions()
394 self._calculate_user_group_permissions()
395 return self._permission_structure()
395 return self._permission_structure()
396
396
397 def _admin_permissions(self):
397 def _admin_permissions(self):
398 """
398 """
399 admin user have all default rights for repositories
399 admin user have all default rights for repositories
400 and groups set to admin
400 and groups set to admin
401 """
401 """
402 self.permissions_global.add('hg.admin')
402 self.permissions_global.add('hg.admin')
403 self.permissions_global.add('hg.create.write_on_repogroup.true')
403 self.permissions_global.add('hg.create.write_on_repogroup.true')
404
404
405 # repositories
405 # repositories
406 for perm in self.default_repo_perms:
406 for perm in self.default_repo_perms:
407 r_k = perm.UserRepoToPerm.repository.repo_name
407 r_k = perm.UserRepoToPerm.repository.repo_name
408 p = 'repository.admin'
408 p = 'repository.admin'
409 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
409 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
410
410
411 # repository groups
411 # repository groups
412 for perm in self.default_repo_groups_perms:
412 for perm in self.default_repo_groups_perms:
413 rg_k = perm.UserRepoGroupToPerm.group.group_name
413 rg_k = perm.UserRepoGroupToPerm.group.group_name
414 p = 'group.admin'
414 p = 'group.admin'
415 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
415 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
416
416
417 # user groups
417 # user groups
418 for perm in self.default_user_group_perms:
418 for perm in self.default_user_group_perms:
419 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
419 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
420 p = 'usergroup.admin'
420 p = 'usergroup.admin'
421 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
421 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
422
422
423 return self._permission_structure()
423 return self._permission_structure()
424
424
425 def _calculate_global_default_permissions(self):
425 def _calculate_global_default_permissions(self):
426 """
426 """
427 global permissions taken from the default user
427 global permissions taken from the default user
428 """
428 """
429 default_global_perms = UserToPerm.query()\
429 default_global_perms = UserToPerm.query()\
430 .filter(UserToPerm.user_id == self.default_user_id)\
430 .filter(UserToPerm.user_id == self.default_user_id)\
431 .options(joinedload(UserToPerm.permission))
431 .options(joinedload(UserToPerm.permission))
432
432
433 for perm in default_global_perms:
433 for perm in default_global_perms:
434 self.permissions_global.add(perm.permission.permission_name)
434 self.permissions_global.add(perm.permission.permission_name)
435
435
436 def _calculate_global_permissions(self):
436 def _calculate_global_permissions(self):
437 """
437 """
438 Set global system permissions with user permissions or permissions
438 Set global system permissions with user permissions or permissions
439 taken from the user groups of the current user.
439 taken from the user groups of the current user.
440
440
441 The permissions include repo creating, repo group creating, forking
441 The permissions include repo creating, repo group creating, forking
442 etc.
442 etc.
443 """
443 """
444
444
445 # now we read the defined permissions and overwrite what we have set
445 # now we read the defined permissions and overwrite what we have set
446 # before those can be configured from groups or users explicitly.
446 # before those can be configured from groups or users explicitly.
447
447
448 # TODO: johbo: This seems to be out of sync, find out the reason
448 # TODO: johbo: This seems to be out of sync, find out the reason
449 # for the comment below and update it.
449 # for the comment below and update it.
450
450
451 # In case we want to extend this list we should be always in sync with
451 # In case we want to extend this list we should be always in sync with
452 # User.DEFAULT_USER_PERMISSIONS definitions
452 # User.DEFAULT_USER_PERMISSIONS definitions
453 _configurable = frozenset([
453 _configurable = frozenset([
454 'hg.fork.none', 'hg.fork.repository',
454 'hg.fork.none', 'hg.fork.repository',
455 'hg.create.none', 'hg.create.repository',
455 'hg.create.none', 'hg.create.repository',
456 'hg.usergroup.create.false', 'hg.usergroup.create.true',
456 'hg.usergroup.create.false', 'hg.usergroup.create.true',
457 'hg.repogroup.create.false', 'hg.repogroup.create.true',
457 'hg.repogroup.create.false', 'hg.repogroup.create.true',
458 'hg.create.write_on_repogroup.false',
458 'hg.create.write_on_repogroup.false',
459 'hg.create.write_on_repogroup.true',
459 'hg.create.write_on_repogroup.true',
460 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
460 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
461 ])
461 ])
462
462
463 # USER GROUPS comes first user group global permissions
463 # USER GROUPS comes first user group global permissions
464 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
464 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
465 .options(joinedload(UserGroupToPerm.permission))\
465 .options(joinedload(UserGroupToPerm.permission))\
466 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
466 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
467 UserGroupMember.users_group_id))\
467 UserGroupMember.users_group_id))\
468 .filter(UserGroupMember.user_id == self.user_id)\
468 .filter(UserGroupMember.user_id == self.user_id)\
469 .order_by(UserGroupToPerm.users_group_id)\
469 .order_by(UserGroupToPerm.users_group_id)\
470 .all()
470 .all()
471
471
472 # need to group here by groups since user can be in more than
472 # need to group here by groups since user can be in more than
473 # one group, so we get all groups
473 # one group, so we get all groups
474 _explicit_grouped_perms = [
474 _explicit_grouped_perms = [
475 [x, list(y)] for x, y in
475 [x, list(y)] for x, y in
476 itertools.groupby(user_perms_from_users_groups,
476 itertools.groupby(user_perms_from_users_groups,
477 lambda _x: _x.users_group)]
477 lambda _x: _x.users_group)]
478
478
479 for gr, perms in _explicit_grouped_perms:
479 for gr, perms in _explicit_grouped_perms:
480 # since user can be in multiple groups iterate over them and
480 # since user can be in multiple groups iterate over them and
481 # select the lowest permissions first (more explicit)
481 # select the lowest permissions first (more explicit)
482 # TODO: marcink: do this^^
482 # TODO: marcink: do this^^
483
483
484 # group doesn't inherit default permissions so we actually set them
484 # group doesn't inherit default permissions so we actually set them
485 if not gr.inherit_default_permissions:
485 if not gr.inherit_default_permissions:
486 # NEED TO IGNORE all previously set configurable permissions
486 # NEED TO IGNORE all previously set configurable permissions
487 # and replace them with explicitly set from this user
487 # and replace them with explicitly set from this user
488 # group permissions
488 # group permissions
489 self.permissions_global = self.permissions_global.difference(
489 self.permissions_global = self.permissions_global.difference(
490 _configurable)
490 _configurable)
491 for perm in perms:
491 for perm in perms:
492 self.permissions_global.add(perm.permission.permission_name)
492 self.permissions_global.add(perm.permission.permission_name)
493
493
494 # user explicit global permissions
494 # user explicit global permissions
495 user_perms = Session().query(UserToPerm)\
495 user_perms = Session().query(UserToPerm)\
496 .options(joinedload(UserToPerm.permission))\
496 .options(joinedload(UserToPerm.permission))\
497 .filter(UserToPerm.user_id == self.user_id).all()
497 .filter(UserToPerm.user_id == self.user_id).all()
498
498
499 if not self.inherit_default_permissions:
499 if not self.inherit_default_permissions:
500 # NEED TO IGNORE all configurable permissions and
500 # NEED TO IGNORE all configurable permissions and
501 # replace them with explicitly set from this user permissions
501 # replace them with explicitly set from this user permissions
502 self.permissions_global = self.permissions_global.difference(
502 self.permissions_global = self.permissions_global.difference(
503 _configurable)
503 _configurable)
504 for perm in user_perms:
504 for perm in user_perms:
505 self.permissions_global.add(perm.permission.permission_name)
505 self.permissions_global.add(perm.permission.permission_name)
506
506
507 def _calculate_default_permissions(self):
507 def _calculate_default_permissions(self):
508 """
508 """
509 Set default user permissions for repositories, repository groups
509 Set default user permissions for repositories, repository groups
510 taken from the default user.
510 taken from the default user.
511
511
512 Calculate inheritance of object permissions based on what we have now
512 Calculate inheritance of object permissions based on what we have now
513 in GLOBAL permissions. We check if .false is in GLOBAL since this is
513 in GLOBAL permissions. We check if .false is in GLOBAL since this is
514 explicitly set. Inherit is the opposite of .false being there.
514 explicitly set. Inherit is the opposite of .false being there.
515
515
516 .. note::
516 .. note::
517
517
518 the syntax is little bit odd but what we need to check here is
518 the syntax is little bit odd but what we need to check here is
519 the opposite of .false permission being in the list so even for
519 the opposite of .false permission being in the list so even for
520 inconsistent state when both .true/.false is there
520 inconsistent state when both .true/.false is there
521 .false is more important
521 .false is more important
522
522
523 """
523 """
524 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
524 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
525 in self.permissions_global)
525 in self.permissions_global)
526
526
527 # defaults for repositories, taken from `default` user permissions
527 # defaults for repositories, taken from `default` user permissions
528 # on given repo
528 # on given repo
529 for perm in self.default_repo_perms:
529 for perm in self.default_repo_perms:
530 r_k = perm.UserRepoToPerm.repository.repo_name
530 r_k = perm.UserRepoToPerm.repository.repo_name
531 o = PermOrigin.REPO_DEFAULT
531 o = PermOrigin.REPO_DEFAULT
532 if perm.Repository.private and not (
532 if perm.Repository.private and not (
533 perm.Repository.user_id == self.user_id):
533 perm.Repository.user_id == self.user_id):
534 # disable defaults for private repos,
534 # disable defaults for private repos,
535 p = 'repository.none'
535 p = 'repository.none'
536 o = PermOrigin.REPO_PRIVATE
536 o = PermOrigin.REPO_PRIVATE
537 elif perm.Repository.user_id == self.user_id:
537 elif perm.Repository.user_id == self.user_id:
538 # set admin if owner
538 # set admin if owner
539 p = 'repository.admin'
539 p = 'repository.admin'
540 o = PermOrigin.REPO_OWNER
540 o = PermOrigin.REPO_OWNER
541 else:
541 else:
542 p = perm.Permission.permission_name
542 p = perm.Permission.permission_name
543 # if we decide this user isn't inheriting permissions from
543 # if we decide this user isn't inheriting permissions from
544 # default user we set him to .none so only explicit
544 # default user we set him to .none so only explicit
545 # permissions work
545 # permissions work
546 if not user_inherit_object_permissions:
546 if not user_inherit_object_permissions:
547 p = 'repository.none'
547 p = 'repository.none'
548 self.permissions_repositories[r_k] = p, o
548 self.permissions_repositories[r_k] = p, o
549
549
550 # defaults for repository groups taken from `default` user permission
550 # defaults for repository groups taken from `default` user permission
551 # on given group
551 # on given group
552 for perm in self.default_repo_groups_perms:
552 for perm in self.default_repo_groups_perms:
553 rg_k = perm.UserRepoGroupToPerm.group.group_name
553 rg_k = perm.UserRepoGroupToPerm.group.group_name
554 o = PermOrigin.REPOGROUP_DEFAULT
554 o = PermOrigin.REPOGROUP_DEFAULT
555 if perm.RepoGroup.user_id == self.user_id:
555 if perm.RepoGroup.user_id == self.user_id:
556 # set admin if owner
556 # set admin if owner
557 p = 'group.admin'
557 p = 'group.admin'
558 o = PermOrigin.REPOGROUP_OWNER
558 o = PermOrigin.REPOGROUP_OWNER
559 else:
559 else:
560 p = perm.Permission.permission_name
560 p = perm.Permission.permission_name
561
561
562 # if we decide this user isn't inheriting permissions from default
562 # if we decide this user isn't inheriting permissions from default
563 # user we set him to .none so only explicit permissions work
563 # user we set him to .none so only explicit permissions work
564 if not user_inherit_object_permissions:
564 if not user_inherit_object_permissions:
565 p = 'group.none'
565 p = 'group.none'
566 self.permissions_repository_groups[rg_k] = p, o
566 self.permissions_repository_groups[rg_k] = p, o
567
567
568 # defaults for user groups taken from `default` user permission
568 # defaults for user groups taken from `default` user permission
569 # on given user group
569 # on given user group
570 for perm in self.default_user_group_perms:
570 for perm in self.default_user_group_perms:
571 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
571 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
572 o = PermOrigin.USERGROUP_DEFAULT
572 o = PermOrigin.USERGROUP_DEFAULT
573 if perm.UserGroup.user_id == self.user_id:
573 if perm.UserGroup.user_id == self.user_id:
574 # set admin if owner
574 # set admin if owner
575 p = 'usergroup.admin'
575 p = 'usergroup.admin'
576 o = PermOrigin.USERGROUP_OWNER
576 o = PermOrigin.USERGROUP_OWNER
577 else:
577 else:
578 p = perm.Permission.permission_name
578 p = perm.Permission.permission_name
579
579
580 # if we decide this user isn't inheriting permissions from default
580 # if we decide this user isn't inheriting permissions from default
581 # user we set him to .none so only explicit permissions work
581 # user we set him to .none so only explicit permissions work
582 if not user_inherit_object_permissions:
582 if not user_inherit_object_permissions:
583 p = 'usergroup.none'
583 p = 'usergroup.none'
584 self.permissions_user_groups[u_k] = p, o
584 self.permissions_user_groups[u_k] = p, o
585
585
586 def _calculate_repository_permissions(self):
586 def _calculate_repository_permissions(self):
587 """
587 """
588 Repository permissions for the current user.
588 Repository permissions for the current user.
589
589
590 Check if the user is part of user groups for this repository and
590 Check if the user is part of user groups for this repository and
591 fill in the permission from it. `_choose_permission` decides of which
591 fill in the permission from it. `_choose_permission` decides of which
592 permission should be selected based on selected method.
592 permission should be selected based on selected method.
593 """
593 """
594
594
595 # user group for repositories permissions
595 # user group for repositories permissions
596 user_repo_perms_from_user_group = Permission\
596 user_repo_perms_from_user_group = Permission\
597 .get_default_repo_perms_from_user_group(
597 .get_default_repo_perms_from_user_group(
598 self.user_id, self.scope_repo_id)
598 self.user_id, self.scope_repo_id)
599
599
600 multiple_counter = collections.defaultdict(int)
600 multiple_counter = collections.defaultdict(int)
601 for perm in user_repo_perms_from_user_group:
601 for perm in user_repo_perms_from_user_group:
602 r_k = perm.UserGroupRepoToPerm.repository.repo_name
602 r_k = perm.UserGroupRepoToPerm.repository.repo_name
603 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
603 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
604 multiple_counter[r_k] += 1
604 multiple_counter[r_k] += 1
605 p = perm.Permission.permission_name
605 p = perm.Permission.permission_name
606 o = PermOrigin.REPO_USERGROUP % ug_k
606 o = PermOrigin.REPO_USERGROUP % ug_k
607
607
608 if perm.Repository.user_id == self.user_id:
608 if perm.Repository.user_id == self.user_id:
609 # set admin if owner
609 # set admin if owner
610 p = 'repository.admin'
610 p = 'repository.admin'
611 o = PermOrigin.REPO_OWNER
611 o = PermOrigin.REPO_OWNER
612 else:
612 else:
613 if multiple_counter[r_k] > 1:
613 if multiple_counter[r_k] > 1:
614 cur_perm = self.permissions_repositories[r_k]
614 cur_perm = self.permissions_repositories[r_k]
615 p = self._choose_permission(p, cur_perm)
615 p = self._choose_permission(p, cur_perm)
616 self.permissions_repositories[r_k] = p, o
616 self.permissions_repositories[r_k] = p, o
617
617
618 # user explicit permissions for repositories, overrides any specified
618 # user explicit permissions for repositories, overrides any specified
619 # by the group permission
619 # by the group permission
620 user_repo_perms = Permission.get_default_repo_perms(
620 user_repo_perms = Permission.get_default_repo_perms(
621 self.user_id, self.scope_repo_id)
621 self.user_id, self.scope_repo_id)
622 for perm in user_repo_perms:
622 for perm in user_repo_perms:
623 r_k = perm.UserRepoToPerm.repository.repo_name
623 r_k = perm.UserRepoToPerm.repository.repo_name
624 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
624 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
625 # set admin if owner
625 # set admin if owner
626 if perm.Repository.user_id == self.user_id:
626 if perm.Repository.user_id == self.user_id:
627 p = 'repository.admin'
627 p = 'repository.admin'
628 o = PermOrigin.REPO_OWNER
628 o = PermOrigin.REPO_OWNER
629 else:
629 else:
630 p = perm.Permission.permission_name
630 p = perm.Permission.permission_name
631 if not self.explicit:
631 if not self.explicit:
632 cur_perm = self.permissions_repositories.get(
632 cur_perm = self.permissions_repositories.get(
633 r_k, 'repository.none')
633 r_k, 'repository.none')
634 p = self._choose_permission(p, cur_perm)
634 p = self._choose_permission(p, cur_perm)
635 self.permissions_repositories[r_k] = p, o
635 self.permissions_repositories[r_k] = p, o
636
636
637 def _calculate_repository_group_permissions(self):
637 def _calculate_repository_group_permissions(self):
638 """
638 """
639 Repository group permissions for the current user.
639 Repository group permissions for the current user.
640
640
641 Check if the user is part of user groups for repository groups and
641 Check if the user is part of user groups for repository groups and
642 fill in the permissions from it. `_choose_permmission` decides of which
642 fill in the permissions from it. `_choose_permmission` decides of which
643 permission should be selected based on selected method.
643 permission should be selected based on selected method.
644 """
644 """
645 # user group for repo groups permissions
645 # user group for repo groups permissions
646 user_repo_group_perms_from_user_group = Permission\
646 user_repo_group_perms_from_user_group = Permission\
647 .get_default_group_perms_from_user_group(
647 .get_default_group_perms_from_user_group(
648 self.user_id, self.scope_repo_group_id)
648 self.user_id, self.scope_repo_group_id)
649
649
650 multiple_counter = collections.defaultdict(int)
650 multiple_counter = collections.defaultdict(int)
651 for perm in user_repo_group_perms_from_user_group:
651 for perm in user_repo_group_perms_from_user_group:
652 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
652 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
653 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
653 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
654 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
654 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
655 multiple_counter[g_k] += 1
655 multiple_counter[g_k] += 1
656 p = perm.Permission.permission_name
656 p = perm.Permission.permission_name
657 if perm.RepoGroup.user_id == self.user_id:
657 if perm.RepoGroup.user_id == self.user_id:
658 # set admin if owner, even for member of other user group
658 # set admin if owner, even for member of other user group
659 p = 'group.admin'
659 p = 'group.admin'
660 o = PermOrigin.REPOGROUP_OWNER
660 o = PermOrigin.REPOGROUP_OWNER
661 else:
661 else:
662 if multiple_counter[g_k] > 1:
662 if multiple_counter[g_k] > 1:
663 cur_perm = self.permissions_repository_groups[g_k]
663 cur_perm = self.permissions_repository_groups[g_k]
664 p = self._choose_permission(p, cur_perm)
664 p = self._choose_permission(p, cur_perm)
665 self.permissions_repository_groups[g_k] = p, o
665 self.permissions_repository_groups[g_k] = p, o
666
666
667 # user explicit permissions for repository groups
667 # user explicit permissions for repository groups
668 user_repo_groups_perms = Permission.get_default_group_perms(
668 user_repo_groups_perms = Permission.get_default_group_perms(
669 self.user_id, self.scope_repo_group_id)
669 self.user_id, self.scope_repo_group_id)
670 for perm in user_repo_groups_perms:
670 for perm in user_repo_groups_perms:
671 rg_k = perm.UserRepoGroupToPerm.group.group_name
671 rg_k = perm.UserRepoGroupToPerm.group.group_name
672 u_k = perm.UserRepoGroupToPerm.user.username
672 u_k = perm.UserRepoGroupToPerm.user.username
673 o = PermOrigin.REPOGROUP_USER % u_k
673 o = PermOrigin.REPOGROUP_USER % u_k
674
674
675 if perm.RepoGroup.user_id == self.user_id:
675 if perm.RepoGroup.user_id == self.user_id:
676 # set admin if owner
676 # set admin if owner
677 p = 'group.admin'
677 p = 'group.admin'
678 o = PermOrigin.REPOGROUP_OWNER
678 o = PermOrigin.REPOGROUP_OWNER
679 else:
679 else:
680 p = perm.Permission.permission_name
680 p = perm.Permission.permission_name
681 if not self.explicit:
681 if not self.explicit:
682 cur_perm = self.permissions_repository_groups.get(
682 cur_perm = self.permissions_repository_groups.get(
683 rg_k, 'group.none')
683 rg_k, 'group.none')
684 p = self._choose_permission(p, cur_perm)
684 p = self._choose_permission(p, cur_perm)
685 self.permissions_repository_groups[rg_k] = p, o
685 self.permissions_repository_groups[rg_k] = p, o
686
686
687 def _calculate_user_group_permissions(self):
687 def _calculate_user_group_permissions(self):
688 """
688 """
689 User group permissions for the current user.
689 User group permissions for the current user.
690 """
690 """
691 # user group for user group permissions
691 # user group for user group permissions
692 user_group_from_user_group = Permission\
692 user_group_from_user_group = Permission\
693 .get_default_user_group_perms_from_user_group(
693 .get_default_user_group_perms_from_user_group(
694 self.user_id, self.scope_user_group_id)
694 self.user_id, self.scope_user_group_id)
695
695
696 multiple_counter = collections.defaultdict(int)
696 multiple_counter = collections.defaultdict(int)
697 for perm in user_group_from_user_group:
697 for perm in user_group_from_user_group:
698 g_k = perm.UserGroupUserGroupToPerm\
698 g_k = perm.UserGroupUserGroupToPerm\
699 .target_user_group.users_group_name
699 .target_user_group.users_group_name
700 u_k = perm.UserGroupUserGroupToPerm\
700 u_k = perm.UserGroupUserGroupToPerm\
701 .user_group.users_group_name
701 .user_group.users_group_name
702 o = PermOrigin.USERGROUP_USERGROUP % u_k
702 o = PermOrigin.USERGROUP_USERGROUP % u_k
703 multiple_counter[g_k] += 1
703 multiple_counter[g_k] += 1
704 p = perm.Permission.permission_name
704 p = perm.Permission.permission_name
705
705
706 if perm.UserGroup.user_id == self.user_id:
706 if perm.UserGroup.user_id == self.user_id:
707 # set admin if owner, even for member of other user group
707 # set admin if owner, even for member of other user group
708 p = 'usergroup.admin'
708 p = 'usergroup.admin'
709 o = PermOrigin.USERGROUP_OWNER
709 o = PermOrigin.USERGROUP_OWNER
710 else:
710 else:
711 if multiple_counter[g_k] > 1:
711 if multiple_counter[g_k] > 1:
712 cur_perm = self.permissions_user_groups[g_k]
712 cur_perm = self.permissions_user_groups[g_k]
713 p = self._choose_permission(p, cur_perm)
713 p = self._choose_permission(p, cur_perm)
714 self.permissions_user_groups[g_k] = p, o
714 self.permissions_user_groups[g_k] = p, o
715
715
716 # user explicit permission for user groups
716 # user explicit permission for user groups
717 user_user_groups_perms = Permission.get_default_user_group_perms(
717 user_user_groups_perms = Permission.get_default_user_group_perms(
718 self.user_id, self.scope_user_group_id)
718 self.user_id, self.scope_user_group_id)
719 for perm in user_user_groups_perms:
719 for perm in user_user_groups_perms:
720 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
720 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
721 u_k = perm.UserUserGroupToPerm.user.username
721 u_k = perm.UserUserGroupToPerm.user.username
722 o = PermOrigin.USERGROUP_USER % u_k
722 o = PermOrigin.USERGROUP_USER % u_k
723
723
724 if perm.UserGroup.user_id == self.user_id:
724 if perm.UserGroup.user_id == self.user_id:
725 # set admin if owner
725 # set admin if owner
726 p = 'usergroup.admin'
726 p = 'usergroup.admin'
727 o = PermOrigin.USERGROUP_OWNER
727 o = PermOrigin.USERGROUP_OWNER
728 else:
728 else:
729 p = perm.Permission.permission_name
729 p = perm.Permission.permission_name
730 if not self.explicit:
730 if not self.explicit:
731 cur_perm = self.permissions_user_groups.get(
731 cur_perm = self.permissions_user_groups.get(
732 ug_k, 'usergroup.none')
732 ug_k, 'usergroup.none')
733 p = self._choose_permission(p, cur_perm)
733 p = self._choose_permission(p, cur_perm)
734 self.permissions_user_groups[ug_k] = p, o
734 self.permissions_user_groups[ug_k] = p, o
735
735
736 def _choose_permission(self, new_perm, cur_perm):
736 def _choose_permission(self, new_perm, cur_perm):
737 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
737 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
738 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
738 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
739 if self.algo == 'higherwin':
739 if self.algo == 'higherwin':
740 if new_perm_val > cur_perm_val:
740 if new_perm_val > cur_perm_val:
741 return new_perm
741 return new_perm
742 return cur_perm
742 return cur_perm
743 elif self.algo == 'lowerwin':
743 elif self.algo == 'lowerwin':
744 if new_perm_val < cur_perm_val:
744 if new_perm_val < cur_perm_val:
745 return new_perm
745 return new_perm
746 return cur_perm
746 return cur_perm
747
747
748 def _permission_structure(self):
748 def _permission_structure(self):
749 return {
749 return {
750 'global': self.permissions_global,
750 'global': self.permissions_global,
751 'repositories': self.permissions_repositories,
751 'repositories': self.permissions_repositories,
752 'repositories_groups': self.permissions_repository_groups,
752 'repositories_groups': self.permissions_repository_groups,
753 'user_groups': self.permissions_user_groups,
753 'user_groups': self.permissions_user_groups,
754 }
754 }
755
755
756
756
757 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
757 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
758 """
758 """
759 Check if given controller_name is in whitelist of auth token access
759 Check if given controller_name is in whitelist of auth token access
760 """
760 """
761 if not whitelist:
761 if not whitelist:
762 from rhodecode import CONFIG
762 from rhodecode import CONFIG
763 whitelist = aslist(
763 whitelist = aslist(
764 CONFIG.get('api_access_controllers_whitelist'), sep=',')
764 CONFIG.get('api_access_controllers_whitelist'), sep=',')
765 log.debug(
765 log.debug(
766 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
766 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
767
767
768 auth_token_access_valid = False
768 auth_token_access_valid = False
769 for entry in whitelist:
769 for entry in whitelist:
770 if fnmatch.fnmatch(controller_name, entry):
770 if fnmatch.fnmatch(controller_name, entry):
771 auth_token_access_valid = True
771 auth_token_access_valid = True
772 break
772 break
773
773
774 if auth_token_access_valid:
774 if auth_token_access_valid:
775 log.debug('controller:%s matches entry in whitelist'
775 log.debug('controller:%s matches entry in whitelist'
776 % (controller_name,))
776 % (controller_name,))
777 else:
777 else:
778 msg = ('controller: %s does *NOT* match any entry in whitelist'
778 msg = ('controller: %s does *NOT* match any entry in whitelist'
779 % (controller_name,))
779 % (controller_name,))
780 if auth_token:
780 if auth_token:
781 # if we use auth token key and don't have access it's a warning
781 # if we use auth token key and don't have access it's a warning
782 log.warning(msg)
782 log.warning(msg)
783 else:
783 else:
784 log.debug(msg)
784 log.debug(msg)
785
785
786 return auth_token_access_valid
786 return auth_token_access_valid
787
787
788
788
789 class AuthUser(object):
789 class AuthUser(object):
790 """
790 """
791 A simple object that handles all attributes of user in RhodeCode
791 A simple object that handles all attributes of user in RhodeCode
792
792
793 It does lookup based on API key,given user, or user present in session
793 It does lookup based on API key,given user, or user present in session
794 Then it fills all required information for such user. It also checks if
794 Then it fills all required information for such user. It also checks if
795 anonymous access is enabled and if so, it returns default user as logged in
795 anonymous access is enabled and if so, it returns default user as logged in
796 """
796 """
797 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
797 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
798
798
799 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
799 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
800
800
801 self.user_id = user_id
801 self.user_id = user_id
802 self._api_key = api_key
802 self._api_key = api_key
803
803
804 self.api_key = None
804 self.api_key = None
805 self.feed_token = ''
805 self.feed_token = ''
806 self.username = username
806 self.username = username
807 self.ip_addr = ip_addr
807 self.ip_addr = ip_addr
808 self.name = ''
808 self.name = ''
809 self.lastname = ''
809 self.lastname = ''
810 self.email = ''
810 self.email = ''
811 self.is_authenticated = False
811 self.is_authenticated = False
812 self.admin = False
812 self.admin = False
813 self.inherit_default_permissions = False
813 self.inherit_default_permissions = False
814 self.password = ''
814 self.password = ''
815
815
816 self.anonymous_user = None # propagated on propagate_data
816 self.anonymous_user = None # propagated on propagate_data
817 self.propagate_data()
817 self.propagate_data()
818 self._instance = None
818 self._instance = None
819 self._permissions_scoped_cache = {} # used to bind scoped calculation
819 self._permissions_scoped_cache = {} # used to bind scoped calculation
820
820
821 @LazyProperty
821 @LazyProperty
822 def permissions(self):
822 def permissions(self):
823 return self.get_perms(user=self, cache=False)
823 return self.get_perms(user=self, cache=False)
824
824
825 def permissions_with_scope(self, scope):
825 def permissions_with_scope(self, scope):
826 """
826 """
827 Call the get_perms function with scoped data. The scope in that function
827 Call the get_perms function with scoped data. The scope in that function
828 narrows the SQL calls to the given ID of objects resulting in fetching
828 narrows the SQL calls to the given ID of objects resulting in fetching
829 Just particular permission we want to obtain. If scope is an empty dict
829 Just particular permission we want to obtain. If scope is an empty dict
830 then it basically narrows the scope to GLOBAL permissions only.
830 then it basically narrows the scope to GLOBAL permissions only.
831
831
832 :param scope: dict
832 :param scope: dict
833 """
833 """
834 if 'repo_name' in scope:
834 if 'repo_name' in scope:
835 obj = Repository.get_by_repo_name(scope['repo_name'])
835 obj = Repository.get_by_repo_name(scope['repo_name'])
836 if obj:
836 if obj:
837 scope['repo_id'] = obj.repo_id
837 scope['repo_id'] = obj.repo_id
838 _scope = {
838 _scope = {
839 'repo_id': -1,
839 'repo_id': -1,
840 'user_group_id': -1,
840 'user_group_id': -1,
841 'repo_group_id': -1,
841 'repo_group_id': -1,
842 }
842 }
843 _scope.update(scope)
843 _scope.update(scope)
844 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
844 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
845 _scope.items())))
845 _scope.items())))
846 if cache_key not in self._permissions_scoped_cache:
846 if cache_key not in self._permissions_scoped_cache:
847 # store in cache to mimic how the @LazyProperty works,
847 # store in cache to mimic how the @LazyProperty works,
848 # the difference here is that we use the unique key calculated
848 # the difference here is that we use the unique key calculated
849 # from params and values
849 # from params and values
850 res = self.get_perms(user=self, cache=False, scope=_scope)
850 res = self.get_perms(user=self, cache=False, scope=_scope)
851 self._permissions_scoped_cache[cache_key] = res
851 self._permissions_scoped_cache[cache_key] = res
852 return self._permissions_scoped_cache[cache_key]
852 return self._permissions_scoped_cache[cache_key]
853
853
854 def get_instance(self):
854 def get_instance(self):
855 return User.get(self.user_id)
855 return User.get(self.user_id)
856
856
857 def update_lastactivity(self):
857 def update_lastactivity(self):
858 if self.user_id:
858 if self.user_id:
859 User.get(self.user_id).update_lastactivity()
859 User.get(self.user_id).update_lastactivity()
860
860
861 def propagate_data(self):
861 def propagate_data(self):
862 """
862 """
863 Fills in user data and propagates values to this instance. Maps fetched
863 Fills in user data and propagates values to this instance. Maps fetched
864 user attributes to this class instance attributes
864 user attributes to this class instance attributes
865 """
865 """
866 log.debug('starting data propagation for new potential AuthUser')
866 log.debug('starting data propagation for new potential AuthUser')
867 user_model = UserModel()
867 user_model = UserModel()
868 anon_user = self.anonymous_user = User.get_default_user(cache=True)
868 anon_user = self.anonymous_user = User.get_default_user(cache=True)
869 is_user_loaded = False
869 is_user_loaded = False
870
870
871 # lookup by userid
871 # lookup by userid
872 if self.user_id is not None and self.user_id != anon_user.user_id:
872 if self.user_id is not None and self.user_id != anon_user.user_id:
873 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
873 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
874 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
874 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
875
875
876 # try go get user by api key
876 # try go get user by api key
877 elif self._api_key and self._api_key != anon_user.api_key:
877 elif self._api_key and self._api_key != anon_user.api_key:
878 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
878 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
879 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
879 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
880
880
881 # lookup by username
881 # lookup by username
882 elif self.username:
882 elif self.username:
883 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
883 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
884 is_user_loaded = user_model.fill_data(self, username=self.username)
884 is_user_loaded = user_model.fill_data(self, username=self.username)
885 else:
885 else:
886 log.debug('No data in %s that could been used to log in' % self)
886 log.debug('No data in %s that could been used to log in' % self)
887
887
888 if not is_user_loaded:
888 if not is_user_loaded:
889 log.debug('Failed to load user. Fallback to default user')
889 log.debug('Failed to load user. Fallback to default user')
890 # if we cannot authenticate user try anonymous
890 # if we cannot authenticate user try anonymous
891 if anon_user.active:
891 if anon_user.active:
892 user_model.fill_data(self, user_id=anon_user.user_id)
892 user_model.fill_data(self, user_id=anon_user.user_id)
893 # then we set this user is logged in
893 # then we set this user is logged in
894 self.is_authenticated = True
894 self.is_authenticated = True
895 else:
895 else:
896 # in case of disabled anonymous user we reset some of the
896 # in case of disabled anonymous user we reset some of the
897 # parameters so such user is "corrupted", skipping the fill_data
897 # parameters so such user is "corrupted", skipping the fill_data
898 for attr in ['user_id', 'username', 'admin', 'active']:
898 for attr in ['user_id', 'username', 'admin', 'active']:
899 setattr(self, attr, None)
899 setattr(self, attr, None)
900 self.is_authenticated = False
900 self.is_authenticated = False
901
901
902 if not self.username:
902 if not self.username:
903 self.username = 'None'
903 self.username = 'None'
904
904
905 log.debug('Auth User is now %s' % self)
905 log.debug('Auth User is now %s' % self)
906
906
907 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
907 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
908 cache=False):
908 cache=False):
909 """
909 """
910 Fills user permission attribute with permissions taken from database
910 Fills user permission attribute with permissions taken from database
911 works for permissions given for repositories, and for permissions that
911 works for permissions given for repositories, and for permissions that
912 are granted to groups
912 are granted to groups
913
913
914 :param user: instance of User object from database
914 :param user: instance of User object from database
915 :param explicit: In case there are permissions both for user and a group
915 :param explicit: In case there are permissions both for user and a group
916 that user is part of, explicit flag will defiine if user will
916 that user is part of, explicit flag will defiine if user will
917 explicitly override permissions from group, if it's False it will
917 explicitly override permissions from group, if it's False it will
918 make decision based on the algo
918 make decision based on the algo
919 :param algo: algorithm to decide what permission should be choose if
919 :param algo: algorithm to decide what permission should be choose if
920 it's multiple defined, eg user in two different groups. It also
920 it's multiple defined, eg user in two different groups. It also
921 decides if explicit flag is turned off how to specify the permission
921 decides if explicit flag is turned off how to specify the permission
922 for case when user is in a group + have defined separate permission
922 for case when user is in a group + have defined separate permission
923 """
923 """
924 user_id = user.user_id
924 user_id = user.user_id
925 user_is_admin = user.is_admin
925 user_is_admin = user.is_admin
926
926
927 # inheritance of global permissions like create repo/fork repo etc
927 # inheritance of global permissions like create repo/fork repo etc
928 user_inherit_default_permissions = user.inherit_default_permissions
928 user_inherit_default_permissions = user.inherit_default_permissions
929
929
930 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
930 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
931 compute = caches.conditional_cache(
931 compute = caches.conditional_cache(
932 'short_term', 'cache_desc',
932 'short_term', 'cache_desc',
933 condition=cache, func=_cached_perms_data)
933 condition=cache, func=_cached_perms_data)
934 result = compute(user_id, scope, user_is_admin,
934 result = compute(user_id, scope, user_is_admin,
935 user_inherit_default_permissions, explicit, algo)
935 user_inherit_default_permissions, explicit, algo)
936
936
937 result_repr = []
937 result_repr = []
938 for k in result:
938 for k in result:
939 result_repr.append((k, len(result[k])))
939 result_repr.append((k, len(result[k])))
940
940
941 log.debug('PERMISSION tree computed %s' % (result_repr,))
941 log.debug('PERMISSION tree computed %s' % (result_repr,))
942 return result
942 return result
943
943
944 @property
944 @property
945 def is_default(self):
945 def is_default(self):
946 return self.username == User.DEFAULT_USER
946 return self.username == User.DEFAULT_USER
947
947
948 @property
948 @property
949 def is_admin(self):
949 def is_admin(self):
950 return self.admin
950 return self.admin
951
951
952 @property
952 @property
953 def is_user_object(self):
953 def is_user_object(self):
954 return self.user_id is not None
954 return self.user_id is not None
955
955
956 @property
956 @property
957 def repositories_admin(self):
957 def repositories_admin(self):
958 """
958 """
959 Returns list of repositories you're an admin of
959 Returns list of repositories you're an admin of
960 """
960 """
961 return [
961 return [
962 x[0] for x in self.permissions['repositories'].iteritems()
962 x[0] for x in self.permissions['repositories'].iteritems()
963 if x[1] == 'repository.admin']
963 if x[1] == 'repository.admin']
964
964
965 @property
965 @property
966 def repository_groups_admin(self):
966 def repository_groups_admin(self):
967 """
967 """
968 Returns list of repository groups you're an admin of
968 Returns list of repository groups you're an admin of
969 """
969 """
970 return [
970 return [
971 x[0] for x in self.permissions['repositories_groups'].iteritems()
971 x[0] for x in self.permissions['repositories_groups'].iteritems()
972 if x[1] == 'group.admin']
972 if x[1] == 'group.admin']
973
973
974 @property
974 @property
975 def user_groups_admin(self):
975 def user_groups_admin(self):
976 """
976 """
977 Returns list of user groups you're an admin of
977 Returns list of user groups you're an admin of
978 """
978 """
979 return [
979 return [
980 x[0] for x in self.permissions['user_groups'].iteritems()
980 x[0] for x in self.permissions['user_groups'].iteritems()
981 if x[1] == 'usergroup.admin']
981 if x[1] == 'usergroup.admin']
982
982
983 @property
983 @property
984 def ip_allowed(self):
984 def ip_allowed(self):
985 """
985 """
986 Checks if ip_addr used in constructor is allowed from defined list of
986 Checks if ip_addr used in constructor is allowed from defined list of
987 allowed ip_addresses for user
987 allowed ip_addresses for user
988
988
989 :returns: boolean, True if ip is in allowed ip range
989 :returns: boolean, True if ip is in allowed ip range
990 """
990 """
991 # check IP
991 # check IP
992 inherit = self.inherit_default_permissions
992 inherit = self.inherit_default_permissions
993 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
993 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
994 inherit_from_default=inherit)
994 inherit_from_default=inherit)
995 @property
995 @property
996 def personal_repo_group(self):
996 def personal_repo_group(self):
997 return RepoGroup.get_user_personal_repo_group(self.user_id)
997 return RepoGroup.get_user_personal_repo_group(self.user_id)
998
998
999 @classmethod
999 @classmethod
1000 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1000 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1001 allowed_ips = AuthUser.get_allowed_ips(
1001 allowed_ips = AuthUser.get_allowed_ips(
1002 user_id, cache=True, inherit_from_default=inherit_from_default)
1002 user_id, cache=True, inherit_from_default=inherit_from_default)
1003 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1003 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1004 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
1004 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
1005 return True
1005 return True
1006 else:
1006 else:
1007 log.info('Access for IP:%s forbidden, '
1007 log.info('Access for IP:%s forbidden, '
1008 'not in %s' % (ip_addr, allowed_ips))
1008 'not in %s' % (ip_addr, allowed_ips))
1009 return False
1009 return False
1010
1010
1011 def __repr__(self):
1011 def __repr__(self):
1012 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1012 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1013 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1013 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1014
1014
1015 def set_authenticated(self, authenticated=True):
1015 def set_authenticated(self, authenticated=True):
1016 if self.user_id != self.anonymous_user.user_id:
1016 if self.user_id != self.anonymous_user.user_id:
1017 self.is_authenticated = authenticated
1017 self.is_authenticated = authenticated
1018
1018
1019 def get_cookie_store(self):
1019 def get_cookie_store(self):
1020 return {
1020 return {
1021 'username': self.username,
1021 'username': self.username,
1022 'password': md5(self.password),
1022 'password': md5(self.password),
1023 'user_id': self.user_id,
1023 'user_id': self.user_id,
1024 'is_authenticated': self.is_authenticated
1024 'is_authenticated': self.is_authenticated
1025 }
1025 }
1026
1026
1027 @classmethod
1027 @classmethod
1028 def from_cookie_store(cls, cookie_store):
1028 def from_cookie_store(cls, cookie_store):
1029 """
1029 """
1030 Creates AuthUser from a cookie store
1030 Creates AuthUser from a cookie store
1031
1031
1032 :param cls:
1032 :param cls:
1033 :param cookie_store:
1033 :param cookie_store:
1034 """
1034 """
1035 user_id = cookie_store.get('user_id')
1035 user_id = cookie_store.get('user_id')
1036 username = cookie_store.get('username')
1036 username = cookie_store.get('username')
1037 api_key = cookie_store.get('api_key')
1037 api_key = cookie_store.get('api_key')
1038 return AuthUser(user_id, api_key, username)
1038 return AuthUser(user_id, api_key, username)
1039
1039
1040 @classmethod
1040 @classmethod
1041 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1041 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1042 _set = set()
1042 _set = set()
1043
1043
1044 if inherit_from_default:
1044 if inherit_from_default:
1045 default_ips = UserIpMap.query().filter(
1045 default_ips = UserIpMap.query().filter(
1046 UserIpMap.user == User.get_default_user(cache=True))
1046 UserIpMap.user == User.get_default_user(cache=True))
1047 if cache:
1047 if cache:
1048 default_ips = default_ips.options(
1048 default_ips = default_ips.options(
1049 FromCache("sql_cache_short", "get_user_ips_default"))
1049 FromCache("sql_cache_short", "get_user_ips_default"))
1050
1050
1051 # populate from default user
1051 # populate from default user
1052 for ip in default_ips:
1052 for ip in default_ips:
1053 try:
1053 try:
1054 _set.add(ip.ip_addr)
1054 _set.add(ip.ip_addr)
1055 except ObjectDeletedError:
1055 except ObjectDeletedError:
1056 # since we use heavy caching sometimes it happens that
1056 # since we use heavy caching sometimes it happens that
1057 # we get deleted objects here, we just skip them
1057 # we get deleted objects here, we just skip them
1058 pass
1058 pass
1059
1059
1060 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1060 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1061 if cache:
1061 if cache:
1062 user_ips = user_ips.options(
1062 user_ips = user_ips.options(
1063 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1063 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1064
1064
1065 for ip in user_ips:
1065 for ip in user_ips:
1066 try:
1066 try:
1067 _set.add(ip.ip_addr)
1067 _set.add(ip.ip_addr)
1068 except ObjectDeletedError:
1068 except ObjectDeletedError:
1069 # since we use heavy caching sometimes it happens that we get
1069 # since we use heavy caching sometimes it happens that we get
1070 # deleted objects here, we just skip them
1070 # deleted objects here, we just skip them
1071 pass
1071 pass
1072 return _set or set(['0.0.0.0/0', '::/0'])
1072 return _set or set(['0.0.0.0/0', '::/0'])
1073
1073
1074
1074
1075 def set_available_permissions(config):
1075 def set_available_permissions(config):
1076 """
1076 """
1077 This function will propagate pylons globals with all available defined
1077 This function will propagate pylons globals with all available defined
1078 permission given in db. We don't want to check each time from db for new
1078 permission given in db. We don't want to check each time from db for new
1079 permissions since adding a new permission also requires application restart
1079 permissions since adding a new permission also requires application restart
1080 ie. to decorate new views with the newly created permission
1080 ie. to decorate new views with the newly created permission
1081
1081
1082 :param config: current pylons config instance
1082 :param config: current pylons config instance
1083
1083
1084 """
1084 """
1085 log.info('getting information about all available permissions')
1085 log.info('getting information about all available permissions')
1086 try:
1086 try:
1087 sa = meta.Session
1087 sa = meta.Session
1088 all_perms = sa.query(Permission).all()
1088 all_perms = sa.query(Permission).all()
1089 config['available_permissions'] = [x.permission_name for x in all_perms]
1089 config['available_permissions'] = [x.permission_name for x in all_perms]
1090 except Exception:
1090 except Exception:
1091 log.error(traceback.format_exc())
1091 log.error(traceback.format_exc())
1092 finally:
1092 finally:
1093 meta.Session.remove()
1093 meta.Session.remove()
1094
1094
1095
1095
1096 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1096 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1097 """
1097 """
1098 Return the current authentication token, creating one if one doesn't
1098 Return the current authentication token, creating one if one doesn't
1099 already exist and the save_if_missing flag is present.
1099 already exist and the save_if_missing flag is present.
1100
1100
1101 :param session: pass in the pylons session, else we use the global ones
1101 :param session: pass in the pylons session, else we use the global ones
1102 :param force_new: force to re-generate the token and store it in session
1102 :param force_new: force to re-generate the token and store it in session
1103 :param save_if_missing: save the newly generated token if it's missing in
1103 :param save_if_missing: save the newly generated token if it's missing in
1104 session
1104 session
1105 """
1105 """
1106 if not session:
1106 if not session:
1107 from pylons import session
1107 from pylons import session
1108
1108
1109 if (csrf_token_key not in session and save_if_missing) or force_new:
1109 if (csrf_token_key not in session and save_if_missing) or force_new:
1110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1110 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1111 session[csrf_token_key] = token
1111 session[csrf_token_key] = token
1112 if hasattr(session, 'save'):
1112 if hasattr(session, 'save'):
1113 session.save()
1113 session.save()
1114 return session.get(csrf_token_key)
1114 return session.get(csrf_token_key)
1115
1115
1116
1116
1117 # CHECK DECORATORS
1117 # CHECK DECORATORS
1118 class CSRFRequired(object):
1118 class CSRFRequired(object):
1119 """
1119 """
1120 Decorator for authenticating a form
1120 Decorator for authenticating a form
1121
1121
1122 This decorator uses an authorization token stored in the client's
1122 This decorator uses an authorization token stored in the client's
1123 session for prevention of certain Cross-site request forgery (CSRF)
1123 session for prevention of certain Cross-site request forgery (CSRF)
1124 attacks (See
1124 attacks (See
1125 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1125 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1126 information).
1126 information).
1127
1127
1128 For use with the ``webhelpers.secure_form`` helper functions.
1128 For use with the ``webhelpers.secure_form`` helper functions.
1129
1129
1130 """
1130 """
1131 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1131 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1132 except_methods=None):
1132 except_methods=None):
1133 self.token = token
1133 self.token = token
1134 self.header = header
1134 self.header = header
1135 self.except_methods = except_methods or []
1135 self.except_methods = except_methods or []
1136
1136
1137 def __call__(self, func):
1137 def __call__(self, func):
1138 return get_cython_compat_decorator(self.__wrapper, func)
1138 return get_cython_compat_decorator(self.__wrapper, func)
1139
1139
1140 def _get_csrf(self, _request):
1140 def _get_csrf(self, _request):
1141 return _request.POST.get(self.token, _request.headers.get(self.header))
1141 return _request.POST.get(self.token, _request.headers.get(self.header))
1142
1142
1143 def check_csrf(self, _request, cur_token):
1143 def check_csrf(self, _request, cur_token):
1144 supplied_token = self._get_csrf(_request)
1144 supplied_token = self._get_csrf(_request)
1145 return supplied_token and supplied_token == cur_token
1145 return supplied_token and supplied_token == cur_token
1146
1146
1147 def __wrapper(self, func, *fargs, **fkwargs):
1147 def __wrapper(self, func, *fargs, **fkwargs):
1148 if request.method in self.except_methods:
1148 if request.method in self.except_methods:
1149 return func(*fargs, **fkwargs)
1149 return func(*fargs, **fkwargs)
1150
1150
1151 cur_token = get_csrf_token(save_if_missing=False)
1151 cur_token = get_csrf_token(save_if_missing=False)
1152 if self.check_csrf(request, cur_token):
1152 if self.check_csrf(request, cur_token):
1153 if request.POST.get(self.token):
1153 if request.POST.get(self.token):
1154 del request.POST[self.token]
1154 del request.POST[self.token]
1155 return func(*fargs, **fkwargs)
1155 return func(*fargs, **fkwargs)
1156 else:
1156 else:
1157 reason = 'token-missing'
1157 reason = 'token-missing'
1158 supplied_token = self._get_csrf(request)
1158 supplied_token = self._get_csrf(request)
1159 if supplied_token and cur_token != supplied_token:
1159 if supplied_token and cur_token != supplied_token:
1160 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1160 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1161 supplied_token or ''[:6])
1161 supplied_token or ''[:6])
1162
1162
1163 csrf_message = \
1163 csrf_message = \
1164 ("Cross-site request forgery detected, request denied. See "
1164 ("Cross-site request forgery detected, request denied. See "
1165 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1165 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1166 "more information.")
1166 "more information.")
1167 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1167 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1168 'REMOTE_ADDR:%s, HEADERS:%s' % (
1168 'REMOTE_ADDR:%s, HEADERS:%s' % (
1169 request, reason, request.remote_addr, request.headers))
1169 request, reason, request.remote_addr, request.headers))
1170
1170
1171 raise HTTPForbidden(explanation=csrf_message)
1171 raise HTTPForbidden(explanation=csrf_message)
1172
1172
1173
1173
1174 class LoginRequired(object):
1174 class LoginRequired(object):
1175 """
1175 """
1176 Must be logged in to execute this function else
1176 Must be logged in to execute this function else
1177 redirect to login page
1177 redirect to login page
1178
1178
1179 :param api_access: if enabled this checks only for valid auth token
1179 :param api_access: if enabled this checks only for valid auth token
1180 and grants access based on valid token
1180 and grants access based on valid token
1181 """
1181 """
1182 def __init__(self, auth_token_access=None):
1182 def __init__(self, auth_token_access=None):
1183 self.auth_token_access = auth_token_access
1183 self.auth_token_access = auth_token_access
1184
1184
1185 def __call__(self, func):
1185 def __call__(self, func):
1186 return get_cython_compat_decorator(self.__wrapper, func)
1186 return get_cython_compat_decorator(self.__wrapper, func)
1187
1187
1188 def __wrapper(self, func, *fargs, **fkwargs):
1188 def __wrapper(self, func, *fargs, **fkwargs):
1189 from rhodecode.lib import helpers as h
1189 from rhodecode.lib import helpers as h
1190 cls = fargs[0]
1190 cls = fargs[0]
1191 user = cls._rhodecode_user
1191 user = cls._rhodecode_user
1192 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1192 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1193 log.debug('Starting login restriction checks for user: %s' % (user,))
1193 log.debug('Starting login restriction checks for user: %s' % (user,))
1194 # check if our IP is allowed
1194 # check if our IP is allowed
1195 ip_access_valid = True
1195 ip_access_valid = True
1196 if not user.ip_allowed:
1196 if not user.ip_allowed:
1197 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1197 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1198 category='warning')
1198 category='warning')
1199 ip_access_valid = False
1199 ip_access_valid = False
1200
1200
1201 # check if we used an APIKEY and it's a valid one
1201 # check if we used an APIKEY and it's a valid one
1202 # defined white-list of controllers which API access will be enabled
1202 # defined white-list of controllers which API access will be enabled
1203 _auth_token = request.GET.get(
1203 _auth_token = request.GET.get(
1204 'auth_token', '') or request.GET.get('api_key', '')
1204 'auth_token', '') or request.GET.get('api_key', '')
1205 auth_token_access_valid = allowed_auth_token_access(
1205 auth_token_access_valid = allowed_auth_token_access(
1206 loc, auth_token=_auth_token)
1206 loc, auth_token=_auth_token)
1207
1207
1208 # explicit controller is enabled or API is in our whitelist
1208 # explicit controller is enabled or API is in our whitelist
1209 if self.auth_token_access or auth_token_access_valid:
1209 if self.auth_token_access or auth_token_access_valid:
1210 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1210 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1211 db_user = user.get_instance()
1211 db_user = user.get_instance()
1212
1212
1213 if db_user:
1213 if db_user:
1214 if self.auth_token_access:
1214 if self.auth_token_access:
1215 roles = self.auth_token_access
1215 roles = self.auth_token_access
1216 else:
1216 else:
1217 roles = [UserApiKeys.ROLE_HTTP]
1217 roles = [UserApiKeys.ROLE_HTTP]
1218 token_match = db_user.authenticate_by_token(
1218 token_match = db_user.authenticate_by_token(
1219 _auth_token, roles=roles)
1219 _auth_token, roles=roles)
1220 else:
1220 else:
1221 log.debug('Unable to fetch db instance for auth user: %s', user)
1221 log.debug('Unable to fetch db instance for auth user: %s', user)
1222 token_match = False
1222 token_match = False
1223
1223
1224 if _auth_token and token_match:
1224 if _auth_token and token_match:
1225 auth_token_access_valid = True
1225 auth_token_access_valid = True
1226 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1226 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1227 else:
1227 else:
1228 auth_token_access_valid = False
1228 auth_token_access_valid = False
1229 if not _auth_token:
1229 if not _auth_token:
1230 log.debug("AUTH TOKEN *NOT* present in request")
1230 log.debug("AUTH TOKEN *NOT* present in request")
1231 else:
1231 else:
1232 log.warning(
1232 log.warning(
1233 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1233 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1234
1234
1235 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1235 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1236 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1236 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1237 else 'AUTH_TOKEN_AUTH'
1237 else 'AUTH_TOKEN_AUTH'
1238
1238
1239 if ip_access_valid and (
1239 if ip_access_valid and (
1240 user.is_authenticated or auth_token_access_valid):
1240 user.is_authenticated or auth_token_access_valid):
1241 log.info(
1241 log.info(
1242 'user %s authenticating with:%s IS authenticated on func %s'
1242 'user %s authenticating with:%s IS authenticated on func %s'
1243 % (user, reason, loc))
1243 % (user, reason, loc))
1244
1244
1245 # update user data to check last activity
1245 # update user data to check last activity
1246 user.update_lastactivity()
1246 user.update_lastactivity()
1247 Session().commit()
1247 Session().commit()
1248 return func(*fargs, **fkwargs)
1248 return func(*fargs, **fkwargs)
1249 else:
1249 else:
1250 log.warning(
1250 log.warning(
1251 'user %s authenticating with:%s NOT authenticated on '
1251 'user %s authenticating with:%s NOT authenticated on '
1252 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1252 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1253 % (user, reason, loc, ip_access_valid,
1253 % (user, reason, loc, ip_access_valid,
1254 auth_token_access_valid))
1254 auth_token_access_valid))
1255 # we preserve the get PARAM
1255 # we preserve the get PARAM
1256 came_from = request.path_qs
1256 came_from = request.path_qs
1257 log.debug('redirecting to login page with %s' % (came_from,))
1257 log.debug('redirecting to login page with %s' % (came_from,))
1258 return redirect(
1258 return redirect(
1259 h.route_path('login', _query={'came_from': came_from}))
1259 h.route_path('login', _query={'came_from': came_from}))
1260
1260
1261
1261
1262 class NotAnonymous(object):
1262 class NotAnonymous(object):
1263 """
1263 """
1264 Must be logged in to execute this function else
1264 Must be logged in to execute this function else
1265 redirect to login page"""
1265 redirect to login page"""
1266
1266
1267 def __call__(self, func):
1267 def __call__(self, func):
1268 return get_cython_compat_decorator(self.__wrapper, func)
1268 return get_cython_compat_decorator(self.__wrapper, func)
1269
1269
1270 def __wrapper(self, func, *fargs, **fkwargs):
1270 def __wrapper(self, func, *fargs, **fkwargs):
1271 import rhodecode.lib.helpers as h
1271 import rhodecode.lib.helpers as h
1272 cls = fargs[0]
1272 cls = fargs[0]
1273 self.user = cls._rhodecode_user
1273 self.user = cls._rhodecode_user
1274
1274
1275 log.debug('Checking if user is not anonymous @%s' % cls)
1275 log.debug('Checking if user is not anonymous @%s' % cls)
1276
1276
1277 anonymous = self.user.username == User.DEFAULT_USER
1277 anonymous = self.user.username == User.DEFAULT_USER
1278
1278
1279 if anonymous:
1279 if anonymous:
1280 came_from = request.path_qs
1280 came_from = request.path_qs
1281 h.flash(_('You need to be a registered user to '
1281 h.flash(_('You need to be a registered user to '
1282 'perform this action'),
1282 'perform this action'),
1283 category='warning')
1283 category='warning')
1284 return redirect(
1284 return redirect(
1285 h.route_path('login', _query={'came_from': came_from}))
1285 h.route_path('login', _query={'came_from': came_from}))
1286 else:
1286 else:
1287 return func(*fargs, **fkwargs)
1287 return func(*fargs, **fkwargs)
1288
1288
1289
1289
1290 class XHRRequired(object):
1290 class XHRRequired(object):
1291 def __call__(self, func):
1291 def __call__(self, func):
1292 return get_cython_compat_decorator(self.__wrapper, func)
1292 return get_cython_compat_decorator(self.__wrapper, func)
1293
1293
1294 def __wrapper(self, func, *fargs, **fkwargs):
1294 def __wrapper(self, func, *fargs, **fkwargs):
1295 log.debug('Checking if request is XMLHttpRequest (XHR)')
1295 log.debug('Checking if request is XMLHttpRequest (XHR)')
1296 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1296 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1297 if not request.is_xhr:
1297 if not request.is_xhr:
1298 abort(400, detail=xhr_message)
1298 abort(400, detail=xhr_message)
1299
1299
1300 return func(*fargs, **fkwargs)
1300 return func(*fargs, **fkwargs)
1301
1301
1302
1302
1303 class HasAcceptedRepoType(object):
1303 class HasAcceptedRepoType(object):
1304 """
1304 """
1305 Check if requested repo is within given repo type aliases
1305 Check if requested repo is within given repo type aliases
1306
1306
1307 TODO: anderson: not sure where to put this decorator
1307 TODO: anderson: not sure where to put this decorator
1308 """
1308 """
1309
1309
1310 def __init__(self, *repo_type_list):
1310 def __init__(self, *repo_type_list):
1311 self.repo_type_list = set(repo_type_list)
1311 self.repo_type_list = set(repo_type_list)
1312
1312
1313 def __call__(self, func):
1313 def __call__(self, func):
1314 return get_cython_compat_decorator(self.__wrapper, func)
1314 return get_cython_compat_decorator(self.__wrapper, func)
1315
1315
1316 def __wrapper(self, func, *fargs, **fkwargs):
1316 def __wrapper(self, func, *fargs, **fkwargs):
1317 import rhodecode.lib.helpers as h
1317 import rhodecode.lib.helpers as h
1318 cls = fargs[0]
1318 cls = fargs[0]
1319 rhodecode_repo = cls.rhodecode_repo
1319 rhodecode_repo = cls.rhodecode_repo
1320
1320
1321 log.debug('%s checking repo type for %s in %s',
1321 log.debug('%s checking repo type for %s in %s',
1322 self.__class__.__name__,
1322 self.__class__.__name__,
1323 rhodecode_repo.alias, self.repo_type_list)
1323 rhodecode_repo.alias, self.repo_type_list)
1324
1324
1325 if rhodecode_repo.alias in self.repo_type_list:
1325 if rhodecode_repo.alias in self.repo_type_list:
1326 return func(*fargs, **fkwargs)
1326 return func(*fargs, **fkwargs)
1327 else:
1327 else:
1328 h.flash(h.literal(
1328 h.flash(h.literal(
1329 _('Action not supported for %s.' % rhodecode_repo.alias)),
1329 _('Action not supported for %s.' % rhodecode_repo.alias)),
1330 category='warning')
1330 category='warning')
1331 return redirect(
1331 return redirect(
1332 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1332 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1333
1333
1334
1334
1335 class PermsDecorator(object):
1335 class PermsDecorator(object):
1336 """
1336 """
1337 Base class for controller decorators, we extract the current user from
1337 Base class for controller decorators, we extract the current user from
1338 the class itself, which has it stored in base controllers
1338 the class itself, which has it stored in base controllers
1339 """
1339 """
1340
1340
1341 def __init__(self, *required_perms):
1341 def __init__(self, *required_perms):
1342 self.required_perms = set(required_perms)
1342 self.required_perms = set(required_perms)
1343
1343
1344 def __call__(self, func):
1344 def __call__(self, func):
1345 return get_cython_compat_decorator(self.__wrapper, func)
1345 return get_cython_compat_decorator(self.__wrapper, func)
1346
1346
1347 def _get_request(self):
1347 def _get_request(self):
1348 from pyramid.threadlocal import get_current_request
1348 from pyramid.threadlocal import get_current_request
1349 pyramid_request = get_current_request()
1349 pyramid_request = get_current_request()
1350 if not pyramid_request:
1350 if not pyramid_request:
1351 # return global request of pylons in case pyramid isn't available
1351 # return global request of pylons in case pyramid isn't available
1352 return request
1352 return request
1353 return pyramid_request
1353 return pyramid_request
1354
1354
1355 def _get_came_from(self):
1355 def _get_came_from(self):
1356 _request = self._get_request()
1356 _request = self._get_request()
1357
1357
1358 # both pylons/pyramid has this attribute
1358 # both pylons/pyramid has this attribute
1359 return _request.path_qs
1359 return _request.path_qs
1360
1360
1361 def __wrapper(self, func, *fargs, **fkwargs):
1361 def __wrapper(self, func, *fargs, **fkwargs):
1362 import rhodecode.lib.helpers as h
1362 import rhodecode.lib.helpers as h
1363 cls = fargs[0]
1363 cls = fargs[0]
1364 _user = cls._rhodecode_user
1364 _user = cls._rhodecode_user
1365
1365
1366 log.debug('checking %s permissions %s for %s %s',
1366 log.debug('checking %s permissions %s for %s %s',
1367 self.__class__.__name__, self.required_perms, cls, _user)
1367 self.__class__.__name__, self.required_perms, cls, _user)
1368
1368
1369 if self.check_permissions(_user):
1369 if self.check_permissions(_user):
1370 log.debug('Permission granted for %s %s', cls, _user)
1370 log.debug('Permission granted for %s %s', cls, _user)
1371 return func(*fargs, **fkwargs)
1371 return func(*fargs, **fkwargs)
1372
1372
1373 else:
1373 else:
1374 log.debug('Permission denied for %s %s', cls, _user)
1374 log.debug('Permission denied for %s %s', cls, _user)
1375 anonymous = _user.username == User.DEFAULT_USER
1375 anonymous = _user.username == User.DEFAULT_USER
1376
1376
1377 if anonymous:
1377 if anonymous:
1378 came_from = self._get_came_from()
1378 came_from = self._get_came_from()
1379 h.flash(_('You need to be signed in to view this page'),
1379 h.flash(_('You need to be signed in to view this page'),
1380 category='warning')
1380 category='warning')
1381 raise HTTPFound(
1381 raise HTTPFound(
1382 h.route_path('login', _query={'came_from': came_from}))
1382 h.route_path('login', _query={'came_from': came_from}))
1383
1383
1384 else:
1384 else:
1385 # redirect with forbidden ret code
1385 # redirect with forbidden ret code
1386 raise HTTPForbidden()
1386 raise HTTPForbidden()
1387
1387
1388 def check_permissions(self, user):
1388 def check_permissions(self, user):
1389 """Dummy function for overriding"""
1389 """Dummy function for overriding"""
1390 raise NotImplementedError(
1390 raise NotImplementedError(
1391 'You have to write this function in child class')
1391 'You have to write this function in child class')
1392
1392
1393
1393
1394 class HasPermissionAllDecorator(PermsDecorator):
1394 class HasPermissionAllDecorator(PermsDecorator):
1395 """
1395 """
1396 Checks for access permission for all given predicates. All of them
1396 Checks for access permission for all given predicates. All of them
1397 have to be meet in order to fulfill the request
1397 have to be meet in order to fulfill the request
1398 """
1398 """
1399
1399
1400 def check_permissions(self, user):
1400 def check_permissions(self, user):
1401 perms = user.permissions_with_scope({})
1401 perms = user.permissions_with_scope({})
1402 if self.required_perms.issubset(perms['global']):
1402 if self.required_perms.issubset(perms['global']):
1403 return True
1403 return True
1404 return False
1404 return False
1405
1405
1406
1406
1407 class HasPermissionAnyDecorator(PermsDecorator):
1407 class HasPermissionAnyDecorator(PermsDecorator):
1408 """
1408 """
1409 Checks for access permission for any of given predicates. In order to
1409 Checks for access permission for any of given predicates. In order to
1410 fulfill the request any of predicates must be meet
1410 fulfill the request any of predicates must be meet
1411 """
1411 """
1412
1412
1413 def check_permissions(self, user):
1413 def check_permissions(self, user):
1414 perms = user.permissions_with_scope({})
1414 perms = user.permissions_with_scope({})
1415 if self.required_perms.intersection(perms['global']):
1415 if self.required_perms.intersection(perms['global']):
1416 return True
1416 return True
1417 return False
1417 return False
1418
1418
1419
1419
1420 class HasRepoPermissionAllDecorator(PermsDecorator):
1420 class HasRepoPermissionAllDecorator(PermsDecorator):
1421 """
1421 """
1422 Checks for access permission for all given predicates for specific
1422 Checks for access permission for all given predicates for specific
1423 repository. All of them have to be meet in order to fulfill the request
1423 repository. All of them have to be meet in order to fulfill the request
1424 """
1424 """
1425 def _get_repo_name(self):
1425 def _get_repo_name(self):
1426 _request = self._get_request()
1426 _request = self._get_request()
1427 return get_repo_slug(_request)
1427 return get_repo_slug(_request)
1428
1428
1429 def check_permissions(self, user):
1429 def check_permissions(self, user):
1430 perms = user.permissions
1430 perms = user.permissions
1431 repo_name = self._get_repo_name()
1431 repo_name = self._get_repo_name()
1432
1432 try:
1433 try:
1433 user_perms = set([perms['repositories'][repo_name]])
1434 user_perms = set([perms['repositories'][repo_name]])
1434 except KeyError:
1435 except KeyError:
1436 log.debug('cannot locate repo with name: `%s` in permissions defs',
1437 repo_name)
1435 return False
1438 return False
1439
1440 log.debug('checking `%s` permissions for repo `%s`',
1441 user_perms, repo_name)
1436 if self.required_perms.issubset(user_perms):
1442 if self.required_perms.issubset(user_perms):
1437 return True
1443 return True
1438 return False
1444 return False
1439
1445
1440
1446
1441 class HasRepoPermissionAnyDecorator(PermsDecorator):
1447 class HasRepoPermissionAnyDecorator(PermsDecorator):
1442 """
1448 """
1443 Checks for access permission for any of given predicates for specific
1449 Checks for access permission for any of given predicates for specific
1444 repository. In order to fulfill the request any of predicates must be meet
1450 repository. In order to fulfill the request any of predicates must be meet
1445 """
1451 """
1446 def _get_repo_name(self):
1452 def _get_repo_name(self):
1447 _request = self._get_request()
1453 _request = self._get_request()
1448 return get_repo_slug(_request)
1454 return get_repo_slug(_request)
1449
1455
1450 def check_permissions(self, user):
1456 def check_permissions(self, user):
1451 perms = user.permissions
1457 perms = user.permissions
1452 repo_name = self._get_repo_name()
1458 repo_name = self._get_repo_name()
1459
1453 try:
1460 try:
1454 user_perms = set([perms['repositories'][repo_name]])
1461 user_perms = set([perms['repositories'][repo_name]])
1455 except KeyError:
1462 except KeyError:
1463 log.debug('cannot locate repo with name: `%s` in permissions defs',
1464 repo_name)
1456 return False
1465 return False
1457
1466
1467 log.debug('checking `%s` permissions for repo `%s`',
1468 user_perms, repo_name)
1458 if self.required_perms.intersection(user_perms):
1469 if self.required_perms.intersection(user_perms):
1459 return True
1470 return True
1460 return False
1471 return False
1461
1472
1462
1473
1463 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1474 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1464 """
1475 """
1465 Checks for access permission for all given predicates for specific
1476 Checks for access permission for all given predicates for specific
1466 repository group. All of them have to be meet in order to
1477 repository group. All of them have to be meet in order to
1467 fulfill the request
1478 fulfill the request
1468 """
1479 """
1469 def _get_repo_group_name(self):
1480 def _get_repo_group_name(self):
1470 _request = self._get_request()
1481 _request = self._get_request()
1471 return get_repo_group_slug(_request)
1482 return get_repo_group_slug(_request)
1472
1483
1473 def check_permissions(self, user):
1484 def check_permissions(self, user):
1474 perms = user.permissions
1485 perms = user.permissions
1475 group_name = self._get_repo_group_name()
1486 group_name = self._get_repo_group_name()
1476 try:
1487 try:
1477 user_perms = set([perms['repositories_groups'][group_name]])
1488 user_perms = set([perms['repositories_groups'][group_name]])
1478 except KeyError:
1489 except KeyError:
1490 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1491 group_name)
1479 return False
1492 return False
1480
1493
1494 log.debug('checking `%s` permissions for repo group `%s`',
1495 user_perms, group_name)
1481 if self.required_perms.issubset(user_perms):
1496 if self.required_perms.issubset(user_perms):
1482 return True
1497 return True
1483 return False
1498 return False
1484
1499
1485
1500
1486 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1501 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1487 """
1502 """
1488 Checks for access permission for any of given predicates for specific
1503 Checks for access permission for any of given predicates for specific
1489 repository group. In order to fulfill the request any
1504 repository group. In order to fulfill the request any
1490 of predicates must be met
1505 of predicates must be met
1491 """
1506 """
1492 def _get_repo_group_name(self):
1507 def _get_repo_group_name(self):
1493 _request = self._get_request()
1508 _request = self._get_request()
1494 return get_repo_group_slug(_request)
1509 return get_repo_group_slug(_request)
1495
1510
1496 def check_permissions(self, user):
1511 def check_permissions(self, user):
1497 perms = user.permissions
1512 perms = user.permissions
1498 group_name = self._get_repo_group_name()
1513 group_name = self._get_repo_group_name()
1514
1499 try:
1515 try:
1500 user_perms = set([perms['repositories_groups'][group_name]])
1516 user_perms = set([perms['repositories_groups'][group_name]])
1501 except KeyError:
1517 except KeyError:
1518 log.debug('cannot locate repo group with name: `%s` in permissions defs',
1519 group_name)
1502 return False
1520 return False
1503
1521
1522 log.debug('checking `%s` permissions for repo group `%s`',
1523 user_perms, group_name)
1504 if self.required_perms.intersection(user_perms):
1524 if self.required_perms.intersection(user_perms):
1505 return True
1525 return True
1506 return False
1526 return False
1507
1527
1508
1528
1509 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1529 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1510 """
1530 """
1511 Checks for access permission for all given predicates for specific
1531 Checks for access permission for all given predicates for specific
1512 user group. All of them have to be meet in order to fulfill the request
1532 user group. All of them have to be meet in order to fulfill the request
1513 """
1533 """
1514 def _get_user_group_name(self):
1534 def _get_user_group_name(self):
1515 _request = self._get_request()
1535 _request = self._get_request()
1516 return get_user_group_slug(_request)
1536 return get_user_group_slug(_request)
1517
1537
1518 def check_permissions(self, user):
1538 def check_permissions(self, user):
1519 perms = user.permissions
1539 perms = user.permissions
1520 group_name = self._get_user_group_name()
1540 group_name = self._get_user_group_name()
1521 try:
1541 try:
1522 user_perms = set([perms['user_groups'][group_name]])
1542 user_perms = set([perms['user_groups'][group_name]])
1523 except KeyError:
1543 except KeyError:
1524 return False
1544 return False
1525
1545
1526 if self.required_perms.issubset(user_perms):
1546 if self.required_perms.issubset(user_perms):
1527 return True
1547 return True
1528 return False
1548 return False
1529
1549
1530
1550
1531 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1551 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1532 """
1552 """
1533 Checks for access permission for any of given predicates for specific
1553 Checks for access permission for any of given predicates for specific
1534 user group. In order to fulfill the request any of predicates must be meet
1554 user group. In order to fulfill the request any of predicates must be meet
1535 """
1555 """
1536 def _get_user_group_name(self):
1556 def _get_user_group_name(self):
1537 _request = self._get_request()
1557 _request = self._get_request()
1538 return get_user_group_slug(_request)
1558 return get_user_group_slug(_request)
1539
1559
1540 def check_permissions(self, user):
1560 def check_permissions(self, user):
1541 perms = user.permissions
1561 perms = user.permissions
1542 group_name = self._get_user_group_name()
1562 group_name = self._get_user_group_name()
1543 try:
1563 try:
1544 user_perms = set([perms['user_groups'][group_name]])
1564 user_perms = set([perms['user_groups'][group_name]])
1545 except KeyError:
1565 except KeyError:
1546 return False
1566 return False
1547
1567
1548 if self.required_perms.intersection(user_perms):
1568 if self.required_perms.intersection(user_perms):
1549 return True
1569 return True
1550 return False
1570 return False
1551
1571
1552
1572
1553 # CHECK FUNCTIONS
1573 # CHECK FUNCTIONS
1554 class PermsFunction(object):
1574 class PermsFunction(object):
1555 """Base function for other check functions"""
1575 """Base function for other check functions"""
1556
1576
1557 def __init__(self, *perms):
1577 def __init__(self, *perms):
1558 self.required_perms = set(perms)
1578 self.required_perms = set(perms)
1559 self.repo_name = None
1579 self.repo_name = None
1560 self.repo_group_name = None
1580 self.repo_group_name = None
1561 self.user_group_name = None
1581 self.user_group_name = None
1562
1582
1563 def __bool__(self):
1583 def __bool__(self):
1564 frame = inspect.currentframe()
1584 frame = inspect.currentframe()
1565 stack_trace = traceback.format_stack(frame)
1585 stack_trace = traceback.format_stack(frame)
1566 log.error('Checking bool value on a class instance of perm '
1586 log.error('Checking bool value on a class instance of perm '
1567 'function is not allowed: %s' % ''.join(stack_trace))
1587 'function is not allowed: %s' % ''.join(stack_trace))
1568 # rather than throwing errors, here we always return False so if by
1588 # rather than throwing errors, here we always return False so if by
1569 # accident someone checks truth for just an instance it will always end
1589 # accident someone checks truth for just an instance it will always end
1570 # up in returning False
1590 # up in returning False
1571 return False
1591 return False
1572 __nonzero__ = __bool__
1592 __nonzero__ = __bool__
1573
1593
1574 def __call__(self, check_location='', user=None):
1594 def __call__(self, check_location='', user=None):
1575 if not user:
1595 if not user:
1576 log.debug('Using user attribute from global request')
1596 log.debug('Using user attribute from global request')
1577 # TODO: remove this someday,put as user as attribute here
1597 # TODO: remove this someday,put as user as attribute here
1578 user = request.user
1598 user = request.user
1579
1599
1580 # init auth user if not already given
1600 # init auth user if not already given
1581 if not isinstance(user, AuthUser):
1601 if not isinstance(user, AuthUser):
1582 log.debug('Wrapping user %s into AuthUser', user)
1602 log.debug('Wrapping user %s into AuthUser', user)
1583 user = AuthUser(user.user_id)
1603 user = AuthUser(user.user_id)
1584
1604
1585 cls_name = self.__class__.__name__
1605 cls_name = self.__class__.__name__
1586 check_scope = self._get_check_scope(cls_name)
1606 check_scope = self._get_check_scope(cls_name)
1587 check_location = check_location or 'unspecified location'
1607 check_location = check_location or 'unspecified location'
1588
1608
1589 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1609 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1590 self.required_perms, user, check_scope, check_location)
1610 self.required_perms, user, check_scope, check_location)
1591 if not user:
1611 if not user:
1592 log.warning('Empty user given for permission check')
1612 log.warning('Empty user given for permission check')
1593 return False
1613 return False
1594
1614
1595 if self.check_permissions(user):
1615 if self.check_permissions(user):
1596 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1616 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1597 check_scope, user, check_location)
1617 check_scope, user, check_location)
1598 return True
1618 return True
1599
1619
1600 else:
1620 else:
1601 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1621 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1602 check_scope, user, check_location)
1622 check_scope, user, check_location)
1603 return False
1623 return False
1604
1624
1605 def _get_request(self):
1625 def _get_request(self):
1606 from pyramid.threadlocal import get_current_request
1626 from pyramid.threadlocal import get_current_request
1607 pyramid_request = get_current_request()
1627 pyramid_request = get_current_request()
1608 if not pyramid_request:
1628 if not pyramid_request:
1609 # return global request of pylons incase pyramid one isn't available
1629 # return global request of pylons incase pyramid one isn't available
1610 return request
1630 return request
1611 return pyramid_request
1631 return pyramid_request
1612
1632
1613 def _get_check_scope(self, cls_name):
1633 def _get_check_scope(self, cls_name):
1614 return {
1634 return {
1615 'HasPermissionAll': 'GLOBAL',
1635 'HasPermissionAll': 'GLOBAL',
1616 'HasPermissionAny': 'GLOBAL',
1636 'HasPermissionAny': 'GLOBAL',
1617 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1637 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1618 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1638 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1619 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1639 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1620 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1640 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1621 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1641 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1622 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1642 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1623 }.get(cls_name, '?:%s' % cls_name)
1643 }.get(cls_name, '?:%s' % cls_name)
1624
1644
1625 def check_permissions(self, user):
1645 def check_permissions(self, user):
1626 """Dummy function for overriding"""
1646 """Dummy function for overriding"""
1627 raise Exception('You have to write this function in child class')
1647 raise Exception('You have to write this function in child class')
1628
1648
1629
1649
1630 class HasPermissionAll(PermsFunction):
1650 class HasPermissionAll(PermsFunction):
1631 def check_permissions(self, user):
1651 def check_permissions(self, user):
1632 perms = user.permissions_with_scope({})
1652 perms = user.permissions_with_scope({})
1633 if self.required_perms.issubset(perms.get('global')):
1653 if self.required_perms.issubset(perms.get('global')):
1634 return True
1654 return True
1635 return False
1655 return False
1636
1656
1637
1657
1638 class HasPermissionAny(PermsFunction):
1658 class HasPermissionAny(PermsFunction):
1639 def check_permissions(self, user):
1659 def check_permissions(self, user):
1640 perms = user.permissions_with_scope({})
1660 perms = user.permissions_with_scope({})
1641 if self.required_perms.intersection(perms.get('global')):
1661 if self.required_perms.intersection(perms.get('global')):
1642 return True
1662 return True
1643 return False
1663 return False
1644
1664
1645
1665
1646 class HasRepoPermissionAll(PermsFunction):
1666 class HasRepoPermissionAll(PermsFunction):
1647 def __call__(self, repo_name=None, check_location='', user=None):
1667 def __call__(self, repo_name=None, check_location='', user=None):
1648 self.repo_name = repo_name
1668 self.repo_name = repo_name
1649 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1669 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1650
1670
1651 def _get_repo_name(self):
1671 def _get_repo_name(self):
1652 if not self.repo_name:
1672 if not self.repo_name:
1653 _request = self._get_request()
1673 _request = self._get_request()
1654 self.repo_name = get_repo_slug(_request)
1674 self.repo_name = get_repo_slug(_request)
1655 return self.repo_name
1675 return self.repo_name
1656
1676
1657 def check_permissions(self, user):
1677 def check_permissions(self, user):
1658 self.repo_name = self._get_repo_name()
1678 self.repo_name = self._get_repo_name()
1659 perms = user.permissions
1679 perms = user.permissions
1660 try:
1680 try:
1661 user_perms = set([perms['repositories'][self.repo_name]])
1681 user_perms = set([perms['repositories'][self.repo_name]])
1662 except KeyError:
1682 except KeyError:
1663 return False
1683 return False
1664 if self.required_perms.issubset(user_perms):
1684 if self.required_perms.issubset(user_perms):
1665 return True
1685 return True
1666 return False
1686 return False
1667
1687
1668
1688
1669 class HasRepoPermissionAny(PermsFunction):
1689 class HasRepoPermissionAny(PermsFunction):
1670 def __call__(self, repo_name=None, check_location='', user=None):
1690 def __call__(self, repo_name=None, check_location='', user=None):
1671 self.repo_name = repo_name
1691 self.repo_name = repo_name
1672 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1692 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1673
1693
1674 def _get_repo_name(self):
1694 def _get_repo_name(self):
1675 if not self.repo_name:
1695 if not self.repo_name:
1676 self.repo_name = get_repo_slug(request)
1696 self.repo_name = get_repo_slug(request)
1677 return self.repo_name
1697 return self.repo_name
1678
1698
1679 def check_permissions(self, user):
1699 def check_permissions(self, user):
1680 self.repo_name = self._get_repo_name()
1700 self.repo_name = self._get_repo_name()
1681 perms = user.permissions
1701 perms = user.permissions
1682 try:
1702 try:
1683 user_perms = set([perms['repositories'][self.repo_name]])
1703 user_perms = set([perms['repositories'][self.repo_name]])
1684 except KeyError:
1704 except KeyError:
1685 return False
1705 return False
1686 if self.required_perms.intersection(user_perms):
1706 if self.required_perms.intersection(user_perms):
1687 return True
1707 return True
1688 return False
1708 return False
1689
1709
1690
1710
1691 class HasRepoGroupPermissionAny(PermsFunction):
1711 class HasRepoGroupPermissionAny(PermsFunction):
1692 def __call__(self, group_name=None, check_location='', user=None):
1712 def __call__(self, group_name=None, check_location='', user=None):
1693 self.repo_group_name = group_name
1713 self.repo_group_name = group_name
1694 return super(HasRepoGroupPermissionAny, self).__call__(
1714 return super(HasRepoGroupPermissionAny, self).__call__(
1695 check_location, user)
1715 check_location, user)
1696
1716
1697 def check_permissions(self, user):
1717 def check_permissions(self, user):
1698 perms = user.permissions
1718 perms = user.permissions
1699 try:
1719 try:
1700 user_perms = set(
1720 user_perms = set(
1701 [perms['repositories_groups'][self.repo_group_name]])
1721 [perms['repositories_groups'][self.repo_group_name]])
1702 except KeyError:
1722 except KeyError:
1703 return False
1723 return False
1704 if self.required_perms.intersection(user_perms):
1724 if self.required_perms.intersection(user_perms):
1705 return True
1725 return True
1706 return False
1726 return False
1707
1727
1708
1728
1709 class HasRepoGroupPermissionAll(PermsFunction):
1729 class HasRepoGroupPermissionAll(PermsFunction):
1710 def __call__(self, group_name=None, check_location='', user=None):
1730 def __call__(self, group_name=None, check_location='', user=None):
1711 self.repo_group_name = group_name
1731 self.repo_group_name = group_name
1712 return super(HasRepoGroupPermissionAll, self).__call__(
1732 return super(HasRepoGroupPermissionAll, self).__call__(
1713 check_location, user)
1733 check_location, user)
1714
1734
1715 def check_permissions(self, user):
1735 def check_permissions(self, user):
1716 perms = user.permissions
1736 perms = user.permissions
1717 try:
1737 try:
1718 user_perms = set(
1738 user_perms = set(
1719 [perms['repositories_groups'][self.repo_group_name]])
1739 [perms['repositories_groups'][self.repo_group_name]])
1720 except KeyError:
1740 except KeyError:
1721 return False
1741 return False
1722 if self.required_perms.issubset(user_perms):
1742 if self.required_perms.issubset(user_perms):
1723 return True
1743 return True
1724 return False
1744 return False
1725
1745
1726
1746
1727 class HasUserGroupPermissionAny(PermsFunction):
1747 class HasUserGroupPermissionAny(PermsFunction):
1728 def __call__(self, user_group_name=None, check_location='', user=None):
1748 def __call__(self, user_group_name=None, check_location='', user=None):
1729 self.user_group_name = user_group_name
1749 self.user_group_name = user_group_name
1730 return super(HasUserGroupPermissionAny, self).__call__(
1750 return super(HasUserGroupPermissionAny, self).__call__(
1731 check_location, user)
1751 check_location, user)
1732
1752
1733 def check_permissions(self, user):
1753 def check_permissions(self, user):
1734 perms = user.permissions
1754 perms = user.permissions
1735 try:
1755 try:
1736 user_perms = set([perms['user_groups'][self.user_group_name]])
1756 user_perms = set([perms['user_groups'][self.user_group_name]])
1737 except KeyError:
1757 except KeyError:
1738 return False
1758 return False
1739 if self.required_perms.intersection(user_perms):
1759 if self.required_perms.intersection(user_perms):
1740 return True
1760 return True
1741 return False
1761 return False
1742
1762
1743
1763
1744 class HasUserGroupPermissionAll(PermsFunction):
1764 class HasUserGroupPermissionAll(PermsFunction):
1745 def __call__(self, user_group_name=None, check_location='', user=None):
1765 def __call__(self, user_group_name=None, check_location='', user=None):
1746 self.user_group_name = user_group_name
1766 self.user_group_name = user_group_name
1747 return super(HasUserGroupPermissionAll, self).__call__(
1767 return super(HasUserGroupPermissionAll, self).__call__(
1748 check_location, user)
1768 check_location, user)
1749
1769
1750 def check_permissions(self, user):
1770 def check_permissions(self, user):
1751 perms = user.permissions
1771 perms = user.permissions
1752 try:
1772 try:
1753 user_perms = set([perms['user_groups'][self.user_group_name]])
1773 user_perms = set([perms['user_groups'][self.user_group_name]])
1754 except KeyError:
1774 except KeyError:
1755 return False
1775 return False
1756 if self.required_perms.issubset(user_perms):
1776 if self.required_perms.issubset(user_perms):
1757 return True
1777 return True
1758 return False
1778 return False
1759
1779
1760
1780
1761 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1781 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1762 class HasPermissionAnyMiddleware(object):
1782 class HasPermissionAnyMiddleware(object):
1763 def __init__(self, *perms):
1783 def __init__(self, *perms):
1764 self.required_perms = set(perms)
1784 self.required_perms = set(perms)
1765
1785
1766 def __call__(self, user, repo_name):
1786 def __call__(self, user, repo_name):
1767 # repo_name MUST be unicode, since we handle keys in permission
1787 # repo_name MUST be unicode, since we handle keys in permission
1768 # dict by unicode
1788 # dict by unicode
1769 repo_name = safe_unicode(repo_name)
1789 repo_name = safe_unicode(repo_name)
1770 user = AuthUser(user.user_id)
1790 user = AuthUser(user.user_id)
1771 log.debug(
1791 log.debug(
1772 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1792 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1773 self.required_perms, user, repo_name)
1793 self.required_perms, user, repo_name)
1774
1794
1775 if self.check_permissions(user, repo_name):
1795 if self.check_permissions(user, repo_name):
1776 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1796 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1777 repo_name, user, 'PermissionMiddleware')
1797 repo_name, user, 'PermissionMiddleware')
1778 return True
1798 return True
1779
1799
1780 else:
1800 else:
1781 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1801 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1782 repo_name, user, 'PermissionMiddleware')
1802 repo_name, user, 'PermissionMiddleware')
1783 return False
1803 return False
1784
1804
1785 def check_permissions(self, user, repo_name):
1805 def check_permissions(self, user, repo_name):
1786 perms = user.permissions_with_scope({'repo_name': repo_name})
1806 perms = user.permissions_with_scope({'repo_name': repo_name})
1787
1807
1788 try:
1808 try:
1789 user_perms = set([perms['repositories'][repo_name]])
1809 user_perms = set([perms['repositories'][repo_name]])
1790 except Exception:
1810 except Exception:
1791 log.exception('Error while accessing user permissions')
1811 log.exception('Error while accessing user permissions')
1792 return False
1812 return False
1793
1813
1794 if self.required_perms.intersection(user_perms):
1814 if self.required_perms.intersection(user_perms):
1795 return True
1815 return True
1796 return False
1816 return False
1797
1817
1798
1818
1799 # SPECIAL VERSION TO HANDLE API AUTH
1819 # SPECIAL VERSION TO HANDLE API AUTH
1800 class _BaseApiPerm(object):
1820 class _BaseApiPerm(object):
1801 def __init__(self, *perms):
1821 def __init__(self, *perms):
1802 self.required_perms = set(perms)
1822 self.required_perms = set(perms)
1803
1823
1804 def __call__(self, check_location=None, user=None, repo_name=None,
1824 def __call__(self, check_location=None, user=None, repo_name=None,
1805 group_name=None, user_group_name=None):
1825 group_name=None, user_group_name=None):
1806 cls_name = self.__class__.__name__
1826 cls_name = self.__class__.__name__
1807 check_scope = 'global:%s' % (self.required_perms,)
1827 check_scope = 'global:%s' % (self.required_perms,)
1808 if repo_name:
1828 if repo_name:
1809 check_scope += ', repo_name:%s' % (repo_name,)
1829 check_scope += ', repo_name:%s' % (repo_name,)
1810
1830
1811 if group_name:
1831 if group_name:
1812 check_scope += ', repo_group_name:%s' % (group_name,)
1832 check_scope += ', repo_group_name:%s' % (group_name,)
1813
1833
1814 if user_group_name:
1834 if user_group_name:
1815 check_scope += ', user_group_name:%s' % (user_group_name,)
1835 check_scope += ', user_group_name:%s' % (user_group_name,)
1816
1836
1817 log.debug(
1837 log.debug(
1818 'checking cls:%s %s %s @ %s'
1838 'checking cls:%s %s %s @ %s'
1819 % (cls_name, self.required_perms, check_scope, check_location))
1839 % (cls_name, self.required_perms, check_scope, check_location))
1820 if not user:
1840 if not user:
1821 log.debug('Empty User passed into arguments')
1841 log.debug('Empty User passed into arguments')
1822 return False
1842 return False
1823
1843
1824 # process user
1844 # process user
1825 if not isinstance(user, AuthUser):
1845 if not isinstance(user, AuthUser):
1826 user = AuthUser(user.user_id)
1846 user = AuthUser(user.user_id)
1827 if not check_location:
1847 if not check_location:
1828 check_location = 'unspecified'
1848 check_location = 'unspecified'
1829 if self.check_permissions(user.permissions, repo_name, group_name,
1849 if self.check_permissions(user.permissions, repo_name, group_name,
1830 user_group_name):
1850 user_group_name):
1831 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1851 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1832 check_scope, user, check_location)
1852 check_scope, user, check_location)
1833 return True
1853 return True
1834
1854
1835 else:
1855 else:
1836 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1856 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1837 check_scope, user, check_location)
1857 check_scope, user, check_location)
1838 return False
1858 return False
1839
1859
1840 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1860 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1841 user_group_name=None):
1861 user_group_name=None):
1842 """
1862 """
1843 implement in child class should return True if permissions are ok,
1863 implement in child class should return True if permissions are ok,
1844 False otherwise
1864 False otherwise
1845
1865
1846 :param perm_defs: dict with permission definitions
1866 :param perm_defs: dict with permission definitions
1847 :param repo_name: repo name
1867 :param repo_name: repo name
1848 """
1868 """
1849 raise NotImplementedError()
1869 raise NotImplementedError()
1850
1870
1851
1871
1852 class HasPermissionAllApi(_BaseApiPerm):
1872 class HasPermissionAllApi(_BaseApiPerm):
1853 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1873 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1854 user_group_name=None):
1874 user_group_name=None):
1855 if self.required_perms.issubset(perm_defs.get('global')):
1875 if self.required_perms.issubset(perm_defs.get('global')):
1856 return True
1876 return True
1857 return False
1877 return False
1858
1878
1859
1879
1860 class HasPermissionAnyApi(_BaseApiPerm):
1880 class HasPermissionAnyApi(_BaseApiPerm):
1861 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1881 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1862 user_group_name=None):
1882 user_group_name=None):
1863 if self.required_perms.intersection(perm_defs.get('global')):
1883 if self.required_perms.intersection(perm_defs.get('global')):
1864 return True
1884 return True
1865 return False
1885 return False
1866
1886
1867
1887
1868 class HasRepoPermissionAllApi(_BaseApiPerm):
1888 class HasRepoPermissionAllApi(_BaseApiPerm):
1869 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1889 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1870 user_group_name=None):
1890 user_group_name=None):
1871 try:
1891 try:
1872 _user_perms = set([perm_defs['repositories'][repo_name]])
1892 _user_perms = set([perm_defs['repositories'][repo_name]])
1873 except KeyError:
1893 except KeyError:
1874 log.warning(traceback.format_exc())
1894 log.warning(traceback.format_exc())
1875 return False
1895 return False
1876 if self.required_perms.issubset(_user_perms):
1896 if self.required_perms.issubset(_user_perms):
1877 return True
1897 return True
1878 return False
1898 return False
1879
1899
1880
1900
1881 class HasRepoPermissionAnyApi(_BaseApiPerm):
1901 class HasRepoPermissionAnyApi(_BaseApiPerm):
1882 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1902 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1883 user_group_name=None):
1903 user_group_name=None):
1884 try:
1904 try:
1885 _user_perms = set([perm_defs['repositories'][repo_name]])
1905 _user_perms = set([perm_defs['repositories'][repo_name]])
1886 except KeyError:
1906 except KeyError:
1887 log.warning(traceback.format_exc())
1907 log.warning(traceback.format_exc())
1888 return False
1908 return False
1889 if self.required_perms.intersection(_user_perms):
1909 if self.required_perms.intersection(_user_perms):
1890 return True
1910 return True
1891 return False
1911 return False
1892
1912
1893
1913
1894 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1914 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1895 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1915 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1896 user_group_name=None):
1916 user_group_name=None):
1897 try:
1917 try:
1898 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1918 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1899 except KeyError:
1919 except KeyError:
1900 log.warning(traceback.format_exc())
1920 log.warning(traceback.format_exc())
1901 return False
1921 return False
1902 if self.required_perms.intersection(_user_perms):
1922 if self.required_perms.intersection(_user_perms):
1903 return True
1923 return True
1904 return False
1924 return False
1905
1925
1906
1926
1907 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1927 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1908 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1928 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1909 user_group_name=None):
1929 user_group_name=None):
1910 try:
1930 try:
1911 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1931 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1912 except KeyError:
1932 except KeyError:
1913 log.warning(traceback.format_exc())
1933 log.warning(traceback.format_exc())
1914 return False
1934 return False
1915 if self.required_perms.issubset(_user_perms):
1935 if self.required_perms.issubset(_user_perms):
1916 return True
1936 return True
1917 return False
1937 return False
1918
1938
1919
1939
1920 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1940 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1921 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1941 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1922 user_group_name=None):
1942 user_group_name=None):
1923 try:
1943 try:
1924 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1944 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1925 except KeyError:
1945 except KeyError:
1926 log.warning(traceback.format_exc())
1946 log.warning(traceback.format_exc())
1927 return False
1947 return False
1928 if self.required_perms.intersection(_user_perms):
1948 if self.required_perms.intersection(_user_perms):
1929 return True
1949 return True
1930 return False
1950 return False
1931
1951
1932
1952
1933 def check_ip_access(source_ip, allowed_ips=None):
1953 def check_ip_access(source_ip, allowed_ips=None):
1934 """
1954 """
1935 Checks if source_ip is a subnet of any of allowed_ips.
1955 Checks if source_ip is a subnet of any of allowed_ips.
1936
1956
1937 :param source_ip:
1957 :param source_ip:
1938 :param allowed_ips: list of allowed ips together with mask
1958 :param allowed_ips: list of allowed ips together with mask
1939 """
1959 """
1940 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1960 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1941 source_ip_address = ipaddress.ip_address(source_ip)
1961 source_ip_address = ipaddress.ip_address(source_ip)
1942 if isinstance(allowed_ips, (tuple, list, set)):
1962 if isinstance(allowed_ips, (tuple, list, set)):
1943 for ip in allowed_ips:
1963 for ip in allowed_ips:
1944 try:
1964 try:
1945 network_address = ipaddress.ip_network(ip, strict=False)
1965 network_address = ipaddress.ip_network(ip, strict=False)
1946 if source_ip_address in network_address:
1966 if source_ip_address in network_address:
1947 log.debug('IP %s is network %s' %
1967 log.debug('IP %s is network %s' %
1948 (source_ip_address, network_address))
1968 (source_ip_address, network_address))
1949 return True
1969 return True
1950 # for any case we cannot determine the IP, don't crash just
1970 # for any case we cannot determine the IP, don't crash just
1951 # skip it and log as error, we want to say forbidden still when
1971 # skip it and log as error, we want to say forbidden still when
1952 # sending bad IP
1972 # sending bad IP
1953 except Exception:
1973 except Exception:
1954 log.error(traceback.format_exc())
1974 log.error(traceback.format_exc())
1955 continue
1975 continue
1956 return False
1976 return False
1957
1977
1958
1978
1959 def get_cython_compat_decorator(wrapper, func):
1979 def get_cython_compat_decorator(wrapper, func):
1960 """
1980 """
1961 Creates a cython compatible decorator. The previously used
1981 Creates a cython compatible decorator. The previously used
1962 decorator.decorator() function seems to be incompatible with cython.
1982 decorator.decorator() function seems to be incompatible with cython.
1963
1983
1964 :param wrapper: __wrapper method of the decorator class
1984 :param wrapper: __wrapper method of the decorator class
1965 :param func: decorated function
1985 :param func: decorated function
1966 """
1986 """
1967 @wraps(func)
1987 @wraps(func)
1968 def local_wrapper(*args, **kwds):
1988 def local_wrapper(*args, **kwds):
1969 return wrapper(func, *args, **kwds)
1989 return wrapper(func, *args, **kwds)
1970 local_wrapper.__wrapped__ = func
1990 local_wrapper.__wrapped__ = func
1971 return local_wrapper
1991 return local_wrapper
1972
1992
1973
1993
@@ -1,594 +1,594 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31 import pyramid.threadlocal
31 import pyramid.threadlocal
32
32
33 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from pylons import config, tmpl_context as c, request, session, url
36 from pylons import config, tmpl_context as c, request, session, url
37 from pylons.controllers import WSGIController
37 from pylons.controllers import WSGIController
38 from pylons.controllers.util import redirect
38 from pylons.controllers.util import redirect
39 from pylons.i18n import translation
39 from pylons.i18n import translation
40 # marcink: don't remove this import
40 # marcink: don't remove this import
41 from pylons.templating import render_mako as render # noqa
41 from pylons.templating import render_mako as render # noqa
42 from pylons.i18n.translation import _
42 from pylons.i18n.translation import _
43 from webob.exc import HTTPFound
43 from webob.exc import HTTPFound
44
44
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import helpers as h
49 from rhodecode.lib import helpers as h
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.utils import (
52 from rhodecode.lib.utils import (
53 get_repo_slug, set_rhodecode_config, password_changed,
53 get_repo_slug, set_rhodecode_config, password_changed,
54 get_enabled_hook_classes)
54 get_enabled_hook_classes)
55 from rhodecode.lib.utils2 import (
55 from rhodecode.lib.utils2 import (
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.model import meta
58 from rhodecode.model import meta
59 from rhodecode.model.db import Repository, User, ChangesetComment
59 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63
63
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 def _filter_proxy(ip):
68 def _filter_proxy(ip):
69 """
69 """
70 Passed in IP addresses in HEADERS can be in a special format of multiple
70 Passed in IP addresses in HEADERS can be in a special format of multiple
71 ips. Those comma separated IPs are passed from various proxies in the
71 ips. Those comma separated IPs are passed from various proxies in the
72 chain of request processing. The left-most being the original client.
72 chain of request processing. The left-most being the original client.
73 We only care about the first IP which came from the org. client.
73 We only care about the first IP which came from the org. client.
74
74
75 :param ip: ip string from headers
75 :param ip: ip string from headers
76 """
76 """
77 if ',' in ip:
77 if ',' in ip:
78 _ips = ip.split(',')
78 _ips = ip.split(',')
79 _first_ip = _ips[0].strip()
79 _first_ip = _ips[0].strip()
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
81 return _first_ip
81 return _first_ip
82 return ip
82 return ip
83
83
84
84
85 def _filter_port(ip):
85 def _filter_port(ip):
86 """
86 """
87 Removes a port from ip, there are 4 main cases to handle here.
87 Removes a port from ip, there are 4 main cases to handle here.
88 - ipv4 eg. 127.0.0.1
88 - ipv4 eg. 127.0.0.1
89 - ipv6 eg. ::1
89 - ipv6 eg. ::1
90 - ipv4+port eg. 127.0.0.1:8080
90 - ipv4+port eg. 127.0.0.1:8080
91 - ipv6+port eg. [::1]:8080
91 - ipv6+port eg. [::1]:8080
92
92
93 :param ip:
93 :param ip:
94 """
94 """
95 def is_ipv6(ip_addr):
95 def is_ipv6(ip_addr):
96 if hasattr(socket, 'inet_pton'):
96 if hasattr(socket, 'inet_pton'):
97 try:
97 try:
98 socket.inet_pton(socket.AF_INET6, ip_addr)
98 socket.inet_pton(socket.AF_INET6, ip_addr)
99 except socket.error:
99 except socket.error:
100 return False
100 return False
101 else:
101 else:
102 # fallback to ipaddress
102 # fallback to ipaddress
103 try:
103 try:
104 ipaddress.IPv6Address(ip_addr)
104 ipaddress.IPv6Address(ip_addr)
105 except Exception:
105 except Exception:
106 return False
106 return False
107 return True
107 return True
108
108
109 if ':' not in ip: # must be ipv4 pure ip
109 if ':' not in ip: # must be ipv4 pure ip
110 return ip
110 return ip
111
111
112 if '[' in ip and ']' in ip: # ipv6 with port
112 if '[' in ip and ']' in ip: # ipv6 with port
113 return ip.split(']')[0][1:].lower()
113 return ip.split(']')[0][1:].lower()
114
114
115 # must be ipv6 or ipv4 with port
115 # must be ipv6 or ipv4 with port
116 if is_ipv6(ip):
116 if is_ipv6(ip):
117 return ip
117 return ip
118 else:
118 else:
119 ip, _port = ip.split(':')[:2] # means ipv4+port
119 ip, _port = ip.split(':')[:2] # means ipv4+port
120 return ip
120 return ip
121
121
122
122
123 def get_ip_addr(environ):
123 def get_ip_addr(environ):
124 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key = 'HTTP_X_REAL_IP'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
126 def_key = 'REMOTE_ADDR'
126 def_key = 'REMOTE_ADDR'
127 _filters = lambda x: _filter_port(_filter_proxy(x))
127 _filters = lambda x: _filter_port(_filter_proxy(x))
128
128
129 ip = environ.get(proxy_key)
129 ip = environ.get(proxy_key)
130 if ip:
130 if ip:
131 return _filters(ip)
131 return _filters(ip)
132
132
133 ip = environ.get(proxy_key2)
133 ip = environ.get(proxy_key2)
134 if ip:
134 if ip:
135 return _filters(ip)
135 return _filters(ip)
136
136
137 ip = environ.get(def_key, '0.0.0.0')
137 ip = environ.get(def_key, '0.0.0.0')
138 return _filters(ip)
138 return _filters(ip)
139
139
140
140
141 def get_server_ip_addr(environ, log_errors=True):
141 def get_server_ip_addr(environ, log_errors=True):
142 hostname = environ.get('SERVER_NAME')
142 hostname = environ.get('SERVER_NAME')
143 try:
143 try:
144 return socket.gethostbyname(hostname)
144 return socket.gethostbyname(hostname)
145 except Exception as e:
145 except Exception as e:
146 if log_errors:
146 if log_errors:
147 # in some cases this lookup is not possible, and we don't want to
147 # in some cases this lookup is not possible, and we don't want to
148 # make it an exception in logs
148 # make it an exception in logs
149 log.exception('Could not retrieve server ip address: %s', e)
149 log.exception('Could not retrieve server ip address: %s', e)
150 return hostname
150 return hostname
151
151
152
152
153 def get_server_port(environ):
153 def get_server_port(environ):
154 return environ.get('SERVER_PORT')
154 return environ.get('SERVER_PORT')
155
155
156
156
157 def get_access_path(environ):
157 def get_access_path(environ):
158 path = environ.get('PATH_INFO')
158 path = environ.get('PATH_INFO')
159 org_req = environ.get('pylons.original_request')
159 org_req = environ.get('pylons.original_request')
160 if org_req:
160 if org_req:
161 path = org_req.environ.get('PATH_INFO')
161 path = org_req.environ.get('PATH_INFO')
162 return path
162 return path
163
163
164
164
165 def get_user_agent(environ):
165 def get_user_agent(environ):
166 return environ.get('HTTP_USER_AGENT')
166 return environ.get('HTTP_USER_AGENT')
167
167
168
168
169 def vcs_operation_context(
169 def vcs_operation_context(
170 environ, repo_name, username, action, scm, check_locking=True,
170 environ, repo_name, username, action, scm, check_locking=True,
171 is_shadow_repo=False):
171 is_shadow_repo=False):
172 """
172 """
173 Generate the context for a vcs operation, e.g. push or pull.
173 Generate the context for a vcs operation, e.g. push or pull.
174
174
175 This context is passed over the layers so that hooks triggered by the
175 This context is passed over the layers so that hooks triggered by the
176 vcs operation know details like the user, the user's IP address etc.
176 vcs operation know details like the user, the user's IP address etc.
177
177
178 :param check_locking: Allows to switch of the computation of the locking
178 :param check_locking: Allows to switch of the computation of the locking
179 data. This serves mainly the need of the simplevcs middleware to be
179 data. This serves mainly the need of the simplevcs middleware to be
180 able to disable this for certain operations.
180 able to disable this for certain operations.
181
181
182 """
182 """
183 # Tri-state value: False: unlock, None: nothing, True: lock
183 # Tri-state value: False: unlock, None: nothing, True: lock
184 make_lock = None
184 make_lock = None
185 locked_by = [None, None, None]
185 locked_by = [None, None, None]
186 is_anonymous = username == User.DEFAULT_USER
186 is_anonymous = username == User.DEFAULT_USER
187 if not is_anonymous and check_locking:
187 if not is_anonymous and check_locking:
188 log.debug('Checking locking on repository "%s"', repo_name)
188 log.debug('Checking locking on repository "%s"', repo_name)
189 user = User.get_by_username(username)
189 user = User.get_by_username(username)
190 repo = Repository.get_by_repo_name(repo_name)
190 repo = Repository.get_by_repo_name(repo_name)
191 make_lock, __, locked_by = repo.get_locking_state(
191 make_lock, __, locked_by = repo.get_locking_state(
192 action, user.user_id)
192 action, user.user_id)
193
193
194 settings_model = VcsSettingsModel(repo=repo_name)
194 settings_model = VcsSettingsModel(repo=repo_name)
195 ui_settings = settings_model.get_ui_settings()
195 ui_settings = settings_model.get_ui_settings()
196
196
197 extras = {
197 extras = {
198 'ip': get_ip_addr(environ),
198 'ip': get_ip_addr(environ),
199 'username': username,
199 'username': username,
200 'action': action,
200 'action': action,
201 'repository': repo_name,
201 'repository': repo_name,
202 'scm': scm,
202 'scm': scm,
203 'config': rhodecode.CONFIG['__file__'],
203 'config': rhodecode.CONFIG['__file__'],
204 'make_lock': make_lock,
204 'make_lock': make_lock,
205 'locked_by': locked_by,
205 'locked_by': locked_by,
206 'server_url': utils2.get_server_url(environ),
206 'server_url': utils2.get_server_url(environ),
207 'user_agent': get_user_agent(environ),
207 'user_agent': get_user_agent(environ),
208 'hooks': get_enabled_hook_classes(ui_settings),
208 'hooks': get_enabled_hook_classes(ui_settings),
209 'is_shadow_repo': is_shadow_repo,
209 'is_shadow_repo': is_shadow_repo,
210 }
210 }
211 return extras
211 return extras
212
212
213
213
214 class BasicAuth(AuthBasicAuthenticator):
214 class BasicAuth(AuthBasicAuthenticator):
215
215
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
216 def __init__(self, realm, authfunc, registry, auth_http_code=None,
217 initial_call_detection=False, acl_repo_name=None):
217 initial_call_detection=False, acl_repo_name=None):
218 self.realm = realm
218 self.realm = realm
219 self.initial_call = initial_call_detection
219 self.initial_call = initial_call_detection
220 self.authfunc = authfunc
220 self.authfunc = authfunc
221 self.registry = registry
221 self.registry = registry
222 self.acl_repo_name = acl_repo_name
222 self.acl_repo_name = acl_repo_name
223 self._rc_auth_http_code = auth_http_code
223 self._rc_auth_http_code = auth_http_code
224
224
225 def _get_response_from_code(self, http_code):
225 def _get_response_from_code(self, http_code):
226 try:
226 try:
227 return get_exception(safe_int(http_code))
227 return get_exception(safe_int(http_code))
228 except Exception:
228 except Exception:
229 log.exception('Failed to fetch response for code %s' % http_code)
229 log.exception('Failed to fetch response for code %s' % http_code)
230 return HTTPForbidden
230 return HTTPForbidden
231
231
232 def build_authentication(self):
232 def build_authentication(self):
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
234 if self._rc_auth_http_code and not self.initial_call:
234 if self._rc_auth_http_code and not self.initial_call:
235 # return alternative HTTP code if alternative http return code
235 # return alternative HTTP code if alternative http return code
236 # is specified in RhodeCode config, but ONLY if it's not the
236 # is specified in RhodeCode config, but ONLY if it's not the
237 # FIRST call
237 # FIRST call
238 custom_response_klass = self._get_response_from_code(
238 custom_response_klass = self._get_response_from_code(
239 self._rc_auth_http_code)
239 self._rc_auth_http_code)
240 return custom_response_klass(headers=head)
240 return custom_response_klass(headers=head)
241 return HTTPUnauthorized(headers=head)
241 return HTTPUnauthorized(headers=head)
242
242
243 def authenticate(self, environ):
243 def authenticate(self, environ):
244 authorization = AUTHORIZATION(environ)
244 authorization = AUTHORIZATION(environ)
245 if not authorization:
245 if not authorization:
246 return self.build_authentication()
246 return self.build_authentication()
247 (authmeth, auth) = authorization.split(' ', 1)
247 (authmeth, auth) = authorization.split(' ', 1)
248 if 'basic' != authmeth.lower():
248 if 'basic' != authmeth.lower():
249 return self.build_authentication()
249 return self.build_authentication()
250 auth = auth.strip().decode('base64')
250 auth = auth.strip().decode('base64')
251 _parts = auth.split(':', 1)
251 _parts = auth.split(':', 1)
252 if len(_parts) == 2:
252 if len(_parts) == 2:
253 username, password = _parts
253 username, password = _parts
254 if self.authfunc(
254 if self.authfunc(
255 username, password, environ, VCS_TYPE,
255 username, password, environ, VCS_TYPE,
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
256 registry=self.registry, acl_repo_name=self.acl_repo_name):
257 return username
257 return username
258 if username and password:
258 if username and password:
259 # we mark that we actually executed authentication once, at
259 # we mark that we actually executed authentication once, at
260 # that point we can use the alternative auth code
260 # that point we can use the alternative auth code
261 self.initial_call = False
261 self.initial_call = False
262
262
263 return self.build_authentication()
263 return self.build_authentication()
264
264
265 __call__ = authenticate
265 __call__ = authenticate
266
266
267
267
268 def attach_context_attributes(context, request):
268 def attach_context_attributes(context, request):
269 """
269 """
270 Attach variables into template context called `c`, please note that
270 Attach variables into template context called `c`, please note that
271 request could be pylons or pyramid request in here.
271 request could be pylons or pyramid request in here.
272 """
272 """
273 rc_config = SettingsModel().get_all_settings(cache=True)
273 rc_config = SettingsModel().get_all_settings(cache=True)
274
274
275 context.rhodecode_version = rhodecode.__version__
275 context.rhodecode_version = rhodecode.__version__
276 context.rhodecode_edition = config.get('rhodecode.edition')
276 context.rhodecode_edition = config.get('rhodecode.edition')
277 # unique secret + version does not leak the version but keep consistency
277 # unique secret + version does not leak the version but keep consistency
278 context.rhodecode_version_hash = md5(
278 context.rhodecode_version_hash = md5(
279 config.get('beaker.session.secret', '') +
279 config.get('beaker.session.secret', '') +
280 rhodecode.__version__)[:8]
280 rhodecode.__version__)[:8]
281
281
282 # Default language set for the incoming request
282 # Default language set for the incoming request
283 context.language = translation.get_lang()[0]
283 context.language = translation.get_lang()[0]
284
284
285 # Visual options
285 # Visual options
286 context.visual = AttributeDict({})
286 context.visual = AttributeDict({})
287
287
288 # DB stored Visual Items
288 # DB stored Visual Items
289 context.visual.show_public_icon = str2bool(
289 context.visual.show_public_icon = str2bool(
290 rc_config.get('rhodecode_show_public_icon'))
290 rc_config.get('rhodecode_show_public_icon'))
291 context.visual.show_private_icon = str2bool(
291 context.visual.show_private_icon = str2bool(
292 rc_config.get('rhodecode_show_private_icon'))
292 rc_config.get('rhodecode_show_private_icon'))
293 context.visual.stylify_metatags = str2bool(
293 context.visual.stylify_metatags = str2bool(
294 rc_config.get('rhodecode_stylify_metatags'))
294 rc_config.get('rhodecode_stylify_metatags'))
295 context.visual.dashboard_items = safe_int(
295 context.visual.dashboard_items = safe_int(
296 rc_config.get('rhodecode_dashboard_items', 100))
296 rc_config.get('rhodecode_dashboard_items', 100))
297 context.visual.admin_grid_items = safe_int(
297 context.visual.admin_grid_items = safe_int(
298 rc_config.get('rhodecode_admin_grid_items', 100))
298 rc_config.get('rhodecode_admin_grid_items', 100))
299 context.visual.repository_fields = str2bool(
299 context.visual.repository_fields = str2bool(
300 rc_config.get('rhodecode_repository_fields'))
300 rc_config.get('rhodecode_repository_fields'))
301 context.visual.show_version = str2bool(
301 context.visual.show_version = str2bool(
302 rc_config.get('rhodecode_show_version'))
302 rc_config.get('rhodecode_show_version'))
303 context.visual.use_gravatar = str2bool(
303 context.visual.use_gravatar = str2bool(
304 rc_config.get('rhodecode_use_gravatar'))
304 rc_config.get('rhodecode_use_gravatar'))
305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
305 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
306 context.visual.default_renderer = rc_config.get(
306 context.visual.default_renderer = rc_config.get(
307 'rhodecode_markup_renderer', 'rst')
307 'rhodecode_markup_renderer', 'rst')
308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
308 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
309 context.visual.rhodecode_support_url = \
309 context.visual.rhodecode_support_url = \
310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
310 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
311
311
312 context.pre_code = rc_config.get('rhodecode_pre_code')
312 context.pre_code = rc_config.get('rhodecode_pre_code')
313 context.post_code = rc_config.get('rhodecode_post_code')
313 context.post_code = rc_config.get('rhodecode_post_code')
314 context.rhodecode_name = rc_config.get('rhodecode_title')
314 context.rhodecode_name = rc_config.get('rhodecode_title')
315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
315 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
316 # if we have specified default_encoding in the request, it has more
316 # if we have specified default_encoding in the request, it has more
317 # priority
317 # priority
318 if request.GET.get('default_encoding'):
318 if request.GET.get('default_encoding'):
319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
319 context.default_encodings.insert(0, request.GET.get('default_encoding'))
320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
320 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
321
321
322 # INI stored
322 # INI stored
323 context.labs_active = str2bool(
323 context.labs_active = str2bool(
324 config.get('labs_settings_active', 'false'))
324 config.get('labs_settings_active', 'false'))
325 context.visual.allow_repo_location_change = str2bool(
325 context.visual.allow_repo_location_change = str2bool(
326 config.get('allow_repo_location_change', True))
326 config.get('allow_repo_location_change', True))
327 context.visual.allow_custom_hooks_settings = str2bool(
327 context.visual.allow_custom_hooks_settings = str2bool(
328 config.get('allow_custom_hooks_settings', True))
328 config.get('allow_custom_hooks_settings', True))
329 context.debug_style = str2bool(config.get('debug_style', False))
329 context.debug_style = str2bool(config.get('debug_style', False))
330
330
331 context.rhodecode_instanceid = config.get('instance_id')
331 context.rhodecode_instanceid = config.get('instance_id')
332
332
333 # AppEnlight
333 # AppEnlight
334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
334 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
335 context.appenlight_api_public_key = config.get(
335 context.appenlight_api_public_key = config.get(
336 'appenlight.api_public_key', '')
336 'appenlight.api_public_key', '')
337 context.appenlight_server_url = config.get('appenlight.server_url', '')
337 context.appenlight_server_url = config.get('appenlight.server_url', '')
338
338
339 # JS template context
339 # JS template context
340 context.template_context = {
340 context.template_context = {
341 'repo_name': None,
341 'repo_name': None,
342 'repo_type': None,
342 'repo_type': None,
343 'repo_landing_commit': None,
343 'repo_landing_commit': None,
344 'rhodecode_user': {
344 'rhodecode_user': {
345 'username': None,
345 'username': None,
346 'email': None,
346 'email': None,
347 'notification_status': False
347 'notification_status': False
348 },
348 },
349 'visual': {
349 'visual': {
350 'default_renderer': None
350 'default_renderer': None
351 },
351 },
352 'commit_data': {
352 'commit_data': {
353 'commit_id': None
353 'commit_id': None
354 },
354 },
355 'pull_request_data': {'pull_request_id': None},
355 'pull_request_data': {'pull_request_id': None},
356 'timeago': {
356 'timeago': {
357 'refresh_time': 120 * 1000,
357 'refresh_time': 120 * 1000,
358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
358 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
359 },
359 },
360 'pylons_dispatch': {
360 'pylons_dispatch': {
361 # 'controller': request.environ['pylons.routes_dict']['controller'],
361 # 'controller': request.environ['pylons.routes_dict']['controller'],
362 # 'action': request.environ['pylons.routes_dict']['action'],
362 # 'action': request.environ['pylons.routes_dict']['action'],
363 },
363 },
364 'pyramid_dispatch': {
364 'pyramid_dispatch': {
365
365
366 },
366 },
367 'extra': {'plugins': {}}
367 'extra': {'plugins': {}}
368 }
368 }
369 # END CONFIG VARS
369 # END CONFIG VARS
370
370
371 # TODO: This dosn't work when called from pylons compatibility tween.
371 # TODO: This dosn't work when called from pylons compatibility tween.
372 # Fix this and remove it from base controller.
372 # Fix this and remove it from base controller.
373 # context.repo_name = get_repo_slug(request) # can be empty
373 # context.repo_name = get_repo_slug(request) # can be empty
374
374
375 diffmode = 'sideside'
375 diffmode = 'sideside'
376 if request.GET.get('diffmode'):
376 if request.GET.get('diffmode'):
377 if request.GET['diffmode'] == 'unified':
377 if request.GET['diffmode'] == 'unified':
378 diffmode = 'unified'
378 diffmode = 'unified'
379 elif request.session.get('diffmode'):
379 elif request.session.get('diffmode'):
380 diffmode = request.session['diffmode']
380 diffmode = request.session['diffmode']
381
381
382 context.diffmode = diffmode
382 context.diffmode = diffmode
383
383
384 if request.session.get('diffmode') != diffmode:
384 if request.session.get('diffmode') != diffmode:
385 request.session['diffmode'] = diffmode
385 request.session['diffmode'] = diffmode
386
386
387 context.csrf_token = auth.get_csrf_token()
387 context.csrf_token = auth.get_csrf_token()
388 context.backends = rhodecode.BACKENDS.keys()
388 context.backends = rhodecode.BACKENDS.keys()
389 context.backends.sort()
389 context.backends.sort()
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
390 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
391 context.rhodecode_user.user_id)
391 context.rhodecode_user.user_id)
392
392
393 context.pyramid_request = pyramid.threadlocal.get_current_request()
393 context.pyramid_request = pyramid.threadlocal.get_current_request()
394
394
395
395
396 def get_auth_user(environ):
396 def get_auth_user(environ):
397 ip_addr = get_ip_addr(environ)
397 ip_addr = get_ip_addr(environ)
398 # make sure that we update permissions each time we call controller
398 # make sure that we update permissions each time we call controller
399 _auth_token = (request.GET.get('auth_token', '') or
399 _auth_token = (request.GET.get('auth_token', '') or
400 request.GET.get('api_key', ''))
400 request.GET.get('api_key', ''))
401
401
402 if _auth_token:
402 if _auth_token:
403 # when using API_KEY we assume user exists, and
403 # when using API_KEY we assume user exists, and
404 # doesn't need auth based on cookies.
404 # doesn't need auth based on cookies.
405 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
405 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
406 authenticated = False
406 authenticated = False
407 else:
407 else:
408 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
408 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
409 try:
409 try:
410 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
410 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
411 ip_addr=ip_addr)
411 ip_addr=ip_addr)
412 except UserCreationError as e:
412 except UserCreationError as e:
413 h.flash(e, 'error')
413 h.flash(e, 'error')
414 # container auth or other auth functions that create users
414 # container auth or other auth functions that create users
415 # on the fly can throw this exception signaling that there's
415 # on the fly can throw this exception signaling that there's
416 # issue with user creation, explanation should be provided
416 # issue with user creation, explanation should be provided
417 # in Exception itself. We then create a simple blank
417 # in Exception itself. We then create a simple blank
418 # AuthUser
418 # AuthUser
419 auth_user = AuthUser(ip_addr=ip_addr)
419 auth_user = AuthUser(ip_addr=ip_addr)
420
420
421 if password_changed(auth_user, session):
421 if password_changed(auth_user, session):
422 session.invalidate()
422 session.invalidate()
423 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
423 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
424 auth_user = AuthUser(ip_addr=ip_addr)
424 auth_user = AuthUser(ip_addr=ip_addr)
425
425
426 authenticated = cookie_store.get('is_authenticated')
426 authenticated = cookie_store.get('is_authenticated')
427
427
428 if not auth_user.is_authenticated and auth_user.is_user_object:
428 if not auth_user.is_authenticated and auth_user.is_user_object:
429 # user is not authenticated and not empty
429 # user is not authenticated and not empty
430 auth_user.set_authenticated(authenticated)
430 auth_user.set_authenticated(authenticated)
431
431
432 return auth_user
432 return auth_user
433
433
434
434
435 class BaseController(WSGIController):
435 class BaseController(WSGIController):
436
436
437 def __before__(self):
437 def __before__(self):
438 """
438 """
439 __before__ is called before controller methods and after __call__
439 __before__ is called before controller methods and after __call__
440 """
440 """
441 # on each call propagate settings calls into global settings.
441 # on each call propagate settings calls into global settings.
442 set_rhodecode_config(config)
442 set_rhodecode_config(config)
443 attach_context_attributes(c, request)
443 attach_context_attributes(c, request)
444
444
445 # TODO: Remove this when fixed in attach_context_attributes()
445 # TODO: Remove this when fixed in attach_context_attributes()
446 c.repo_name = get_repo_slug(request) # can be empty
446 c.repo_name = get_repo_slug(request) # can be empty
447
447
448 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
448 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
449 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
449 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
450 self.sa = meta.Session
450 self.sa = meta.Session
451 self.scm_model = ScmModel(self.sa)
451 self.scm_model = ScmModel(self.sa)
452
452
453 # set user language
453 # set user language
454 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
454 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
455 if user_lang:
455 if user_lang:
456 translation.set_lang(user_lang)
456 translation.set_lang(user_lang)
457 log.debug('set language to %s for user %s',
457 log.debug('set language to %s for user %s',
458 user_lang, self._rhodecode_user)
458 user_lang, self._rhodecode_user)
459
459
460 def _dispatch_redirect(self, with_url, environ, start_response):
460 def _dispatch_redirect(self, with_url, environ, start_response):
461 resp = HTTPFound(with_url)
461 resp = HTTPFound(with_url)
462 environ['SCRIPT_NAME'] = '' # handle prefix middleware
462 environ['SCRIPT_NAME'] = '' # handle prefix middleware
463 environ['PATH_INFO'] = with_url
463 environ['PATH_INFO'] = with_url
464 return resp(environ, start_response)
464 return resp(environ, start_response)
465
465
466 def __call__(self, environ, start_response):
466 def __call__(self, environ, start_response):
467 """Invoke the Controller"""
467 """Invoke the Controller"""
468 # WSGIController.__call__ dispatches to the Controller method
468 # WSGIController.__call__ dispatches to the Controller method
469 # the request is routed to. This routing information is
469 # the request is routed to. This routing information is
470 # available in environ['pylons.routes_dict']
470 # available in environ['pylons.routes_dict']
471 from rhodecode.lib import helpers as h
471 from rhodecode.lib import helpers as h
472
472
473 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
473 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
474 if environ.get('debugtoolbar.wants_pylons_context', False):
474 if environ.get('debugtoolbar.wants_pylons_context', False):
475 environ['debugtoolbar.pylons_context'] = c._current_obj()
475 environ['debugtoolbar.pylons_context'] = c._current_obj()
476
476
477 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
477 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
478 environ['pylons.routes_dict']['action']])
478 environ['pylons.routes_dict']['action']])
479
479
480 self.rc_config = SettingsModel().get_all_settings(cache=True)
480 self.rc_config = SettingsModel().get_all_settings(cache=True)
481 self.ip_addr = get_ip_addr(environ)
481 self.ip_addr = get_ip_addr(environ)
482
482
483 # The rhodecode auth user is looked up and passed through the
483 # The rhodecode auth user is looked up and passed through the
484 # environ by the pylons compatibility tween in pyramid.
484 # environ by the pylons compatibility tween in pyramid.
485 # So we can just grab it from there.
485 # So we can just grab it from there.
486 auth_user = environ['rc_auth_user']
486 auth_user = environ['rc_auth_user']
487
487
488 # set globals for auth user
488 # set globals for auth user
489 request.user = auth_user
489 request.user = auth_user
490 c.rhodecode_user = self._rhodecode_user = auth_user
490 c.rhodecode_user = self._rhodecode_user = auth_user
491
491
492 log.info('IP: %s User: %s accessed %s [%s]' % (
492 log.info('IP: %s User: %s accessed %s [%s]' % (
493 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
493 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
494 _route_name)
494 _route_name)
495 )
495 )
496
496
497 user_obj = auth_user.get_instance()
497 user_obj = auth_user.get_instance()
498 if user_obj and user_obj.user_data.get('force_password_change'):
498 if user_obj and user_obj.user_data.get('force_password_change'):
499 h.flash('You are required to change your password', 'warning',
499 h.flash('You are required to change your password', 'warning',
500 ignore_duplicate=True)
500 ignore_duplicate=True)
501 return self._dispatch_redirect(
501 return self._dispatch_redirect(
502 url('my_account_password'), environ, start_response)
502 url('my_account_password'), environ, start_response)
503
503
504 return WSGIController.__call__(self, environ, start_response)
504 return WSGIController.__call__(self, environ, start_response)
505
505
506
506
507 class BaseRepoController(BaseController):
507 class BaseRepoController(BaseController):
508 """
508 """
509 Base class for controllers responsible for loading all needed data for
509 Base class for controllers responsible for loading all needed data for
510 repository loaded items are
510 repository loaded items are
511
511
512 c.rhodecode_repo: instance of scm repository
512 c.rhodecode_repo: instance of scm repository
513 c.rhodecode_db_repo: instance of db
513 c.rhodecode_db_repo: instance of db
514 c.repository_requirements_missing: shows that repository specific data
514 c.repository_requirements_missing: shows that repository specific data
515 could not be displayed due to the missing requirements
515 could not be displayed due to the missing requirements
516 c.repository_pull_requests: show number of open pull requests
516 c.repository_pull_requests: show number of open pull requests
517 """
517 """
518
518
519 def __before__(self):
519 def __before__(self):
520 super(BaseRepoController, self).__before__()
520 super(BaseRepoController, self).__before__()
521 if c.repo_name: # extracted from routes
521 if c.repo_name: # extracted from routes
522 db_repo = Repository.get_by_repo_name(c.repo_name)
522 db_repo = Repository.get_by_repo_name(c.repo_name)
523 if not db_repo:
523 if not db_repo:
524 return
524 return
525
525
526 log.debug(
526 log.debug(
527 'Found repository in database %s with state `%s`',
527 'Found repository in database %s with state `%s`',
528 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
528 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
529 route = getattr(request.environ.get('routes.route'), 'name', '')
529 route = getattr(request.environ.get('routes.route'), 'name', '')
530
530
531 # allow to delete repos that are somehow damages in filesystem
531 # allow to delete repos that are somehow damages in filesystem
532 if route in ['delete_repo']:
532 if route in ['delete_repo']:
533 return
533 return
534
534
535 if db_repo.repo_state in [Repository.STATE_PENDING]:
535 if db_repo.repo_state in [Repository.STATE_PENDING]:
536 if route in ['repo_creating_home']:
536 if route in ['repo_creating_home']:
537 return
537 return
538 check_url = url('repo_creating_home', repo_name=c.repo_name)
538 check_url = url('repo_creating_home', repo_name=c.repo_name)
539 return redirect(check_url)
539 return redirect(check_url)
540
540
541 self.rhodecode_db_repo = db_repo
541 self.rhodecode_db_repo = db_repo
542
542
543 missing_requirements = False
543 missing_requirements = False
544 try:
544 try:
545 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
545 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
546 except RepositoryRequirementError as e:
546 except RepositoryRequirementError as e:
547 missing_requirements = True
547 missing_requirements = True
548 self._handle_missing_requirements(e)
548 self._handle_missing_requirements(e)
549
549
550 if self.rhodecode_repo is None and not missing_requirements:
550 if self.rhodecode_repo is None and not missing_requirements:
551 log.error('%s this repository is present in database but it '
551 log.error('%s this repository is present in database but it '
552 'cannot be created as an scm instance', c.repo_name)
552 'cannot be created as an scm instance', c.repo_name)
553
553
554 h.flash(_(
554 h.flash(_(
555 "The repository at %(repo_name)s cannot be located.") %
555 "The repository at %(repo_name)s cannot be located.") %
556 {'repo_name': c.repo_name},
556 {'repo_name': c.repo_name},
557 category='error', ignore_duplicate=True)
557 category='error', ignore_duplicate=True)
558 redirect(url('home'))
558 redirect(h.route_path('home'))
559
559
560 # update last change according to VCS data
560 # update last change according to VCS data
561 if not missing_requirements:
561 if not missing_requirements:
562 commit = db_repo.get_commit(
562 commit = db_repo.get_commit(
563 pre_load=["author", "date", "message", "parents"])
563 pre_load=["author", "date", "message", "parents"])
564 db_repo.update_commit_cache(commit)
564 db_repo.update_commit_cache(commit)
565
565
566 # Prepare context
566 # Prepare context
567 c.rhodecode_db_repo = db_repo
567 c.rhodecode_db_repo = db_repo
568 c.rhodecode_repo = self.rhodecode_repo
568 c.rhodecode_repo = self.rhodecode_repo
569 c.repository_requirements_missing = missing_requirements
569 c.repository_requirements_missing = missing_requirements
570
570
571 self._update_global_counters(self.scm_model, db_repo)
571 self._update_global_counters(self.scm_model, db_repo)
572
572
573 def _update_global_counters(self, scm_model, db_repo):
573 def _update_global_counters(self, scm_model, db_repo):
574 """
574 """
575 Base variables that are exposed to every page of repository
575 Base variables that are exposed to every page of repository
576 """
576 """
577 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
577 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
578
578
579 def _handle_missing_requirements(self, error):
579 def _handle_missing_requirements(self, error):
580 self.rhodecode_repo = None
580 self.rhodecode_repo = None
581 log.error(
581 log.error(
582 'Requirements are missing for repository %s: %s',
582 'Requirements are missing for repository %s: %s',
583 c.repo_name, error.message)
583 c.repo_name, error.message)
584
584
585 summary_url = url('summary_home', repo_name=c.repo_name)
585 summary_url = url('summary_home', repo_name=c.repo_name)
586 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
586 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
587 settings_update_url = url('repo', repo_name=c.repo_name)
587 settings_update_url = url('repo', repo_name=c.repo_name)
588 path = request.path
588 path = request.path
589 should_redirect = (
589 should_redirect = (
590 path not in (summary_url, settings_update_url)
590 path not in (summary_url, settings_update_url)
591 and '/settings' not in path or path == statistics_url
591 and '/settings' not in path or path == statistics_url
592 )
592 )
593 if should_redirect:
593 if should_redirect:
594 redirect(summary_url)
594 redirect(summary_url)
@@ -1,2027 +1,2027 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 from collections import OrderedDict
39 from collections import OrderedDict
40
40
41 import pygments
41 import pygments
42 import itertools
42 import itertools
43 import fnmatch
43 import fnmatch
44
44
45 from datetime import datetime
45 from datetime import datetime
46 from functools import partial
46 from functools import partial
47 from pygments.formatters.html import HtmlFormatter
47 from pygments.formatters.html import HtmlFormatter
48 from pygments import highlight as code_highlight
48 from pygments import highlight as code_highlight
49 from pygments.lexers import (
49 from pygments.lexers import (
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 from pylons import url as pylons_url
51 from pylons import url as pylons_url
52 from pylons.i18n.translation import _, ungettext
52 from pylons.i18n.translation import _, ungettext
53 from pyramid.threadlocal import get_current_request
53 from pyramid.threadlocal import get_current_request
54
54
55 from webhelpers.html import literal, HTML, escape
55 from webhelpers.html import literal, HTML, escape
56 from webhelpers.html.tools import *
56 from webhelpers.html.tools import *
57 from webhelpers.html.builder import make_tag
57 from webhelpers.html.builder import make_tag
58 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
59 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
60 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
61 submit, text, password, textarea, title, ul, xml_declaration, radio
61 submit, text, password, textarea, title, ul, xml_declaration, radio
62 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 from webhelpers.html.tools import auto_link, button_to, highlight, \
63 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
64 from webhelpers.pylonslib import Flash as _Flash
64 from webhelpers.pylonslib import Flash as _Flash
65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
65 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
66 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
67 replace_whitespace, urlify, truncate, wrap_paragraphs
67 replace_whitespace, urlify, truncate, wrap_paragraphs
68 from webhelpers.date import time_ago_in_words
68 from webhelpers.date import time_ago_in_words
69 from webhelpers.paginate import Page as _Page
69 from webhelpers.paginate import Page as _Page
70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
70 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
71 convert_boolean_attrs, NotGiven, _make_safe_id_component
71 convert_boolean_attrs, NotGiven, _make_safe_id_component
72 from webhelpers2.number import format_byte_size
72 from webhelpers2.number import format_byte_size
73
73
74 from rhodecode.lib.action_parser import action_parser
74 from rhodecode.lib.action_parser import action_parser
75 from rhodecode.lib.ext_json import json
75 from rhodecode.lib.ext_json import json
76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
76 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
77 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
78 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
79 AttributeDict, safe_int, md5, md5_safe
79 AttributeDict, safe_int, md5, md5_safe
80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
80 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
81 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
82 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
83 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
83 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
84 from rhodecode.model.changeset_status import ChangesetStatusModel
84 from rhodecode.model.changeset_status import ChangesetStatusModel
85 from rhodecode.model.db import Permission, User, Repository
85 from rhodecode.model.db import Permission, User, Repository
86 from rhodecode.model.repo_group import RepoGroupModel
86 from rhodecode.model.repo_group import RepoGroupModel
87 from rhodecode.model.settings import IssueTrackerSettingsModel
87 from rhodecode.model.settings import IssueTrackerSettingsModel
88
88
89 log = logging.getLogger(__name__)
89 log = logging.getLogger(__name__)
90
90
91
91
92 DEFAULT_USER = User.DEFAULT_USER
92 DEFAULT_USER = User.DEFAULT_USER
93 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
93 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
94
94
95
95
96 def url(*args, **kw):
96 def url(*args, **kw):
97 return pylons_url(*args, **kw)
97 return pylons_url(*args, **kw)
98
98
99
99
100 def pylons_url_current(*args, **kw):
100 def pylons_url_current(*args, **kw):
101 """
101 """
102 This function overrides pylons.url.current() which returns the current
102 This function overrides pylons.url.current() which returns the current
103 path so that it will also work from a pyramid only context. This
103 path so that it will also work from a pyramid only context. This
104 should be removed once port to pyramid is complete.
104 should be removed once port to pyramid is complete.
105 """
105 """
106 if not args and not kw:
106 if not args and not kw:
107 request = get_current_request()
107 request = get_current_request()
108 return request.path
108 return request.path
109 return pylons_url.current(*args, **kw)
109 return pylons_url.current(*args, **kw)
110
110
111 url.current = pylons_url_current
111 url.current = pylons_url_current
112
112
113
113
114 def url_replace(**qargs):
114 def url_replace(**qargs):
115 """ Returns the current request url while replacing query string args """
115 """ Returns the current request url while replacing query string args """
116
116
117 request = get_current_request()
117 request = get_current_request()
118 new_args = request.GET.mixed()
118 new_args = request.GET.mixed()
119 new_args.update(qargs)
119 new_args.update(qargs)
120 return url('', **new_args)
120 return url('', **new_args)
121
121
122
122
123 def asset(path, ver=None, **kwargs):
123 def asset(path, ver=None, **kwargs):
124 """
124 """
125 Helper to generate a static asset file path for rhodecode assets
125 Helper to generate a static asset file path for rhodecode assets
126
126
127 eg. h.asset('images/image.png', ver='3923')
127 eg. h.asset('images/image.png', ver='3923')
128
128
129 :param path: path of asset
129 :param path: path of asset
130 :param ver: optional version query param to append as ?ver=
130 :param ver: optional version query param to append as ?ver=
131 """
131 """
132 request = get_current_request()
132 request = get_current_request()
133 query = {}
133 query = {}
134 query.update(kwargs)
134 query.update(kwargs)
135 if ver:
135 if ver:
136 query = {'ver': ver}
136 query = {'ver': ver}
137 return request.static_path(
137 return request.static_path(
138 'rhodecode:public/{}'.format(path), _query=query)
138 'rhodecode:public/{}'.format(path), _query=query)
139
139
140
140
141 default_html_escape_table = {
141 default_html_escape_table = {
142 ord('&'): u'&amp;',
142 ord('&'): u'&amp;',
143 ord('<'): u'&lt;',
143 ord('<'): u'&lt;',
144 ord('>'): u'&gt;',
144 ord('>'): u'&gt;',
145 ord('"'): u'&quot;',
145 ord('"'): u'&quot;',
146 ord("'"): u'&#39;',
146 ord("'"): u'&#39;',
147 }
147 }
148
148
149
149
150 def html_escape(text, html_escape_table=default_html_escape_table):
150 def html_escape(text, html_escape_table=default_html_escape_table):
151 """Produce entities within text."""
151 """Produce entities within text."""
152 return text.translate(html_escape_table)
152 return text.translate(html_escape_table)
153
153
154
154
155 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
155 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
156 """
156 """
157 Truncate string ``s`` at the first occurrence of ``sub``.
157 Truncate string ``s`` at the first occurrence of ``sub``.
158
158
159 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
159 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
160 """
160 """
161 suffix_if_chopped = suffix_if_chopped or ''
161 suffix_if_chopped = suffix_if_chopped or ''
162 pos = s.find(sub)
162 pos = s.find(sub)
163 if pos == -1:
163 if pos == -1:
164 return s
164 return s
165
165
166 if inclusive:
166 if inclusive:
167 pos += len(sub)
167 pos += len(sub)
168
168
169 chopped = s[:pos]
169 chopped = s[:pos]
170 left = s[pos:].strip()
170 left = s[pos:].strip()
171
171
172 if left and suffix_if_chopped:
172 if left and suffix_if_chopped:
173 chopped += suffix_if_chopped
173 chopped += suffix_if_chopped
174
174
175 return chopped
175 return chopped
176
176
177
177
178 def shorter(text, size=20):
178 def shorter(text, size=20):
179 postfix = '...'
179 postfix = '...'
180 if len(text) > size:
180 if len(text) > size:
181 return text[:size - len(postfix)] + postfix
181 return text[:size - len(postfix)] + postfix
182 return text
182 return text
183
183
184
184
185 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
185 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 """
186 """
187 Reset button
187 Reset button
188 """
188 """
189 _set_input_attrs(attrs, type, name, value)
189 _set_input_attrs(attrs, type, name, value)
190 _set_id_attr(attrs, id, name)
190 _set_id_attr(attrs, id, name)
191 convert_boolean_attrs(attrs, ["disabled"])
191 convert_boolean_attrs(attrs, ["disabled"])
192 return HTML.input(**attrs)
192 return HTML.input(**attrs)
193
193
194 reset = _reset
194 reset = _reset
195 safeid = _make_safe_id_component
195 safeid = _make_safe_id_component
196
196
197
197
198 def branding(name, length=40):
198 def branding(name, length=40):
199 return truncate(name, length, indicator="")
199 return truncate(name, length, indicator="")
200
200
201
201
202 def FID(raw_id, path):
202 def FID(raw_id, path):
203 """
203 """
204 Creates a unique ID for filenode based on it's hash of path and commit
204 Creates a unique ID for filenode based on it's hash of path and commit
205 it's safe to use in urls
205 it's safe to use in urls
206
206
207 :param raw_id:
207 :param raw_id:
208 :param path:
208 :param path:
209 """
209 """
210
210
211 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
211 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
212
212
213
213
214 class _GetError(object):
214 class _GetError(object):
215 """Get error from form_errors, and represent it as span wrapped error
215 """Get error from form_errors, and represent it as span wrapped error
216 message
216 message
217
217
218 :param field_name: field to fetch errors for
218 :param field_name: field to fetch errors for
219 :param form_errors: form errors dict
219 :param form_errors: form errors dict
220 """
220 """
221
221
222 def __call__(self, field_name, form_errors):
222 def __call__(self, field_name, form_errors):
223 tmpl = """<span class="error_msg">%s</span>"""
223 tmpl = """<span class="error_msg">%s</span>"""
224 if form_errors and field_name in form_errors:
224 if form_errors and field_name in form_errors:
225 return literal(tmpl % form_errors.get(field_name))
225 return literal(tmpl % form_errors.get(field_name))
226
226
227 get_error = _GetError()
227 get_error = _GetError()
228
228
229
229
230 class _ToolTip(object):
230 class _ToolTip(object):
231
231
232 def __call__(self, tooltip_title, trim_at=50):
232 def __call__(self, tooltip_title, trim_at=50):
233 """
233 """
234 Special function just to wrap our text into nice formatted
234 Special function just to wrap our text into nice formatted
235 autowrapped text
235 autowrapped text
236
236
237 :param tooltip_title:
237 :param tooltip_title:
238 """
238 """
239 tooltip_title = escape(tooltip_title)
239 tooltip_title = escape(tooltip_title)
240 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
240 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
241 return tooltip_title
241 return tooltip_title
242 tooltip = _ToolTip()
242 tooltip = _ToolTip()
243
243
244
244
245 def files_breadcrumbs(repo_name, commit_id, file_path):
245 def files_breadcrumbs(repo_name, commit_id, file_path):
246 if isinstance(file_path, str):
246 if isinstance(file_path, str):
247 file_path = safe_unicode(file_path)
247 file_path = safe_unicode(file_path)
248
248
249 # TODO: johbo: Is this always a url like path, or is this operating
249 # TODO: johbo: Is this always a url like path, or is this operating
250 # system dependent?
250 # system dependent?
251 path_segments = file_path.split('/')
251 path_segments = file_path.split('/')
252
252
253 repo_name_html = escape(repo_name)
253 repo_name_html = escape(repo_name)
254 if len(path_segments) == 1 and path_segments[0] == '':
254 if len(path_segments) == 1 and path_segments[0] == '':
255 url_segments = [repo_name_html]
255 url_segments = [repo_name_html]
256 else:
256 else:
257 url_segments = [
257 url_segments = [
258 link_to(
258 link_to(
259 repo_name_html,
259 repo_name_html,
260 url('files_home',
260 url('files_home',
261 repo_name=repo_name,
261 repo_name=repo_name,
262 revision=commit_id,
262 revision=commit_id,
263 f_path=''),
263 f_path=''),
264 class_='pjax-link')]
264 class_='pjax-link')]
265
265
266 last_cnt = len(path_segments) - 1
266 last_cnt = len(path_segments) - 1
267 for cnt, segment in enumerate(path_segments):
267 for cnt, segment in enumerate(path_segments):
268 if not segment:
268 if not segment:
269 continue
269 continue
270 segment_html = escape(segment)
270 segment_html = escape(segment)
271
271
272 if cnt != last_cnt:
272 if cnt != last_cnt:
273 url_segments.append(
273 url_segments.append(
274 link_to(
274 link_to(
275 segment_html,
275 segment_html,
276 url('files_home',
276 url('files_home',
277 repo_name=repo_name,
277 repo_name=repo_name,
278 revision=commit_id,
278 revision=commit_id,
279 f_path='/'.join(path_segments[:cnt + 1])),
279 f_path='/'.join(path_segments[:cnt + 1])),
280 class_='pjax-link'))
280 class_='pjax-link'))
281 else:
281 else:
282 url_segments.append(segment_html)
282 url_segments.append(segment_html)
283
283
284 return literal('/'.join(url_segments))
284 return literal('/'.join(url_segments))
285
285
286
286
287 class CodeHtmlFormatter(HtmlFormatter):
287 class CodeHtmlFormatter(HtmlFormatter):
288 """
288 """
289 My code Html Formatter for source codes
289 My code Html Formatter for source codes
290 """
290 """
291
291
292 def wrap(self, source, outfile):
292 def wrap(self, source, outfile):
293 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
293 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
294
294
295 def _wrap_code(self, source):
295 def _wrap_code(self, source):
296 for cnt, it in enumerate(source):
296 for cnt, it in enumerate(source):
297 i, t = it
297 i, t = it
298 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
298 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
299 yield i, t
299 yield i, t
300
300
301 def _wrap_tablelinenos(self, inner):
301 def _wrap_tablelinenos(self, inner):
302 dummyoutfile = StringIO.StringIO()
302 dummyoutfile = StringIO.StringIO()
303 lncount = 0
303 lncount = 0
304 for t, line in inner:
304 for t, line in inner:
305 if t:
305 if t:
306 lncount += 1
306 lncount += 1
307 dummyoutfile.write(line)
307 dummyoutfile.write(line)
308
308
309 fl = self.linenostart
309 fl = self.linenostart
310 mw = len(str(lncount + fl - 1))
310 mw = len(str(lncount + fl - 1))
311 sp = self.linenospecial
311 sp = self.linenospecial
312 st = self.linenostep
312 st = self.linenostep
313 la = self.lineanchors
313 la = self.lineanchors
314 aln = self.anchorlinenos
314 aln = self.anchorlinenos
315 nocls = self.noclasses
315 nocls = self.noclasses
316 if sp:
316 if sp:
317 lines = []
317 lines = []
318
318
319 for i in range(fl, fl + lncount):
319 for i in range(fl, fl + lncount):
320 if i % st == 0:
320 if i % st == 0:
321 if i % sp == 0:
321 if i % sp == 0:
322 if aln:
322 if aln:
323 lines.append('<a href="#%s%d" class="special">%*d</a>' %
323 lines.append('<a href="#%s%d" class="special">%*d</a>' %
324 (la, i, mw, i))
324 (la, i, mw, i))
325 else:
325 else:
326 lines.append('<span class="special">%*d</span>' % (mw, i))
326 lines.append('<span class="special">%*d</span>' % (mw, i))
327 else:
327 else:
328 if aln:
328 if aln:
329 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
329 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
330 else:
330 else:
331 lines.append('%*d' % (mw, i))
331 lines.append('%*d' % (mw, i))
332 else:
332 else:
333 lines.append('')
333 lines.append('')
334 ls = '\n'.join(lines)
334 ls = '\n'.join(lines)
335 else:
335 else:
336 lines = []
336 lines = []
337 for i in range(fl, fl + lncount):
337 for i in range(fl, fl + lncount):
338 if i % st == 0:
338 if i % st == 0:
339 if aln:
339 if aln:
340 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
340 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
341 else:
341 else:
342 lines.append('%*d' % (mw, i))
342 lines.append('%*d' % (mw, i))
343 else:
343 else:
344 lines.append('')
344 lines.append('')
345 ls = '\n'.join(lines)
345 ls = '\n'.join(lines)
346
346
347 # in case you wonder about the seemingly redundant <div> here: since the
347 # in case you wonder about the seemingly redundant <div> here: since the
348 # content in the other cell also is wrapped in a div, some browsers in
348 # content in the other cell also is wrapped in a div, some browsers in
349 # some configurations seem to mess up the formatting...
349 # some configurations seem to mess up the formatting...
350 if nocls:
350 if nocls:
351 yield 0, ('<table class="%stable">' % self.cssclass +
351 yield 0, ('<table class="%stable">' % self.cssclass +
352 '<tr><td><div class="linenodiv" '
352 '<tr><td><div class="linenodiv" '
353 'style="background-color: #f0f0f0; padding-right: 10px">'
353 'style="background-color: #f0f0f0; padding-right: 10px">'
354 '<pre style="line-height: 125%">' +
354 '<pre style="line-height: 125%">' +
355 ls + '</pre></div></td><td id="hlcode" class="code">')
355 ls + '</pre></div></td><td id="hlcode" class="code">')
356 else:
356 else:
357 yield 0, ('<table class="%stable">' % self.cssclass +
357 yield 0, ('<table class="%stable">' % self.cssclass +
358 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
358 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
359 ls + '</pre></div></td><td id="hlcode" class="code">')
359 ls + '</pre></div></td><td id="hlcode" class="code">')
360 yield 0, dummyoutfile.getvalue()
360 yield 0, dummyoutfile.getvalue()
361 yield 0, '</td></tr></table>'
361 yield 0, '</td></tr></table>'
362
362
363
363
364 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
364 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
365 def __init__(self, **kw):
365 def __init__(self, **kw):
366 # only show these line numbers if set
366 # only show these line numbers if set
367 self.only_lines = kw.pop('only_line_numbers', [])
367 self.only_lines = kw.pop('only_line_numbers', [])
368 self.query_terms = kw.pop('query_terms', [])
368 self.query_terms = kw.pop('query_terms', [])
369 self.max_lines = kw.pop('max_lines', 5)
369 self.max_lines = kw.pop('max_lines', 5)
370 self.line_context = kw.pop('line_context', 3)
370 self.line_context = kw.pop('line_context', 3)
371 self.url = kw.pop('url', None)
371 self.url = kw.pop('url', None)
372
372
373 super(CodeHtmlFormatter, self).__init__(**kw)
373 super(CodeHtmlFormatter, self).__init__(**kw)
374
374
375 def _wrap_code(self, source):
375 def _wrap_code(self, source):
376 for cnt, it in enumerate(source):
376 for cnt, it in enumerate(source):
377 i, t = it
377 i, t = it
378 t = '<pre>%s</pre>' % t
378 t = '<pre>%s</pre>' % t
379 yield i, t
379 yield i, t
380
380
381 def _wrap_tablelinenos(self, inner):
381 def _wrap_tablelinenos(self, inner):
382 yield 0, '<table class="code-highlight %stable">' % self.cssclass
382 yield 0, '<table class="code-highlight %stable">' % self.cssclass
383
383
384 last_shown_line_number = 0
384 last_shown_line_number = 0
385 current_line_number = 1
385 current_line_number = 1
386
386
387 for t, line in inner:
387 for t, line in inner:
388 if not t:
388 if not t:
389 yield t, line
389 yield t, line
390 continue
390 continue
391
391
392 if current_line_number in self.only_lines:
392 if current_line_number in self.only_lines:
393 if last_shown_line_number + 1 != current_line_number:
393 if last_shown_line_number + 1 != current_line_number:
394 yield 0, '<tr>'
394 yield 0, '<tr>'
395 yield 0, '<td class="line">...</td>'
395 yield 0, '<td class="line">...</td>'
396 yield 0, '<td id="hlcode" class="code"></td>'
396 yield 0, '<td id="hlcode" class="code"></td>'
397 yield 0, '</tr>'
397 yield 0, '</tr>'
398
398
399 yield 0, '<tr>'
399 yield 0, '<tr>'
400 if self.url:
400 if self.url:
401 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
401 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
402 self.url, current_line_number, current_line_number)
402 self.url, current_line_number, current_line_number)
403 else:
403 else:
404 yield 0, '<td class="line"><a href="">%i</a></td>' % (
404 yield 0, '<td class="line"><a href="">%i</a></td>' % (
405 current_line_number)
405 current_line_number)
406 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
406 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
407 yield 0, '</tr>'
407 yield 0, '</tr>'
408
408
409 last_shown_line_number = current_line_number
409 last_shown_line_number = current_line_number
410
410
411 current_line_number += 1
411 current_line_number += 1
412
412
413
413
414 yield 0, '</table>'
414 yield 0, '</table>'
415
415
416
416
417 def extract_phrases(text_query):
417 def extract_phrases(text_query):
418 """
418 """
419 Extracts phrases from search term string making sure phrases
419 Extracts phrases from search term string making sure phrases
420 contained in double quotes are kept together - and discarding empty values
420 contained in double quotes are kept together - and discarding empty values
421 or fully whitespace values eg.
421 or fully whitespace values eg.
422
422
423 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
423 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
424
424
425 """
425 """
426
426
427 in_phrase = False
427 in_phrase = False
428 buf = ''
428 buf = ''
429 phrases = []
429 phrases = []
430 for char in text_query:
430 for char in text_query:
431 if in_phrase:
431 if in_phrase:
432 if char == '"': # end phrase
432 if char == '"': # end phrase
433 phrases.append(buf)
433 phrases.append(buf)
434 buf = ''
434 buf = ''
435 in_phrase = False
435 in_phrase = False
436 continue
436 continue
437 else:
437 else:
438 buf += char
438 buf += char
439 continue
439 continue
440 else:
440 else:
441 if char == '"': # start phrase
441 if char == '"': # start phrase
442 in_phrase = True
442 in_phrase = True
443 phrases.append(buf)
443 phrases.append(buf)
444 buf = ''
444 buf = ''
445 continue
445 continue
446 elif char == ' ':
446 elif char == ' ':
447 phrases.append(buf)
447 phrases.append(buf)
448 buf = ''
448 buf = ''
449 continue
449 continue
450 else:
450 else:
451 buf += char
451 buf += char
452
452
453 phrases.append(buf)
453 phrases.append(buf)
454 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
454 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
455 return phrases
455 return phrases
456
456
457
457
458 def get_matching_offsets(text, phrases):
458 def get_matching_offsets(text, phrases):
459 """
459 """
460 Returns a list of string offsets in `text` that the list of `terms` match
460 Returns a list of string offsets in `text` that the list of `terms` match
461
461
462 >>> get_matching_offsets('some text here', ['some', 'here'])
462 >>> get_matching_offsets('some text here', ['some', 'here'])
463 [(0, 4), (10, 14)]
463 [(0, 4), (10, 14)]
464
464
465 """
465 """
466 offsets = []
466 offsets = []
467 for phrase in phrases:
467 for phrase in phrases:
468 for match in re.finditer(phrase, text):
468 for match in re.finditer(phrase, text):
469 offsets.append((match.start(), match.end()))
469 offsets.append((match.start(), match.end()))
470
470
471 return offsets
471 return offsets
472
472
473
473
474 def normalize_text_for_matching(x):
474 def normalize_text_for_matching(x):
475 """
475 """
476 Replaces all non alnum characters to spaces and lower cases the string,
476 Replaces all non alnum characters to spaces and lower cases the string,
477 useful for comparing two text strings without punctuation
477 useful for comparing two text strings without punctuation
478 """
478 """
479 return re.sub(r'[^\w]', ' ', x.lower())
479 return re.sub(r'[^\w]', ' ', x.lower())
480
480
481
481
482 def get_matching_line_offsets(lines, terms):
482 def get_matching_line_offsets(lines, terms):
483 """ Return a set of `lines` indices (starting from 1) matching a
483 """ Return a set of `lines` indices (starting from 1) matching a
484 text search query, along with `context` lines above/below matching lines
484 text search query, along with `context` lines above/below matching lines
485
485
486 :param lines: list of strings representing lines
486 :param lines: list of strings representing lines
487 :param terms: search term string to match in lines eg. 'some text'
487 :param terms: search term string to match in lines eg. 'some text'
488 :param context: number of lines above/below a matching line to add to result
488 :param context: number of lines above/below a matching line to add to result
489 :param max_lines: cut off for lines of interest
489 :param max_lines: cut off for lines of interest
490 eg.
490 eg.
491
491
492 text = '''
492 text = '''
493 words words words
493 words words words
494 words words words
494 words words words
495 some text some
495 some text some
496 words words words
496 words words words
497 words words words
497 words words words
498 text here what
498 text here what
499 '''
499 '''
500 get_matching_line_offsets(text, 'text', context=1)
500 get_matching_line_offsets(text, 'text', context=1)
501 {3: [(5, 9)], 6: [(0, 4)]]
501 {3: [(5, 9)], 6: [(0, 4)]]
502
502
503 """
503 """
504 matching_lines = {}
504 matching_lines = {}
505 phrases = [normalize_text_for_matching(phrase)
505 phrases = [normalize_text_for_matching(phrase)
506 for phrase in extract_phrases(terms)]
506 for phrase in extract_phrases(terms)]
507
507
508 for line_index, line in enumerate(lines, start=1):
508 for line_index, line in enumerate(lines, start=1):
509 match_offsets = get_matching_offsets(
509 match_offsets = get_matching_offsets(
510 normalize_text_for_matching(line), phrases)
510 normalize_text_for_matching(line), phrases)
511 if match_offsets:
511 if match_offsets:
512 matching_lines[line_index] = match_offsets
512 matching_lines[line_index] = match_offsets
513
513
514 return matching_lines
514 return matching_lines
515
515
516
516
517 def hsv_to_rgb(h, s, v):
517 def hsv_to_rgb(h, s, v):
518 """ Convert hsv color values to rgb """
518 """ Convert hsv color values to rgb """
519
519
520 if s == 0.0:
520 if s == 0.0:
521 return v, v, v
521 return v, v, v
522 i = int(h * 6.0) # XXX assume int() truncates!
522 i = int(h * 6.0) # XXX assume int() truncates!
523 f = (h * 6.0) - i
523 f = (h * 6.0) - i
524 p = v * (1.0 - s)
524 p = v * (1.0 - s)
525 q = v * (1.0 - s * f)
525 q = v * (1.0 - s * f)
526 t = v * (1.0 - s * (1.0 - f))
526 t = v * (1.0 - s * (1.0 - f))
527 i = i % 6
527 i = i % 6
528 if i == 0:
528 if i == 0:
529 return v, t, p
529 return v, t, p
530 if i == 1:
530 if i == 1:
531 return q, v, p
531 return q, v, p
532 if i == 2:
532 if i == 2:
533 return p, v, t
533 return p, v, t
534 if i == 3:
534 if i == 3:
535 return p, q, v
535 return p, q, v
536 if i == 4:
536 if i == 4:
537 return t, p, v
537 return t, p, v
538 if i == 5:
538 if i == 5:
539 return v, p, q
539 return v, p, q
540
540
541
541
542 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
542 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
543 """
543 """
544 Generator for getting n of evenly distributed colors using
544 Generator for getting n of evenly distributed colors using
545 hsv color and golden ratio. It always return same order of colors
545 hsv color and golden ratio. It always return same order of colors
546
546
547 :param n: number of colors to generate
547 :param n: number of colors to generate
548 :param saturation: saturation of returned colors
548 :param saturation: saturation of returned colors
549 :param lightness: lightness of returned colors
549 :param lightness: lightness of returned colors
550 :returns: RGB tuple
550 :returns: RGB tuple
551 """
551 """
552
552
553 golden_ratio = 0.618033988749895
553 golden_ratio = 0.618033988749895
554 h = 0.22717784590367374
554 h = 0.22717784590367374
555
555
556 for _ in xrange(n):
556 for _ in xrange(n):
557 h += golden_ratio
557 h += golden_ratio
558 h %= 1
558 h %= 1
559 HSV_tuple = [h, saturation, lightness]
559 HSV_tuple = [h, saturation, lightness]
560 RGB_tuple = hsv_to_rgb(*HSV_tuple)
560 RGB_tuple = hsv_to_rgb(*HSV_tuple)
561 yield map(lambda x: str(int(x * 256)), RGB_tuple)
561 yield map(lambda x: str(int(x * 256)), RGB_tuple)
562
562
563
563
564 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
564 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
565 """
565 """
566 Returns a function which when called with an argument returns a unique
566 Returns a function which when called with an argument returns a unique
567 color for that argument, eg.
567 color for that argument, eg.
568
568
569 :param n: number of colors to generate
569 :param n: number of colors to generate
570 :param saturation: saturation of returned colors
570 :param saturation: saturation of returned colors
571 :param lightness: lightness of returned colors
571 :param lightness: lightness of returned colors
572 :returns: css RGB string
572 :returns: css RGB string
573
573
574 >>> color_hash = color_hasher()
574 >>> color_hash = color_hasher()
575 >>> color_hash('hello')
575 >>> color_hash('hello')
576 'rgb(34, 12, 59)'
576 'rgb(34, 12, 59)'
577 >>> color_hash('hello')
577 >>> color_hash('hello')
578 'rgb(34, 12, 59)'
578 'rgb(34, 12, 59)'
579 >>> color_hash('other')
579 >>> color_hash('other')
580 'rgb(90, 224, 159)'
580 'rgb(90, 224, 159)'
581 """
581 """
582
582
583 color_dict = {}
583 color_dict = {}
584 cgenerator = unique_color_generator(
584 cgenerator = unique_color_generator(
585 saturation=saturation, lightness=lightness)
585 saturation=saturation, lightness=lightness)
586
586
587 def get_color_string(thing):
587 def get_color_string(thing):
588 if thing in color_dict:
588 if thing in color_dict:
589 col = color_dict[thing]
589 col = color_dict[thing]
590 else:
590 else:
591 col = color_dict[thing] = cgenerator.next()
591 col = color_dict[thing] = cgenerator.next()
592 return "rgb(%s)" % (', '.join(col))
592 return "rgb(%s)" % (', '.join(col))
593
593
594 return get_color_string
594 return get_color_string
595
595
596
596
597 def get_lexer_safe(mimetype=None, filepath=None):
597 def get_lexer_safe(mimetype=None, filepath=None):
598 """
598 """
599 Tries to return a relevant pygments lexer using mimetype/filepath name,
599 Tries to return a relevant pygments lexer using mimetype/filepath name,
600 defaulting to plain text if none could be found
600 defaulting to plain text if none could be found
601 """
601 """
602 lexer = None
602 lexer = None
603 try:
603 try:
604 if mimetype:
604 if mimetype:
605 lexer = get_lexer_for_mimetype(mimetype)
605 lexer = get_lexer_for_mimetype(mimetype)
606 if not lexer:
606 if not lexer:
607 lexer = get_lexer_for_filename(filepath)
607 lexer = get_lexer_for_filename(filepath)
608 except pygments.util.ClassNotFound:
608 except pygments.util.ClassNotFound:
609 pass
609 pass
610
610
611 if not lexer:
611 if not lexer:
612 lexer = get_lexer_by_name('text')
612 lexer = get_lexer_by_name('text')
613
613
614 return lexer
614 return lexer
615
615
616
616
617 def get_lexer_for_filenode(filenode):
617 def get_lexer_for_filenode(filenode):
618 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
618 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
619 return lexer
619 return lexer
620
620
621
621
622 def pygmentize(filenode, **kwargs):
622 def pygmentize(filenode, **kwargs):
623 """
623 """
624 pygmentize function using pygments
624 pygmentize function using pygments
625
625
626 :param filenode:
626 :param filenode:
627 """
627 """
628 lexer = get_lexer_for_filenode(filenode)
628 lexer = get_lexer_for_filenode(filenode)
629 return literal(code_highlight(filenode.content, lexer,
629 return literal(code_highlight(filenode.content, lexer,
630 CodeHtmlFormatter(**kwargs)))
630 CodeHtmlFormatter(**kwargs)))
631
631
632
632
633 def is_following_repo(repo_name, user_id):
633 def is_following_repo(repo_name, user_id):
634 from rhodecode.model.scm import ScmModel
634 from rhodecode.model.scm import ScmModel
635 return ScmModel().is_following_repo(repo_name, user_id)
635 return ScmModel().is_following_repo(repo_name, user_id)
636
636
637
637
638 class _Message(object):
638 class _Message(object):
639 """A message returned by ``Flash.pop_messages()``.
639 """A message returned by ``Flash.pop_messages()``.
640
640
641 Converting the message to a string returns the message text. Instances
641 Converting the message to a string returns the message text. Instances
642 also have the following attributes:
642 also have the following attributes:
643
643
644 * ``message``: the message text.
644 * ``message``: the message text.
645 * ``category``: the category specified when the message was created.
645 * ``category``: the category specified when the message was created.
646 """
646 """
647
647
648 def __init__(self, category, message):
648 def __init__(self, category, message):
649 self.category = category
649 self.category = category
650 self.message = message
650 self.message = message
651
651
652 def __str__(self):
652 def __str__(self):
653 return self.message
653 return self.message
654
654
655 __unicode__ = __str__
655 __unicode__ = __str__
656
656
657 def __html__(self):
657 def __html__(self):
658 return escape(safe_unicode(self.message))
658 return escape(safe_unicode(self.message))
659
659
660
660
661 class Flash(_Flash):
661 class Flash(_Flash):
662
662
663 def pop_messages(self):
663 def pop_messages(self):
664 """Return all accumulated messages and delete them from the session.
664 """Return all accumulated messages and delete them from the session.
665
665
666 The return value is a list of ``Message`` objects.
666 The return value is a list of ``Message`` objects.
667 """
667 """
668 from pylons import session
668 from pylons import session
669
669
670 messages = []
670 messages = []
671
671
672 # Pop the 'old' pylons flash messages. They are tuples of the form
672 # Pop the 'old' pylons flash messages. They are tuples of the form
673 # (category, message)
673 # (category, message)
674 for cat, msg in session.pop(self.session_key, []):
674 for cat, msg in session.pop(self.session_key, []):
675 messages.append(_Message(cat, msg))
675 messages.append(_Message(cat, msg))
676
676
677 # Pop the 'new' pyramid flash messages for each category as list
677 # Pop the 'new' pyramid flash messages for each category as list
678 # of strings.
678 # of strings.
679 for cat in self.categories:
679 for cat in self.categories:
680 for msg in session.pop_flash(queue=cat):
680 for msg in session.pop_flash(queue=cat):
681 messages.append(_Message(cat, msg))
681 messages.append(_Message(cat, msg))
682 # Map messages from the default queue to the 'notice' category.
682 # Map messages from the default queue to the 'notice' category.
683 for msg in session.pop_flash():
683 for msg in session.pop_flash():
684 messages.append(_Message('notice', msg))
684 messages.append(_Message('notice', msg))
685
685
686 session.save()
686 session.save()
687 return messages
687 return messages
688
688
689 def json_alerts(self):
689 def json_alerts(self):
690 payloads = []
690 payloads = []
691 messages = flash.pop_messages()
691 messages = flash.pop_messages()
692 if messages:
692 if messages:
693 for message in messages:
693 for message in messages:
694 subdata = {}
694 subdata = {}
695 if hasattr(message.message, 'rsplit'):
695 if hasattr(message.message, 'rsplit'):
696 flash_data = message.message.rsplit('|DELIM|', 1)
696 flash_data = message.message.rsplit('|DELIM|', 1)
697 org_message = flash_data[0]
697 org_message = flash_data[0]
698 if len(flash_data) > 1:
698 if len(flash_data) > 1:
699 subdata = json.loads(flash_data[1])
699 subdata = json.loads(flash_data[1])
700 else:
700 else:
701 org_message = message.message
701 org_message = message.message
702 payloads.append({
702 payloads.append({
703 'message': {
703 'message': {
704 'message': u'{}'.format(org_message),
704 'message': u'{}'.format(org_message),
705 'level': message.category,
705 'level': message.category,
706 'force': True,
706 'force': True,
707 'subdata': subdata
707 'subdata': subdata
708 }
708 }
709 })
709 })
710 return json.dumps(payloads)
710 return json.dumps(payloads)
711
711
712 flash = Flash()
712 flash = Flash()
713
713
714 #==============================================================================
714 #==============================================================================
715 # SCM FILTERS available via h.
715 # SCM FILTERS available via h.
716 #==============================================================================
716 #==============================================================================
717 from rhodecode.lib.vcs.utils import author_name, author_email
717 from rhodecode.lib.vcs.utils import author_name, author_email
718 from rhodecode.lib.utils2 import credentials_filter, age as _age
718 from rhodecode.lib.utils2 import credentials_filter, age as _age
719 from rhodecode.model.db import User, ChangesetStatus
719 from rhodecode.model.db import User, ChangesetStatus
720
720
721 age = _age
721 age = _age
722 capitalize = lambda x: x.capitalize()
722 capitalize = lambda x: x.capitalize()
723 email = author_email
723 email = author_email
724 short_id = lambda x: x[:12]
724 short_id = lambda x: x[:12]
725 hide_credentials = lambda x: ''.join(credentials_filter(x))
725 hide_credentials = lambda x: ''.join(credentials_filter(x))
726
726
727
727
728 def age_component(datetime_iso, value=None, time_is_local=False):
728 def age_component(datetime_iso, value=None, time_is_local=False):
729 title = value or format_date(datetime_iso)
729 title = value or format_date(datetime_iso)
730 tzinfo = '+00:00'
730 tzinfo = '+00:00'
731
731
732 # detect if we have a timezone info, otherwise, add it
732 # detect if we have a timezone info, otherwise, add it
733 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
733 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
734 if time_is_local:
734 if time_is_local:
735 tzinfo = time.strftime("+%H:%M",
735 tzinfo = time.strftime("+%H:%M",
736 time.gmtime(
736 time.gmtime(
737 (datetime.now() - datetime.utcnow()).seconds + 1
737 (datetime.now() - datetime.utcnow()).seconds + 1
738 )
738 )
739 )
739 )
740
740
741 return literal(
741 return literal(
742 '<time class="timeago tooltip" '
742 '<time class="timeago tooltip" '
743 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
743 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
744 datetime_iso, title, tzinfo))
744 datetime_iso, title, tzinfo))
745
745
746
746
747 def _shorten_commit_id(commit_id):
747 def _shorten_commit_id(commit_id):
748 from rhodecode import CONFIG
748 from rhodecode import CONFIG
749 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
749 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
750 return commit_id[:def_len]
750 return commit_id[:def_len]
751
751
752
752
753 def show_id(commit):
753 def show_id(commit):
754 """
754 """
755 Configurable function that shows ID
755 Configurable function that shows ID
756 by default it's r123:fffeeefffeee
756 by default it's r123:fffeeefffeee
757
757
758 :param commit: commit instance
758 :param commit: commit instance
759 """
759 """
760 from rhodecode import CONFIG
760 from rhodecode import CONFIG
761 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
761 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
762
762
763 raw_id = _shorten_commit_id(commit.raw_id)
763 raw_id = _shorten_commit_id(commit.raw_id)
764 if show_idx:
764 if show_idx:
765 return 'r%s:%s' % (commit.idx, raw_id)
765 return 'r%s:%s' % (commit.idx, raw_id)
766 else:
766 else:
767 return '%s' % (raw_id, )
767 return '%s' % (raw_id, )
768
768
769
769
770 def format_date(date):
770 def format_date(date):
771 """
771 """
772 use a standardized formatting for dates used in RhodeCode
772 use a standardized formatting for dates used in RhodeCode
773
773
774 :param date: date/datetime object
774 :param date: date/datetime object
775 :return: formatted date
775 :return: formatted date
776 """
776 """
777
777
778 if date:
778 if date:
779 _fmt = "%a, %d %b %Y %H:%M:%S"
779 _fmt = "%a, %d %b %Y %H:%M:%S"
780 return safe_unicode(date.strftime(_fmt))
780 return safe_unicode(date.strftime(_fmt))
781
781
782 return u""
782 return u""
783
783
784
784
785 class _RepoChecker(object):
785 class _RepoChecker(object):
786
786
787 def __init__(self, backend_alias):
787 def __init__(self, backend_alias):
788 self._backend_alias = backend_alias
788 self._backend_alias = backend_alias
789
789
790 def __call__(self, repository):
790 def __call__(self, repository):
791 if hasattr(repository, 'alias'):
791 if hasattr(repository, 'alias'):
792 _type = repository.alias
792 _type = repository.alias
793 elif hasattr(repository, 'repo_type'):
793 elif hasattr(repository, 'repo_type'):
794 _type = repository.repo_type
794 _type = repository.repo_type
795 else:
795 else:
796 _type = repository
796 _type = repository
797 return _type == self._backend_alias
797 return _type == self._backend_alias
798
798
799 is_git = _RepoChecker('git')
799 is_git = _RepoChecker('git')
800 is_hg = _RepoChecker('hg')
800 is_hg = _RepoChecker('hg')
801 is_svn = _RepoChecker('svn')
801 is_svn = _RepoChecker('svn')
802
802
803
803
804 def get_repo_type_by_name(repo_name):
804 def get_repo_type_by_name(repo_name):
805 repo = Repository.get_by_repo_name(repo_name)
805 repo = Repository.get_by_repo_name(repo_name)
806 return repo.repo_type
806 return repo.repo_type
807
807
808
808
809 def is_svn_without_proxy(repository):
809 def is_svn_without_proxy(repository):
810 if is_svn(repository):
810 if is_svn(repository):
811 from rhodecode.model.settings import VcsSettingsModel
811 from rhodecode.model.settings import VcsSettingsModel
812 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
812 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
813 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
813 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
814 return False
814 return False
815
815
816
816
817 def discover_user(author):
817 def discover_user(author):
818 """
818 """
819 Tries to discover RhodeCode User based on the autho string. Author string
819 Tries to discover RhodeCode User based on the autho string. Author string
820 is typically `FirstName LastName <email@address.com>`
820 is typically `FirstName LastName <email@address.com>`
821 """
821 """
822
822
823 # if author is already an instance use it for extraction
823 # if author is already an instance use it for extraction
824 if isinstance(author, User):
824 if isinstance(author, User):
825 return author
825 return author
826
826
827 # Valid email in the attribute passed, see if they're in the system
827 # Valid email in the attribute passed, see if they're in the system
828 _email = author_email(author)
828 _email = author_email(author)
829 if _email != '':
829 if _email != '':
830 user = User.get_by_email(_email, case_insensitive=True, cache=True)
830 user = User.get_by_email(_email, case_insensitive=True, cache=True)
831 if user is not None:
831 if user is not None:
832 return user
832 return user
833
833
834 # Maybe it's a username, we try to extract it and fetch by username ?
834 # Maybe it's a username, we try to extract it and fetch by username ?
835 _author = author_name(author)
835 _author = author_name(author)
836 user = User.get_by_username(_author, case_insensitive=True, cache=True)
836 user = User.get_by_username(_author, case_insensitive=True, cache=True)
837 if user is not None:
837 if user is not None:
838 return user
838 return user
839
839
840 return None
840 return None
841
841
842
842
843 def email_or_none(author):
843 def email_or_none(author):
844 # extract email from the commit string
844 # extract email from the commit string
845 _email = author_email(author)
845 _email = author_email(author)
846
846
847 # If we have an email, use it, otherwise
847 # If we have an email, use it, otherwise
848 # see if it contains a username we can get an email from
848 # see if it contains a username we can get an email from
849 if _email != '':
849 if _email != '':
850 return _email
850 return _email
851 else:
851 else:
852 user = User.get_by_username(
852 user = User.get_by_username(
853 author_name(author), case_insensitive=True, cache=True)
853 author_name(author), case_insensitive=True, cache=True)
854
854
855 if user is not None:
855 if user is not None:
856 return user.email
856 return user.email
857
857
858 # No valid email, not a valid user in the system, none!
858 # No valid email, not a valid user in the system, none!
859 return None
859 return None
860
860
861
861
862 def link_to_user(author, length=0, **kwargs):
862 def link_to_user(author, length=0, **kwargs):
863 user = discover_user(author)
863 user = discover_user(author)
864 # user can be None, but if we have it already it means we can re-use it
864 # user can be None, but if we have it already it means we can re-use it
865 # in the person() function, so we save 1 intensive-query
865 # in the person() function, so we save 1 intensive-query
866 if user:
866 if user:
867 author = user
867 author = user
868
868
869 display_person = person(author, 'username_or_name_or_email')
869 display_person = person(author, 'username_or_name_or_email')
870 if length:
870 if length:
871 display_person = shorter(display_person, length)
871 display_person = shorter(display_person, length)
872
872
873 if user:
873 if user:
874 return link_to(
874 return link_to(
875 escape(display_person),
875 escape(display_person),
876 route_path('user_profile', username=user.username),
876 route_path('user_profile', username=user.username),
877 **kwargs)
877 **kwargs)
878 else:
878 else:
879 return escape(display_person)
879 return escape(display_person)
880
880
881
881
882 def person(author, show_attr="username_and_name"):
882 def person(author, show_attr="username_and_name"):
883 user = discover_user(author)
883 user = discover_user(author)
884 if user:
884 if user:
885 return getattr(user, show_attr)
885 return getattr(user, show_attr)
886 else:
886 else:
887 _author = author_name(author)
887 _author = author_name(author)
888 _email = email(author)
888 _email = email(author)
889 return _author or _email
889 return _author or _email
890
890
891
891
892 def author_string(email):
892 def author_string(email):
893 if email:
893 if email:
894 user = User.get_by_email(email, case_insensitive=True, cache=True)
894 user = User.get_by_email(email, case_insensitive=True, cache=True)
895 if user:
895 if user:
896 if user.firstname or user.lastname:
896 if user.firstname or user.lastname:
897 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
897 return '%s %s &lt;%s&gt;' % (user.firstname, user.lastname, email)
898 else:
898 else:
899 return email
899 return email
900 else:
900 else:
901 return email
901 return email
902 else:
902 else:
903 return None
903 return None
904
904
905
905
906 def person_by_id(id_, show_attr="username_and_name"):
906 def person_by_id(id_, show_attr="username_and_name"):
907 # attr to return from fetched user
907 # attr to return from fetched user
908 person_getter = lambda usr: getattr(usr, show_attr)
908 person_getter = lambda usr: getattr(usr, show_attr)
909
909
910 #maybe it's an ID ?
910 #maybe it's an ID ?
911 if str(id_).isdigit() or isinstance(id_, int):
911 if str(id_).isdigit() or isinstance(id_, int):
912 id_ = int(id_)
912 id_ = int(id_)
913 user = User.get(id_)
913 user = User.get(id_)
914 if user is not None:
914 if user is not None:
915 return person_getter(user)
915 return person_getter(user)
916 return id_
916 return id_
917
917
918
918
919 def gravatar_with_user(author, show_disabled=False):
919 def gravatar_with_user(author, show_disabled=False):
920 from rhodecode.lib.utils import PartialRenderer
920 from rhodecode.lib.utils import PartialRenderer
921 _render = PartialRenderer('base/base.mako')
921 _render = PartialRenderer('base/base.mako')
922 return _render('gravatar_with_user', author, show_disabled=show_disabled)
922 return _render('gravatar_with_user', author, show_disabled=show_disabled)
923
923
924
924
925 def desc_stylize(value):
925 def desc_stylize(value):
926 """
926 """
927 converts tags from value into html equivalent
927 converts tags from value into html equivalent
928
928
929 :param value:
929 :param value:
930 """
930 """
931 if not value:
931 if not value:
932 return ''
932 return ''
933
933
934 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
934 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
935 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
935 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
936 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
936 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
937 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
937 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
938 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
938 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
939 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
939 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
940 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
940 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
941 '<div class="metatag" tag="lang">\\2</div>', value)
941 '<div class="metatag" tag="lang">\\2</div>', value)
942 value = re.sub(r'\[([a-z]+)\]',
942 value = re.sub(r'\[([a-z]+)\]',
943 '<div class="metatag" tag="\\1">\\1</div>', value)
943 '<div class="metatag" tag="\\1">\\1</div>', value)
944
944
945 return value
945 return value
946
946
947
947
948 def escaped_stylize(value):
948 def escaped_stylize(value):
949 """
949 """
950 converts tags from value into html equivalent, but escaping its value first
950 converts tags from value into html equivalent, but escaping its value first
951 """
951 """
952 if not value:
952 if not value:
953 return ''
953 return ''
954
954
955 # Using default webhelper escape method, but has to force it as a
955 # Using default webhelper escape method, but has to force it as a
956 # plain unicode instead of a markup tag to be used in regex expressions
956 # plain unicode instead of a markup tag to be used in regex expressions
957 value = unicode(escape(safe_unicode(value)))
957 value = unicode(escape(safe_unicode(value)))
958
958
959 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
959 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
960 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
960 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
961 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
961 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
962 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
962 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
963 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
963 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
964 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
964 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
965 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
965 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
966 '<div class="metatag" tag="lang">\\2</div>', value)
966 '<div class="metatag" tag="lang">\\2</div>', value)
967 value = re.sub(r'\[([a-z]+)\]',
967 value = re.sub(r'\[([a-z]+)\]',
968 '<div class="metatag" tag="\\1">\\1</div>', value)
968 '<div class="metatag" tag="\\1">\\1</div>', value)
969
969
970 return value
970 return value
971
971
972
972
973 def bool2icon(value):
973 def bool2icon(value):
974 """
974 """
975 Returns boolean value of a given value, represented as html element with
975 Returns boolean value of a given value, represented as html element with
976 classes that will represent icons
976 classes that will represent icons
977
977
978 :param value: given value to convert to html node
978 :param value: given value to convert to html node
979 """
979 """
980
980
981 if value: # does bool conversion
981 if value: # does bool conversion
982 return HTML.tag('i', class_="icon-true")
982 return HTML.tag('i', class_="icon-true")
983 else: # not true as bool
983 else: # not true as bool
984 return HTML.tag('i', class_="icon-false")
984 return HTML.tag('i', class_="icon-false")
985
985
986
986
987 #==============================================================================
987 #==============================================================================
988 # PERMS
988 # PERMS
989 #==============================================================================
989 #==============================================================================
990 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
990 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
991 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
991 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
992 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
992 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
993 csrf_token_key
993 csrf_token_key
994
994
995
995
996 #==============================================================================
996 #==============================================================================
997 # GRAVATAR URL
997 # GRAVATAR URL
998 #==============================================================================
998 #==============================================================================
999 class InitialsGravatar(object):
999 class InitialsGravatar(object):
1000 def __init__(self, email_address, first_name, last_name, size=30,
1000 def __init__(self, email_address, first_name, last_name, size=30,
1001 background=None, text_color='#fff'):
1001 background=None, text_color='#fff'):
1002 self.size = size
1002 self.size = size
1003 self.first_name = first_name
1003 self.first_name = first_name
1004 self.last_name = last_name
1004 self.last_name = last_name
1005 self.email_address = email_address
1005 self.email_address = email_address
1006 self.background = background or self.str2color(email_address)
1006 self.background = background or self.str2color(email_address)
1007 self.text_color = text_color
1007 self.text_color = text_color
1008
1008
1009 def get_color_bank(self):
1009 def get_color_bank(self):
1010 """
1010 """
1011 returns a predefined list of colors that gravatars can use.
1011 returns a predefined list of colors that gravatars can use.
1012 Those are randomized distinct colors that guarantee readability and
1012 Those are randomized distinct colors that guarantee readability and
1013 uniqueness.
1013 uniqueness.
1014
1014
1015 generated with: http://phrogz.net/css/distinct-colors.html
1015 generated with: http://phrogz.net/css/distinct-colors.html
1016 """
1016 """
1017 return [
1017 return [
1018 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1018 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1019 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1019 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1020 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1020 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1021 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1021 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1022 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1022 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1023 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1023 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1024 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1024 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1025 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1025 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1026 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1026 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1027 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1027 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1028 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1028 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1029 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1029 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1030 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1030 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1031 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1031 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1032 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1032 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1033 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1033 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1034 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1034 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1035 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1035 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1036 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1036 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1037 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1037 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1038 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1038 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1039 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1039 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1040 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1040 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1041 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1041 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1042 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1042 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1043 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1043 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1044 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1044 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1045 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1045 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1046 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1046 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1047 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1047 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1048 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1048 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1049 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1049 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1050 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1050 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1051 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1051 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1052 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1052 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1053 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1053 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1054 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1054 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1055 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1055 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1056 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1056 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1057 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1057 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1058 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1058 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1059 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1059 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1060 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1060 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1061 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1061 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1062 '#4f8c46', '#368dd9', '#5c0073'
1062 '#4f8c46', '#368dd9', '#5c0073'
1063 ]
1063 ]
1064
1064
1065 def rgb_to_hex_color(self, rgb_tuple):
1065 def rgb_to_hex_color(self, rgb_tuple):
1066 """
1066 """
1067 Converts an rgb_tuple passed to an hex color.
1067 Converts an rgb_tuple passed to an hex color.
1068
1068
1069 :param rgb_tuple: tuple with 3 ints represents rgb color space
1069 :param rgb_tuple: tuple with 3 ints represents rgb color space
1070 """
1070 """
1071 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1071 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1072
1072
1073 def email_to_int_list(self, email_str):
1073 def email_to_int_list(self, email_str):
1074 """
1074 """
1075 Get every byte of the hex digest value of email and turn it to integer.
1075 Get every byte of the hex digest value of email and turn it to integer.
1076 It's going to be always between 0-255
1076 It's going to be always between 0-255
1077 """
1077 """
1078 digest = md5_safe(email_str.lower())
1078 digest = md5_safe(email_str.lower())
1079 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1079 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1080
1080
1081 def pick_color_bank_index(self, email_str, color_bank):
1081 def pick_color_bank_index(self, email_str, color_bank):
1082 return self.email_to_int_list(email_str)[0] % len(color_bank)
1082 return self.email_to_int_list(email_str)[0] % len(color_bank)
1083
1083
1084 def str2color(self, email_str):
1084 def str2color(self, email_str):
1085 """
1085 """
1086 Tries to map in a stable algorithm an email to color
1086 Tries to map in a stable algorithm an email to color
1087
1087
1088 :param email_str:
1088 :param email_str:
1089 """
1089 """
1090 color_bank = self.get_color_bank()
1090 color_bank = self.get_color_bank()
1091 # pick position (module it's length so we always find it in the
1091 # pick position (module it's length so we always find it in the
1092 # bank even if it's smaller than 256 values
1092 # bank even if it's smaller than 256 values
1093 pos = self.pick_color_bank_index(email_str, color_bank)
1093 pos = self.pick_color_bank_index(email_str, color_bank)
1094 return color_bank[pos]
1094 return color_bank[pos]
1095
1095
1096 def normalize_email(self, email_address):
1096 def normalize_email(self, email_address):
1097 import unicodedata
1097 import unicodedata
1098 # default host used to fill in the fake/missing email
1098 # default host used to fill in the fake/missing email
1099 default_host = u'localhost'
1099 default_host = u'localhost'
1100
1100
1101 if not email_address:
1101 if not email_address:
1102 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1102 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1103
1103
1104 email_address = safe_unicode(email_address)
1104 email_address = safe_unicode(email_address)
1105
1105
1106 if u'@' not in email_address:
1106 if u'@' not in email_address:
1107 email_address = u'%s@%s' % (email_address, default_host)
1107 email_address = u'%s@%s' % (email_address, default_host)
1108
1108
1109 if email_address.endswith(u'@'):
1109 if email_address.endswith(u'@'):
1110 email_address = u'%s%s' % (email_address, default_host)
1110 email_address = u'%s%s' % (email_address, default_host)
1111
1111
1112 email_address = unicodedata.normalize('NFKD', email_address)\
1112 email_address = unicodedata.normalize('NFKD', email_address)\
1113 .encode('ascii', 'ignore')
1113 .encode('ascii', 'ignore')
1114 return email_address
1114 return email_address
1115
1115
1116 def get_initials(self):
1116 def get_initials(self):
1117 """
1117 """
1118 Returns 2 letter initials calculated based on the input.
1118 Returns 2 letter initials calculated based on the input.
1119 The algorithm picks first given email address, and takes first letter
1119 The algorithm picks first given email address, and takes first letter
1120 of part before @, and then the first letter of server name. In case
1120 of part before @, and then the first letter of server name. In case
1121 the part before @ is in a format of `somestring.somestring2` it replaces
1121 the part before @ is in a format of `somestring.somestring2` it replaces
1122 the server letter with first letter of somestring2
1122 the server letter with first letter of somestring2
1123
1123
1124 In case function was initialized with both first and lastname, this
1124 In case function was initialized with both first and lastname, this
1125 overrides the extraction from email by first letter of the first and
1125 overrides the extraction from email by first letter of the first and
1126 last name. We add special logic to that functionality, In case Full name
1126 last name. We add special logic to that functionality, In case Full name
1127 is compound, like Guido Von Rossum, we use last part of the last name
1127 is compound, like Guido Von Rossum, we use last part of the last name
1128 (Von Rossum) picking `R`.
1128 (Von Rossum) picking `R`.
1129
1129
1130 Function also normalizes the non-ascii characters to they ascii
1130 Function also normalizes the non-ascii characters to they ascii
1131 representation, eg Ą => A
1131 representation, eg Ą => A
1132 """
1132 """
1133 import unicodedata
1133 import unicodedata
1134 # replace non-ascii to ascii
1134 # replace non-ascii to ascii
1135 first_name = unicodedata.normalize(
1135 first_name = unicodedata.normalize(
1136 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1136 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1137 last_name = unicodedata.normalize(
1137 last_name = unicodedata.normalize(
1138 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1138 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1139
1139
1140 # do NFKD encoding, and also make sure email has proper format
1140 # do NFKD encoding, and also make sure email has proper format
1141 email_address = self.normalize_email(self.email_address)
1141 email_address = self.normalize_email(self.email_address)
1142
1142
1143 # first push the email initials
1143 # first push the email initials
1144 prefix, server = email_address.split('@', 1)
1144 prefix, server = email_address.split('@', 1)
1145
1145
1146 # check if prefix is maybe a 'firstname.lastname' syntax
1146 # check if prefix is maybe a 'firstname.lastname' syntax
1147 _dot_split = prefix.rsplit('.', 1)
1147 _dot_split = prefix.rsplit('.', 1)
1148 if len(_dot_split) == 2:
1148 if len(_dot_split) == 2:
1149 initials = [_dot_split[0][0], _dot_split[1][0]]
1149 initials = [_dot_split[0][0], _dot_split[1][0]]
1150 else:
1150 else:
1151 initials = [prefix[0], server[0]]
1151 initials = [prefix[0], server[0]]
1152
1152
1153 # then try to replace either firtname or lastname
1153 # then try to replace either firtname or lastname
1154 fn_letter = (first_name or " ")[0].strip()
1154 fn_letter = (first_name or " ")[0].strip()
1155 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1155 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1156
1156
1157 if fn_letter:
1157 if fn_letter:
1158 initials[0] = fn_letter
1158 initials[0] = fn_letter
1159
1159
1160 if ln_letter:
1160 if ln_letter:
1161 initials[1] = ln_letter
1161 initials[1] = ln_letter
1162
1162
1163 return ''.join(initials).upper()
1163 return ''.join(initials).upper()
1164
1164
1165 def get_img_data_by_type(self, font_family, img_type):
1165 def get_img_data_by_type(self, font_family, img_type):
1166 default_user = """
1166 default_user = """
1167 <svg xmlns="http://www.w3.org/2000/svg"
1167 <svg xmlns="http://www.w3.org/2000/svg"
1168 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1168 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1169 viewBox="-15 -10 439.165 429.164"
1169 viewBox="-15 -10 439.165 429.164"
1170
1170
1171 xml:space="preserve"
1171 xml:space="preserve"
1172 style="background:{background};" >
1172 style="background:{background};" >
1173
1173
1174 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1174 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1175 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1175 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1176 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1176 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1177 168.596,153.916,216.671,
1177 168.596,153.916,216.671,
1178 204.583,216.671z" fill="{text_color}"/>
1178 204.583,216.671z" fill="{text_color}"/>
1179 <path d="M407.164,374.717L360.88,
1179 <path d="M407.164,374.717L360.88,
1180 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1180 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1181 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1181 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1182 15.366-44.203,23.488-69.076,23.488c-24.877,
1182 15.366-44.203,23.488-69.076,23.488c-24.877,
1183 0-48.762-8.122-69.078-23.488
1183 0-48.762-8.122-69.078-23.488
1184 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1184 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1185 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1185 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1186 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1186 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1187 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1187 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1188 19.402-10.527 C409.699,390.129,
1188 19.402-10.527 C409.699,390.129,
1189 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1189 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1190 </svg>""".format(
1190 </svg>""".format(
1191 size=self.size,
1191 size=self.size,
1192 background='#979797', # @grey4
1192 background='#979797', # @grey4
1193 text_color=self.text_color,
1193 text_color=self.text_color,
1194 font_family=font_family)
1194 font_family=font_family)
1195
1195
1196 return {
1196 return {
1197 "default_user": default_user
1197 "default_user": default_user
1198 }[img_type]
1198 }[img_type]
1199
1199
1200 def get_img_data(self, svg_type=None):
1200 def get_img_data(self, svg_type=None):
1201 """
1201 """
1202 generates the svg metadata for image
1202 generates the svg metadata for image
1203 """
1203 """
1204
1204
1205 font_family = ','.join([
1205 font_family = ','.join([
1206 'proximanovaregular',
1206 'proximanovaregular',
1207 'Proxima Nova Regular',
1207 'Proxima Nova Regular',
1208 'Proxima Nova',
1208 'Proxima Nova',
1209 'Arial',
1209 'Arial',
1210 'Lucida Grande',
1210 'Lucida Grande',
1211 'sans-serif'
1211 'sans-serif'
1212 ])
1212 ])
1213 if svg_type:
1213 if svg_type:
1214 return self.get_img_data_by_type(font_family, svg_type)
1214 return self.get_img_data_by_type(font_family, svg_type)
1215
1215
1216 initials = self.get_initials()
1216 initials = self.get_initials()
1217 img_data = """
1217 img_data = """
1218 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1218 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1219 width="{size}" height="{size}"
1219 width="{size}" height="{size}"
1220 style="width: 100%; height: 100%; background-color: {background}"
1220 style="width: 100%; height: 100%; background-color: {background}"
1221 viewBox="0 0 {size} {size}">
1221 viewBox="0 0 {size} {size}">
1222 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1222 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1223 pointer-events="auto" fill="{text_color}"
1223 pointer-events="auto" fill="{text_color}"
1224 font-family="{font_family}"
1224 font-family="{font_family}"
1225 style="font-weight: 400; font-size: {f_size}px;">{text}
1225 style="font-weight: 400; font-size: {f_size}px;">{text}
1226 </text>
1226 </text>
1227 </svg>""".format(
1227 </svg>""".format(
1228 size=self.size,
1228 size=self.size,
1229 f_size=self.size/1.85, # scale the text inside the box nicely
1229 f_size=self.size/1.85, # scale the text inside the box nicely
1230 background=self.background,
1230 background=self.background,
1231 text_color=self.text_color,
1231 text_color=self.text_color,
1232 text=initials.upper(),
1232 text=initials.upper(),
1233 font_family=font_family)
1233 font_family=font_family)
1234
1234
1235 return img_data
1235 return img_data
1236
1236
1237 def generate_svg(self, svg_type=None):
1237 def generate_svg(self, svg_type=None):
1238 img_data = self.get_img_data(svg_type)
1238 img_data = self.get_img_data(svg_type)
1239 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1239 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1240
1240
1241
1241
1242 def initials_gravatar(email_address, first_name, last_name, size=30):
1242 def initials_gravatar(email_address, first_name, last_name, size=30):
1243 svg_type = None
1243 svg_type = None
1244 if email_address == User.DEFAULT_USER_EMAIL:
1244 if email_address == User.DEFAULT_USER_EMAIL:
1245 svg_type = 'default_user'
1245 svg_type = 'default_user'
1246 klass = InitialsGravatar(email_address, first_name, last_name, size)
1246 klass = InitialsGravatar(email_address, first_name, last_name, size)
1247 return klass.generate_svg(svg_type=svg_type)
1247 return klass.generate_svg(svg_type=svg_type)
1248
1248
1249
1249
1250 def gravatar_url(email_address, size=30):
1250 def gravatar_url(email_address, size=30):
1251 # doh, we need to re-import those to mock it later
1251 # doh, we need to re-import those to mock it later
1252 from pylons import tmpl_context as c
1252 from pylons import tmpl_context as c
1253
1253
1254 _use_gravatar = c.visual.use_gravatar
1254 _use_gravatar = c.visual.use_gravatar
1255 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1255 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1256
1256
1257 email_address = email_address or User.DEFAULT_USER_EMAIL
1257 email_address = email_address or User.DEFAULT_USER_EMAIL
1258 if isinstance(email_address, unicode):
1258 if isinstance(email_address, unicode):
1259 # hashlib crashes on unicode items
1259 # hashlib crashes on unicode items
1260 email_address = safe_str(email_address)
1260 email_address = safe_str(email_address)
1261
1261
1262 # empty email or default user
1262 # empty email or default user
1263 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1263 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1264 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1264 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1265
1265
1266 if _use_gravatar:
1266 if _use_gravatar:
1267 # TODO: Disuse pyramid thread locals. Think about another solution to
1267 # TODO: Disuse pyramid thread locals. Think about another solution to
1268 # get the host and schema here.
1268 # get the host and schema here.
1269 request = get_current_request()
1269 request = get_current_request()
1270 tmpl = safe_str(_gravatar_url)
1270 tmpl = safe_str(_gravatar_url)
1271 tmpl = tmpl.replace('{email}', email_address)\
1271 tmpl = tmpl.replace('{email}', email_address)\
1272 .replace('{md5email}', md5_safe(email_address.lower())) \
1272 .replace('{md5email}', md5_safe(email_address.lower())) \
1273 .replace('{netloc}', request.host)\
1273 .replace('{netloc}', request.host)\
1274 .replace('{scheme}', request.scheme)\
1274 .replace('{scheme}', request.scheme)\
1275 .replace('{size}', safe_str(size))
1275 .replace('{size}', safe_str(size))
1276 return tmpl
1276 return tmpl
1277 else:
1277 else:
1278 return initials_gravatar(email_address, '', '', size=size)
1278 return initials_gravatar(email_address, '', '', size=size)
1279
1279
1280
1280
1281 class Page(_Page):
1281 class Page(_Page):
1282 """
1282 """
1283 Custom pager to match rendering style with paginator
1283 Custom pager to match rendering style with paginator
1284 """
1284 """
1285
1285
1286 def _get_pos(self, cur_page, max_page, items):
1286 def _get_pos(self, cur_page, max_page, items):
1287 edge = (items / 2) + 1
1287 edge = (items / 2) + 1
1288 if (cur_page <= edge):
1288 if (cur_page <= edge):
1289 radius = max(items / 2, items - cur_page)
1289 radius = max(items / 2, items - cur_page)
1290 elif (max_page - cur_page) < edge:
1290 elif (max_page - cur_page) < edge:
1291 radius = (items - 1) - (max_page - cur_page)
1291 radius = (items - 1) - (max_page - cur_page)
1292 else:
1292 else:
1293 radius = items / 2
1293 radius = items / 2
1294
1294
1295 left = max(1, (cur_page - (radius)))
1295 left = max(1, (cur_page - (radius)))
1296 right = min(max_page, cur_page + (radius))
1296 right = min(max_page, cur_page + (radius))
1297 return left, cur_page, right
1297 return left, cur_page, right
1298
1298
1299 def _range(self, regexp_match):
1299 def _range(self, regexp_match):
1300 """
1300 """
1301 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1301 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1302
1302
1303 Arguments:
1303 Arguments:
1304
1304
1305 regexp_match
1305 regexp_match
1306 A "re" (regular expressions) match object containing the
1306 A "re" (regular expressions) match object containing the
1307 radius of linked pages around the current page in
1307 radius of linked pages around the current page in
1308 regexp_match.group(1) as a string
1308 regexp_match.group(1) as a string
1309
1309
1310 This function is supposed to be called as a callable in
1310 This function is supposed to be called as a callable in
1311 re.sub.
1311 re.sub.
1312
1312
1313 """
1313 """
1314 radius = int(regexp_match.group(1))
1314 radius = int(regexp_match.group(1))
1315
1315
1316 # Compute the first and last page number within the radius
1316 # Compute the first and last page number within the radius
1317 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1317 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1318 # -> leftmost_page = 5
1318 # -> leftmost_page = 5
1319 # -> rightmost_page = 9
1319 # -> rightmost_page = 9
1320 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1320 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1321 self.last_page,
1321 self.last_page,
1322 (radius * 2) + 1)
1322 (radius * 2) + 1)
1323 nav_items = []
1323 nav_items = []
1324
1324
1325 # Create a link to the first page (unless we are on the first page
1325 # Create a link to the first page (unless we are on the first page
1326 # or there would be no need to insert '..' spacers)
1326 # or there would be no need to insert '..' spacers)
1327 if self.page != self.first_page and self.first_page < leftmost_page:
1327 if self.page != self.first_page and self.first_page < leftmost_page:
1328 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1328 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1329
1329
1330 # Insert dots if there are pages between the first page
1330 # Insert dots if there are pages between the first page
1331 # and the currently displayed page range
1331 # and the currently displayed page range
1332 if leftmost_page - self.first_page > 1:
1332 if leftmost_page - self.first_page > 1:
1333 # Wrap in a SPAN tag if nolink_attr is set
1333 # Wrap in a SPAN tag if nolink_attr is set
1334 text = '..'
1334 text = '..'
1335 if self.dotdot_attr:
1335 if self.dotdot_attr:
1336 text = HTML.span(c=text, **self.dotdot_attr)
1336 text = HTML.span(c=text, **self.dotdot_attr)
1337 nav_items.append(text)
1337 nav_items.append(text)
1338
1338
1339 for thispage in xrange(leftmost_page, rightmost_page + 1):
1339 for thispage in xrange(leftmost_page, rightmost_page + 1):
1340 # Hilight the current page number and do not use a link
1340 # Hilight the current page number and do not use a link
1341 if thispage == self.page:
1341 if thispage == self.page:
1342 text = '%s' % (thispage,)
1342 text = '%s' % (thispage,)
1343 # Wrap in a SPAN tag if nolink_attr is set
1343 # Wrap in a SPAN tag if nolink_attr is set
1344 if self.curpage_attr:
1344 if self.curpage_attr:
1345 text = HTML.span(c=text, **self.curpage_attr)
1345 text = HTML.span(c=text, **self.curpage_attr)
1346 nav_items.append(text)
1346 nav_items.append(text)
1347 # Otherwise create just a link to that page
1347 # Otherwise create just a link to that page
1348 else:
1348 else:
1349 text = '%s' % (thispage,)
1349 text = '%s' % (thispage,)
1350 nav_items.append(self._pagerlink(thispage, text))
1350 nav_items.append(self._pagerlink(thispage, text))
1351
1351
1352 # Insert dots if there are pages between the displayed
1352 # Insert dots if there are pages between the displayed
1353 # page numbers and the end of the page range
1353 # page numbers and the end of the page range
1354 if self.last_page - rightmost_page > 1:
1354 if self.last_page - rightmost_page > 1:
1355 text = '..'
1355 text = '..'
1356 # Wrap in a SPAN tag if nolink_attr is set
1356 # Wrap in a SPAN tag if nolink_attr is set
1357 if self.dotdot_attr:
1357 if self.dotdot_attr:
1358 text = HTML.span(c=text, **self.dotdot_attr)
1358 text = HTML.span(c=text, **self.dotdot_attr)
1359 nav_items.append(text)
1359 nav_items.append(text)
1360
1360
1361 # Create a link to the very last page (unless we are on the last
1361 # Create a link to the very last page (unless we are on the last
1362 # page or there would be no need to insert '..' spacers)
1362 # page or there would be no need to insert '..' spacers)
1363 if self.page != self.last_page and rightmost_page < self.last_page:
1363 if self.page != self.last_page and rightmost_page < self.last_page:
1364 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1364 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1365
1365
1366 ## prerender links
1366 ## prerender links
1367 #_page_link = url.current()
1367 #_page_link = url.current()
1368 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1368 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1369 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1369 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1370 return self.separator.join(nav_items)
1370 return self.separator.join(nav_items)
1371
1371
1372 def pager(self, format='~2~', page_param='page', partial_param='partial',
1372 def pager(self, format='~2~', page_param='page', partial_param='partial',
1373 show_if_single_page=False, separator=' ', onclick=None,
1373 show_if_single_page=False, separator=' ', onclick=None,
1374 symbol_first='<<', symbol_last='>>',
1374 symbol_first='<<', symbol_last='>>',
1375 symbol_previous='<', symbol_next='>',
1375 symbol_previous='<', symbol_next='>',
1376 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1376 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1377 curpage_attr={'class': 'pager_curpage'},
1377 curpage_attr={'class': 'pager_curpage'},
1378 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1378 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1379
1379
1380 self.curpage_attr = curpage_attr
1380 self.curpage_attr = curpage_attr
1381 self.separator = separator
1381 self.separator = separator
1382 self.pager_kwargs = kwargs
1382 self.pager_kwargs = kwargs
1383 self.page_param = page_param
1383 self.page_param = page_param
1384 self.partial_param = partial_param
1384 self.partial_param = partial_param
1385 self.onclick = onclick
1385 self.onclick = onclick
1386 self.link_attr = link_attr
1386 self.link_attr = link_attr
1387 self.dotdot_attr = dotdot_attr
1387 self.dotdot_attr = dotdot_attr
1388
1388
1389 # Don't show navigator if there is no more than one page
1389 # Don't show navigator if there is no more than one page
1390 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1390 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1391 return ''
1391 return ''
1392
1392
1393 from string import Template
1393 from string import Template
1394 # Replace ~...~ in token format by range of pages
1394 # Replace ~...~ in token format by range of pages
1395 result = re.sub(r'~(\d+)~', self._range, format)
1395 result = re.sub(r'~(\d+)~', self._range, format)
1396
1396
1397 # Interpolate '%' variables
1397 # Interpolate '%' variables
1398 result = Template(result).safe_substitute({
1398 result = Template(result).safe_substitute({
1399 'first_page': self.first_page,
1399 'first_page': self.first_page,
1400 'last_page': self.last_page,
1400 'last_page': self.last_page,
1401 'page': self.page,
1401 'page': self.page,
1402 'page_count': self.page_count,
1402 'page_count': self.page_count,
1403 'items_per_page': self.items_per_page,
1403 'items_per_page': self.items_per_page,
1404 'first_item': self.first_item,
1404 'first_item': self.first_item,
1405 'last_item': self.last_item,
1405 'last_item': self.last_item,
1406 'item_count': self.item_count,
1406 'item_count': self.item_count,
1407 'link_first': self.page > self.first_page and \
1407 'link_first': self.page > self.first_page and \
1408 self._pagerlink(self.first_page, symbol_first) or '',
1408 self._pagerlink(self.first_page, symbol_first) or '',
1409 'link_last': self.page < self.last_page and \
1409 'link_last': self.page < self.last_page and \
1410 self._pagerlink(self.last_page, symbol_last) or '',
1410 self._pagerlink(self.last_page, symbol_last) or '',
1411 'link_previous': self.previous_page and \
1411 'link_previous': self.previous_page and \
1412 self._pagerlink(self.previous_page, symbol_previous) \
1412 self._pagerlink(self.previous_page, symbol_previous) \
1413 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1413 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1414 'link_next': self.next_page and \
1414 'link_next': self.next_page and \
1415 self._pagerlink(self.next_page, symbol_next) \
1415 self._pagerlink(self.next_page, symbol_next) \
1416 or HTML.span(symbol_next, class_="pg-next disabled")
1416 or HTML.span(symbol_next, class_="pg-next disabled")
1417 })
1417 })
1418
1418
1419 return literal(result)
1419 return literal(result)
1420
1420
1421
1421
1422 #==============================================================================
1422 #==============================================================================
1423 # REPO PAGER, PAGER FOR REPOSITORY
1423 # REPO PAGER, PAGER FOR REPOSITORY
1424 #==============================================================================
1424 #==============================================================================
1425 class RepoPage(Page):
1425 class RepoPage(Page):
1426
1426
1427 def __init__(self, collection, page=1, items_per_page=20,
1427 def __init__(self, collection, page=1, items_per_page=20,
1428 item_count=None, url=None, **kwargs):
1428 item_count=None, url=None, **kwargs):
1429
1429
1430 """Create a "RepoPage" instance. special pager for paging
1430 """Create a "RepoPage" instance. special pager for paging
1431 repository
1431 repository
1432 """
1432 """
1433 self._url_generator = url
1433 self._url_generator = url
1434
1434
1435 # Safe the kwargs class-wide so they can be used in the pager() method
1435 # Safe the kwargs class-wide so they can be used in the pager() method
1436 self.kwargs = kwargs
1436 self.kwargs = kwargs
1437
1437
1438 # Save a reference to the collection
1438 # Save a reference to the collection
1439 self.original_collection = collection
1439 self.original_collection = collection
1440
1440
1441 self.collection = collection
1441 self.collection = collection
1442
1442
1443 # The self.page is the number of the current page.
1443 # The self.page is the number of the current page.
1444 # The first page has the number 1!
1444 # The first page has the number 1!
1445 try:
1445 try:
1446 self.page = int(page) # make it int() if we get it as a string
1446 self.page = int(page) # make it int() if we get it as a string
1447 except (ValueError, TypeError):
1447 except (ValueError, TypeError):
1448 self.page = 1
1448 self.page = 1
1449
1449
1450 self.items_per_page = items_per_page
1450 self.items_per_page = items_per_page
1451
1451
1452 # Unless the user tells us how many items the collections has
1452 # Unless the user tells us how many items the collections has
1453 # we calculate that ourselves.
1453 # we calculate that ourselves.
1454 if item_count is not None:
1454 if item_count is not None:
1455 self.item_count = item_count
1455 self.item_count = item_count
1456 else:
1456 else:
1457 self.item_count = len(self.collection)
1457 self.item_count = len(self.collection)
1458
1458
1459 # Compute the number of the first and last available page
1459 # Compute the number of the first and last available page
1460 if self.item_count > 0:
1460 if self.item_count > 0:
1461 self.first_page = 1
1461 self.first_page = 1
1462 self.page_count = int(math.ceil(float(self.item_count) /
1462 self.page_count = int(math.ceil(float(self.item_count) /
1463 self.items_per_page))
1463 self.items_per_page))
1464 self.last_page = self.first_page + self.page_count - 1
1464 self.last_page = self.first_page + self.page_count - 1
1465
1465
1466 # Make sure that the requested page number is the range of
1466 # Make sure that the requested page number is the range of
1467 # valid pages
1467 # valid pages
1468 if self.page > self.last_page:
1468 if self.page > self.last_page:
1469 self.page = self.last_page
1469 self.page = self.last_page
1470 elif self.page < self.first_page:
1470 elif self.page < self.first_page:
1471 self.page = self.first_page
1471 self.page = self.first_page
1472
1472
1473 # Note: the number of items on this page can be less than
1473 # Note: the number of items on this page can be less than
1474 # items_per_page if the last page is not full
1474 # items_per_page if the last page is not full
1475 self.first_item = max(0, (self.item_count) - (self.page *
1475 self.first_item = max(0, (self.item_count) - (self.page *
1476 items_per_page))
1476 items_per_page))
1477 self.last_item = ((self.item_count - 1) - items_per_page *
1477 self.last_item = ((self.item_count - 1) - items_per_page *
1478 (self.page - 1))
1478 (self.page - 1))
1479
1479
1480 self.items = list(self.collection[self.first_item:self.last_item + 1])
1480 self.items = list(self.collection[self.first_item:self.last_item + 1])
1481
1481
1482 # Links to previous and next page
1482 # Links to previous and next page
1483 if self.page > self.first_page:
1483 if self.page > self.first_page:
1484 self.previous_page = self.page - 1
1484 self.previous_page = self.page - 1
1485 else:
1485 else:
1486 self.previous_page = None
1486 self.previous_page = None
1487
1487
1488 if self.page < self.last_page:
1488 if self.page < self.last_page:
1489 self.next_page = self.page + 1
1489 self.next_page = self.page + 1
1490 else:
1490 else:
1491 self.next_page = None
1491 self.next_page = None
1492
1492
1493 # No items available
1493 # No items available
1494 else:
1494 else:
1495 self.first_page = None
1495 self.first_page = None
1496 self.page_count = 0
1496 self.page_count = 0
1497 self.last_page = None
1497 self.last_page = None
1498 self.first_item = None
1498 self.first_item = None
1499 self.last_item = None
1499 self.last_item = None
1500 self.previous_page = None
1500 self.previous_page = None
1501 self.next_page = None
1501 self.next_page = None
1502 self.items = []
1502 self.items = []
1503
1503
1504 # This is a subclass of the 'list' type. Initialise the list now.
1504 # This is a subclass of the 'list' type. Initialise the list now.
1505 list.__init__(self, reversed(self.items))
1505 list.__init__(self, reversed(self.items))
1506
1506
1507
1507
1508 def changed_tooltip(nodes):
1508 def changed_tooltip(nodes):
1509 """
1509 """
1510 Generates a html string for changed nodes in commit page.
1510 Generates a html string for changed nodes in commit page.
1511 It limits the output to 30 entries
1511 It limits the output to 30 entries
1512
1512
1513 :param nodes: LazyNodesGenerator
1513 :param nodes: LazyNodesGenerator
1514 """
1514 """
1515 if nodes:
1515 if nodes:
1516 pref = ': <br/> '
1516 pref = ': <br/> '
1517 suf = ''
1517 suf = ''
1518 if len(nodes) > 30:
1518 if len(nodes) > 30:
1519 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1519 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1520 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1520 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1521 for x in nodes[:30]]) + suf)
1521 for x in nodes[:30]]) + suf)
1522 else:
1522 else:
1523 return ': ' + _('No Files')
1523 return ': ' + _('No Files')
1524
1524
1525
1525
1526 def breadcrumb_repo_link(repo):
1526 def breadcrumb_repo_link(repo):
1527 """
1527 """
1528 Makes a breadcrumbs path link to repo
1528 Makes a breadcrumbs path link to repo
1529
1529
1530 ex::
1530 ex::
1531 group >> subgroup >> repo
1531 group >> subgroup >> repo
1532
1532
1533 :param repo: a Repository instance
1533 :param repo: a Repository instance
1534 """
1534 """
1535
1535
1536 path = [
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 for group in repo.groups_with_parents
1538 for group in repo.groups_with_parents
1539 ] + [
1539 ] + [
1540 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1540 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1541 ]
1541 ]
1542
1542
1543 return literal(' &raquo; '.join(path))
1543 return literal(' &raquo; '.join(path))
1544
1544
1545
1545
1546 def format_byte_size_binary(file_size):
1546 def format_byte_size_binary(file_size):
1547 """
1547 """
1548 Formats file/folder sizes to standard.
1548 Formats file/folder sizes to standard.
1549 """
1549 """
1550 formatted_size = format_byte_size(file_size, binary=True)
1550 formatted_size = format_byte_size(file_size, binary=True)
1551 return formatted_size
1551 return formatted_size
1552
1552
1553
1553
1554 def urlify_text(text_, safe=True):
1554 def urlify_text(text_, safe=True):
1555 """
1555 """
1556 Extrac urls from text and make html links out of them
1556 Extrac urls from text and make html links out of them
1557
1557
1558 :param text_:
1558 :param text_:
1559 """
1559 """
1560
1560
1561 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1561 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1562 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1562 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1563
1563
1564 def url_func(match_obj):
1564 def url_func(match_obj):
1565 url_full = match_obj.groups()[0]
1565 url_full = match_obj.groups()[0]
1566 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1566 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1567 _newtext = url_pat.sub(url_func, text_)
1567 _newtext = url_pat.sub(url_func, text_)
1568 if safe:
1568 if safe:
1569 return literal(_newtext)
1569 return literal(_newtext)
1570 return _newtext
1570 return _newtext
1571
1571
1572
1572
1573 def urlify_commits(text_, repository):
1573 def urlify_commits(text_, repository):
1574 """
1574 """
1575 Extract commit ids from text and make link from them
1575 Extract commit ids from text and make link from them
1576
1576
1577 :param text_:
1577 :param text_:
1578 :param repository: repo name to build the URL with
1578 :param repository: repo name to build the URL with
1579 """
1579 """
1580 from pylons import url # doh, we need to re-import url to mock it later
1580 from pylons import url # doh, we need to re-import url to mock it later
1581 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1581 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1582
1582
1583 def url_func(match_obj):
1583 def url_func(match_obj):
1584 commit_id = match_obj.groups()[1]
1584 commit_id = match_obj.groups()[1]
1585 pref = match_obj.groups()[0]
1585 pref = match_obj.groups()[0]
1586 suf = match_obj.groups()[2]
1586 suf = match_obj.groups()[2]
1587
1587
1588 tmpl = (
1588 tmpl = (
1589 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1589 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1590 '%(commit_id)s</a>%(suf)s'
1590 '%(commit_id)s</a>%(suf)s'
1591 )
1591 )
1592 return tmpl % {
1592 return tmpl % {
1593 'pref': pref,
1593 'pref': pref,
1594 'cls': 'revision-link',
1594 'cls': 'revision-link',
1595 'url': url('changeset_home', repo_name=repository,
1595 'url': url('changeset_home', repo_name=repository,
1596 revision=commit_id, qualified=True),
1596 revision=commit_id, qualified=True),
1597 'commit_id': commit_id,
1597 'commit_id': commit_id,
1598 'suf': suf
1598 'suf': suf
1599 }
1599 }
1600
1600
1601 newtext = URL_PAT.sub(url_func, text_)
1601 newtext = URL_PAT.sub(url_func, text_)
1602
1602
1603 return newtext
1603 return newtext
1604
1604
1605
1605
1606 def _process_url_func(match_obj, repo_name, uid, entry,
1606 def _process_url_func(match_obj, repo_name, uid, entry,
1607 return_raw_data=False, link_format='html'):
1607 return_raw_data=False, link_format='html'):
1608 pref = ''
1608 pref = ''
1609 if match_obj.group().startswith(' '):
1609 if match_obj.group().startswith(' '):
1610 pref = ' '
1610 pref = ' '
1611
1611
1612 issue_id = ''.join(match_obj.groups())
1612 issue_id = ''.join(match_obj.groups())
1613
1613
1614 if link_format == 'html':
1614 if link_format == 'html':
1615 tmpl = (
1615 tmpl = (
1616 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1616 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1617 '%(issue-prefix)s%(id-repr)s'
1617 '%(issue-prefix)s%(id-repr)s'
1618 '</a>')
1618 '</a>')
1619 elif link_format == 'rst':
1619 elif link_format == 'rst':
1620 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1620 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1621 elif link_format == 'markdown':
1621 elif link_format == 'markdown':
1622 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1622 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1623 else:
1623 else:
1624 raise ValueError('Bad link_format:{}'.format(link_format))
1624 raise ValueError('Bad link_format:{}'.format(link_format))
1625
1625
1626 (repo_name_cleaned,
1626 (repo_name_cleaned,
1627 parent_group_name) = RepoGroupModel().\
1627 parent_group_name) = RepoGroupModel().\
1628 _get_group_name_and_parent(repo_name)
1628 _get_group_name_and_parent(repo_name)
1629
1629
1630 # variables replacement
1630 # variables replacement
1631 named_vars = {
1631 named_vars = {
1632 'id': issue_id,
1632 'id': issue_id,
1633 'repo': repo_name,
1633 'repo': repo_name,
1634 'repo_name': repo_name_cleaned,
1634 'repo_name': repo_name_cleaned,
1635 'group_name': parent_group_name
1635 'group_name': parent_group_name
1636 }
1636 }
1637 # named regex variables
1637 # named regex variables
1638 named_vars.update(match_obj.groupdict())
1638 named_vars.update(match_obj.groupdict())
1639 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1639 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1640
1640
1641 data = {
1641 data = {
1642 'pref': pref,
1642 'pref': pref,
1643 'cls': 'issue-tracker-link',
1643 'cls': 'issue-tracker-link',
1644 'url': _url,
1644 'url': _url,
1645 'id-repr': issue_id,
1645 'id-repr': issue_id,
1646 'issue-prefix': entry['pref'],
1646 'issue-prefix': entry['pref'],
1647 'serv': entry['url'],
1647 'serv': entry['url'],
1648 }
1648 }
1649 if return_raw_data:
1649 if return_raw_data:
1650 return {
1650 return {
1651 'id': issue_id,
1651 'id': issue_id,
1652 'url': _url
1652 'url': _url
1653 }
1653 }
1654 return tmpl % data
1654 return tmpl % data
1655
1655
1656
1656
1657 def process_patterns(text_string, repo_name, link_format='html'):
1657 def process_patterns(text_string, repo_name, link_format='html'):
1658 allowed_formats = ['html', 'rst', 'markdown']
1658 allowed_formats = ['html', 'rst', 'markdown']
1659 if link_format not in allowed_formats:
1659 if link_format not in allowed_formats:
1660 raise ValueError('Link format can be only one of:{} got {}'.format(
1660 raise ValueError('Link format can be only one of:{} got {}'.format(
1661 allowed_formats, link_format))
1661 allowed_formats, link_format))
1662
1662
1663 repo = None
1663 repo = None
1664 if repo_name:
1664 if repo_name:
1665 # Retrieving repo_name to avoid invalid repo_name to explode on
1665 # Retrieving repo_name to avoid invalid repo_name to explode on
1666 # IssueTrackerSettingsModel but still passing invalid name further down
1666 # IssueTrackerSettingsModel but still passing invalid name further down
1667 repo = Repository.get_by_repo_name(repo_name, cache=True)
1667 repo = Repository.get_by_repo_name(repo_name, cache=True)
1668
1668
1669 settings_model = IssueTrackerSettingsModel(repo=repo)
1669 settings_model = IssueTrackerSettingsModel(repo=repo)
1670 active_entries = settings_model.get_settings(cache=True)
1670 active_entries = settings_model.get_settings(cache=True)
1671
1671
1672 issues_data = []
1672 issues_data = []
1673 newtext = text_string
1673 newtext = text_string
1674
1674
1675 for uid, entry in active_entries.items():
1675 for uid, entry in active_entries.items():
1676 log.debug('found issue tracker entry with uid %s' % (uid,))
1676 log.debug('found issue tracker entry with uid %s' % (uid,))
1677
1677
1678 if not (entry['pat'] and entry['url']):
1678 if not (entry['pat'] and entry['url']):
1679 log.debug('skipping due to missing data')
1679 log.debug('skipping due to missing data')
1680 continue
1680 continue
1681
1681
1682 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1682 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1683 % (uid, entry['pat'], entry['url'], entry['pref']))
1683 % (uid, entry['pat'], entry['url'], entry['pref']))
1684
1684
1685 try:
1685 try:
1686 pattern = re.compile(r'%s' % entry['pat'])
1686 pattern = re.compile(r'%s' % entry['pat'])
1687 except re.error:
1687 except re.error:
1688 log.exception(
1688 log.exception(
1689 'issue tracker pattern: `%s` failed to compile',
1689 'issue tracker pattern: `%s` failed to compile',
1690 entry['pat'])
1690 entry['pat'])
1691 continue
1691 continue
1692
1692
1693 data_func = partial(
1693 data_func = partial(
1694 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1694 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1695 return_raw_data=True)
1695 return_raw_data=True)
1696
1696
1697 for match_obj in pattern.finditer(text_string):
1697 for match_obj in pattern.finditer(text_string):
1698 issues_data.append(data_func(match_obj))
1698 issues_data.append(data_func(match_obj))
1699
1699
1700 url_func = partial(
1700 url_func = partial(
1701 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1701 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1702 link_format=link_format)
1702 link_format=link_format)
1703
1703
1704 newtext = pattern.sub(url_func, newtext)
1704 newtext = pattern.sub(url_func, newtext)
1705 log.debug('processed prefix:uid `%s`' % (uid,))
1705 log.debug('processed prefix:uid `%s`' % (uid,))
1706
1706
1707 return newtext, issues_data
1707 return newtext, issues_data
1708
1708
1709
1709
1710 def urlify_commit_message(commit_text, repository=None):
1710 def urlify_commit_message(commit_text, repository=None):
1711 """
1711 """
1712 Parses given text message and makes proper links.
1712 Parses given text message and makes proper links.
1713 issues are linked to given issue-server, and rest is a commit link
1713 issues are linked to given issue-server, and rest is a commit link
1714
1714
1715 :param commit_text:
1715 :param commit_text:
1716 :param repository:
1716 :param repository:
1717 """
1717 """
1718 from pylons import url # doh, we need to re-import url to mock it later
1718 from pylons import url # doh, we need to re-import url to mock it later
1719
1719
1720 def escaper(string):
1720 def escaper(string):
1721 return string.replace('<', '&lt;').replace('>', '&gt;')
1721 return string.replace('<', '&lt;').replace('>', '&gt;')
1722
1722
1723 newtext = escaper(commit_text)
1723 newtext = escaper(commit_text)
1724
1724
1725 # extract http/https links and make them real urls
1725 # extract http/https links and make them real urls
1726 newtext = urlify_text(newtext, safe=False)
1726 newtext = urlify_text(newtext, safe=False)
1727
1727
1728 # urlify commits - extract commit ids and make link out of them, if we have
1728 # urlify commits - extract commit ids and make link out of them, if we have
1729 # the scope of repository present.
1729 # the scope of repository present.
1730 if repository:
1730 if repository:
1731 newtext = urlify_commits(newtext, repository)
1731 newtext = urlify_commits(newtext, repository)
1732
1732
1733 # process issue tracker patterns
1733 # process issue tracker patterns
1734 newtext, issues = process_patterns(newtext, repository or '')
1734 newtext, issues = process_patterns(newtext, repository or '')
1735
1735
1736 return literal(newtext)
1736 return literal(newtext)
1737
1737
1738
1738
1739 def render_binary(repo_name, file_obj):
1739 def render_binary(repo_name, file_obj):
1740 """
1740 """
1741 Choose how to render a binary file
1741 Choose how to render a binary file
1742 """
1742 """
1743 filename = file_obj.name
1743 filename = file_obj.name
1744
1744
1745 # images
1745 # images
1746 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1746 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1747 if fnmatch.fnmatch(filename, pat=ext):
1747 if fnmatch.fnmatch(filename, pat=ext):
1748 alt = filename
1748 alt = filename
1749 src = url('files_raw_home', repo_name=repo_name,
1749 src = url('files_raw_home', repo_name=repo_name,
1750 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1750 revision=file_obj.commit.raw_id, f_path=file_obj.path)
1751 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1751 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1752
1752
1753
1753
1754 def renderer_from_filename(filename, exclude=None):
1754 def renderer_from_filename(filename, exclude=None):
1755 """
1755 """
1756 choose a renderer based on filename, this works only for text based files
1756 choose a renderer based on filename, this works only for text based files
1757 """
1757 """
1758
1758
1759 # ipython
1759 # ipython
1760 for ext in ['*.ipynb']:
1760 for ext in ['*.ipynb']:
1761 if fnmatch.fnmatch(filename, pat=ext):
1761 if fnmatch.fnmatch(filename, pat=ext):
1762 return 'jupyter'
1762 return 'jupyter'
1763
1763
1764 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1764 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1765 if is_markup:
1765 if is_markup:
1766 return is_markup
1766 return is_markup
1767 return None
1767 return None
1768
1768
1769
1769
1770 def render(source, renderer='rst', mentions=False, relative_url=None,
1770 def render(source, renderer='rst', mentions=False, relative_url=None,
1771 repo_name=None):
1771 repo_name=None):
1772
1772
1773 def maybe_convert_relative_links(html_source):
1773 def maybe_convert_relative_links(html_source):
1774 if relative_url:
1774 if relative_url:
1775 return relative_links(html_source, relative_url)
1775 return relative_links(html_source, relative_url)
1776 return html_source
1776 return html_source
1777
1777
1778 if renderer == 'rst':
1778 if renderer == 'rst':
1779 if repo_name:
1779 if repo_name:
1780 # process patterns on comments if we pass in repo name
1780 # process patterns on comments if we pass in repo name
1781 source, issues = process_patterns(
1781 source, issues = process_patterns(
1782 source, repo_name, link_format='rst')
1782 source, repo_name, link_format='rst')
1783
1783
1784 return literal(
1784 return literal(
1785 '<div class="rst-block">%s</div>' %
1785 '<div class="rst-block">%s</div>' %
1786 maybe_convert_relative_links(
1786 maybe_convert_relative_links(
1787 MarkupRenderer.rst(source, mentions=mentions)))
1787 MarkupRenderer.rst(source, mentions=mentions)))
1788 elif renderer == 'markdown':
1788 elif renderer == 'markdown':
1789 if repo_name:
1789 if repo_name:
1790 # process patterns on comments if we pass in repo name
1790 # process patterns on comments if we pass in repo name
1791 source, issues = process_patterns(
1791 source, issues = process_patterns(
1792 source, repo_name, link_format='markdown')
1792 source, repo_name, link_format='markdown')
1793
1793
1794 return literal(
1794 return literal(
1795 '<div class="markdown-block">%s</div>' %
1795 '<div class="markdown-block">%s</div>' %
1796 maybe_convert_relative_links(
1796 maybe_convert_relative_links(
1797 MarkupRenderer.markdown(source, flavored=True,
1797 MarkupRenderer.markdown(source, flavored=True,
1798 mentions=mentions)))
1798 mentions=mentions)))
1799 elif renderer == 'jupyter':
1799 elif renderer == 'jupyter':
1800 return literal(
1800 return literal(
1801 '<div class="ipynb">%s</div>' %
1801 '<div class="ipynb">%s</div>' %
1802 maybe_convert_relative_links(
1802 maybe_convert_relative_links(
1803 MarkupRenderer.jupyter(source)))
1803 MarkupRenderer.jupyter(source)))
1804
1804
1805 # None means just show the file-source
1805 # None means just show the file-source
1806 return None
1806 return None
1807
1807
1808
1808
1809 def commit_status(repo, commit_id):
1809 def commit_status(repo, commit_id):
1810 return ChangesetStatusModel().get_status(repo, commit_id)
1810 return ChangesetStatusModel().get_status(repo, commit_id)
1811
1811
1812
1812
1813 def commit_status_lbl(commit_status):
1813 def commit_status_lbl(commit_status):
1814 return dict(ChangesetStatus.STATUSES).get(commit_status)
1814 return dict(ChangesetStatus.STATUSES).get(commit_status)
1815
1815
1816
1816
1817 def commit_time(repo_name, commit_id):
1817 def commit_time(repo_name, commit_id):
1818 repo = Repository.get_by_repo_name(repo_name)
1818 repo = Repository.get_by_repo_name(repo_name)
1819 commit = repo.get_commit(commit_id=commit_id)
1819 commit = repo.get_commit(commit_id=commit_id)
1820 return commit.date
1820 return commit.date
1821
1821
1822
1822
1823 def get_permission_name(key):
1823 def get_permission_name(key):
1824 return dict(Permission.PERMS).get(key)
1824 return dict(Permission.PERMS).get(key)
1825
1825
1826
1826
1827 def journal_filter_help():
1827 def journal_filter_help():
1828 return _(
1828 return _(
1829 'Example filter terms:\n' +
1829 'Example filter terms:\n' +
1830 ' repository:vcs\n' +
1830 ' repository:vcs\n' +
1831 ' username:marcin\n' +
1831 ' username:marcin\n' +
1832 ' action:*push*\n' +
1832 ' action:*push*\n' +
1833 ' ip:127.0.0.1\n' +
1833 ' ip:127.0.0.1\n' +
1834 ' date:20120101\n' +
1834 ' date:20120101\n' +
1835 ' date:[20120101100000 TO 20120102]\n' +
1835 ' date:[20120101100000 TO 20120102]\n' +
1836 '\n' +
1836 '\n' +
1837 'Generate wildcards using \'*\' character:\n' +
1837 'Generate wildcards using \'*\' character:\n' +
1838 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1838 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1839 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1839 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1840 '\n' +
1840 '\n' +
1841 'Optional AND / OR operators in queries\n' +
1841 'Optional AND / OR operators in queries\n' +
1842 ' "repository:vcs OR repository:test"\n' +
1842 ' "repository:vcs OR repository:test"\n' +
1843 ' "username:test AND repository:test*"\n'
1843 ' "username:test AND repository:test*"\n'
1844 )
1844 )
1845
1845
1846
1846
1847 def search_filter_help(searcher):
1847 def search_filter_help(searcher):
1848
1848
1849 terms = ''
1849 terms = ''
1850 return _(
1850 return _(
1851 'Example filter terms for `{searcher}` search:\n' +
1851 'Example filter terms for `{searcher}` search:\n' +
1852 '{terms}\n' +
1852 '{terms}\n' +
1853 'Generate wildcards using \'*\' character:\n' +
1853 'Generate wildcards using \'*\' character:\n' +
1854 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1854 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1855 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1855 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1856 '\n' +
1856 '\n' +
1857 'Optional AND / OR operators in queries\n' +
1857 'Optional AND / OR operators in queries\n' +
1858 ' "repo_name:vcs OR repo_name:test"\n' +
1858 ' "repo_name:vcs OR repo_name:test"\n' +
1859 ' "owner:test AND repo_name:test*"\n' +
1859 ' "owner:test AND repo_name:test*"\n' +
1860 'More: {search_doc}'
1860 'More: {search_doc}'
1861 ).format(searcher=searcher.name,
1861 ).format(searcher=searcher.name,
1862 terms=terms, search_doc=searcher.query_lang_doc)
1862 terms=terms, search_doc=searcher.query_lang_doc)
1863
1863
1864
1864
1865 def not_mapped_error(repo_name):
1865 def not_mapped_error(repo_name):
1866 flash(_('%s repository is not mapped to db perhaps'
1866 flash(_('%s repository is not mapped to db perhaps'
1867 ' it was created or renamed from the filesystem'
1867 ' it was created or renamed from the filesystem'
1868 ' please run the application again'
1868 ' please run the application again'
1869 ' in order to rescan repositories') % repo_name, category='error')
1869 ' in order to rescan repositories') % repo_name, category='error')
1870
1870
1871
1871
1872 def ip_range(ip_addr):
1872 def ip_range(ip_addr):
1873 from rhodecode.model.db import UserIpMap
1873 from rhodecode.model.db import UserIpMap
1874 s, e = UserIpMap._get_ip_range(ip_addr)
1874 s, e = UserIpMap._get_ip_range(ip_addr)
1875 return '%s - %s' % (s, e)
1875 return '%s - %s' % (s, e)
1876
1876
1877
1877
1878 def form(url, method='post', needs_csrf_token=True, **attrs):
1878 def form(url, method='post', needs_csrf_token=True, **attrs):
1879 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1879 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1880 if method.lower() != 'get' and needs_csrf_token:
1880 if method.lower() != 'get' and needs_csrf_token:
1881 raise Exception(
1881 raise Exception(
1882 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1882 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1883 'CSRF token. If the endpoint does not require such token you can ' +
1883 'CSRF token. If the endpoint does not require such token you can ' +
1884 'explicitly set the parameter needs_csrf_token to false.')
1884 'explicitly set the parameter needs_csrf_token to false.')
1885
1885
1886 return wh_form(url, method=method, **attrs)
1886 return wh_form(url, method=method, **attrs)
1887
1887
1888
1888
1889 def secure_form(url, method="POST", multipart=False, **attrs):
1889 def secure_form(url, method="POST", multipart=False, **attrs):
1890 """Start a form tag that points the action to an url. This
1890 """Start a form tag that points the action to an url. This
1891 form tag will also include the hidden field containing
1891 form tag will also include the hidden field containing
1892 the auth token.
1892 the auth token.
1893
1893
1894 The url options should be given either as a string, or as a
1894 The url options should be given either as a string, or as a
1895 ``url()`` function. The method for the form defaults to POST.
1895 ``url()`` function. The method for the form defaults to POST.
1896
1896
1897 Options:
1897 Options:
1898
1898
1899 ``multipart``
1899 ``multipart``
1900 If set to True, the enctype is set to "multipart/form-data".
1900 If set to True, the enctype is set to "multipart/form-data".
1901 ``method``
1901 ``method``
1902 The method to use when submitting the form, usually either
1902 The method to use when submitting the form, usually either
1903 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1903 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1904 hidden input with name _method is added to simulate the verb
1904 hidden input with name _method is added to simulate the verb
1905 over POST.
1905 over POST.
1906
1906
1907 """
1907 """
1908 from webhelpers.pylonslib.secure_form import insecure_form
1908 from webhelpers.pylonslib.secure_form import insecure_form
1909 form = insecure_form(url, method, multipart, **attrs)
1909 form = insecure_form(url, method, multipart, **attrs)
1910 token = csrf_input()
1910 token = csrf_input()
1911 return literal("%s\n%s" % (form, token))
1911 return literal("%s\n%s" % (form, token))
1912
1912
1913 def csrf_input():
1913 def csrf_input():
1914 return literal(
1914 return literal(
1915 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1915 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1916 csrf_token_key, csrf_token_key, get_csrf_token()))
1916 csrf_token_key, csrf_token_key, get_csrf_token()))
1917
1917
1918 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1918 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1919 select_html = select(name, selected, options, **attrs)
1919 select_html = select(name, selected, options, **attrs)
1920 select2 = """
1920 select2 = """
1921 <script>
1921 <script>
1922 $(document).ready(function() {
1922 $(document).ready(function() {
1923 $('#%s').select2({
1923 $('#%s').select2({
1924 containerCssClass: 'drop-menu',
1924 containerCssClass: 'drop-menu',
1925 dropdownCssClass: 'drop-menu-dropdown',
1925 dropdownCssClass: 'drop-menu-dropdown',
1926 dropdownAutoWidth: true%s
1926 dropdownAutoWidth: true%s
1927 });
1927 });
1928 });
1928 });
1929 </script>
1929 </script>
1930 """
1930 """
1931 filter_option = """,
1931 filter_option = """,
1932 minimumResultsForSearch: -1
1932 minimumResultsForSearch: -1
1933 """
1933 """
1934 input_id = attrs.get('id') or name
1934 input_id = attrs.get('id') or name
1935 filter_enabled = "" if enable_filter else filter_option
1935 filter_enabled = "" if enable_filter else filter_option
1936 select_script = literal(select2 % (input_id, filter_enabled))
1936 select_script = literal(select2 % (input_id, filter_enabled))
1937
1937
1938 return literal(select_html+select_script)
1938 return literal(select_html+select_script)
1939
1939
1940
1940
1941 def get_visual_attr(tmpl_context_var, attr_name):
1941 def get_visual_attr(tmpl_context_var, attr_name):
1942 """
1942 """
1943 A safe way to get a variable from visual variable of template context
1943 A safe way to get a variable from visual variable of template context
1944
1944
1945 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1945 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1946 :param attr_name: name of the attribute we fetch from the c.visual
1946 :param attr_name: name of the attribute we fetch from the c.visual
1947 """
1947 """
1948 visual = getattr(tmpl_context_var, 'visual', None)
1948 visual = getattr(tmpl_context_var, 'visual', None)
1949 if not visual:
1949 if not visual:
1950 return
1950 return
1951 else:
1951 else:
1952 return getattr(visual, attr_name, None)
1952 return getattr(visual, attr_name, None)
1953
1953
1954
1954
1955 def get_last_path_part(file_node):
1955 def get_last_path_part(file_node):
1956 if not file_node.path:
1956 if not file_node.path:
1957 return u''
1957 return u''
1958
1958
1959 path = safe_unicode(file_node.path.split('/')[-1])
1959 path = safe_unicode(file_node.path.split('/')[-1])
1960 return u'../' + path
1960 return u'../' + path
1961
1961
1962
1962
1963 def route_url(*args, **kwds):
1963 def route_url(*args, **kwargs):
1964 """
1964 """
1965 Wrapper around pyramids `route_url` (fully qualified url) function.
1965 Wrapper around pyramids `route_url` (fully qualified url) function.
1966 It is used to generate URLs from within pylons views or templates.
1966 It is used to generate URLs from within pylons views or templates.
1967 This will be removed when pyramid migration if finished.
1967 This will be removed when pyramid migration if finished.
1968 """
1968 """
1969 req = get_current_request()
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 Wrapper around pyramids `route_path` function. It is used to generate
1975 Wrapper around pyramids `route_path` function. It is used to generate
1976 URLs from within pylons views or templates. This will be removed when
1976 URLs from within pylons views or templates. This will be removed when
1977 pyramid migration if finished.
1977 pyramid migration if finished.
1978 """
1978 """
1979 req = get_current_request()
1979 req = get_current_request()
1980 return req.route_path(*args, **kwds)
1980 return req.route_path(*args, **kwargs)
1981
1981
1982
1982
1983 def route_path_or_none(*args, **kwargs):
1983 def route_path_or_none(*args, **kwargs):
1984 try:
1984 try:
1985 return route_path(*args, **kwargs)
1985 return route_path(*args, **kwargs)
1986 except KeyError:
1986 except KeyError:
1987 return None
1987 return None
1988
1988
1989
1989
1990 def static_url(*args, **kwds):
1990 def static_url(*args, **kwds):
1991 """
1991 """
1992 Wrapper around pyramids `route_path` function. It is used to generate
1992 Wrapper around pyramids `route_path` function. It is used to generate
1993 URLs from within pylons views or templates. This will be removed when
1993 URLs from within pylons views or templates. This will be removed when
1994 pyramid migration if finished.
1994 pyramid migration if finished.
1995 """
1995 """
1996 req = get_current_request()
1996 req = get_current_request()
1997 return req.static_url(*args, **kwds)
1997 return req.static_url(*args, **kwds)
1998
1998
1999
1999
2000 def resource_path(*args, **kwds):
2000 def resource_path(*args, **kwds):
2001 """
2001 """
2002 Wrapper around pyramids `route_path` function. It is used to generate
2002 Wrapper around pyramids `route_path` function. It is used to generate
2003 URLs from within pylons views or templates. This will be removed when
2003 URLs from within pylons views or templates. This will be removed when
2004 pyramid migration if finished.
2004 pyramid migration if finished.
2005 """
2005 """
2006 req = get_current_request()
2006 req = get_current_request()
2007 return req.resource_path(*args, **kwds)
2007 return req.resource_path(*args, **kwds)
2008
2008
2009
2009
2010 def api_call_example(method, args):
2010 def api_call_example(method, args):
2011 """
2011 """
2012 Generates an API call example via CURL
2012 Generates an API call example via CURL
2013 """
2013 """
2014 args_json = json.dumps(OrderedDict([
2014 args_json = json.dumps(OrderedDict([
2015 ('id', 1),
2015 ('id', 1),
2016 ('auth_token', 'SECRET'),
2016 ('auth_token', 'SECRET'),
2017 ('method', method),
2017 ('method', method),
2018 ('args', args)
2018 ('args', args)
2019 ]))
2019 ]))
2020 return literal(
2020 return literal(
2021 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2021 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2022 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2022 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2023 "and needs to be of `api calls` role."
2023 "and needs to be of `api calls` role."
2024 .format(
2024 .format(
2025 api_url=route_url('apiv2'),
2025 api_url=route_url('apiv2'),
2026 token_url=route_url('my_account_auth_tokens'),
2026 token_url=route_url('my_account_auth_tokens'),
2027 data=args_json))
2027 data=args_json))
@@ -1,1043 +1,1044 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Utilities library for RhodeCode
22 Utilities library for RhodeCode
23 """
23 """
24
24
25 import datetime
25 import datetime
26 import decorator
26 import decorator
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30 import re
30 import re
31 import shutil
31 import shutil
32 import tempfile
32 import tempfile
33 import traceback
33 import traceback
34 import tarfile
34 import tarfile
35 import warnings
35 import warnings
36 import hashlib
36 import hashlib
37 from os.path import join as jn
37 from os.path import join as jn
38
38
39 import paste
39 import paste
40 import pkg_resources
40 import pkg_resources
41 from paste.script.command import Command, BadCommand
41 from paste.script.command import Command, BadCommand
42 from webhelpers.text import collapse, remove_formatting, strip_tags
42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 from mako import exceptions
43 from mako import exceptions
44 from pyramid.threadlocal import get_current_registry
44 from pyramid.threadlocal import get_current_registry
45 from pyramid.request import Request
45 from pyramid.request import Request
46
46
47 from rhodecode.lib.fakemod import create_module
47 from rhodecode.lib.fakemod import create_module
48 from rhodecode.lib.vcs.backends.base import Config
48 from rhodecode.lib.vcs.backends.base import Config
49 from rhodecode.lib.vcs.exceptions import VCSError
49 from rhodecode.lib.vcs.exceptions import VCSError
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 from rhodecode.lib.utils2 import (
51 from rhodecode.lib.utils2 import (
52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 from rhodecode.model import meta
53 from rhodecode.model import meta
54 from rhodecode.model.db import (
54 from rhodecode.model.db import (
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62
62
63 # String which contains characters that are not allowed in slug names for
63 # String which contains characters that are not allowed in slug names for
64 # repositories or repository groups. It is properly escaped to use it in
64 # repositories or repository groups. It is properly escaped to use it in
65 # regular expressions.
65 # regular expressions.
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67
67
68 # Regex that matches forbidden characters in repo/group slugs.
68 # Regex that matches forbidden characters in repo/group slugs.
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70
70
71 # Regex that matches allowed characters in repo/group slugs.
71 # Regex that matches allowed characters in repo/group slugs.
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73
73
74 # Regex that matches whole repo/group slugs.
74 # Regex that matches whole repo/group slugs.
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76
76
77 _license_cache = None
77 _license_cache = None
78
78
79
79
80 def repo_name_slug(value):
80 def repo_name_slug(value):
81 """
81 """
82 Return slug of name of repository
82 Return slug of name of repository
83 This function is called on each creation/modification
83 This function is called on each creation/modification
84 of repository to prevent bad names in repo
84 of repository to prevent bad names in repo
85 """
85 """
86 replacement_char = '-'
86 replacement_char = '-'
87
87
88 slug = remove_formatting(value)
88 slug = remove_formatting(value)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 slug = re.sub('[\s]+', '-', slug)
90 slug = re.sub('[\s]+', '-', slug)
91 slug = collapse(slug, replacement_char)
91 slug = collapse(slug, replacement_char)
92 return slug
92 return slug
93
93
94
94
95 #==============================================================================
95 #==============================================================================
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 #==============================================================================
97 #==============================================================================
98 def get_repo_slug(request):
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 # pyramid
100 # pyramid
101 _repo = request.matchdict.get('repo_name')
101 _repo = request.db_repo.repo_name
102 else:
102 else:
103 # TODO(marcink): remove after pylons migration...
103 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
104
105
105 if _repo:
106 if _repo:
106 _repo = _repo.rstrip('/')
107 _repo = _repo.rstrip('/')
107 return _repo
108 return _repo
108
109
109
110
110 def get_repo_group_slug(request):
111 def get_repo_group_slug(request):
111 if isinstance(request, Request) and getattr(request, 'matchdict', None):
112 if isinstance(request, Request) and getattr(request, 'matchdict', None):
112 # pyramid
113 # pyramid
113 _group = request.matchdict.get('group_name')
114 _group = request.matchdict.get('repo_group_name')
114 else:
115 else:
115 _group = request.environ['pylons.routes_dict'].get('group_name')
116 _group = request.environ['pylons.routes_dict'].get('group_name')
116
117
117 if _group:
118 if _group:
118 _group = _group.rstrip('/')
119 _group = _group.rstrip('/')
119 return _group
120 return _group
120
121
121
122
122 def get_user_group_slug(request):
123 def get_user_group_slug(request):
123 if isinstance(request, Request) and getattr(request, 'matchdict', None):
124 if isinstance(request, Request) and getattr(request, 'matchdict', None):
124 # pyramid
125 # pyramid
125 _group = request.matchdict.get('user_group_id')
126 _group = request.matchdict.get('user_group_id')
126 else:
127 else:
127 _group = request.environ['pylons.routes_dict'].get('user_group_id')
128 _group = request.environ['pylons.routes_dict'].get('user_group_id')
128
129
129 try:
130 try:
130 _group = UserGroup.get(_group)
131 _group = UserGroup.get(_group)
131 if _group:
132 if _group:
132 _group = _group.users_group_name
133 _group = _group.users_group_name
133 except Exception:
134 except Exception:
134 log.debug(traceback.format_exc())
135 log.debug(traceback.format_exc())
135 # catch all failures here
136 # catch all failures here
136 pass
137 pass
137
138
138 return _group
139 return _group
139
140
140
141
141 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
142 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
142 """
143 """
143 Action logger for various actions made by users
144 Action logger for various actions made by users
144
145
145 :param user: user that made this action, can be a unique username string or
146 :param user: user that made this action, can be a unique username string or
146 object containing user_id attribute
147 object containing user_id attribute
147 :param action: action to log, should be on of predefined unique actions for
148 :param action: action to log, should be on of predefined unique actions for
148 easy translations
149 easy translations
149 :param repo: string name of repository or object containing repo_id,
150 :param repo: string name of repository or object containing repo_id,
150 that action was made on
151 that action was made on
151 :param ipaddr: optional ip address from what the action was made
152 :param ipaddr: optional ip address from what the action was made
152 :param sa: optional sqlalchemy session
153 :param sa: optional sqlalchemy session
153
154
154 """
155 """
155
156
156 if not sa:
157 if not sa:
157 sa = meta.Session()
158 sa = meta.Session()
158 # if we don't get explicit IP address try to get one from registered user
159 # if we don't get explicit IP address try to get one from registered user
159 # in tmpl context var
160 # in tmpl context var
160 if not ipaddr:
161 if not ipaddr:
161 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
162 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
162
163
163 try:
164 try:
164 if getattr(user, 'user_id', None):
165 if getattr(user, 'user_id', None):
165 user_obj = User.get(user.user_id)
166 user_obj = User.get(user.user_id)
166 elif isinstance(user, basestring):
167 elif isinstance(user, basestring):
167 user_obj = User.get_by_username(user)
168 user_obj = User.get_by_username(user)
168 else:
169 else:
169 raise Exception('You have to provide a user object or a username')
170 raise Exception('You have to provide a user object or a username')
170
171
171 if getattr(repo, 'repo_id', None):
172 if getattr(repo, 'repo_id', None):
172 repo_obj = Repository.get(repo.repo_id)
173 repo_obj = Repository.get(repo.repo_id)
173 repo_name = repo_obj.repo_name
174 repo_name = repo_obj.repo_name
174 elif isinstance(repo, basestring):
175 elif isinstance(repo, basestring):
175 repo_name = repo.lstrip('/')
176 repo_name = repo.lstrip('/')
176 repo_obj = Repository.get_by_repo_name(repo_name)
177 repo_obj = Repository.get_by_repo_name(repo_name)
177 else:
178 else:
178 repo_obj = None
179 repo_obj = None
179 repo_name = ''
180 repo_name = ''
180
181
181 user_log = UserLog()
182 user_log = UserLog()
182 user_log.user_id = user_obj.user_id
183 user_log.user_id = user_obj.user_id
183 user_log.username = user_obj.username
184 user_log.username = user_obj.username
184 action = safe_unicode(action)
185 action = safe_unicode(action)
185 user_log.action = action[:1200000]
186 user_log.action = action[:1200000]
186
187
187 user_log.repository = repo_obj
188 user_log.repository = repo_obj
188 user_log.repository_name = repo_name
189 user_log.repository_name = repo_name
189
190
190 user_log.action_date = datetime.datetime.now()
191 user_log.action_date = datetime.datetime.now()
191 user_log.user_ip = ipaddr
192 user_log.user_ip = ipaddr
192 sa.add(user_log)
193 sa.add(user_log)
193
194
194 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
195 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
195 action, safe_unicode(repo), user_obj, ipaddr)
196 action, safe_unicode(repo), user_obj, ipaddr)
196 if commit:
197 if commit:
197 sa.commit()
198 sa.commit()
198 except Exception:
199 except Exception:
199 log.error(traceback.format_exc())
200 log.error(traceback.format_exc())
200 raise
201 raise
201
202
202
203
203 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
204 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
204 """
205 """
205 Scans given path for repos and return (name,(type,path)) tuple
206 Scans given path for repos and return (name,(type,path)) tuple
206
207
207 :param path: path to scan for repositories
208 :param path: path to scan for repositories
208 :param recursive: recursive search and return names with subdirs in front
209 :param recursive: recursive search and return names with subdirs in front
209 """
210 """
210
211
211 # remove ending slash for better results
212 # remove ending slash for better results
212 path = path.rstrip(os.sep)
213 path = path.rstrip(os.sep)
213 log.debug('now scanning in %s location recursive:%s...', path, recursive)
214 log.debug('now scanning in %s location recursive:%s...', path, recursive)
214
215
215 def _get_repos(p):
216 def _get_repos(p):
216 dirpaths = _get_dirpaths(p)
217 dirpaths = _get_dirpaths(p)
217 if not _is_dir_writable(p):
218 if not _is_dir_writable(p):
218 log.warning('repo path without write access: %s', p)
219 log.warning('repo path without write access: %s', p)
219
220
220 for dirpath in dirpaths:
221 for dirpath in dirpaths:
221 if os.path.isfile(os.path.join(p, dirpath)):
222 if os.path.isfile(os.path.join(p, dirpath)):
222 continue
223 continue
223 cur_path = os.path.join(p, dirpath)
224 cur_path = os.path.join(p, dirpath)
224
225
225 # skip removed repos
226 # skip removed repos
226 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
227 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
227 continue
228 continue
228
229
229 #skip .<somethin> dirs
230 #skip .<somethin> dirs
230 if dirpath.startswith('.'):
231 if dirpath.startswith('.'):
231 continue
232 continue
232
233
233 try:
234 try:
234 scm_info = get_scm(cur_path)
235 scm_info = get_scm(cur_path)
235 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
236 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
236 except VCSError:
237 except VCSError:
237 if not recursive:
238 if not recursive:
238 continue
239 continue
239 #check if this dir containts other repos for recursive scan
240 #check if this dir containts other repos for recursive scan
240 rec_path = os.path.join(p, dirpath)
241 rec_path = os.path.join(p, dirpath)
241 if os.path.isdir(rec_path):
242 if os.path.isdir(rec_path):
242 for inner_scm in _get_repos(rec_path):
243 for inner_scm in _get_repos(rec_path):
243 yield inner_scm
244 yield inner_scm
244
245
245 return _get_repos(path)
246 return _get_repos(path)
246
247
247
248
248 def _get_dirpaths(p):
249 def _get_dirpaths(p):
249 try:
250 try:
250 # OS-independable way of checking if we have at least read-only
251 # OS-independable way of checking if we have at least read-only
251 # access or not.
252 # access or not.
252 dirpaths = os.listdir(p)
253 dirpaths = os.listdir(p)
253 except OSError:
254 except OSError:
254 log.warning('ignoring repo path without read access: %s', p)
255 log.warning('ignoring repo path without read access: %s', p)
255 return []
256 return []
256
257
257 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
258 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
258 # decode paths and suddenly returns unicode objects itself. The items it
259 # decode paths and suddenly returns unicode objects itself. The items it
259 # cannot decode are returned as strings and cause issues.
260 # cannot decode are returned as strings and cause issues.
260 #
261 #
261 # Those paths are ignored here until a solid solution for path handling has
262 # Those paths are ignored here until a solid solution for path handling has
262 # been built.
263 # been built.
263 expected_type = type(p)
264 expected_type = type(p)
264
265
265 def _has_correct_type(item):
266 def _has_correct_type(item):
266 if type(item) is not expected_type:
267 if type(item) is not expected_type:
267 log.error(
268 log.error(
268 u"Ignoring path %s since it cannot be decoded into unicode.",
269 u"Ignoring path %s since it cannot be decoded into unicode.",
269 # Using "repr" to make sure that we see the byte value in case
270 # Using "repr" to make sure that we see the byte value in case
270 # of support.
271 # of support.
271 repr(item))
272 repr(item))
272 return False
273 return False
273 return True
274 return True
274
275
275 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
276 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
276
277
277 return dirpaths
278 return dirpaths
278
279
279
280
280 def _is_dir_writable(path):
281 def _is_dir_writable(path):
281 """
282 """
282 Probe if `path` is writable.
283 Probe if `path` is writable.
283
284
284 Due to trouble on Cygwin / Windows, this is actually probing if it is
285 Due to trouble on Cygwin / Windows, this is actually probing if it is
285 possible to create a file inside of `path`, stat does not produce reliable
286 possible to create a file inside of `path`, stat does not produce reliable
286 results in this case.
287 results in this case.
287 """
288 """
288 try:
289 try:
289 with tempfile.TemporaryFile(dir=path):
290 with tempfile.TemporaryFile(dir=path):
290 pass
291 pass
291 except OSError:
292 except OSError:
292 return False
293 return False
293 return True
294 return True
294
295
295
296
296 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
297 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
297 """
298 """
298 Returns True if given path is a valid repository False otherwise.
299 Returns True if given path is a valid repository False otherwise.
299 If expect_scm param is given also, compare if given scm is the same
300 If expect_scm param is given also, compare if given scm is the same
300 as expected from scm parameter. If explicit_scm is given don't try to
301 as expected from scm parameter. If explicit_scm is given don't try to
301 detect the scm, just use the given one to check if repo is valid
302 detect the scm, just use the given one to check if repo is valid
302
303
303 :param repo_name:
304 :param repo_name:
304 :param base_path:
305 :param base_path:
305 :param expect_scm:
306 :param expect_scm:
306 :param explicit_scm:
307 :param explicit_scm:
307
308
308 :return True: if given path is a valid repository
309 :return True: if given path is a valid repository
309 """
310 """
310 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
311 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
311 log.debug('Checking if `%s` is a valid path for repository. '
312 log.debug('Checking if `%s` is a valid path for repository. '
312 'Explicit type: %s', repo_name, explicit_scm)
313 'Explicit type: %s', repo_name, explicit_scm)
313
314
314 try:
315 try:
315 if explicit_scm:
316 if explicit_scm:
316 detected_scms = [get_scm_backend(explicit_scm)]
317 detected_scms = [get_scm_backend(explicit_scm)]
317 else:
318 else:
318 detected_scms = get_scm(full_path)
319 detected_scms = get_scm(full_path)
319
320
320 if expect_scm:
321 if expect_scm:
321 return detected_scms[0] == expect_scm
322 return detected_scms[0] == expect_scm
322 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
323 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
323 return True
324 return True
324 except VCSError:
325 except VCSError:
325 log.debug('path: %s is not a valid repo !', full_path)
326 log.debug('path: %s is not a valid repo !', full_path)
326 return False
327 return False
327
328
328
329
329 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
330 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
330 """
331 """
331 Returns True if given path is a repository group, False otherwise
332 Returns True if given path is a repository group, False otherwise
332
333
333 :param repo_name:
334 :param repo_name:
334 :param base_path:
335 :param base_path:
335 """
336 """
336 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
337 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
337 log.debug('Checking if `%s` is a valid path for repository group',
338 log.debug('Checking if `%s` is a valid path for repository group',
338 repo_group_name)
339 repo_group_name)
339
340
340 # check if it's not a repo
341 # check if it's not a repo
341 if is_valid_repo(repo_group_name, base_path):
342 if is_valid_repo(repo_group_name, base_path):
342 log.debug('Repo called %s exist, it is not a valid '
343 log.debug('Repo called %s exist, it is not a valid '
343 'repo group' % repo_group_name)
344 'repo group' % repo_group_name)
344 return False
345 return False
345
346
346 try:
347 try:
347 # we need to check bare git repos at higher level
348 # we need to check bare git repos at higher level
348 # since we might match branches/hooks/info/objects or possible
349 # since we might match branches/hooks/info/objects or possible
349 # other things inside bare git repo
350 # other things inside bare git repo
350 scm_ = get_scm(os.path.dirname(full_path))
351 scm_ = get_scm(os.path.dirname(full_path))
351 log.debug('path: %s is a vcs object:%s, not valid '
352 log.debug('path: %s is a vcs object:%s, not valid '
352 'repo group' % (full_path, scm_))
353 'repo group' % (full_path, scm_))
353 return False
354 return False
354 except VCSError:
355 except VCSError:
355 pass
356 pass
356
357
357 # check if it's a valid path
358 # check if it's a valid path
358 if skip_path_check or os.path.isdir(full_path):
359 if skip_path_check or os.path.isdir(full_path):
359 log.debug('path: %s is a valid repo group !', full_path)
360 log.debug('path: %s is a valid repo group !', full_path)
360 return True
361 return True
361
362
362 log.debug('path: %s is not a valid repo group !', full_path)
363 log.debug('path: %s is not a valid repo group !', full_path)
363 return False
364 return False
364
365
365
366
366 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
367 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
367 while True:
368 while True:
368 ok = raw_input(prompt)
369 ok = raw_input(prompt)
369 if ok.lower() in ('y', 'ye', 'yes'):
370 if ok.lower() in ('y', 'ye', 'yes'):
370 return True
371 return True
371 if ok.lower() in ('n', 'no', 'nop', 'nope'):
372 if ok.lower() in ('n', 'no', 'nop', 'nope'):
372 return False
373 return False
373 retries = retries - 1
374 retries = retries - 1
374 if retries < 0:
375 if retries < 0:
375 raise IOError
376 raise IOError
376 print(complaint)
377 print(complaint)
377
378
378 # propagated from mercurial documentation
379 # propagated from mercurial documentation
379 ui_sections = [
380 ui_sections = [
380 'alias', 'auth',
381 'alias', 'auth',
381 'decode/encode', 'defaults',
382 'decode/encode', 'defaults',
382 'diff', 'email',
383 'diff', 'email',
383 'extensions', 'format',
384 'extensions', 'format',
384 'merge-patterns', 'merge-tools',
385 'merge-patterns', 'merge-tools',
385 'hooks', 'http_proxy',
386 'hooks', 'http_proxy',
386 'smtp', 'patch',
387 'smtp', 'patch',
387 'paths', 'profiling',
388 'paths', 'profiling',
388 'server', 'trusted',
389 'server', 'trusted',
389 'ui', 'web', ]
390 'ui', 'web', ]
390
391
391
392
392 def config_data_from_db(clear_session=True, repo=None):
393 def config_data_from_db(clear_session=True, repo=None):
393 """
394 """
394 Read the configuration data from the database and return configuration
395 Read the configuration data from the database and return configuration
395 tuples.
396 tuples.
396 """
397 """
397 from rhodecode.model.settings import VcsSettingsModel
398 from rhodecode.model.settings import VcsSettingsModel
398
399
399 config = []
400 config = []
400
401
401 sa = meta.Session()
402 sa = meta.Session()
402 settings_model = VcsSettingsModel(repo=repo, sa=sa)
403 settings_model = VcsSettingsModel(repo=repo, sa=sa)
403
404
404 ui_settings = settings_model.get_ui_settings()
405 ui_settings = settings_model.get_ui_settings()
405
406
406 for setting in ui_settings:
407 for setting in ui_settings:
407 if setting.active:
408 if setting.active:
408 log.debug(
409 log.debug(
409 'settings ui from db: [%s] %s=%s',
410 'settings ui from db: [%s] %s=%s',
410 setting.section, setting.key, setting.value)
411 setting.section, setting.key, setting.value)
411 config.append((
412 config.append((
412 safe_str(setting.section), safe_str(setting.key),
413 safe_str(setting.section), safe_str(setting.key),
413 safe_str(setting.value)))
414 safe_str(setting.value)))
414 if setting.key == 'push_ssl':
415 if setting.key == 'push_ssl':
415 # force set push_ssl requirement to False, rhodecode
416 # force set push_ssl requirement to False, rhodecode
416 # handles that
417 # handles that
417 config.append((
418 config.append((
418 safe_str(setting.section), safe_str(setting.key), False))
419 safe_str(setting.section), safe_str(setting.key), False))
419 if clear_session:
420 if clear_session:
420 meta.Session.remove()
421 meta.Session.remove()
421
422
422 # TODO: mikhail: probably it makes no sense to re-read hooks information.
423 # TODO: mikhail: probably it makes no sense to re-read hooks information.
423 # It's already there and activated/deactivated
424 # It's already there and activated/deactivated
424 skip_entries = []
425 skip_entries = []
425 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
426 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
426 if 'pull' not in enabled_hook_classes:
427 if 'pull' not in enabled_hook_classes:
427 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
428 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
428 if 'push' not in enabled_hook_classes:
429 if 'push' not in enabled_hook_classes:
429 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
430 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
430 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
431 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
431 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
432 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
432
433
433 config = [entry for entry in config if entry[:2] not in skip_entries]
434 config = [entry for entry in config if entry[:2] not in skip_entries]
434
435
435 return config
436 return config
436
437
437
438
438 def make_db_config(clear_session=True, repo=None):
439 def make_db_config(clear_session=True, repo=None):
439 """
440 """
440 Create a :class:`Config` instance based on the values in the database.
441 Create a :class:`Config` instance based on the values in the database.
441 """
442 """
442 config = Config()
443 config = Config()
443 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
444 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
444 for section, option, value in config_data:
445 for section, option, value in config_data:
445 config.set(section, option, value)
446 config.set(section, option, value)
446 return config
447 return config
447
448
448
449
449 def get_enabled_hook_classes(ui_settings):
450 def get_enabled_hook_classes(ui_settings):
450 """
451 """
451 Return the enabled hook classes.
452 Return the enabled hook classes.
452
453
453 :param ui_settings: List of ui_settings as returned
454 :param ui_settings: List of ui_settings as returned
454 by :meth:`VcsSettingsModel.get_ui_settings`
455 by :meth:`VcsSettingsModel.get_ui_settings`
455
456
456 :return: a list with the enabled hook classes. The order is not guaranteed.
457 :return: a list with the enabled hook classes. The order is not guaranteed.
457 :rtype: list
458 :rtype: list
458 """
459 """
459 enabled_hooks = []
460 enabled_hooks = []
460 active_hook_keys = [
461 active_hook_keys = [
461 key for section, key, value, active in ui_settings
462 key for section, key, value, active in ui_settings
462 if section == 'hooks' and active]
463 if section == 'hooks' and active]
463
464
464 hook_names = {
465 hook_names = {
465 RhodeCodeUi.HOOK_PUSH: 'push',
466 RhodeCodeUi.HOOK_PUSH: 'push',
466 RhodeCodeUi.HOOK_PULL: 'pull',
467 RhodeCodeUi.HOOK_PULL: 'pull',
467 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
468 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
468 }
469 }
469
470
470 for key in active_hook_keys:
471 for key in active_hook_keys:
471 hook = hook_names.get(key)
472 hook = hook_names.get(key)
472 if hook:
473 if hook:
473 enabled_hooks.append(hook)
474 enabled_hooks.append(hook)
474
475
475 return enabled_hooks
476 return enabled_hooks
476
477
477
478
478 def set_rhodecode_config(config):
479 def set_rhodecode_config(config):
479 """
480 """
480 Updates pylons config with new settings from database
481 Updates pylons config with new settings from database
481
482
482 :param config:
483 :param config:
483 """
484 """
484 from rhodecode.model.settings import SettingsModel
485 from rhodecode.model.settings import SettingsModel
485 app_settings = SettingsModel().get_all_settings()
486 app_settings = SettingsModel().get_all_settings()
486
487
487 for k, v in app_settings.items():
488 for k, v in app_settings.items():
488 config[k] = v
489 config[k] = v
489
490
490
491
491 def get_rhodecode_realm():
492 def get_rhodecode_realm():
492 """
493 """
493 Return the rhodecode realm from database.
494 Return the rhodecode realm from database.
494 """
495 """
495 from rhodecode.model.settings import SettingsModel
496 from rhodecode.model.settings import SettingsModel
496 realm = SettingsModel().get_setting_by_name('realm')
497 realm = SettingsModel().get_setting_by_name('realm')
497 return safe_str(realm.app_settings_value)
498 return safe_str(realm.app_settings_value)
498
499
499
500
500 def get_rhodecode_base_path():
501 def get_rhodecode_base_path():
501 """
502 """
502 Returns the base path. The base path is the filesystem path which points
503 Returns the base path. The base path is the filesystem path which points
503 to the repository store.
504 to the repository store.
504 """
505 """
505 from rhodecode.model.settings import SettingsModel
506 from rhodecode.model.settings import SettingsModel
506 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
507 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
507 return safe_str(paths_ui.ui_value)
508 return safe_str(paths_ui.ui_value)
508
509
509
510
510 def map_groups(path):
511 def map_groups(path):
511 """
512 """
512 Given a full path to a repository, create all nested groups that this
513 Given a full path to a repository, create all nested groups that this
513 repo is inside. This function creates parent-child relationships between
514 repo is inside. This function creates parent-child relationships between
514 groups and creates default perms for all new groups.
515 groups and creates default perms for all new groups.
515
516
516 :param paths: full path to repository
517 :param paths: full path to repository
517 """
518 """
518 from rhodecode.model.repo_group import RepoGroupModel
519 from rhodecode.model.repo_group import RepoGroupModel
519 sa = meta.Session()
520 sa = meta.Session()
520 groups = path.split(Repository.NAME_SEP)
521 groups = path.split(Repository.NAME_SEP)
521 parent = None
522 parent = None
522 group = None
523 group = None
523
524
524 # last element is repo in nested groups structure
525 # last element is repo in nested groups structure
525 groups = groups[:-1]
526 groups = groups[:-1]
526 rgm = RepoGroupModel(sa)
527 rgm = RepoGroupModel(sa)
527 owner = User.get_first_super_admin()
528 owner = User.get_first_super_admin()
528 for lvl, group_name in enumerate(groups):
529 for lvl, group_name in enumerate(groups):
529 group_name = '/'.join(groups[:lvl] + [group_name])
530 group_name = '/'.join(groups[:lvl] + [group_name])
530 group = RepoGroup.get_by_group_name(group_name)
531 group = RepoGroup.get_by_group_name(group_name)
531 desc = '%s group' % group_name
532 desc = '%s group' % group_name
532
533
533 # skip folders that are now removed repos
534 # skip folders that are now removed repos
534 if REMOVED_REPO_PAT.match(group_name):
535 if REMOVED_REPO_PAT.match(group_name):
535 break
536 break
536
537
537 if group is None:
538 if group is None:
538 log.debug('creating group level: %s group_name: %s',
539 log.debug('creating group level: %s group_name: %s',
539 lvl, group_name)
540 lvl, group_name)
540 group = RepoGroup(group_name, parent)
541 group = RepoGroup(group_name, parent)
541 group.group_description = desc
542 group.group_description = desc
542 group.user = owner
543 group.user = owner
543 sa.add(group)
544 sa.add(group)
544 perm_obj = rgm._create_default_perms(group)
545 perm_obj = rgm._create_default_perms(group)
545 sa.add(perm_obj)
546 sa.add(perm_obj)
546 sa.flush()
547 sa.flush()
547
548
548 parent = group
549 parent = group
549 return group
550 return group
550
551
551
552
552 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
553 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
553 """
554 """
554 maps all repos given in initial_repo_list, non existing repositories
555 maps all repos given in initial_repo_list, non existing repositories
555 are created, if remove_obsolete is True it also checks for db entries
556 are created, if remove_obsolete is True it also checks for db entries
556 that are not in initial_repo_list and removes them.
557 that are not in initial_repo_list and removes them.
557
558
558 :param initial_repo_list: list of repositories found by scanning methods
559 :param initial_repo_list: list of repositories found by scanning methods
559 :param remove_obsolete: check for obsolete entries in database
560 :param remove_obsolete: check for obsolete entries in database
560 """
561 """
561 from rhodecode.model.repo import RepoModel
562 from rhodecode.model.repo import RepoModel
562 from rhodecode.model.scm import ScmModel
563 from rhodecode.model.scm import ScmModel
563 from rhodecode.model.repo_group import RepoGroupModel
564 from rhodecode.model.repo_group import RepoGroupModel
564 from rhodecode.model.settings import SettingsModel
565 from rhodecode.model.settings import SettingsModel
565
566
566 sa = meta.Session()
567 sa = meta.Session()
567 repo_model = RepoModel()
568 repo_model = RepoModel()
568 user = User.get_first_super_admin()
569 user = User.get_first_super_admin()
569 added = []
570 added = []
570
571
571 # creation defaults
572 # creation defaults
572 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
573 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
573 enable_statistics = defs.get('repo_enable_statistics')
574 enable_statistics = defs.get('repo_enable_statistics')
574 enable_locking = defs.get('repo_enable_locking')
575 enable_locking = defs.get('repo_enable_locking')
575 enable_downloads = defs.get('repo_enable_downloads')
576 enable_downloads = defs.get('repo_enable_downloads')
576 private = defs.get('repo_private')
577 private = defs.get('repo_private')
577
578
578 for name, repo in initial_repo_list.items():
579 for name, repo in initial_repo_list.items():
579 group = map_groups(name)
580 group = map_groups(name)
580 unicode_name = safe_unicode(name)
581 unicode_name = safe_unicode(name)
581 db_repo = repo_model.get_by_repo_name(unicode_name)
582 db_repo = repo_model.get_by_repo_name(unicode_name)
582 # found repo that is on filesystem not in RhodeCode database
583 # found repo that is on filesystem not in RhodeCode database
583 if not db_repo:
584 if not db_repo:
584 log.info('repository %s not found, creating now', name)
585 log.info('repository %s not found, creating now', name)
585 added.append(name)
586 added.append(name)
586 desc = (repo.description
587 desc = (repo.description
587 if repo.description != 'unknown'
588 if repo.description != 'unknown'
588 else '%s repository' % name)
589 else '%s repository' % name)
589
590
590 db_repo = repo_model._create_repo(
591 db_repo = repo_model._create_repo(
591 repo_name=name,
592 repo_name=name,
592 repo_type=repo.alias,
593 repo_type=repo.alias,
593 description=desc,
594 description=desc,
594 repo_group=getattr(group, 'group_id', None),
595 repo_group=getattr(group, 'group_id', None),
595 owner=user,
596 owner=user,
596 enable_locking=enable_locking,
597 enable_locking=enable_locking,
597 enable_downloads=enable_downloads,
598 enable_downloads=enable_downloads,
598 enable_statistics=enable_statistics,
599 enable_statistics=enable_statistics,
599 private=private,
600 private=private,
600 state=Repository.STATE_CREATED
601 state=Repository.STATE_CREATED
601 )
602 )
602 sa.commit()
603 sa.commit()
603 # we added that repo just now, and make sure we updated server info
604 # we added that repo just now, and make sure we updated server info
604 if db_repo.repo_type == 'git':
605 if db_repo.repo_type == 'git':
605 git_repo = db_repo.scm_instance()
606 git_repo = db_repo.scm_instance()
606 # update repository server-info
607 # update repository server-info
607 log.debug('Running update server info')
608 log.debug('Running update server info')
608 git_repo._update_server_info()
609 git_repo._update_server_info()
609
610
610 db_repo.update_commit_cache()
611 db_repo.update_commit_cache()
611
612
612 config = db_repo._config
613 config = db_repo._config
613 config.set('extensions', 'largefiles', '')
614 config.set('extensions', 'largefiles', '')
614 ScmModel().install_hooks(
615 ScmModel().install_hooks(
615 db_repo.scm_instance(config=config),
616 db_repo.scm_instance(config=config),
616 repo_type=db_repo.repo_type)
617 repo_type=db_repo.repo_type)
617
618
618 removed = []
619 removed = []
619 if remove_obsolete:
620 if remove_obsolete:
620 # remove from database those repositories that are not in the filesystem
621 # remove from database those repositories that are not in the filesystem
621 for repo in sa.query(Repository).all():
622 for repo in sa.query(Repository).all():
622 if repo.repo_name not in initial_repo_list.keys():
623 if repo.repo_name not in initial_repo_list.keys():
623 log.debug("Removing non-existing repository found in db `%s`",
624 log.debug("Removing non-existing repository found in db `%s`",
624 repo.repo_name)
625 repo.repo_name)
625 try:
626 try:
626 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
627 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
627 sa.commit()
628 sa.commit()
628 removed.append(repo.repo_name)
629 removed.append(repo.repo_name)
629 except Exception:
630 except Exception:
630 # don't hold further removals on error
631 # don't hold further removals on error
631 log.error(traceback.format_exc())
632 log.error(traceback.format_exc())
632 sa.rollback()
633 sa.rollback()
633
634
634 def splitter(full_repo_name):
635 def splitter(full_repo_name):
635 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
636 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
636 gr_name = None
637 gr_name = None
637 if len(_parts) == 2:
638 if len(_parts) == 2:
638 gr_name = _parts[0]
639 gr_name = _parts[0]
639 return gr_name
640 return gr_name
640
641
641 initial_repo_group_list = [splitter(x) for x in
642 initial_repo_group_list = [splitter(x) for x in
642 initial_repo_list.keys() if splitter(x)]
643 initial_repo_list.keys() if splitter(x)]
643
644
644 # remove from database those repository groups that are not in the
645 # remove from database those repository groups that are not in the
645 # filesystem due to parent child relationships we need to delete them
646 # filesystem due to parent child relationships we need to delete them
646 # in a specific order of most nested first
647 # in a specific order of most nested first
647 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
648 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
648 nested_sort = lambda gr: len(gr.split('/'))
649 nested_sort = lambda gr: len(gr.split('/'))
649 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
650 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
650 if group_name not in initial_repo_group_list:
651 if group_name not in initial_repo_group_list:
651 repo_group = RepoGroup.get_by_group_name(group_name)
652 repo_group = RepoGroup.get_by_group_name(group_name)
652 if (repo_group.children.all() or
653 if (repo_group.children.all() or
653 not RepoGroupModel().check_exist_filesystem(
654 not RepoGroupModel().check_exist_filesystem(
654 group_name=group_name, exc_on_failure=False)):
655 group_name=group_name, exc_on_failure=False)):
655 continue
656 continue
656
657
657 log.info(
658 log.info(
658 'Removing non-existing repository group found in db `%s`',
659 'Removing non-existing repository group found in db `%s`',
659 group_name)
660 group_name)
660 try:
661 try:
661 RepoGroupModel(sa).delete(group_name, fs_remove=False)
662 RepoGroupModel(sa).delete(group_name, fs_remove=False)
662 sa.commit()
663 sa.commit()
663 removed.append(group_name)
664 removed.append(group_name)
664 except Exception:
665 except Exception:
665 # don't hold further removals on error
666 # don't hold further removals on error
666 log.exception(
667 log.exception(
667 'Unable to remove repository group `%s`',
668 'Unable to remove repository group `%s`',
668 group_name)
669 group_name)
669 sa.rollback()
670 sa.rollback()
670 raise
671 raise
671
672
672 return added, removed
673 return added, removed
673
674
674
675
675 def get_default_cache_settings(settings):
676 def get_default_cache_settings(settings):
676 cache_settings = {}
677 cache_settings = {}
677 for key in settings.keys():
678 for key in settings.keys():
678 for prefix in ['beaker.cache.', 'cache.']:
679 for prefix in ['beaker.cache.', 'cache.']:
679 if key.startswith(prefix):
680 if key.startswith(prefix):
680 name = key.split(prefix)[1].strip()
681 name = key.split(prefix)[1].strip()
681 cache_settings[name] = settings[key].strip()
682 cache_settings[name] = settings[key].strip()
682 return cache_settings
683 return cache_settings
683
684
684
685
685 # set cache regions for beaker so celery can utilise it
686 # set cache regions for beaker so celery can utilise it
686 def add_cache(settings):
687 def add_cache(settings):
687 from rhodecode.lib import caches
688 from rhodecode.lib import caches
688 cache_settings = {'regions': None}
689 cache_settings = {'regions': None}
689 # main cache settings used as default ...
690 # main cache settings used as default ...
690 cache_settings.update(get_default_cache_settings(settings))
691 cache_settings.update(get_default_cache_settings(settings))
691
692
692 if cache_settings['regions']:
693 if cache_settings['regions']:
693 for region in cache_settings['regions'].split(','):
694 for region in cache_settings['regions'].split(','):
694 region = region.strip()
695 region = region.strip()
695 region_settings = {}
696 region_settings = {}
696 for key, value in cache_settings.items():
697 for key, value in cache_settings.items():
697 if key.startswith(region):
698 if key.startswith(region):
698 region_settings[key.split('.')[1]] = value
699 region_settings[key.split('.')[1]] = value
699
700
700 caches.configure_cache_region(
701 caches.configure_cache_region(
701 region, region_settings, cache_settings)
702 region, region_settings, cache_settings)
702
703
703
704
704 def load_rcextensions(root_path):
705 def load_rcextensions(root_path):
705 import rhodecode
706 import rhodecode
706 from rhodecode.config import conf
707 from rhodecode.config import conf
707
708
708 path = os.path.join(root_path, 'rcextensions', '__init__.py')
709 path = os.path.join(root_path, 'rcextensions', '__init__.py')
709 if os.path.isfile(path):
710 if os.path.isfile(path):
710 rcext = create_module('rc', path)
711 rcext = create_module('rc', path)
711 EXT = rhodecode.EXTENSIONS = rcext
712 EXT = rhodecode.EXTENSIONS = rcext
712 log.debug('Found rcextensions now loading %s...', rcext)
713 log.debug('Found rcextensions now loading %s...', rcext)
713
714
714 # Additional mappings that are not present in the pygments lexers
715 # Additional mappings that are not present in the pygments lexers
715 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
716 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
716
717
717 # auto check if the module is not missing any data, set to default if is
718 # auto check if the module is not missing any data, set to default if is
718 # this will help autoupdate new feature of rcext module
719 # this will help autoupdate new feature of rcext module
719 #from rhodecode.config import rcextensions
720 #from rhodecode.config import rcextensions
720 #for k in dir(rcextensions):
721 #for k in dir(rcextensions):
721 # if not k.startswith('_') and not hasattr(EXT, k):
722 # if not k.startswith('_') and not hasattr(EXT, k):
722 # setattr(EXT, k, getattr(rcextensions, k))
723 # setattr(EXT, k, getattr(rcextensions, k))
723
724
724
725
725 def get_custom_lexer(extension):
726 def get_custom_lexer(extension):
726 """
727 """
727 returns a custom lexer if it is defined in rcextensions module, or None
728 returns a custom lexer if it is defined in rcextensions module, or None
728 if there's no custom lexer defined
729 if there's no custom lexer defined
729 """
730 """
730 import rhodecode
731 import rhodecode
731 from pygments import lexers
732 from pygments import lexers
732
733
733 # custom override made by RhodeCode
734 # custom override made by RhodeCode
734 if extension in ['mako']:
735 if extension in ['mako']:
735 return lexers.get_lexer_by_name('html+mako')
736 return lexers.get_lexer_by_name('html+mako')
736
737
737 # check if we didn't define this extension as other lexer
738 # check if we didn't define this extension as other lexer
738 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
739 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
739 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
740 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
740 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
741 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
741 return lexers.get_lexer_by_name(_lexer_name)
742 return lexers.get_lexer_by_name(_lexer_name)
742
743
743
744
744 #==============================================================================
745 #==============================================================================
745 # TEST FUNCTIONS AND CREATORS
746 # TEST FUNCTIONS AND CREATORS
746 #==============================================================================
747 #==============================================================================
747 def create_test_index(repo_location, config):
748 def create_test_index(repo_location, config):
748 """
749 """
749 Makes default test index.
750 Makes default test index.
750 """
751 """
751 import rc_testdata
752 import rc_testdata
752
753
753 rc_testdata.extract_search_index(
754 rc_testdata.extract_search_index(
754 'vcs_search_index', os.path.dirname(config['search.location']))
755 'vcs_search_index', os.path.dirname(config['search.location']))
755
756
756
757
757 def create_test_directory(test_path):
758 def create_test_directory(test_path):
758 """
759 """
759 Create test directory if it doesn't exist.
760 Create test directory if it doesn't exist.
760 """
761 """
761 if not os.path.isdir(test_path):
762 if not os.path.isdir(test_path):
762 log.debug('Creating testdir %s', test_path)
763 log.debug('Creating testdir %s', test_path)
763 os.makedirs(test_path)
764 os.makedirs(test_path)
764
765
765
766
766 def create_test_database(test_path, config):
767 def create_test_database(test_path, config):
767 """
768 """
768 Makes a fresh database.
769 Makes a fresh database.
769 """
770 """
770 from rhodecode.lib.db_manage import DbManage
771 from rhodecode.lib.db_manage import DbManage
771
772
772 # PART ONE create db
773 # PART ONE create db
773 dbconf = config['sqlalchemy.db1.url']
774 dbconf = config['sqlalchemy.db1.url']
774 log.debug('making test db %s', dbconf)
775 log.debug('making test db %s', dbconf)
775
776
776 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
777 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
777 tests=True, cli_args={'force_ask': True})
778 tests=True, cli_args={'force_ask': True})
778 dbmanage.create_tables(override=True)
779 dbmanage.create_tables(override=True)
779 dbmanage.set_db_version()
780 dbmanage.set_db_version()
780 # for tests dynamically set new root paths based on generated content
781 # for tests dynamically set new root paths based on generated content
781 dbmanage.create_settings(dbmanage.config_prompt(test_path))
782 dbmanage.create_settings(dbmanage.config_prompt(test_path))
782 dbmanage.create_default_user()
783 dbmanage.create_default_user()
783 dbmanage.create_test_admin_and_users()
784 dbmanage.create_test_admin_and_users()
784 dbmanage.create_permissions()
785 dbmanage.create_permissions()
785 dbmanage.populate_default_permissions()
786 dbmanage.populate_default_permissions()
786 Session().commit()
787 Session().commit()
787
788
788
789
789 def create_test_repositories(test_path, config):
790 def create_test_repositories(test_path, config):
790 """
791 """
791 Creates test repositories in the temporary directory. Repositories are
792 Creates test repositories in the temporary directory. Repositories are
792 extracted from archives within the rc_testdata package.
793 extracted from archives within the rc_testdata package.
793 """
794 """
794 import rc_testdata
795 import rc_testdata
795 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
796 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
796
797
797 log.debug('making test vcs repositories')
798 log.debug('making test vcs repositories')
798
799
799 idx_path = config['search.location']
800 idx_path = config['search.location']
800 data_path = config['cache_dir']
801 data_path = config['cache_dir']
801
802
802 # clean index and data
803 # clean index and data
803 if idx_path and os.path.exists(idx_path):
804 if idx_path and os.path.exists(idx_path):
804 log.debug('remove %s', idx_path)
805 log.debug('remove %s', idx_path)
805 shutil.rmtree(idx_path)
806 shutil.rmtree(idx_path)
806
807
807 if data_path and os.path.exists(data_path):
808 if data_path and os.path.exists(data_path):
808 log.debug('remove %s', data_path)
809 log.debug('remove %s', data_path)
809 shutil.rmtree(data_path)
810 shutil.rmtree(data_path)
810
811
811 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
812 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
812 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
813 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
813
814
814 # Note: Subversion is in the process of being integrated with the system,
815 # Note: Subversion is in the process of being integrated with the system,
815 # until we have a properly packed version of the test svn repository, this
816 # until we have a properly packed version of the test svn repository, this
816 # tries to copy over the repo from a package "rc_testdata"
817 # tries to copy over the repo from a package "rc_testdata"
817 svn_repo_path = rc_testdata.get_svn_repo_archive()
818 svn_repo_path = rc_testdata.get_svn_repo_archive()
818 with tarfile.open(svn_repo_path) as tar:
819 with tarfile.open(svn_repo_path) as tar:
819 tar.extractall(jn(test_path, SVN_REPO))
820 tar.extractall(jn(test_path, SVN_REPO))
820
821
821
822
822 #==============================================================================
823 #==============================================================================
823 # PASTER COMMANDS
824 # PASTER COMMANDS
824 #==============================================================================
825 #==============================================================================
825 class BasePasterCommand(Command):
826 class BasePasterCommand(Command):
826 """
827 """
827 Abstract Base Class for paster commands.
828 Abstract Base Class for paster commands.
828
829
829 The celery commands are somewhat aggressive about loading
830 The celery commands are somewhat aggressive about loading
830 celery.conf, and since our module sets the `CELERY_LOADER`
831 celery.conf, and since our module sets the `CELERY_LOADER`
831 environment variable to our loader, we have to bootstrap a bit and
832 environment variable to our loader, we have to bootstrap a bit and
832 make sure we've had a chance to load the pylons config off of the
833 make sure we've had a chance to load the pylons config off of the
833 command line, otherwise everything fails.
834 command line, otherwise everything fails.
834 """
835 """
835 min_args = 1
836 min_args = 1
836 min_args_error = "Please provide a paster config file as an argument."
837 min_args_error = "Please provide a paster config file as an argument."
837 takes_config_file = 1
838 takes_config_file = 1
838 requires_config_file = True
839 requires_config_file = True
839
840
840 def notify_msg(self, msg, log=False):
841 def notify_msg(self, msg, log=False):
841 """Make a notification to user, additionally if logger is passed
842 """Make a notification to user, additionally if logger is passed
842 it logs this action using given logger
843 it logs this action using given logger
843
844
844 :param msg: message that will be printed to user
845 :param msg: message that will be printed to user
845 :param log: logging instance, to use to additionally log this message
846 :param log: logging instance, to use to additionally log this message
846
847
847 """
848 """
848 if log and isinstance(log, logging):
849 if log and isinstance(log, logging):
849 log(msg)
850 log(msg)
850
851
851 def run(self, args):
852 def run(self, args):
852 """
853 """
853 Overrides Command.run
854 Overrides Command.run
854
855
855 Checks for a config file argument and loads it.
856 Checks for a config file argument and loads it.
856 """
857 """
857 if len(args) < self.min_args:
858 if len(args) < self.min_args:
858 raise BadCommand(
859 raise BadCommand(
859 self.min_args_error % {'min_args': self.min_args,
860 self.min_args_error % {'min_args': self.min_args,
860 'actual_args': len(args)})
861 'actual_args': len(args)})
861
862
862 # Decrement because we're going to lob off the first argument.
863 # Decrement because we're going to lob off the first argument.
863 # @@ This is hacky
864 # @@ This is hacky
864 self.min_args -= 1
865 self.min_args -= 1
865 self.bootstrap_config(args[0])
866 self.bootstrap_config(args[0])
866 self.update_parser()
867 self.update_parser()
867 return super(BasePasterCommand, self).run(args[1:])
868 return super(BasePasterCommand, self).run(args[1:])
868
869
869 def update_parser(self):
870 def update_parser(self):
870 """
871 """
871 Abstract method. Allows for the class' parser to be updated
872 Abstract method. Allows for the class' parser to be updated
872 before the superclass' `run` method is called. Necessary to
873 before the superclass' `run` method is called. Necessary to
873 allow options/arguments to be passed through to the underlying
874 allow options/arguments to be passed through to the underlying
874 celery command.
875 celery command.
875 """
876 """
876 raise NotImplementedError("Abstract Method.")
877 raise NotImplementedError("Abstract Method.")
877
878
878 def bootstrap_config(self, conf):
879 def bootstrap_config(self, conf):
879 """
880 """
880 Loads the pylons configuration.
881 Loads the pylons configuration.
881 """
882 """
882 from pylons import config as pylonsconfig
883 from pylons import config as pylonsconfig
883
884
884 self.path_to_ini_file = os.path.realpath(conf)
885 self.path_to_ini_file = os.path.realpath(conf)
885 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
886 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
886 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
887 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
887
888
888 def _init_session(self):
889 def _init_session(self):
889 """
890 """
890 Inits SqlAlchemy Session
891 Inits SqlAlchemy Session
891 """
892 """
892 logging.config.fileConfig(self.path_to_ini_file)
893 logging.config.fileConfig(self.path_to_ini_file)
893 from pylons import config
894 from pylons import config
894 from rhodecode.config.utils import initialize_database
895 from rhodecode.config.utils import initialize_database
895
896
896 # get to remove repos !!
897 # get to remove repos !!
897 add_cache(config)
898 add_cache(config)
898 initialize_database(config)
899 initialize_database(config)
899
900
900
901
901 @decorator.decorator
902 @decorator.decorator
902 def jsonify(func, *args, **kwargs):
903 def jsonify(func, *args, **kwargs):
903 """Action decorator that formats output for JSON
904 """Action decorator that formats output for JSON
904
905
905 Given a function that will return content, this decorator will turn
906 Given a function that will return content, this decorator will turn
906 the result into JSON, with a content-type of 'application/json' and
907 the result into JSON, with a content-type of 'application/json' and
907 output it.
908 output it.
908
909
909 """
910 """
910 from pylons.decorators.util import get_pylons
911 from pylons.decorators.util import get_pylons
911 from rhodecode.lib.ext_json import json
912 from rhodecode.lib.ext_json import json
912 pylons = get_pylons(args)
913 pylons = get_pylons(args)
913 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
914 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
914 data = func(*args, **kwargs)
915 data = func(*args, **kwargs)
915 if isinstance(data, (list, tuple)):
916 if isinstance(data, (list, tuple)):
916 msg = "JSON responses with Array envelopes are susceptible to " \
917 msg = "JSON responses with Array envelopes are susceptible to " \
917 "cross-site data leak attacks, see " \
918 "cross-site data leak attacks, see " \
918 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
919 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
919 warnings.warn(msg, Warning, 2)
920 warnings.warn(msg, Warning, 2)
920 log.warning(msg)
921 log.warning(msg)
921 log.debug("Returning JSON wrapped action output")
922 log.debug("Returning JSON wrapped action output")
922 return json.dumps(data, encoding='utf-8')
923 return json.dumps(data, encoding='utf-8')
923
924
924
925
925 class PartialRenderer(object):
926 class PartialRenderer(object):
926 """
927 """
927 Partial renderer used to render chunks of html used in datagrids
928 Partial renderer used to render chunks of html used in datagrids
928 use like::
929 use like::
929
930
930 _render = PartialRenderer('data_table/_dt_elements.mako')
931 _render = PartialRenderer('data_table/_dt_elements.mako')
931 _render('quick_menu', args, kwargs)
932 _render('quick_menu', args, kwargs)
932 PartialRenderer.h,
933 PartialRenderer.h,
933 c,
934 c,
934 _,
935 _,
935 ungettext
936 ungettext
936 are the template stuff initialized inside and can be re-used later
937 are the template stuff initialized inside and can be re-used later
937
938
938 :param tmpl_name: template path relate to /templates/ dir
939 :param tmpl_name: template path relate to /templates/ dir
939 """
940 """
940
941
941 def __init__(self, tmpl_name):
942 def __init__(self, tmpl_name):
942 import rhodecode
943 import rhodecode
943 from pylons import request, tmpl_context as c
944 from pylons import request, tmpl_context as c
944 from pylons.i18n.translation import _, ungettext
945 from pylons.i18n.translation import _, ungettext
945 from rhodecode.lib import helpers as h
946 from rhodecode.lib import helpers as h
946
947
947 self.tmpl_name = tmpl_name
948 self.tmpl_name = tmpl_name
948 self.rhodecode = rhodecode
949 self.rhodecode = rhodecode
949 self.c = c
950 self.c = c
950 self._ = _
951 self._ = _
951 self.ungettext = ungettext
952 self.ungettext = ungettext
952 self.h = h
953 self.h = h
953 self.request = request
954 self.request = request
954
955
955 def _mako_lookup(self):
956 def _mako_lookup(self):
956 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
957 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
957 return _tmpl_lookup.get_template(self.tmpl_name)
958 return _tmpl_lookup.get_template(self.tmpl_name)
958
959
959 def _update_kwargs_for_render(self, kwargs):
960 def _update_kwargs_for_render(self, kwargs):
960 """
961 """
961 Inject params required for Mako rendering
962 Inject params required for Mako rendering
962 """
963 """
963 _kwargs = {
964 _kwargs = {
964 '_': self._,
965 '_': self._,
965 'h': self.h,
966 'h': self.h,
966 'c': self.c,
967 'c': self.c,
967 'request': self.request,
968 'request': self.request,
968 'ungettext': self.ungettext,
969 'ungettext': self.ungettext,
969 }
970 }
970 _kwargs.update(kwargs)
971 _kwargs.update(kwargs)
971 return _kwargs
972 return _kwargs
972
973
973 def _render_with_exc(self, render_func, args, kwargs):
974 def _render_with_exc(self, render_func, args, kwargs):
974 try:
975 try:
975 return render_func.render(*args, **kwargs)
976 return render_func.render(*args, **kwargs)
976 except:
977 except:
977 log.error(exceptions.text_error_template().render())
978 log.error(exceptions.text_error_template().render())
978 raise
979 raise
979
980
980 def _get_template(self, template_obj, def_name):
981 def _get_template(self, template_obj, def_name):
981 if def_name:
982 if def_name:
982 tmpl = template_obj.get_def(def_name)
983 tmpl = template_obj.get_def(def_name)
983 else:
984 else:
984 tmpl = template_obj
985 tmpl = template_obj
985 return tmpl
986 return tmpl
986
987
987 def render(self, def_name, *args, **kwargs):
988 def render(self, def_name, *args, **kwargs):
988 lookup_obj = self._mako_lookup()
989 lookup_obj = self._mako_lookup()
989 tmpl = self._get_template(lookup_obj, def_name=def_name)
990 tmpl = self._get_template(lookup_obj, def_name=def_name)
990 kwargs = self._update_kwargs_for_render(kwargs)
991 kwargs = self._update_kwargs_for_render(kwargs)
991 return self._render_with_exc(tmpl, args, kwargs)
992 return self._render_with_exc(tmpl, args, kwargs)
992
993
993 def __call__(self, tmpl, *args, **kwargs):
994 def __call__(self, tmpl, *args, **kwargs):
994 return self.render(tmpl, *args, **kwargs)
995 return self.render(tmpl, *args, **kwargs)
995
996
996
997
997 def password_changed(auth_user, session):
998 def password_changed(auth_user, session):
998 # Never report password change in case of default user or anonymous user.
999 # Never report password change in case of default user or anonymous user.
999 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
1000 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
1000 return False
1001 return False
1001
1002
1002 password_hash = md5(auth_user.password) if auth_user.password else None
1003 password_hash = md5(auth_user.password) if auth_user.password else None
1003 rhodecode_user = session.get('rhodecode_user', {})
1004 rhodecode_user = session.get('rhodecode_user', {})
1004 session_password_hash = rhodecode_user.get('password', '')
1005 session_password_hash = rhodecode_user.get('password', '')
1005 return password_hash != session_password_hash
1006 return password_hash != session_password_hash
1006
1007
1007
1008
1008 def read_opensource_licenses():
1009 def read_opensource_licenses():
1009 global _license_cache
1010 global _license_cache
1010
1011
1011 if not _license_cache:
1012 if not _license_cache:
1012 licenses = pkg_resources.resource_string(
1013 licenses = pkg_resources.resource_string(
1013 'rhodecode', 'config/licenses.json')
1014 'rhodecode', 'config/licenses.json')
1014 _license_cache = json.loads(licenses)
1015 _license_cache = json.loads(licenses)
1015
1016
1016 return _license_cache
1017 return _license_cache
1017
1018
1018
1019
1019 def get_registry(request):
1020 def get_registry(request):
1020 """
1021 """
1021 Utility to get the pyramid registry from a request. During migration to
1022 Utility to get the pyramid registry from a request. During migration to
1022 pyramid we sometimes want to use the pyramid registry from pylons context.
1023 pyramid we sometimes want to use the pyramid registry from pylons context.
1023 Therefore this utility returns `request.registry` for pyramid requests and
1024 Therefore this utility returns `request.registry` for pyramid requests and
1024 uses `get_current_registry()` for pylons requests.
1025 uses `get_current_registry()` for pylons requests.
1025 """
1026 """
1026 try:
1027 try:
1027 return request.registry
1028 return request.registry
1028 except AttributeError:
1029 except AttributeError:
1029 return get_current_registry()
1030 return get_current_registry()
1030
1031
1031
1032
1032 def generate_platform_uuid():
1033 def generate_platform_uuid():
1033 """
1034 """
1034 Generates platform UUID based on it's name
1035 Generates platform UUID based on it's name
1035 """
1036 """
1036 import platform
1037 import platform
1037
1038
1038 try:
1039 try:
1039 uuid_list = [platform.platform()]
1040 uuid_list = [platform.platform()]
1040 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
1041 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
1041 except Exception as e:
1042 except Exception as e:
1042 log.error('Failed to generate host uuid: %s' % e)
1043 log.error('Failed to generate host uuid: %s' % e)
1043 return 'UNDEFINED'
1044 return 'UNDEFINED'
@@ -1,962 +1,963 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26
26
27 import collections
27 import collections
28 import datetime
28 import datetime
29 import dateutil.relativedelta
29 import dateutil.relativedelta
30 import hashlib
30 import hashlib
31 import logging
31 import logging
32 import re
32 import re
33 import sys
33 import sys
34 import time
34 import time
35 import threading
35 import threading
36 import urllib
36 import urllib
37 import urlobject
37 import urlobject
38 import uuid
38 import uuid
39
39
40 import pygments.lexers
40 import pygments.lexers
41 import sqlalchemy
41 import sqlalchemy
42 import sqlalchemy.engine.url
42 import sqlalchemy.engine.url
43 import webob
43 import webob
44 import routes.util
44 import routes.util
45
45
46 import rhodecode
46 import rhodecode
47 from rhodecode.translation import _, _pluralize
47 from rhodecode.translation import _, _pluralize
48
48
49
49
50 def md5(s):
50 def md5(s):
51 return hashlib.md5(s).hexdigest()
51 return hashlib.md5(s).hexdigest()
52
52
53
53
54 def md5_safe(s):
54 def md5_safe(s):
55 return md5(safe_str(s))
55 return md5(safe_str(s))
56
56
57
57
58 def __get_lem(extra_mapping=None):
58 def __get_lem(extra_mapping=None):
59 """
59 """
60 Get language extension map based on what's inside pygments lexers
60 Get language extension map based on what's inside pygments lexers
61 """
61 """
62 d = collections.defaultdict(lambda: [])
62 d = collections.defaultdict(lambda: [])
63
63
64 def __clean(s):
64 def __clean(s):
65 s = s.lstrip('*')
65 s = s.lstrip('*')
66 s = s.lstrip('.')
66 s = s.lstrip('.')
67
67
68 if s.find('[') != -1:
68 if s.find('[') != -1:
69 exts = []
69 exts = []
70 start, stop = s.find('['), s.find(']')
70 start, stop = s.find('['), s.find(']')
71
71
72 for suffix in s[start + 1:stop]:
72 for suffix in s[start + 1:stop]:
73 exts.append(s[:s.find('[')] + suffix)
73 exts.append(s[:s.find('[')] + suffix)
74 return [e.lower() for e in exts]
74 return [e.lower() for e in exts]
75 else:
75 else:
76 return [s.lower()]
76 return [s.lower()]
77
77
78 for lx, t in sorted(pygments.lexers.LEXERS.items()):
78 for lx, t in sorted(pygments.lexers.LEXERS.items()):
79 m = map(__clean, t[-2])
79 m = map(__clean, t[-2])
80 if m:
80 if m:
81 m = reduce(lambda x, y: x + y, m)
81 m = reduce(lambda x, y: x + y, m)
82 for ext in m:
82 for ext in m:
83 desc = lx.replace('Lexer', '')
83 desc = lx.replace('Lexer', '')
84 d[ext].append(desc)
84 d[ext].append(desc)
85
85
86 data = dict(d)
86 data = dict(d)
87
87
88 extra_mapping = extra_mapping or {}
88 extra_mapping = extra_mapping or {}
89 if extra_mapping:
89 if extra_mapping:
90 for k, v in extra_mapping.items():
90 for k, v in extra_mapping.items():
91 if k not in data:
91 if k not in data:
92 # register new mapping2lexer
92 # register new mapping2lexer
93 data[k] = [v]
93 data[k] = [v]
94
94
95 return data
95 return data
96
96
97
97
98 def str2bool(_str):
98 def str2bool(_str):
99 """
99 """
100 returns True/False value from given string, it tries to translate the
100 returns True/False value from given string, it tries to translate the
101 string into boolean
101 string into boolean
102
102
103 :param _str: string value to translate into boolean
103 :param _str: string value to translate into boolean
104 :rtype: boolean
104 :rtype: boolean
105 :returns: boolean from given string
105 :returns: boolean from given string
106 """
106 """
107 if _str is None:
107 if _str is None:
108 return False
108 return False
109 if _str in (True, False):
109 if _str in (True, False):
110 return _str
110 return _str
111 _str = str(_str).strip().lower()
111 _str = str(_str).strip().lower()
112 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
112 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
113
113
114
114
115 def aslist(obj, sep=None, strip=True):
115 def aslist(obj, sep=None, strip=True):
116 """
116 """
117 Returns given string separated by sep as list
117 Returns given string separated by sep as list
118
118
119 :param obj:
119 :param obj:
120 :param sep:
120 :param sep:
121 :param strip:
121 :param strip:
122 """
122 """
123 if isinstance(obj, (basestring,)):
123 if isinstance(obj, (basestring,)):
124 lst = obj.split(sep)
124 lst = obj.split(sep)
125 if strip:
125 if strip:
126 lst = [v.strip() for v in lst]
126 lst = [v.strip() for v in lst]
127 return lst
127 return lst
128 elif isinstance(obj, (list, tuple)):
128 elif isinstance(obj, (list, tuple)):
129 return obj
129 return obj
130 elif obj is None:
130 elif obj is None:
131 return []
131 return []
132 else:
132 else:
133 return [obj]
133 return [obj]
134
134
135
135
136 def convert_line_endings(line, mode):
136 def convert_line_endings(line, mode):
137 """
137 """
138 Converts a given line "line end" accordingly to given mode
138 Converts a given line "line end" accordingly to given mode
139
139
140 Available modes are::
140 Available modes are::
141 0 - Unix
141 0 - Unix
142 1 - Mac
142 1 - Mac
143 2 - DOS
143 2 - DOS
144
144
145 :param line: given line to convert
145 :param line: given line to convert
146 :param mode: mode to convert to
146 :param mode: mode to convert to
147 :rtype: str
147 :rtype: str
148 :return: converted line according to mode
148 :return: converted line according to mode
149 """
149 """
150 if mode == 0:
150 if mode == 0:
151 line = line.replace('\r\n', '\n')
151 line = line.replace('\r\n', '\n')
152 line = line.replace('\r', '\n')
152 line = line.replace('\r', '\n')
153 elif mode == 1:
153 elif mode == 1:
154 line = line.replace('\r\n', '\r')
154 line = line.replace('\r\n', '\r')
155 line = line.replace('\n', '\r')
155 line = line.replace('\n', '\r')
156 elif mode == 2:
156 elif mode == 2:
157 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
157 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
158 return line
158 return line
159
159
160
160
161 def detect_mode(line, default):
161 def detect_mode(line, default):
162 """
162 """
163 Detects line break for given line, if line break couldn't be found
163 Detects line break for given line, if line break couldn't be found
164 given default value is returned
164 given default value is returned
165
165
166 :param line: str line
166 :param line: str line
167 :param default: default
167 :param default: default
168 :rtype: int
168 :rtype: int
169 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
169 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
170 """
170 """
171 if line.endswith('\r\n'):
171 if line.endswith('\r\n'):
172 return 2
172 return 2
173 elif line.endswith('\n'):
173 elif line.endswith('\n'):
174 return 0
174 return 0
175 elif line.endswith('\r'):
175 elif line.endswith('\r'):
176 return 1
176 return 1
177 else:
177 else:
178 return default
178 return default
179
179
180
180
181 def safe_int(val, default=None):
181 def safe_int(val, default=None):
182 """
182 """
183 Returns int() of val if val is not convertable to int use default
183 Returns int() of val if val is not convertable to int use default
184 instead
184 instead
185
185
186 :param val:
186 :param val:
187 :param default:
187 :param default:
188 """
188 """
189
189
190 try:
190 try:
191 val = int(val)
191 val = int(val)
192 except (ValueError, TypeError):
192 except (ValueError, TypeError):
193 val = default
193 val = default
194
194
195 return val
195 return val
196
196
197
197
198 def safe_unicode(str_, from_encoding=None):
198 def safe_unicode(str_, from_encoding=None):
199 """
199 """
200 safe unicode function. Does few trick to turn str_ into unicode
200 safe unicode function. Does few trick to turn str_ into unicode
201
201
202 In case of UnicodeDecode error, we try to return it with encoding detected
202 In case of UnicodeDecode error, we try to return it with encoding detected
203 by chardet library if it fails fallback to unicode with errors replaced
203 by chardet library if it fails fallback to unicode with errors replaced
204
204
205 :param str_: string to decode
205 :param str_: string to decode
206 :rtype: unicode
206 :rtype: unicode
207 :returns: unicode object
207 :returns: unicode object
208 """
208 """
209 if isinstance(str_, unicode):
209 if isinstance(str_, unicode):
210 return str_
210 return str_
211
211
212 if not from_encoding:
212 if not from_encoding:
213 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
213 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
214 'utf8'), sep=',')
214 'utf8'), sep=',')
215 from_encoding = DEFAULT_ENCODINGS
215 from_encoding = DEFAULT_ENCODINGS
216
216
217 if not isinstance(from_encoding, (list, tuple)):
217 if not isinstance(from_encoding, (list, tuple)):
218 from_encoding = [from_encoding]
218 from_encoding = [from_encoding]
219
219
220 try:
220 try:
221 return unicode(str_)
221 return unicode(str_)
222 except UnicodeDecodeError:
222 except UnicodeDecodeError:
223 pass
223 pass
224
224
225 for enc in from_encoding:
225 for enc in from_encoding:
226 try:
226 try:
227 return unicode(str_, enc)
227 return unicode(str_, enc)
228 except UnicodeDecodeError:
228 except UnicodeDecodeError:
229 pass
229 pass
230
230
231 try:
231 try:
232 import chardet
232 import chardet
233 encoding = chardet.detect(str_)['encoding']
233 encoding = chardet.detect(str_)['encoding']
234 if encoding is None:
234 if encoding is None:
235 raise Exception()
235 raise Exception()
236 return str_.decode(encoding)
236 return str_.decode(encoding)
237 except (ImportError, UnicodeDecodeError, Exception):
237 except (ImportError, UnicodeDecodeError, Exception):
238 return unicode(str_, from_encoding[0], 'replace')
238 return unicode(str_, from_encoding[0], 'replace')
239
239
240
240
241 def safe_str(unicode_, to_encoding=None):
241 def safe_str(unicode_, to_encoding=None):
242 """
242 """
243 safe str function. Does few trick to turn unicode_ into string
243 safe str function. Does few trick to turn unicode_ into string
244
244
245 In case of UnicodeEncodeError, we try to return it with encoding detected
245 In case of UnicodeEncodeError, we try to return it with encoding detected
246 by chardet library if it fails fallback to string with errors replaced
246 by chardet library if it fails fallback to string with errors replaced
247
247
248 :param unicode_: unicode to encode
248 :param unicode_: unicode to encode
249 :rtype: str
249 :rtype: str
250 :returns: str object
250 :returns: str object
251 """
251 """
252
252
253 # if it's not basestr cast to str
253 # if it's not basestr cast to str
254 if not isinstance(unicode_, basestring):
254 if not isinstance(unicode_, basestring):
255 return str(unicode_)
255 return str(unicode_)
256
256
257 if isinstance(unicode_, str):
257 if isinstance(unicode_, str):
258 return unicode_
258 return unicode_
259
259
260 if not to_encoding:
260 if not to_encoding:
261 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
262 'utf8'), sep=',')
262 'utf8'), sep=',')
263 to_encoding = DEFAULT_ENCODINGS
263 to_encoding = DEFAULT_ENCODINGS
264
264
265 if not isinstance(to_encoding, (list, tuple)):
265 if not isinstance(to_encoding, (list, tuple)):
266 to_encoding = [to_encoding]
266 to_encoding = [to_encoding]
267
267
268 for enc in to_encoding:
268 for enc in to_encoding:
269 try:
269 try:
270 return unicode_.encode(enc)
270 return unicode_.encode(enc)
271 except UnicodeEncodeError:
271 except UnicodeEncodeError:
272 pass
272 pass
273
273
274 try:
274 try:
275 import chardet
275 import chardet
276 encoding = chardet.detect(unicode_)['encoding']
276 encoding = chardet.detect(unicode_)['encoding']
277 if encoding is None:
277 if encoding is None:
278 raise UnicodeEncodeError()
278 raise UnicodeEncodeError()
279
279
280 return unicode_.encode(encoding)
280 return unicode_.encode(encoding)
281 except (ImportError, UnicodeEncodeError):
281 except (ImportError, UnicodeEncodeError):
282 return unicode_.encode(to_encoding[0], 'replace')
282 return unicode_.encode(to_encoding[0], 'replace')
283
283
284
284
285 def remove_suffix(s, suffix):
285 def remove_suffix(s, suffix):
286 if s.endswith(suffix):
286 if s.endswith(suffix):
287 s = s[:-1 * len(suffix)]
287 s = s[:-1 * len(suffix)]
288 return s
288 return s
289
289
290
290
291 def remove_prefix(s, prefix):
291 def remove_prefix(s, prefix):
292 if s.startswith(prefix):
292 if s.startswith(prefix):
293 s = s[len(prefix):]
293 s = s[len(prefix):]
294 return s
294 return s
295
295
296
296
297 def find_calling_context(ignore_modules=None):
297 def find_calling_context(ignore_modules=None):
298 """
298 """
299 Look through the calling stack and return the frame which called
299 Look through the calling stack and return the frame which called
300 this function and is part of core module ( ie. rhodecode.* )
300 this function and is part of core module ( ie. rhodecode.* )
301
301
302 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
302 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
303 """
303 """
304
304
305 ignore_modules = ignore_modules or []
305 ignore_modules = ignore_modules or []
306
306
307 f = sys._getframe(2)
307 f = sys._getframe(2)
308 while f.f_back is not None:
308 while f.f_back is not None:
309 name = f.f_globals.get('__name__')
309 name = f.f_globals.get('__name__')
310 if name and name.startswith(__name__.split('.')[0]):
310 if name and name.startswith(__name__.split('.')[0]):
311 if name not in ignore_modules:
311 if name not in ignore_modules:
312 return f
312 return f
313 f = f.f_back
313 f = f.f_back
314 return None
314 return None
315
315
316
316
317 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
317 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
318 """Custom engine_from_config functions."""
318 """Custom engine_from_config functions."""
319 log = logging.getLogger('sqlalchemy.engine')
319 log = logging.getLogger('sqlalchemy.engine')
320 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
320 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
321
321
322 def color_sql(sql):
322 def color_sql(sql):
323 color_seq = '\033[1;33m' # This is yellow: code 33
323 color_seq = '\033[1;33m' # This is yellow: code 33
324 normal = '\x1b[0m'
324 normal = '\x1b[0m'
325 return ''.join([color_seq, sql, normal])
325 return ''.join([color_seq, sql, normal])
326
326
327 if configuration['debug']:
327 if configuration['debug']:
328 # attach events only for debug configuration
328 # attach events only for debug configuration
329
329
330 def before_cursor_execute(conn, cursor, statement,
330 def before_cursor_execute(conn, cursor, statement,
331 parameters, context, executemany):
331 parameters, context, executemany):
332 setattr(conn, 'query_start_time', time.time())
332 setattr(conn, 'query_start_time', time.time())
333 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
333 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
334 calling_context = find_calling_context(ignore_modules=[
334 calling_context = find_calling_context(ignore_modules=[
335 'rhodecode.lib.caching_query',
335 'rhodecode.lib.caching_query',
336 'rhodecode.model.settings',
336 'rhodecode.model.settings',
337 ])
337 ])
338 if calling_context:
338 if calling_context:
339 log.info(color_sql('call context %s:%s' % (
339 log.info(color_sql('call context %s:%s' % (
340 calling_context.f_code.co_filename,
340 calling_context.f_code.co_filename,
341 calling_context.f_lineno,
341 calling_context.f_lineno,
342 )))
342 )))
343
343
344 def after_cursor_execute(conn, cursor, statement,
344 def after_cursor_execute(conn, cursor, statement,
345 parameters, context, executemany):
345 parameters, context, executemany):
346 delattr(conn, 'query_start_time')
346 delattr(conn, 'query_start_time')
347
347
348 sqlalchemy.event.listen(engine, "before_cursor_execute",
348 sqlalchemy.event.listen(engine, "before_cursor_execute",
349 before_cursor_execute)
349 before_cursor_execute)
350 sqlalchemy.event.listen(engine, "after_cursor_execute",
350 sqlalchemy.event.listen(engine, "after_cursor_execute",
351 after_cursor_execute)
351 after_cursor_execute)
352
352
353 return engine
353 return engine
354
354
355
355
356 def get_encryption_key(config):
356 def get_encryption_key(config):
357 secret = config.get('rhodecode.encrypted_values.secret')
357 secret = config.get('rhodecode.encrypted_values.secret')
358 default = config['beaker.session.secret']
358 default = config['beaker.session.secret']
359 return secret or default
359 return secret or default
360
360
361
361
362 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
362 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
363 short_format=False):
363 short_format=False):
364 """
364 """
365 Turns a datetime into an age string.
365 Turns a datetime into an age string.
366 If show_short_version is True, this generates a shorter string with
366 If show_short_version is True, this generates a shorter string with
367 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
367 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
368
368
369 * IMPORTANT*
369 * IMPORTANT*
370 Code of this function is written in special way so it's easier to
370 Code of this function is written in special way so it's easier to
371 backport it to javascript. If you mean to update it, please also update
371 backport it to javascript. If you mean to update it, please also update
372 `jquery.timeago-extension.js` file
372 `jquery.timeago-extension.js` file
373
373
374 :param prevdate: datetime object
374 :param prevdate: datetime object
375 :param now: get current time, if not define we use
375 :param now: get current time, if not define we use
376 `datetime.datetime.now()`
376 `datetime.datetime.now()`
377 :param show_short_version: if it should approximate the date and
377 :param show_short_version: if it should approximate the date and
378 return a shorter string
378 return a shorter string
379 :param show_suffix:
379 :param show_suffix:
380 :param short_format: show short format, eg 2D instead of 2 days
380 :param short_format: show short format, eg 2D instead of 2 days
381 :rtype: unicode
381 :rtype: unicode
382 :returns: unicode words describing age
382 :returns: unicode words describing age
383 """
383 """
384
384
385 def _get_relative_delta(now, prevdate):
385 def _get_relative_delta(now, prevdate):
386 base = dateutil.relativedelta.relativedelta(now, prevdate)
386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 return {
387 return {
388 'year': base.years,
388 'year': base.years,
389 'month': base.months,
389 'month': base.months,
390 'day': base.days,
390 'day': base.days,
391 'hour': base.hours,
391 'hour': base.hours,
392 'minute': base.minutes,
392 'minute': base.minutes,
393 'second': base.seconds,
393 'second': base.seconds,
394 }
394 }
395
395
396 def _is_leap_year(year):
396 def _is_leap_year(year):
397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398
398
399 def get_month(prevdate):
399 def get_month(prevdate):
400 return prevdate.month
400 return prevdate.month
401
401
402 def get_year(prevdate):
402 def get_year(prevdate):
403 return prevdate.year
403 return prevdate.year
404
404
405 now = now or datetime.datetime.now()
405 now = now or datetime.datetime.now()
406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 deltas = {}
407 deltas = {}
408 future = False
408 future = False
409
409
410 if prevdate > now:
410 if prevdate > now:
411 now_old = now
411 now_old = now
412 now = prevdate
412 now = prevdate
413 prevdate = now_old
413 prevdate = now_old
414 future = True
414 future = True
415 if future:
415 if future:
416 prevdate = prevdate.replace(microsecond=0)
416 prevdate = prevdate.replace(microsecond=0)
417 # Get date parts deltas
417 # Get date parts deltas
418 for part in order:
418 for part in order:
419 rel_delta = _get_relative_delta(now, prevdate)
419 rel_delta = _get_relative_delta(now, prevdate)
420 deltas[part] = rel_delta[part]
420 deltas[part] = rel_delta[part]
421
421
422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 # not 1 hour, -59 minutes and -59 seconds)
423 # not 1 hour, -59 minutes and -59 seconds)
424 offsets = [[5, 60], [4, 60], [3, 24]]
424 offsets = [[5, 60], [4, 60], [3, 24]]
425 for element in offsets: # seconds, minutes, hours
425 for element in offsets: # seconds, minutes, hours
426 num = element[0]
426 num = element[0]
427 length = element[1]
427 length = element[1]
428
428
429 part = order[num]
429 part = order[num]
430 carry_part = order[num - 1]
430 carry_part = order[num - 1]
431
431
432 if deltas[part] < 0:
432 if deltas[part] < 0:
433 deltas[part] += length
433 deltas[part] += length
434 deltas[carry_part] -= 1
434 deltas[carry_part] -= 1
435
435
436 # Same thing for days except that the increment depends on the (variable)
436 # Same thing for days except that the increment depends on the (variable)
437 # number of days in the month
437 # number of days in the month
438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 if deltas['day'] < 0:
439 if deltas['day'] < 0:
440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 deltas['day'] += 29
441 deltas['day'] += 29
442 else:
442 else:
443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444
444
445 deltas['month'] -= 1
445 deltas['month'] -= 1
446
446
447 if deltas['month'] < 0:
447 if deltas['month'] < 0:
448 deltas['month'] += 12
448 deltas['month'] += 12
449 deltas['year'] -= 1
449 deltas['year'] -= 1
450
450
451 # Format the result
451 # Format the result
452 if short_format:
452 if short_format:
453 fmt_funcs = {
453 fmt_funcs = {
454 'year': lambda d: u'%dy' % d,
454 'year': lambda d: u'%dy' % d,
455 'month': lambda d: u'%dm' % d,
455 'month': lambda d: u'%dm' % d,
456 'day': lambda d: u'%dd' % d,
456 'day': lambda d: u'%dd' % d,
457 'hour': lambda d: u'%dh' % d,
457 'hour': lambda d: u'%dh' % d,
458 'minute': lambda d: u'%dmin' % d,
458 'minute': lambda d: u'%dmin' % d,
459 'second': lambda d: u'%dsec' % d,
459 'second': lambda d: u'%dsec' % d,
460 }
460 }
461 else:
461 else:
462 fmt_funcs = {
462 fmt_funcs = {
463 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
463 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
464 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
464 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
465 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
465 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
466 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
466 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
467 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
467 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
468 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
468 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
469 }
469 }
470
470
471 i = 0
471 i = 0
472 for part in order:
472 for part in order:
473 value = deltas[part]
473 value = deltas[part]
474 if value != 0:
474 if value != 0:
475
475
476 if i < 5:
476 if i < 5:
477 sub_part = order[i + 1]
477 sub_part = order[i + 1]
478 sub_value = deltas[sub_part]
478 sub_value = deltas[sub_part]
479 else:
479 else:
480 sub_value = 0
480 sub_value = 0
481
481
482 if sub_value == 0 or show_short_version:
482 if sub_value == 0 or show_short_version:
483 _val = fmt_funcs[part](value)
483 _val = fmt_funcs[part](value)
484 if future:
484 if future:
485 if show_suffix:
485 if show_suffix:
486 return _(u'in ${ago}', mapping={'ago': _val})
486 return _(u'in ${ago}', mapping={'ago': _val})
487 else:
487 else:
488 return _(_val)
488 return _(_val)
489
489
490 else:
490 else:
491 if show_suffix:
491 if show_suffix:
492 return _(u'${ago} ago', mapping={'ago': _val})
492 return _(u'${ago} ago', mapping={'ago': _val})
493 else:
493 else:
494 return _(_val)
494 return _(_val)
495
495
496 val = fmt_funcs[part](value)
496 val = fmt_funcs[part](value)
497 val_detail = fmt_funcs[sub_part](sub_value)
497 val_detail = fmt_funcs[sub_part](sub_value)
498 mapping = {'val': val, 'detail': val_detail}
498 mapping = {'val': val, 'detail': val_detail}
499
499
500 if short_format:
500 if short_format:
501 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
501 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
502 if show_suffix:
502 if show_suffix:
503 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
503 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
504 if future:
504 if future:
505 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
505 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
506 else:
506 else:
507 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
507 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
508 if show_suffix:
508 if show_suffix:
509 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
509 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
510 if future:
510 if future:
511 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
511 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
512
512
513 return datetime_tmpl
513 return datetime_tmpl
514 i += 1
514 i += 1
515 return _(u'just now')
515 return _(u'just now')
516
516
517
517
518 def cleaned_uri(uri):
518 def cleaned_uri(uri):
519 """
519 """
520 Quotes '[' and ']' from uri if there is only one of them.
520 Quotes '[' and ']' from uri if there is only one of them.
521 according to RFC3986 we cannot use such chars in uri
521 according to RFC3986 we cannot use such chars in uri
522 :param uri:
522 :param uri:
523 :return: uri without this chars
523 :return: uri without this chars
524 """
524 """
525 return urllib.quote(uri, safe='@$:/')
525 return urllib.quote(uri, safe='@$:/')
526
526
527
527
528 def uri_filter(uri):
528 def uri_filter(uri):
529 """
529 """
530 Removes user:password from given url string
530 Removes user:password from given url string
531
531
532 :param uri:
532 :param uri:
533 :rtype: unicode
533 :rtype: unicode
534 :returns: filtered list of strings
534 :returns: filtered list of strings
535 """
535 """
536 if not uri:
536 if not uri:
537 return ''
537 return ''
538
538
539 proto = ''
539 proto = ''
540
540
541 for pat in ('https://', 'http://'):
541 for pat in ('https://', 'http://'):
542 if uri.startswith(pat):
542 if uri.startswith(pat):
543 uri = uri[len(pat):]
543 uri = uri[len(pat):]
544 proto = pat
544 proto = pat
545 break
545 break
546
546
547 # remove passwords and username
547 # remove passwords and username
548 uri = uri[uri.find('@') + 1:]
548 uri = uri[uri.find('@') + 1:]
549
549
550 # get the port
550 # get the port
551 cred_pos = uri.find(':')
551 cred_pos = uri.find(':')
552 if cred_pos == -1:
552 if cred_pos == -1:
553 host, port = uri, None
553 host, port = uri, None
554 else:
554 else:
555 host, port = uri[:cred_pos], uri[cred_pos + 1:]
555 host, port = uri[:cred_pos], uri[cred_pos + 1:]
556
556
557 return filter(None, [proto, host, port])
557 return filter(None, [proto, host, port])
558
558
559
559
560 def credentials_filter(uri):
560 def credentials_filter(uri):
561 """
561 """
562 Returns a url with removed credentials
562 Returns a url with removed credentials
563
563
564 :param uri:
564 :param uri:
565 """
565 """
566
566
567 uri = uri_filter(uri)
567 uri = uri_filter(uri)
568 # check if we have port
568 # check if we have port
569 if len(uri) > 2 and uri[2]:
569 if len(uri) > 2 and uri[2]:
570 uri[2] = ':' + uri[2]
570 uri[2] = ':' + uri[2]
571
571
572 return ''.join(uri)
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 parsed_url = urlobject.URLObject(qualifed_home_url)
577 parsed_url = urlobject.URLObject(qualifed_home_url)
577 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
578 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
578 args = {
579 args = {
579 'scheme': parsed_url.scheme,
580 'scheme': parsed_url.scheme,
580 'user': '',
581 'user': '',
581 # path if we use proxy-prefix
582 # path if we use proxy-prefix
582 'netloc': parsed_url.netloc+decoded_path,
583 'netloc': parsed_url.netloc+decoded_path,
583 'prefix': decoded_path,
584 'prefix': decoded_path,
584 'repo': repo_name,
585 'repo': repo_name,
585 'repoid': str(repo_id)
586 'repoid': str(repo_id)
586 }
587 }
587 args.update(override)
588 args.update(override)
588 args['user'] = urllib.quote(safe_str(args['user']))
589 args['user'] = urllib.quote(safe_str(args['user']))
589
590
590 for k, v in args.items():
591 for k, v in args.items():
591 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
592 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
592
593
593 # remove leading @ sign if it's present. Case of empty user
594 # remove leading @ sign if it's present. Case of empty user
594 url_obj = urlobject.URLObject(uri_tmpl)
595 url_obj = urlobject.URLObject(uri_tmpl)
595 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
596 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
596
597
597 return safe_unicode(url)
598 return safe_unicode(url)
598
599
599
600
600 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
601 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
601 """
602 """
602 Safe version of get_commit if this commit doesn't exists for a
603 Safe version of get_commit if this commit doesn't exists for a
603 repository it returns a Dummy one instead
604 repository it returns a Dummy one instead
604
605
605 :param repo: repository instance
606 :param repo: repository instance
606 :param commit_id: commit id as str
607 :param commit_id: commit id as str
607 :param pre_load: optional list of commit attributes to load
608 :param pre_load: optional list of commit attributes to load
608 """
609 """
609 # TODO(skreft): remove these circular imports
610 # TODO(skreft): remove these circular imports
610 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
611 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
611 from rhodecode.lib.vcs.exceptions import RepositoryError
612 from rhodecode.lib.vcs.exceptions import RepositoryError
612 if not isinstance(repo, BaseRepository):
613 if not isinstance(repo, BaseRepository):
613 raise Exception('You must pass an Repository '
614 raise Exception('You must pass an Repository '
614 'object as first argument got %s', type(repo))
615 'object as first argument got %s', type(repo))
615
616
616 try:
617 try:
617 commit = repo.get_commit(
618 commit = repo.get_commit(
618 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
619 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
619 except (RepositoryError, LookupError):
620 except (RepositoryError, LookupError):
620 commit = EmptyCommit()
621 commit = EmptyCommit()
621 return commit
622 return commit
622
623
623
624
624 def datetime_to_time(dt):
625 def datetime_to_time(dt):
625 if dt:
626 if dt:
626 return time.mktime(dt.timetuple())
627 return time.mktime(dt.timetuple())
627
628
628
629
629 def time_to_datetime(tm):
630 def time_to_datetime(tm):
630 if tm:
631 if tm:
631 if isinstance(tm, basestring):
632 if isinstance(tm, basestring):
632 try:
633 try:
633 tm = float(tm)
634 tm = float(tm)
634 except ValueError:
635 except ValueError:
635 return
636 return
636 return datetime.datetime.fromtimestamp(tm)
637 return datetime.datetime.fromtimestamp(tm)
637
638
638
639
639 def time_to_utcdatetime(tm):
640 def time_to_utcdatetime(tm):
640 if tm:
641 if tm:
641 if isinstance(tm, basestring):
642 if isinstance(tm, basestring):
642 try:
643 try:
643 tm = float(tm)
644 tm = float(tm)
644 except ValueError:
645 except ValueError:
645 return
646 return
646 return datetime.datetime.utcfromtimestamp(tm)
647 return datetime.datetime.utcfromtimestamp(tm)
647
648
648
649
649 MENTIONS_REGEX = re.compile(
650 MENTIONS_REGEX = re.compile(
650 # ^@ or @ without any special chars in front
651 # ^@ or @ without any special chars in front
651 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
652 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
652 # main body starts with letter, then can be . - _
653 # main body starts with letter, then can be . - _
653 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
654 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
654 re.VERBOSE | re.MULTILINE)
655 re.VERBOSE | re.MULTILINE)
655
656
656
657
657 def extract_mentioned_users(s):
658 def extract_mentioned_users(s):
658 """
659 """
659 Returns unique usernames from given string s that have @mention
660 Returns unique usernames from given string s that have @mention
660
661
661 :param s: string to get mentions
662 :param s: string to get mentions
662 """
663 """
663 usrs = set()
664 usrs = set()
664 for username in MENTIONS_REGEX.findall(s):
665 for username in MENTIONS_REGEX.findall(s):
665 usrs.add(username)
666 usrs.add(username)
666
667
667 return sorted(list(usrs), key=lambda k: k.lower())
668 return sorted(list(usrs), key=lambda k: k.lower())
668
669
669
670
670 class StrictAttributeDict(dict):
671 class StrictAttributeDict(dict):
671 """
672 """
672 Strict Version of Attribute dict which raises an Attribute error when
673 Strict Version of Attribute dict which raises an Attribute error when
673 requested attribute is not set
674 requested attribute is not set
674 """
675 """
675 def __getattr__(self, attr):
676 def __getattr__(self, attr):
676 try:
677 try:
677 return self[attr]
678 return self[attr]
678 except KeyError:
679 except KeyError:
679 raise AttributeError('%s object has no attribute %s' % (
680 raise AttributeError('%s object has no attribute %s' % (
680 self.__class__, attr))
681 self.__class__, attr))
681 __setattr__ = dict.__setitem__
682 __setattr__ = dict.__setitem__
682 __delattr__ = dict.__delitem__
683 __delattr__ = dict.__delitem__
683
684
684
685
685 class AttributeDict(dict):
686 class AttributeDict(dict):
686 def __getattr__(self, attr):
687 def __getattr__(self, attr):
687 return self.get(attr, None)
688 return self.get(attr, None)
688 __setattr__ = dict.__setitem__
689 __setattr__ = dict.__setitem__
689 __delattr__ = dict.__delitem__
690 __delattr__ = dict.__delitem__
690
691
691
692
692 def fix_PATH(os_=None):
693 def fix_PATH(os_=None):
693 """
694 """
694 Get current active python path, and append it to PATH variable to fix
695 Get current active python path, and append it to PATH variable to fix
695 issues of subprocess calls and different python versions
696 issues of subprocess calls and different python versions
696 """
697 """
697 if os_ is None:
698 if os_ is None:
698 import os
699 import os
699 else:
700 else:
700 os = os_
701 os = os_
701
702
702 cur_path = os.path.split(sys.executable)[0]
703 cur_path = os.path.split(sys.executable)[0]
703 if not os.environ['PATH'].startswith(cur_path):
704 if not os.environ['PATH'].startswith(cur_path):
704 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
705 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
705
706
706
707
707 def obfuscate_url_pw(engine):
708 def obfuscate_url_pw(engine):
708 _url = engine or ''
709 _url = engine or ''
709 try:
710 try:
710 _url = sqlalchemy.engine.url.make_url(engine)
711 _url = sqlalchemy.engine.url.make_url(engine)
711 if _url.password:
712 if _url.password:
712 _url.password = 'XXXXX'
713 _url.password = 'XXXXX'
713 except Exception:
714 except Exception:
714 pass
715 pass
715 return unicode(_url)
716 return unicode(_url)
716
717
717
718
718 def get_server_url(environ):
719 def get_server_url(environ):
719 req = webob.Request(environ)
720 req = webob.Request(environ)
720 return req.host_url + req.script_name
721 return req.host_url + req.script_name
721
722
722
723
723 def unique_id(hexlen=32):
724 def unique_id(hexlen=32):
724 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
725 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
725 return suuid(truncate_to=hexlen, alphabet=alphabet)
726 return suuid(truncate_to=hexlen, alphabet=alphabet)
726
727
727
728
728 def suuid(url=None, truncate_to=22, alphabet=None):
729 def suuid(url=None, truncate_to=22, alphabet=None):
729 """
730 """
730 Generate and return a short URL safe UUID.
731 Generate and return a short URL safe UUID.
731
732
732 If the url parameter is provided, set the namespace to the provided
733 If the url parameter is provided, set the namespace to the provided
733 URL and generate a UUID.
734 URL and generate a UUID.
734
735
735 :param url to get the uuid for
736 :param url to get the uuid for
736 :truncate_to: truncate the basic 22 UUID to shorter version
737 :truncate_to: truncate the basic 22 UUID to shorter version
737
738
738 The IDs won't be universally unique any longer, but the probability of
739 The IDs won't be universally unique any longer, but the probability of
739 a collision will still be very low.
740 a collision will still be very low.
740 """
741 """
741 # Define our alphabet.
742 # Define our alphabet.
742 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
743 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
743
744
744 # If no URL is given, generate a random UUID.
745 # If no URL is given, generate a random UUID.
745 if url is None:
746 if url is None:
746 unique_id = uuid.uuid4().int
747 unique_id = uuid.uuid4().int
747 else:
748 else:
748 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
749 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
749
750
750 alphabet_length = len(_ALPHABET)
751 alphabet_length = len(_ALPHABET)
751 output = []
752 output = []
752 while unique_id > 0:
753 while unique_id > 0:
753 digit = unique_id % alphabet_length
754 digit = unique_id % alphabet_length
754 output.append(_ALPHABET[digit])
755 output.append(_ALPHABET[digit])
755 unique_id = int(unique_id / alphabet_length)
756 unique_id = int(unique_id / alphabet_length)
756 return "".join(output)[:truncate_to]
757 return "".join(output)[:truncate_to]
757
758
758
759
759 def get_current_rhodecode_user():
760 def get_current_rhodecode_user():
760 """
761 """
761 Gets rhodecode user from threadlocal tmpl_context variable if it's
762 Gets rhodecode user from threadlocal tmpl_context variable if it's
762 defined, else returns None.
763 defined, else returns None.
763 """
764 """
764 from pylons import tmpl_context as c
765 from pylons import tmpl_context as c
765 if hasattr(c, 'rhodecode_user'):
766 if hasattr(c, 'rhodecode_user'):
766 return c.rhodecode_user
767 return c.rhodecode_user
767
768
768 return None
769 return None
769
770
770
771
771 def action_logger_generic(action, namespace=''):
772 def action_logger_generic(action, namespace=''):
772 """
773 """
773 A generic logger for actions useful to the system overview, tries to find
774 A generic logger for actions useful to the system overview, tries to find
774 an acting user for the context of the call otherwise reports unknown user
775 an acting user for the context of the call otherwise reports unknown user
775
776
776 :param action: logging message eg 'comment 5 deleted'
777 :param action: logging message eg 'comment 5 deleted'
777 :param type: string
778 :param type: string
778
779
779 :param namespace: namespace of the logging message eg. 'repo.comments'
780 :param namespace: namespace of the logging message eg. 'repo.comments'
780 :param type: string
781 :param type: string
781
782
782 """
783 """
783
784
784 logger_name = 'rhodecode.actions'
785 logger_name = 'rhodecode.actions'
785
786
786 if namespace:
787 if namespace:
787 logger_name += '.' + namespace
788 logger_name += '.' + namespace
788
789
789 log = logging.getLogger(logger_name)
790 log = logging.getLogger(logger_name)
790
791
791 # get a user if we can
792 # get a user if we can
792 user = get_current_rhodecode_user()
793 user = get_current_rhodecode_user()
793
794
794 logfunc = log.info
795 logfunc = log.info
795
796
796 if not user:
797 if not user:
797 user = '<unknown user>'
798 user = '<unknown user>'
798 logfunc = log.warning
799 logfunc = log.warning
799
800
800 logfunc('Logging action by {}: {}'.format(user, action))
801 logfunc('Logging action by {}: {}'.format(user, action))
801
802
802
803
803 def escape_split(text, sep=',', maxsplit=-1):
804 def escape_split(text, sep=',', maxsplit=-1):
804 r"""
805 r"""
805 Allows for escaping of the separator: e.g. arg='foo\, bar'
806 Allows for escaping of the separator: e.g. arg='foo\, bar'
806
807
807 It should be noted that the way bash et. al. do command line parsing, those
808 It should be noted that the way bash et. al. do command line parsing, those
808 single quotes are required.
809 single quotes are required.
809 """
810 """
810 escaped_sep = r'\%s' % sep
811 escaped_sep = r'\%s' % sep
811
812
812 if escaped_sep not in text:
813 if escaped_sep not in text:
813 return text.split(sep, maxsplit)
814 return text.split(sep, maxsplit)
814
815
815 before, _mid, after = text.partition(escaped_sep)
816 before, _mid, after = text.partition(escaped_sep)
816 startlist = before.split(sep, maxsplit) # a regular split is fine here
817 startlist = before.split(sep, maxsplit) # a regular split is fine here
817 unfinished = startlist[-1]
818 unfinished = startlist[-1]
818 startlist = startlist[:-1]
819 startlist = startlist[:-1]
819
820
820 # recurse because there may be more escaped separators
821 # recurse because there may be more escaped separators
821 endlist = escape_split(after, sep, maxsplit)
822 endlist = escape_split(after, sep, maxsplit)
822
823
823 # finish building the escaped value. we use endlist[0] becaue the first
824 # finish building the escaped value. we use endlist[0] becaue the first
824 # part of the string sent in recursion is the rest of the escaped value.
825 # part of the string sent in recursion is the rest of the escaped value.
825 unfinished += sep + endlist[0]
826 unfinished += sep + endlist[0]
826
827
827 return startlist + [unfinished] + endlist[1:] # put together all the parts
828 return startlist + [unfinished] + endlist[1:] # put together all the parts
828
829
829
830
830 class OptionalAttr(object):
831 class OptionalAttr(object):
831 """
832 """
832 Special Optional Option that defines other attribute. Example::
833 Special Optional Option that defines other attribute. Example::
833
834
834 def test(apiuser, userid=Optional(OAttr('apiuser')):
835 def test(apiuser, userid=Optional(OAttr('apiuser')):
835 user = Optional.extract(userid)
836 user = Optional.extract(userid)
836 # calls
837 # calls
837
838
838 """
839 """
839
840
840 def __init__(self, attr_name):
841 def __init__(self, attr_name):
841 self.attr_name = attr_name
842 self.attr_name = attr_name
842
843
843 def __repr__(self):
844 def __repr__(self):
844 return '<OptionalAttr:%s>' % self.attr_name
845 return '<OptionalAttr:%s>' % self.attr_name
845
846
846 def __call__(self):
847 def __call__(self):
847 return self
848 return self
848
849
849
850
850 # alias
851 # alias
851 OAttr = OptionalAttr
852 OAttr = OptionalAttr
852
853
853
854
854 class Optional(object):
855 class Optional(object):
855 """
856 """
856 Defines an optional parameter::
857 Defines an optional parameter::
857
858
858 param = param.getval() if isinstance(param, Optional) else param
859 param = param.getval() if isinstance(param, Optional) else param
859 param = param() if isinstance(param, Optional) else param
860 param = param() if isinstance(param, Optional) else param
860
861
861 is equivalent of::
862 is equivalent of::
862
863
863 param = Optional.extract(param)
864 param = Optional.extract(param)
864
865
865 """
866 """
866
867
867 def __init__(self, type_):
868 def __init__(self, type_):
868 self.type_ = type_
869 self.type_ = type_
869
870
870 def __repr__(self):
871 def __repr__(self):
871 return '<Optional:%s>' % self.type_.__repr__()
872 return '<Optional:%s>' % self.type_.__repr__()
872
873
873 def __call__(self):
874 def __call__(self):
874 return self.getval()
875 return self.getval()
875
876
876 def getval(self):
877 def getval(self):
877 """
878 """
878 returns value from this Optional instance
879 returns value from this Optional instance
879 """
880 """
880 if isinstance(self.type_, OAttr):
881 if isinstance(self.type_, OAttr):
881 # use params name
882 # use params name
882 return self.type_.attr_name
883 return self.type_.attr_name
883 return self.type_
884 return self.type_
884
885
885 @classmethod
886 @classmethod
886 def extract(cls, val):
887 def extract(cls, val):
887 """
888 """
888 Extracts value from Optional() instance
889 Extracts value from Optional() instance
889
890
890 :param val:
891 :param val:
891 :return: original value if it's not Optional instance else
892 :return: original value if it's not Optional instance else
892 value of instance
893 value of instance
893 """
894 """
894 if isinstance(val, cls):
895 if isinstance(val, cls):
895 return val.getval()
896 return val.getval()
896 return val
897 return val
897
898
898
899
899 def get_routes_generator_for_server_url(server_url):
900 def get_routes_generator_for_server_url(server_url):
900 parsed_url = urlobject.URLObject(server_url)
901 parsed_url = urlobject.URLObject(server_url)
901 netloc = safe_str(parsed_url.netloc)
902 netloc = safe_str(parsed_url.netloc)
902 script_name = safe_str(parsed_url.path)
903 script_name = safe_str(parsed_url.path)
903
904
904 if ':' in netloc:
905 if ':' in netloc:
905 server_name, server_port = netloc.split(':')
906 server_name, server_port = netloc.split(':')
906 else:
907 else:
907 server_name = netloc
908 server_name = netloc
908 server_port = (parsed_url.scheme == 'https' and '443' or '80')
909 server_port = (parsed_url.scheme == 'https' and '443' or '80')
909
910
910 environ = {
911 environ = {
911 'REQUEST_METHOD': 'GET',
912 'REQUEST_METHOD': 'GET',
912 'PATH_INFO': '/',
913 'PATH_INFO': '/',
913 'SERVER_NAME': server_name,
914 'SERVER_NAME': server_name,
914 'SERVER_PORT': server_port,
915 'SERVER_PORT': server_port,
915 'SCRIPT_NAME': script_name,
916 'SCRIPT_NAME': script_name,
916 }
917 }
917 if parsed_url.scheme == 'https':
918 if parsed_url.scheme == 'https':
918 environ['HTTPS'] = 'on'
919 environ['HTTPS'] = 'on'
919 environ['wsgi.url_scheme'] = 'https'
920 environ['wsgi.url_scheme'] = 'https'
920
921
921 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
922 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
922
923
923
924
924 def glob2re(pat):
925 def glob2re(pat):
925 """
926 """
926 Translate a shell PATTERN to a regular expression.
927 Translate a shell PATTERN to a regular expression.
927
928
928 There is no way to quote meta-characters.
929 There is no way to quote meta-characters.
929 """
930 """
930
931
931 i, n = 0, len(pat)
932 i, n = 0, len(pat)
932 res = ''
933 res = ''
933 while i < n:
934 while i < n:
934 c = pat[i]
935 c = pat[i]
935 i = i+1
936 i = i+1
936 if c == '*':
937 if c == '*':
937 #res = res + '.*'
938 #res = res + '.*'
938 res = res + '[^/]*'
939 res = res + '[^/]*'
939 elif c == '?':
940 elif c == '?':
940 #res = res + '.'
941 #res = res + '.'
941 res = res + '[^/]'
942 res = res + '[^/]'
942 elif c == '[':
943 elif c == '[':
943 j = i
944 j = i
944 if j < n and pat[j] == '!':
945 if j < n and pat[j] == '!':
945 j = j+1
946 j = j+1
946 if j < n and pat[j] == ']':
947 if j < n and pat[j] == ']':
947 j = j+1
948 j = j+1
948 while j < n and pat[j] != ']':
949 while j < n and pat[j] != ']':
949 j = j+1
950 j = j+1
950 if j >= n:
951 if j >= n:
951 res = res + '\\['
952 res = res + '\\['
952 else:
953 else:
953 stuff = pat[i:j].replace('\\','\\\\')
954 stuff = pat[i:j].replace('\\','\\\\')
954 i = j+1
955 i = j+1
955 if stuff[0] == '!':
956 if stuff[0] == '!':
956 stuff = '^' + stuff[1:]
957 stuff = '^' + stuff[1:]
957 elif stuff[0] == '^':
958 elif stuff[0] == '^':
958 stuff = '\\' + stuff
959 stuff = '\\' + stuff
959 res = '%s[%s]' % (res, stuff)
960 res = '%s[%s]' % (res, stuff)
960 else:
961 else:
961 res = res + re.escape(c)
962 res = res + re.escape(c)
962 return res + '\Z(?ms)'
963 return res + '\Z(?ms)'
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now