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