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