##// END OF EJS Templates
events: ported pylons part to pyramid....
marcink -
r1959:b0de121b default
parent child Browse files
Show More
@@ -1,102 +1,112 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 import datetime
19
20
20 from datetime import datetime
21 from zope.cachedescriptors.property import Lazy as LazyProperty
21 from pyramid.threadlocal import get_current_request
22 from pyramid.threadlocal import get_current_request
23
22 from rhodecode.lib.utils2 import AttributeDict
24 from rhodecode.lib.utils2 import AttributeDict
23
25
24
26
25 # this is a user object to be used for events caused by the system (eg. shell)
27 # this is a user object to be used for events caused by the system (eg. shell)
26 SYSTEM_USER = AttributeDict(dict(
28 SYSTEM_USER = AttributeDict(dict(
27 username='__SYSTEM__',
29 username='__SYSTEM__',
28 user_id='__SYSTEM_ID__'
30 user_id='__SYSTEM_ID__'
29 ))
31 ))
30
32
31 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
32
34
33
35
34 class RhodecodeEvent(object):
36 class RhodecodeEvent(object):
35 """
37 """
36 Base event class for all RhodeCode events
38 Base event class for all RhodeCode events
37 """
39 """
38 name = "RhodeCodeEvent"
40 name = "RhodeCodeEvent"
41 no_url_set = '<no server_url available>'
39
42
40 def __init__(self, request=None):
43 def __init__(self, request=None):
41 self.request = request or get_current_request()
44 self._request = request
42 self.utc_timestamp = datetime.utcnow()
45 self.utc_timestamp = datetime.datetime.utcnow()
46
47 def get_request(self):
48 if self._request:
49 return self._request
50 return get_current_request()
51
52 @LazyProperty
53 def request(self):
54 return self.get_request()
43
55
44 @property
56 @property
45 def auth_user(self):
57 def auth_user(self):
46 if not self.request:
58 if not self.request:
47 return
59 return
48
60
49 user = getattr(self.request, 'user', None)
61 user = getattr(self.request, 'user', None)
50 if user:
62 if user:
51 return user
63 return user
52
64
53 api_user = getattr(self.request, 'rpc_user', None)
65 api_user = getattr(self.request, 'rpc_user', None)
54 if api_user:
66 if api_user:
55 return api_user
67 return api_user
56
68
57 @property
69 @property
58 def actor(self):
70 def actor(self):
59 auth_user = self.auth_user
71 auth_user = self.auth_user
60
61 if auth_user:
72 if auth_user:
62 instance = auth_user.get_instance()
73 instance = auth_user.get_instance()
63 if not instance:
74 if not instance:
64 return AttributeDict(dict(
75 return AttributeDict(dict(
65 username=auth_user.username,
76 username=auth_user.username,
66 user_id=auth_user.user_id,
77 user_id=auth_user.user_id,
67 ))
78 ))
68 return instance
79 return instance
69
80
70 return SYSTEM_USER
81 return SYSTEM_USER
71
82
72 @property
83 @property
73 def actor_ip(self):
84 def actor_ip(self):
74 auth_user = self.auth_user
85 auth_user = self.auth_user
75 if auth_user:
86 if auth_user:
76 return auth_user.ip_addr
87 return auth_user.ip_addr
77 return '<no ip available>'
88 return '<no ip available>'
78
89
79 @property
90 @property
80 def server_url(self):
91 def server_url(self):
81 default = '<no server_url available>'
82 if self.request:
92 if self.request:
83 try:
93 try:
84 return self.request.route_url('home')
94 return self.request.route_url('home')
85 except Exception:
95 except Exception:
86 log.exception('Failed to fetch URL for server')
96 log.exception('Failed to fetch URL for server')
87 return default
97 return self.no_url_set
88
98
89 return default
99 return self.no_url_set
90
100
91 def as_dict(self):
101 def as_dict(self):
92 data = {
102 data = {
93 'name': self.name,
103 'name': self.name,
94 'utc_timestamp': self.utc_timestamp,
104 'utc_timestamp': self.utc_timestamp,
95 'actor_ip': self.actor_ip,
105 'actor_ip': self.actor_ip,
96 'actor': {
106 'actor': {
97 'username': self.actor.username,
107 'username': self.actor.username,
98 'user_id': self.actor.user_id
108 'user_id': self.actor.user_id
99 },
109 },
100 'server_url': self.server_url
110 'server_url': self.server_url
101 }
111 }
102 return data
112 return data
@@ -1,142 +1,144 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
18
19 import logging
19 import logging
20
20
21 from rhodecode.translation import lazy_ugettext
21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.repo import (
22 from rhodecode.events.repo import (
23 RepoEvent, _commits_as_dict, _issues_as_dict)
23 RepoEvent, _commits_as_dict, _issues_as_dict)
24
24
25 log = logging.getLogger(__name__)
25 log = logging.getLogger(__name__)
26
26
27
27
28 class PullRequestEvent(RepoEvent):
28 class PullRequestEvent(RepoEvent):
29 """
29 """
30 Base class for pull request events.
30 Base class for pull request events.
31
31
32 :param pullrequest: a :class:`PullRequest` instance
32 :param pullrequest: a :class:`PullRequest` instance
33 """
33 """
34
34
35 def __init__(self, pullrequest):
35 def __init__(self, pullrequest):
36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
37 self.pullrequest = pullrequest
37 self.pullrequest = pullrequest
38
38
39 def as_dict(self):
39 def as_dict(self):
40 from rhodecode.model.pull_request import PullRequestModel
40 from rhodecode.model.pull_request import PullRequestModel
41 data = super(PullRequestEvent, self).as_dict()
41 data = super(PullRequestEvent, self).as_dict()
42
42
43 commits = _commits_as_dict(
43 commits = _commits_as_dict(
44 self,
44 self,
45 commit_ids=self.pullrequest.revisions,
45 commit_ids=self.pullrequest.revisions,
46 repos=[self.pullrequest.source_repo]
46 repos=[self.pullrequest.source_repo]
47 )
47 )
48 issues = _issues_as_dict(commits)
48 issues = _issues_as_dict(commits)
49
49
50 data.update({
50 data.update({
51 'pullrequest': {
51 'pullrequest': {
52 'title': self.pullrequest.title,
52 'title': self.pullrequest.title,
53 'issues': issues,
53 'issues': issues,
54 'pull_request_id': self.pullrequest.pull_request_id,
54 'pull_request_id': self.pullrequest.pull_request_id,
55 'url': PullRequestModel().get_url(self.pullrequest),
55 'url': PullRequestModel().get_url(
56 self.pullrequest, request=self.request),
56 'permalink_url': PullRequestModel().get_url(
57 'permalink_url': PullRequestModel().get_url(
57 self.pullrequest, permalink=True),
58 self.pullrequest, request=self.request, permalink=True),
58 'status': self.pullrequest.calculated_review_status(),
59 'status': self.pullrequest.calculated_review_status(),
59 'commits': commits,
60 'commits': commits,
60 }
61 }
61 })
62 })
62 return data
63 return data
63
64
64
65
65 class PullRequestCreateEvent(PullRequestEvent):
66 class PullRequestCreateEvent(PullRequestEvent):
66 """
67 """
67 An instance of this class is emitted as an :term:`event` after a pull
68 An instance of this class is emitted as an :term:`event` after a pull
68 request is created.
69 request is created.
69 """
70 """
70 name = 'pullrequest-create'
71 name = 'pullrequest-create'
71 display_name = lazy_ugettext('pullrequest created')
72 display_name = lazy_ugettext('pullrequest created')
72
73
73
74
74 class PullRequestCloseEvent(PullRequestEvent):
75 class PullRequestCloseEvent(PullRequestEvent):
75 """
76 """
76 An instance of this class is emitted as an :term:`event` after a pull
77 An instance of this class is emitted as an :term:`event` after a pull
77 request is closed.
78 request is closed.
78 """
79 """
79 name = 'pullrequest-close'
80 name = 'pullrequest-close'
80 display_name = lazy_ugettext('pullrequest closed')
81 display_name = lazy_ugettext('pullrequest closed')
81
82
82
83
83 class PullRequestUpdateEvent(PullRequestEvent):
84 class PullRequestUpdateEvent(PullRequestEvent):
84 """
85 """
85 An instance of this class is emitted as an :term:`event` after a pull
86 An instance of this class is emitted as an :term:`event` after a pull
86 request's commits have been updated.
87 request's commits have been updated.
87 """
88 """
88 name = 'pullrequest-update'
89 name = 'pullrequest-update'
89 display_name = lazy_ugettext('pullrequest commits updated')
90 display_name = lazy_ugettext('pullrequest commits updated')
90
91
91
92
92 class PullRequestReviewEvent(PullRequestEvent):
93 class PullRequestReviewEvent(PullRequestEvent):
93 """
94 """
94 An instance of this class is emitted as an :term:`event` after a pull
95 An instance of this class is emitted as an :term:`event` after a pull
95 request review has changed.
96 request review has changed.
96 """
97 """
97 name = 'pullrequest-review'
98 name = 'pullrequest-review'
98 display_name = lazy_ugettext('pullrequest review changed')
99 display_name = lazy_ugettext('pullrequest review changed')
99
100
100
101
101 class PullRequestMergeEvent(PullRequestEvent):
102 class PullRequestMergeEvent(PullRequestEvent):
102 """
103 """
103 An instance of this class is emitted as an :term:`event` after a pull
104 An instance of this class is emitted as an :term:`event` after a pull
104 request is merged.
105 request is merged.
105 """
106 """
106 name = 'pullrequest-merge'
107 name = 'pullrequest-merge'
107 display_name = lazy_ugettext('pullrequest merged')
108 display_name = lazy_ugettext('pullrequest merged')
108
109
109
110
110 class PullRequestCommentEvent(PullRequestEvent):
111 class PullRequestCommentEvent(PullRequestEvent):
111 """
112 """
112 An instance of this class is emitted as an :term:`event` after a pull
113 An instance of this class is emitted as an :term:`event` after a pull
113 request comment is created.
114 request comment is created.
114 """
115 """
115 name = 'pullrequest-comment'
116 name = 'pullrequest-comment'
116 display_name = lazy_ugettext('pullrequest commented')
117 display_name = lazy_ugettext('pullrequest commented')
117
118
118 def __init__(self, pullrequest, comment):
119 def __init__(self, pullrequest, comment):
119 super(PullRequestCommentEvent, self).__init__(pullrequest)
120 super(PullRequestCommentEvent, self).__init__(pullrequest)
120 self.comment = comment
121 self.comment = comment
121
122
122 def as_dict(self):
123 def as_dict(self):
123 from rhodecode.model.comment import CommentsModel
124 from rhodecode.model.comment import CommentsModel
124 data = super(PullRequestCommentEvent, self).as_dict()
125 data = super(PullRequestCommentEvent, self).as_dict()
125
126
126 status = None
127 status = None
127 if self.comment.status_change:
128 if self.comment.status_change:
128 status = self.comment.status_change[0].status
129 status = self.comment.status_change[0].status
129
130
130 data.update({
131 data.update({
131 'comment': {
132 'comment': {
132 'status': status,
133 'status': status,
133 'text': self.comment.text,
134 'text': self.comment.text,
134 'type': self.comment.comment_type,
135 'type': self.comment.comment_type,
135 'file': self.comment.f_path,
136 'file': self.comment.f_path,
136 'line': self.comment.line_no,
137 'line': self.comment.line_no,
137 'url': CommentsModel().get_url(self.comment),
138 'url': CommentsModel().get_url(
139 self.comment, request=self.request),
138 'permalink_url': CommentsModel().get_url(
140 'permalink_url': CommentsModel().get_url(
139 self.comment, permalink=True),
141 self.comment, request=self.request, permalink=True),
140 }
142 }
141 })
143 })
142 return data
144 return data
@@ -1,277 +1,282 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
18
19 import collections
19 import collections
20 import logging
20 import logging
21
21
22 from rhodecode.translation import lazy_ugettext
22 from rhodecode.translation import lazy_ugettext
23 from rhodecode.model.db import User, Repository, Session
23 from rhodecode.model.db import User, Repository, Session
24 from rhodecode.events.base import RhodecodeEvent
24 from rhodecode.events.base import RhodecodeEvent
25 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
25 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 def _commits_as_dict(event, commit_ids, repos):
30 def _commits_as_dict(event, commit_ids, repos):
31 """
31 """
32 Helper function to serialize commit_ids
32 Helper function to serialize commit_ids
33
33
34 :param event: class calling this method
34 :param event: class calling this method
35 :param commit_ids: commits to get
35 :param commit_ids: commits to get
36 :param repos: list of repos to check
36 :param repos: list of repos to check
37 """
37 """
38 from rhodecode.lib.utils2 import extract_mentioned_users
38 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.helpers import (
39 from rhodecode.lib.helpers import (
40 urlify_commit_message, process_patterns, chop_at_smart)
40 urlify_commit_message, process_patterns, chop_at_smart)
41 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.repo import RepoModel
42
42
43 if not repos:
43 if not repos:
44 raise Exception('no repo defined')
44 raise Exception('no repo defined')
45
45
46 if not isinstance(repos, (tuple, list)):
46 if not isinstance(repos, (tuple, list)):
47 repos = [repos]
47 repos = [repos]
48
48
49 if not commit_ids:
49 if not commit_ids:
50 return []
50 return []
51
51
52 needed_commits = list(commit_ids)
52 needed_commits = list(commit_ids)
53
53
54 commits = []
54 commits = []
55 reviewers = []
55 reviewers = []
56 for repo in repos:
56 for repo in repos:
57 if not needed_commits:
57 if not needed_commits:
58 return commits # return early if we have the commits we need
58 return commits # return early if we have the commits we need
59
59
60 vcs_repo = repo.scm_instance(cache=False)
60 vcs_repo = repo.scm_instance(cache=False)
61 try:
61 try:
62 # use copy of needed_commits since we modify it while iterating
62 # use copy of needed_commits since we modify it while iterating
63 for commit_id in list(needed_commits):
63 for commit_id in list(needed_commits):
64 try:
64 try:
65 cs = vcs_repo.get_changeset(commit_id)
65 cs = vcs_repo.get_changeset(commit_id)
66 except CommitDoesNotExistError:
66 except CommitDoesNotExistError:
67 continue # maybe its in next repo
67 continue # maybe its in next repo
68
68
69 cs_data = cs.__json__()
69 cs_data = cs.__json__()
70 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
70 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
71 cs_data['reviewers'] = reviewers
71 cs_data['reviewers'] = reviewers
72 cs_data['url'] = RepoModel().get_commit_url(
72 cs_data['url'] = RepoModel().get_commit_url(
73 repo, cs_data['raw_id'], request=event.request)
73 repo, cs_data['raw_id'], request=event.request)
74 cs_data['permalink_url'] = RepoModel().get_commit_url(
74 cs_data['permalink_url'] = RepoModel().get_commit_url(
75 repo, cs_data['raw_id'], request=event.request, permalink=True)
75 repo, cs_data['raw_id'], request=event.request, permalink=True)
76 urlified_message, issues_data = process_patterns(
76 urlified_message, issues_data = process_patterns(
77 cs_data['message'], repo.repo_name)
77 cs_data['message'], repo.repo_name)
78 cs_data['issues'] = issues_data
78 cs_data['issues'] = issues_data
79 cs_data['message_html'] = urlify_commit_message(
79 cs_data['message_html'] = urlify_commit_message(
80 cs_data['message'], repo.repo_name)
80 cs_data['message'], repo.repo_name)
81 cs_data['message_html_title'] = chop_at_smart(
81 cs_data['message_html_title'] = chop_at_smart(
82 cs_data['message'], '\n', suffix_if_chopped='...')
82 cs_data['message'], '\n', suffix_if_chopped='...')
83 commits.append(cs_data)
83 commits.append(cs_data)
84
84
85 needed_commits.remove(commit_id)
85 needed_commits.remove(commit_id)
86
86
87 except Exception as e:
87 except Exception as e:
88 log.exception(e)
88 log.exception(e)
89 # we don't send any commits when crash happens, only full list
89 # we don't send any commits when crash happens, only full list
90 # matters we short circuit then.
90 # matters we short circuit then.
91 return []
91 return []
92
92
93 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
93 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
94 if missing_commits:
94 if missing_commits:
95 log.error('missing commits: %s' % ', '.join(missing_commits))
95 log.error('missing commits: %s' % ', '.join(missing_commits))
96
96
97 return commits
97 return commits
98
98
99
99
100 def _issues_as_dict(commits):
100 def _issues_as_dict(commits):
101 """ Helper function to serialize issues from commits """
101 """ Helper function to serialize issues from commits """
102 issues = {}
102 issues = {}
103 for commit in commits:
103 for commit in commits:
104 for issue in commit['issues']:
104 for issue in commit['issues']:
105 issues[issue['id']] = issue
105 issues[issue['id']] = issue
106 return issues
106 return issues
107
107
108
108
109 class RepoEvent(RhodecodeEvent):
109 class RepoEvent(RhodecodeEvent):
110 """
110 """
111 Base class for events acting on a repository.
111 Base class for events acting on a repository.
112
112
113 :param repo: a :class:`Repository` instance
113 :param repo: a :class:`Repository` instance
114 """
114 """
115
115
116 def __init__(self, repo):
116 def __init__(self, repo):
117 super(RepoEvent, self).__init__()
117 super(RepoEvent, self).__init__()
118 self.repo = repo
118 self.repo = repo
119
119
120 def as_dict(self):
120 def as_dict(self):
121 from rhodecode.model.repo import RepoModel
121 from rhodecode.model.repo import RepoModel
122 data = super(RepoEvent, self).as_dict()
122 data = super(RepoEvent, self).as_dict()
123
123 extra_fields = collections.OrderedDict()
124 extra_fields = collections.OrderedDict()
124 for field in self.repo.extra_fields:
125 for field in self.repo.extra_fields:
125 extra_fields[field.field_key] = field.field_value
126 extra_fields[field.field_key] = field.field_value
126
127
127 data.update({
128 data.update({
128 'repo': {
129 'repo': {
129 'repo_id': self.repo.repo_id,
130 'repo_id': self.repo.repo_id,
130 'repo_name': self.repo.repo_name,
131 'repo_name': self.repo.repo_name,
131 'repo_type': self.repo.repo_type,
132 'repo_type': self.repo.repo_type,
132 'url': RepoModel().get_url(
133 'url': RepoModel().get_url(
133 self.repo, request=self.request),
134 self.repo, request=self.request),
134 'permalink_url': RepoModel().get_url(
135 'permalink_url': RepoModel().get_url(
135 self.repo, request=self.request, permalink=True),
136 self.repo, request=self.request, permalink=True),
136 'extra_fields': extra_fields
137 'extra_fields': extra_fields
137 }
138 }
138 })
139 })
139 return data
140 return data
140
141
141
142
142 class RepoPreCreateEvent(RepoEvent):
143 class RepoPreCreateEvent(RepoEvent):
143 """
144 """
144 An instance of this class is emitted as an :term:`event` before a repo is
145 An instance of this class is emitted as an :term:`event` before a repo is
145 created.
146 created.
146 """
147 """
147 name = 'repo-pre-create'
148 name = 'repo-pre-create'
148 display_name = lazy_ugettext('repository pre create')
149 display_name = lazy_ugettext('repository pre create')
149
150
150
151
151 class RepoCreateEvent(RepoEvent):
152 class RepoCreateEvent(RepoEvent):
152 """
153 """
153 An instance of this class is emitted as an :term:`event` whenever a repo is
154 An instance of this class is emitted as an :term:`event` whenever a repo is
154 created.
155 created.
155 """
156 """
156 name = 'repo-create'
157 name = 'repo-create'
157 display_name = lazy_ugettext('repository created')
158 display_name = lazy_ugettext('repository created')
158
159
159
160
160 class RepoPreDeleteEvent(RepoEvent):
161 class RepoPreDeleteEvent(RepoEvent):
161 """
162 """
162 An instance of this class is emitted as an :term:`event` whenever a repo is
163 An instance of this class is emitted as an :term:`event` whenever a repo is
163 created.
164 created.
164 """
165 """
165 name = 'repo-pre-delete'
166 name = 'repo-pre-delete'
166 display_name = lazy_ugettext('repository pre delete')
167 display_name = lazy_ugettext('repository pre delete')
167
168
168
169
169 class RepoDeleteEvent(RepoEvent):
170 class RepoDeleteEvent(RepoEvent):
170 """
171 """
171 An instance of this class is emitted as an :term:`event` whenever a repo is
172 An instance of this class is emitted as an :term:`event` whenever a repo is
172 created.
173 created.
173 """
174 """
174 name = 'repo-delete'
175 name = 'repo-delete'
175 display_name = lazy_ugettext('repository deleted')
176 display_name = lazy_ugettext('repository deleted')
176
177
177
178
178 class RepoVCSEvent(RepoEvent):
179 class RepoVCSEvent(RepoEvent):
179 """
180 """
180 Base class for events triggered by the VCS
181 Base class for events triggered by the VCS
181 """
182 """
182 def __init__(self, repo_name, extras):
183 def __init__(self, repo_name, extras):
183 self.repo = Repository.get_by_repo_name(repo_name)
184 self.repo = Repository.get_by_repo_name(repo_name)
184 if not self.repo:
185 if not self.repo:
185 raise Exception('repo by this name %s does not exist' % repo_name)
186 raise Exception('repo by this name %s does not exist' % repo_name)
186 self.extras = extras
187 self.extras = extras
187 super(RepoVCSEvent, self).__init__(self.repo)
188 super(RepoVCSEvent, self).__init__(self.repo)
188
189
189 @property
190 @property
190 def actor(self):
191 def actor(self):
191 if self.extras.get('username'):
192 if self.extras.get('username'):
192 return User.get_by_username(self.extras['username'])
193 return User.get_by_username(self.extras['username'])
193
194
194 @property
195 @property
195 def actor_ip(self):
196 def actor_ip(self):
196 if self.extras.get('ip'):
197 if self.extras.get('ip'):
197 return self.extras['ip']
198 return self.extras['ip']
198
199
199 @property
200 @property
200 def server_url(self):
201 def server_url(self):
201 if self.extras.get('server_url'):
202 if self.extras.get('server_url'):
202 return self.extras['server_url']
203 return self.extras['server_url']
203
204
205 @property
206 def request(self):
207 return self.extras.get('request') or self.get_request()
208
204
209
205 class RepoPrePullEvent(RepoVCSEvent):
210 class RepoPrePullEvent(RepoVCSEvent):
206 """
211 """
207 An instance of this class is emitted as an :term:`event` before commits
212 An instance of this class is emitted as an :term:`event` before commits
208 are pulled from a repo.
213 are pulled from a repo.
209 """
214 """
210 name = 'repo-pre-pull'
215 name = 'repo-pre-pull'
211 display_name = lazy_ugettext('repository pre pull')
216 display_name = lazy_ugettext('repository pre pull')
212
217
213
218
214 class RepoPullEvent(RepoVCSEvent):
219 class RepoPullEvent(RepoVCSEvent):
215 """
220 """
216 An instance of this class is emitted as an :term:`event` after commits
221 An instance of this class is emitted as an :term:`event` after commits
217 are pulled from a repo.
222 are pulled from a repo.
218 """
223 """
219 name = 'repo-pull'
224 name = 'repo-pull'
220 display_name = lazy_ugettext('repository pull')
225 display_name = lazy_ugettext('repository pull')
221
226
222
227
223 class RepoPrePushEvent(RepoVCSEvent):
228 class RepoPrePushEvent(RepoVCSEvent):
224 """
229 """
225 An instance of this class is emitted as an :term:`event` before commits
230 An instance of this class is emitted as an :term:`event` before commits
226 are pushed to a repo.
231 are pushed to a repo.
227 """
232 """
228 name = 'repo-pre-push'
233 name = 'repo-pre-push'
229 display_name = lazy_ugettext('repository pre push')
234 display_name = lazy_ugettext('repository pre push')
230
235
231
236
232 class RepoPushEvent(RepoVCSEvent):
237 class RepoPushEvent(RepoVCSEvent):
233 """
238 """
234 An instance of this class is emitted as an :term:`event` after commits
239 An instance of this class is emitted as an :term:`event` after commits
235 are pushed to a repo.
240 are pushed to a repo.
236
241
237 :param extras: (optional) dict of data from proxied VCS actions
242 :param extras: (optional) dict of data from proxied VCS actions
238 """
243 """
239 name = 'repo-push'
244 name = 'repo-push'
240 display_name = lazy_ugettext('repository push')
245 display_name = lazy_ugettext('repository push')
241
246
242 def __init__(self, repo_name, pushed_commit_ids, extras):
247 def __init__(self, repo_name, pushed_commit_ids, extras):
243 super(RepoPushEvent, self).__init__(repo_name, extras)
248 super(RepoPushEvent, self).__init__(repo_name, extras)
244 self.pushed_commit_ids = pushed_commit_ids
249 self.pushed_commit_ids = pushed_commit_ids
245
250
246 def as_dict(self):
251 def as_dict(self):
247 data = super(RepoPushEvent, self).as_dict()
252 data = super(RepoPushEvent, self).as_dict()
248
253
249 def branch_url(branch_name):
254 def branch_url(branch_name):
250 return '{}/changelog?branch={}'.format(
255 return '{}/changelog?branch={}'.format(
251 data['repo']['url'], branch_name)
256 data['repo']['url'], branch_name)
252
257
253 commits = _commits_as_dict(
258 commits = _commits_as_dict(
254 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
259 self, commit_ids=self.pushed_commit_ids, repos=[self.repo])
255
260
256 last_branch = None
261 last_branch = None
257 for commit in reversed(commits):
262 for commit in reversed(commits):
258 commit['branch'] = commit['branch'] or last_branch
263 commit['branch'] = commit['branch'] or last_branch
259 last_branch = commit['branch']
264 last_branch = commit['branch']
260 issues = _issues_as_dict(commits)
265 issues = _issues_as_dict(commits)
261
266
262 branches = set(
267 branches = set(
263 commit['branch'] for commit in commits if commit['branch'])
268 commit['branch'] for commit in commits if commit['branch'])
264 branches = [
269 branches = [
265 {
270 {
266 'name': branch,
271 'name': branch,
267 'url': branch_url(branch)
272 'url': branch_url(branch)
268 }
273 }
269 for branch in branches
274 for branch in branches
270 ]
275 ]
271
276
272 data['push'] = {
277 data['push'] = {
273 'commits': commits,
278 'commits': commits,
274 'issues': issues,
279 'issues': issues,
275 'branches': branches,
280 'branches': branches,
276 }
281 }
277 return data
282 return data
@@ -1,672 +1,702 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 markupsafe
30 import markupsafe
31 import ipaddress
31 import ipaddress
32 import pyramid.threadlocal
32 import pyramid.threadlocal
33
33
34 from paste.auth.basic import AuthBasicAuthenticator
34 from paste.auth.basic import AuthBasicAuthenticator
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
37 from pylons import config, tmpl_context as c, request, url
37 from pylons import config, tmpl_context as c, request, url
38 from pylons.controllers import WSGIController
38 from pylons.controllers import WSGIController
39 from pylons.controllers.util import redirect
39 from pylons.controllers.util import redirect
40 from pylons.i18n import translation
40 from pylons.i18n import translation
41 # marcink: don't remove this import
41 # marcink: don't remove this import
42 from pylons.templating import render_mako, pylons_globals, literal, cached_template
42 from pylons.templating import render_mako, pylons_globals, literal, cached_template
43 from pylons.i18n.translation import _
43 from pylons.i18n.translation import _
44 from webob.exc import HTTPFound
44 from webob.exc import HTTPFound
45
45
46
46
47 import rhodecode
47 import rhodecode
48 from rhodecode.authentication.base import VCS_TYPE
48 from rhodecode.authentication.base import VCS_TYPE
49 from rhodecode.lib import auth, utils2
49 from rhodecode.lib import auth, utils2
50 from rhodecode.lib import helpers as h
50 from rhodecode.lib import helpers as h
51 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
51 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
52 from rhodecode.lib.exceptions import UserCreationError
52 from rhodecode.lib.exceptions import UserCreationError
53 from rhodecode.lib.utils import (
53 from rhodecode.lib.utils import (
54 get_repo_slug, set_rhodecode_config, password_changed,
54 get_repo_slug, set_rhodecode_config, password_changed,
55 get_enabled_hook_classes)
55 get_enabled_hook_classes)
56 from rhodecode.lib.utils2 import (
56 from rhodecode.lib.utils2 import (
57 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
57 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
58 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
58 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
59 from rhodecode.model import meta
59 from rhodecode.model import meta
60 from rhodecode.model.db import Repository, User, ChangesetComment
60 from rhodecode.model.db import Repository, User, ChangesetComment
61 from rhodecode.model.notification import NotificationModel
61 from rhodecode.model.notification import NotificationModel
62 from rhodecode.model.scm import ScmModel
62 from rhodecode.model.scm import ScmModel
63 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
63 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
64
64
65
65
66 log = logging.getLogger(__name__)
66 log = logging.getLogger(__name__)
67
67
68
68
69 # hack to make the migration to pyramid easier
69 # hack to make the migration to pyramid easier
70 def render(template_name, extra_vars=None, cache_key=None,
70 def render(template_name, extra_vars=None, cache_key=None,
71 cache_type=None, cache_expire=None):
71 cache_type=None, cache_expire=None):
72 """Render a template with Mako
72 """Render a template with Mako
73
73
74 Accepts the cache options ``cache_key``, ``cache_type``, and
74 Accepts the cache options ``cache_key``, ``cache_type``, and
75 ``cache_expire``.
75 ``cache_expire``.
76
76
77 """
77 """
78 # Create a render callable for the cache function
78 # Create a render callable for the cache function
79 def render_template():
79 def render_template():
80 # Pull in extra vars if needed
80 # Pull in extra vars if needed
81 globs = extra_vars or {}
81 globs = extra_vars or {}
82
82
83 # Second, get the globals
83 # Second, get the globals
84 globs.update(pylons_globals())
84 globs.update(pylons_globals())
85
85
86 globs['_ungettext'] = globs['ungettext']
86 globs['_ungettext'] = globs['ungettext']
87 # Grab a template reference
87 # Grab a template reference
88 template = globs['app_globals'].mako_lookup.get_template(template_name)
88 template = globs['app_globals'].mako_lookup.get_template(template_name)
89
89
90 return literal(template.render_unicode(**globs))
90 return literal(template.render_unicode(**globs))
91
91
92 return cached_template(template_name, render_template, cache_key=cache_key,
92 return cached_template(template_name, render_template, cache_key=cache_key,
93 cache_type=cache_type, cache_expire=cache_expire)
93 cache_type=cache_type, cache_expire=cache_expire)
94
94
95 def _filter_proxy(ip):
95 def _filter_proxy(ip):
96 """
96 """
97 Passed in IP addresses in HEADERS can be in a special format of multiple
97 Passed in IP addresses in HEADERS can be in a special format of multiple
98 ips. Those comma separated IPs are passed from various proxies in the
98 ips. Those comma separated IPs are passed from various proxies in the
99 chain of request processing. The left-most being the original client.
99 chain of request processing. The left-most being the original client.
100 We only care about the first IP which came from the org. client.
100 We only care about the first IP which came from the org. client.
101
101
102 :param ip: ip string from headers
102 :param ip: ip string from headers
103 """
103 """
104 if ',' in ip:
104 if ',' in ip:
105 _ips = ip.split(',')
105 _ips = ip.split(',')
106 _first_ip = _ips[0].strip()
106 _first_ip = _ips[0].strip()
107 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
107 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
108 return _first_ip
108 return _first_ip
109 return ip
109 return ip
110
110
111
111
112 def _filter_port(ip):
112 def _filter_port(ip):
113 """
113 """
114 Removes a port from ip, there are 4 main cases to handle here.
114 Removes a port from ip, there are 4 main cases to handle here.
115 - ipv4 eg. 127.0.0.1
115 - ipv4 eg. 127.0.0.1
116 - ipv6 eg. ::1
116 - ipv6 eg. ::1
117 - ipv4+port eg. 127.0.0.1:8080
117 - ipv4+port eg. 127.0.0.1:8080
118 - ipv6+port eg. [::1]:8080
118 - ipv6+port eg. [::1]:8080
119
119
120 :param ip:
120 :param ip:
121 """
121 """
122 def is_ipv6(ip_addr):
122 def is_ipv6(ip_addr):
123 if hasattr(socket, 'inet_pton'):
123 if hasattr(socket, 'inet_pton'):
124 try:
124 try:
125 socket.inet_pton(socket.AF_INET6, ip_addr)
125 socket.inet_pton(socket.AF_INET6, ip_addr)
126 except socket.error:
126 except socket.error:
127 return False
127 return False
128 else:
128 else:
129 # fallback to ipaddress
129 # fallback to ipaddress
130 try:
130 try:
131 ipaddress.IPv6Address(safe_unicode(ip_addr))
131 ipaddress.IPv6Address(safe_unicode(ip_addr))
132 except Exception:
132 except Exception:
133 return False
133 return False
134 return True
134 return True
135
135
136 if ':' not in ip: # must be ipv4 pure ip
136 if ':' not in ip: # must be ipv4 pure ip
137 return ip
137 return ip
138
138
139 if '[' in ip and ']' in ip: # ipv6 with port
139 if '[' in ip and ']' in ip: # ipv6 with port
140 return ip.split(']')[0][1:].lower()
140 return ip.split(']')[0][1:].lower()
141
141
142 # must be ipv6 or ipv4 with port
142 # must be ipv6 or ipv4 with port
143 if is_ipv6(ip):
143 if is_ipv6(ip):
144 return ip
144 return ip
145 else:
145 else:
146 ip, _port = ip.split(':')[:2] # means ipv4+port
146 ip, _port = ip.split(':')[:2] # means ipv4+port
147 return ip
147 return ip
148
148
149
149
150 def get_ip_addr(environ):
150 def get_ip_addr(environ):
151 proxy_key = 'HTTP_X_REAL_IP'
151 proxy_key = 'HTTP_X_REAL_IP'
152 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
152 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
153 def_key = 'REMOTE_ADDR'
153 def_key = 'REMOTE_ADDR'
154 _filters = lambda x: _filter_port(_filter_proxy(x))
154 _filters = lambda x: _filter_port(_filter_proxy(x))
155
155
156 ip = environ.get(proxy_key)
156 ip = environ.get(proxy_key)
157 if ip:
157 if ip:
158 return _filters(ip)
158 return _filters(ip)
159
159
160 ip = environ.get(proxy_key2)
160 ip = environ.get(proxy_key2)
161 if ip:
161 if ip:
162 return _filters(ip)
162 return _filters(ip)
163
163
164 ip = environ.get(def_key, '0.0.0.0')
164 ip = environ.get(def_key, '0.0.0.0')
165 return _filters(ip)
165 return _filters(ip)
166
166
167
167
168 def get_server_ip_addr(environ, log_errors=True):
168 def get_server_ip_addr(environ, log_errors=True):
169 hostname = environ.get('SERVER_NAME')
169 hostname = environ.get('SERVER_NAME')
170 try:
170 try:
171 return socket.gethostbyname(hostname)
171 return socket.gethostbyname(hostname)
172 except Exception as e:
172 except Exception as e:
173 if log_errors:
173 if log_errors:
174 # in some cases this lookup is not possible, and we don't want to
174 # in some cases this lookup is not possible, and we don't want to
175 # make it an exception in logs
175 # make it an exception in logs
176 log.exception('Could not retrieve server ip address: %s', e)
176 log.exception('Could not retrieve server ip address: %s', e)
177 return hostname
177 return hostname
178
178
179
179
180 def get_server_port(environ):
180 def get_server_port(environ):
181 return environ.get('SERVER_PORT')
181 return environ.get('SERVER_PORT')
182
182
183
183
184 def get_access_path(environ):
184 def get_access_path(environ):
185 path = environ.get('PATH_INFO')
185 path = environ.get('PATH_INFO')
186 org_req = environ.get('pylons.original_request')
186 org_req = environ.get('pylons.original_request')
187 if org_req:
187 if org_req:
188 path = org_req.environ.get('PATH_INFO')
188 path = org_req.environ.get('PATH_INFO')
189 return path
189 return path
190
190
191
191
192 def get_user_agent(environ):
192 def get_user_agent(environ):
193 return environ.get('HTTP_USER_AGENT')
193 return environ.get('HTTP_USER_AGENT')
194
194
195
195
196 def vcs_operation_context(
196 def vcs_operation_context(
197 environ, repo_name, username, action, scm, check_locking=True,
197 environ, repo_name, username, action, scm, check_locking=True,
198 is_shadow_repo=False):
198 is_shadow_repo=False):
199 """
199 """
200 Generate the context for a vcs operation, e.g. push or pull.
200 Generate the context for a vcs operation, e.g. push or pull.
201
201
202 This context is passed over the layers so that hooks triggered by the
202 This context is passed over the layers so that hooks triggered by the
203 vcs operation know details like the user, the user's IP address etc.
203 vcs operation know details like the user, the user's IP address etc.
204
204
205 :param check_locking: Allows to switch of the computation of the locking
205 :param check_locking: Allows to switch of the computation of the locking
206 data. This serves mainly the need of the simplevcs middleware to be
206 data. This serves mainly the need of the simplevcs middleware to be
207 able to disable this for certain operations.
207 able to disable this for certain operations.
208
208
209 """
209 """
210 # Tri-state value: False: unlock, None: nothing, True: lock
210 # Tri-state value: False: unlock, None: nothing, True: lock
211 make_lock = None
211 make_lock = None
212 locked_by = [None, None, None]
212 locked_by = [None, None, None]
213 is_anonymous = username == User.DEFAULT_USER
213 is_anonymous = username == User.DEFAULT_USER
214 if not is_anonymous and check_locking:
214 if not is_anonymous and check_locking:
215 log.debug('Checking locking on repository "%s"', repo_name)
215 log.debug('Checking locking on repository "%s"', repo_name)
216 user = User.get_by_username(username)
216 user = User.get_by_username(username)
217 repo = Repository.get_by_repo_name(repo_name)
217 repo = Repository.get_by_repo_name(repo_name)
218 make_lock, __, locked_by = repo.get_locking_state(
218 make_lock, __, locked_by = repo.get_locking_state(
219 action, user.user_id)
219 action, user.user_id)
220
220
221 settings_model = VcsSettingsModel(repo=repo_name)
221 settings_model = VcsSettingsModel(repo=repo_name)
222 ui_settings = settings_model.get_ui_settings()
222 ui_settings = settings_model.get_ui_settings()
223
223
224 extras = {
224 extras = {
225 'ip': get_ip_addr(environ),
225 'ip': get_ip_addr(environ),
226 'username': username,
226 'username': username,
227 'action': action,
227 'action': action,
228 'repository': repo_name,
228 'repository': repo_name,
229 'scm': scm,
229 'scm': scm,
230 'config': rhodecode.CONFIG['__file__'],
230 'config': rhodecode.CONFIG['__file__'],
231 'make_lock': make_lock,
231 'make_lock': make_lock,
232 'locked_by': locked_by,
232 'locked_by': locked_by,
233 'server_url': utils2.get_server_url(environ),
233 'server_url': utils2.get_server_url(environ),
234 'user_agent': get_user_agent(environ),
234 'user_agent': get_user_agent(environ),
235 'hooks': get_enabled_hook_classes(ui_settings),
235 'hooks': get_enabled_hook_classes(ui_settings),
236 'is_shadow_repo': is_shadow_repo,
236 'is_shadow_repo': is_shadow_repo,
237 }
237 }
238 return extras
238 return extras
239
239
240
240
241 class BasicAuth(AuthBasicAuthenticator):
241 class BasicAuth(AuthBasicAuthenticator):
242
242
243 def __init__(self, realm, authfunc, registry, auth_http_code=None,
243 def __init__(self, realm, authfunc, registry, auth_http_code=None,
244 initial_call_detection=False, acl_repo_name=None):
244 initial_call_detection=False, acl_repo_name=None):
245 self.realm = realm
245 self.realm = realm
246 self.initial_call = initial_call_detection
246 self.initial_call = initial_call_detection
247 self.authfunc = authfunc
247 self.authfunc = authfunc
248 self.registry = registry
248 self.registry = registry
249 self.acl_repo_name = acl_repo_name
249 self.acl_repo_name = acl_repo_name
250 self._rc_auth_http_code = auth_http_code
250 self._rc_auth_http_code = auth_http_code
251
251
252 def _get_response_from_code(self, http_code):
252 def _get_response_from_code(self, http_code):
253 try:
253 try:
254 return get_exception(safe_int(http_code))
254 return get_exception(safe_int(http_code))
255 except Exception:
255 except Exception:
256 log.exception('Failed to fetch response for code %s' % http_code)
256 log.exception('Failed to fetch response for code %s' % http_code)
257 return HTTPForbidden
257 return HTTPForbidden
258
258
259 def build_authentication(self):
259 def build_authentication(self):
260 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
260 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
261 if self._rc_auth_http_code and not self.initial_call:
261 if self._rc_auth_http_code and not self.initial_call:
262 # return alternative HTTP code if alternative http return code
262 # return alternative HTTP code if alternative http return code
263 # is specified in RhodeCode config, but ONLY if it's not the
263 # is specified in RhodeCode config, but ONLY if it's not the
264 # FIRST call
264 # FIRST call
265 custom_response_klass = self._get_response_from_code(
265 custom_response_klass = self._get_response_from_code(
266 self._rc_auth_http_code)
266 self._rc_auth_http_code)
267 return custom_response_klass(headers=head)
267 return custom_response_klass(headers=head)
268 return HTTPUnauthorized(headers=head)
268 return HTTPUnauthorized(headers=head)
269
269
270 def authenticate(self, environ):
270 def authenticate(self, environ):
271 authorization = AUTHORIZATION(environ)
271 authorization = AUTHORIZATION(environ)
272 if not authorization:
272 if not authorization:
273 return self.build_authentication()
273 return self.build_authentication()
274 (authmeth, auth) = authorization.split(' ', 1)
274 (authmeth, auth) = authorization.split(' ', 1)
275 if 'basic' != authmeth.lower():
275 if 'basic' != authmeth.lower():
276 return self.build_authentication()
276 return self.build_authentication()
277 auth = auth.strip().decode('base64')
277 auth = auth.strip().decode('base64')
278 _parts = auth.split(':', 1)
278 _parts = auth.split(':', 1)
279 if len(_parts) == 2:
279 if len(_parts) == 2:
280 username, password = _parts
280 username, password = _parts
281 if self.authfunc(
281 if self.authfunc(
282 username, password, environ, VCS_TYPE,
282 username, password, environ, VCS_TYPE,
283 registry=self.registry, acl_repo_name=self.acl_repo_name):
283 registry=self.registry, acl_repo_name=self.acl_repo_name):
284 return username
284 return username
285 if username and password:
285 if username and password:
286 # we mark that we actually executed authentication once, at
286 # we mark that we actually executed authentication once, at
287 # that point we can use the alternative auth code
287 # that point we can use the alternative auth code
288 self.initial_call = False
288 self.initial_call = False
289
289
290 return self.build_authentication()
290 return self.build_authentication()
291
291
292 __call__ = authenticate
292 __call__ = authenticate
293
293
294
294
295 def calculate_version_hash():
295 def calculate_version_hash():
296 return md5(
296 return md5(
297 config.get('beaker.session.secret', '') +
297 config.get('beaker.session.secret', '') +
298 rhodecode.__version__)[:8]
298 rhodecode.__version__)[:8]
299
299
300
300
301 def get_current_lang(request):
301 def get_current_lang(request):
302 # NOTE(marcink): remove after pyramid move
302 # NOTE(marcink): remove after pyramid move
303 try:
303 try:
304 return translation.get_lang()[0]
304 return translation.get_lang()[0]
305 except:
305 except:
306 pass
306 pass
307
307
308 return getattr(request, '_LOCALE_', request.locale_name)
308 return getattr(request, '_LOCALE_', request.locale_name)
309
309
310
310
311 def attach_context_attributes(context, request, user_id):
311 def attach_context_attributes(context, request, user_id):
312 """
312 """
313 Attach variables into template context called `c`, please note that
313 Attach variables into template context called `c`, please note that
314 request could be pylons or pyramid request in here.
314 request could be pylons or pyramid request in here.
315 """
315 """
316
316
317 rc_config = SettingsModel().get_all_settings(cache=True)
317 rc_config = SettingsModel().get_all_settings(cache=True)
318
318
319 context.rhodecode_version = rhodecode.__version__
319 context.rhodecode_version = rhodecode.__version__
320 context.rhodecode_edition = config.get('rhodecode.edition')
320 context.rhodecode_edition = config.get('rhodecode.edition')
321 # unique secret + version does not leak the version but keep consistency
321 # unique secret + version does not leak the version but keep consistency
322 context.rhodecode_version_hash = calculate_version_hash()
322 context.rhodecode_version_hash = calculate_version_hash()
323
323
324 # Default language set for the incoming request
324 # Default language set for the incoming request
325 context.language = get_current_lang(request)
325 context.language = get_current_lang(request)
326
326
327 # Visual options
327 # Visual options
328 context.visual = AttributeDict({})
328 context.visual = AttributeDict({})
329
329
330 # DB stored Visual Items
330 # DB stored Visual Items
331 context.visual.show_public_icon = str2bool(
331 context.visual.show_public_icon = str2bool(
332 rc_config.get('rhodecode_show_public_icon'))
332 rc_config.get('rhodecode_show_public_icon'))
333 context.visual.show_private_icon = str2bool(
333 context.visual.show_private_icon = str2bool(
334 rc_config.get('rhodecode_show_private_icon'))
334 rc_config.get('rhodecode_show_private_icon'))
335 context.visual.stylify_metatags = str2bool(
335 context.visual.stylify_metatags = str2bool(
336 rc_config.get('rhodecode_stylify_metatags'))
336 rc_config.get('rhodecode_stylify_metatags'))
337 context.visual.dashboard_items = safe_int(
337 context.visual.dashboard_items = safe_int(
338 rc_config.get('rhodecode_dashboard_items', 100))
338 rc_config.get('rhodecode_dashboard_items', 100))
339 context.visual.admin_grid_items = safe_int(
339 context.visual.admin_grid_items = safe_int(
340 rc_config.get('rhodecode_admin_grid_items', 100))
340 rc_config.get('rhodecode_admin_grid_items', 100))
341 context.visual.repository_fields = str2bool(
341 context.visual.repository_fields = str2bool(
342 rc_config.get('rhodecode_repository_fields'))
342 rc_config.get('rhodecode_repository_fields'))
343 context.visual.show_version = str2bool(
343 context.visual.show_version = str2bool(
344 rc_config.get('rhodecode_show_version'))
344 rc_config.get('rhodecode_show_version'))
345 context.visual.use_gravatar = str2bool(
345 context.visual.use_gravatar = str2bool(
346 rc_config.get('rhodecode_use_gravatar'))
346 rc_config.get('rhodecode_use_gravatar'))
347 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
347 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
348 context.visual.default_renderer = rc_config.get(
348 context.visual.default_renderer = rc_config.get(
349 'rhodecode_markup_renderer', 'rst')
349 'rhodecode_markup_renderer', 'rst')
350 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
350 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
351 context.visual.rhodecode_support_url = \
351 context.visual.rhodecode_support_url = \
352 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
352 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
353
353
354 context.visual.affected_files_cut_off = 60
354 context.visual.affected_files_cut_off = 60
355
355
356 context.pre_code = rc_config.get('rhodecode_pre_code')
356 context.pre_code = rc_config.get('rhodecode_pre_code')
357 context.post_code = rc_config.get('rhodecode_post_code')
357 context.post_code = rc_config.get('rhodecode_post_code')
358 context.rhodecode_name = rc_config.get('rhodecode_title')
358 context.rhodecode_name = rc_config.get('rhodecode_title')
359 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
359 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
360 # if we have specified default_encoding in the request, it has more
360 # if we have specified default_encoding in the request, it has more
361 # priority
361 # priority
362 if request.GET.get('default_encoding'):
362 if request.GET.get('default_encoding'):
363 context.default_encodings.insert(0, request.GET.get('default_encoding'))
363 context.default_encodings.insert(0, request.GET.get('default_encoding'))
364 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
364 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
365
365
366 # INI stored
366 # INI stored
367 context.labs_active = str2bool(
367 context.labs_active = str2bool(
368 config.get('labs_settings_active', 'false'))
368 config.get('labs_settings_active', 'false'))
369 context.visual.allow_repo_location_change = str2bool(
369 context.visual.allow_repo_location_change = str2bool(
370 config.get('allow_repo_location_change', True))
370 config.get('allow_repo_location_change', True))
371 context.visual.allow_custom_hooks_settings = str2bool(
371 context.visual.allow_custom_hooks_settings = str2bool(
372 config.get('allow_custom_hooks_settings', True))
372 config.get('allow_custom_hooks_settings', True))
373 context.debug_style = str2bool(config.get('debug_style', False))
373 context.debug_style = str2bool(config.get('debug_style', False))
374
374
375 context.rhodecode_instanceid = config.get('instance_id')
375 context.rhodecode_instanceid = config.get('instance_id')
376
376
377 context.visual.cut_off_limit_diff = safe_int(
377 context.visual.cut_off_limit_diff = safe_int(
378 config.get('cut_off_limit_diff'))
378 config.get('cut_off_limit_diff'))
379 context.visual.cut_off_limit_file = safe_int(
379 context.visual.cut_off_limit_file = safe_int(
380 config.get('cut_off_limit_file'))
380 config.get('cut_off_limit_file'))
381
381
382 # AppEnlight
382 # AppEnlight
383 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
383 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
384 context.appenlight_api_public_key = config.get(
384 context.appenlight_api_public_key = config.get(
385 'appenlight.api_public_key', '')
385 'appenlight.api_public_key', '')
386 context.appenlight_server_url = config.get('appenlight.server_url', '')
386 context.appenlight_server_url = config.get('appenlight.server_url', '')
387
387
388 # JS template context
388 # JS template context
389 context.template_context = {
389 context.template_context = {
390 'repo_name': None,
390 'repo_name': None,
391 'repo_type': None,
391 'repo_type': None,
392 'repo_landing_commit': None,
392 'repo_landing_commit': None,
393 'rhodecode_user': {
393 'rhodecode_user': {
394 'username': None,
394 'username': None,
395 'email': None,
395 'email': None,
396 'notification_status': False
396 'notification_status': False
397 },
397 },
398 'visual': {
398 'visual': {
399 'default_renderer': None
399 'default_renderer': None
400 },
400 },
401 'commit_data': {
401 'commit_data': {
402 'commit_id': None
402 'commit_id': None
403 },
403 },
404 'pull_request_data': {'pull_request_id': None},
404 'pull_request_data': {'pull_request_id': None},
405 'timeago': {
405 'timeago': {
406 'refresh_time': 120 * 1000,
406 'refresh_time': 120 * 1000,
407 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
407 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
408 },
408 },
409 'pylons_dispatch': {
409 'pylons_dispatch': {
410 # 'controller': request.environ['pylons.routes_dict']['controller'],
410 # 'controller': request.environ['pylons.routes_dict']['controller'],
411 # 'action': request.environ['pylons.routes_dict']['action'],
411 # 'action': request.environ['pylons.routes_dict']['action'],
412 },
412 },
413 'pyramid_dispatch': {
413 'pyramid_dispatch': {
414
414
415 },
415 },
416 'extra': {'plugins': {}}
416 'extra': {'plugins': {}}
417 }
417 }
418 # END CONFIG VARS
418 # END CONFIG VARS
419
419
420 # TODO: This dosn't work when called from pylons compatibility tween.
420 # TODO: This dosn't work when called from pylons compatibility tween.
421 # Fix this and remove it from base controller.
421 # Fix this and remove it from base controller.
422 # context.repo_name = get_repo_slug(request) # can be empty
422 # context.repo_name = get_repo_slug(request) # can be empty
423
423
424 diffmode = 'sideside'
424 diffmode = 'sideside'
425 if request.GET.get('diffmode'):
425 if request.GET.get('diffmode'):
426 if request.GET['diffmode'] == 'unified':
426 if request.GET['diffmode'] == 'unified':
427 diffmode = 'unified'
427 diffmode = 'unified'
428 elif request.session.get('diffmode'):
428 elif request.session.get('diffmode'):
429 diffmode = request.session['diffmode']
429 diffmode = request.session['diffmode']
430
430
431 context.diffmode = diffmode
431 context.diffmode = diffmode
432
432
433 if request.session.get('diffmode') != diffmode:
433 if request.session.get('diffmode') != diffmode:
434 request.session['diffmode'] = diffmode
434 request.session['diffmode'] = diffmode
435
435
436 context.csrf_token = auth.get_csrf_token(session=request.session)
436 context.csrf_token = auth.get_csrf_token(session=request.session)
437 context.backends = rhodecode.BACKENDS.keys()
437 context.backends = rhodecode.BACKENDS.keys()
438 context.backends.sort()
438 context.backends.sort()
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
439 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
440
440
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
441 # NOTE(marcink): when migrated to pyramid we don't need to set this anymore,
442 # given request will ALWAYS be pyramid one
442 # given request will ALWAYS be pyramid one
443 pyramid_request = pyramid.threadlocal.get_current_request()
443 pyramid_request = pyramid.threadlocal.get_current_request()
444 context.pyramid_request = pyramid_request
444 context.pyramid_request = pyramid_request
445
445
446 # web case
446 # web case
447 if hasattr(pyramid_request, 'user'):
447 if hasattr(pyramid_request, 'user'):
448 context.auth_user = pyramid_request.user
448 context.auth_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
449 context.rhodecode_user = pyramid_request.user
450
450
451 # api case
451 # api case
452 if hasattr(pyramid_request, 'rpc_user'):
452 if hasattr(pyramid_request, 'rpc_user'):
453 context.auth_user = pyramid_request.rpc_user
453 context.auth_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
454 context.rhodecode_user = pyramid_request.rpc_user
455
455
456 # attach the whole call context to the request
456 # attach the whole call context to the request
457 request.call_context = context
457 request.call_context = context
458
458
459
459
460 def get_auth_user(request):
460 def get_auth_user(request):
461 environ = request.environ
461 environ = request.environ
462 session = request.session
462 session = request.session
463
463
464 ip_addr = get_ip_addr(environ)
464 ip_addr = get_ip_addr(environ)
465 # make sure that we update permissions each time we call controller
465 # make sure that we update permissions each time we call controller
466 _auth_token = (request.GET.get('auth_token', '') or
466 _auth_token = (request.GET.get('auth_token', '') or
467 request.GET.get('api_key', ''))
467 request.GET.get('api_key', ''))
468
468
469 if _auth_token:
469 if _auth_token:
470 # when using API_KEY we assume user exists, and
470 # when using API_KEY we assume user exists, and
471 # doesn't need auth based on cookies.
471 # doesn't need auth based on cookies.
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
472 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
473 authenticated = False
473 authenticated = False
474 else:
474 else:
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
475 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
476 try:
476 try:
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
477 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
478 ip_addr=ip_addr)
478 ip_addr=ip_addr)
479 except UserCreationError as e:
479 except UserCreationError as e:
480 h.flash(e, 'error')
480 h.flash(e, 'error')
481 # container auth or other auth functions that create users
481 # container auth or other auth functions that create users
482 # on the fly can throw this exception signaling that there's
482 # on the fly can throw this exception signaling that there's
483 # issue with user creation, explanation should be provided
483 # issue with user creation, explanation should be provided
484 # in Exception itself. We then create a simple blank
484 # in Exception itself. We then create a simple blank
485 # AuthUser
485 # AuthUser
486 auth_user = AuthUser(ip_addr=ip_addr)
486 auth_user = AuthUser(ip_addr=ip_addr)
487
487
488 if password_changed(auth_user, session):
488 if password_changed(auth_user, session):
489 session.invalidate()
489 session.invalidate()
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
490 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
491 auth_user = AuthUser(ip_addr=ip_addr)
491 auth_user = AuthUser(ip_addr=ip_addr)
492
492
493 authenticated = cookie_store.get('is_authenticated')
493 authenticated = cookie_store.get('is_authenticated')
494
494
495 if not auth_user.is_authenticated and auth_user.is_user_object:
495 if not auth_user.is_authenticated and auth_user.is_user_object:
496 # user is not authenticated and not empty
496 # user is not authenticated and not empty
497 auth_user.set_authenticated(authenticated)
497 auth_user.set_authenticated(authenticated)
498
498
499 return auth_user
499 return auth_user
500
500
501
501
502 class BaseController(WSGIController):
502 class BaseController(WSGIController):
503
503
504 def __before__(self):
504 def __before__(self):
505 """
505 """
506 __before__ is called before controller methods and after __call__
506 __before__ is called before controller methods and after __call__
507 """
507 """
508 # on each call propagate settings calls into global settings.
508 # on each call propagate settings calls into global settings.
509 set_rhodecode_config(config)
509 set_rhodecode_config(config)
510 attach_context_attributes(c, request, self._rhodecode_user.user_id)
510 attach_context_attributes(c, request, self._rhodecode_user.user_id)
511
511
512 # TODO: Remove this when fixed in attach_context_attributes()
512 # TODO: Remove this when fixed in attach_context_attributes()
513 c.repo_name = get_repo_slug(request) # can be empty
513 c.repo_name = get_repo_slug(request) # can be empty
514
514
515 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
515 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
516 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
516 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
517 self.sa = meta.Session
517 self.sa = meta.Session
518 self.scm_model = ScmModel(self.sa)
518 self.scm_model = ScmModel(self.sa)
519
519
520 # set user language
520 # set user language
521 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
521 user_lang = getattr(c.pyramid_request, '_LOCALE_', None)
522 if user_lang:
522 if user_lang:
523 translation.set_lang(user_lang)
523 translation.set_lang(user_lang)
524 log.debug('set language to %s for user %s',
524 log.debug('set language to %s for user %s',
525 user_lang, self._rhodecode_user)
525 user_lang, self._rhodecode_user)
526
526
527 def _dispatch_redirect(self, with_url, environ, start_response):
527 def _dispatch_redirect(self, with_url, environ, start_response):
528 resp = HTTPFound(with_url)
528 resp = HTTPFound(with_url)
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
529 environ['SCRIPT_NAME'] = '' # handle prefix middleware
530 environ['PATH_INFO'] = with_url
530 environ['PATH_INFO'] = with_url
531 return resp(environ, start_response)
531 return resp(environ, start_response)
532
532
533 def __call__(self, environ, start_response):
533 def __call__(self, environ, start_response):
534 """Invoke the Controller"""
534 """Invoke the Controller"""
535 # WSGIController.__call__ dispatches to the Controller method
535 # WSGIController.__call__ dispatches to the Controller method
536 # the request is routed to. This routing information is
536 # the request is routed to. This routing information is
537 # available in environ['pylons.routes_dict']
537 # available in environ['pylons.routes_dict']
538 from rhodecode.lib import helpers as h
538 from rhodecode.lib import helpers as h
539
539
540 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
540 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
541 if environ.get('debugtoolbar.wants_pylons_context', False):
541 if environ.get('debugtoolbar.wants_pylons_context', False):
542 environ['debugtoolbar.pylons_context'] = c._current_obj()
542 environ['debugtoolbar.pylons_context'] = c._current_obj()
543
543
544 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
544 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
545 environ['pylons.routes_dict']['action']])
545 environ['pylons.routes_dict']['action']])
546
546
547 self.rc_config = SettingsModel().get_all_settings(cache=True)
547 self.rc_config = SettingsModel().get_all_settings(cache=True)
548 self.ip_addr = get_ip_addr(environ)
548 self.ip_addr = get_ip_addr(environ)
549
549
550 # The rhodecode auth user is looked up and passed through the
550 # The rhodecode auth user is looked up and passed through the
551 # environ by the pylons compatibility tween in pyramid.
551 # environ by the pylons compatibility tween in pyramid.
552 # So we can just grab it from there.
552 # So we can just grab it from there.
553 auth_user = environ['rc_auth_user']
553 auth_user = environ['rc_auth_user']
554
554
555 # set globals for auth user
555 # set globals for auth user
556 request.user = auth_user
556 request.user = auth_user
557 self._rhodecode_user = auth_user
557 self._rhodecode_user = auth_user
558
558
559 log.info('IP: %s User: %s accessed %s [%s]' % (
559 log.info('IP: %s User: %s accessed %s [%s]' % (
560 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
560 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
561 _route_name)
561 _route_name)
562 )
562 )
563
563
564 user_obj = auth_user.get_instance()
564 user_obj = auth_user.get_instance()
565 if user_obj and user_obj.user_data.get('force_password_change'):
565 if user_obj and user_obj.user_data.get('force_password_change'):
566 h.flash('You are required to change your password', 'warning',
566 h.flash('You are required to change your password', 'warning',
567 ignore_duplicate=True)
567 ignore_duplicate=True)
568 return self._dispatch_redirect(
568 return self._dispatch_redirect(
569 url('my_account_password'), environ, start_response)
569 url('my_account_password'), environ, start_response)
570
570
571 return WSGIController.__call__(self, environ, start_response)
571 return WSGIController.__call__(self, environ, start_response)
572
572
573
573
574 def h_filter(s):
574 def h_filter(s):
575 """
575 """
576 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
576 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
577 we wrap this with additional functionality that converts None to empty
577 we wrap this with additional functionality that converts None to empty
578 strings
578 strings
579 """
579 """
580 if s is None:
580 if s is None:
581 return markupsafe.Markup()
581 return markupsafe.Markup()
582 return markupsafe.escape(s)
582 return markupsafe.escape(s)
583
583
584
584
585 def add_events_routes(config):
586 """
587 Adds routing that can be used in events. Because some events are triggered
588 outside of pyramid context, we need to bootstrap request with some
589 routing registered
590 """
591 config.add_route(name='home', pattern='/')
592
593 config.add_route(name='repo_summary', pattern='/{repo_name}')
594 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
595 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
596
597 config.add_route(name='pullrequest_show',
598 pattern='/{repo_name}/pull-request/{pull_request_id}')
599 config.add_route(name='pull_requests_global',
600 pattern='/pull-request/{pull_request_id}')
601
602 config.add_route(name='repo_commit',
603 pattern='/{repo_name}/changeset/{commit_id}')
604 config.add_route(name='repo_files',
605 pattern='/{repo_name}/files/{commit_id}/{f_path}')
606
607
608 def bootstrap_request():
609 import pyramid.testing
610 request = pyramid.testing.DummyRequest()
611 config = pyramid.testing.setUp(request=request)
612 add_events_routes(config)
613
614
585 class BaseRepoController(BaseController):
615 class BaseRepoController(BaseController):
586 """
616 """
587 Base class for controllers responsible for loading all needed data for
617 Base class for controllers responsible for loading all needed data for
588 repository loaded items are
618 repository loaded items are
589
619
590 c.rhodecode_repo: instance of scm repository
620 c.rhodecode_repo: instance of scm repository
591 c.rhodecode_db_repo: instance of db
621 c.rhodecode_db_repo: instance of db
592 c.repository_requirements_missing: shows that repository specific data
622 c.repository_requirements_missing: shows that repository specific data
593 could not be displayed due to the missing requirements
623 could not be displayed due to the missing requirements
594 c.repository_pull_requests: show number of open pull requests
624 c.repository_pull_requests: show number of open pull requests
595 """
625 """
596
626
597 def __before__(self):
627 def __before__(self):
598 super(BaseRepoController, self).__before__()
628 super(BaseRepoController, self).__before__()
599 if c.repo_name: # extracted from routes
629 if c.repo_name: # extracted from routes
600 db_repo = Repository.get_by_repo_name(c.repo_name)
630 db_repo = Repository.get_by_repo_name(c.repo_name)
601 if not db_repo:
631 if not db_repo:
602 return
632 return
603
633
604 log.debug(
634 log.debug(
605 'Found repository in database %s with state `%s`',
635 'Found repository in database %s with state `%s`',
606 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
636 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
607 route = getattr(request.environ.get('routes.route'), 'name', '')
637 route = getattr(request.environ.get('routes.route'), 'name', '')
608
638
609 # allow to delete repos that are somehow damages in filesystem
639 # allow to delete repos that are somehow damages in filesystem
610 if route in ['delete_repo']:
640 if route in ['delete_repo']:
611 return
641 return
612
642
613 if db_repo.repo_state in [Repository.STATE_PENDING]:
643 if db_repo.repo_state in [Repository.STATE_PENDING]:
614 if route in ['repo_creating_home']:
644 if route in ['repo_creating_home']:
615 return
645 return
616 check_url = url('repo_creating_home', repo_name=c.repo_name)
646 check_url = url('repo_creating_home', repo_name=c.repo_name)
617 return redirect(check_url)
647 return redirect(check_url)
618
648
619 self.rhodecode_db_repo = db_repo
649 self.rhodecode_db_repo = db_repo
620
650
621 missing_requirements = False
651 missing_requirements = False
622 try:
652 try:
623 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
653 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
624 except RepositoryRequirementError as e:
654 except RepositoryRequirementError as e:
625 missing_requirements = True
655 missing_requirements = True
626 self._handle_missing_requirements(e)
656 self._handle_missing_requirements(e)
627
657
628 if self.rhodecode_repo is None and not missing_requirements:
658 if self.rhodecode_repo is None and not missing_requirements:
629 log.error('%s this repository is present in database but it '
659 log.error('%s this repository is present in database but it '
630 'cannot be created as an scm instance', c.repo_name)
660 'cannot be created as an scm instance', c.repo_name)
631
661
632 h.flash(_(
662 h.flash(_(
633 "The repository at %(repo_name)s cannot be located.") %
663 "The repository at %(repo_name)s cannot be located.") %
634 {'repo_name': c.repo_name},
664 {'repo_name': c.repo_name},
635 category='error', ignore_duplicate=True)
665 category='error', ignore_duplicate=True)
636 redirect(h.route_path('home'))
666 redirect(h.route_path('home'))
637
667
638 # update last change according to VCS data
668 # update last change according to VCS data
639 if not missing_requirements:
669 if not missing_requirements:
640 commit = db_repo.get_commit(
670 commit = db_repo.get_commit(
641 pre_load=["author", "date", "message", "parents"])
671 pre_load=["author", "date", "message", "parents"])
642 db_repo.update_commit_cache(commit)
672 db_repo.update_commit_cache(commit)
643
673
644 # Prepare context
674 # Prepare context
645 c.rhodecode_db_repo = db_repo
675 c.rhodecode_db_repo = db_repo
646 c.rhodecode_repo = self.rhodecode_repo
676 c.rhodecode_repo = self.rhodecode_repo
647 c.repository_requirements_missing = missing_requirements
677 c.repository_requirements_missing = missing_requirements
648
678
649 self._update_global_counters(self.scm_model, db_repo)
679 self._update_global_counters(self.scm_model, db_repo)
650
680
651 def _update_global_counters(self, scm_model, db_repo):
681 def _update_global_counters(self, scm_model, db_repo):
652 """
682 """
653 Base variables that are exposed to every page of repository
683 Base variables that are exposed to every page of repository
654 """
684 """
655 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
685 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
656
686
657 def _handle_missing_requirements(self, error):
687 def _handle_missing_requirements(self, error):
658 self.rhodecode_repo = None
688 self.rhodecode_repo = None
659 log.error(
689 log.error(
660 'Requirements are missing for repository %s: %s',
690 'Requirements are missing for repository %s: %s',
661 c.repo_name, error.message)
691 c.repo_name, error.message)
662
692
663 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
693 summary_url = h.route_path('repo_summary', repo_name=c.repo_name)
664 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
694 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
665 settings_update_url = url('repo', repo_name=c.repo_name)
695 settings_update_url = url('repo', repo_name=c.repo_name)
666 path = request.path
696 path = request.path
667 should_redirect = (
697 should_redirect = (
668 path not in (summary_url, settings_update_url)
698 path not in (summary_url, settings_update_url)
669 and '/settings' not in path or path == statistics_url
699 and '/settings' not in path or path == statistics_url
670 )
700 )
671 if should_redirect:
701 if should_redirect:
672 redirect(summary_url)
702 redirect(summary_url)
@@ -1,241 +1,237 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 json
21 import json
22 import logging
22 import logging
23 import traceback
23 import traceback
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27
27
28 import pylons
29 import rhodecode
28 import rhodecode
30
31 from rhodecode.model import meta
29 from rhodecode.model import meta
30 from rhodecode.lib.base import bootstrap_request
32 from rhodecode.lib import hooks_base
31 from rhodecode.lib import hooks_base
33 from rhodecode.lib.utils2 import (
32 from rhodecode.lib.utils2 import AttributeDict
34 AttributeDict, safe_str, get_routes_generator_for_server_url)
35
33
36
34
37 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
38
36
39
37
40 class HooksHttpHandler(BaseHTTPRequestHandler):
38 class HooksHttpHandler(BaseHTTPRequestHandler):
41 def do_POST(self):
39 def do_POST(self):
42 method, extras = self._read_request()
40 method, extras = self._read_request()
43 try:
41 try:
44 result = self._call_hook(method, extras)
42 result = self._call_hook(method, extras)
45 except Exception as e:
43 except Exception as e:
46 exc_tb = traceback.format_exc()
44 exc_tb = traceback.format_exc()
47 result = {
45 result = {
48 'exception': e.__class__.__name__,
46 'exception': e.__class__.__name__,
49 'exception_traceback': exc_tb,
47 'exception_traceback': exc_tb,
50 'exception_args': e.args
48 'exception_args': e.args
51 }
49 }
52 self._write_response(result)
50 self._write_response(result)
53
51
54 def _read_request(self):
52 def _read_request(self):
55 length = int(self.headers['Content-Length'])
53 length = int(self.headers['Content-Length'])
56 body = self.rfile.read(length).decode('utf-8')
54 body = self.rfile.read(length).decode('utf-8')
57 data = json.loads(body)
55 data = json.loads(body)
58 return data['method'], data['extras']
56 return data['method'], data['extras']
59
57
60 def _write_response(self, result):
58 def _write_response(self, result):
61 self.send_response(200)
59 self.send_response(200)
62 self.send_header("Content-type", "text/json")
60 self.send_header("Content-type", "text/json")
63 self.end_headers()
61 self.end_headers()
64 self.wfile.write(json.dumps(result))
62 self.wfile.write(json.dumps(result))
65
63
66 def _call_hook(self, method, extras):
64 def _call_hook(self, method, extras):
67 hooks = Hooks()
65 hooks = Hooks()
68 try:
66 try:
69 result = getattr(hooks, method)(extras)
67 result = getattr(hooks, method)(extras)
70 finally:
68 finally:
71 meta.Session.remove()
69 meta.Session.remove()
72 return result
70 return result
73
71
74 def log_message(self, format, *args):
72 def log_message(self, format, *args):
75 """
73 """
76 This is an overridden method of BaseHTTPRequestHandler which logs using
74 This is an overridden method of BaseHTTPRequestHandler which logs using
77 logging library instead of writing directly to stderr.
75 logging library instead of writing directly to stderr.
78 """
76 """
79
77
80 message = format % args
78 message = format % args
81
79
82 # TODO: mikhail: add different log levels support
80 # TODO: mikhail: add different log levels support
83 log.debug(
81 log.debug(
84 "%s - - [%s] %s", self.client_address[0],
82 "%s - - [%s] %s", self.client_address[0],
85 self.log_date_time_string(), message)
83 self.log_date_time_string(), message)
86
84
87
85
88 class DummyHooksCallbackDaemon(object):
86 class DummyHooksCallbackDaemon(object):
89 def __init__(self):
87 def __init__(self):
90 self.hooks_module = Hooks.__module__
88 self.hooks_module = Hooks.__module__
91
89
92 def __enter__(self):
90 def __enter__(self):
93 log.debug('Running dummy hooks callback daemon')
91 log.debug('Running dummy hooks callback daemon')
94 return self
92 return self
95
93
96 def __exit__(self, exc_type, exc_val, exc_tb):
94 def __exit__(self, exc_type, exc_val, exc_tb):
97 log.debug('Exiting dummy hooks callback daemon')
95 log.debug('Exiting dummy hooks callback daemon')
98
96
99
97
100 class ThreadedHookCallbackDaemon(object):
98 class ThreadedHookCallbackDaemon(object):
101
99
102 _callback_thread = None
100 _callback_thread = None
103 _daemon = None
101 _daemon = None
104 _done = False
102 _done = False
105
103
106 def __init__(self):
104 def __init__(self):
107 self._prepare()
105 self._prepare()
108
106
109 def __enter__(self):
107 def __enter__(self):
110 self._run()
108 self._run()
111 return self
109 return self
112
110
113 def __exit__(self, exc_type, exc_val, exc_tb):
111 def __exit__(self, exc_type, exc_val, exc_tb):
114 self._stop()
112 self._stop()
115
113
116 def _prepare(self):
114 def _prepare(self):
117 raise NotImplementedError()
115 raise NotImplementedError()
118
116
119 def _run(self):
117 def _run(self):
120 raise NotImplementedError()
118 raise NotImplementedError()
121
119
122 def _stop(self):
120 def _stop(self):
123 raise NotImplementedError()
121 raise NotImplementedError()
124
122
125
123
126 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
124 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
127 """
125 """
128 Context manager which will run a callback daemon in a background thread.
126 Context manager which will run a callback daemon in a background thread.
129 """
127 """
130
128
131 hooks_uri = None
129 hooks_uri = None
132
130
133 IP_ADDRESS = '127.0.0.1'
131 IP_ADDRESS = '127.0.0.1'
134
132
135 # From Python docs: Polling reduces our responsiveness to a shutdown
133 # From Python docs: Polling reduces our responsiveness to a shutdown
136 # request and wastes cpu at all other times.
134 # request and wastes cpu at all other times.
137 POLL_INTERVAL = 0.1
135 POLL_INTERVAL = 0.1
138
136
139 def _prepare(self):
137 def _prepare(self):
140 log.debug("Preparing callback daemon and registering hook object")
138 log.debug("Preparing callback daemon and registering hook object")
141
139
142 self._done = False
140 self._done = False
143 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
141 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
144 _, port = self._daemon.server_address
142 _, port = self._daemon.server_address
145 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
143 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
146
144
147 log.debug("Hooks uri is: %s", self.hooks_uri)
145 log.debug("Hooks uri is: %s", self.hooks_uri)
148
146
149 def _run(self):
147 def _run(self):
150 log.debug("Running event loop of callback daemon in background thread")
148 log.debug("Running event loop of callback daemon in background thread")
151 callback_thread = threading.Thread(
149 callback_thread = threading.Thread(
152 target=self._daemon.serve_forever,
150 target=self._daemon.serve_forever,
153 kwargs={'poll_interval': self.POLL_INTERVAL})
151 kwargs={'poll_interval': self.POLL_INTERVAL})
154 callback_thread.daemon = True
152 callback_thread.daemon = True
155 callback_thread.start()
153 callback_thread.start()
156 self._callback_thread = callback_thread
154 self._callback_thread = callback_thread
157
155
158 def _stop(self):
156 def _stop(self):
159 log.debug("Waiting for background thread to finish.")
157 log.debug("Waiting for background thread to finish.")
160 self._daemon.shutdown()
158 self._daemon.shutdown()
161 self._callback_thread.join()
159 self._callback_thread.join()
162 self._daemon = None
160 self._daemon = None
163 self._callback_thread = None
161 self._callback_thread = None
164
162
165
163
166 def prepare_callback_daemon(extras, protocol, use_direct_calls):
164 def prepare_callback_daemon(extras, protocol, use_direct_calls):
167 callback_daemon = None
165 callback_daemon = None
168
166
169 if use_direct_calls:
167 if use_direct_calls:
170 callback_daemon = DummyHooksCallbackDaemon()
168 callback_daemon = DummyHooksCallbackDaemon()
171 extras['hooks_module'] = callback_daemon.hooks_module
169 extras['hooks_module'] = callback_daemon.hooks_module
172 else:
170 else:
173 if protocol == 'http':
171 if protocol == 'http':
174 callback_daemon = HttpHooksCallbackDaemon()
172 callback_daemon = HttpHooksCallbackDaemon()
175 else:
173 else:
176 log.error('Unsupported callback daemon protocol "%s"', protocol)
174 log.error('Unsupported callback daemon protocol "%s"', protocol)
177 raise Exception('Unsupported callback daemon protocol.')
175 raise Exception('Unsupported callback daemon protocol.')
178
176
179 extras['hooks_uri'] = callback_daemon.hooks_uri
177 extras['hooks_uri'] = callback_daemon.hooks_uri
180 extras['hooks_protocol'] = protocol
178 extras['hooks_protocol'] = protocol
181
179
182 return callback_daemon, extras
180 return callback_daemon, extras
183
181
184
182
185 class Hooks(object):
183 class Hooks(object):
186 """
184 """
187 Exposes the hooks for remote call backs
185 Exposes the hooks for remote call backs
188 """
186 """
189
187
190 def repo_size(self, extras):
188 def repo_size(self, extras):
191 log.debug("Called repo_size of Hooks object")
189 log.debug("Called repo_size of %s object", self)
192 return self._call_hook(hooks_base.repo_size, extras)
190 return self._call_hook(hooks_base.repo_size, extras)
193
191
194 def pre_pull(self, extras):
192 def pre_pull(self, extras):
195 log.debug("Called pre_pull of Hooks object")
193 log.debug("Called pre_pull of %s object", self)
196 return self._call_hook(hooks_base.pre_pull, extras)
194 return self._call_hook(hooks_base.pre_pull, extras)
197
195
198 def post_pull(self, extras):
196 def post_pull(self, extras):
199 log.debug("Called post_pull of Hooks object")
197 log.debug("Called post_pull of %s object", self)
200 return self._call_hook(hooks_base.post_pull, extras)
198 return self._call_hook(hooks_base.post_pull, extras)
201
199
202 def pre_push(self, extras):
200 def pre_push(self, extras):
203 log.debug("Called pre_push of Hooks object")
201 log.debug("Called pre_push of %s object", self)
204 return self._call_hook(hooks_base.pre_push, extras)
202 return self._call_hook(hooks_base.pre_push, extras)
205
203
206 def post_push(self, extras):
204 def post_push(self, extras):
207 log.debug("Called post_push of Hooks object")
205 log.debug("Called post_push of %s object", self)
208 return self._call_hook(hooks_base.post_push, extras)
206 return self._call_hook(hooks_base.post_push, extras)
209
207
210 def _call_hook(self, hook, extras):
208 def _call_hook(self, hook, extras):
211 extras = AttributeDict(extras)
209 extras = AttributeDict(extras)
212 pylons_router = get_routes_generator_for_server_url(extras.server_url)
210 extras.request = bootstrap_request()
213 pylons.url._push_object(pylons_router)
214
211
215 try:
212 try:
216 result = hook(extras)
213 result = hook(extras)
217 except Exception as error:
214 except Exception as error:
218 exc_tb = traceback.format_exc()
215 exc_tb = traceback.format_exc()
219 log.exception('Exception when handling hook %s', hook)
216 log.exception('Exception when handling hook %s', hook)
220 error_args = error.args
217 error_args = error.args
221 return {
218 return {
222 'status': 128,
219 'status': 128,
223 'output': '',
220 'output': '',
224 'exception': type(error).__name__,
221 'exception': type(error).__name__,
225 'exception_traceback': exc_tb,
222 'exception_traceback': exc_tb,
226 'exception_args': error_args,
223 'exception_args': error_args,
227 }
224 }
228 finally:
225 finally:
229 pylons.url._pop_object()
230 meta.Session.remove()
226 meta.Session.remove()
231
227
232 return {
228 return {
233 'status': result.status,
229 'status': result.status,
234 'output': result.output,
230 'output': result.output,
235 }
231 }
236
232
237 def __enter__(self):
233 def __enter__(self):
238 return self
234 return self
239
235
240 def __exit__(self, exc_type, exc_val, exc_tb):
236 def __exit__(self, exc_type, exc_val, exc_tb):
241 pass
237 pass
General Comments 0
You need to be logged in to leave comments. Login now