##// END OF EJS Templates
issue-trackers: fix XSS with description field.
marcink -
r4487:779812b2 default
parent child Browse files
Show More
@@ -1,2106 +1,2106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27 import base64
27 import base64
28
28
29 import os
29 import os
30 import random
30 import random
31 import hashlib
31 import hashlib
32 import StringIO
32 import StringIO
33 import textwrap
33 import textwrap
34 import urllib
34 import urllib
35 import math
35 import math
36 import logging
36 import logging
37 import re
37 import re
38 import time
38 import time
39 import string
39 import string
40 import hashlib
40 import hashlib
41 from collections import OrderedDict
41 from collections import OrderedDict
42
42
43 import pygments
43 import pygments
44 import itertools
44 import itertools
45 import fnmatch
45 import fnmatch
46 import bleach
46 import bleach
47
47
48 from pyramid import compat
48 from pyramid import compat
49 from datetime import datetime
49 from datetime import datetime
50 from functools import partial
50 from functools import partial
51 from pygments.formatters.html import HtmlFormatter
51 from pygments.formatters.html import HtmlFormatter
52 from pygments.lexers import (
52 from pygments.lexers import (
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54
54
55 from pyramid.threadlocal import get_current_request
55 from pyramid.threadlocal import get_current_request
56 from tempita import looper
56 from tempita import looper
57 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html import literal, HTML, escape
58 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html._autolink import _auto_link_urls
59 from webhelpers2.html.tools import (
59 from webhelpers2.html.tools import (
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61
61
62 from webhelpers2.text import (
62 from webhelpers2.text import (
63 chop_at, collapse, convert_accented_entities,
63 chop_at, collapse, convert_accented_entities,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 from webhelpers2.date import time_ago_in_words
66 from webhelpers2.date import time_ago_in_words
67
67
68 from webhelpers2.html.tags import (
68 from webhelpers2.html.tags import (
69 _input, NotGiven, _make_safe_id_component as safeid,
69 _input, NotGiven, _make_safe_id_component as safeid,
70 form as insecure_form,
70 form as insecure_form,
71 auto_discovery_link, checkbox, end_form, file,
71 auto_discovery_link, checkbox, end_form, file,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 ul, radio, Options)
74 ul, radio, Options)
75
75
76 from webhelpers2.number import format_byte_size
76 from webhelpers2.number import format_byte_size
77
77
78 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.action_parser import action_parser
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.ext_json import json
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 from rhodecode.lib.utils2 import (
82 from rhodecode.lib.utils2 import (
83 str2bool, safe_unicode, safe_str,
83 str2bool, safe_unicode, safe_str,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 from rhodecode.model.changeset_status import ChangesetStatusModel
92 from rhodecode.model.changeset_status import ChangesetStatusModel
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
94 from rhodecode.model.repo_group import RepoGroupModel
94 from rhodecode.model.repo_group import RepoGroupModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
96
96
97
97
98 log = logging.getLogger(__name__)
98 log = logging.getLogger(__name__)
99
99
100
100
101 DEFAULT_USER = User.DEFAULT_USER
101 DEFAULT_USER = User.DEFAULT_USER
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
103
103
104
104
105 def asset(path, ver=None, **kwargs):
105 def asset(path, ver=None, **kwargs):
106 """
106 """
107 Helper to generate a static asset file path for rhodecode assets
107 Helper to generate a static asset file path for rhodecode assets
108
108
109 eg. h.asset('images/image.png', ver='3923')
109 eg. h.asset('images/image.png', ver='3923')
110
110
111 :param path: path of asset
111 :param path: path of asset
112 :param ver: optional version query param to append as ?ver=
112 :param ver: optional version query param to append as ?ver=
113 """
113 """
114 request = get_current_request()
114 request = get_current_request()
115 query = {}
115 query = {}
116 query.update(kwargs)
116 query.update(kwargs)
117 if ver:
117 if ver:
118 query = {'ver': ver}
118 query = {'ver': ver}
119 return request.static_path(
119 return request.static_path(
120 'rhodecode:public/{}'.format(path), _query=query)
120 'rhodecode:public/{}'.format(path), _query=query)
121
121
122
122
123 default_html_escape_table = {
123 default_html_escape_table = {
124 ord('&'): u'&amp;',
124 ord('&'): u'&amp;',
125 ord('<'): u'&lt;',
125 ord('<'): u'&lt;',
126 ord('>'): u'&gt;',
126 ord('>'): u'&gt;',
127 ord('"'): u'&quot;',
127 ord('"'): u'&quot;',
128 ord("'"): u'&#39;',
128 ord("'"): u'&#39;',
129 }
129 }
130
130
131
131
132 def html_escape(text, html_escape_table=default_html_escape_table):
132 def html_escape(text, html_escape_table=default_html_escape_table):
133 """Produce entities within text."""
133 """Produce entities within text."""
134 return text.translate(html_escape_table)
134 return text.translate(html_escape_table)
135
135
136
136
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
138 """
138 """
139 Truncate string ``s`` at the first occurrence of ``sub``.
139 Truncate string ``s`` at the first occurrence of ``sub``.
140
140
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
142 """
142 """
143 suffix_if_chopped = suffix_if_chopped or ''
143 suffix_if_chopped = suffix_if_chopped or ''
144 pos = s.find(sub)
144 pos = s.find(sub)
145 if pos == -1:
145 if pos == -1:
146 return s
146 return s
147
147
148 if inclusive:
148 if inclusive:
149 pos += len(sub)
149 pos += len(sub)
150
150
151 chopped = s[:pos]
151 chopped = s[:pos]
152 left = s[pos:].strip()
152 left = s[pos:].strip()
153
153
154 if left and suffix_if_chopped:
154 if left and suffix_if_chopped:
155 chopped += suffix_if_chopped
155 chopped += suffix_if_chopped
156
156
157 return chopped
157 return chopped
158
158
159
159
160 def shorter(text, size=20, prefix=False):
160 def shorter(text, size=20, prefix=False):
161 postfix = '...'
161 postfix = '...'
162 if len(text) > size:
162 if len(text) > size:
163 if prefix:
163 if prefix:
164 # shorten in front
164 # shorten in front
165 return postfix + text[-(size - len(postfix)):]
165 return postfix + text[-(size - len(postfix)):]
166 else:
166 else:
167 return text[:size - len(postfix)] + postfix
167 return text[:size - len(postfix)] + postfix
168 return text
168 return text
169
169
170
170
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 """
172 """
173 Reset button
173 Reset button
174 """
174 """
175 return _input(type, name, value, id, attrs)
175 return _input(type, name, value, id, attrs)
176
176
177
177
178 def select(name, selected_values, options, id=NotGiven, **attrs):
178 def select(name, selected_values, options, id=NotGiven, **attrs):
179
179
180 if isinstance(options, (list, tuple)):
180 if isinstance(options, (list, tuple)):
181 options_iter = options
181 options_iter = options
182 # Handle old value,label lists ... where value also can be value,label lists
182 # Handle old value,label lists ... where value also can be value,label lists
183 options = Options()
183 options = Options()
184 for opt in options_iter:
184 for opt in options_iter:
185 if isinstance(opt, tuple) and len(opt) == 2:
185 if isinstance(opt, tuple) and len(opt) == 2:
186 value, label = opt
186 value, label = opt
187 elif isinstance(opt, basestring):
187 elif isinstance(opt, basestring):
188 value = label = opt
188 value = label = opt
189 else:
189 else:
190 raise ValueError('invalid select option type %r' % type(opt))
190 raise ValueError('invalid select option type %r' % type(opt))
191
191
192 if isinstance(value, (list, tuple)):
192 if isinstance(value, (list, tuple)):
193 option_group = options.add_optgroup(label)
193 option_group = options.add_optgroup(label)
194 for opt2 in value:
194 for opt2 in value:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
196 group_value, group_label = opt2
196 group_value, group_label = opt2
197 elif isinstance(opt2, basestring):
197 elif isinstance(opt2, basestring):
198 group_value = group_label = opt2
198 group_value = group_label = opt2
199 else:
199 else:
200 raise ValueError('invalid select option type %r' % type(opt2))
200 raise ValueError('invalid select option type %r' % type(opt2))
201
201
202 option_group.add_option(group_label, group_value)
202 option_group.add_option(group_label, group_value)
203 else:
203 else:
204 options.add_option(label, value)
204 options.add_option(label, value)
205
205
206 return raw_select(name, selected_values, options, id=id, **attrs)
206 return raw_select(name, selected_values, options, id=id, **attrs)
207
207
208
208
209 def branding(name, length=40):
209 def branding(name, length=40):
210 return truncate(name, length, indicator="")
210 return truncate(name, length, indicator="")
211
211
212
212
213 def FID(raw_id, path):
213 def FID(raw_id, path):
214 """
214 """
215 Creates a unique ID for filenode based on it's hash of path and commit
215 Creates a unique ID for filenode based on it's hash of path and commit
216 it's safe to use in urls
216 it's safe to use in urls
217
217
218 :param raw_id:
218 :param raw_id:
219 :param path:
219 :param path:
220 """
220 """
221
221
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
223
223
224
224
225 class _GetError(object):
225 class _GetError(object):
226 """Get error from form_errors, and represent it as span wrapped error
226 """Get error from form_errors, and represent it as span wrapped error
227 message
227 message
228
228
229 :param field_name: field to fetch errors for
229 :param field_name: field to fetch errors for
230 :param form_errors: form errors dict
230 :param form_errors: form errors dict
231 """
231 """
232
232
233 def __call__(self, field_name, form_errors):
233 def __call__(self, field_name, form_errors):
234 tmpl = """<span class="error_msg">%s</span>"""
234 tmpl = """<span class="error_msg">%s</span>"""
235 if form_errors and field_name in form_errors:
235 if form_errors and field_name in form_errors:
236 return literal(tmpl % form_errors.get(field_name))
236 return literal(tmpl % form_errors.get(field_name))
237
237
238
238
239 get_error = _GetError()
239 get_error = _GetError()
240
240
241
241
242 class _ToolTip(object):
242 class _ToolTip(object):
243
243
244 def __call__(self, tooltip_title, trim_at=50):
244 def __call__(self, tooltip_title, trim_at=50):
245 """
245 """
246 Special function just to wrap our text into nice formatted
246 Special function just to wrap our text into nice formatted
247 autowrapped text
247 autowrapped text
248
248
249 :param tooltip_title:
249 :param tooltip_title:
250 """
250 """
251 tooltip_title = escape(tooltip_title)
251 tooltip_title = escape(tooltip_title)
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
253 return tooltip_title
253 return tooltip_title
254
254
255
255
256 tooltip = _ToolTip()
256 tooltip = _ToolTip()
257
257
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
259
259
260
260
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
263 copy_path_icon=True):
263 copy_path_icon=True):
264 if isinstance(file_path, str):
264 if isinstance(file_path, str):
265 file_path = safe_unicode(file_path)
265 file_path = safe_unicode(file_path)
266
266
267 if at_ref:
267 if at_ref:
268 route_qry = {'at': at_ref}
268 route_qry = {'at': at_ref}
269 default_landing_ref = at_ref or landing_ref_name or commit_id
269 default_landing_ref = at_ref or landing_ref_name or commit_id
270 else:
270 else:
271 route_qry = None
271 route_qry = None
272 default_landing_ref = commit_id
272 default_landing_ref = commit_id
273
273
274 # first segment is a `HOME` link to repo files root location
274 # first segment is a `HOME` link to repo files root location
275 root_name = literal(u'<i class="icon-home"></i>')
275 root_name = literal(u'<i class="icon-home"></i>')
276
276
277 url_segments = [
277 url_segments = [
278 link_to(
278 link_to(
279 root_name,
279 root_name,
280 repo_files_by_ref_url(
280 repo_files_by_ref_url(
281 repo_name,
281 repo_name,
282 repo_type,
282 repo_type,
283 f_path=None, # None here is a special case for SVN repos,
283 f_path=None, # None here is a special case for SVN repos,
284 # that won't prefix with a ref
284 # that won't prefix with a ref
285 ref_name=default_landing_ref,
285 ref_name=default_landing_ref,
286 commit_id=commit_id,
286 commit_id=commit_id,
287 query=route_qry
287 query=route_qry
288 )
288 )
289 )]
289 )]
290
290
291 path_segments = file_path.split('/')
291 path_segments = file_path.split('/')
292 last_cnt = len(path_segments) - 1
292 last_cnt = len(path_segments) - 1
293 for cnt, segment in enumerate(path_segments):
293 for cnt, segment in enumerate(path_segments):
294 if not segment:
294 if not segment:
295 continue
295 continue
296 segment_html = escape(segment)
296 segment_html = escape(segment)
297
297
298 last_item = cnt == last_cnt
298 last_item = cnt == last_cnt
299
299
300 if last_item and hide_last_item:
300 if last_item and hide_last_item:
301 # iterate over and hide last element
301 # iterate over and hide last element
302 continue
302 continue
303
303
304 if last_item and linkify_last_item is False:
304 if last_item and linkify_last_item is False:
305 # plain version
305 # plain version
306 url_segments.append(segment_html)
306 url_segments.append(segment_html)
307 else:
307 else:
308 url_segments.append(
308 url_segments.append(
309 link_to(
309 link_to(
310 segment_html,
310 segment_html,
311 repo_files_by_ref_url(
311 repo_files_by_ref_url(
312 repo_name,
312 repo_name,
313 repo_type,
313 repo_type,
314 f_path='/'.join(path_segments[:cnt + 1]),
314 f_path='/'.join(path_segments[:cnt + 1]),
315 ref_name=default_landing_ref,
315 ref_name=default_landing_ref,
316 commit_id=commit_id,
316 commit_id=commit_id,
317 query=route_qry
317 query=route_qry
318 ),
318 ),
319 ))
319 ))
320
320
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
322 if limit_items and len(limited_url_segments) < len(url_segments):
322 if limit_items and len(limited_url_segments) < len(url_segments):
323 url_segments = limited_url_segments
323 url_segments = limited_url_segments
324
324
325 full_path = file_path
325 full_path = file_path
326 if copy_path_icon:
326 if copy_path_icon:
327 icon = files_icon.format(escape(full_path))
327 icon = files_icon.format(escape(full_path))
328 else:
328 else:
329 icon = ''
329 icon = ''
330
330
331 if file_path == '':
331 if file_path == '':
332 return root_name
332 return root_name
333 else:
333 else:
334 return literal(' / '.join(url_segments) + icon)
334 return literal(' / '.join(url_segments) + icon)
335
335
336
336
337 def files_url_data(request):
337 def files_url_data(request):
338 matchdict = request.matchdict
338 matchdict = request.matchdict
339
339
340 if 'f_path' not in matchdict:
340 if 'f_path' not in matchdict:
341 matchdict['f_path'] = ''
341 matchdict['f_path'] = ''
342
342
343 if 'commit_id' not in matchdict:
343 if 'commit_id' not in matchdict:
344 matchdict['commit_id'] = 'tip'
344 matchdict['commit_id'] = 'tip'
345
345
346 return json.dumps(matchdict)
346 return json.dumps(matchdict)
347
347
348
348
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
350 _is_svn = is_svn(db_repo_type)
350 _is_svn = is_svn(db_repo_type)
351 final_f_path = f_path
351 final_f_path = f_path
352
352
353 if _is_svn:
353 if _is_svn:
354 """
354 """
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
356 actually commit_id followed by the ref_name. This should be done only in case
356 actually commit_id followed by the ref_name. This should be done only in case
357 This is a initial landing url, without additional paths.
357 This is a initial landing url, without additional paths.
358
358
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
360 """
360 """
361
361
362 if ref_name and ref_name != 'tip':
362 if ref_name and ref_name != 'tip':
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
365 # of files link. If we are in the tree we don't need this since we traverse the url
365 # of files link. If we are in the tree we don't need this since we traverse the url
366 # that has everything stored
366 # that has everything stored
367 if f_path in ['', '/']:
367 if f_path in ['', '/']:
368 final_f_path = '/'.join([ref_name, f_path])
368 final_f_path = '/'.join([ref_name, f_path])
369
369
370 # SVN always needs a commit_id explicitly, without a named REF
370 # SVN always needs a commit_id explicitly, without a named REF
371 default_commit_id = commit_id
371 default_commit_id = commit_id
372 else:
372 else:
373 """
373 """
374 For git and mercurial we construct a new URL using the names instead of commit_id
374 For git and mercurial we construct a new URL using the names instead of commit_id
375 like: /master/some_path?at=master
375 like: /master/some_path?at=master
376 """
376 """
377 # We currently do not support branches with slashes
377 # We currently do not support branches with slashes
378 if '/' in ref_name:
378 if '/' in ref_name:
379 default_commit_id = commit_id
379 default_commit_id = commit_id
380 else:
380 else:
381 default_commit_id = ref_name
381 default_commit_id = ref_name
382
382
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
384 # we translate it to string to not have None
384 # we translate it to string to not have None
385 final_f_path = final_f_path or ''
385 final_f_path = final_f_path or ''
386
386
387 files_url = route_path(
387 files_url = route_path(
388 'repo_files',
388 'repo_files',
389 repo_name=db_repo_name,
389 repo_name=db_repo_name,
390 commit_id=default_commit_id,
390 commit_id=default_commit_id,
391 f_path=final_f_path,
391 f_path=final_f_path,
392 _query=query
392 _query=query
393 )
393 )
394 return files_url
394 return files_url
395
395
396
396
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
398 """
398 """
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
400
400
401 If ``outfile`` is given and a valid file object (an object
401 If ``outfile`` is given and a valid file object (an object
402 with a ``write`` method), the result will be written to it, otherwise
402 with a ``write`` method), the result will be written to it, otherwise
403 it is returned as a string.
403 it is returned as a string.
404 """
404 """
405 if use_hl_filter:
405 if use_hl_filter:
406 # add HL filter
406 # add HL filter
407 from rhodecode.lib.index import search_utils
407 from rhodecode.lib.index import search_utils
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
409 return pygments.format(pygments.lex(code, lexer), formatter)
409 return pygments.format(pygments.lex(code, lexer), formatter)
410
410
411
411
412 class CodeHtmlFormatter(HtmlFormatter):
412 class CodeHtmlFormatter(HtmlFormatter):
413 """
413 """
414 My code Html Formatter for source codes
414 My code Html Formatter for source codes
415 """
415 """
416
416
417 def wrap(self, source, outfile):
417 def wrap(self, source, outfile):
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
419
419
420 def _wrap_code(self, source):
420 def _wrap_code(self, source):
421 for cnt, it in enumerate(source):
421 for cnt, it in enumerate(source):
422 i, t = it
422 i, t = it
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
424 yield i, t
424 yield i, t
425
425
426 def _wrap_tablelinenos(self, inner):
426 def _wrap_tablelinenos(self, inner):
427 dummyoutfile = StringIO.StringIO()
427 dummyoutfile = StringIO.StringIO()
428 lncount = 0
428 lncount = 0
429 for t, line in inner:
429 for t, line in inner:
430 if t:
430 if t:
431 lncount += 1
431 lncount += 1
432 dummyoutfile.write(line)
432 dummyoutfile.write(line)
433
433
434 fl = self.linenostart
434 fl = self.linenostart
435 mw = len(str(lncount + fl - 1))
435 mw = len(str(lncount + fl - 1))
436 sp = self.linenospecial
436 sp = self.linenospecial
437 st = self.linenostep
437 st = self.linenostep
438 la = self.lineanchors
438 la = self.lineanchors
439 aln = self.anchorlinenos
439 aln = self.anchorlinenos
440 nocls = self.noclasses
440 nocls = self.noclasses
441 if sp:
441 if sp:
442 lines = []
442 lines = []
443
443
444 for i in range(fl, fl + lncount):
444 for i in range(fl, fl + lncount):
445 if i % st == 0:
445 if i % st == 0:
446 if i % sp == 0:
446 if i % sp == 0:
447 if aln:
447 if aln:
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
449 (la, i, mw, i))
449 (la, i, mw, i))
450 else:
450 else:
451 lines.append('<span class="special">%*d</span>' % (mw, i))
451 lines.append('<span class="special">%*d</span>' % (mw, i))
452 else:
452 else:
453 if aln:
453 if aln:
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
455 else:
455 else:
456 lines.append('%*d' % (mw, i))
456 lines.append('%*d' % (mw, i))
457 else:
457 else:
458 lines.append('')
458 lines.append('')
459 ls = '\n'.join(lines)
459 ls = '\n'.join(lines)
460 else:
460 else:
461 lines = []
461 lines = []
462 for i in range(fl, fl + lncount):
462 for i in range(fl, fl + lncount):
463 if i % st == 0:
463 if i % st == 0:
464 if aln:
464 if aln:
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
466 else:
466 else:
467 lines.append('%*d' % (mw, i))
467 lines.append('%*d' % (mw, i))
468 else:
468 else:
469 lines.append('')
469 lines.append('')
470 ls = '\n'.join(lines)
470 ls = '\n'.join(lines)
471
471
472 # in case you wonder about the seemingly redundant <div> here: since the
472 # in case you wonder about the seemingly redundant <div> here: since the
473 # content in the other cell also is wrapped in a div, some browsers in
473 # content in the other cell also is wrapped in a div, some browsers in
474 # some configurations seem to mess up the formatting...
474 # some configurations seem to mess up the formatting...
475 if nocls:
475 if nocls:
476 yield 0, ('<table class="%stable">' % self.cssclass +
476 yield 0, ('<table class="%stable">' % self.cssclass +
477 '<tr><td><div class="linenodiv" '
477 '<tr><td><div class="linenodiv" '
478 'style="background-color: #f0f0f0; padding-right: 10px">'
478 'style="background-color: #f0f0f0; padding-right: 10px">'
479 '<pre style="line-height: 125%">' +
479 '<pre style="line-height: 125%">' +
480 ls + '</pre></div></td><td id="hlcode" class="code">')
480 ls + '</pre></div></td><td id="hlcode" class="code">')
481 else:
481 else:
482 yield 0, ('<table class="%stable">' % self.cssclass +
482 yield 0, ('<table class="%stable">' % self.cssclass +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
484 ls + '</pre></div></td><td id="hlcode" class="code">')
484 ls + '</pre></div></td><td id="hlcode" class="code">')
485 yield 0, dummyoutfile.getvalue()
485 yield 0, dummyoutfile.getvalue()
486 yield 0, '</td></tr></table>'
486 yield 0, '</td></tr></table>'
487
487
488
488
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
490 def __init__(self, **kw):
490 def __init__(self, **kw):
491 # only show these line numbers if set
491 # only show these line numbers if set
492 self.only_lines = kw.pop('only_line_numbers', [])
492 self.only_lines = kw.pop('only_line_numbers', [])
493 self.query_terms = kw.pop('query_terms', [])
493 self.query_terms = kw.pop('query_terms', [])
494 self.max_lines = kw.pop('max_lines', 5)
494 self.max_lines = kw.pop('max_lines', 5)
495 self.line_context = kw.pop('line_context', 3)
495 self.line_context = kw.pop('line_context', 3)
496 self.url = kw.pop('url', None)
496 self.url = kw.pop('url', None)
497
497
498 super(CodeHtmlFormatter, self).__init__(**kw)
498 super(CodeHtmlFormatter, self).__init__(**kw)
499
499
500 def _wrap_code(self, source):
500 def _wrap_code(self, source):
501 for cnt, it in enumerate(source):
501 for cnt, it in enumerate(source):
502 i, t = it
502 i, t = it
503 t = '<pre>%s</pre>' % t
503 t = '<pre>%s</pre>' % t
504 yield i, t
504 yield i, t
505
505
506 def _wrap_tablelinenos(self, inner):
506 def _wrap_tablelinenos(self, inner):
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
508
508
509 last_shown_line_number = 0
509 last_shown_line_number = 0
510 current_line_number = 1
510 current_line_number = 1
511
511
512 for t, line in inner:
512 for t, line in inner:
513 if not t:
513 if not t:
514 yield t, line
514 yield t, line
515 continue
515 continue
516
516
517 if current_line_number in self.only_lines:
517 if current_line_number in self.only_lines:
518 if last_shown_line_number + 1 != current_line_number:
518 if last_shown_line_number + 1 != current_line_number:
519 yield 0, '<tr>'
519 yield 0, '<tr>'
520 yield 0, '<td class="line">...</td>'
520 yield 0, '<td class="line">...</td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
522 yield 0, '</tr>'
522 yield 0, '</tr>'
523
523
524 yield 0, '<tr>'
524 yield 0, '<tr>'
525 if self.url:
525 if self.url:
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
527 self.url, current_line_number, current_line_number)
527 self.url, current_line_number, current_line_number)
528 else:
528 else:
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
530 current_line_number)
530 current_line_number)
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
532 yield 0, '</tr>'
532 yield 0, '</tr>'
533
533
534 last_shown_line_number = current_line_number
534 last_shown_line_number = current_line_number
535
535
536 current_line_number += 1
536 current_line_number += 1
537
537
538 yield 0, '</table>'
538 yield 0, '</table>'
539
539
540
540
541 def hsv_to_rgb(h, s, v):
541 def hsv_to_rgb(h, s, v):
542 """ Convert hsv color values to rgb """
542 """ Convert hsv color values to rgb """
543
543
544 if s == 0.0:
544 if s == 0.0:
545 return v, v, v
545 return v, v, v
546 i = int(h * 6.0) # XXX assume int() truncates!
546 i = int(h * 6.0) # XXX assume int() truncates!
547 f = (h * 6.0) - i
547 f = (h * 6.0) - i
548 p = v * (1.0 - s)
548 p = v * (1.0 - s)
549 q = v * (1.0 - s * f)
549 q = v * (1.0 - s * f)
550 t = v * (1.0 - s * (1.0 - f))
550 t = v * (1.0 - s * (1.0 - f))
551 i = i % 6
551 i = i % 6
552 if i == 0:
552 if i == 0:
553 return v, t, p
553 return v, t, p
554 if i == 1:
554 if i == 1:
555 return q, v, p
555 return q, v, p
556 if i == 2:
556 if i == 2:
557 return p, v, t
557 return p, v, t
558 if i == 3:
558 if i == 3:
559 return p, q, v
559 return p, q, v
560 if i == 4:
560 if i == 4:
561 return t, p, v
561 return t, p, v
562 if i == 5:
562 if i == 5:
563 return v, p, q
563 return v, p, q
564
564
565
565
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
567 """
567 """
568 Generator for getting n of evenly distributed colors using
568 Generator for getting n of evenly distributed colors using
569 hsv color and golden ratio. It always return same order of colors
569 hsv color and golden ratio. It always return same order of colors
570
570
571 :param n: number of colors to generate
571 :param n: number of colors to generate
572 :param saturation: saturation of returned colors
572 :param saturation: saturation of returned colors
573 :param lightness: lightness of returned colors
573 :param lightness: lightness of returned colors
574 :returns: RGB tuple
574 :returns: RGB tuple
575 """
575 """
576
576
577 golden_ratio = 0.618033988749895
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
578 h = 0.22717784590367374
579
579
580 for _ in xrange(n):
580 for _ in xrange(n):
581 h += golden_ratio
581 h += golden_ratio
582 h %= 1
582 h %= 1
583 HSV_tuple = [h, saturation, lightness]
583 HSV_tuple = [h, saturation, lightness]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
586
587
587
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
589 """
589 """
590 Returns a function which when called with an argument returns a unique
590 Returns a function which when called with an argument returns a unique
591 color for that argument, eg.
591 color for that argument, eg.
592
592
593 :param n: number of colors to generate
593 :param n: number of colors to generate
594 :param saturation: saturation of returned colors
594 :param saturation: saturation of returned colors
595 :param lightness: lightness of returned colors
595 :param lightness: lightness of returned colors
596 :returns: css RGB string
596 :returns: css RGB string
597
597
598 >>> color_hash = color_hasher()
598 >>> color_hash = color_hasher()
599 >>> color_hash('hello')
599 >>> color_hash('hello')
600 'rgb(34, 12, 59)'
600 'rgb(34, 12, 59)'
601 >>> color_hash('hello')
601 >>> color_hash('hello')
602 'rgb(34, 12, 59)'
602 'rgb(34, 12, 59)'
603 >>> color_hash('other')
603 >>> color_hash('other')
604 'rgb(90, 224, 159)'
604 'rgb(90, 224, 159)'
605 """
605 """
606
606
607 color_dict = {}
607 color_dict = {}
608 cgenerator = unique_color_generator(
608 cgenerator = unique_color_generator(
609 saturation=saturation, lightness=lightness)
609 saturation=saturation, lightness=lightness)
610
610
611 def get_color_string(thing):
611 def get_color_string(thing):
612 if thing in color_dict:
612 if thing in color_dict:
613 col = color_dict[thing]
613 col = color_dict[thing]
614 else:
614 else:
615 col = color_dict[thing] = cgenerator.next()
615 col = color_dict[thing] = cgenerator.next()
616 return "rgb(%s)" % (', '.join(col))
616 return "rgb(%s)" % (', '.join(col))
617
617
618 return get_color_string
618 return get_color_string
619
619
620
620
621 def get_lexer_safe(mimetype=None, filepath=None):
621 def get_lexer_safe(mimetype=None, filepath=None):
622 """
622 """
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
624 defaulting to plain text if none could be found
624 defaulting to plain text if none could be found
625 """
625 """
626 lexer = None
626 lexer = None
627 try:
627 try:
628 if mimetype:
628 if mimetype:
629 lexer = get_lexer_for_mimetype(mimetype)
629 lexer = get_lexer_for_mimetype(mimetype)
630 if not lexer:
630 if not lexer:
631 lexer = get_lexer_for_filename(filepath)
631 lexer = get_lexer_for_filename(filepath)
632 except pygments.util.ClassNotFound:
632 except pygments.util.ClassNotFound:
633 pass
633 pass
634
634
635 if not lexer:
635 if not lexer:
636 lexer = get_lexer_by_name('text')
636 lexer = get_lexer_by_name('text')
637
637
638 return lexer
638 return lexer
639
639
640
640
641 def get_lexer_for_filenode(filenode):
641 def get_lexer_for_filenode(filenode):
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
643 return lexer
643 return lexer
644
644
645
645
646 def pygmentize(filenode, **kwargs):
646 def pygmentize(filenode, **kwargs):
647 """
647 """
648 pygmentize function using pygments
648 pygmentize function using pygments
649
649
650 :param filenode:
650 :param filenode:
651 """
651 """
652 lexer = get_lexer_for_filenode(filenode)
652 lexer = get_lexer_for_filenode(filenode)
653 return literal(code_highlight(filenode.content, lexer,
653 return literal(code_highlight(filenode.content, lexer,
654 CodeHtmlFormatter(**kwargs)))
654 CodeHtmlFormatter(**kwargs)))
655
655
656
656
657 def is_following_repo(repo_name, user_id):
657 def is_following_repo(repo_name, user_id):
658 from rhodecode.model.scm import ScmModel
658 from rhodecode.model.scm import ScmModel
659 return ScmModel().is_following_repo(repo_name, user_id)
659 return ScmModel().is_following_repo(repo_name, user_id)
660
660
661
661
662 class _Message(object):
662 class _Message(object):
663 """A message returned by ``Flash.pop_messages()``.
663 """A message returned by ``Flash.pop_messages()``.
664
664
665 Converting the message to a string returns the message text. Instances
665 Converting the message to a string returns the message text. Instances
666 also have the following attributes:
666 also have the following attributes:
667
667
668 * ``message``: the message text.
668 * ``message``: the message text.
669 * ``category``: the category specified when the message was created.
669 * ``category``: the category specified when the message was created.
670 """
670 """
671
671
672 def __init__(self, category, message, sub_data=None):
672 def __init__(self, category, message, sub_data=None):
673 self.category = category
673 self.category = category
674 self.message = message
674 self.message = message
675 self.sub_data = sub_data or {}
675 self.sub_data = sub_data or {}
676
676
677 def __str__(self):
677 def __str__(self):
678 return self.message
678 return self.message
679
679
680 __unicode__ = __str__
680 __unicode__ = __str__
681
681
682 def __html__(self):
682 def __html__(self):
683 return escape(safe_unicode(self.message))
683 return escape(safe_unicode(self.message))
684
684
685
685
686 class Flash(object):
686 class Flash(object):
687 # List of allowed categories. If None, allow any category.
687 # List of allowed categories. If None, allow any category.
688 categories = ["warning", "notice", "error", "success"]
688 categories = ["warning", "notice", "error", "success"]
689
689
690 # Default category if none is specified.
690 # Default category if none is specified.
691 default_category = "notice"
691 default_category = "notice"
692
692
693 def __init__(self, session_key="flash", categories=None,
693 def __init__(self, session_key="flash", categories=None,
694 default_category=None):
694 default_category=None):
695 """
695 """
696 Instantiate a ``Flash`` object.
696 Instantiate a ``Flash`` object.
697
697
698 ``session_key`` is the key to save the messages under in the user's
698 ``session_key`` is the key to save the messages under in the user's
699 session.
699 session.
700
700
701 ``categories`` is an optional list which overrides the default list
701 ``categories`` is an optional list which overrides the default list
702 of categories.
702 of categories.
703
703
704 ``default_category`` overrides the default category used for messages
704 ``default_category`` overrides the default category used for messages
705 when none is specified.
705 when none is specified.
706 """
706 """
707 self.session_key = session_key
707 self.session_key = session_key
708 if categories is not None:
708 if categories is not None:
709 self.categories = categories
709 self.categories = categories
710 if default_category is not None:
710 if default_category is not None:
711 self.default_category = default_category
711 self.default_category = default_category
712 if self.categories and self.default_category not in self.categories:
712 if self.categories and self.default_category not in self.categories:
713 raise ValueError(
713 raise ValueError(
714 "unrecognized default category %r" % (self.default_category,))
714 "unrecognized default category %r" % (self.default_category,))
715
715
716 def pop_messages(self, session=None, request=None):
716 def pop_messages(self, session=None, request=None):
717 """
717 """
718 Return all accumulated messages and delete them from the session.
718 Return all accumulated messages and delete them from the session.
719
719
720 The return value is a list of ``Message`` objects.
720 The return value is a list of ``Message`` objects.
721 """
721 """
722 messages = []
722 messages = []
723
723
724 if not session:
724 if not session:
725 if not request:
725 if not request:
726 request = get_current_request()
726 request = get_current_request()
727 session = request.session
727 session = request.session
728
728
729 # Pop the 'old' pylons flash messages. They are tuples of the form
729 # Pop the 'old' pylons flash messages. They are tuples of the form
730 # (category, message)
730 # (category, message)
731 for cat, msg in session.pop(self.session_key, []):
731 for cat, msg in session.pop(self.session_key, []):
732 messages.append(_Message(cat, msg))
732 messages.append(_Message(cat, msg))
733
733
734 # Pop the 'new' pyramid flash messages for each category as list
734 # Pop the 'new' pyramid flash messages for each category as list
735 # of strings.
735 # of strings.
736 for cat in self.categories:
736 for cat in self.categories:
737 for msg in session.pop_flash(queue=cat):
737 for msg in session.pop_flash(queue=cat):
738 sub_data = {}
738 sub_data = {}
739 if hasattr(msg, 'rsplit'):
739 if hasattr(msg, 'rsplit'):
740 flash_data = msg.rsplit('|DELIM|', 1)
740 flash_data = msg.rsplit('|DELIM|', 1)
741 org_message = flash_data[0]
741 org_message = flash_data[0]
742 if len(flash_data) > 1:
742 if len(flash_data) > 1:
743 sub_data = json.loads(flash_data[1])
743 sub_data = json.loads(flash_data[1])
744 else:
744 else:
745 org_message = msg
745 org_message = msg
746
746
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
748
748
749 # Map messages from the default queue to the 'notice' category.
749 # Map messages from the default queue to the 'notice' category.
750 for msg in session.pop_flash():
750 for msg in session.pop_flash():
751 messages.append(_Message('notice', msg))
751 messages.append(_Message('notice', msg))
752
752
753 session.save()
753 session.save()
754 return messages
754 return messages
755
755
756 def json_alerts(self, session=None, request=None):
756 def json_alerts(self, session=None, request=None):
757 payloads = []
757 payloads = []
758 messages = flash.pop_messages(session=session, request=request) or []
758 messages = flash.pop_messages(session=session, request=request) or []
759 for message in messages:
759 for message in messages:
760 payloads.append({
760 payloads.append({
761 'message': {
761 'message': {
762 'message': u'{}'.format(message.message),
762 'message': u'{}'.format(message.message),
763 'level': message.category,
763 'level': message.category,
764 'force': True,
764 'force': True,
765 'subdata': message.sub_data
765 'subdata': message.sub_data
766 }
766 }
767 })
767 })
768 return json.dumps(payloads)
768 return json.dumps(payloads)
769
769
770 def __call__(self, message, category=None, ignore_duplicate=True,
770 def __call__(self, message, category=None, ignore_duplicate=True,
771 session=None, request=None):
771 session=None, request=None):
772
772
773 if not session:
773 if not session:
774 if not request:
774 if not request:
775 request = get_current_request()
775 request = get_current_request()
776 session = request.session
776 session = request.session
777
777
778 session.flash(
778 session.flash(
779 message, queue=category, allow_duplicate=not ignore_duplicate)
779 message, queue=category, allow_duplicate=not ignore_duplicate)
780
780
781
781
782 flash = Flash()
782 flash = Flash()
783
783
784 #==============================================================================
784 #==============================================================================
785 # SCM FILTERS available via h.
785 # SCM FILTERS available via h.
786 #==============================================================================
786 #==============================================================================
787 from rhodecode.lib.vcs.utils import author_name, author_email
787 from rhodecode.lib.vcs.utils import author_name, author_email
788 from rhodecode.lib.utils2 import age, age_from_seconds
788 from rhodecode.lib.utils2 import age, age_from_seconds
789 from rhodecode.model.db import User, ChangesetStatus
789 from rhodecode.model.db import User, ChangesetStatus
790
790
791
791
792 email = author_email
792 email = author_email
793
793
794
794
795 def capitalize(raw_text):
795 def capitalize(raw_text):
796 return raw_text.capitalize()
796 return raw_text.capitalize()
797
797
798
798
799 def short_id(long_id):
799 def short_id(long_id):
800 return long_id[:12]
800 return long_id[:12]
801
801
802
802
803 def hide_credentials(url):
803 def hide_credentials(url):
804 from rhodecode.lib.utils2 import credentials_filter
804 from rhodecode.lib.utils2 import credentials_filter
805 return credentials_filter(url)
805 return credentials_filter(url)
806
806
807
807
808 import pytz
808 import pytz
809 import tzlocal
809 import tzlocal
810 local_timezone = tzlocal.get_localzone()
810 local_timezone = tzlocal.get_localzone()
811
811
812
812
813 def get_timezone(datetime_iso, time_is_local=False):
813 def get_timezone(datetime_iso, time_is_local=False):
814 tzinfo = '+00:00'
814 tzinfo = '+00:00'
815
815
816 # detect if we have a timezone info, otherwise, add it
816 # detect if we have a timezone info, otherwise, add it
817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
818 force_timezone = os.environ.get('RC_TIMEZONE', '')
818 force_timezone = os.environ.get('RC_TIMEZONE', '')
819 if force_timezone:
819 if force_timezone:
820 force_timezone = pytz.timezone(force_timezone)
820 force_timezone = pytz.timezone(force_timezone)
821 timezone = force_timezone or local_timezone
821 timezone = force_timezone or local_timezone
822 offset = timezone.localize(datetime_iso).strftime('%z')
822 offset = timezone.localize(datetime_iso).strftime('%z')
823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 return tzinfo
824 return tzinfo
825
825
826
826
827 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
827 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
828 title = value or format_date(datetime_iso)
828 title = value or format_date(datetime_iso)
829 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
829 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
830
830
831 return literal(
831 return literal(
832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
833 cls='tooltip' if tooltip else '',
833 cls='tooltip' if tooltip else '',
834 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
834 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
835 title=title, dt=datetime_iso, tzinfo=tzinfo
835 title=title, dt=datetime_iso, tzinfo=tzinfo
836 ))
836 ))
837
837
838
838
839 def _shorten_commit_id(commit_id, commit_len=None):
839 def _shorten_commit_id(commit_id, commit_len=None):
840 if commit_len is None:
840 if commit_len is None:
841 request = get_current_request()
841 request = get_current_request()
842 commit_len = request.call_context.visual.show_sha_length
842 commit_len = request.call_context.visual.show_sha_length
843 return commit_id[:commit_len]
843 return commit_id[:commit_len]
844
844
845
845
846 def show_id(commit, show_idx=None, commit_len=None):
846 def show_id(commit, show_idx=None, commit_len=None):
847 """
847 """
848 Configurable function that shows ID
848 Configurable function that shows ID
849 by default it's r123:fffeeefffeee
849 by default it's r123:fffeeefffeee
850
850
851 :param commit: commit instance
851 :param commit: commit instance
852 """
852 """
853 if show_idx is None:
853 if show_idx is None:
854 request = get_current_request()
854 request = get_current_request()
855 show_idx = request.call_context.visual.show_revision_number
855 show_idx = request.call_context.visual.show_revision_number
856
856
857 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
857 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
858 if show_idx:
858 if show_idx:
859 return 'r%s:%s' % (commit.idx, raw_id)
859 return 'r%s:%s' % (commit.idx, raw_id)
860 else:
860 else:
861 return '%s' % (raw_id, )
861 return '%s' % (raw_id, )
862
862
863
863
864 def format_date(date):
864 def format_date(date):
865 """
865 """
866 use a standardized formatting for dates used in RhodeCode
866 use a standardized formatting for dates used in RhodeCode
867
867
868 :param date: date/datetime object
868 :param date: date/datetime object
869 :return: formatted date
869 :return: formatted date
870 """
870 """
871
871
872 if date:
872 if date:
873 _fmt = "%a, %d %b %Y %H:%M:%S"
873 _fmt = "%a, %d %b %Y %H:%M:%S"
874 return safe_unicode(date.strftime(_fmt))
874 return safe_unicode(date.strftime(_fmt))
875
875
876 return u""
876 return u""
877
877
878
878
879 class _RepoChecker(object):
879 class _RepoChecker(object):
880
880
881 def __init__(self, backend_alias):
881 def __init__(self, backend_alias):
882 self._backend_alias = backend_alias
882 self._backend_alias = backend_alias
883
883
884 def __call__(self, repository):
884 def __call__(self, repository):
885 if hasattr(repository, 'alias'):
885 if hasattr(repository, 'alias'):
886 _type = repository.alias
886 _type = repository.alias
887 elif hasattr(repository, 'repo_type'):
887 elif hasattr(repository, 'repo_type'):
888 _type = repository.repo_type
888 _type = repository.repo_type
889 else:
889 else:
890 _type = repository
890 _type = repository
891 return _type == self._backend_alias
891 return _type == self._backend_alias
892
892
893
893
894 is_git = _RepoChecker('git')
894 is_git = _RepoChecker('git')
895 is_hg = _RepoChecker('hg')
895 is_hg = _RepoChecker('hg')
896 is_svn = _RepoChecker('svn')
896 is_svn = _RepoChecker('svn')
897
897
898
898
899 def get_repo_type_by_name(repo_name):
899 def get_repo_type_by_name(repo_name):
900 repo = Repository.get_by_repo_name(repo_name)
900 repo = Repository.get_by_repo_name(repo_name)
901 if repo:
901 if repo:
902 return repo.repo_type
902 return repo.repo_type
903
903
904
904
905 def is_svn_without_proxy(repository):
905 def is_svn_without_proxy(repository):
906 if is_svn(repository):
906 if is_svn(repository):
907 from rhodecode.model.settings import VcsSettingsModel
907 from rhodecode.model.settings import VcsSettingsModel
908 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
908 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
909 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
909 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
910 return False
910 return False
911
911
912
912
913 def discover_user(author):
913 def discover_user(author):
914 """
914 """
915 Tries to discover RhodeCode User based on the author string. Author string
915 Tries to discover RhodeCode User based on the author string. Author string
916 is typically `FirstName LastName <email@address.com>`
916 is typically `FirstName LastName <email@address.com>`
917 """
917 """
918
918
919 # if author is already an instance use it for extraction
919 # if author is already an instance use it for extraction
920 if isinstance(author, User):
920 if isinstance(author, User):
921 return author
921 return author
922
922
923 # Valid email in the attribute passed, see if they're in the system
923 # Valid email in the attribute passed, see if they're in the system
924 _email = author_email(author)
924 _email = author_email(author)
925 if _email != '':
925 if _email != '':
926 user = User.get_by_email(_email, case_insensitive=True, cache=True)
926 user = User.get_by_email(_email, case_insensitive=True, cache=True)
927 if user is not None:
927 if user is not None:
928 return user
928 return user
929
929
930 # Maybe it's a username, we try to extract it and fetch by username ?
930 # Maybe it's a username, we try to extract it and fetch by username ?
931 _author = author_name(author)
931 _author = author_name(author)
932 user = User.get_by_username(_author, case_insensitive=True, cache=True)
932 user = User.get_by_username(_author, case_insensitive=True, cache=True)
933 if user is not None:
933 if user is not None:
934 return user
934 return user
935
935
936 return None
936 return None
937
937
938
938
939 def email_or_none(author):
939 def email_or_none(author):
940 # extract email from the commit string
940 # extract email from the commit string
941 _email = author_email(author)
941 _email = author_email(author)
942
942
943 # If we have an email, use it, otherwise
943 # If we have an email, use it, otherwise
944 # see if it contains a username we can get an email from
944 # see if it contains a username we can get an email from
945 if _email != '':
945 if _email != '':
946 return _email
946 return _email
947 else:
947 else:
948 user = User.get_by_username(
948 user = User.get_by_username(
949 author_name(author), case_insensitive=True, cache=True)
949 author_name(author), case_insensitive=True, cache=True)
950
950
951 if user is not None:
951 if user is not None:
952 return user.email
952 return user.email
953
953
954 # No valid email, not a valid user in the system, none!
954 # No valid email, not a valid user in the system, none!
955 return None
955 return None
956
956
957
957
958 def link_to_user(author, length=0, **kwargs):
958 def link_to_user(author, length=0, **kwargs):
959 user = discover_user(author)
959 user = discover_user(author)
960 # user can be None, but if we have it already it means we can re-use it
960 # user can be None, but if we have it already it means we can re-use it
961 # in the person() function, so we save 1 intensive-query
961 # in the person() function, so we save 1 intensive-query
962 if user:
962 if user:
963 author = user
963 author = user
964
964
965 display_person = person(author, 'username_or_name_or_email')
965 display_person = person(author, 'username_or_name_or_email')
966 if length:
966 if length:
967 display_person = shorter(display_person, length)
967 display_person = shorter(display_person, length)
968
968
969 if user and user.username != user.DEFAULT_USER:
969 if user and user.username != user.DEFAULT_USER:
970 return link_to(
970 return link_to(
971 escape(display_person),
971 escape(display_person),
972 route_path('user_profile', username=user.username),
972 route_path('user_profile', username=user.username),
973 **kwargs)
973 **kwargs)
974 else:
974 else:
975 return escape(display_person)
975 return escape(display_person)
976
976
977
977
978 def link_to_group(users_group_name, **kwargs):
978 def link_to_group(users_group_name, **kwargs):
979 return link_to(
979 return link_to(
980 escape(users_group_name),
980 escape(users_group_name),
981 route_path('user_group_profile', user_group_name=users_group_name),
981 route_path('user_group_profile', user_group_name=users_group_name),
982 **kwargs)
982 **kwargs)
983
983
984
984
985 def person(author, show_attr="username_and_name"):
985 def person(author, show_attr="username_and_name"):
986 user = discover_user(author)
986 user = discover_user(author)
987 if user:
987 if user:
988 return getattr(user, show_attr)
988 return getattr(user, show_attr)
989 else:
989 else:
990 _author = author_name(author)
990 _author = author_name(author)
991 _email = email(author)
991 _email = email(author)
992 return _author or _email
992 return _author or _email
993
993
994
994
995 def author_string(email):
995 def author_string(email):
996 if email:
996 if email:
997 user = User.get_by_email(email, case_insensitive=True, cache=True)
997 user = User.get_by_email(email, case_insensitive=True, cache=True)
998 if user:
998 if user:
999 if user.first_name or user.last_name:
999 if user.first_name or user.last_name:
1000 return '%s %s &lt;%s&gt;' % (
1000 return '%s %s &lt;%s&gt;' % (
1001 user.first_name, user.last_name, email)
1001 user.first_name, user.last_name, email)
1002 else:
1002 else:
1003 return email
1003 return email
1004 else:
1004 else:
1005 return email
1005 return email
1006 else:
1006 else:
1007 return None
1007 return None
1008
1008
1009
1009
1010 def person_by_id(id_, show_attr="username_and_name"):
1010 def person_by_id(id_, show_attr="username_and_name"):
1011 # attr to return from fetched user
1011 # attr to return from fetched user
1012 person_getter = lambda usr: getattr(usr, show_attr)
1012 person_getter = lambda usr: getattr(usr, show_attr)
1013
1013
1014 #maybe it's an ID ?
1014 #maybe it's an ID ?
1015 if str(id_).isdigit() or isinstance(id_, int):
1015 if str(id_).isdigit() or isinstance(id_, int):
1016 id_ = int(id_)
1016 id_ = int(id_)
1017 user = User.get(id_)
1017 user = User.get(id_)
1018 if user is not None:
1018 if user is not None:
1019 return person_getter(user)
1019 return person_getter(user)
1020 return id_
1020 return id_
1021
1021
1022
1022
1023 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1023 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1024 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1024 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1025 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1025 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1026
1026
1027
1027
1028 tags_paterns = OrderedDict((
1028 tags_paterns = OrderedDict((
1029 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1029 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1030 '<div class="metatag" tag="lang">\\2</div>')),
1030 '<div class="metatag" tag="lang">\\2</div>')),
1031
1031
1032 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1032 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1033 '<div class="metatag" tag="see">see: \\1 </div>')),
1033 '<div class="metatag" tag="see">see: \\1 </div>')),
1034
1034
1035 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1035 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1036 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1036 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1037
1037
1038 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1038 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1039 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1039 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1040
1040
1041 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1041 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1042 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1042 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1043
1043
1044 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1044 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1045 '<div class="metatag" tag="state \\1">\\1</div>')),
1045 '<div class="metatag" tag="state \\1">\\1</div>')),
1046
1046
1047 # label in grey
1047 # label in grey
1048 ('label', (re.compile(r'\[([a-z]+)\]'),
1048 ('label', (re.compile(r'\[([a-z]+)\]'),
1049 '<div class="metatag" tag="label">\\1</div>')),
1049 '<div class="metatag" tag="label">\\1</div>')),
1050
1050
1051 # generic catch all in grey
1051 # generic catch all in grey
1052 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1052 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1053 '<div class="metatag" tag="generic">\\1</div>')),
1053 '<div class="metatag" tag="generic">\\1</div>')),
1054 ))
1054 ))
1055
1055
1056
1056
1057 def extract_metatags(value):
1057 def extract_metatags(value):
1058 """
1058 """
1059 Extract supported meta-tags from given text value
1059 Extract supported meta-tags from given text value
1060 """
1060 """
1061 tags = []
1061 tags = []
1062 if not value:
1062 if not value:
1063 return tags, ''
1063 return tags, ''
1064
1064
1065 for key, val in tags_paterns.items():
1065 for key, val in tags_paterns.items():
1066 pat, replace_html = val
1066 pat, replace_html = val
1067 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1067 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1068 value = pat.sub('', value)
1068 value = pat.sub('', value)
1069
1069
1070 return tags, value
1070 return tags, value
1071
1071
1072
1072
1073 def style_metatag(tag_type, value):
1073 def style_metatag(tag_type, value):
1074 """
1074 """
1075 converts tags from value into html equivalent
1075 converts tags from value into html equivalent
1076 """
1076 """
1077 if not value:
1077 if not value:
1078 return ''
1078 return ''
1079
1079
1080 html_value = value
1080 html_value = value
1081 tag_data = tags_paterns.get(tag_type)
1081 tag_data = tags_paterns.get(tag_type)
1082 if tag_data:
1082 if tag_data:
1083 pat, replace_html = tag_data
1083 pat, replace_html = tag_data
1084 # convert to plain `unicode` instead of a markup tag to be used in
1084 # convert to plain `unicode` instead of a markup tag to be used in
1085 # regex expressions. safe_unicode doesn't work here
1085 # regex expressions. safe_unicode doesn't work here
1086 html_value = pat.sub(replace_html, unicode(value))
1086 html_value = pat.sub(replace_html, unicode(value))
1087
1087
1088 return html_value
1088 return html_value
1089
1089
1090
1090
1091 def bool2icon(value, show_at_false=True):
1091 def bool2icon(value, show_at_false=True):
1092 """
1092 """
1093 Returns boolean value of a given value, represented as html element with
1093 Returns boolean value of a given value, represented as html element with
1094 classes that will represent icons
1094 classes that will represent icons
1095
1095
1096 :param value: given value to convert to html node
1096 :param value: given value to convert to html node
1097 """
1097 """
1098
1098
1099 if value: # does bool conversion
1099 if value: # does bool conversion
1100 return HTML.tag('i', class_="icon-true", title='True')
1100 return HTML.tag('i', class_="icon-true", title='True')
1101 else: # not true as bool
1101 else: # not true as bool
1102 if show_at_false:
1102 if show_at_false:
1103 return HTML.tag('i', class_="icon-false", title='False')
1103 return HTML.tag('i', class_="icon-false", title='False')
1104 return HTML.tag('i')
1104 return HTML.tag('i')
1105
1105
1106 #==============================================================================
1106 #==============================================================================
1107 # PERMS
1107 # PERMS
1108 #==============================================================================
1108 #==============================================================================
1109 from rhodecode.lib.auth import (
1109 from rhodecode.lib.auth import (
1110 HasPermissionAny, HasPermissionAll,
1110 HasPermissionAny, HasPermissionAll,
1111 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1111 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1112 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1112 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1113 csrf_token_key, AuthUser)
1113 csrf_token_key, AuthUser)
1114
1114
1115
1115
1116 #==============================================================================
1116 #==============================================================================
1117 # GRAVATAR URL
1117 # GRAVATAR URL
1118 #==============================================================================
1118 #==============================================================================
1119 class InitialsGravatar(object):
1119 class InitialsGravatar(object):
1120 def __init__(self, email_address, first_name, last_name, size=30,
1120 def __init__(self, email_address, first_name, last_name, size=30,
1121 background=None, text_color='#fff'):
1121 background=None, text_color='#fff'):
1122 self.size = size
1122 self.size = size
1123 self.first_name = first_name
1123 self.first_name = first_name
1124 self.last_name = last_name
1124 self.last_name = last_name
1125 self.email_address = email_address
1125 self.email_address = email_address
1126 self.background = background or self.str2color(email_address)
1126 self.background = background or self.str2color(email_address)
1127 self.text_color = text_color
1127 self.text_color = text_color
1128
1128
1129 def get_color_bank(self):
1129 def get_color_bank(self):
1130 """
1130 """
1131 returns a predefined list of colors that gravatars can use.
1131 returns a predefined list of colors that gravatars can use.
1132 Those are randomized distinct colors that guarantee readability and
1132 Those are randomized distinct colors that guarantee readability and
1133 uniqueness.
1133 uniqueness.
1134
1134
1135 generated with: http://phrogz.net/css/distinct-colors.html
1135 generated with: http://phrogz.net/css/distinct-colors.html
1136 """
1136 """
1137 return [
1137 return [
1138 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1138 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1139 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1139 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1140 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1140 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1141 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1141 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1142 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1142 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1143 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1143 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1144 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1144 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1145 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1145 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1146 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1146 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1147 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1147 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1148 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1148 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1149 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1149 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1150 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1150 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1151 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1151 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1152 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1152 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1153 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1153 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1154 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1154 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1155 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1155 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1156 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1156 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1157 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1157 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1158 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1158 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1159 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1159 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1160 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1160 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1161 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1161 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1162 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1162 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1163 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1163 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1164 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1164 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1165 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1165 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1166 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1166 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1167 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1167 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1168 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1168 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1169 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1169 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1170 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1170 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1171 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1171 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1172 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1172 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1173 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1173 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1174 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1174 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1175 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1175 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1176 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1176 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1177 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1177 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1178 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1178 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1179 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1179 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1180 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1180 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1181 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1181 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1182 '#4f8c46', '#368dd9', '#5c0073'
1182 '#4f8c46', '#368dd9', '#5c0073'
1183 ]
1183 ]
1184
1184
1185 def rgb_to_hex_color(self, rgb_tuple):
1185 def rgb_to_hex_color(self, rgb_tuple):
1186 """
1186 """
1187 Converts an rgb_tuple passed to an hex color.
1187 Converts an rgb_tuple passed to an hex color.
1188
1188
1189 :param rgb_tuple: tuple with 3 ints represents rgb color space
1189 :param rgb_tuple: tuple with 3 ints represents rgb color space
1190 """
1190 """
1191 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1191 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1192
1192
1193 def email_to_int_list(self, email_str):
1193 def email_to_int_list(self, email_str):
1194 """
1194 """
1195 Get every byte of the hex digest value of email and turn it to integer.
1195 Get every byte of the hex digest value of email and turn it to integer.
1196 It's going to be always between 0-255
1196 It's going to be always between 0-255
1197 """
1197 """
1198 digest = md5_safe(email_str.lower())
1198 digest = md5_safe(email_str.lower())
1199 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1199 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1200
1200
1201 def pick_color_bank_index(self, email_str, color_bank):
1201 def pick_color_bank_index(self, email_str, color_bank):
1202 return self.email_to_int_list(email_str)[0] % len(color_bank)
1202 return self.email_to_int_list(email_str)[0] % len(color_bank)
1203
1203
1204 def str2color(self, email_str):
1204 def str2color(self, email_str):
1205 """
1205 """
1206 Tries to map in a stable algorithm an email to color
1206 Tries to map in a stable algorithm an email to color
1207
1207
1208 :param email_str:
1208 :param email_str:
1209 """
1209 """
1210 color_bank = self.get_color_bank()
1210 color_bank = self.get_color_bank()
1211 # pick position (module it's length so we always find it in the
1211 # pick position (module it's length so we always find it in the
1212 # bank even if it's smaller than 256 values
1212 # bank even if it's smaller than 256 values
1213 pos = self.pick_color_bank_index(email_str, color_bank)
1213 pos = self.pick_color_bank_index(email_str, color_bank)
1214 return color_bank[pos]
1214 return color_bank[pos]
1215
1215
1216 def normalize_email(self, email_address):
1216 def normalize_email(self, email_address):
1217 import unicodedata
1217 import unicodedata
1218 # default host used to fill in the fake/missing email
1218 # default host used to fill in the fake/missing email
1219 default_host = u'localhost'
1219 default_host = u'localhost'
1220
1220
1221 if not email_address:
1221 if not email_address:
1222 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1222 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1223
1223
1224 email_address = safe_unicode(email_address)
1224 email_address = safe_unicode(email_address)
1225
1225
1226 if u'@' not in email_address:
1226 if u'@' not in email_address:
1227 email_address = u'%s@%s' % (email_address, default_host)
1227 email_address = u'%s@%s' % (email_address, default_host)
1228
1228
1229 if email_address.endswith(u'@'):
1229 if email_address.endswith(u'@'):
1230 email_address = u'%s%s' % (email_address, default_host)
1230 email_address = u'%s%s' % (email_address, default_host)
1231
1231
1232 email_address = unicodedata.normalize('NFKD', email_address)\
1232 email_address = unicodedata.normalize('NFKD', email_address)\
1233 .encode('ascii', 'ignore')
1233 .encode('ascii', 'ignore')
1234 return email_address
1234 return email_address
1235
1235
1236 def get_initials(self):
1236 def get_initials(self):
1237 """
1237 """
1238 Returns 2 letter initials calculated based on the input.
1238 Returns 2 letter initials calculated based on the input.
1239 The algorithm picks first given email address, and takes first letter
1239 The algorithm picks first given email address, and takes first letter
1240 of part before @, and then the first letter of server name. In case
1240 of part before @, and then the first letter of server name. In case
1241 the part before @ is in a format of `somestring.somestring2` it replaces
1241 the part before @ is in a format of `somestring.somestring2` it replaces
1242 the server letter with first letter of somestring2
1242 the server letter with first letter of somestring2
1243
1243
1244 In case function was initialized with both first and lastname, this
1244 In case function was initialized with both first and lastname, this
1245 overrides the extraction from email by first letter of the first and
1245 overrides the extraction from email by first letter of the first and
1246 last name. We add special logic to that functionality, In case Full name
1246 last name. We add special logic to that functionality, In case Full name
1247 is compound, like Guido Von Rossum, we use last part of the last name
1247 is compound, like Guido Von Rossum, we use last part of the last name
1248 (Von Rossum) picking `R`.
1248 (Von Rossum) picking `R`.
1249
1249
1250 Function also normalizes the non-ascii characters to they ascii
1250 Function also normalizes the non-ascii characters to they ascii
1251 representation, eg Δ„ => A
1251 representation, eg Δ„ => A
1252 """
1252 """
1253 import unicodedata
1253 import unicodedata
1254 # replace non-ascii to ascii
1254 # replace non-ascii to ascii
1255 first_name = unicodedata.normalize(
1255 first_name = unicodedata.normalize(
1256 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1256 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1257 last_name = unicodedata.normalize(
1257 last_name = unicodedata.normalize(
1258 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1258 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1259
1259
1260 # do NFKD encoding, and also make sure email has proper format
1260 # do NFKD encoding, and also make sure email has proper format
1261 email_address = self.normalize_email(self.email_address)
1261 email_address = self.normalize_email(self.email_address)
1262
1262
1263 # first push the email initials
1263 # first push the email initials
1264 prefix, server = email_address.split('@', 1)
1264 prefix, server = email_address.split('@', 1)
1265
1265
1266 # check if prefix is maybe a 'first_name.last_name' syntax
1266 # check if prefix is maybe a 'first_name.last_name' syntax
1267 _dot_split = prefix.rsplit('.', 1)
1267 _dot_split = prefix.rsplit('.', 1)
1268 if len(_dot_split) == 2 and _dot_split[1]:
1268 if len(_dot_split) == 2 and _dot_split[1]:
1269 initials = [_dot_split[0][0], _dot_split[1][0]]
1269 initials = [_dot_split[0][0], _dot_split[1][0]]
1270 else:
1270 else:
1271 initials = [prefix[0], server[0]]
1271 initials = [prefix[0], server[0]]
1272
1272
1273 # then try to replace either first_name or last_name
1273 # then try to replace either first_name or last_name
1274 fn_letter = (first_name or " ")[0].strip()
1274 fn_letter = (first_name or " ")[0].strip()
1275 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1275 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1276
1276
1277 if fn_letter:
1277 if fn_letter:
1278 initials[0] = fn_letter
1278 initials[0] = fn_letter
1279
1279
1280 if ln_letter:
1280 if ln_letter:
1281 initials[1] = ln_letter
1281 initials[1] = ln_letter
1282
1282
1283 return ''.join(initials).upper()
1283 return ''.join(initials).upper()
1284
1284
1285 def get_img_data_by_type(self, font_family, img_type):
1285 def get_img_data_by_type(self, font_family, img_type):
1286 default_user = """
1286 default_user = """
1287 <svg xmlns="http://www.w3.org/2000/svg"
1287 <svg xmlns="http://www.w3.org/2000/svg"
1288 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1288 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1289 viewBox="-15 -10 439.165 429.164"
1289 viewBox="-15 -10 439.165 429.164"
1290
1290
1291 xml:space="preserve"
1291 xml:space="preserve"
1292 style="background:{background};" >
1292 style="background:{background};" >
1293
1293
1294 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1294 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1295 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1295 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1296 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1296 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1297 168.596,153.916,216.671,
1297 168.596,153.916,216.671,
1298 204.583,216.671z" fill="{text_color}"/>
1298 204.583,216.671z" fill="{text_color}"/>
1299 <path d="M407.164,374.717L360.88,
1299 <path d="M407.164,374.717L360.88,
1300 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1300 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1301 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1301 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1302 15.366-44.203,23.488-69.076,23.488c-24.877,
1302 15.366-44.203,23.488-69.076,23.488c-24.877,
1303 0-48.762-8.122-69.078-23.488
1303 0-48.762-8.122-69.078-23.488
1304 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1304 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1305 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1305 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1306 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1306 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1307 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1307 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1308 19.402-10.527 C409.699,390.129,
1308 19.402-10.527 C409.699,390.129,
1309 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1309 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1310 </svg>""".format(
1310 </svg>""".format(
1311 size=self.size,
1311 size=self.size,
1312 background='#979797', # @grey4
1312 background='#979797', # @grey4
1313 text_color=self.text_color,
1313 text_color=self.text_color,
1314 font_family=font_family)
1314 font_family=font_family)
1315
1315
1316 return {
1316 return {
1317 "default_user": default_user
1317 "default_user": default_user
1318 }[img_type]
1318 }[img_type]
1319
1319
1320 def get_img_data(self, svg_type=None):
1320 def get_img_data(self, svg_type=None):
1321 """
1321 """
1322 generates the svg metadata for image
1322 generates the svg metadata for image
1323 """
1323 """
1324 fonts = [
1324 fonts = [
1325 '-apple-system',
1325 '-apple-system',
1326 'BlinkMacSystemFont',
1326 'BlinkMacSystemFont',
1327 'Segoe UI',
1327 'Segoe UI',
1328 'Roboto',
1328 'Roboto',
1329 'Oxygen-Sans',
1329 'Oxygen-Sans',
1330 'Ubuntu',
1330 'Ubuntu',
1331 'Cantarell',
1331 'Cantarell',
1332 'Helvetica Neue',
1332 'Helvetica Neue',
1333 'sans-serif'
1333 'sans-serif'
1334 ]
1334 ]
1335 font_family = ','.join(fonts)
1335 font_family = ','.join(fonts)
1336 if svg_type:
1336 if svg_type:
1337 return self.get_img_data_by_type(font_family, svg_type)
1337 return self.get_img_data_by_type(font_family, svg_type)
1338
1338
1339 initials = self.get_initials()
1339 initials = self.get_initials()
1340 img_data = """
1340 img_data = """
1341 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1341 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1342 width="{size}" height="{size}"
1342 width="{size}" height="{size}"
1343 style="width: 100%; height: 100%; background-color: {background}"
1343 style="width: 100%; height: 100%; background-color: {background}"
1344 viewBox="0 0 {size} {size}">
1344 viewBox="0 0 {size} {size}">
1345 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1345 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1346 pointer-events="auto" fill="{text_color}"
1346 pointer-events="auto" fill="{text_color}"
1347 font-family="{font_family}"
1347 font-family="{font_family}"
1348 style="font-weight: 400; font-size: {f_size}px;">{text}
1348 style="font-weight: 400; font-size: {f_size}px;">{text}
1349 </text>
1349 </text>
1350 </svg>""".format(
1350 </svg>""".format(
1351 size=self.size,
1351 size=self.size,
1352 f_size=self.size/2.05, # scale the text inside the box nicely
1352 f_size=self.size/2.05, # scale the text inside the box nicely
1353 background=self.background,
1353 background=self.background,
1354 text_color=self.text_color,
1354 text_color=self.text_color,
1355 text=initials.upper(),
1355 text=initials.upper(),
1356 font_family=font_family)
1356 font_family=font_family)
1357
1357
1358 return img_data
1358 return img_data
1359
1359
1360 def generate_svg(self, svg_type=None):
1360 def generate_svg(self, svg_type=None):
1361 img_data = self.get_img_data(svg_type)
1361 img_data = self.get_img_data(svg_type)
1362 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1362 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1363
1363
1364
1364
1365 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1365 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1366
1366
1367 svg_type = None
1367 svg_type = None
1368 if email_address == User.DEFAULT_USER_EMAIL:
1368 if email_address == User.DEFAULT_USER_EMAIL:
1369 svg_type = 'default_user'
1369 svg_type = 'default_user'
1370
1370
1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1372
1372
1373 if store_on_disk:
1373 if store_on_disk:
1374 from rhodecode.apps.file_store import utils as store_utils
1374 from rhodecode.apps.file_store import utils as store_utils
1375 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1375 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1376 FileOverSizeException
1376 FileOverSizeException
1377 from rhodecode.model.db import Session
1377 from rhodecode.model.db import Session
1378
1378
1379 image_key = md5_safe(email_address.lower()
1379 image_key = md5_safe(email_address.lower()
1380 + first_name.lower() + last_name.lower())
1380 + first_name.lower() + last_name.lower())
1381
1381
1382 storage = store_utils.get_file_storage(request.registry.settings)
1382 storage = store_utils.get_file_storage(request.registry.settings)
1383 filename = '{}.svg'.format(image_key)
1383 filename = '{}.svg'.format(image_key)
1384 subdir = 'gravatars'
1384 subdir = 'gravatars'
1385 # since final name has a counter, we apply the 0
1385 # since final name has a counter, we apply the 0
1386 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1386 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1387 store_uid = os.path.join(subdir, uid)
1387 store_uid = os.path.join(subdir, uid)
1388
1388
1389 db_entry = FileStore.get_by_store_uid(store_uid)
1389 db_entry = FileStore.get_by_store_uid(store_uid)
1390 if db_entry:
1390 if db_entry:
1391 return request.route_path('download_file', fid=store_uid)
1391 return request.route_path('download_file', fid=store_uid)
1392
1392
1393 img_data = klass.get_img_data(svg_type=svg_type)
1393 img_data = klass.get_img_data(svg_type=svg_type)
1394 img_file = store_utils.bytes_to_file_obj(img_data)
1394 img_file = store_utils.bytes_to_file_obj(img_data)
1395
1395
1396 try:
1396 try:
1397 store_uid, metadata = storage.save_file(
1397 store_uid, metadata = storage.save_file(
1398 img_file, filename, directory=subdir,
1398 img_file, filename, directory=subdir,
1399 extensions=['.svg'], randomized_name=False)
1399 extensions=['.svg'], randomized_name=False)
1400 except (FileNotAllowedException, FileOverSizeException):
1400 except (FileNotAllowedException, FileOverSizeException):
1401 raise
1401 raise
1402
1402
1403 try:
1403 try:
1404 entry = FileStore.create(
1404 entry = FileStore.create(
1405 file_uid=store_uid, filename=metadata["filename"],
1405 file_uid=store_uid, filename=metadata["filename"],
1406 file_hash=metadata["sha256"], file_size=metadata["size"],
1406 file_hash=metadata["sha256"], file_size=metadata["size"],
1407 file_display_name=filename,
1407 file_display_name=filename,
1408 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1408 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1409 hidden=True, check_acl=False, user_id=1
1409 hidden=True, check_acl=False, user_id=1
1410 )
1410 )
1411 Session().add(entry)
1411 Session().add(entry)
1412 Session().commit()
1412 Session().commit()
1413 log.debug('Stored upload in DB as %s', entry)
1413 log.debug('Stored upload in DB as %s', entry)
1414 except Exception:
1414 except Exception:
1415 raise
1415 raise
1416
1416
1417 return request.route_path('download_file', fid=store_uid)
1417 return request.route_path('download_file', fid=store_uid)
1418
1418
1419 else:
1419 else:
1420 return klass.generate_svg(svg_type=svg_type)
1420 return klass.generate_svg(svg_type=svg_type)
1421
1421
1422
1422
1423 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1423 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1424 return safe_str(gravatar_url_tmpl)\
1424 return safe_str(gravatar_url_tmpl)\
1425 .replace('{email}', email_address) \
1425 .replace('{email}', email_address) \
1426 .replace('{md5email}', md5_safe(email_address.lower())) \
1426 .replace('{md5email}', md5_safe(email_address.lower())) \
1427 .replace('{netloc}', request.host) \
1427 .replace('{netloc}', request.host) \
1428 .replace('{scheme}', request.scheme) \
1428 .replace('{scheme}', request.scheme) \
1429 .replace('{size}', safe_str(size))
1429 .replace('{size}', safe_str(size))
1430
1430
1431
1431
1432 def gravatar_url(email_address, size=30, request=None):
1432 def gravatar_url(email_address, size=30, request=None):
1433 request = request or get_current_request()
1433 request = request or get_current_request()
1434 _use_gravatar = request.call_context.visual.use_gravatar
1434 _use_gravatar = request.call_context.visual.use_gravatar
1435
1435
1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1437 if isinstance(email_address, unicode):
1437 if isinstance(email_address, unicode):
1438 # hashlib crashes on unicode items
1438 # hashlib crashes on unicode items
1439 email_address = safe_str(email_address)
1439 email_address = safe_str(email_address)
1440
1440
1441 # empty email or default user
1441 # empty email or default user
1442 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1442 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1443 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1443 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1444
1444
1445 if _use_gravatar:
1445 if _use_gravatar:
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1447 or User.DEFAULT_GRAVATAR_URL
1447 or User.DEFAULT_GRAVATAR_URL
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1449
1449
1450 else:
1450 else:
1451 return initials_gravatar(request, email_address, '', '', size=size)
1451 return initials_gravatar(request, email_address, '', '', size=size)
1452
1452
1453
1453
1454 def breadcrumb_repo_link(repo):
1454 def breadcrumb_repo_link(repo):
1455 """
1455 """
1456 Makes a breadcrumbs path link to repo
1456 Makes a breadcrumbs path link to repo
1457
1457
1458 ex::
1458 ex::
1459 group >> subgroup >> repo
1459 group >> subgroup >> repo
1460
1460
1461 :param repo: a Repository instance
1461 :param repo: a Repository instance
1462 """
1462 """
1463
1463
1464 path = [
1464 path = [
1465 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1465 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1466 title='last change:{}'.format(format_date(group.last_commit_change)))
1466 title='last change:{}'.format(format_date(group.last_commit_change)))
1467 for group in repo.groups_with_parents
1467 for group in repo.groups_with_parents
1468 ] + [
1468 ] + [
1469 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1469 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1470 title='last change:{}'.format(format_date(repo.last_commit_change)))
1470 title='last change:{}'.format(format_date(repo.last_commit_change)))
1471 ]
1471 ]
1472
1472
1473 return literal(' &raquo; '.join(path))
1473 return literal(' &raquo; '.join(path))
1474
1474
1475
1475
1476 def breadcrumb_repo_group_link(repo_group):
1476 def breadcrumb_repo_group_link(repo_group):
1477 """
1477 """
1478 Makes a breadcrumbs path link to repo
1478 Makes a breadcrumbs path link to repo
1479
1479
1480 ex::
1480 ex::
1481 group >> subgroup
1481 group >> subgroup
1482
1482
1483 :param repo_group: a Repository Group instance
1483 :param repo_group: a Repository Group instance
1484 """
1484 """
1485
1485
1486 path = [
1486 path = [
1487 link_to(group.name,
1487 link_to(group.name,
1488 route_path('repo_group_home', repo_group_name=group.group_name),
1488 route_path('repo_group_home', repo_group_name=group.group_name),
1489 title='last change:{}'.format(format_date(group.last_commit_change)))
1489 title='last change:{}'.format(format_date(group.last_commit_change)))
1490 for group in repo_group.parents
1490 for group in repo_group.parents
1491 ] + [
1491 ] + [
1492 link_to(repo_group.name,
1492 link_to(repo_group.name,
1493 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1493 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1494 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1494 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1495 ]
1495 ]
1496
1496
1497 return literal(' &raquo; '.join(path))
1497 return literal(' &raquo; '.join(path))
1498
1498
1499
1499
1500 def format_byte_size_binary(file_size):
1500 def format_byte_size_binary(file_size):
1501 """
1501 """
1502 Formats file/folder sizes to standard.
1502 Formats file/folder sizes to standard.
1503 """
1503 """
1504 if file_size is None:
1504 if file_size is None:
1505 file_size = 0
1505 file_size = 0
1506
1506
1507 formatted_size = format_byte_size(file_size, binary=True)
1507 formatted_size = format_byte_size(file_size, binary=True)
1508 return formatted_size
1508 return formatted_size
1509
1509
1510
1510
1511 def urlify_text(text_, safe=True, **href_attrs):
1511 def urlify_text(text_, safe=True, **href_attrs):
1512 """
1512 """
1513 Extract urls from text and make html links out of them
1513 Extract urls from text and make html links out of them
1514 """
1514 """
1515
1515
1516 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1516 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1517 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1517 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1518
1518
1519 def url_func(match_obj):
1519 def url_func(match_obj):
1520 url_full = match_obj.groups()[0]
1520 url_full = match_obj.groups()[0]
1521 a_options = dict(href_attrs)
1521 a_options = dict(href_attrs)
1522 a_options['href'] = url_full
1522 a_options['href'] = url_full
1523 a_text = url_full
1523 a_text = url_full
1524 return HTML.tag("a", a_text, **a_options)
1524 return HTML.tag("a", a_text, **a_options)
1525
1525
1526 _new_text = url_pat.sub(url_func, text_)
1526 _new_text = url_pat.sub(url_func, text_)
1527
1527
1528 if safe:
1528 if safe:
1529 return literal(_new_text)
1529 return literal(_new_text)
1530 return _new_text
1530 return _new_text
1531
1531
1532
1532
1533 def urlify_commits(text_, repo_name):
1533 def urlify_commits(text_, repo_name):
1534 """
1534 """
1535 Extract commit ids from text and make link from them
1535 Extract commit ids from text and make link from them
1536
1536
1537 :param text_:
1537 :param text_:
1538 :param repo_name: repo name to build the URL with
1538 :param repo_name: repo name to build the URL with
1539 """
1539 """
1540
1540
1541 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1541 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1542
1542
1543 def url_func(match_obj):
1543 def url_func(match_obj):
1544 commit_id = match_obj.groups()[1]
1544 commit_id = match_obj.groups()[1]
1545 pref = match_obj.groups()[0]
1545 pref = match_obj.groups()[0]
1546 suf = match_obj.groups()[2]
1546 suf = match_obj.groups()[2]
1547
1547
1548 tmpl = (
1548 tmpl = (
1549 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1549 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1550 '%(commit_id)s</a>%(suf)s'
1550 '%(commit_id)s</a>%(suf)s'
1551 )
1551 )
1552 return tmpl % {
1552 return tmpl % {
1553 'pref': pref,
1553 'pref': pref,
1554 'cls': 'revision-link',
1554 'cls': 'revision-link',
1555 'url': route_url(
1555 'url': route_url(
1556 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1556 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1557 'commit_id': commit_id,
1557 'commit_id': commit_id,
1558 'suf': suf,
1558 'suf': suf,
1559 'hovercard_alt': 'Commit: {}'.format(commit_id),
1559 'hovercard_alt': 'Commit: {}'.format(commit_id),
1560 'hovercard_url': route_url(
1560 'hovercard_url': route_url(
1561 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1561 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1562 }
1562 }
1563
1563
1564 new_text = url_pat.sub(url_func, text_)
1564 new_text = url_pat.sub(url_func, text_)
1565
1565
1566 return new_text
1566 return new_text
1567
1567
1568
1568
1569 def _process_url_func(match_obj, repo_name, uid, entry,
1569 def _process_url_func(match_obj, repo_name, uid, entry,
1570 return_raw_data=False, link_format='html'):
1570 return_raw_data=False, link_format='html'):
1571 pref = ''
1571 pref = ''
1572 if match_obj.group().startswith(' '):
1572 if match_obj.group().startswith(' '):
1573 pref = ' '
1573 pref = ' '
1574
1574
1575 issue_id = ''.join(match_obj.groups())
1575 issue_id = ''.join(match_obj.groups())
1576
1576
1577 if link_format == 'html':
1577 if link_format == 'html':
1578 tmpl = (
1578 tmpl = (
1579 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1579 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1580 '%(issue-prefix)s%(id-repr)s'
1580 '%(issue-prefix)s%(id-repr)s'
1581 '</a>')
1581 '</a>')
1582 elif link_format == 'html+hovercard':
1582 elif link_format == 'html+hovercard':
1583 tmpl = (
1583 tmpl = (
1584 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1584 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1585 '%(issue-prefix)s%(id-repr)s'
1585 '%(issue-prefix)s%(id-repr)s'
1586 '</a>')
1586 '</a>')
1587 elif link_format in ['rst', 'rst+hovercard']:
1587 elif link_format in ['rst', 'rst+hovercard']:
1588 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1588 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1589 elif link_format in ['markdown', 'markdown+hovercard']:
1589 elif link_format in ['markdown', 'markdown+hovercard']:
1590 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1590 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1591 else:
1591 else:
1592 raise ValueError('Bad link_format:{}'.format(link_format))
1592 raise ValueError('Bad link_format:{}'.format(link_format))
1593
1593
1594 (repo_name_cleaned,
1594 (repo_name_cleaned,
1595 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1595 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1596
1596
1597 # variables replacement
1597 # variables replacement
1598 named_vars = {
1598 named_vars = {
1599 'id': issue_id,
1599 'id': issue_id,
1600 'repo': repo_name,
1600 'repo': repo_name,
1601 'repo_name': repo_name_cleaned,
1601 'repo_name': repo_name_cleaned,
1602 'group_name': parent_group_name,
1602 'group_name': parent_group_name,
1603 # set dummy keys so we always have them
1603 # set dummy keys so we always have them
1604 'hostname': '',
1604 'hostname': '',
1605 'netloc': '',
1605 'netloc': '',
1606 'scheme': ''
1606 'scheme': ''
1607 }
1607 }
1608
1608
1609 request = get_current_request()
1609 request = get_current_request()
1610 if request:
1610 if request:
1611 # exposes, hostname, netloc, scheme
1611 # exposes, hostname, netloc, scheme
1612 host_data = get_host_info(request)
1612 host_data = get_host_info(request)
1613 named_vars.update(host_data)
1613 named_vars.update(host_data)
1614
1614
1615 # named regex variables
1615 # named regex variables
1616 named_vars.update(match_obj.groupdict())
1616 named_vars.update(match_obj.groupdict())
1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1618 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1618 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1620
1620
1621 def quote_cleaner(input_str):
1621 def quote_cleaner(input_str):
1622 """Remove quotes as it's HTML"""
1622 """Remove quotes as it's HTML"""
1623 return input_str.replace('"', '')
1623 return input_str.replace('"', '')
1624
1624
1625 data = {
1625 data = {
1626 'pref': pref,
1626 'pref': pref,
1627 'cls': quote_cleaner('issue-tracker-link'),
1627 'cls': quote_cleaner('issue-tracker-link'),
1628 'url': quote_cleaner(_url),
1628 'url': quote_cleaner(_url),
1629 'id-repr': issue_id,
1629 'id-repr': issue_id,
1630 'issue-prefix': entry['pref'],
1630 'issue-prefix': entry['pref'],
1631 'serv': entry['url'],
1631 'serv': entry['url'],
1632 'title': bleach.clean(desc, strip=True),
1632 'title': bleach.clean(desc, strip=True),
1633 'hovercard_url': hovercard_url
1633 'hovercard_url': hovercard_url
1634 }
1634 }
1635
1635
1636 if return_raw_data:
1636 if return_raw_data:
1637 return {
1637 return {
1638 'id': issue_id,
1638 'id': issue_id,
1639 'url': _url
1639 'url': _url
1640 }
1640 }
1641 return tmpl % data
1641 return tmpl % data
1642
1642
1643
1643
1644 def get_active_pattern_entries(repo_name):
1644 def get_active_pattern_entries(repo_name):
1645 repo = None
1645 repo = None
1646 if repo_name:
1646 if repo_name:
1647 # Retrieving repo_name to avoid invalid repo_name to explode on
1647 # Retrieving repo_name to avoid invalid repo_name to explode on
1648 # IssueTrackerSettingsModel but still passing invalid name further down
1648 # IssueTrackerSettingsModel but still passing invalid name further down
1649 repo = Repository.get_by_repo_name(repo_name, cache=True)
1649 repo = Repository.get_by_repo_name(repo_name, cache=True)
1650
1650
1651 settings_model = IssueTrackerSettingsModel(repo=repo)
1651 settings_model = IssueTrackerSettingsModel(repo=repo)
1652 active_entries = settings_model.get_settings(cache=True)
1652 active_entries = settings_model.get_settings(cache=True)
1653 return active_entries
1653 return active_entries
1654
1654
1655
1655
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1657
1657
1658 allowed_link_formats = [
1658 allowed_link_formats = [
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1660
1660
1661
1661
1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1663
1663
1664 if link_format not in allowed_link_formats:
1664 if link_format not in allowed_link_formats:
1665 raise ValueError('Link format can be only one of:{} got {}'.format(
1665 raise ValueError('Link format can be only one of:{} got {}'.format(
1666 allowed_link_formats, link_format))
1666 allowed_link_formats, link_format))
1667
1667
1668 if active_entries is None:
1668 if active_entries is None:
1669 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1669 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1670 active_entries = get_active_pattern_entries(repo_name)
1670 active_entries = get_active_pattern_entries(repo_name)
1671
1671
1672 issues_data = []
1672 issues_data = []
1673 new_text = text_string
1673 new_text = text_string
1674
1674
1675 log.debug('Got %s entries to process', len(active_entries))
1675 log.debug('Got %s entries to process', len(active_entries))
1676 for uid, entry in active_entries.items():
1676 for uid, entry in active_entries.items():
1677 log.debug('found issue tracker entry with uid %s', uid)
1677 log.debug('found issue tracker entry with uid %s', uid)
1678
1678
1679 if not (entry['pat'] and entry['url']):
1679 if not (entry['pat'] and entry['url']):
1680 log.debug('skipping due to missing data')
1680 log.debug('skipping due to missing data')
1681 continue
1681 continue
1682
1682
1683 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1683 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1684 uid, entry['pat'], entry['url'], entry['pref'])
1684 uid, entry['pat'], entry['url'], entry['pref'])
1685
1685
1686 if entry.get('pat_compiled'):
1686 if entry.get('pat_compiled'):
1687 pattern = entry['pat_compiled']
1687 pattern = entry['pat_compiled']
1688 else:
1688 else:
1689 try:
1689 try:
1690 pattern = re.compile(r'%s' % entry['pat'])
1690 pattern = re.compile(r'%s' % entry['pat'])
1691 except re.error:
1691 except re.error:
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1693 continue
1693 continue
1694
1694
1695 data_func = partial(
1695 data_func = partial(
1696 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1696 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1697 return_raw_data=True)
1697 return_raw_data=True)
1698
1698
1699 for match_obj in pattern.finditer(text_string):
1699 for match_obj in pattern.finditer(text_string):
1700 issues_data.append(data_func(match_obj))
1700 issues_data.append(data_func(match_obj))
1701
1701
1702 url_func = partial(
1702 url_func = partial(
1703 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1703 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1704 link_format=link_format)
1704 link_format=link_format)
1705
1705
1706 new_text = pattern.sub(url_func, new_text)
1706 new_text = pattern.sub(url_func, new_text)
1707 log.debug('processed prefix:uid `%s`', uid)
1707 log.debug('processed prefix:uid `%s`', uid)
1708
1708
1709 # finally use global replace, eg !123 -> pr-link, those will not catch
1709 # finally use global replace, eg !123 -> pr-link, those will not catch
1710 # if already similar pattern exists
1710 # if already similar pattern exists
1711 server_url = '${scheme}://${netloc}'
1711 server_url = '${scheme}://${netloc}'
1712 pr_entry = {
1712 pr_entry = {
1713 'pref': '!',
1713 'pref': '!',
1714 'url': server_url + '/_admin/pull-requests/${id}',
1714 'url': server_url + '/_admin/pull-requests/${id}',
1715 'desc': 'Pull Request !${id}',
1715 'desc': 'Pull Request !${id}',
1716 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1716 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1717 }
1717 }
1718 pr_url_func = partial(
1718 pr_url_func = partial(
1719 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1719 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1720 link_format=link_format+'+hovercard')
1720 link_format=link_format+'+hovercard')
1721 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1721 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1722 log.debug('processed !pr pattern')
1722 log.debug('processed !pr pattern')
1723
1723
1724 return new_text, issues_data
1724 return new_text, issues_data
1725
1725
1726
1726
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1728 issues_container=None):
1729 """
1729 """
1730 Parses given text message and makes proper links.
1730 Parses given text message and makes proper links.
1731 issues are linked to given issue-server, and rest is a commit link
1731 issues are linked to given issue-server, and rest is a commit link
1732 """
1732 """
1733
1733
1734 def escaper(_text):
1734 def escaper(_text):
1735 return _text.replace('<', '&lt;').replace('>', '&gt;')
1735 return _text.replace('<', '&lt;').replace('>', '&gt;')
1736
1736
1737 new_text = escaper(commit_text)
1737 new_text = escaper(commit_text)
1738
1738
1739 # extract http/https links and make them real urls
1739 # extract http/https links and make them real urls
1740 new_text = urlify_text(new_text, safe=False)
1740 new_text = urlify_text(new_text, safe=False)
1741
1741
1742 # urlify commits - extract commit ids and make link out of them, if we have
1742 # urlify commits - extract commit ids and make link out of them, if we have
1743 # the scope of repository present.
1743 # the scope of repository present.
1744 if repository:
1744 if repository:
1745 new_text = urlify_commits(new_text, repository)
1745 new_text = urlify_commits(new_text, repository)
1746
1746
1747 # process issue tracker patterns
1747 # process issue tracker patterns
1748 new_text, issues = process_patterns(new_text, repository or '',
1748 new_text, issues = process_patterns(new_text, repository or '',
1749 active_entries=active_pattern_entries)
1749 active_entries=active_pattern_entries)
1750
1750
1751 if issues_container is not None:
1751 if issues_container is not None:
1752 issues_container.extend(issues)
1752 issues_container.extend(issues)
1753
1753
1754 return literal(new_text)
1754 return literal(new_text)
1755
1755
1756
1756
1757 def render_binary(repo_name, file_obj):
1757 def render_binary(repo_name, file_obj):
1758 """
1758 """
1759 Choose how to render a binary file
1759 Choose how to render a binary file
1760 """
1760 """
1761
1761
1762 # unicode
1762 # unicode
1763 filename = file_obj.name
1763 filename = file_obj.name
1764
1764
1765 # images
1765 # images
1766 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1766 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1767 if fnmatch.fnmatch(filename, pat=ext):
1767 if fnmatch.fnmatch(filename, pat=ext):
1768 src = route_path(
1768 src = route_path(
1769 'repo_file_raw', repo_name=repo_name,
1769 'repo_file_raw', repo_name=repo_name,
1770 commit_id=file_obj.commit.raw_id,
1770 commit_id=file_obj.commit.raw_id,
1771 f_path=file_obj.path)
1771 f_path=file_obj.path)
1772
1772
1773 return literal(
1773 return literal(
1774 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1774 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1775
1775
1776
1776
1777 def renderer_from_filename(filename, exclude=None):
1777 def renderer_from_filename(filename, exclude=None):
1778 """
1778 """
1779 choose a renderer based on filename, this works only for text based files
1779 choose a renderer based on filename, this works only for text based files
1780 """
1780 """
1781
1781
1782 # ipython
1782 # ipython
1783 for ext in ['*.ipynb']:
1783 for ext in ['*.ipynb']:
1784 if fnmatch.fnmatch(filename, pat=ext):
1784 if fnmatch.fnmatch(filename, pat=ext):
1785 return 'jupyter'
1785 return 'jupyter'
1786
1786
1787 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1787 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1788 if is_markup:
1788 if is_markup:
1789 return is_markup
1789 return is_markup
1790 return None
1790 return None
1791
1791
1792
1792
1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1794 repo_name=None, active_pattern_entries=None, issues_container=None):
1794 repo_name=None, active_pattern_entries=None, issues_container=None):
1795
1795
1796 def maybe_convert_relative_links(html_source):
1796 def maybe_convert_relative_links(html_source):
1797 if relative_urls:
1797 if relative_urls:
1798 return relative_links(html_source, relative_urls)
1798 return relative_links(html_source, relative_urls)
1799 return html_source
1799 return html_source
1800
1800
1801 if renderer == 'plain':
1801 if renderer == 'plain':
1802 return literal(
1802 return literal(
1803 MarkupRenderer.plain(source, leading_newline=False))
1803 MarkupRenderer.plain(source, leading_newline=False))
1804
1804
1805 elif renderer == 'rst':
1805 elif renderer == 'rst':
1806 if repo_name:
1806 if repo_name:
1807 # process patterns on comments if we pass in repo name
1807 # process patterns on comments if we pass in repo name
1808 source, issues = process_patterns(
1808 source, issues = process_patterns(
1809 source, repo_name, link_format='rst',
1809 source, repo_name, link_format='rst',
1810 active_entries=active_pattern_entries)
1810 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1811 if issues_container is not None:
1812 issues_container.extend(issues)
1812 issues_container.extend(issues)
1813
1813
1814 return literal(
1814 return literal(
1815 '<div class="rst-block">%s</div>' %
1815 '<div class="rst-block">%s</div>' %
1816 maybe_convert_relative_links(
1816 maybe_convert_relative_links(
1817 MarkupRenderer.rst(source, mentions=mentions)))
1817 MarkupRenderer.rst(source, mentions=mentions)))
1818
1818
1819 elif renderer == 'markdown':
1819 elif renderer == 'markdown':
1820 if repo_name:
1820 if repo_name:
1821 # process patterns on comments if we pass in repo name
1821 # process patterns on comments if we pass in repo name
1822 source, issues = process_patterns(
1822 source, issues = process_patterns(
1823 source, repo_name, link_format='markdown',
1823 source, repo_name, link_format='markdown',
1824 active_entries=active_pattern_entries)
1824 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1825 if issues_container is not None:
1826 issues_container.extend(issues)
1826 issues_container.extend(issues)
1827
1827
1828 return literal(
1828 return literal(
1829 '<div class="markdown-block">%s</div>' %
1829 '<div class="markdown-block">%s</div>' %
1830 maybe_convert_relative_links(
1830 maybe_convert_relative_links(
1831 MarkupRenderer.markdown(source, flavored=True,
1831 MarkupRenderer.markdown(source, flavored=True,
1832 mentions=mentions)))
1832 mentions=mentions)))
1833
1833
1834 elif renderer == 'jupyter':
1834 elif renderer == 'jupyter':
1835 return literal(
1835 return literal(
1836 '<div class="ipynb">%s</div>' %
1836 '<div class="ipynb">%s</div>' %
1837 maybe_convert_relative_links(
1837 maybe_convert_relative_links(
1838 MarkupRenderer.jupyter(source)))
1838 MarkupRenderer.jupyter(source)))
1839
1839
1840 # None means just show the file-source
1840 # None means just show the file-source
1841 return None
1841 return None
1842
1842
1843
1843
1844 def commit_status(repo, commit_id):
1844 def commit_status(repo, commit_id):
1845 return ChangesetStatusModel().get_status(repo, commit_id)
1845 return ChangesetStatusModel().get_status(repo, commit_id)
1846
1846
1847
1847
1848 def commit_status_lbl(commit_status):
1848 def commit_status_lbl(commit_status):
1849 return dict(ChangesetStatus.STATUSES).get(commit_status)
1849 return dict(ChangesetStatus.STATUSES).get(commit_status)
1850
1850
1851
1851
1852 def commit_time(repo_name, commit_id):
1852 def commit_time(repo_name, commit_id):
1853 repo = Repository.get_by_repo_name(repo_name)
1853 repo = Repository.get_by_repo_name(repo_name)
1854 commit = repo.get_commit(commit_id=commit_id)
1854 commit = repo.get_commit(commit_id=commit_id)
1855 return commit.date
1855 return commit.date
1856
1856
1857
1857
1858 def get_permission_name(key):
1858 def get_permission_name(key):
1859 return dict(Permission.PERMS).get(key)
1859 return dict(Permission.PERMS).get(key)
1860
1860
1861
1861
1862 def journal_filter_help(request):
1862 def journal_filter_help(request):
1863 _ = request.translate
1863 _ = request.translate
1864 from rhodecode.lib.audit_logger import ACTIONS
1864 from rhodecode.lib.audit_logger import ACTIONS
1865 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1865 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1866
1866
1867 return _(
1867 return _(
1868 'Example filter terms:\n' +
1868 'Example filter terms:\n' +
1869 ' repository:vcs\n' +
1869 ' repository:vcs\n' +
1870 ' username:marcin\n' +
1870 ' username:marcin\n' +
1871 ' username:(NOT marcin)\n' +
1871 ' username:(NOT marcin)\n' +
1872 ' action:*push*\n' +
1872 ' action:*push*\n' +
1873 ' ip:127.0.0.1\n' +
1873 ' ip:127.0.0.1\n' +
1874 ' date:20120101\n' +
1874 ' date:20120101\n' +
1875 ' date:[20120101100000 TO 20120102]\n' +
1875 ' date:[20120101100000 TO 20120102]\n' +
1876 '\n' +
1876 '\n' +
1877 'Actions: {actions}\n' +
1877 'Actions: {actions}\n' +
1878 '\n' +
1878 '\n' +
1879 'Generate wildcards using \'*\' character:\n' +
1879 'Generate wildcards using \'*\' character:\n' +
1880 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1880 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1881 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1881 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1882 '\n' +
1882 '\n' +
1883 'Optional AND / OR operators in queries\n' +
1883 'Optional AND / OR operators in queries\n' +
1884 ' "repository:vcs OR repository:test"\n' +
1884 ' "repository:vcs OR repository:test"\n' +
1885 ' "username:test AND repository:test*"\n'
1885 ' "username:test AND repository:test*"\n'
1886 ).format(actions=actions)
1886 ).format(actions=actions)
1887
1887
1888
1888
1889 def not_mapped_error(repo_name):
1889 def not_mapped_error(repo_name):
1890 from rhodecode.translation import _
1890 from rhodecode.translation import _
1891 flash(_('%s repository is not mapped to db perhaps'
1891 flash(_('%s repository is not mapped to db perhaps'
1892 ' it was created or renamed from the filesystem'
1892 ' it was created or renamed from the filesystem'
1893 ' please run the application again'
1893 ' please run the application again'
1894 ' in order to rescan repositories') % repo_name, category='error')
1894 ' in order to rescan repositories') % repo_name, category='error')
1895
1895
1896
1896
1897 def ip_range(ip_addr):
1897 def ip_range(ip_addr):
1898 from rhodecode.model.db import UserIpMap
1898 from rhodecode.model.db import UserIpMap
1899 s, e = UserIpMap._get_ip_range(ip_addr)
1899 s, e = UserIpMap._get_ip_range(ip_addr)
1900 return '%s - %s' % (s, e)
1900 return '%s - %s' % (s, e)
1901
1901
1902
1902
1903 def form(url, method='post', needs_csrf_token=True, **attrs):
1903 def form(url, method='post', needs_csrf_token=True, **attrs):
1904 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1904 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1905 if method.lower() != 'get' and needs_csrf_token:
1905 if method.lower() != 'get' and needs_csrf_token:
1906 raise Exception(
1906 raise Exception(
1907 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1907 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1908 'CSRF token. If the endpoint does not require such token you can ' +
1908 'CSRF token. If the endpoint does not require such token you can ' +
1909 'explicitly set the parameter needs_csrf_token to false.')
1909 'explicitly set the parameter needs_csrf_token to false.')
1910
1910
1911 return insecure_form(url, method=method, **attrs)
1911 return insecure_form(url, method=method, **attrs)
1912
1912
1913
1913
1914 def secure_form(form_url, method="POST", multipart=False, **attrs):
1914 def secure_form(form_url, method="POST", multipart=False, **attrs):
1915 """Start a form tag that points the action to an url. This
1915 """Start a form tag that points the action to an url. This
1916 form tag will also include the hidden field containing
1916 form tag will also include the hidden field containing
1917 the auth token.
1917 the auth token.
1918
1918
1919 The url options should be given either as a string, or as a
1919 The url options should be given either as a string, or as a
1920 ``url()`` function. The method for the form defaults to POST.
1920 ``url()`` function. The method for the form defaults to POST.
1921
1921
1922 Options:
1922 Options:
1923
1923
1924 ``multipart``
1924 ``multipart``
1925 If set to True, the enctype is set to "multipart/form-data".
1925 If set to True, the enctype is set to "multipart/form-data".
1926 ``method``
1926 ``method``
1927 The method to use when submitting the form, usually either
1927 The method to use when submitting the form, usually either
1928 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1928 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1929 hidden input with name _method is added to simulate the verb
1929 hidden input with name _method is added to simulate the verb
1930 over POST.
1930 over POST.
1931
1931
1932 """
1932 """
1933
1933
1934 if 'request' in attrs:
1934 if 'request' in attrs:
1935 session = attrs['request'].session
1935 session = attrs['request'].session
1936 del attrs['request']
1936 del attrs['request']
1937 else:
1937 else:
1938 raise ValueError(
1938 raise ValueError(
1939 'Calling this form requires request= to be passed as argument')
1939 'Calling this form requires request= to be passed as argument')
1940
1940
1941 _form = insecure_form(form_url, method, multipart, **attrs)
1941 _form = insecure_form(form_url, method, multipart, **attrs)
1942 token = literal(
1942 token = literal(
1943 '<input type="hidden" name="{}" value="{}">'.format(
1943 '<input type="hidden" name="{}" value="{}">'.format(
1944 csrf_token_key, get_csrf_token(session)))
1944 csrf_token_key, get_csrf_token(session)))
1945
1945
1946 return literal("%s\n%s" % (_form, token))
1946 return literal("%s\n%s" % (_form, token))
1947
1947
1948
1948
1949 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1949 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1950 select_html = select(name, selected, options, **attrs)
1950 select_html = select(name, selected, options, **attrs)
1951
1951
1952 select2 = """
1952 select2 = """
1953 <script>
1953 <script>
1954 $(document).ready(function() {
1954 $(document).ready(function() {
1955 $('#%s').select2({
1955 $('#%s').select2({
1956 containerCssClass: 'drop-menu %s',
1956 containerCssClass: 'drop-menu %s',
1957 dropdownCssClass: 'drop-menu-dropdown',
1957 dropdownCssClass: 'drop-menu-dropdown',
1958 dropdownAutoWidth: true%s
1958 dropdownAutoWidth: true%s
1959 });
1959 });
1960 });
1960 });
1961 </script>
1961 </script>
1962 """
1962 """
1963
1963
1964 filter_option = """,
1964 filter_option = """,
1965 minimumResultsForSearch: -1
1965 minimumResultsForSearch: -1
1966 """
1966 """
1967 input_id = attrs.get('id') or name
1967 input_id = attrs.get('id') or name
1968 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1968 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1969 filter_enabled = "" if enable_filter else filter_option
1969 filter_enabled = "" if enable_filter else filter_option
1970 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1970 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1971
1971
1972 return literal(select_html+select_script)
1972 return literal(select_html+select_script)
1973
1973
1974
1974
1975 def get_visual_attr(tmpl_context_var, attr_name):
1975 def get_visual_attr(tmpl_context_var, attr_name):
1976 """
1976 """
1977 A safe way to get a variable from visual variable of template context
1977 A safe way to get a variable from visual variable of template context
1978
1978
1979 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1979 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1980 :param attr_name: name of the attribute we fetch from the c.visual
1980 :param attr_name: name of the attribute we fetch from the c.visual
1981 """
1981 """
1982 visual = getattr(tmpl_context_var, 'visual', None)
1982 visual = getattr(tmpl_context_var, 'visual', None)
1983 if not visual:
1983 if not visual:
1984 return
1984 return
1985 else:
1985 else:
1986 return getattr(visual, attr_name, None)
1986 return getattr(visual, attr_name, None)
1987
1987
1988
1988
1989 def get_last_path_part(file_node):
1989 def get_last_path_part(file_node):
1990 if not file_node.path:
1990 if not file_node.path:
1991 return u'/'
1991 return u'/'
1992
1992
1993 path = safe_unicode(file_node.path.split('/')[-1])
1993 path = safe_unicode(file_node.path.split('/')[-1])
1994 return u'../' + path
1994 return u'../' + path
1995
1995
1996
1996
1997 def route_url(*args, **kwargs):
1997 def route_url(*args, **kwargs):
1998 """
1998 """
1999 Wrapper around pyramids `route_url` (fully qualified url) function.
1999 Wrapper around pyramids `route_url` (fully qualified url) function.
2000 """
2000 """
2001 req = get_current_request()
2001 req = get_current_request()
2002 return req.route_url(*args, **kwargs)
2002 return req.route_url(*args, **kwargs)
2003
2003
2004
2004
2005 def route_path(*args, **kwargs):
2005 def route_path(*args, **kwargs):
2006 """
2006 """
2007 Wrapper around pyramids `route_path` function.
2007 Wrapper around pyramids `route_path` function.
2008 """
2008 """
2009 req = get_current_request()
2009 req = get_current_request()
2010 return req.route_path(*args, **kwargs)
2010 return req.route_path(*args, **kwargs)
2011
2011
2012
2012
2013 def route_path_or_none(*args, **kwargs):
2013 def route_path_or_none(*args, **kwargs):
2014 try:
2014 try:
2015 return route_path(*args, **kwargs)
2015 return route_path(*args, **kwargs)
2016 except KeyError:
2016 except KeyError:
2017 return None
2017 return None
2018
2018
2019
2019
2020 def current_route_path(request, **kw):
2020 def current_route_path(request, **kw):
2021 new_args = request.GET.mixed()
2021 new_args = request.GET.mixed()
2022 new_args.update(kw)
2022 new_args.update(kw)
2023 return request.current_route_path(_query=new_args)
2023 return request.current_route_path(_query=new_args)
2024
2024
2025
2025
2026 def curl_api_example(method, args):
2026 def curl_api_example(method, args):
2027 args_json = json.dumps(OrderedDict([
2027 args_json = json.dumps(OrderedDict([
2028 ('id', 1),
2028 ('id', 1),
2029 ('auth_token', 'SECRET'),
2029 ('auth_token', 'SECRET'),
2030 ('method', method),
2030 ('method', method),
2031 ('args', args)
2031 ('args', args)
2032 ]))
2032 ]))
2033
2033
2034 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2034 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2035 api_url=route_url('apiv2'),
2035 api_url=route_url('apiv2'),
2036 args_json=args_json
2036 args_json=args_json
2037 )
2037 )
2038
2038
2039
2039
2040 def api_call_example(method, args):
2040 def api_call_example(method, args):
2041 """
2041 """
2042 Generates an API call example via CURL
2042 Generates an API call example via CURL
2043 """
2043 """
2044 curl_call = curl_api_example(method, args)
2044 curl_call = curl_api_example(method, args)
2045
2045
2046 return literal(
2046 return literal(
2047 curl_call +
2047 curl_call +
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2049 "and needs to be of `api calls` role."
2049 "and needs to be of `api calls` role."
2050 .format(token_url=route_url('my_account_auth_tokens')))
2050 .format(token_url=route_url('my_account_auth_tokens')))
2051
2051
2052
2052
2053 def notification_description(notification, request):
2053 def notification_description(notification, request):
2054 """
2054 """
2055 Generate notification human readable description based on notification type
2055 Generate notification human readable description based on notification type
2056 """
2056 """
2057 from rhodecode.model.notification import NotificationModel
2057 from rhodecode.model.notification import NotificationModel
2058 return NotificationModel().make_description(
2058 return NotificationModel().make_description(
2059 notification, translate=request.translate)
2059 notification, translate=request.translate)
2060
2060
2061
2061
2062 def go_import_header(request, db_repo=None):
2062 def go_import_header(request, db_repo=None):
2063 """
2063 """
2064 Creates a header for go-import functionality in Go Lang
2064 Creates a header for go-import functionality in Go Lang
2065 """
2065 """
2066
2066
2067 if not db_repo:
2067 if not db_repo:
2068 return
2068 return
2069 if 'go-get' not in request.GET:
2069 if 'go-get' not in request.GET:
2070 return
2070 return
2071
2071
2072 clone_url = db_repo.clone_url()
2072 clone_url = db_repo.clone_url()
2073 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2073 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2074 # we have a repo and go-get flag,
2074 # we have a repo and go-get flag,
2075 return literal('<meta name="go-import" content="{} {} {}">'.format(
2075 return literal('<meta name="go-import" content="{} {} {}">'.format(
2076 prefix, db_repo.repo_type, clone_url))
2076 prefix, db_repo.repo_type, clone_url))
2077
2077
2078
2078
2079 def reviewer_as_json(*args, **kwargs):
2079 def reviewer_as_json(*args, **kwargs):
2080 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2080 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2081 return _reviewer_as_json(*args, **kwargs)
2081 return _reviewer_as_json(*args, **kwargs)
2082
2082
2083
2083
2084 def get_repo_view_type(request):
2084 def get_repo_view_type(request):
2085 route_name = request.matched_route.name
2085 route_name = request.matched_route.name
2086 route_to_view_type = {
2086 route_to_view_type = {
2087 'repo_changelog': 'commits',
2087 'repo_changelog': 'commits',
2088 'repo_commits': 'commits',
2088 'repo_commits': 'commits',
2089 'repo_files': 'files',
2089 'repo_files': 'files',
2090 'repo_summary': 'summary',
2090 'repo_summary': 'summary',
2091 'repo_commit': 'commit'
2091 'repo_commit': 'commit'
2092 }
2092 }
2093
2093
2094 return route_to_view_type.get(route_name)
2094 return route_to_view_type.get(route_name)
2095
2095
2096
2096
2097 def is_active(menu_entry, selected):
2097 def is_active(menu_entry, selected):
2098 """
2098 """
2099 Returns active class for selecting menus in templates
2099 Returns active class for selecting menus in templates
2100 <li class=${h.is_active('settings', current_active)}></li>
2100 <li class=${h.is_active('settings', current_active)}></li>
2101 """
2101 """
2102 if not isinstance(menu_entry, list):
2102 if not isinstance(menu_entry, list):
2103 menu_entry = [menu_entry]
2103 menu_entry = [menu_entry]
2104
2104
2105 if selected in menu_entry:
2105 if selected in menu_entry:
2106 return "active"
2106 return "active"
General Comments 0
You need to be logged in to leave comments. Login now