##// END OF EJS Templates
hipchat: escape user entered data to avoid xss/formatting problems
dan -
r934:3d0f257a default
parent child Browse files
Show More
@@ -1,255 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
22 import deform
23 import re
23 import re
24 import logging
24 import logging
25 import requests
25 import requests
26 import colander
26 import colander
27 import textwrap
27 import textwrap
28 from celery.task import task
28 from celery.task import task
29 from mako.template import Template
29 from mako.template import Template
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.translation import _
32 from rhodecode.translation import _
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib.celerylib import run_task
34 from rhodecode.lib.celerylib import run_task
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.integrations.types.base import IntegrationTypeBase
36 from rhodecode.integrations.types.base import IntegrationTypeBase
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class HipchatSettingsSchema(colander.Schema):
41 class HipchatSettingsSchema(colander.Schema):
42 color_choices = [
42 color_choices = [
43 ('yellow', _('Yellow')),
43 ('yellow', _('Yellow')),
44 ('red', _('Red')),
44 ('red', _('Red')),
45 ('green', _('Green')),
45 ('green', _('Green')),
46 ('purple', _('Purple')),
46 ('purple', _('Purple')),
47 ('gray', _('Gray')),
47 ('gray', _('Gray')),
48 ]
48 ]
49
49
50 server_url = colander.SchemaNode(
50 server_url = colander.SchemaNode(
51 colander.String(),
51 colander.String(),
52 title=_('Hipchat server URL'),
52 title=_('Hipchat server URL'),
53 description=_('Hipchat integration url.'),
53 description=_('Hipchat integration url.'),
54 default='',
54 default='',
55 preparer=strip_whitespace,
55 preparer=strip_whitespace,
56 validator=colander.url,
56 validator=colander.url,
57 widget=deform.widget.TextInputWidget(
57 widget=deform.widget.TextInputWidget(
58 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
58 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
59 ),
59 ),
60 )
60 )
61 notify = colander.SchemaNode(
61 notify = colander.SchemaNode(
62 colander.Bool(),
62 colander.Bool(),
63 title=_('Notify'),
63 title=_('Notify'),
64 description=_('Make a notification to the users in room.'),
64 description=_('Make a notification to the users in room.'),
65 missing=False,
65 missing=False,
66 default=False,
66 default=False,
67 )
67 )
68 color = colander.SchemaNode(
68 color = colander.SchemaNode(
69 colander.String(),
69 colander.String(),
70 title=_('Color'),
70 title=_('Color'),
71 description=_('Background color of message.'),
71 description=_('Background color of message.'),
72 missing='',
72 missing='',
73 validator=colander.OneOf([x[0] for x in color_choices]),
73 validator=colander.OneOf([x[0] for x in color_choices]),
74 widget=deform.widget.Select2Widget(
74 widget=deform.widget.Select2Widget(
75 values=color_choices,
75 values=color_choices,
76 ),
76 ),
77 )
77 )
78
78
79
79
80 repo_push_template = Template('''
80 repo_push_template = Template('''
81 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
81 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
82 <br>
82 <br>
83 <ul>
83 <ul>
84 %for branch, branch_commits in branches_commits.items():
84 %for branch, branch_commits in branches_commits.items():
85 <li>
85 <li>
86 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
86 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
87 <ul>
87 <ul>
88 %for commit in branch_commits['commits']:
88 %for commit in branch_commits['commits']:
89 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
89 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
90 %endfor
90 %endfor
91 </ul>
91 </ul>
92 </li>
92 </li>
93 %endfor
93 %endfor
94 ''')
94 ''')
95
95
96
96
97 class HipchatIntegrationType(IntegrationTypeBase):
97 class HipchatIntegrationType(IntegrationTypeBase):
98 key = 'hipchat'
98 key = 'hipchat'
99 display_name = _('Hipchat')
99 display_name = _('Hipchat')
100 description = _('Send events such as repo pushes and pull requests to '
100 description = _('Send events such as repo pushes and pull requests to '
101 'your hipchat channel.')
101 'your hipchat channel.')
102 icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
102 icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
103 valid_events = [
103 valid_events = [
104 events.PullRequestCloseEvent,
104 events.PullRequestCloseEvent,
105 events.PullRequestMergeEvent,
105 events.PullRequestMergeEvent,
106 events.PullRequestUpdateEvent,
106 events.PullRequestUpdateEvent,
107 events.PullRequestCommentEvent,
107 events.PullRequestCommentEvent,
108 events.PullRequestReviewEvent,
108 events.PullRequestReviewEvent,
109 events.PullRequestCreateEvent,
109 events.PullRequestCreateEvent,
110 events.RepoPushEvent,
110 events.RepoPushEvent,
111 events.RepoCreateEvent,
111 events.RepoCreateEvent,
112 ]
112 ]
113
113
114 def send_event(self, event):
114 def send_event(self, event):
115 if event.__class__ not in self.valid_events:
115 if event.__class__ not in self.valid_events:
116 log.debug('event not valid: %r' % event)
116 log.debug('event not valid: %r' % event)
117 return
117 return
118
118
119 if event.name not in self.settings['events']:
119 if event.name not in self.settings['events']:
120 log.debug('event ignored: %r' % event)
120 log.debug('event ignored: %r' % event)
121 return
121 return
122
122
123 data = event.as_dict()
123 data = event.as_dict()
124
124
125 text = '<b>%s<b> caused a <b>%s</b> event' % (
125 text = '<b>%s<b> caused a <b>%s</b> event' % (
126 data['actor']['username'], event.name)
126 data['actor']['username'], event.name)
127
127
128 log.debug('handling hipchat event for %s' % event.name)
128 log.debug('handling hipchat event for %s' % event.name)
129
129
130 if isinstance(event, events.PullRequestCommentEvent):
130 if isinstance(event, events.PullRequestCommentEvent):
131 text = self.format_pull_request_comment_event(event, data)
131 text = self.format_pull_request_comment_event(event, data)
132 elif isinstance(event, events.PullRequestReviewEvent):
132 elif isinstance(event, events.PullRequestReviewEvent):
133 text = self.format_pull_request_review_event(event, data)
133 text = self.format_pull_request_review_event(event, data)
134 elif isinstance(event, events.PullRequestEvent):
134 elif isinstance(event, events.PullRequestEvent):
135 text = self.format_pull_request_event(event, data)
135 text = self.format_pull_request_event(event, data)
136 elif isinstance(event, events.RepoPushEvent):
136 elif isinstance(event, events.RepoPushEvent):
137 text = self.format_repo_push_event(data)
137 text = self.format_repo_push_event(data)
138 elif isinstance(event, events.RepoCreateEvent):
138 elif isinstance(event, events.RepoCreateEvent):
139 text = self.format_repo_create_event(data)
139 text = self.format_repo_create_event(data)
140 else:
140 else:
141 log.error('unhandled event type: %r' % event)
141 log.error('unhandled event type: %r' % event)
142
142
143 run_task(post_text_to_hipchat, self.settings, text)
143 run_task(post_text_to_hipchat, self.settings, text)
144
144
145 def settings_schema(self):
145 def settings_schema(self):
146 schema = HipchatSettingsSchema()
146 schema = HipchatSettingsSchema()
147 schema.add(colander.SchemaNode(
147 schema.add(colander.SchemaNode(
148 colander.Set(),
148 colander.Set(),
149 widget=deform.widget.CheckboxChoiceWidget(
149 widget=deform.widget.CheckboxChoiceWidget(
150 values=sorted(
150 values=sorted(
151 [(e.name, e.display_name) for e in self.valid_events]
151 [(e.name, e.display_name) for e in self.valid_events]
152 )
152 )
153 ),
153 ),
154 description="Events activated for this integration",
154 description="Events activated for this integration",
155 name='events'
155 name='events'
156 ))
156 ))
157
157
158 return schema
158 return schema
159
159
160 def format_pull_request_comment_event(self, event, data):
160 def format_pull_request_comment_event(self, event, data):
161 comment_text = data['comment']['text']
161 comment_text = data['comment']['text']
162 if len(comment_text) > 200:
162 if len(comment_text) > 200:
163 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
163 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
164 comment_text=comment_text[:200],
164 comment_text=h.html_escape(comment_text[:200]),
165 comment_url=data['comment']['url'],
165 comment_url=data['comment']['url'],
166 )
166 )
167
167
168 comment_status = ''
168 comment_status = ''
169 if data['comment']['status']:
169 if data['comment']['status']:
170 comment_status = '[{}]: '.format(data['comment']['status'])
170 comment_status = '[{}]: '.format(data['comment']['status'])
171
171
172 return (textwrap.dedent(
172 return (textwrap.dedent(
173 '''
173 '''
174 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
174 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
175 >>> {comment_status}{comment_text}
175 >>> {comment_status}{comment_text}
176 ''').format(
176 ''').format(
177 comment_status=comment_status,
177 comment_status=comment_status,
178 user=data['actor']['username'],
178 user=data['actor']['username'],
179 number=data['pullrequest']['pull_request_id'],
179 number=data['pullrequest']['pull_request_id'],
180 pr_url=data['pullrequest']['url'],
180 pr_url=data['pullrequest']['url'],
181 pr_status=data['pullrequest']['status'],
181 pr_status=data['pullrequest']['status'],
182 pr_title=data['pullrequest']['title'],
182 pr_title=h.html_escape(data['pullrequest']['title']),
183 comment_text=comment_text
183 comment_text=h.html_escape(comment_text)
184 )
184 )
185 )
185 )
186
186
187 def format_pull_request_review_event(self, event, data):
187 def format_pull_request_review_event(self, event, data):
188 return (textwrap.dedent(
188 return (textwrap.dedent(
189 '''
189 '''
190 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
190 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
191 ''').format(
191 ''').format(
192 user=data['actor']['username'],
192 user=data['actor']['username'],
193 number=data['pullrequest']['pull_request_id'],
193 number=data['pullrequest']['pull_request_id'],
194 pr_url=data['pullrequest']['url'],
194 pr_url=data['pullrequest']['url'],
195 pr_status=data['pullrequest']['status'],
195 pr_status=data['pullrequest']['status'],
196 pr_title=data['pullrequest']['title'],
196 pr_title=h.html_escape(data['pullrequest']['title']),
197 )
197 )
198 )
198 )
199
199
200 def format_pull_request_event(self, event, data):
200 def format_pull_request_event(self, event, data):
201 action = {
201 action = {
202 events.PullRequestCloseEvent: 'closed',
202 events.PullRequestCloseEvent: 'closed',
203 events.PullRequestMergeEvent: 'merged',
203 events.PullRequestMergeEvent: 'merged',
204 events.PullRequestUpdateEvent: 'updated',
204 events.PullRequestUpdateEvent: 'updated',
205 events.PullRequestCreateEvent: 'created',
205 events.PullRequestCreateEvent: 'created',
206 }.get(event.__class__, str(event.__class__))
206 }.get(event.__class__, str(event.__class__))
207
207
208 return ('Pull request <a href="{url}">#{number}</a> - {title} '
208 return ('Pull request <a href="{url}">#{number}</a> - {title} '
209 '{action} by {user}').format(
209 '{action} by {user}').format(
210 user=data['actor']['username'],
210 user=data['actor']['username'],
211 number=data['pullrequest']['pull_request_id'],
211 number=data['pullrequest']['pull_request_id'],
212 url=data['pullrequest']['url'],
212 url=data['pullrequest']['url'],
213 title=data['pullrequest']['title'],
213 title=h.html_escape(data['pullrequest']['title']),
214 action=action
214 action=action
215 )
215 )
216
216
217 def format_repo_push_event(self, data):
217 def format_repo_push_event(self, data):
218 branch_data = {branch['name']: branch
218 branch_data = {branch['name']: branch
219 for branch in data['push']['branches']}
219 for branch in data['push']['branches']}
220
220
221 branches_commits = {}
221 branches_commits = {}
222 for commit in data['push']['commits']:
222 for commit in data['push']['commits']:
223 log.critical(commit)
223 log.critical(commit)
224 if commit['branch'] not in branches_commits:
224 if commit['branch'] not in branches_commits:
225 branch_commits = {'branch': branch_data[commit['branch']],
225 branch_commits = {'branch': branch_data[commit['branch']],
226 'commits': []}
226 'commits': []}
227 branches_commits[commit['branch']] = branch_commits
227 branches_commits[commit['branch']] = branch_commits
228
228
229 branch_commits = branches_commits[commit['branch']]
229 branch_commits = branches_commits[commit['branch']]
230 branch_commits['commits'].append(commit)
230 branch_commits['commits'].append(commit)
231
231
232 result = repo_push_template.render(
232 result = repo_push_template.render(
233 data=data,
233 data=data,
234 branches_commits=branches_commits,
234 branches_commits=branches_commits,
235 )
235 )
236 return result
236 return result
237
237
238 def format_repo_create_event(self, data):
238 def format_repo_create_event(self, data):
239 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
239 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
240 data['repo']['url'],
240 data['repo']['url'],
241 data['repo']['repo_name'],
241 h.html_escape(data['repo']['repo_name']),
242 data['repo']['repo_type'],
242 data['repo']['repo_type'],
243 data['actor']['username'],
243 data['actor']['username'],
244 )
244 )
245
245
246
246
247 @task(ignore_result=True)
247 @task(ignore_result=True)
248 def post_text_to_hipchat(settings, text):
248 def post_text_to_hipchat(settings, text):
249 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
249 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
250 resp = requests.post(settings['server_url'], json={
250 resp = requests.post(settings['server_url'], json={
251 "message": text,
251 "message": text,
252 "color": settings.get('color', 'yellow'),
252 "color": settings.get('color', 'yellow'),
253 "notify": settings.get('notify', False),
253 "notify": settings.get('notify', False),
254 })
254 })
255 resp.raise_for_status() # raise exception on a failed request
255 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now