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