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