##// END OF EJS Templates
visual: fixed show revision/commit lenght settings
dan -
r3404:3901c5ef default
parent child Browse files
Show More
@@ -1,567 +1,576 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 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 from rhodecode.apps._base import TemplateArgs
38 39 from rhodecode.authentication.base import VCS_TYPE
39 40 from rhodecode.lib import auth, utils2
40 41 from rhodecode.lib import helpers as h
41 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 43 from rhodecode.lib.exceptions import UserCreationError
43 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 45 from rhodecode.lib.utils2 import (
45 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 47 from rhodecode.model.db import Repository, User, ChangesetComment
47 48 from rhodecode.model.notification import NotificationModel
48 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 50
50 51 log = logging.getLogger(__name__)
51 52
52 53
53 54 def _filter_proxy(ip):
54 55 """
55 56 Passed in IP addresses in HEADERS can be in a special format of multiple
56 57 ips. Those comma separated IPs are passed from various proxies in the
57 58 chain of request processing. The left-most being the original client.
58 59 We only care about the first IP which came from the org. client.
59 60
60 61 :param ip: ip string from headers
61 62 """
62 63 if ',' in ip:
63 64 _ips = ip.split(',')
64 65 _first_ip = _ips[0].strip()
65 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 67 return _first_ip
67 68 return ip
68 69
69 70
70 71 def _filter_port(ip):
71 72 """
72 73 Removes a port from ip, there are 4 main cases to handle here.
73 74 - ipv4 eg. 127.0.0.1
74 75 - ipv6 eg. ::1
75 76 - ipv4+port eg. 127.0.0.1:8080
76 77 - ipv6+port eg. [::1]:8080
77 78
78 79 :param ip:
79 80 """
80 81 def is_ipv6(ip_addr):
81 82 if hasattr(socket, 'inet_pton'):
82 83 try:
83 84 socket.inet_pton(socket.AF_INET6, ip_addr)
84 85 except socket.error:
85 86 return False
86 87 else:
87 88 # fallback to ipaddress
88 89 try:
89 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 91 except Exception:
91 92 return False
92 93 return True
93 94
94 95 if ':' not in ip: # must be ipv4 pure ip
95 96 return ip
96 97
97 98 if '[' in ip and ']' in ip: # ipv6 with port
98 99 return ip.split(']')[0][1:].lower()
99 100
100 101 # must be ipv6 or ipv4 with port
101 102 if is_ipv6(ip):
102 103 return ip
103 104 else:
104 105 ip, _port = ip.split(':')[:2] # means ipv4+port
105 106 return ip
106 107
107 108
108 109 def get_ip_addr(environ):
109 110 proxy_key = 'HTTP_X_REAL_IP'
110 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 112 def_key = 'REMOTE_ADDR'
112 113 _filters = lambda x: _filter_port(_filter_proxy(x))
113 114
114 115 ip = environ.get(proxy_key)
115 116 if ip:
116 117 return _filters(ip)
117 118
118 119 ip = environ.get(proxy_key2)
119 120 if ip:
120 121 return _filters(ip)
121 122
122 123 ip = environ.get(def_key, '0.0.0.0')
123 124 return _filters(ip)
124 125
125 126
126 127 def get_server_ip_addr(environ, log_errors=True):
127 128 hostname = environ.get('SERVER_NAME')
128 129 try:
129 130 return socket.gethostbyname(hostname)
130 131 except Exception as e:
131 132 if log_errors:
132 133 # in some cases this lookup is not possible, and we don't want to
133 134 # make it an exception in logs
134 135 log.exception('Could not retrieve server ip address: %s', e)
135 136 return hostname
136 137
137 138
138 139 def get_server_port(environ):
139 140 return environ.get('SERVER_PORT')
140 141
141 142
142 143 def get_access_path(environ):
143 144 path = environ.get('PATH_INFO')
144 145 org_req = environ.get('pylons.original_request')
145 146 if org_req:
146 147 path = org_req.environ.get('PATH_INFO')
147 148 return path
148 149
149 150
150 151 def get_user_agent(environ):
151 152 return environ.get('HTTP_USER_AGENT')
152 153
153 154
154 155 def vcs_operation_context(
155 156 environ, repo_name, username, action, scm, check_locking=True,
156 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
157 158 """
158 159 Generate the context for a vcs operation, e.g. push or pull.
159 160
160 161 This context is passed over the layers so that hooks triggered by the
161 162 vcs operation know details like the user, the user's IP address etc.
162 163
163 164 :param check_locking: Allows to switch of the computation of the locking
164 165 data. This serves mainly the need of the simplevcs middleware to be
165 166 able to disable this for certain operations.
166 167
167 168 """
168 169 # Tri-state value: False: unlock, None: nothing, True: lock
169 170 make_lock = None
170 171 locked_by = [None, None, None]
171 172 is_anonymous = username == User.DEFAULT_USER
172 173 user = User.get_by_username(username)
173 174 if not is_anonymous and check_locking:
174 175 log.debug('Checking locking on repository "%s"', repo_name)
175 176 repo = Repository.get_by_repo_name(repo_name)
176 177 make_lock, __, locked_by = repo.get_locking_state(
177 178 action, user.user_id)
178 179 user_id = user.user_id
179 180 settings_model = VcsSettingsModel(repo=repo_name)
180 181 ui_settings = settings_model.get_ui_settings()
181 182
182 183 # NOTE(marcink): This should be also in sync with
183 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
184 185 store = [x for x in ui_settings if x.key == '/']
185 186 repo_store = ''
186 187 if store:
187 188 repo_store = store[0].value
188 189
189 190 scm_data = {
190 191 'ip': get_ip_addr(environ),
191 192 'username': username,
192 193 'user_id': user_id,
193 194 'action': action,
194 195 'repository': repo_name,
195 196 'scm': scm,
196 197 'config': rhodecode.CONFIG['__file__'],
197 198 'repo_store': repo_store,
198 199 'make_lock': make_lock,
199 200 'locked_by': locked_by,
200 201 'server_url': utils2.get_server_url(environ),
201 202 'user_agent': get_user_agent(environ),
202 203 'hooks': get_enabled_hook_classes(ui_settings),
203 204 'is_shadow_repo': is_shadow_repo,
204 205 'detect_force_push': detect_force_push,
205 206 'check_branch_perms': check_branch_perms,
206 207 }
207 208 return scm_data
208 209
209 210
210 211 class BasicAuth(AuthBasicAuthenticator):
211 212
212 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
213 214 initial_call_detection=False, acl_repo_name=None):
214 215 self.realm = realm
215 216 self.initial_call = initial_call_detection
216 217 self.authfunc = authfunc
217 218 self.registry = registry
218 219 self.acl_repo_name = acl_repo_name
219 220 self._rc_auth_http_code = auth_http_code
220 221
221 222 def _get_response_from_code(self, http_code):
222 223 try:
223 224 return get_exception(safe_int(http_code))
224 225 except Exception:
225 226 log.exception('Failed to fetch response for code %s', http_code)
226 227 return HTTPForbidden
227 228
228 229 def get_rc_realm(self):
229 230 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
230 231
231 232 def build_authentication(self):
232 233 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
233 234 if self._rc_auth_http_code and not self.initial_call:
234 235 # return alternative HTTP code if alternative http return code
235 236 # is specified in RhodeCode config, but ONLY if it's not the
236 237 # FIRST call
237 238 custom_response_klass = self._get_response_from_code(
238 239 self._rc_auth_http_code)
239 240 return custom_response_klass(headers=head)
240 241 return HTTPUnauthorized(headers=head)
241 242
242 243 def authenticate(self, environ):
243 244 authorization = AUTHORIZATION(environ)
244 245 if not authorization:
245 246 return self.build_authentication()
246 247 (authmeth, auth) = authorization.split(' ', 1)
247 248 if 'basic' != authmeth.lower():
248 249 return self.build_authentication()
249 250 auth = auth.strip().decode('base64')
250 251 _parts = auth.split(':', 1)
251 252 if len(_parts) == 2:
252 253 username, password = _parts
253 254 auth_data = self.authfunc(
254 255 username, password, environ, VCS_TYPE,
255 256 registry=self.registry, acl_repo_name=self.acl_repo_name)
256 257 if auth_data:
257 258 return {'username': username, 'auth_data': auth_data}
258 259 if username and password:
259 260 # we mark that we actually executed authentication once, at
260 261 # that point we can use the alternative auth code
261 262 self.initial_call = False
262 263
263 264 return self.build_authentication()
264 265
265 266 __call__ = authenticate
266 267
267 268
268 269 def calculate_version_hash(config):
269 270 return sha1(
270 271 config.get('beaker.session.secret', '') +
271 272 rhodecode.__version__)[:8]
272 273
273 274
274 275 def get_current_lang(request):
275 276 # NOTE(marcink): remove after pyramid move
276 277 try:
277 278 return translation.get_lang()[0]
278 279 except:
279 280 pass
280 281
281 282 return getattr(request, '_LOCALE_', request.locale_name)
282 283
283 284
284 285 def attach_context_attributes(context, request, user_id):
285 286 """
286 287 Attach variables into template context called `c`.
287 288 """
288 289 config = request.registry.settings
289 290
290 291
291 292 rc_config = SettingsModel().get_all_settings(cache=True)
292 293
293 294 context.rhodecode_version = rhodecode.__version__
294 295 context.rhodecode_edition = config.get('rhodecode.edition')
295 296 # unique secret + version does not leak the version but keep consistency
296 297 context.rhodecode_version_hash = calculate_version_hash(config)
297 298
298 299 # Default language set for the incoming request
299 300 context.language = get_current_lang(request)
300 301
301 302 # Visual options
302 303 context.visual = AttributeDict({})
303 304
304 305 # DB stored Visual Items
305 306 context.visual.show_public_icon = str2bool(
306 307 rc_config.get('rhodecode_show_public_icon'))
307 308 context.visual.show_private_icon = str2bool(
308 309 rc_config.get('rhodecode_show_private_icon'))
309 310 context.visual.stylify_metatags = str2bool(
310 311 rc_config.get('rhodecode_stylify_metatags'))
311 312 context.visual.dashboard_items = safe_int(
312 313 rc_config.get('rhodecode_dashboard_items', 100))
313 314 context.visual.admin_grid_items = safe_int(
314 315 rc_config.get('rhodecode_admin_grid_items', 100))
316 context.visual.show_revision_number = str2bool(
317 rc_config.get('rhodecode_show_revision_number', True))
318 context.visual.show_sha_length = safe_int(
319 rc_config.get('rhodecode_show_sha_length', 100))
315 320 context.visual.repository_fields = str2bool(
316 321 rc_config.get('rhodecode_repository_fields'))
317 322 context.visual.show_version = str2bool(
318 323 rc_config.get('rhodecode_show_version'))
319 324 context.visual.use_gravatar = str2bool(
320 325 rc_config.get('rhodecode_use_gravatar'))
321 326 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
322 327 context.visual.default_renderer = rc_config.get(
323 328 'rhodecode_markup_renderer', 'rst')
324 329 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
325 330 context.visual.rhodecode_support_url = \
326 331 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
327 332
328 333 context.visual.affected_files_cut_off = 60
329 334
330 335 context.pre_code = rc_config.get('rhodecode_pre_code')
331 336 context.post_code = rc_config.get('rhodecode_post_code')
332 337 context.rhodecode_name = rc_config.get('rhodecode_title')
333 338 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
334 339 # if we have specified default_encoding in the request, it has more
335 340 # priority
336 341 if request.GET.get('default_encoding'):
337 342 context.default_encodings.insert(0, request.GET.get('default_encoding'))
338 343 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
339 344 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
340 345
341 346 # INI stored
342 347 context.labs_active = str2bool(
343 348 config.get('labs_settings_active', 'false'))
344 349 context.ssh_enabled = str2bool(
345 350 config.get('ssh.generate_authorized_keyfile', 'false'))
346 351
347 352 context.visual.allow_repo_location_change = str2bool(
348 353 config.get('allow_repo_location_change', True))
349 354 context.visual.allow_custom_hooks_settings = str2bool(
350 355 config.get('allow_custom_hooks_settings', True))
351 356 context.debug_style = str2bool(config.get('debug_style', False))
352 357
353 358 context.rhodecode_instanceid = config.get('instance_id')
354 359
355 360 context.visual.cut_off_limit_diff = safe_int(
356 361 config.get('cut_off_limit_diff'))
357 362 context.visual.cut_off_limit_file = safe_int(
358 363 config.get('cut_off_limit_file'))
359 364
360 365 # AppEnlight
361 366 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
362 367 context.appenlight_api_public_key = config.get(
363 368 'appenlight.api_public_key', '')
364 369 context.appenlight_server_url = config.get('appenlight.server_url', '')
365 370
366 371 diffmode = {
367 372 "unified": "unified",
368 373 "sideside": "sideside"
369 374 }.get(request.GET.get('diffmode'))
370 375
371 376 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
372 377 request.session['rc_user_session_attr.diffmode'] = diffmode
373 378
374 379 # session settings per user
375 380 session_attrs = {
376 381 # defaults
377 382 "clone_url_format": "http",
378 383 "diffmode": "sideside"
379 384 }
380 385 for k, v in request.session.items():
381 386 pref = 'rc_user_session_attr.'
382 387 if k and k.startswith(pref):
383 388 k = k[len(pref):]
384 389 session_attrs[k] = v
385 390
386 391 context.user_session_attrs = session_attrs
387 392
388 393 # JS template context
389 394 context.template_context = {
390 395 'repo_name': None,
391 396 'repo_type': None,
392 397 'repo_landing_commit': None,
393 398 'rhodecode_user': {
394 399 'username': None,
395 400 'email': None,
396 401 'notification_status': False
397 402 },
398 403 'session_attrs': session_attrs,
399 404 'visual': {
400 405 'default_renderer': None
401 406 },
402 407 'commit_data': {
403 408 'commit_id': None
404 409 },
405 410 'pull_request_data': {'pull_request_id': None},
406 411 'timeago': {
407 412 'refresh_time': 120 * 1000,
408 413 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
409 414 },
410 415 'pyramid_dispatch': {
411 416
412 417 },
413 418 'extra': {'plugins': {}}
414 419 }
415 420 # END CONFIG VARS
416 421
417 422 context.csrf_token = auth.get_csrf_token(session=request.session)
418 423 context.backends = rhodecode.BACKENDS.keys()
419 424 context.backends.sort()
420 425 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
421 426
422 427 # web case
423 428 if hasattr(request, 'user'):
424 429 context.auth_user = request.user
425 430 context.rhodecode_user = request.user
426 431
427 432 # api case
428 433 if hasattr(request, 'rpc_user'):
429 434 context.auth_user = request.rpc_user
430 435 context.rhodecode_user = request.rpc_user
431 436
432 437 # attach the whole call context to the request
433 438 request.call_context = context
434 439
435 440
436 441 def get_auth_user(request):
437 442 environ = request.environ
438 443 session = request.session
439 444
440 445 ip_addr = get_ip_addr(environ)
441 446 # make sure that we update permissions each time we call controller
442 447 _auth_token = (request.GET.get('auth_token', '') or
443 448 request.GET.get('api_key', ''))
444 449
445 450 if _auth_token:
446 451 # when using API_KEY we assume user exists, and
447 452 # doesn't need auth based on cookies.
448 453 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
449 454 authenticated = False
450 455 else:
451 456 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
452 457 try:
453 458 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
454 459 ip_addr=ip_addr)
455 460 except UserCreationError as e:
456 461 h.flash(e, 'error')
457 462 # container auth or other auth functions that create users
458 463 # on the fly can throw this exception signaling that there's
459 464 # issue with user creation, explanation should be provided
460 465 # in Exception itself. We then create a simple blank
461 466 # AuthUser
462 467 auth_user = AuthUser(ip_addr=ip_addr)
463 468
464 469 # in case someone changes a password for user it triggers session
465 470 # flush and forces a re-login
466 471 if password_changed(auth_user, session):
467 472 session.invalidate()
468 473 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
469 474 auth_user = AuthUser(ip_addr=ip_addr)
470 475
471 476 authenticated = cookie_store.get('is_authenticated')
472 477
473 478 if not auth_user.is_authenticated and auth_user.is_user_object:
474 479 # user is not authenticated and not empty
475 480 auth_user.set_authenticated(authenticated)
476 481
477 482 return auth_user
478 483
479 484
480 485 def h_filter(s):
481 486 """
482 487 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
483 488 we wrap this with additional functionality that converts None to empty
484 489 strings
485 490 """
486 491 if s is None:
487 492 return markupsafe.Markup()
488 493 return markupsafe.escape(s)
489 494
490 495
491 496 def add_events_routes(config):
492 497 """
493 498 Adds routing that can be used in events. Because some events are triggered
494 499 outside of pyramid context, we need to bootstrap request with some
495 500 routing registered
496 501 """
497 502
498 503 from rhodecode.apps._base import ADMIN_PREFIX
499 504
500 505 config.add_route(name='home', pattern='/')
501 506
502 507 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
503 508 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
504 509 config.add_route(name='repo_summary', pattern='/{repo_name}')
505 510 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
506 511 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
507 512
508 513 config.add_route(name='pullrequest_show',
509 514 pattern='/{repo_name}/pull-request/{pull_request_id}')
510 515 config.add_route(name='pull_requests_global',
511 516 pattern='/pull-request/{pull_request_id}')
512 517 config.add_route(name='repo_commit',
513 518 pattern='/{repo_name}/changeset/{commit_id}')
514 519
515 520 config.add_route(name='repo_files',
516 521 pattern='/{repo_name}/files/{commit_id}/{f_path}')
517 522
518 523
519 524 def bootstrap_config(request):
520 525 import pyramid.testing
521 526 registry = pyramid.testing.Registry('RcTestRegistry')
522 527
523 528 config = pyramid.testing.setUp(registry=registry, request=request)
524 529
525 530 # allow pyramid lookup in testing
526 531 config.include('pyramid_mako')
527 532 config.include('pyramid_beaker')
528 533 config.include('rhodecode.lib.rc_cache')
529 534
530 535 add_events_routes(config)
531 536
532 537 return config
533 538
534 539
535 540 def bootstrap_request(**kwargs):
536 541 import pyramid.testing
537 542
538 543 class TestRequest(pyramid.testing.DummyRequest):
539 544 application_url = kwargs.pop('application_url', 'http://example.com')
540 545 host = kwargs.pop('host', 'example.com:80')
541 546 domain = kwargs.pop('domain', 'example.com')
542 547
543 548 def translate(self, msg):
544 549 return msg
545 550
546 551 def plularize(self, singular, plural, n):
547 552 return singular
548 553
549 554 def get_partial_renderer(self, tmpl_name):
550 555
551 556 from rhodecode.lib.partial_renderer import get_partial_renderer
552 557 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
553 558
554 _call_context = {}
559 _call_context = TemplateArgs()
560 _call_context.visual = TemplateArgs()
561 _call_context.visual.show_sha_length = 12
562 _call_context.visual.show_revision_number = True
563
555 564 @property
556 565 def call_context(self):
557 566 return self._call_context
558 567
559 568 class TestDummySession(pyramid.testing.DummySession):
560 569 def save(*arg, **kw):
561 570 pass
562 571
563 572 request = TestRequest(**kwargs)
564 573 request.session = TestDummySession()
565 574
566 575 return request
567 576
@@ -1,2018 +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 urlparse
38 38 import time
39 39 import string
40 40 import hashlib
41 41 from collections import OrderedDict
42 42
43 43 import pygments
44 44 import itertools
45 45 import fnmatch
46 46 import bleach
47 47
48 48 from 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 def _shorten_commit_id(commit_id):
687 from rhodecode import CONFIG
688 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
689 return commit_id[:def_len]
686 def _shorten_commit_id(commit_id, commit_len=None):
687 if commit_len is None:
688 request = get_current_request()
689 commit_len = request.call_context.visual.show_sha_length
690 return commit_id[:commit_len]
690 691
691 692
692 def show_id(commit):
693 def show_id(commit, show_idx=None, commit_len=None):
693 694 """
694 695 Configurable function that shows ID
695 696 by default it's r123:fffeeefffeee
696 697
697 698 :param commit: commit instance
698 699 """
699 from rhodecode import CONFIG
700 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
700 if show_idx is None:
701 request = get_current_request()
702 show_idx = request.call_context.visual.show_revision_number
701 703
702 raw_id = _shorten_commit_id(commit.raw_id)
704 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
703 705 if show_idx:
704 706 return 'r%s:%s' % (commit.idx, raw_id)
705 707 else:
706 708 return '%s' % (raw_id, )
707 709
708 710
709 711 def format_date(date):
710 712 """
711 713 use a standardized formatting for dates used in RhodeCode
712 714
713 715 :param date: date/datetime object
714 716 :return: formatted date
715 717 """
716 718
717 719 if date:
718 720 _fmt = "%a, %d %b %Y %H:%M:%S"
719 721 return safe_unicode(date.strftime(_fmt))
720 722
721 723 return u""
722 724
723 725
724 726 class _RepoChecker(object):
725 727
726 728 def __init__(self, backend_alias):
727 729 self._backend_alias = backend_alias
728 730
729 731 def __call__(self, repository):
730 732 if hasattr(repository, 'alias'):
731 733 _type = repository.alias
732 734 elif hasattr(repository, 'repo_type'):
733 735 _type = repository.repo_type
734 736 else:
735 737 _type = repository
736 738 return _type == self._backend_alias
737 739
738 740
739 741 is_git = _RepoChecker('git')
740 742 is_hg = _RepoChecker('hg')
741 743 is_svn = _RepoChecker('svn')
742 744
743 745
744 746 def get_repo_type_by_name(repo_name):
745 747 repo = Repository.get_by_repo_name(repo_name)
746 748 if repo:
747 749 return repo.repo_type
748 750
749 751
750 752 def is_svn_without_proxy(repository):
751 753 if is_svn(repository):
752 754 from rhodecode.model.settings import VcsSettingsModel
753 755 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
754 756 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
755 757 return False
756 758
757 759
758 760 def discover_user(author):
759 761 """
760 762 Tries to discover RhodeCode User based on the autho string. Author string
761 763 is typically `FirstName LastName <email@address.com>`
762 764 """
763 765
764 766 # if author is already an instance use it for extraction
765 767 if isinstance(author, User):
766 768 return author
767 769
768 770 # Valid email in the attribute passed, see if they're in the system
769 771 _email = author_email(author)
770 772 if _email != '':
771 773 user = User.get_by_email(_email, case_insensitive=True, cache=True)
772 774 if user is not None:
773 775 return user
774 776
775 777 # Maybe it's a username, we try to extract it and fetch by username ?
776 778 _author = author_name(author)
777 779 user = User.get_by_username(_author, case_insensitive=True, cache=True)
778 780 if user is not None:
779 781 return user
780 782
781 783 return None
782 784
783 785
784 786 def email_or_none(author):
785 787 # extract email from the commit string
786 788 _email = author_email(author)
787 789
788 790 # If we have an email, use it, otherwise
789 791 # see if it contains a username we can get an email from
790 792 if _email != '':
791 793 return _email
792 794 else:
793 795 user = User.get_by_username(
794 796 author_name(author), case_insensitive=True, cache=True)
795 797
796 798 if user is not None:
797 799 return user.email
798 800
799 801 # No valid email, not a valid user in the system, none!
800 802 return None
801 803
802 804
803 805 def link_to_user(author, length=0, **kwargs):
804 806 user = discover_user(author)
805 807 # user can be None, but if we have it already it means we can re-use it
806 808 # in the person() function, so we save 1 intensive-query
807 809 if user:
808 810 author = user
809 811
810 812 display_person = person(author, 'username_or_name_or_email')
811 813 if length:
812 814 display_person = shorter(display_person, length)
813 815
814 816 if user:
815 817 return link_to(
816 818 escape(display_person),
817 819 route_path('user_profile', username=user.username),
818 820 **kwargs)
819 821 else:
820 822 return escape(display_person)
821 823
822 824
823 825 def link_to_group(users_group_name, **kwargs):
824 826 return link_to(
825 827 escape(users_group_name),
826 828 route_path('user_group_profile', user_group_name=users_group_name),
827 829 **kwargs)
828 830
829 831
830 832 def person(author, show_attr="username_and_name"):
831 833 user = discover_user(author)
832 834 if user:
833 835 return getattr(user, show_attr)
834 836 else:
835 837 _author = author_name(author)
836 838 _email = email(author)
837 839 return _author or _email
838 840
839 841
840 842 def author_string(email):
841 843 if email:
842 844 user = User.get_by_email(email, case_insensitive=True, cache=True)
843 845 if user:
844 846 if user.first_name or user.last_name:
845 847 return '%s %s &lt;%s&gt;' % (
846 848 user.first_name, user.last_name, email)
847 849 else:
848 850 return email
849 851 else:
850 852 return email
851 853 else:
852 854 return None
853 855
854 856
855 857 def person_by_id(id_, show_attr="username_and_name"):
856 858 # attr to return from fetched user
857 859 person_getter = lambda usr: getattr(usr, show_attr)
858 860
859 861 #maybe it's an ID ?
860 862 if str(id_).isdigit() or isinstance(id_, int):
861 863 id_ = int(id_)
862 864 user = User.get(id_)
863 865 if user is not None:
864 866 return person_getter(user)
865 867 return id_
866 868
867 869
868 870 def gravatar_with_user(request, author, show_disabled=False):
869 871 _render = request.get_partial_renderer(
870 872 'rhodecode:templates/base/base.mako')
871 873 return _render('gravatar_with_user', author, show_disabled=show_disabled)
872 874
873 875
874 876 tags_paterns = OrderedDict((
875 877 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
876 878 '<div class="metatag" tag="lang">\\2</div>')),
877 879
878 880 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
879 881 '<div class="metatag" tag="see">see: \\1 </div>')),
880 882
881 883 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
882 884 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
883 885
884 886 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
885 887 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
886 888
887 889 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
888 890 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
889 891
890 892 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
891 893 '<div class="metatag" tag="state \\1">\\1</div>')),
892 894
893 895 # label in grey
894 896 ('label', (re.compile(r'\[([a-z]+)\]'),
895 897 '<div class="metatag" tag="label">\\1</div>')),
896 898
897 899 # generic catch all in grey
898 900 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
899 901 '<div class="metatag" tag="generic">\\1</div>')),
900 902 ))
901 903
902 904
903 905 def extract_metatags(value):
904 906 """
905 907 Extract supported meta-tags from given text value
906 908 """
907 909 tags = []
908 910 if not value:
909 911 return tags, ''
910 912
911 913 for key, val in tags_paterns.items():
912 914 pat, replace_html = val
913 915 tags.extend([(key, x.group()) for x in pat.finditer(value)])
914 916 value = pat.sub('', value)
915 917
916 918 return tags, value
917 919
918 920
919 921 def style_metatag(tag_type, value):
920 922 """
921 923 converts tags from value into html equivalent
922 924 """
923 925 if not value:
924 926 return ''
925 927
926 928 html_value = value
927 929 tag_data = tags_paterns.get(tag_type)
928 930 if tag_data:
929 931 pat, replace_html = tag_data
930 932 # convert to plain `unicode` instead of a markup tag to be used in
931 933 # regex expressions. safe_unicode doesn't work here
932 934 html_value = pat.sub(replace_html, unicode(value))
933 935
934 936 return html_value
935 937
936 938
937 939 def bool2icon(value, show_at_false=True):
938 940 """
939 941 Returns boolean value of a given value, represented as html element with
940 942 classes that will represent icons
941 943
942 944 :param value: given value to convert to html node
943 945 """
944 946
945 947 if value: # does bool conversion
946 948 return HTML.tag('i', class_="icon-true")
947 949 else: # not true as bool
948 950 if show_at_false:
949 951 return HTML.tag('i', class_="icon-false")
950 952 return HTML.tag('i')
951 953
952 954 #==============================================================================
953 955 # PERMS
954 956 #==============================================================================
955 957 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
956 958 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
957 959 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
958 960 csrf_token_key
959 961
960 962
961 963 #==============================================================================
962 964 # GRAVATAR URL
963 965 #==============================================================================
964 966 class InitialsGravatar(object):
965 967 def __init__(self, email_address, first_name, last_name, size=30,
966 968 background=None, text_color='#fff'):
967 969 self.size = size
968 970 self.first_name = first_name
969 971 self.last_name = last_name
970 972 self.email_address = email_address
971 973 self.background = background or self.str2color(email_address)
972 974 self.text_color = text_color
973 975
974 976 def get_color_bank(self):
975 977 """
976 978 returns a predefined list of colors that gravatars can use.
977 979 Those are randomized distinct colors that guarantee readability and
978 980 uniqueness.
979 981
980 982 generated with: http://phrogz.net/css/distinct-colors.html
981 983 """
982 984 return [
983 985 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
984 986 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
985 987 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
986 988 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
987 989 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
988 990 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
989 991 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
990 992 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
991 993 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
992 994 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
993 995 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
994 996 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
995 997 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
996 998 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
997 999 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
998 1000 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
999 1001 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1000 1002 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1001 1003 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1002 1004 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1003 1005 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1004 1006 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1005 1007 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1006 1008 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1007 1009 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1008 1010 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1009 1011 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1010 1012 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1011 1013 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1012 1014 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1013 1015 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1014 1016 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1015 1017 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1016 1018 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1017 1019 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1018 1020 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1019 1021 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1020 1022 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1021 1023 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1022 1024 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1023 1025 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1024 1026 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1025 1027 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1026 1028 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1027 1029 '#4f8c46', '#368dd9', '#5c0073'
1028 1030 ]
1029 1031
1030 1032 def rgb_to_hex_color(self, rgb_tuple):
1031 1033 """
1032 1034 Converts an rgb_tuple passed to an hex color.
1033 1035
1034 1036 :param rgb_tuple: tuple with 3 ints represents rgb color space
1035 1037 """
1036 1038 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1037 1039
1038 1040 def email_to_int_list(self, email_str):
1039 1041 """
1040 1042 Get every byte of the hex digest value of email and turn it to integer.
1041 1043 It's going to be always between 0-255
1042 1044 """
1043 1045 digest = md5_safe(email_str.lower())
1044 1046 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1045 1047
1046 1048 def pick_color_bank_index(self, email_str, color_bank):
1047 1049 return self.email_to_int_list(email_str)[0] % len(color_bank)
1048 1050
1049 1051 def str2color(self, email_str):
1050 1052 """
1051 1053 Tries to map in a stable algorithm an email to color
1052 1054
1053 1055 :param email_str:
1054 1056 """
1055 1057 color_bank = self.get_color_bank()
1056 1058 # pick position (module it's length so we always find it in the
1057 1059 # bank even if it's smaller than 256 values
1058 1060 pos = self.pick_color_bank_index(email_str, color_bank)
1059 1061 return color_bank[pos]
1060 1062
1061 1063 def normalize_email(self, email_address):
1062 1064 import unicodedata
1063 1065 # default host used to fill in the fake/missing email
1064 1066 default_host = u'localhost'
1065 1067
1066 1068 if not email_address:
1067 1069 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1068 1070
1069 1071 email_address = safe_unicode(email_address)
1070 1072
1071 1073 if u'@' not in email_address:
1072 1074 email_address = u'%s@%s' % (email_address, default_host)
1073 1075
1074 1076 if email_address.endswith(u'@'):
1075 1077 email_address = u'%s%s' % (email_address, default_host)
1076 1078
1077 1079 email_address = unicodedata.normalize('NFKD', email_address)\
1078 1080 .encode('ascii', 'ignore')
1079 1081 return email_address
1080 1082
1081 1083 def get_initials(self):
1082 1084 """
1083 1085 Returns 2 letter initials calculated based on the input.
1084 1086 The algorithm picks first given email address, and takes first letter
1085 1087 of part before @, and then the first letter of server name. In case
1086 1088 the part before @ is in a format of `somestring.somestring2` it replaces
1087 1089 the server letter with first letter of somestring2
1088 1090
1089 1091 In case function was initialized with both first and lastname, this
1090 1092 overrides the extraction from email by first letter of the first and
1091 1093 last name. We add special logic to that functionality, In case Full name
1092 1094 is compound, like Guido Von Rossum, we use last part of the last name
1093 1095 (Von Rossum) picking `R`.
1094 1096
1095 1097 Function also normalizes the non-ascii characters to they ascii
1096 1098 representation, eg Δ„ => A
1097 1099 """
1098 1100 import unicodedata
1099 1101 # replace non-ascii to ascii
1100 1102 first_name = unicodedata.normalize(
1101 1103 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1102 1104 last_name = unicodedata.normalize(
1103 1105 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1104 1106
1105 1107 # do NFKD encoding, and also make sure email has proper format
1106 1108 email_address = self.normalize_email(self.email_address)
1107 1109
1108 1110 # first push the email initials
1109 1111 prefix, server = email_address.split('@', 1)
1110 1112
1111 1113 # check if prefix is maybe a 'first_name.last_name' syntax
1112 1114 _dot_split = prefix.rsplit('.', 1)
1113 1115 if len(_dot_split) == 2 and _dot_split[1]:
1114 1116 initials = [_dot_split[0][0], _dot_split[1][0]]
1115 1117 else:
1116 1118 initials = [prefix[0], server[0]]
1117 1119
1118 1120 # then try to replace either first_name or last_name
1119 1121 fn_letter = (first_name or " ")[0].strip()
1120 1122 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1121 1123
1122 1124 if fn_letter:
1123 1125 initials[0] = fn_letter
1124 1126
1125 1127 if ln_letter:
1126 1128 initials[1] = ln_letter
1127 1129
1128 1130 return ''.join(initials).upper()
1129 1131
1130 1132 def get_img_data_by_type(self, font_family, img_type):
1131 1133 default_user = """
1132 1134 <svg xmlns="http://www.w3.org/2000/svg"
1133 1135 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1134 1136 viewBox="-15 -10 439.165 429.164"
1135 1137
1136 1138 xml:space="preserve"
1137 1139 style="background:{background};" >
1138 1140
1139 1141 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1140 1142 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1141 1143 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1142 1144 168.596,153.916,216.671,
1143 1145 204.583,216.671z" fill="{text_color}"/>
1144 1146 <path d="M407.164,374.717L360.88,
1145 1147 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1146 1148 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1147 1149 15.366-44.203,23.488-69.076,23.488c-24.877,
1148 1150 0-48.762-8.122-69.078-23.488
1149 1151 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1150 1152 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1151 1153 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1152 1154 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1153 1155 19.402-10.527 C409.699,390.129,
1154 1156 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1155 1157 </svg>""".format(
1156 1158 size=self.size,
1157 1159 background='#979797', # @grey4
1158 1160 text_color=self.text_color,
1159 1161 font_family=font_family)
1160 1162
1161 1163 return {
1162 1164 "default_user": default_user
1163 1165 }[img_type]
1164 1166
1165 1167 def get_img_data(self, svg_type=None):
1166 1168 """
1167 1169 generates the svg metadata for image
1168 1170 """
1169 1171 fonts = [
1170 1172 '-apple-system',
1171 1173 'BlinkMacSystemFont',
1172 1174 'Segoe UI',
1173 1175 'Roboto',
1174 1176 'Oxygen-Sans',
1175 1177 'Ubuntu',
1176 1178 'Cantarell',
1177 1179 'Helvetica Neue',
1178 1180 'sans-serif'
1179 1181 ]
1180 1182 font_family = ','.join(fonts)
1181 1183 if svg_type:
1182 1184 return self.get_img_data_by_type(font_family, svg_type)
1183 1185
1184 1186 initials = self.get_initials()
1185 1187 img_data = """
1186 1188 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1187 1189 width="{size}" height="{size}"
1188 1190 style="width: 100%; height: 100%; background-color: {background}"
1189 1191 viewBox="0 0 {size} {size}">
1190 1192 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1191 1193 pointer-events="auto" fill="{text_color}"
1192 1194 font-family="{font_family}"
1193 1195 style="font-weight: 400; font-size: {f_size}px;">{text}
1194 1196 </text>
1195 1197 </svg>""".format(
1196 1198 size=self.size,
1197 1199 f_size=self.size/1.85, # scale the text inside the box nicely
1198 1200 background=self.background,
1199 1201 text_color=self.text_color,
1200 1202 text=initials.upper(),
1201 1203 font_family=font_family)
1202 1204
1203 1205 return img_data
1204 1206
1205 1207 def generate_svg(self, svg_type=None):
1206 1208 img_data = self.get_img_data(svg_type)
1207 1209 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1208 1210
1209 1211
1210 1212 def initials_gravatar(email_address, first_name, last_name, size=30):
1211 1213 svg_type = None
1212 1214 if email_address == User.DEFAULT_USER_EMAIL:
1213 1215 svg_type = 'default_user'
1214 1216 klass = InitialsGravatar(email_address, first_name, last_name, size)
1215 1217 return klass.generate_svg(svg_type=svg_type)
1216 1218
1217 1219
1218 1220 def gravatar_url(email_address, size=30, request=None):
1219 1221 request = get_current_request()
1220 1222 _use_gravatar = request.call_context.visual.use_gravatar
1221 1223 _gravatar_url = request.call_context.visual.gravatar_url
1222 1224
1223 1225 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1224 1226
1225 1227 email_address = email_address or User.DEFAULT_USER_EMAIL
1226 1228 if isinstance(email_address, unicode):
1227 1229 # hashlib crashes on unicode items
1228 1230 email_address = safe_str(email_address)
1229 1231
1230 1232 # empty email or default user
1231 1233 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1232 1234 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1233 1235
1234 1236 if _use_gravatar:
1235 1237 # TODO: Disuse pyramid thread locals. Think about another solution to
1236 1238 # get the host and schema here.
1237 1239 request = get_current_request()
1238 1240 tmpl = safe_str(_gravatar_url)
1239 1241 tmpl = tmpl.replace('{email}', email_address)\
1240 1242 .replace('{md5email}', md5_safe(email_address.lower())) \
1241 1243 .replace('{netloc}', request.host)\
1242 1244 .replace('{scheme}', request.scheme)\
1243 1245 .replace('{size}', safe_str(size))
1244 1246 return tmpl
1245 1247 else:
1246 1248 return initials_gravatar(email_address, '', '', size=size)
1247 1249
1248 1250
1249 1251 class Page(_Page):
1250 1252 """
1251 1253 Custom pager to match rendering style with paginator
1252 1254 """
1253 1255
1254 1256 def _get_pos(self, cur_page, max_page, items):
1255 1257 edge = (items / 2) + 1
1256 1258 if (cur_page <= edge):
1257 1259 radius = max(items / 2, items - cur_page)
1258 1260 elif (max_page - cur_page) < edge:
1259 1261 radius = (items - 1) - (max_page - cur_page)
1260 1262 else:
1261 1263 radius = items / 2
1262 1264
1263 1265 left = max(1, (cur_page - (radius)))
1264 1266 right = min(max_page, cur_page + (radius))
1265 1267 return left, cur_page, right
1266 1268
1267 1269 def _range(self, regexp_match):
1268 1270 """
1269 1271 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1270 1272
1271 1273 Arguments:
1272 1274
1273 1275 regexp_match
1274 1276 A "re" (regular expressions) match object containing the
1275 1277 radius of linked pages around the current page in
1276 1278 regexp_match.group(1) as a string
1277 1279
1278 1280 This function is supposed to be called as a callable in
1279 1281 re.sub.
1280 1282
1281 1283 """
1282 1284 radius = int(regexp_match.group(1))
1283 1285
1284 1286 # Compute the first and last page number within the radius
1285 1287 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1286 1288 # -> leftmost_page = 5
1287 1289 # -> rightmost_page = 9
1288 1290 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1289 1291 self.last_page,
1290 1292 (radius * 2) + 1)
1291 1293 nav_items = []
1292 1294
1293 1295 # Create a link to the first page (unless we are on the first page
1294 1296 # or there would be no need to insert '..' spacers)
1295 1297 if self.page != self.first_page and self.first_page < leftmost_page:
1296 1298 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1297 1299
1298 1300 # Insert dots if there are pages between the first page
1299 1301 # and the currently displayed page range
1300 1302 if leftmost_page - self.first_page > 1:
1301 1303 # Wrap in a SPAN tag if nolink_attr is set
1302 1304 text = '..'
1303 1305 if self.dotdot_attr:
1304 1306 text = HTML.span(c=text, **self.dotdot_attr)
1305 1307 nav_items.append(text)
1306 1308
1307 1309 for thispage in xrange(leftmost_page, rightmost_page + 1):
1308 1310 # Hilight the current page number and do not use a link
1309 1311 if thispage == self.page:
1310 1312 text = '%s' % (thispage,)
1311 1313 # Wrap in a SPAN tag if nolink_attr is set
1312 1314 if self.curpage_attr:
1313 1315 text = HTML.span(c=text, **self.curpage_attr)
1314 1316 nav_items.append(text)
1315 1317 # Otherwise create just a link to that page
1316 1318 else:
1317 1319 text = '%s' % (thispage,)
1318 1320 nav_items.append(self._pagerlink(thispage, text))
1319 1321
1320 1322 # Insert dots if there are pages between the displayed
1321 1323 # page numbers and the end of the page range
1322 1324 if self.last_page - rightmost_page > 1:
1323 1325 text = '..'
1324 1326 # Wrap in a SPAN tag if nolink_attr is set
1325 1327 if self.dotdot_attr:
1326 1328 text = HTML.span(c=text, **self.dotdot_attr)
1327 1329 nav_items.append(text)
1328 1330
1329 1331 # Create a link to the very last page (unless we are on the last
1330 1332 # page or there would be no need to insert '..' spacers)
1331 1333 if self.page != self.last_page and rightmost_page < self.last_page:
1332 1334 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1333 1335
1334 1336 ## prerender links
1335 1337 #_page_link = url.current()
1336 1338 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1337 1339 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1338 1340 return self.separator.join(nav_items)
1339 1341
1340 1342 def pager(self, format='~2~', page_param='page', partial_param='partial',
1341 1343 show_if_single_page=False, separator=' ', onclick=None,
1342 1344 symbol_first='<<', symbol_last='>>',
1343 1345 symbol_previous='<', symbol_next='>',
1344 1346 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1345 1347 curpage_attr={'class': 'pager_curpage'},
1346 1348 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1347 1349
1348 1350 self.curpage_attr = curpage_attr
1349 1351 self.separator = separator
1350 1352 self.pager_kwargs = kwargs
1351 1353 self.page_param = page_param
1352 1354 self.partial_param = partial_param
1353 1355 self.onclick = onclick
1354 1356 self.link_attr = link_attr
1355 1357 self.dotdot_attr = dotdot_attr
1356 1358
1357 1359 # Don't show navigator if there is no more than one page
1358 1360 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1359 1361 return ''
1360 1362
1361 1363 from string import Template
1362 1364 # Replace ~...~ in token format by range of pages
1363 1365 result = re.sub(r'~(\d+)~', self._range, format)
1364 1366
1365 1367 # Interpolate '%' variables
1366 1368 result = Template(result).safe_substitute({
1367 1369 'first_page': self.first_page,
1368 1370 'last_page': self.last_page,
1369 1371 'page': self.page,
1370 1372 'page_count': self.page_count,
1371 1373 'items_per_page': self.items_per_page,
1372 1374 'first_item': self.first_item,
1373 1375 'last_item': self.last_item,
1374 1376 'item_count': self.item_count,
1375 1377 'link_first': self.page > self.first_page and \
1376 1378 self._pagerlink(self.first_page, symbol_first) or '',
1377 1379 'link_last': self.page < self.last_page and \
1378 1380 self._pagerlink(self.last_page, symbol_last) or '',
1379 1381 'link_previous': self.previous_page and \
1380 1382 self._pagerlink(self.previous_page, symbol_previous) \
1381 1383 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1382 1384 'link_next': self.next_page and \
1383 1385 self._pagerlink(self.next_page, symbol_next) \
1384 1386 or HTML.span(symbol_next, class_="pg-next disabled")
1385 1387 })
1386 1388
1387 1389 return literal(result)
1388 1390
1389 1391
1390 1392 #==============================================================================
1391 1393 # REPO PAGER, PAGER FOR REPOSITORY
1392 1394 #==============================================================================
1393 1395 class RepoPage(Page):
1394 1396
1395 1397 def __init__(self, collection, page=1, items_per_page=20,
1396 1398 item_count=None, url=None, **kwargs):
1397 1399
1398 1400 """Create a "RepoPage" instance. special pager for paging
1399 1401 repository
1400 1402 """
1401 1403 self._url_generator = url
1402 1404
1403 1405 # Safe the kwargs class-wide so they can be used in the pager() method
1404 1406 self.kwargs = kwargs
1405 1407
1406 1408 # Save a reference to the collection
1407 1409 self.original_collection = collection
1408 1410
1409 1411 self.collection = collection
1410 1412
1411 1413 # The self.page is the number of the current page.
1412 1414 # The first page has the number 1!
1413 1415 try:
1414 1416 self.page = int(page) # make it int() if we get it as a string
1415 1417 except (ValueError, TypeError):
1416 1418 self.page = 1
1417 1419
1418 1420 self.items_per_page = items_per_page
1419 1421
1420 1422 # Unless the user tells us how many items the collections has
1421 1423 # we calculate that ourselves.
1422 1424 if item_count is not None:
1423 1425 self.item_count = item_count
1424 1426 else:
1425 1427 self.item_count = len(self.collection)
1426 1428
1427 1429 # Compute the number of the first and last available page
1428 1430 if self.item_count > 0:
1429 1431 self.first_page = 1
1430 1432 self.page_count = int(math.ceil(float(self.item_count) /
1431 1433 self.items_per_page))
1432 1434 self.last_page = self.first_page + self.page_count - 1
1433 1435
1434 1436 # Make sure that the requested page number is the range of
1435 1437 # valid pages
1436 1438 if self.page > self.last_page:
1437 1439 self.page = self.last_page
1438 1440 elif self.page < self.first_page:
1439 1441 self.page = self.first_page
1440 1442
1441 1443 # Note: the number of items on this page can be less than
1442 1444 # items_per_page if the last page is not full
1443 1445 self.first_item = max(0, (self.item_count) - (self.page *
1444 1446 items_per_page))
1445 1447 self.last_item = ((self.item_count - 1) - items_per_page *
1446 1448 (self.page - 1))
1447 1449
1448 1450 self.items = list(self.collection[self.first_item:self.last_item + 1])
1449 1451
1450 1452 # Links to previous and next page
1451 1453 if self.page > self.first_page:
1452 1454 self.previous_page = self.page - 1
1453 1455 else:
1454 1456 self.previous_page = None
1455 1457
1456 1458 if self.page < self.last_page:
1457 1459 self.next_page = self.page + 1
1458 1460 else:
1459 1461 self.next_page = None
1460 1462
1461 1463 # No items available
1462 1464 else:
1463 1465 self.first_page = None
1464 1466 self.page_count = 0
1465 1467 self.last_page = None
1466 1468 self.first_item = None
1467 1469 self.last_item = None
1468 1470 self.previous_page = None
1469 1471 self.next_page = None
1470 1472 self.items = []
1471 1473
1472 1474 # This is a subclass of the 'list' type. Initialise the list now.
1473 1475 list.__init__(self, reversed(self.items))
1474 1476
1475 1477
1476 1478 def breadcrumb_repo_link(repo):
1477 1479 """
1478 1480 Makes a breadcrumbs path link to repo
1479 1481
1480 1482 ex::
1481 1483 group >> subgroup >> repo
1482 1484
1483 1485 :param repo: a Repository instance
1484 1486 """
1485 1487
1486 1488 path = [
1487 1489 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1488 1490 for group in repo.groups_with_parents
1489 1491 ] + [
1490 1492 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1491 1493 ]
1492 1494
1493 1495 return literal(' &raquo; '.join(path))
1494 1496
1495 1497
1496 1498 def format_byte_size_binary(file_size):
1497 1499 """
1498 1500 Formats file/folder sizes to standard.
1499 1501 """
1500 1502 if file_size is None:
1501 1503 file_size = 0
1502 1504
1503 1505 formatted_size = format_byte_size(file_size, binary=True)
1504 1506 return formatted_size
1505 1507
1506 1508
1507 1509 def urlify_text(text_, safe=True):
1508 1510 """
1509 1511 Extrac urls from text and make html links out of them
1510 1512
1511 1513 :param text_:
1512 1514 """
1513 1515
1514 1516 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1515 1517 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1516 1518
1517 1519 def url_func(match_obj):
1518 1520 url_full = match_obj.groups()[0]
1519 1521 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1520 1522 _newtext = url_pat.sub(url_func, text_)
1521 1523 if safe:
1522 1524 return literal(_newtext)
1523 1525 return _newtext
1524 1526
1525 1527
1526 1528 def urlify_commits(text_, repository):
1527 1529 """
1528 1530 Extract commit ids from text and make link from them
1529 1531
1530 1532 :param text_:
1531 1533 :param repository: repo name to build the URL with
1532 1534 """
1533 1535
1534 1536 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1535 1537
1536 1538 def url_func(match_obj):
1537 1539 commit_id = match_obj.groups()[1]
1538 1540 pref = match_obj.groups()[0]
1539 1541 suf = match_obj.groups()[2]
1540 1542
1541 1543 tmpl = (
1542 1544 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1543 1545 '%(commit_id)s</a>%(suf)s'
1544 1546 )
1545 1547 return tmpl % {
1546 1548 'pref': pref,
1547 1549 'cls': 'revision-link',
1548 1550 'url': route_url('repo_commit', repo_name=repository,
1549 1551 commit_id=commit_id),
1550 1552 'commit_id': commit_id,
1551 1553 'suf': suf
1552 1554 }
1553 1555
1554 1556 newtext = URL_PAT.sub(url_func, text_)
1555 1557
1556 1558 return newtext
1557 1559
1558 1560
1559 1561 def _process_url_func(match_obj, repo_name, uid, entry,
1560 1562 return_raw_data=False, link_format='html'):
1561 1563 pref = ''
1562 1564 if match_obj.group().startswith(' '):
1563 1565 pref = ' '
1564 1566
1565 1567 issue_id = ''.join(match_obj.groups())
1566 1568
1567 1569 if link_format == 'html':
1568 1570 tmpl = (
1569 1571 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1570 1572 '%(issue-prefix)s%(id-repr)s'
1571 1573 '</a>')
1572 1574 elif link_format == 'rst':
1573 1575 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1574 1576 elif link_format == 'markdown':
1575 1577 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1576 1578 else:
1577 1579 raise ValueError('Bad link_format:{}'.format(link_format))
1578 1580
1579 1581 (repo_name_cleaned,
1580 1582 parent_group_name) = RepoGroupModel().\
1581 1583 _get_group_name_and_parent(repo_name)
1582 1584
1583 1585 # variables replacement
1584 1586 named_vars = {
1585 1587 'id': issue_id,
1586 1588 'repo': repo_name,
1587 1589 'repo_name': repo_name_cleaned,
1588 1590 'group_name': parent_group_name
1589 1591 }
1590 1592 # named regex variables
1591 1593 named_vars.update(match_obj.groupdict())
1592 1594 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1593 1595
1594 1596 data = {
1595 1597 'pref': pref,
1596 1598 'cls': 'issue-tracker-link',
1597 1599 'url': _url,
1598 1600 'id-repr': issue_id,
1599 1601 'issue-prefix': entry['pref'],
1600 1602 'serv': entry['url'],
1601 1603 }
1602 1604 if return_raw_data:
1603 1605 return {
1604 1606 'id': issue_id,
1605 1607 'url': _url
1606 1608 }
1607 1609 return tmpl % data
1608 1610
1609 1611
1610 1612 def get_active_pattern_entries(repo_name):
1611 1613 repo = None
1612 1614 if repo_name:
1613 1615 # Retrieving repo_name to avoid invalid repo_name to explode on
1614 1616 # IssueTrackerSettingsModel but still passing invalid name further down
1615 1617 repo = Repository.get_by_repo_name(repo_name, cache=True)
1616 1618
1617 1619 settings_model = IssueTrackerSettingsModel(repo=repo)
1618 1620 active_entries = settings_model.get_settings(cache=True)
1619 1621 return active_entries
1620 1622
1621 1623
1622 1624 def process_patterns(text_string, repo_name, link_format='html',
1623 1625 active_entries=None):
1624 1626
1625 1627 allowed_formats = ['html', 'rst', 'markdown']
1626 1628 if link_format not in allowed_formats:
1627 1629 raise ValueError('Link format can be only one of:{} got {}'.format(
1628 1630 allowed_formats, link_format))
1629 1631
1630 1632 active_entries = active_entries or get_active_pattern_entries(repo_name)
1631 1633 issues_data = []
1632 1634 newtext = text_string
1633 1635
1634 1636 for uid, entry in active_entries.items():
1635 1637 log.debug('found issue tracker entry with uid %s', uid)
1636 1638
1637 1639 if not (entry['pat'] and entry['url']):
1638 1640 log.debug('skipping due to missing data')
1639 1641 continue
1640 1642
1641 1643 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1642 1644 uid, entry['pat'], entry['url'], entry['pref'])
1643 1645
1644 1646 try:
1645 1647 pattern = re.compile(r'%s' % entry['pat'])
1646 1648 except re.error:
1647 1649 log.exception(
1648 1650 'issue tracker pattern: `%s` failed to compile',
1649 1651 entry['pat'])
1650 1652 continue
1651 1653
1652 1654 data_func = partial(
1653 1655 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1654 1656 return_raw_data=True)
1655 1657
1656 1658 for match_obj in pattern.finditer(text_string):
1657 1659 issues_data.append(data_func(match_obj))
1658 1660
1659 1661 url_func = partial(
1660 1662 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1661 1663 link_format=link_format)
1662 1664
1663 1665 newtext = pattern.sub(url_func, newtext)
1664 1666 log.debug('processed prefix:uid `%s`', uid)
1665 1667
1666 1668 return newtext, issues_data
1667 1669
1668 1670
1669 1671 def urlify_commit_message(commit_text, repository=None,
1670 1672 active_pattern_entries=None):
1671 1673 """
1672 1674 Parses given text message and makes proper links.
1673 1675 issues are linked to given issue-server, and rest is a commit link
1674 1676
1675 1677 :param commit_text:
1676 1678 :param repository:
1677 1679 """
1678 1680 def escaper(string):
1679 1681 return string.replace('<', '&lt;').replace('>', '&gt;')
1680 1682
1681 1683 newtext = escaper(commit_text)
1682 1684
1683 1685 # extract http/https links and make them real urls
1684 1686 newtext = urlify_text(newtext, safe=False)
1685 1687
1686 1688 # urlify commits - extract commit ids and make link out of them, if we have
1687 1689 # the scope of repository present.
1688 1690 if repository:
1689 1691 newtext = urlify_commits(newtext, repository)
1690 1692
1691 1693 # process issue tracker patterns
1692 1694 newtext, issues = process_patterns(newtext, repository or '',
1693 1695 active_entries=active_pattern_entries)
1694 1696
1695 1697 return literal(newtext)
1696 1698
1697 1699
1698 1700 def render_binary(repo_name, file_obj):
1699 1701 """
1700 1702 Choose how to render a binary file
1701 1703 """
1702 1704
1703 1705 filename = file_obj.name
1704 1706
1705 1707 # images
1706 1708 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1707 1709 if fnmatch.fnmatch(filename, pat=ext):
1708 1710 alt = escape(filename)
1709 1711 src = route_path(
1710 1712 'repo_file_raw', repo_name=repo_name,
1711 1713 commit_id=file_obj.commit.raw_id,
1712 1714 f_path=file_obj.path)
1713 1715 return literal(
1714 1716 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1715 1717
1716 1718
1717 1719 def renderer_from_filename(filename, exclude=None):
1718 1720 """
1719 1721 choose a renderer based on filename, this works only for text based files
1720 1722 """
1721 1723
1722 1724 # ipython
1723 1725 for ext in ['*.ipynb']:
1724 1726 if fnmatch.fnmatch(filename, pat=ext):
1725 1727 return 'jupyter'
1726 1728
1727 1729 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1728 1730 if is_markup:
1729 1731 return is_markup
1730 1732 return None
1731 1733
1732 1734
1733 1735 def render(source, renderer='rst', mentions=False, relative_urls=None,
1734 1736 repo_name=None):
1735 1737
1736 1738 def maybe_convert_relative_links(html_source):
1737 1739 if relative_urls:
1738 1740 return relative_links(html_source, relative_urls)
1739 1741 return html_source
1740 1742
1741 1743 if renderer == 'plain':
1742 1744 return literal(
1743 1745 MarkupRenderer.plain(source, leading_newline=False))
1744 1746
1745 1747 elif renderer == 'rst':
1746 1748 if repo_name:
1747 1749 # process patterns on comments if we pass in repo name
1748 1750 source, issues = process_patterns(
1749 1751 source, repo_name, link_format='rst')
1750 1752
1751 1753 return literal(
1752 1754 '<div class="rst-block">%s</div>' %
1753 1755 maybe_convert_relative_links(
1754 1756 MarkupRenderer.rst(source, mentions=mentions)))
1755 1757
1756 1758 elif renderer == 'markdown':
1757 1759 if repo_name:
1758 1760 # process patterns on comments if we pass in repo name
1759 1761 source, issues = process_patterns(
1760 1762 source, repo_name, link_format='markdown')
1761 1763
1762 1764 return literal(
1763 1765 '<div class="markdown-block">%s</div>' %
1764 1766 maybe_convert_relative_links(
1765 1767 MarkupRenderer.markdown(source, flavored=True,
1766 1768 mentions=mentions)))
1767 1769
1768 1770 elif renderer == 'jupyter':
1769 1771 return literal(
1770 1772 '<div class="ipynb">%s</div>' %
1771 1773 maybe_convert_relative_links(
1772 1774 MarkupRenderer.jupyter(source)))
1773 1775
1774 1776 # None means just show the file-source
1775 1777 return None
1776 1778
1777 1779
1778 1780 def commit_status(repo, commit_id):
1779 1781 return ChangesetStatusModel().get_status(repo, commit_id)
1780 1782
1781 1783
1782 1784 def commit_status_lbl(commit_status):
1783 1785 return dict(ChangesetStatus.STATUSES).get(commit_status)
1784 1786
1785 1787
1786 1788 def commit_time(repo_name, commit_id):
1787 1789 repo = Repository.get_by_repo_name(repo_name)
1788 1790 commit = repo.get_commit(commit_id=commit_id)
1789 1791 return commit.date
1790 1792
1791 1793
1792 1794 def get_permission_name(key):
1793 1795 return dict(Permission.PERMS).get(key)
1794 1796
1795 1797
1796 1798 def journal_filter_help(request):
1797 1799 _ = request.translate
1798 1800 from rhodecode.lib.audit_logger import ACTIONS
1799 1801 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1800 1802
1801 1803 return _(
1802 1804 'Example filter terms:\n' +
1803 1805 ' repository:vcs\n' +
1804 1806 ' username:marcin\n' +
1805 1807 ' username:(NOT marcin)\n' +
1806 1808 ' action:*push*\n' +
1807 1809 ' ip:127.0.0.1\n' +
1808 1810 ' date:20120101\n' +
1809 1811 ' date:[20120101100000 TO 20120102]\n' +
1810 1812 '\n' +
1811 1813 'Actions: {actions}\n' +
1812 1814 '\n' +
1813 1815 'Generate wildcards using \'*\' character:\n' +
1814 1816 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1815 1817 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1816 1818 '\n' +
1817 1819 'Optional AND / OR operators in queries\n' +
1818 1820 ' "repository:vcs OR repository:test"\n' +
1819 1821 ' "username:test AND repository:test*"\n'
1820 1822 ).format(actions=actions)
1821 1823
1822 1824
1823 1825 def not_mapped_error(repo_name):
1824 1826 from rhodecode.translation import _
1825 1827 flash(_('%s repository is not mapped to db perhaps'
1826 1828 ' it was created or renamed from the filesystem'
1827 1829 ' please run the application again'
1828 1830 ' in order to rescan repositories') % repo_name, category='error')
1829 1831
1830 1832
1831 1833 def ip_range(ip_addr):
1832 1834 from rhodecode.model.db import UserIpMap
1833 1835 s, e = UserIpMap._get_ip_range(ip_addr)
1834 1836 return '%s - %s' % (s, e)
1835 1837
1836 1838
1837 1839 def form(url, method='post', needs_csrf_token=True, **attrs):
1838 1840 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1839 1841 if method.lower() != 'get' and needs_csrf_token:
1840 1842 raise Exception(
1841 1843 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1842 1844 'CSRF token. If the endpoint does not require such token you can ' +
1843 1845 'explicitly set the parameter needs_csrf_token to false.')
1844 1846
1845 1847 return wh_form(url, method=method, **attrs)
1846 1848
1847 1849
1848 1850 def secure_form(form_url, method="POST", multipart=False, **attrs):
1849 1851 """Start a form tag that points the action to an url. This
1850 1852 form tag will also include the hidden field containing
1851 1853 the auth token.
1852 1854
1853 1855 The url options should be given either as a string, or as a
1854 1856 ``url()`` function. The method for the form defaults to POST.
1855 1857
1856 1858 Options:
1857 1859
1858 1860 ``multipart``
1859 1861 If set to True, the enctype is set to "multipart/form-data".
1860 1862 ``method``
1861 1863 The method to use when submitting the form, usually either
1862 1864 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1863 1865 hidden input with name _method is added to simulate the verb
1864 1866 over POST.
1865 1867
1866 1868 """
1867 1869 from webhelpers.pylonslib.secure_form import insecure_form
1868 1870
1869 1871 if 'request' in attrs:
1870 1872 session = attrs['request'].session
1871 1873 del attrs['request']
1872 1874 else:
1873 1875 raise ValueError(
1874 1876 'Calling this form requires request= to be passed as argument')
1875 1877
1876 1878 form = insecure_form(form_url, method, multipart, **attrs)
1877 1879 token = literal(
1878 1880 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1879 1881 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1880 1882
1881 1883 return literal("%s\n%s" % (form, token))
1882 1884
1883 1885
1884 1886 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1885 1887 select_html = select(name, selected, options, **attrs)
1886 1888 select2 = """
1887 1889 <script>
1888 1890 $(document).ready(function() {
1889 1891 $('#%s').select2({
1890 1892 containerCssClass: 'drop-menu',
1891 1893 dropdownCssClass: 'drop-menu-dropdown',
1892 1894 dropdownAutoWidth: true%s
1893 1895 });
1894 1896 });
1895 1897 </script>
1896 1898 """
1897 1899 filter_option = """,
1898 1900 minimumResultsForSearch: -1
1899 1901 """
1900 1902 input_id = attrs.get('id') or name
1901 1903 filter_enabled = "" if enable_filter else filter_option
1902 1904 select_script = literal(select2 % (input_id, filter_enabled))
1903 1905
1904 1906 return literal(select_html+select_script)
1905 1907
1906 1908
1907 1909 def get_visual_attr(tmpl_context_var, attr_name):
1908 1910 """
1909 1911 A safe way to get a variable from visual variable of template context
1910 1912
1911 1913 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1912 1914 :param attr_name: name of the attribute we fetch from the c.visual
1913 1915 """
1914 1916 visual = getattr(tmpl_context_var, 'visual', None)
1915 1917 if not visual:
1916 1918 return
1917 1919 else:
1918 1920 return getattr(visual, attr_name, None)
1919 1921
1920 1922
1921 1923 def get_last_path_part(file_node):
1922 1924 if not file_node.path:
1923 1925 return u''
1924 1926
1925 1927 path = safe_unicode(file_node.path.split('/')[-1])
1926 1928 return u'../' + path
1927 1929
1928 1930
1929 1931 def route_url(*args, **kwargs):
1930 1932 """
1931 1933 Wrapper around pyramids `route_url` (fully qualified url) function.
1932 1934 """
1933 1935 req = get_current_request()
1934 1936 return req.route_url(*args, **kwargs)
1935 1937
1936 1938
1937 1939 def route_path(*args, **kwargs):
1938 1940 """
1939 1941 Wrapper around pyramids `route_path` function.
1940 1942 """
1941 1943 req = get_current_request()
1942 1944 return req.route_path(*args, **kwargs)
1943 1945
1944 1946
1945 1947 def route_path_or_none(*args, **kwargs):
1946 1948 try:
1947 1949 return route_path(*args, **kwargs)
1948 1950 except KeyError:
1949 1951 return None
1950 1952
1951 1953
1952 1954 def current_route_path(request, **kw):
1953 1955 new_args = request.GET.mixed()
1954 1956 new_args.update(kw)
1955 1957 return request.current_route_path(_query=new_args)
1956 1958
1957 1959
1958 1960 def api_call_example(method, args):
1959 1961 """
1960 1962 Generates an API call example via CURL
1961 1963 """
1962 1964 args_json = json.dumps(OrderedDict([
1963 1965 ('id', 1),
1964 1966 ('auth_token', 'SECRET'),
1965 1967 ('method', method),
1966 1968 ('args', args)
1967 1969 ]))
1968 1970 return literal(
1969 1971 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
1970 1972 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1971 1973 "and needs to be of `api calls` role."
1972 1974 .format(
1973 1975 api_url=route_url('apiv2'),
1974 1976 token_url=route_url('my_account_auth_tokens'),
1975 1977 data=args_json))
1976 1978
1977 1979
1978 1980 def notification_description(notification, request):
1979 1981 """
1980 1982 Generate notification human readable description based on notification type
1981 1983 """
1982 1984 from rhodecode.model.notification import NotificationModel
1983 1985 return NotificationModel().make_description(
1984 1986 notification, translate=request.translate)
1985 1987
1986 1988
1987 1989 def go_import_header(request, db_repo=None):
1988 1990 """
1989 1991 Creates a header for go-import functionality in Go Lang
1990 1992 """
1991 1993
1992 1994 if not db_repo:
1993 1995 return
1994 1996 if 'go-get' not in request.GET:
1995 1997 return
1996 1998
1997 1999 clone_url = db_repo.clone_url()
1998 2000 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1999 2001 # we have a repo and go-get flag,
2000 2002 return literal('<meta name="go-import" content="{} {} {}">'.format(
2001 2003 prefix, db_repo.repo_type, clone_url))
2002 2004
2003 2005
2004 2006 def reviewer_as_json(*args, **kwargs):
2005 2007 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2006 2008 return _reviewer_as_json(*args, **kwargs)
2007 2009
2008 2010
2009 2011 def get_repo_view_type(request):
2010 2012 route_name = request.matched_route.name
2011 2013 route_to_view_type = {
2012 2014 'repo_changelog': 'changelog',
2013 2015 'repo_files': 'files',
2014 2016 'repo_summary': 'summary',
2015 2017 'repo_commit': 'commit'
2016 2018
2017 2019 }
2018 2020 return route_to_view_type.get(route_name)
General Comments 0
You need to be logged in to leave comments. Login now