##// END OF EJS Templates
more detailed logging on auth system...
marcink -
r2125:097327aa beta
parent child Browse files
Show More
@@ -1,25 +1,30 b''
1 1 .. _debugging:
2 2
3 3 ===================
4 DEBUGGING RHODECODE
4 Debugging RhodeCode
5 5 ===================
6 6
7 7 If you encountered problems with RhodeCode here are some instructions how to
8 8 possibly debug them.
9 9
10 10 ** First make sure you're using the latest version available.**
11 11
12 12 enable detailed debug
13 13 ---------------------
14 14
15 15 RhodeCode uses standard python logging modules to log it's output.
16 16 By default only loggers with INFO level are displayed. To enable full output
17 17 change `level = DEBUG` for all logging handlers in currently used .ini file.
18 After this you can check much more detailed output of actions happening on
19 RhodeCode system.
18 This change will allow to see much more detailed output in the logfile or
19 console. This generally helps a lot to track issues.
20 20
21 21
22 22 enable interactive debug mode
23 23 -----------------------------
24 24
25 To enable interactive debug mode simply
25 To enable interactive debug mode simply comment out `set debug = false` in
26 .ini file, this will trigger and interactive debugger each time there an
27 error in browser, or send a http link if error occured in the backend. This
28 is a great tool for fast debugging as you get a handy python console right
29 in the web view. ** NEVER ENABLE THIS ON PRODUCTION ** the interactive console
30 can be a serious security threat to you system.
@@ -1,814 +1,821 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 39 from rhodecode.model.meta import Session
40 40
41 41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 46 from rhodecode.lib.utils2 import str2bool, safe_unicode
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class PasswordGenerator(object):
59 59 """
60 60 This is a simple class for generating password from different sets of
61 61 characters
62 62 usage::
63 63
64 64 passwd_gen = PasswordGenerator()
65 65 #print 8-letter password containing only big and small letters
66 66 of alphabet
67 67 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
68 68 """
69 69 ALPHABETS_NUM = r'''1234567890'''
70 70 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
71 71 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
72 72 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
73 73 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
74 74 + ALPHABETS_NUM + ALPHABETS_SPECIAL
75 75 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
76 76 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
77 77 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
78 78 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
79 79
80 80 def __init__(self, passwd=''):
81 81 self.passwd = passwd
82 82
83 83 def gen_password(self, length, type_=None):
84 84 if type_ is None:
85 85 type_ = self.ALPHABETS_FULL
86 86 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
87 87 return self.passwd
88 88
89 89
90 90 class RhodeCodeCrypto(object):
91 91
92 92 @classmethod
93 93 def hash_string(cls, str_):
94 94 """
95 95 Cryptographic function used for password hashing based on pybcrypt
96 96 or pycrypto in windows
97 97
98 98 :param password: password to hash
99 99 """
100 100 if __platform__ in PLATFORM_WIN:
101 101 return sha256(str_).hexdigest()
102 102 elif __platform__ in PLATFORM_OTHERS:
103 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
104 104 else:
105 105 raise Exception('Unknown or unsupported platform %s' \
106 106 % __platform__)
107 107
108 108 @classmethod
109 109 def hash_check(cls, password, hashed):
110 110 """
111 111 Checks matching password with it's hashed value, runs different
112 112 implementation based on platform it runs on
113 113
114 114 :param password: password
115 115 :param hashed: password in hashed form
116 116 """
117 117
118 118 if __platform__ in PLATFORM_WIN:
119 119 return sha256(password).hexdigest() == hashed
120 120 elif __platform__ in PLATFORM_OTHERS:
121 121 return bcrypt.hashpw(password, hashed) == hashed
122 122 else:
123 123 raise Exception('Unknown or unsupported platform %s' \
124 124 % __platform__)
125 125
126 126
127 127 def get_crypt_password(password):
128 128 return RhodeCodeCrypto.hash_string(password)
129 129
130 130
131 131 def check_password(password, hashed):
132 132 return RhodeCodeCrypto.hash_check(password, hashed)
133 133
134 134
135 135 def generate_api_key(str_, salt=None):
136 136 """
137 137 Generates API KEY from given string
138 138
139 139 :param str_:
140 140 :param salt:
141 141 """
142 142
143 143 if salt is None:
144 144 salt = _RandomNameSequence().next()
145 145
146 146 return hashlib.sha1(str_ + salt).hexdigest()
147 147
148 148
149 149 def authfunc(environ, username, password):
150 150 """
151 151 Dummy authentication wrapper function used in Mercurial and Git for
152 152 access control.
153 153
154 154 :param environ: needed only for using in Basic auth
155 155 """
156 156 return authenticate(username, password)
157 157
158 158
159 159 def authenticate(username, password):
160 160 """
161 161 Authentication function used for access control,
162 162 firstly checks for db authentication then if ldap is enabled for ldap
163 163 authentication, also creates ldap user if not in database
164 164
165 165 :param username: username
166 166 :param password: password
167 167 """
168 168
169 169 user_model = UserModel()
170 170 user = User.get_by_username(username)
171 171
172 172 log.debug('Authenticating user using RhodeCode account')
173 173 if user is not None and not user.ldap_dn:
174 174 if user.active:
175 175 if user.username == 'default' and user.active:
176 176 log.info('user %s authenticated correctly as anonymous user' %
177 177 username)
178 178 return True
179 179
180 180 elif user.username == username and check_password(password,
181 181 user.password):
182 182 log.info('user %s authenticated correctly' % username)
183 183 return True
184 184 else:
185 185 log.warning('user %s tried auth but is disabled' % username)
186 186
187 187 else:
188 188 log.debug('Regular authentication failed')
189 189 user_obj = User.get_by_username(username, case_insensitive=True)
190 190
191 191 if user_obj is not None and not user_obj.ldap_dn:
192 192 log.debug('this user already exists as non ldap')
193 193 return False
194 194
195 195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
196 196 #======================================================================
197 197 # FALLBACK TO LDAP AUTH IF ENABLE
198 198 #======================================================================
199 199 if str2bool(ldap_settings.get('ldap_active')):
200 200 log.debug("Authenticating user using ldap")
201 201 kwargs = {
202 202 'server': ldap_settings.get('ldap_host', ''),
203 203 'base_dn': ldap_settings.get('ldap_base_dn', ''),
204 204 'port': ldap_settings.get('ldap_port'),
205 205 'bind_dn': ldap_settings.get('ldap_dn_user'),
206 206 'bind_pass': ldap_settings.get('ldap_dn_pass'),
207 207 'tls_kind': ldap_settings.get('ldap_tls_kind'),
208 208 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
209 209 'ldap_filter': ldap_settings.get('ldap_filter'),
210 210 'search_scope': ldap_settings.get('ldap_search_scope'),
211 211 'attr_login': ldap_settings.get('ldap_attr_login'),
212 212 'ldap_version': 3,
213 213 }
214 214 log.debug('Checking for ldap authentication')
215 215 try:
216 216 aldap = AuthLdap(**kwargs)
217 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
218 218 password)
219 219 log.debug('Got ldap DN response %s' % user_dn)
220 220
221 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
222 222 .get(k), [''])[0]
223 223
224 224 user_attrs = {
225 225 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
226 226 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
227 227 'email': get_ldap_attr('ldap_attr_email'),
228 228 }
229 229
230 230 # don't store LDAP password since we don't need it. Override
231 231 # with some random generated password
232 232 _password = PasswordGenerator().gen_password(length=8)
233 233 # create this user on the fly if it doesn't exist in rhodecode
234 234 # database
235 235 if user_model.create_ldap(username, _password, user_dn,
236 236 user_attrs):
237 237 log.info('created new ldap user %s' % username)
238 238
239 239 Session.commit()
240 240 return True
241 241 except (LdapUsernameError, LdapPasswordError,):
242 242 pass
243 243 except (Exception,):
244 244 log.error(traceback.format_exc())
245 245 pass
246 246 return False
247 247
248 248
249 249 def login_container_auth(username):
250 250 user = User.get_by_username(username)
251 251 if user is None:
252 252 user_attrs = {
253 253 'name': username,
254 254 'lastname': None,
255 255 'email': None,
256 256 }
257 257 user = UserModel().create_for_container_auth(username, user_attrs)
258 258 if not user:
259 259 return None
260 260 log.info('User %s was created by container authentication' % username)
261 261
262 262 if not user.active:
263 263 return None
264 264
265 265 user.update_lastlogin()
266 266 Session.commit()
267 267
268 268 log.debug('User %s is now logged in by container authentication',
269 269 user.username)
270 270 return user
271 271
272 272
273 273 def get_container_username(environ, config):
274 274 username = None
275 275
276 276 if str2bool(config.get('container_auth_enabled', False)):
277 277 from paste.httpheaders import REMOTE_USER
278 278 username = REMOTE_USER(environ)
279 279
280 280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 281 username = environ.get('HTTP_X_FORWARDED_USER')
282 282
283 283 if username:
284 284 # Removing realm and domain from username
285 285 username = username.partition('@')[0]
286 286 username = username.rpartition('\\')[2]
287 287 log.debug('Received username %s from container' % username)
288 288
289 289 return username
290 290
291 291
292 292 class CookieStoreWrapper(object):
293 293
294 294 def __init__(self, cookie_store):
295 295 self.cookie_store = cookie_store
296 296
297 297 def __repr__(self):
298 298 return 'CookieStore<%s>' % (self.cookie_store)
299 299
300 300 def get(self, key, other=None):
301 301 if isinstance(self.cookie_store, dict):
302 302 return self.cookie_store.get(key, other)
303 303 elif isinstance(self.cookie_store, AuthUser):
304 304 return self.cookie_store.__dict__.get(key, other)
305 305
306 306
307 307 class AuthUser(object):
308 308 """
309 309 A simple object that handles all attributes of user in RhodeCode
310 310
311 311 It does lookup based on API key,given user, or user present in session
312 312 Then it fills all required information for such user. It also checks if
313 313 anonymous access is enabled and if so, it returns default user as logged
314 314 in
315 315 """
316 316
317 317 def __init__(self, user_id=None, api_key=None, username=None):
318 318
319 319 self.user_id = user_id
320 320 self.api_key = None
321 321 self.username = username
322 322
323 323 self.name = ''
324 324 self.lastname = ''
325 325 self.email = ''
326 326 self.is_authenticated = False
327 327 self.admin = False
328 328 self.permissions = {}
329 329 self._api_key = api_key
330 330 self.propagate_data()
331 331 self._instance = None
332 332
333 333 def propagate_data(self):
334 334 user_model = UserModel()
335 335 self.anonymous_user = User.get_by_username('default', cache=True)
336 336 is_user_loaded = False
337 337
338 338 # try go get user by api key
339 339 if self._api_key and self._api_key != self.anonymous_user.api_key:
340 340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 342 # lookup by userid
343 343 elif (self.user_id is not None and
344 344 self.user_id != self.anonymous_user.user_id):
345 345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 347 # lookup by username
348 348 elif self.username and \
349 349 str2bool(config.get('container_auth_enabled', False)):
350 350
351 351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 352 dbuser = login_container_auth(self.username)
353 353 if dbuser is not None:
354 354 for k, v in dbuser.get_dict().items():
355 355 setattr(self, k, v)
356 356 self.set_authenticated()
357 357 is_user_loaded = True
358 358 else:
359 359 log.debug('No data in %s that could been used to log in' % self)
360 360
361 361 if not is_user_loaded:
362 362 # if we cannot authenticate user try anonymous
363 363 if self.anonymous_user.active is True:
364 364 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
365 365 # then we set this user is logged in
366 366 self.is_authenticated = True
367 367 else:
368 368 self.user_id = None
369 369 self.username = None
370 370 self.is_authenticated = False
371 371
372 372 if not self.username:
373 373 self.username = 'None'
374 374
375 375 log.debug('Auth User is now %s' % self)
376 376 user_model.fill_perms(self)
377 377
378 378 @property
379 379 def is_admin(self):
380 380 return self.admin
381 381
382 382 def __repr__(self):
383 383 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
384 384 self.is_authenticated)
385 385
386 386 def set_authenticated(self, authenticated=True):
387 387 if self.user_id != self.anonymous_user.user_id:
388 388 self.is_authenticated = authenticated
389 389
390 390 def get_cookie_store(self):
391 391 return {'username': self.username,
392 392 'user_id': self.user_id,
393 393 'is_authenticated': self.is_authenticated}
394 394
395 395 @classmethod
396 396 def from_cookie_store(cls, cookie_store):
397 397 """
398 398 Creates AuthUser from a cookie store
399 399
400 400 :param cls:
401 401 :param cookie_store:
402 402 """
403 403 user_id = cookie_store.get('user_id')
404 404 username = cookie_store.get('username')
405 405 api_key = cookie_store.get('api_key')
406 406 return AuthUser(user_id, api_key, username)
407 407
408 408
409 409 def set_available_permissions(config):
410 410 """
411 411 This function will propagate pylons globals with all available defined
412 412 permission given in db. We don't want to check each time from db for new
413 413 permissions since adding a new permission also requires application restart
414 414 ie. to decorate new views with the newly created permission
415 415
416 416 :param config: current pylons config instance
417 417
418 418 """
419 419 log.info('getting information about all available permissions')
420 420 try:
421 421 sa = meta.Session
422 422 all_perms = sa.query(Permission).all()
423 423 except Exception:
424 424 pass
425 425 finally:
426 426 meta.Session.remove()
427 427
428 428 config['available_permissions'] = [x.permission_name for x in all_perms]
429 429
430 430
431 431 #==============================================================================
432 432 # CHECK DECORATORS
433 433 #==============================================================================
434 434 class LoginRequired(object):
435 435 """
436 436 Must be logged in to execute this function else
437 437 redirect to login page
438 438
439 439 :param api_access: if enabled this checks only for valid auth token
440 440 and grants access based on valid token
441 441 """
442 442
443 443 def __init__(self, api_access=False):
444 444 self.api_access = api_access
445 445
446 446 def __call__(self, func):
447 447 return decorator(self.__wrapper, func)
448 448
449 449 def __wrapper(self, func, *fargs, **fkwargs):
450 450 cls = fargs[0]
451 451 user = cls.rhodecode_user
452 452
453 453 api_access_ok = False
454 454 if self.api_access:
455 455 log.debug('Checking API KEY access for %s' % cls)
456 456 if user.api_key == request.GET.get('api_key'):
457 457 api_access_ok = True
458 458 else:
459 459 log.debug("API KEY token not valid")
460 460 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
461 461 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
462 462 if user.is_authenticated or api_access_ok:
463 463 log.info('user %s is authenticated and granted access to %s' % (
464 464 user.username, loc)
465 465 )
466 466 return func(*fargs, **fkwargs)
467 467 else:
468 468 log.warn('user %s NOT authenticated on func: %s' % (
469 469 user, loc)
470 470 )
471 471 p = url.current()
472 472
473 473 log.debug('redirecting to login page with %s' % p)
474 474 return redirect(url('login_home', came_from=p))
475 475
476 476
477 477 class NotAnonymous(object):
478 478 """
479 479 Must be logged in to execute this function else
480 480 redirect to login page"""
481 481
482 482 def __call__(self, func):
483 483 return decorator(self.__wrapper, func)
484 484
485 485 def __wrapper(self, func, *fargs, **fkwargs):
486 486 cls = fargs[0]
487 487 self.user = cls.rhodecode_user
488 488
489 489 log.debug('Checking if user is not anonymous @%s' % cls)
490 490
491 491 anonymous = self.user.username == 'default'
492 492
493 493 if anonymous:
494 494 p = url.current()
495 495
496 496 import rhodecode.lib.helpers as h
497 497 h.flash(_('You need to be a registered user to '
498 498 'perform this action'),
499 499 category='warning')
500 500 return redirect(url('login_home', came_from=p))
501 501 else:
502 502 return func(*fargs, **fkwargs)
503 503
504 504
505 505 class PermsDecorator(object):
506 506 """Base class for controller decorators"""
507 507
508 508 def __init__(self, *required_perms):
509 509 available_perms = config['available_permissions']
510 510 for perm in required_perms:
511 511 if perm not in available_perms:
512 512 raise Exception("'%s' permission is not defined" % perm)
513 513 self.required_perms = set(required_perms)
514 514 self.user_perms = None
515 515
516 516 def __call__(self, func):
517 517 return decorator(self.__wrapper, func)
518 518
519 519 def __wrapper(self, func, *fargs, **fkwargs):
520 520 cls = fargs[0]
521 521 self.user = cls.rhodecode_user
522 522 self.user_perms = self.user.permissions
523 523 log.debug('checking %s permissions %s for %s %s',
524 self.__class__.__name__, self.required_perms, cls,
525 self.user)
524 self.__class__.__name__, self.required_perms, cls, self.user)
526 525
527 526 if self.check_permissions():
528 527 log.debug('Permission granted for %s %s' % (cls, self.user))
529 528 return func(*fargs, **fkwargs)
530 529
531 530 else:
532 531 log.debug('Permission denied for %s %s' % (cls, self.user))
533 532 anonymous = self.user.username == 'default'
534 533
535 534 if anonymous:
536 535 p = url.current()
537 536
538 537 import rhodecode.lib.helpers as h
539 538 h.flash(_('You need to be a signed in to '
540 539 'view this page'),
541 540 category='warning')
542 541 return redirect(url('login_home', came_from=p))
543 542
544 543 else:
545 544 # redirect with forbidden ret code
546 545 return abort(403)
547 546
548 547 def check_permissions(self):
549 548 """Dummy function for overriding"""
550 549 raise Exception('You have to write this function in child class')
551 550
552 551
553 552 class HasPermissionAllDecorator(PermsDecorator):
554 553 """
555 554 Checks for access permission for all given predicates. All of them
556 555 have to be meet in order to fulfill the request
557 556 """
558 557
559 558 def check_permissions(self):
560 559 if self.required_perms.issubset(self.user_perms.get('global')):
561 560 return True
562 561 return False
563 562
564 563
565 564 class HasPermissionAnyDecorator(PermsDecorator):
566 565 """
567 566 Checks for access permission for any of given predicates. In order to
568 567 fulfill the request any of predicates must be meet
569 568 """
570 569
571 570 def check_permissions(self):
572 571 if self.required_perms.intersection(self.user_perms.get('global')):
573 572 return True
574 573 return False
575 574
576 575
577 576 class HasRepoPermissionAllDecorator(PermsDecorator):
578 577 """
579 578 Checks for access permission for all given predicates for specific
580 579 repository. All of them have to be meet in order to fulfill the request
581 580 """
582 581
583 582 def check_permissions(self):
584 583 repo_name = get_repo_slug(request)
585 584 try:
586 585 user_perms = set([self.user_perms['repositories'][repo_name]])
587 586 except KeyError:
588 587 return False
589 588 if self.required_perms.issubset(user_perms):
590 589 return True
591 590 return False
592 591
593 592
594 593 class HasRepoPermissionAnyDecorator(PermsDecorator):
595 594 """
596 595 Checks for access permission for any of given predicates for specific
597 596 repository. In order to fulfill the request any of predicates must be meet
598 597 """
599 598
600 599 def check_permissions(self):
601 600 repo_name = get_repo_slug(request)
602 601
603 602 try:
604 603 user_perms = set([self.user_perms['repositories'][repo_name]])
605 604 except KeyError:
606 605 return False
606
607 607 if self.required_perms.intersection(user_perms):
608 608 return True
609 609 return False
610 610
611 611
612 612 class HasReposGroupPermissionAllDecorator(PermsDecorator):
613 613 """
614 614 Checks for access permission for all given predicates for specific
615 615 repository. All of them have to be meet in order to fulfill the request
616 616 """
617 617
618 618 def check_permissions(self):
619 619 group_name = get_repos_group_slug(request)
620 620 try:
621 621 user_perms = set([self.user_perms['repositories_groups'][group_name]])
622 622 except KeyError:
623 623 return False
624 624 if self.required_perms.issubset(user_perms):
625 625 return True
626 626 return False
627 627
628 628
629 629 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
630 630 """
631 631 Checks for access permission for any of given predicates for specific
632 632 repository. In order to fulfill the request any of predicates must be meet
633 633 """
634 634
635 635 def check_permissions(self):
636 636 group_name = get_repos_group_slug(request)
637 637
638 638 try:
639 639 user_perms = set([self.user_perms['repositories_groups'][group_name]])
640 640 except KeyError:
641 641 return False
642 642 if self.required_perms.intersection(user_perms):
643 643 return True
644 644 return False
645 645
646 646
647 647 #==============================================================================
648 648 # CHECK FUNCTIONS
649 649 #==============================================================================
650 650 class PermsFunction(object):
651 651 """Base function for other check functions"""
652 652
653 653 def __init__(self, *perms):
654 654 available_perms = config['available_permissions']
655 655
656 656 for perm in perms:
657 657 if perm not in available_perms:
658 658 raise Exception("'%s' permission is not defined" % perm)
659 659 self.required_perms = set(perms)
660 660 self.user_perms = None
661 self.granted_for = ''
662 661 self.repo_name = None
662 self.group_name = None
663 663
664 664 def __call__(self, check_Location=''):
665 665 user = request.user
666 log.debug('checking %s %s %s', self.__class__.__name__,
667 self.required_perms, user)
666 cls_name = self.__class__.__name__
667 check_scope = {
668 'HasPermissionAll': '',
669 'HasPermissionAny': '',
670 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
671 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
672 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
673 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
674 }.get(cls_name, '?')
675 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
676 self.required_perms, user, check_scope,
677 check_Location or 'unspecified location')
668 678 if not user:
669 679 log.debug('Empty request user')
670 680 return False
671 681 self.user_perms = user.permissions
672 self.granted_for = user
673
674 682 if self.check_permissions():
675 log.debug('Permission granted %s @ %s', self.granted_for,
683 log.debug('Permission granted for user: %s @ %s', user,
676 684 check_Location or 'unspecified location')
677 685 return True
678 686
679 687 else:
680 log.debug('Permission denied for %s @ %s', self.granted_for,
688 log.debug('Permission denied for user: %s @ %s', user,
681 689 check_Location or 'unspecified location')
682 690 return False
683 691
684 692 def check_permissions(self):
685 693 """Dummy function for overriding"""
686 694 raise Exception('You have to write this function in child class')
687 695
688 696
689 697 class HasPermissionAll(PermsFunction):
690 698 def check_permissions(self):
691 699 if self.required_perms.issubset(self.user_perms.get('global')):
692 700 return True
693 701 return False
694 702
695 703
696 704 class HasPermissionAny(PermsFunction):
697 705 def check_permissions(self):
698 706 if self.required_perms.intersection(self.user_perms.get('global')):
699 707 return True
700 708 return False
701 709
702 710
703 711 class HasRepoPermissionAll(PermsFunction):
704
705 712 def __call__(self, repo_name=None, check_Location=''):
706 713 self.repo_name = repo_name
707 714 return super(HasRepoPermissionAll, self).__call__(check_Location)
708 715
709 716 def check_permissions(self):
710 717 if not self.repo_name:
711 718 self.repo_name = get_repo_slug(request)
712 719
713 720 try:
714 self.user_perms = set(
721 self._user_perms = set(
715 722 [self.user_perms['repositories'][self.repo_name]]
716 723 )
717 724 except KeyError:
718 725 return False
719 self.granted_for = self.repo_name
720 if self.required_perms.issubset(self.user_perms):
726 if self.required_perms.issubset(self._user_perms):
721 727 return True
722 728 return False
723 729
724 730
725 731 class HasRepoPermissionAny(PermsFunction):
726
727 732 def __call__(self, repo_name=None, check_Location=''):
728 733 self.repo_name = repo_name
729 734 return super(HasRepoPermissionAny, self).__call__(check_Location)
730 735
731 736 def check_permissions(self):
732 737 if not self.repo_name:
733 738 self.repo_name = get_repo_slug(request)
734 739
735 740 try:
736 self.user_perms = set(
741 self._user_perms = set(
737 742 [self.user_perms['repositories'][self.repo_name]]
738 743 )
739 744 except KeyError:
740 745 return False
741 self.granted_for = self.repo_name
742 if self.required_perms.intersection(self.user_perms):
746 if self.required_perms.intersection(self._user_perms):
743 747 return True
744 748 return False
745 749
746 750
747 751 class HasReposGroupPermissionAny(PermsFunction):
748 752 def __call__(self, group_name=None, check_Location=''):
749 753 self.group_name = group_name
750 754 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
751 755
752 756 def check_permissions(self):
753 757 try:
754 self.user_perms = set(
758 self._user_perms = set(
755 759 [self.user_perms['repositories_groups'][self.group_name]]
756 760 )
757 761 except KeyError:
758 762 return False
759 self.granted_for = self.repo_name
760 if self.required_perms.intersection(self.user_perms):
763 if self.required_perms.intersection(self._user_perms):
761 764 return True
762 765 return False
763 766
764 767
765 768 class HasReposGroupPermissionAll(PermsFunction):
766 769 def __call__(self, group_name=None, check_Location=''):
767 770 self.group_name = group_name
768 771 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
769 772
770 773 def check_permissions(self):
771 774 try:
772 self.user_perms = set(
775 self._user_perms = set(
773 776 [self.user_perms['repositories_groups'][self.group_name]]
774 777 )
775 778 except KeyError:
776 779 return False
777 self.granted_for = self.repo_name
778 if self.required_perms.issubset(self.user_perms):
780 if self.required_perms.issubset(self._user_perms):
779 781 return True
780 782 return False
781 783
782 784
783 785 #==============================================================================
784 786 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
785 787 #==============================================================================
786 788 class HasPermissionAnyMiddleware(object):
787 789 def __init__(self, *perms):
788 790 self.required_perms = set(perms)
789 791
790 792 def __call__(self, user, repo_name):
791 793 # repo_name MUST be unicode, since we handle keys in permission
792 794 # dict by unicode
793 795 repo_name = safe_unicode(repo_name)
794 796 usr = AuthUser(user.user_id)
795 797 try:
796 798 self.user_perms = set([usr.permissions['repositories'][repo_name]])
797 799 except Exception:
798 800 log.error('Exception while accessing permissions %s' %
799 801 traceback.format_exc())
800 802 self.user_perms = set()
801 self.granted_for = ''
802 803 self.username = user.username
803 804 self.repo_name = repo_name
804 805 return self.check_permissions()
805 806
806 807 def check_permissions(self):
807 808 log.debug('checking mercurial protocol '
808 809 'permissions %s for user:%s repository:%s', self.user_perms,
809 810 self.username, self.repo_name)
810 811 if self.required_perms.intersection(self.user_perms):
811 log.debug('permission granted')
812 log.debug('permission granted for user:%s on repo:%s' % (
813 self.username, self.repo_name
814 )
815 )
812 816 return True
813 log.debug('permission denied')
817 log.debug('permission denied for user:%s on repo:%s' % (
818 self.username, self.repo_name
819 )
820 )
814 821 return False
@@ -1,926 +1,926 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12
13 13 from datetime import datetime
14 14 from pygments.formatters.html import HtmlFormatter
15 15 from pygments import highlight as code_highlight
16 16 from pylons import url, request, config
17 17 from pylons.i18n.translation import _, ungettext
18 18 from hashlib import md5
19 19
20 20 from webhelpers.html import literal, HTML, escape
21 21 from webhelpers.html.tools import *
22 22 from webhelpers.html.builder import make_tag
23 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
24 24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
29 29 from webhelpers.number import format_byte_size, format_bit_size
30 30 from webhelpers.pylonslib import Flash as _Flash
31 31 from webhelpers.pylonslib.secure_form import secure_form
32 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
33 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
34 34 replace_whitespace, urlify, truncate, wrap_paragraphs
35 35 from webhelpers.date import time_ago_in_words
36 36 from webhelpers.paginate import Page
37 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
38 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
39 39
40 40 from rhodecode.lib.annotate import annotate_highlight
41 41 from rhodecode.lib.utils import repo_name_slug
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
43 43 get_changeset_safe
44 44 from rhodecode.lib.markup_renderer import MarkupRenderer
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
50 50 """
51 51 Reset button
52 52 """
53 53 _set_input_attrs(attrs, type, name, value)
54 54 _set_id_attr(attrs, id, name)
55 55 convert_boolean_attrs(attrs, ["disabled"])
56 56 return HTML.input(**attrs)
57 57
58 58 reset = _reset
59 59 safeid = _make_safe_id_component
60 60
61 61
62 62 def FID(raw_id, path):
63 63 """
64 64 Creates a uniqe ID for filenode based on it's hash of path and revision
65 65 it's safe to use in urls
66 66
67 67 :param raw_id:
68 68 :param path:
69 69 """
70 70
71 71 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
72 72
73 73
74 74 def get_token():
75 75 """Return the current authentication token, creating one if one doesn't
76 76 already exist.
77 77 """
78 78 token_key = "_authentication_token"
79 79 from pylons import session
80 80 if not token_key in session:
81 81 try:
82 82 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
83 83 except AttributeError: # Python < 2.4
84 84 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
85 85 session[token_key] = token
86 86 if hasattr(session, 'save'):
87 87 session.save()
88 88 return session[token_key]
89 89
90 90 class _GetError(object):
91 91 """Get error from form_errors, and represent it as span wrapped error
92 92 message
93 93
94 94 :param field_name: field to fetch errors for
95 95 :param form_errors: form errors dict
96 96 """
97 97
98 98 def __call__(self, field_name, form_errors):
99 99 tmpl = """<span class="error_msg">%s</span>"""
100 100 if form_errors and form_errors.has_key(field_name):
101 101 return literal(tmpl % form_errors.get(field_name))
102 102
103 103 get_error = _GetError()
104 104
105 105 class _ToolTip(object):
106 106
107 107 def __call__(self, tooltip_title, trim_at=50):
108 108 """Special function just to wrap our text into nice formatted
109 109 autowrapped text
110 110
111 111 :param tooltip_title:
112 112 """
113 113 return escape(tooltip_title)
114 114 tooltip = _ToolTip()
115 115
116 116 class _FilesBreadCrumbs(object):
117 117
118 118 def __call__(self, repo_name, rev, paths):
119 119 if isinstance(paths, str):
120 120 paths = safe_unicode(paths)
121 121 url_l = [link_to(repo_name, url('files_home',
122 122 repo_name=repo_name,
123 123 revision=rev, f_path=''))]
124 124 paths_l = paths.split('/')
125 125 for cnt, p in enumerate(paths_l):
126 126 if p != '':
127 127 url_l.append(link_to(p,
128 128 url('files_home',
129 129 repo_name=repo_name,
130 130 revision=rev,
131 131 f_path='/'.join(paths_l[:cnt + 1])
132 132 )
133 133 )
134 134 )
135 135
136 136 return literal('/'.join(url_l))
137 137
138 138 files_breadcrumbs = _FilesBreadCrumbs()
139 139
140 140 class CodeHtmlFormatter(HtmlFormatter):
141 141 """My code Html Formatter for source codes
142 142 """
143 143
144 144 def wrap(self, source, outfile):
145 145 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
146 146
147 147 def _wrap_code(self, source):
148 148 for cnt, it in enumerate(source):
149 149 i, t = it
150 150 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
151 151 yield i, t
152 152
153 153 def _wrap_tablelinenos(self, inner):
154 154 dummyoutfile = StringIO.StringIO()
155 155 lncount = 0
156 156 for t, line in inner:
157 157 if t:
158 158 lncount += 1
159 159 dummyoutfile.write(line)
160 160
161 161 fl = self.linenostart
162 162 mw = len(str(lncount + fl - 1))
163 163 sp = self.linenospecial
164 164 st = self.linenostep
165 165 la = self.lineanchors
166 166 aln = self.anchorlinenos
167 167 nocls = self.noclasses
168 168 if sp:
169 169 lines = []
170 170
171 171 for i in range(fl, fl + lncount):
172 172 if i % st == 0:
173 173 if i % sp == 0:
174 174 if aln:
175 175 lines.append('<a href="#%s%d" class="special">%*d</a>' %
176 176 (la, i, mw, i))
177 177 else:
178 178 lines.append('<span class="special">%*d</span>' % (mw, i))
179 179 else:
180 180 if aln:
181 181 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
182 182 else:
183 183 lines.append('%*d' % (mw, i))
184 184 else:
185 185 lines.append('')
186 186 ls = '\n'.join(lines)
187 187 else:
188 188 lines = []
189 189 for i in range(fl, fl + lncount):
190 190 if i % st == 0:
191 191 if aln:
192 192 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
193 193 else:
194 194 lines.append('%*d' % (mw, i))
195 195 else:
196 196 lines.append('')
197 197 ls = '\n'.join(lines)
198 198
199 199 # in case you wonder about the seemingly redundant <div> here: since the
200 200 # content in the other cell also is wrapped in a div, some browsers in
201 201 # some configurations seem to mess up the formatting...
202 202 if nocls:
203 203 yield 0, ('<table class="%stable">' % self.cssclass +
204 204 '<tr><td><div class="linenodiv" '
205 205 'style="background-color: #f0f0f0; padding-right: 10px">'
206 206 '<pre style="line-height: 125%">' +
207 207 ls + '</pre></div></td><td id="hlcode" class="code">')
208 208 else:
209 209 yield 0, ('<table class="%stable">' % self.cssclass +
210 210 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
211 211 ls + '</pre></div></td><td id="hlcode" class="code">')
212 212 yield 0, dummyoutfile.getvalue()
213 213 yield 0, '</td></tr></table>'
214 214
215 215
216 216 def pygmentize(filenode, **kwargs):
217 217 """pygmentize function using pygments
218 218
219 219 :param filenode:
220 220 """
221 221
222 222 return literal(code_highlight(filenode.content,
223 223 filenode.lexer, CodeHtmlFormatter(**kwargs)))
224 224
225 225
226 226 def pygmentize_annotation(repo_name, filenode, **kwargs):
227 227 """
228 228 pygmentize function for annotation
229 229
230 230 :param filenode:
231 231 """
232 232
233 233 color_dict = {}
234 234
235 235 def gen_color(n=10000):
236 236 """generator for getting n of evenly distributed colors using
237 237 hsv color and golden ratio. It always return same order of colors
238 238
239 239 :returns: RGB tuple
240 240 """
241 241
242 242 def hsv_to_rgb(h, s, v):
243 243 if s == 0.0:
244 244 return v, v, v
245 245 i = int(h * 6.0) # XXX assume int() truncates!
246 246 f = (h * 6.0) - i
247 247 p = v * (1.0 - s)
248 248 q = v * (1.0 - s * f)
249 249 t = v * (1.0 - s * (1.0 - f))
250 250 i = i % 6
251 251 if i == 0:
252 252 return v, t, p
253 253 if i == 1:
254 254 return q, v, p
255 255 if i == 2:
256 256 return p, v, t
257 257 if i == 3:
258 258 return p, q, v
259 259 if i == 4:
260 260 return t, p, v
261 261 if i == 5:
262 262 return v, p, q
263 263
264 264 golden_ratio = 0.618033988749895
265 265 h = 0.22717784590367374
266 266
267 267 for _ in xrange(n):
268 268 h += golden_ratio
269 269 h %= 1
270 270 HSV_tuple = [h, 0.95, 0.95]
271 271 RGB_tuple = hsv_to_rgb(*HSV_tuple)
272 272 yield map(lambda x: str(int(x * 256)), RGB_tuple)
273 273
274 274 cgenerator = gen_color()
275 275
276 276 def get_color_string(cs):
277 277 if cs in color_dict:
278 278 col = color_dict[cs]
279 279 else:
280 280 col = color_dict[cs] = cgenerator.next()
281 281 return "color: rgb(%s)! important;" % (', '.join(col))
282 282
283 283 def url_func(repo_name):
284 284
285 285 def _url_func(changeset):
286 286 author = changeset.author
287 287 date = changeset.date
288 288 message = tooltip(changeset.message)
289 289
290 290 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
291 291 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
292 292 "</b> %s<br/></div>")
293 293
294 294 tooltip_html = tooltip_html % (author, date, message)
295 295 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
296 296 short_id(changeset.raw_id))
297 297 uri = link_to(
298 298 lnk_format,
299 299 url('changeset_home', repo_name=repo_name,
300 300 revision=changeset.raw_id),
301 301 style=get_color_string(changeset.raw_id),
302 302 class_='tooltip',
303 303 title=tooltip_html
304 304 )
305 305
306 306 uri += '\n'
307 307 return uri
308 308 return _url_func
309 309
310 310 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
311 311
312 312
313 313 def is_following_repo(repo_name, user_id):
314 314 from rhodecode.model.scm import ScmModel
315 315 return ScmModel().is_following_repo(repo_name, user_id)
316 316
317 317 flash = _Flash()
318 318
319 319 #==============================================================================
320 320 # SCM FILTERS available via h.
321 321 #==============================================================================
322 322 from rhodecode.lib.vcs.utils import author_name, author_email
323 323 from rhodecode.lib.utils2 import credentials_filter, age as _age
324 324 from rhodecode.model.db import User
325 325
326 326 age = lambda x: _age(x)
327 327 capitalize = lambda x: x.capitalize()
328 328 email = author_email
329 329 short_id = lambda x: x[:12]
330 330 hide_credentials = lambda x: ''.join(credentials_filter(x))
331 331
332 332
333 333 def is_git(repository):
334 334 if hasattr(repository, 'alias'):
335 335 _type = repository.alias
336 336 elif hasattr(repository, 'repo_type'):
337 337 _type = repository.repo_type
338 338 else:
339 339 _type = repository
340 340 return _type == 'git'
341 341
342 342
343 343 def is_hg(repository):
344 344 if hasattr(repository, 'alias'):
345 345 _type = repository.alias
346 346 elif hasattr(repository, 'repo_type'):
347 347 _type = repository.repo_type
348 348 else:
349 349 _type = repository
350 350 return _type == 'hg'
351 351
352 352
353 353 def email_or_none(author):
354 354 _email = email(author)
355 355 if _email != '':
356 356 return _email
357 357
358 358 # See if it contains a username we can get an email from
359 359 user = User.get_by_username(author_name(author), case_insensitive=True,
360 360 cache=True)
361 361 if user is not None:
362 362 return user.email
363 363
364 364 # No valid email, not a valid user in the system, none!
365 365 return None
366 366
367 367
368 368 def person(author):
369 369 # attr to return from fetched user
370 370 person_getter = lambda usr: usr.username
371 371
372 372 # Valid email in the attribute passed, see if they're in the system
373 373 _email = email(author)
374 374 if _email != '':
375 375 user = User.get_by_email(_email, case_insensitive=True, cache=True)
376 376 if user is not None:
377 377 return person_getter(user)
378 378 return _email
379 379
380 380 # Maybe it's a username?
381 381 _author = author_name(author)
382 382 user = User.get_by_username(_author, case_insensitive=True,
383 383 cache=True)
384 384 if user is not None:
385 385 return person_getter(user)
386 386
387 387 # Still nothing? Just pass back the author name then
388 388 return _author
389 389
390 390
391 391 def bool2icon(value):
392 392 """Returns True/False values represented as small html image of true/false
393 393 icons
394 394
395 395 :param value: bool value
396 396 """
397 397
398 398 if value is True:
399 399 return HTML.tag('img', src=url("/images/icons/accept.png"),
400 400 alt=_('True'))
401 401
402 402 if value is False:
403 403 return HTML.tag('img', src=url("/images/icons/cancel.png"),
404 404 alt=_('False'))
405 405
406 406 return value
407 407
408 408
409 409 def action_parser(user_log, feed=False):
410 410 """
411 411 This helper will action_map the specified string action into translated
412 412 fancy names with icons and links
413 413
414 414 :param user_log: user log instance
415 415 :param feed: use output for feeds (no html and fancy icons)
416 416 """
417 417
418 418 action = user_log.action
419 419 action_params = ' '
420 420
421 421 x = action.split(':')
422 422
423 423 if len(x) > 1:
424 424 action, action_params = x
425 425
426 426 def get_cs_links():
427 427 revs_limit = 3 # display this amount always
428 428 revs_top_limit = 50 # show upto this amount of changesets hidden
429 429 revs_ids = action_params.split(',')
430 430 deleted = user_log.repository is None
431 431 if deleted:
432 432 return ','.join(revs_ids)
433 433
434 434 repo_name = user_log.repository.repo_name
435 435
436 436 repo = user_log.repository.scm_instance
437 437
438 438 message = lambda rev: rev.message
439 439 lnk = lambda rev, repo_name: (
440 440 link_to('r%s:%s' % (rev.revision, rev.short_id),
441 441 url('changeset_home', repo_name=repo_name,
442 442 revision=rev.raw_id),
443 443 title=tooltip(message(rev)), class_='tooltip')
444 444 )
445 445 # get only max revs_top_limit of changeset for performance/ui reasons
446 446 revs = [
447 447 x for x in repo.get_changesets(revs_ids[0],
448 448 revs_ids[:revs_top_limit][-1])
449 449 ]
450 450
451 451 cs_links = []
452 452 cs_links.append(" " + ', '.join(
453 453 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
454 454 )
455 455 )
456 456
457 457 compare_view = (
458 458 ' <div class="compare_view tooltip" title="%s">'
459 459 '<a href="%s">%s</a> </div>' % (
460 460 _('Show all combined changesets %s->%s') % (
461 461 revs_ids[0], revs_ids[-1]
462 462 ),
463 463 url('changeset_home', repo_name=repo_name,
464 464 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
465 465 ),
466 466 _('compare view')
467 467 )
468 468 )
469 469
470 470 # if we have exactly one more than normally displayed
471 471 # just display it, takes less space than displaying
472 472 # "and 1 more revisions"
473 473 if len(revs_ids) == revs_limit + 1:
474 474 rev = revs[revs_limit]
475 475 cs_links.append(", " + lnk(rev, repo_name))
476 476
477 477 # hidden-by-default ones
478 478 if len(revs_ids) > revs_limit + 1:
479 479 uniq_id = revs_ids[0]
480 480 html_tmpl = (
481 481 '<span> %s <a class="show_more" id="_%s" '
482 482 'href="#more">%s</a> %s</span>'
483 483 )
484 484 if not feed:
485 485 cs_links.append(html_tmpl % (
486 486 _('and'),
487 487 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
488 488 _('revisions')
489 489 )
490 490 )
491 491
492 492 if not feed:
493 493 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
494 494 else:
495 495 html_tmpl = '<span id="%s"> %s </span>'
496 496
497 497 morelinks = ', '.join(
498 498 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
499 499 )
500 500
501 501 if len(revs_ids) > revs_top_limit:
502 502 morelinks += ', ...'
503 503
504 504 cs_links.append(html_tmpl % (uniq_id, morelinks))
505 505 if len(revs) > 1:
506 506 cs_links.append(compare_view)
507 507 return ''.join(cs_links)
508 508
509 509 def get_fork_name():
510 510 repo_name = action_params
511 511 return _('fork name ') + str(link_to(action_params, url('summary_home',
512 512 repo_name=repo_name,)))
513 513
514 514 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
515 515 'user_created_repo': (_('[created] repository'), None),
516 516 'user_created_fork': (_('[created] repository as fork'), None),
517 517 'user_forked_repo': (_('[forked] repository'), get_fork_name),
518 518 'user_updated_repo': (_('[updated] repository'), None),
519 519 'admin_deleted_repo': (_('[delete] repository'), None),
520 520 'admin_created_repo': (_('[created] repository'), None),
521 521 'admin_forked_repo': (_('[forked] repository'), None),
522 522 'admin_updated_repo': (_('[updated] repository'), None),
523 523 'push': (_('[pushed] into'), get_cs_links),
524 524 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
525 525 'push_remote': (_('[pulled from remote] into'), get_cs_links),
526 526 'pull': (_('[pulled] from'), None),
527 527 'started_following_repo': (_('[started following] repository'), None),
528 528 'stopped_following_repo': (_('[stopped following] repository'), None),
529 529 }
530 530
531 531 action_str = action_map.get(action, action)
532 532 if feed:
533 533 action = action_str[0].replace('[', '').replace(']', '')
534 534 else:
535 535 action = action_str[0]\
536 536 .replace('[', '<span class="journal_highlight">')\
537 537 .replace(']', '</span>')
538 538
539 539 action_params_func = lambda: ""
540 540
541 541 if callable(action_str[1]):
542 542 action_params_func = action_str[1]
543 543
544 544 return [literal(action), action_params_func]
545 545
546 546
547 547 def action_parser_icon(user_log):
548 548 action = user_log.action
549 549 action_params = None
550 550 x = action.split(':')
551 551
552 552 if len(x) > 1:
553 553 action, action_params = x
554 554
555 555 tmpl = """<img src="%s%s" alt="%s"/>"""
556 556 map = {'user_deleted_repo':'database_delete.png',
557 557 'user_created_repo':'database_add.png',
558 558 'user_created_fork':'arrow_divide.png',
559 559 'user_forked_repo':'arrow_divide.png',
560 560 'user_updated_repo':'database_edit.png',
561 561 'admin_deleted_repo':'database_delete.png',
562 562 'admin_created_repo':'database_add.png',
563 563 'admin_forked_repo':'arrow_divide.png',
564 564 'admin_updated_repo':'database_edit.png',
565 565 'push':'script_add.png',
566 566 'push_local':'script_edit.png',
567 567 'push_remote':'connect.png',
568 568 'pull':'down_16.png',
569 569 'started_following_repo':'heart_add.png',
570 570 'stopped_following_repo':'heart_delete.png',
571 571 }
572 572 return literal(tmpl % ((url('/images/icons/')),
573 573 map.get(action, action), action))
574 574
575 575
576 576 #==============================================================================
577 577 # PERMS
578 578 #==============================================================================
579 579 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
580 580 HasRepoPermissionAny, HasRepoPermissionAll
581 581
582 582
583 583 #==============================================================================
584 584 # GRAVATAR URL
585 585 #==============================================================================
586 586
587 587 def gravatar_url(email_address, size=30):
588 588 if (not str2bool(config['app_conf'].get('use_gravatar')) or
589 589 not email_address or email_address == 'anonymous@rhodecode.org'):
590 590 f = lambda a, l: min(l, key=lambda x: abs(x - a))
591 591 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
592 592
593 593 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
594 594 default = 'identicon'
595 595 baseurl_nossl = "http://www.gravatar.com/avatar/"
596 596 baseurl_ssl = "https://secure.gravatar.com/avatar/"
597 597 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
598 598
599 599 if isinstance(email_address, unicode):
600 600 #hashlib crashes on unicode items
601 601 email_address = safe_str(email_address)
602 602 # construct the url
603 603 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
604 604 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
605 605
606 606 return gravatar_url
607 607
608 608
609 609 #==============================================================================
610 610 # REPO PAGER, PAGER FOR REPOSITORY
611 611 #==============================================================================
612 612 class RepoPage(Page):
613 613
614 614 def __init__(self, collection, page=1, items_per_page=20,
615 615 item_count=None, url=None, **kwargs):
616 616
617 617 """Create a "RepoPage" instance. special pager for paging
618 618 repository
619 619 """
620 620 self._url_generator = url
621 621
622 622 # Safe the kwargs class-wide so they can be used in the pager() method
623 623 self.kwargs = kwargs
624 624
625 625 # Save a reference to the collection
626 626 self.original_collection = collection
627 627
628 628 self.collection = collection
629 629
630 630 # The self.page is the number of the current page.
631 631 # The first page has the number 1!
632 632 try:
633 633 self.page = int(page) # make it int() if we get it as a string
634 634 except (ValueError, TypeError):
635 635 self.page = 1
636 636
637 637 self.items_per_page = items_per_page
638 638
639 639 # Unless the user tells us how many items the collections has
640 640 # we calculate that ourselves.
641 641 if item_count is not None:
642 642 self.item_count = item_count
643 643 else:
644 644 self.item_count = len(self.collection)
645 645
646 646 # Compute the number of the first and last available page
647 647 if self.item_count > 0:
648 648 self.first_page = 1
649 649 self.page_count = int(math.ceil(float(self.item_count) /
650 650 self.items_per_page))
651 651 self.last_page = self.first_page + self.page_count - 1
652 652
653 653 # Make sure that the requested page number is the range of
654 654 # valid pages
655 655 if self.page > self.last_page:
656 656 self.page = self.last_page
657 657 elif self.page < self.first_page:
658 658 self.page = self.first_page
659 659
660 660 # Note: the number of items on this page can be less than
661 661 # items_per_page if the last page is not full
662 662 self.first_item = max(0, (self.item_count) - (self.page *
663 663 items_per_page))
664 664 self.last_item = ((self.item_count - 1) - items_per_page *
665 665 (self.page - 1))
666 666
667 667 self.items = list(self.collection[self.first_item:self.last_item + 1])
668 668
669 669 # Links to previous and next page
670 670 if self.page > self.first_page:
671 671 self.previous_page = self.page - 1
672 672 else:
673 673 self.previous_page = None
674 674
675 675 if self.page < self.last_page:
676 676 self.next_page = self.page + 1
677 677 else:
678 678 self.next_page = None
679 679
680 680 # No items available
681 681 else:
682 682 self.first_page = None
683 683 self.page_count = 0
684 684 self.last_page = None
685 685 self.first_item = None
686 686 self.last_item = None
687 687 self.previous_page = None
688 688 self.next_page = None
689 689 self.items = []
690 690
691 691 # This is a subclass of the 'list' type. Initialise the list now.
692 692 list.__init__(self, reversed(self.items))
693 693
694 694
695 695 def changed_tooltip(nodes):
696 696 """
697 697 Generates a html string for changed nodes in changeset page.
698 698 It limits the output to 30 entries
699 699
700 700 :param nodes: LazyNodesGenerator
701 701 """
702 702 if nodes:
703 703 pref = ': <br/> '
704 704 suf = ''
705 705 if len(nodes) > 30:
706 706 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
707 707 return literal(pref + '<br/> '.join([safe_unicode(x.path)
708 708 for x in nodes[:30]]) + suf)
709 709 else:
710 710 return ': ' + _('No Files')
711 711
712 712
713 713 def repo_link(groups_and_repos):
714 714 """
715 715 Makes a breadcrumbs link to repo within a group
716 716 joins &raquo; on each group to create a fancy link
717 717
718 718 ex::
719 719 group >> subgroup >> repo
720 720
721 721 :param groups_and_repos:
722 722 """
723 723 groups, repo_name = groups_and_repos
724 724
725 725 if not groups:
726 726 return repo_name
727 727 else:
728 728 def make_link(group):
729 729 return link_to(group.name, url('repos_group_home',
730 730 group_name=group.group_name))
731 731 return literal(' &raquo; '.join(map(make_link, groups)) + \
732 732 " &raquo; " + repo_name)
733 733
734 734
735 735 def fancy_file_stats(stats):
736 736 """
737 737 Displays a fancy two colored bar for number of added/deleted
738 738 lines of code on file
739 739
740 740 :param stats: two element list of added/deleted lines of code
741 741 """
742 742
743 743 a, d, t = stats[0], stats[1], stats[0] + stats[1]
744 744 width = 100
745 745 unit = float(width) / (t or 1)
746 746
747 747 # needs > 9% of width to be visible or 0 to be hidden
748 748 a_p = max(9, unit * a) if a > 0 else 0
749 749 d_p = max(9, unit * d) if d > 0 else 0
750 750 p_sum = a_p + d_p
751 751
752 752 if p_sum > width:
753 753 #adjust the percentage to be == 100% since we adjusted to 9
754 754 if a_p > d_p:
755 755 a_p = a_p - (p_sum - width)
756 756 else:
757 757 d_p = d_p - (p_sum - width)
758 758
759 759 a_v = a if a > 0 else ''
760 760 d_v = d if d > 0 else ''
761 761
762 762 def cgen(l_type):
763 763 mapping = {'tr': 'top-right-rounded-corner-mid',
764 764 'tl': 'top-left-rounded-corner-mid',
765 765 'br': 'bottom-right-rounded-corner-mid',
766 766 'bl': 'bottom-left-rounded-corner-mid'}
767 767 map_getter = lambda x: mapping[x]
768 768
769 769 if l_type == 'a' and d_v:
770 770 #case when added and deleted are present
771 771 return ' '.join(map(map_getter, ['tl', 'bl']))
772 772
773 773 if l_type == 'a' and not d_v:
774 774 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
775 775
776 776 if l_type == 'd' and a_v:
777 777 return ' '.join(map(map_getter, ['tr', 'br']))
778 778
779 779 if l_type == 'd' and not a_v:
780 780 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
781 781
782 782 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
783 783 cgen('a'), a_p, a_v
784 784 )
785 785 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
786 786 cgen('d'), d_p, d_v
787 787 )
788 788 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
789 789
790 790
791 791 def urlify_text(text_):
792 792 import re
793 793
794 794 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
795 795 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
796 796
797 797 def url_func(match_obj):
798 798 url_full = match_obj.groups()[0]
799 799 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
800 800
801 801 return literal(url_pat.sub(url_func, text_))
802 802
803 803
804 804 def urlify_changesets(text_, repository):
805 805 """
806 806 Extract revision ids from changeset and make link from them
807
807
808 808 :param text_:
809 809 :param repository:
810 810 """
811 811 import re
812 812 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
813 813
814 814 def url_func(match_obj):
815 815 rev = match_obj.groups()[0]
816 816 pref = ''
817 817 if match_obj.group().startswith(' '):
818 818 pref = ' '
819 819 tmpl = (
820 820 '%(pref)s<a class="%(cls)s" href="%(url)s">'
821 821 '%(rev)s'
822 822 '</a>'
823 823 )
824 824 return tmpl % {
825 825 'pref': pref,
826 826 'cls': 'revision-link',
827 827 'url': url('changeset_home', repo_name=repository, revision=rev),
828 828 'rev': rev,
829 829 }
830 830
831 831 newtext = URL_PAT.sub(url_func, text_)
832 832
833 833 return newtext
834 834
835 835
836 836 def urlify_commit(text_, repository=None, link_=None):
837 837 """
838 838 Parses given text message and makes proper links.
839 839 issues are linked to given issue-server, and rest is a changeset link
840 840 if link_ is given, in other case it's a plain text
841 841
842 842 :param text_:
843 843 :param repository:
844 844 :param link_: changeset link
845 845 """
846 846 import re
847 847 import traceback
848
848
849 849 def escaper(string):
850 850 return string.replace('<', '&lt;').replace('>', '&gt;')
851
851
852 852 def linkify_others(t, l):
853 853 urls = re.compile(r'(\<a.*?\<\/a\>)',)
854 854 links = []
855 855 for e in urls.split(t):
856 856 if not urls.match(e):
857 857 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
858 858 else:
859 859 links.append(e)
860 860
861 861 return ''.join(links)
862
863
862
863
864 864 # urlify changesets - extrac revisions and make link out of them
865 865 text_ = urlify_changesets(escaper(text_), repository)
866 866
867 867 try:
868 868 conf = config['app_conf']
869 869
870 870 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
871 871
872 872 if URL_PAT:
873 873 ISSUE_SERVER_LNK = conf.get('issue_server_link')
874 874 ISSUE_PREFIX = conf.get('issue_prefix')
875 875
876 876 def url_func(match_obj):
877 877 pref = ''
878 878 if match_obj.group().startswith(' '):
879 879 pref = ' '
880 880
881 881 issue_id = ''.join(match_obj.groups())
882 882 tmpl = (
883 883 '%(pref)s<a class="%(cls)s" href="%(url)s">'
884 884 '%(issue-prefix)s%(id-repr)s'
885 885 '</a>'
886 886 )
887 887 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
888 888 if repository:
889 889 url = url.replace('{repo}', repository)
890 890
891 891 return tmpl % {
892 892 'pref': pref,
893 893 'cls': 'issue-tracker-link',
894 894 'url': url,
895 895 'id-repr': issue_id,
896 896 'issue-prefix': ISSUE_PREFIX,
897 897 'serv': ISSUE_SERVER_LNK,
898 898 }
899 899
900 900 newtext = URL_PAT.sub(url_func, text_)
901 901
902 902 if link_:
903 903 # wrap not links into final link => link_
904 904 newtext = linkify_others(newtext, link_)
905 905
906 906 return literal(newtext)
907 907 except:
908 908 log.error(traceback.format_exc())
909 909 pass
910 910
911 911 return text_
912 912
913 913
914 914 def rst(source):
915 915 return literal('<div class="rst-block">%s</div>' %
916 916 MarkupRenderer.rst(source))
917 917
918 918
919 919 def rst_w_mentions(source):
920 920 """
921 921 Wrapped rst renderer with @mention highlighting
922 922
923 923 :param source:
924 924 """
925 925 return literal('<div class="rst-block">%s</div>' %
926 926 MarkupRenderer.rst_with_mentions(source))
General Comments 0
You need to be logged in to leave comments. Login now