##// END OF EJS Templates
utils: drop make_ui parameter clear_session - this utility function should never mess with sessions...
Mads Kiilerich -
r7878:a1115795 default
parent child Browse files
Show More
@@ -1,683 +1,681 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.utils
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Utilities library for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 18, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import datetime
29 29 import logging
30 30 import os
31 31 import re
32 32 import sys
33 33 import traceback
34 34 from distutils.version import StrictVersion
35 35
36 36 import beaker
37 37 from beaker.cache import _cache_decorate
38 38 from tg.i18n import ugettext as _
39 39
40 40 from kallithea.lib.exceptions import HgsubversionImportError
41 41 from kallithea.lib.utils2 import get_current_authuser, safe_str, safe_unicode
42 42 from kallithea.lib.vcs.exceptions import VCSError
43 43 from kallithea.lib.vcs.utils.fakemod import create_module
44 44 from kallithea.lib.vcs.utils.helpers import get_scm
45 45 from kallithea.lib.vcs.utils.hgcompat import config, ui
46 46 from kallithea.model import meta
47 47 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
48 48 from kallithea.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
54 54
55 55
56 56 #==============================================================================
57 57 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
58 58 #==============================================================================
59 59 def get_repo_slug(request):
60 60 _repo = request.environ['pylons.routes_dict'].get('repo_name')
61 61 if _repo:
62 62 _repo = _repo.rstrip('/')
63 63 return _repo
64 64
65 65
66 66 def get_repo_group_slug(request):
67 67 _group = request.environ['pylons.routes_dict'].get('group_name')
68 68 if _group:
69 69 _group = _group.rstrip('/')
70 70 return _group
71 71
72 72
73 73 def get_user_group_slug(request):
74 74 _group = request.environ['pylons.routes_dict'].get('id')
75 75 _group = UserGroup.get(_group)
76 76 if _group:
77 77 return _group.users_group_name
78 78 return None
79 79
80 80
81 81 def _get_permanent_id(s):
82 82 """Helper for decoding stable URLs with repo ID. For a string like '_123'
83 83 return 123.
84 84 """
85 85 by_id_match = re.match(r'^_(\d+)$', s)
86 86 if by_id_match is None:
87 87 return None
88 88 return int(by_id_match.group(1))
89 89
90 90
91 91 def fix_repo_id_name(path):
92 92 """
93 93 Rewrite repo_name for _<ID> permanent URLs.
94 94
95 95 Given a path, if the first path element is like _<ID>, return the path with
96 96 this part expanded to the corresponding full repo name, else return the
97 97 provided path.
98 98 """
99 99 first, rest = path, ''
100 100 if '/' in path:
101 101 first, rest_ = path.split('/', 1)
102 102 rest = '/' + rest_
103 103 repo_id = _get_permanent_id(first)
104 104 if repo_id is not None:
105 105 from kallithea.model.db import Repository
106 106 repo = Repository.get(repo_id)
107 107 if repo is not None:
108 108 return repo.repo_name + rest
109 109 return path
110 110
111 111
112 112 def action_logger(user, action, repo, ipaddr='', commit=False):
113 113 """
114 114 Action logger for various actions made by users
115 115
116 116 :param user: user that made this action, can be a unique username string or
117 117 object containing user_id attribute
118 118 :param action: action to log, should be on of predefined unique actions for
119 119 easy translations
120 120 :param repo: string name of repository or object containing repo_id,
121 121 that action was made on
122 122 :param ipaddr: optional IP address from what the action was made
123 123
124 124 """
125 125
126 126 # if we don't get explicit IP address try to get one from registered user
127 127 # in tmpl context var
128 128 if not ipaddr:
129 129 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
130 130
131 131 if getattr(user, 'user_id', None):
132 132 user_obj = User.get(user.user_id)
133 133 elif isinstance(user, basestring):
134 134 user_obj = User.get_by_username(user)
135 135 else:
136 136 raise Exception('You have to provide a user object or a username')
137 137
138 138 if getattr(repo, 'repo_id', None):
139 139 repo_obj = Repository.get(repo.repo_id)
140 140 repo_name = repo_obj.repo_name
141 141 elif isinstance(repo, basestring):
142 142 repo_name = repo.lstrip('/')
143 143 repo_obj = Repository.get_by_repo_name(repo_name)
144 144 else:
145 145 repo_obj = None
146 146 repo_name = u''
147 147
148 148 user_log = UserLog()
149 149 user_log.user_id = user_obj.user_id
150 150 user_log.username = user_obj.username
151 151 user_log.action = safe_unicode(action)
152 152
153 153 user_log.repository = repo_obj
154 154 user_log.repository_name = repo_name
155 155
156 156 user_log.action_date = datetime.datetime.now()
157 157 user_log.user_ip = ipaddr
158 158 meta.Session().add(user_log)
159 159
160 160 log.info('Logging action:%s on %s by user:%s ip:%s',
161 161 action, safe_unicode(repo), user_obj, ipaddr)
162 162 if commit:
163 163 meta.Session().commit()
164 164
165 165
166 166 def get_filesystem_repos(path):
167 167 """
168 168 Scans given path for repos and return (name,(type,path)) tuple
169 169
170 170 :param path: path to scan for repositories
171 171 :param recursive: recursive search and return names with subdirs in front
172 172 """
173 173
174 174 # remove ending slash for better results
175 175 path = safe_str(path.rstrip(os.sep))
176 176 log.debug('now scanning in %s', path)
177 177
178 178 def isdir(*n):
179 179 return os.path.isdir(os.path.join(*n))
180 180
181 181 for root, dirs, _files in os.walk(path):
182 182 recurse_dirs = []
183 183 for subdir in dirs:
184 184 # skip removed repos
185 185 if REMOVED_REPO_PAT.match(subdir):
186 186 continue
187 187
188 188 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
189 189 if subdir.startswith('.'):
190 190 continue
191 191
192 192 cur_path = os.path.join(root, subdir)
193 193 if isdir(cur_path, '.git'):
194 194 log.warning('ignoring non-bare Git repo: %s', cur_path)
195 195 continue
196 196
197 197 if (isdir(cur_path, '.hg') or
198 198 isdir(cur_path, '.svn') or
199 199 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
200 200 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
201 201
202 202 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
203 203 log.warning('ignoring repo path without access: %s', cur_path)
204 204 continue
205 205
206 206 if not os.access(cur_path, os.W_OK):
207 207 log.warning('repo path without write access: %s', cur_path)
208 208
209 209 try:
210 210 scm_info = get_scm(cur_path)
211 211 assert cur_path.startswith(path)
212 212 repo_path = cur_path[len(path) + 1:]
213 213 yield repo_path, scm_info
214 214 continue # no recursion
215 215 except VCSError:
216 216 # We should perhaps ignore such broken repos, but especially
217 217 # the bare git detection is unreliable so we dive into it
218 218 pass
219 219
220 220 recurse_dirs.append(subdir)
221 221
222 222 dirs[:] = recurse_dirs
223 223
224 224
225 225 def is_valid_repo_uri(repo_type, url, ui):
226 226 """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
227 227 if repo_type == 'hg':
228 228 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
229 229 if url.startswith('http') or url.startswith('ssh'):
230 230 # initially check if it's at least the proper URL
231 231 # or does it pass basic auth
232 232 MercurialRepository._check_url(url, ui)
233 233 elif url.startswith('svn+http'):
234 234 try:
235 235 from hgsubversion.svnrepo import svnremoterepo
236 236 except ImportError:
237 237 raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
238 238 'The "hgsubversion" library is missing'))
239 239 svnremoterepo(ui, url).svn.uuid
240 240 elif url.startswith('git+http'):
241 241 raise NotImplementedError()
242 242 else:
243 243 raise Exception('URI %s not allowed' % (url,))
244 244
245 245 elif repo_type == 'git':
246 246 from kallithea.lib.vcs.backends.git.repository import GitRepository
247 247 if url.startswith('http') or url.startswith('git'):
248 248 # initially check if it's at least the proper URL
249 249 # or does it pass basic auth
250 250 GitRepository._check_url(url)
251 251 elif url.startswith('svn+http'):
252 252 raise NotImplementedError()
253 253 elif url.startswith('hg+http'):
254 254 raise NotImplementedError()
255 255 else:
256 256 raise Exception('URI %s not allowed' % (url))
257 257
258 258
259 259 def is_valid_repo(repo_name, base_path, scm=None):
260 260 """
261 261 Returns True if given path is a valid repository False otherwise.
262 262 If scm param is given also compare if given scm is the same as expected
263 263 from scm parameter
264 264
265 265 :param repo_name:
266 266 :param base_path:
267 267 :param scm:
268 268
269 269 :return True: if given path is a valid repository
270 270 """
271 271 # TODO: paranoid security checks?
272 272 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
273 273
274 274 try:
275 275 scm_ = get_scm(full_path)
276 276 if scm:
277 277 return scm_[0] == scm
278 278 return True
279 279 except VCSError:
280 280 return False
281 281
282 282
283 283 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
284 284 """
285 285 Returns True if given path is a repository group False otherwise
286 286
287 287 :param repo_name:
288 288 :param base_path:
289 289 """
290 290 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
291 291
292 292 # check if it's not a repo
293 293 if is_valid_repo(repo_group_name, base_path):
294 294 return False
295 295
296 296 try:
297 297 # we need to check bare git repos at higher level
298 298 # since we might match branches/hooks/info/objects or possible
299 299 # other things inside bare git repo
300 300 get_scm(os.path.dirname(full_path))
301 301 return False
302 302 except VCSError:
303 303 pass
304 304
305 305 # check if it's a valid path
306 306 if skip_path_check or os.path.isdir(full_path):
307 307 return True
308 308
309 309 return False
310 310
311 311
312 312 # propagated from mercurial documentation
313 313 ui_sections = ['alias', 'auth',
314 314 'decode/encode', 'defaults',
315 315 'diff', 'email',
316 316 'extensions', 'format',
317 317 'merge-patterns', 'merge-tools',
318 318 'hooks', 'http_proxy',
319 319 'smtp', 'patch',
320 320 'paths', 'profiling',
321 321 'server', 'trusted',
322 322 'ui', 'web', ]
323 323
324 324
325 def make_ui(repo_path=None, clear_session=True):
325 def make_ui(repo_path=None):
326 326 """
327 327 Create an Mercurial 'ui' object based on database Ui settings, possibly
328 328 augmenting with content from a hgrc file.
329 329 """
330 330 baseui = ui.ui()
331 331
332 332 # clean the baseui object
333 333 baseui._ocfg = config.config()
334 334 baseui._ucfg = config.config()
335 335 baseui._tcfg = config.config()
336 336
337 337 sa = meta.Session()
338 338 for ui_ in sa.query(Ui).all():
339 339 if ui_.ui_active:
340 340 ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
341 341 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
342 342 ui_.ui_key, ui_val)
343 343 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
344 344 ui_val)
345 if clear_session:
346 meta.Session.remove()
347 345
348 346 # force set push_ssl requirement to False, Kallithea handles that
349 347 baseui.setconfig('web', 'push_ssl', False)
350 348 baseui.setconfig('web', 'allow_push', '*')
351 349 # prevent interactive questions for ssh password / passphrase
352 350 ssh = baseui.config('ui', 'ssh', default='ssh')
353 351 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
354 352 # push / pull hooks
355 353 baseui.setconfig('hooks', 'changegroup.kallithea_log_push_action', 'python:kallithea.lib.hooks.log_push_action')
356 354 baseui.setconfig('hooks', 'outgoing.kallithea_log_pull_action', 'python:kallithea.lib.hooks.log_pull_action')
357 355
358 356 if repo_path is not None:
359 357 hgrc_path = os.path.join(repo_path, '.hg', 'hgrc')
360 358 if os.path.isfile(hgrc_path):
361 359 log.debug('reading hgrc from %s', hgrc_path)
362 360 cfg = config.config()
363 361 cfg.read(hgrc_path)
364 362 for section in ui_sections:
365 363 for k, v in cfg.items(section):
366 364 log.debug('config from file: [%s] %s=%s', section, k, v)
367 365 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
368 366 else:
369 367 log.debug('hgrc file is not present at %s, skipping...', hgrc_path)
370 368
371 369 return baseui
372 370
373 371
374 372 def set_app_settings(config):
375 373 """
376 374 Updates app config with new settings from database
377 375
378 376 :param config:
379 377 """
380 378 hgsettings = Setting.get_app_settings()
381 379
382 380 for k, v in hgsettings.items():
383 381 config[k] = v
384 382
385 383
386 384 def set_vcs_config(config):
387 385 """
388 386 Patch VCS config with some Kallithea specific stuff
389 387
390 388 :param config: kallithea.CONFIG
391 389 """
392 390 from kallithea.lib.vcs import conf
393 391 from kallithea.lib.utils2 import aslist
394 392 conf.settings.BACKENDS = {
395 393 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
396 394 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
397 395 }
398 396
399 397 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
400 398 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
401 399 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
402 400 'utf-8'), sep=',')
403 401
404 402
405 403 def set_indexer_config(config):
406 404 """
407 405 Update Whoosh index mapping
408 406
409 407 :param config: kallithea.CONFIG
410 408 """
411 409 from kallithea.config import conf
412 410
413 411 log.debug('adding extra into INDEX_EXTENSIONS')
414 412 conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
415 413
416 414 log.debug('adding extra into INDEX_FILENAMES')
417 415 conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
418 416
419 417
420 418 def map_groups(path):
421 419 """
422 420 Given a full path to a repository, create all nested groups that this
423 421 repo is inside. This function creates parent-child relationships between
424 422 groups and creates default perms for all new groups.
425 423
426 424 :param paths: full path to repository
427 425 """
428 426 sa = meta.Session()
429 427 groups = path.split(Repository.url_sep())
430 428 parent = None
431 429 group = None
432 430
433 431 # last element is repo in nested groups structure
434 432 groups = groups[:-1]
435 433 rgm = RepoGroupModel()
436 434 owner = User.get_first_admin()
437 435 for lvl, group_name in enumerate(groups):
438 436 group_name = u'/'.join(groups[:lvl] + [group_name])
439 437 group = RepoGroup.get_by_group_name(group_name)
440 438 desc = '%s group' % group_name
441 439
442 440 # skip folders that are now removed repos
443 441 if REMOVED_REPO_PAT.match(group_name):
444 442 break
445 443
446 444 if group is None:
447 445 log.debug('creating group level: %s group_name: %s',
448 446 lvl, group_name)
449 447 group = RepoGroup(group_name, parent)
450 448 group.group_description = desc
451 449 group.owner = owner
452 450 sa.add(group)
453 451 rgm._create_default_perms(group)
454 452 sa.flush()
455 453
456 454 parent = group
457 455 return group
458 456
459 457
460 458 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
461 459 install_git_hooks=False, user=None, overwrite_git_hooks=False):
462 460 """
463 461 maps all repos given in initial_repo_list, non existing repositories
464 462 are created, if remove_obsolete is True it also check for db entries
465 463 that are not in initial_repo_list and removes them.
466 464
467 465 :param initial_repo_list: list of repositories found by scanning methods
468 466 :param remove_obsolete: check for obsolete entries in database
469 467 :param install_git_hooks: if this is True, also check and install git hook
470 468 for a repo if missing
471 469 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
472 470 that may be encountered (even if user-deployed)
473 471 """
474 472 from kallithea.model.repo import RepoModel
475 473 from kallithea.model.scm import ScmModel
476 474 sa = meta.Session()
477 475 repo_model = RepoModel()
478 476 if user is None:
479 477 user = User.get_first_admin()
480 478 added = []
481 479
482 480 # creation defaults
483 481 defs = Setting.get_default_repo_settings(strip_prefix=True)
484 482 enable_statistics = defs.get('repo_enable_statistics')
485 483 enable_downloads = defs.get('repo_enable_downloads')
486 484 private = defs.get('repo_private')
487 485
488 486 for name, repo in initial_repo_list.items():
489 487 group = map_groups(name)
490 488 unicode_name = safe_unicode(name)
491 489 db_repo = repo_model.get_by_repo_name(unicode_name)
492 490 # found repo that is on filesystem not in Kallithea database
493 491 if not db_repo:
494 492 log.info('repository %s not found, creating now', name)
495 493 added.append(name)
496 494 desc = (repo.description
497 495 if repo.description != 'unknown'
498 496 else '%s repository' % name)
499 497
500 498 new_repo = repo_model._create_repo(
501 499 repo_name=name,
502 500 repo_type=repo.alias,
503 501 description=desc,
504 502 repo_group=getattr(group, 'group_id', None),
505 503 owner=user,
506 504 enable_downloads=enable_downloads,
507 505 enable_statistics=enable_statistics,
508 506 private=private,
509 507 state=Repository.STATE_CREATED
510 508 )
511 509 sa.commit()
512 510 # we added that repo just now, and make sure it has githook
513 511 # installed, and updated server info
514 512 if new_repo.repo_type == 'git':
515 513 git_repo = new_repo.scm_instance
516 514 ScmModel().install_git_hooks(git_repo)
517 515 # update repository server-info
518 516 log.debug('Running update server info')
519 517 git_repo._update_server_info()
520 518 new_repo.update_changeset_cache()
521 519 elif install_git_hooks:
522 520 if db_repo.repo_type == 'git':
523 521 ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
524 522
525 523 removed = []
526 524 # remove from database those repositories that are not in the filesystem
527 525 unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
528 526 for repo in sa.query(Repository).all():
529 527 if repo.repo_name not in unicode_initial_repo_list:
530 528 if remove_obsolete:
531 529 log.debug("Removing non-existing repository found in db `%s`",
532 530 repo.repo_name)
533 531 try:
534 532 RepoModel().delete(repo, forks='detach', fs_remove=False)
535 533 sa.commit()
536 534 except Exception:
537 535 #don't hold further removals on error
538 536 log.error(traceback.format_exc())
539 537 sa.rollback()
540 538 removed.append(repo.repo_name)
541 539 return added, removed
542 540
543 541
544 542 def load_rcextensions(root_path):
545 543 import kallithea
546 544 from kallithea.config import conf
547 545
548 546 path = os.path.join(root_path, 'rcextensions', '__init__.py')
549 547 if os.path.isfile(path):
550 548 rcext = create_module('rc', path)
551 549 EXT = kallithea.EXTENSIONS = rcext
552 550 log.debug('Found rcextensions now loading %s...', rcext)
553 551
554 552 # Additional mappings that are not present in the pygments lexers
555 553 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
556 554
557 555 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
558 556
559 557 if getattr(EXT, 'INDEX_EXTENSIONS', []):
560 558 log.debug('settings custom INDEX_EXTENSIONS')
561 559 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
562 560
563 561 # ADDITIONAL MAPPINGS
564 562 log.debug('adding extra into INDEX_EXTENSIONS')
565 563 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
566 564
567 565 # auto check if the module is not missing any data, set to default if is
568 566 # this will help autoupdate new feature of rcext module
569 567 #from kallithea.config import rcextensions
570 568 #for k in dir(rcextensions):
571 569 # if not k.startswith('_') and not hasattr(EXT, k):
572 570 # setattr(EXT, k, getattr(rcextensions, k))
573 571
574 572
575 573 #==============================================================================
576 574 # MISC
577 575 #==============================================================================
578 576
579 577 git_req_ver = StrictVersion('1.7.4')
580 578
581 579 def check_git_version():
582 580 """
583 581 Checks what version of git is installed on the system, and raise a system exit
584 582 if it's too old for Kallithea to work properly.
585 583 """
586 584 from kallithea import BACKENDS
587 585 from kallithea.lib.vcs.backends.git.repository import GitRepository
588 586 from kallithea.lib.vcs.conf import settings
589 587
590 588 if 'git' not in BACKENDS:
591 589 return None
592 590
593 591 if not settings.GIT_EXECUTABLE_PATH:
594 592 log.warning('No git executable configured - check "git_path" in the ini file.')
595 593 return None
596 594
597 595 stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
598 596 _safe=True)
599 597
600 598 if stderr:
601 599 log.warning('Error/stderr from "%s --version": %r', settings.GIT_EXECUTABLE_PATH, stderr)
602 600
603 601 m = re.search(r"\d+.\d+.\d+", stdout)
604 602 if m:
605 603 ver = StrictVersion(m.group(0))
606 604 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
607 605 settings.GIT_EXECUTABLE_PATH, ver, stdout.strip())
608 606 if ver < git_req_ver:
609 607 log.error('Kallithea detected %s version %s, which is too old '
610 608 'for the system to function properly. '
611 609 'Please upgrade to version %s or later. '
612 610 'If you strictly need Mercurial repositories, you can '
613 611 'clear the "git_path" setting in the ini file.',
614 612 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
615 613 log.error("Terminating ...")
616 614 sys.exit(1)
617 615 else:
618 616 ver = StrictVersion('0.0.0')
619 617 log.warning('Error finding version number in "%s --version" stdout: %r',
620 618 settings.GIT_EXECUTABLE_PATH, stdout.strip())
621 619
622 620 return ver
623 621
624 622
625 623 #===============================================================================
626 624 # CACHE RELATED METHODS
627 625 #===============================================================================
628 626
629 627 # set cache regions for beaker so celery can utilise it
630 628 def setup_cache_regions(settings):
631 629 # Create dict with just beaker cache configs with prefix stripped
632 630 cache_settings = {'regions': None}
633 631 prefix = 'beaker.cache.'
634 632 for key in settings:
635 633 if key.startswith(prefix):
636 634 name = key[len(prefix):]
637 635 cache_settings[name] = settings[key]
638 636 # Find all regions, apply defaults, and apply to beaker
639 637 if cache_settings['regions']:
640 638 for region in cache_settings['regions'].split(','):
641 639 region = region.strip()
642 640 prefix = region + '.'
643 641 region_settings = {}
644 642 for key in cache_settings:
645 643 if key.startswith(prefix):
646 644 name = key[len(prefix):]
647 645 region_settings[name] = cache_settings[key]
648 646 region_settings.setdefault('expire',
649 647 cache_settings.get('expire', '60'))
650 648 region_settings.setdefault('lock_dir',
651 649 cache_settings.get('lock_dir'))
652 650 region_settings.setdefault('data_dir',
653 651 cache_settings.get('data_dir'))
654 652 region_settings.setdefault('type',
655 653 cache_settings.get('type', 'memory'))
656 654 beaker.cache.cache_regions[region] = region_settings
657 655
658 656
659 657 def conditional_cache(region, prefix, condition, func):
660 658 """
661 659
662 660 Conditional caching function use like::
663 661 def _c(arg):
664 662 #heavy computation function
665 663 return data
666 664
667 665 # depending from condition the compute is wrapped in cache or not
668 666 compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
669 667 return compute(arg)
670 668
671 669 :param region: name of cache region
672 670 :param prefix: cache region prefix
673 671 :param condition: condition for cache to be triggered, and return data cached
674 672 :param func: wrapped heavy function to compute
675 673
676 674 """
677 675 wrapped = func
678 676 if condition:
679 677 log.debug('conditional_cache: True, wrapping call of '
680 678 'func: %s into %s region cache' % (region, func))
681 679 wrapped = _cache_decorate((prefix,), None, None, region)(func)
682 680
683 681 return wrapped
@@ -1,2548 +1,2548 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.db
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 Database Models for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 08, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import collections
29 29 import datetime
30 30 import functools
31 31 import hashlib
32 32 import logging
33 33 import os
34 34 import time
35 35 import traceback
36 36
37 37 import ipaddr
38 38 import sqlalchemy
39 39 from beaker.cache import cache_region, region_invalidate
40 40 from sqlalchemy import *
41 41 from sqlalchemy.ext.hybrid import hybrid_property
42 42 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
43 43 from tg.i18n import lazy_ugettext as _
44 44 from webob.exc import HTTPNotFound
45 45
46 46 import kallithea
47 47 from kallithea.lib.caching_query import FromCache
48 48 from kallithea.lib.compat import json
49 49 from kallithea.lib.exceptions import DefaultUserException
50 50 from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
51 51 from kallithea.lib.vcs import get_backend
52 52 from kallithea.lib.vcs.backends.base import EmptyChangeset
53 53 from kallithea.lib.vcs.utils.helpers import get_scm
54 54 from kallithea.lib.vcs.utils.lazy import LazyProperty
55 55 from kallithea.model.meta import Base, Session
56 56
57 57
58 58 URL_SEP = '/'
59 59 log = logging.getLogger(__name__)
60 60
61 61 #==============================================================================
62 62 # BASE CLASSES
63 63 #==============================================================================
64 64
65 65 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
66 66
67 67
68 68 class BaseDbModel(object):
69 69 """
70 70 Base Model for all classes
71 71 """
72 72
73 73 @classmethod
74 74 def _get_keys(cls):
75 75 """return column names for this model """
76 76 return class_mapper(cls).c.keys()
77 77
78 78 def get_dict(self):
79 79 """
80 80 return dict with keys and values corresponding
81 81 to this model data """
82 82
83 83 d = {}
84 84 for k in self._get_keys():
85 85 d[k] = getattr(self, k)
86 86
87 87 # also use __json__() if present to get additional fields
88 88 _json_attr = getattr(self, '__json__', None)
89 89 if _json_attr:
90 90 # update with attributes from __json__
91 91 if callable(_json_attr):
92 92 _json_attr = _json_attr()
93 93 for k, val in _json_attr.iteritems():
94 94 d[k] = val
95 95 return d
96 96
97 97 def get_appstruct(self):
98 98 """return list with keys and values tuples corresponding
99 99 to this model data """
100 100
101 101 return [
102 102 (k, getattr(self, k))
103 103 for k in self._get_keys()
104 104 ]
105 105
106 106 def populate_obj(self, populate_dict):
107 107 """populate model with data from given populate_dict"""
108 108
109 109 for k in self._get_keys():
110 110 if k in populate_dict:
111 111 setattr(self, k, populate_dict[k])
112 112
113 113 @classmethod
114 114 def query(cls):
115 115 return Session().query(cls)
116 116
117 117 @classmethod
118 118 def get(cls, id_):
119 119 if id_:
120 120 return cls.query().get(id_)
121 121
122 122 @classmethod
123 123 def guess_instance(cls, value, callback=None):
124 124 """Haphazardly attempt to convert `value` to a `cls` instance.
125 125
126 126 If `value` is None or already a `cls` instance, return it. If `value`
127 127 is a number (or looks like one if you squint just right), assume it's
128 128 a database primary key and let SQLAlchemy sort things out. Otherwise,
129 129 fall back to resolving it using `callback` (if specified); this could
130 130 e.g. be a function that looks up instances by name (though that won't
131 131 work if the name begins with a digit). Otherwise, raise Exception.
132 132 """
133 133
134 134 if value is None:
135 135 return None
136 136 if isinstance(value, cls):
137 137 return value
138 138 if isinstance(value, (int, long)) or safe_str(value).isdigit():
139 139 return cls.get(value)
140 140 if callback is not None:
141 141 return callback(value)
142 142
143 143 raise Exception(
144 144 'given object must be int, long or Instance of %s '
145 145 'got %s, no callback provided' % (cls, type(value))
146 146 )
147 147
148 148 @classmethod
149 149 def get_or_404(cls, id_):
150 150 try:
151 151 id_ = int(id_)
152 152 except (TypeError, ValueError):
153 153 raise HTTPNotFound
154 154
155 155 res = cls.query().get(id_)
156 156 if res is None:
157 157 raise HTTPNotFound
158 158 return res
159 159
160 160 @classmethod
161 161 def delete(cls, id_):
162 162 obj = cls.query().get(id_)
163 163 Session().delete(obj)
164 164
165 165 def __repr__(self):
166 166 if hasattr(self, '__unicode__'):
167 167 # python repr needs to return str
168 168 try:
169 169 return safe_str(self.__unicode__())
170 170 except UnicodeDecodeError:
171 171 pass
172 172 return '<DB:%s>' % (self.__class__.__name__)
173 173
174 174
175 175 _table_args_default_dict = {'extend_existing': True,
176 176 'mysql_engine': 'InnoDB',
177 177 'mysql_charset': 'utf8',
178 178 'sqlite_autoincrement': True,
179 179 }
180 180
181 181 class Setting(Base, BaseDbModel):
182 182 __tablename__ = 'settings'
183 183 __table_args__ = (
184 184 _table_args_default_dict,
185 185 )
186 186
187 187 SETTINGS_TYPES = {
188 188 'str': safe_str,
189 189 'int': safe_int,
190 190 'unicode': safe_unicode,
191 191 'bool': str2bool,
192 192 'list': functools.partial(aslist, sep=',')
193 193 }
194 194 DEFAULT_UPDATE_URL = ''
195 195
196 196 app_settings_id = Column(Integer(), primary_key=True)
197 197 app_settings_name = Column(String(255), nullable=False, unique=True)
198 198 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
199 199 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
200 200
201 201 def __init__(self, key='', val='', type='unicode'):
202 202 self.app_settings_name = key
203 203 self.app_settings_value = val
204 204 self.app_settings_type = type
205 205
206 206 @validates('_app_settings_value')
207 207 def validate_settings_value(self, key, val):
208 208 assert type(val) == unicode
209 209 return val
210 210
211 211 @hybrid_property
212 212 def app_settings_value(self):
213 213 v = self._app_settings_value
214 214 _type = self.app_settings_type
215 215 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
216 216 return converter(v)
217 217
218 218 @app_settings_value.setter
219 219 def app_settings_value(self, val):
220 220 """
221 221 Setter that will always make sure we use unicode in app_settings_value
222 222
223 223 :param val:
224 224 """
225 225 self._app_settings_value = safe_unicode(val)
226 226
227 227 @hybrid_property
228 228 def app_settings_type(self):
229 229 return self._app_settings_type
230 230
231 231 @app_settings_type.setter
232 232 def app_settings_type(self, val):
233 233 if val not in self.SETTINGS_TYPES:
234 234 raise Exception('type must be one of %s got %s'
235 235 % (self.SETTINGS_TYPES.keys(), val))
236 236 self._app_settings_type = val
237 237
238 238 def __unicode__(self):
239 239 return u"<%s('%s:%s[%s]')>" % (
240 240 self.__class__.__name__,
241 241 self.app_settings_name, self.app_settings_value, self.app_settings_type
242 242 )
243 243
244 244 @classmethod
245 245 def get_by_name(cls, key):
246 246 return cls.query() \
247 247 .filter(cls.app_settings_name == key).scalar()
248 248
249 249 @classmethod
250 250 def get_by_name_or_create(cls, key, val='', type='unicode'):
251 251 res = cls.get_by_name(key)
252 252 if res is None:
253 253 res = cls(key, val, type)
254 254 return res
255 255
256 256 @classmethod
257 257 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
258 258 """
259 259 Creates or updates Kallithea setting. If updates are triggered, it will only
260 260 update parameters that are explicitly set. Optional instance will be skipped.
261 261
262 262 :param key:
263 263 :param val:
264 264 :param type:
265 265 :return:
266 266 """
267 267 res = cls.get_by_name(key)
268 268 if res is None:
269 269 val = Optional.extract(val)
270 270 type = Optional.extract(type)
271 271 res = cls(key, val, type)
272 272 Session().add(res)
273 273 else:
274 274 res.app_settings_name = key
275 275 if not isinstance(val, Optional):
276 276 # update if set
277 277 res.app_settings_value = val
278 278 if not isinstance(type, Optional):
279 279 # update if set
280 280 res.app_settings_type = type
281 281 return res
282 282
283 283 @classmethod
284 284 def get_app_settings(cls, cache=False):
285 285
286 286 ret = cls.query()
287 287
288 288 if cache:
289 289 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
290 290
291 291 if ret is None:
292 292 raise Exception('Could not get application settings !')
293 293 settings = {}
294 294 for each in ret:
295 295 settings[each.app_settings_name] = \
296 296 each.app_settings_value
297 297
298 298 return settings
299 299
300 300 @classmethod
301 301 def get_auth_settings(cls, cache=False):
302 302 ret = cls.query() \
303 303 .filter(cls.app_settings_name.startswith('auth_')).all()
304 304 fd = {}
305 305 for row in ret:
306 306 fd[row.app_settings_name] = row.app_settings_value
307 307 return fd
308 308
309 309 @classmethod
310 310 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
311 311 ret = cls.query() \
312 312 .filter(cls.app_settings_name.startswith('default_')).all()
313 313 fd = {}
314 314 for row in ret:
315 315 key = row.app_settings_name
316 316 if strip_prefix:
317 317 key = remove_prefix(key, prefix='default_')
318 318 fd.update({key: row.app_settings_value})
319 319
320 320 return fd
321 321
322 322 @classmethod
323 323 def get_server_info(cls):
324 324 import pkg_resources
325 325 import platform
326 326 from kallithea.lib.utils import check_git_version
327 327 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
328 328 info = {
329 329 'modules': sorted(mods, key=lambda k: k[0].lower()),
330 330 'py_version': platform.python_version(),
331 331 'platform': safe_unicode(platform.platform()),
332 332 'kallithea_version': kallithea.__version__,
333 333 'git_version': safe_unicode(check_git_version()),
334 334 'git_path': kallithea.CONFIG.get('git_path')
335 335 }
336 336 return info
337 337
338 338
339 339 class Ui(Base, BaseDbModel):
340 340 __tablename__ = 'ui'
341 341 __table_args__ = (
342 342 # FIXME: ui_key as key is wrong and should be removed when the corresponding
343 343 # Ui.get_by_key has been replaced by the composite key
344 344 UniqueConstraint('ui_key'),
345 345 UniqueConstraint('ui_section', 'ui_key'),
346 346 _table_args_default_dict,
347 347 )
348 348
349 349 HOOK_UPDATE = 'changegroup.update'
350 350 HOOK_REPO_SIZE = 'changegroup.repo_size'
351 351
352 352 ui_id = Column(Integer(), primary_key=True)
353 353 ui_section = Column(String(255), nullable=False)
354 354 ui_key = Column(String(255), nullable=False)
355 355 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
356 356 ui_active = Column(Boolean(), nullable=False, default=True)
357 357
358 358 @classmethod
359 359 def get_by_key(cls, section, key):
360 360 """ Return specified Ui object, or None if not found. """
361 361 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
362 362
363 363 @classmethod
364 364 def get_or_create(cls, section, key):
365 365 """ Return specified Ui object, creating it if necessary. """
366 366 setting = cls.get_by_key(section, key)
367 367 if setting is None:
368 368 setting = cls(ui_section=section, ui_key=key)
369 369 Session().add(setting)
370 370 return setting
371 371
372 372 @classmethod
373 373 def get_builtin_hooks(cls):
374 374 q = cls.query()
375 375 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
376 376 q = q.filter(cls.ui_section == 'hooks')
377 377 return q.all()
378 378
379 379 @classmethod
380 380 def get_custom_hooks(cls):
381 381 q = cls.query()
382 382 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
383 383 q = q.filter(cls.ui_section == 'hooks')
384 384 return q.all()
385 385
386 386 @classmethod
387 387 def get_repos_location(cls):
388 388 return cls.get_by_key('paths', '/').ui_value
389 389
390 390 @classmethod
391 391 def create_or_update_hook(cls, key, val):
392 392 new_ui = cls.get_or_create('hooks', key)
393 393 new_ui.ui_active = True
394 394 new_ui.ui_value = val
395 395
396 396 def __repr__(self):
397 397 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
398 398 self.ui_key, self.ui_value)
399 399
400 400
401 401 class User(Base, BaseDbModel):
402 402 __tablename__ = 'users'
403 403 __table_args__ = (
404 404 Index('u_username_idx', 'username'),
405 405 Index('u_email_idx', 'email'),
406 406 _table_args_default_dict,
407 407 )
408 408
409 409 DEFAULT_USER = 'default'
410 410 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
411 411 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
412 412 DEFAULT_AUTH_TYPE = 'internal'
413 413
414 414 user_id = Column(Integer(), primary_key=True)
415 415 username = Column(String(255), nullable=False, unique=True)
416 416 password = Column(String(255), nullable=False)
417 417 active = Column(Boolean(), nullable=False, default=True)
418 418 admin = Column(Boolean(), nullable=False, default=False)
419 419 name = Column("firstname", Unicode(255), nullable=False)
420 420 lastname = Column(Unicode(255), nullable=False)
421 421 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
422 422 last_login = Column(DateTime(timezone=False), nullable=True)
423 423 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
424 424 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
425 425 api_key = Column(String(255), nullable=False)
426 426 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
427 427 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
428 428
429 429 user_log = relationship('UserLog')
430 430 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
431 431
432 432 repositories = relationship('Repository')
433 433 repo_groups = relationship('RepoGroup')
434 434 user_groups = relationship('UserGroup')
435 435 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
436 436 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
437 437
438 438 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
439 439 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
440 440
441 441 group_member = relationship('UserGroupMember', cascade='all')
442 442
443 443 # comments created by this user
444 444 user_comments = relationship('ChangesetComment', cascade='all')
445 445 # extra emails for this user
446 446 user_emails = relationship('UserEmailMap', cascade='all')
447 447 # extra API keys
448 448 user_api_keys = relationship('UserApiKeys', cascade='all')
449 449 ssh_keys = relationship('UserSshKeys', cascade='all')
450 450
451 451 @hybrid_property
452 452 def email(self):
453 453 return self._email
454 454
455 455 @email.setter
456 456 def email(self, val):
457 457 self._email = val.lower() if val else None
458 458
459 459 @property
460 460 def firstname(self):
461 461 # alias for future
462 462 return self.name
463 463
464 464 @property
465 465 def emails(self):
466 466 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
467 467 return [self.email] + [x.email for x in other]
468 468
469 469 @property
470 470 def api_keys(self):
471 471 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
472 472 return [self.api_key] + [x.api_key for x in other]
473 473
474 474 @property
475 475 def ip_addresses(self):
476 476 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
477 477 return [x.ip_addr for x in ret]
478 478
479 479 @property
480 480 def full_name(self):
481 481 return '%s %s' % (self.firstname, self.lastname)
482 482
483 483 @property
484 484 def full_name_or_username(self):
485 485 """
486 486 Show full name.
487 487 If full name is not set, fall back to username.
488 488 """
489 489 return ('%s %s' % (self.firstname, self.lastname)
490 490 if (self.firstname and self.lastname) else self.username)
491 491
492 492 @property
493 493 def full_name_and_username(self):
494 494 """
495 495 Show full name and username as 'Firstname Lastname (username)'.
496 496 If full name is not set, fall back to username.
497 497 """
498 498 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
499 499 if (self.firstname and self.lastname) else self.username)
500 500
501 501 @property
502 502 def full_contact(self):
503 503 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
504 504
505 505 @property
506 506 def short_contact(self):
507 507 return '%s %s' % (self.firstname, self.lastname)
508 508
509 509 @property
510 510 def is_admin(self):
511 511 return self.admin
512 512
513 513 @hybrid_property
514 514 def is_default_user(self):
515 515 return self.username == User.DEFAULT_USER
516 516
517 517 @hybrid_property
518 518 def user_data(self):
519 519 if not self._user_data:
520 520 return {}
521 521
522 522 try:
523 523 return json.loads(self._user_data)
524 524 except TypeError:
525 525 return {}
526 526
527 527 @user_data.setter
528 528 def user_data(self, val):
529 529 try:
530 530 self._user_data = json.dumps(val)
531 531 except Exception:
532 532 log.error(traceback.format_exc())
533 533
534 534 def __unicode__(self):
535 535 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
536 536 self.user_id, self.username)
537 537
538 538 @classmethod
539 539 def guess_instance(cls, value):
540 540 return super(User, cls).guess_instance(value, User.get_by_username)
541 541
542 542 @classmethod
543 543 def get_or_404(cls, id_, allow_default=True):
544 544 '''
545 545 Overridden version of BaseDbModel.get_or_404, with an extra check on
546 546 the default user.
547 547 '''
548 548 user = super(User, cls).get_or_404(id_)
549 549 if not allow_default and user.is_default_user:
550 550 raise DefaultUserException()
551 551 return user
552 552
553 553 @classmethod
554 554 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
555 555 """
556 556 For anything that looks like an email address, look up by the email address (matching
557 557 case insensitively).
558 558 For anything else, try to look up by the user name.
559 559
560 560 This assumes no normal username can have '@' symbol.
561 561 """
562 562 if '@' in username_or_email:
563 563 return User.get_by_email(username_or_email, cache=cache)
564 564 else:
565 565 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
566 566
567 567 @classmethod
568 568 def get_by_username(cls, username, case_insensitive=False, cache=False):
569 569 if case_insensitive:
570 570 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
571 571 else:
572 572 q = cls.query().filter(cls.username == username)
573 573
574 574 if cache:
575 575 q = q.options(FromCache(
576 576 "sql_cache_short",
577 577 "get_user_%s" % _hash_key(username)
578 578 )
579 579 )
580 580 return q.scalar()
581 581
582 582 @classmethod
583 583 def get_by_api_key(cls, api_key, cache=False, fallback=True):
584 584 if len(api_key) != 40 or not api_key.isalnum():
585 585 return None
586 586
587 587 q = cls.query().filter(cls.api_key == api_key)
588 588
589 589 if cache:
590 590 q = q.options(FromCache("sql_cache_short",
591 591 "get_api_key_%s" % api_key))
592 592 res = q.scalar()
593 593
594 594 if fallback and not res:
595 595 # fallback to additional keys
596 596 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
597 597 if _res:
598 598 res = _res.user
599 599 if res is None or not res.active or res.is_default_user:
600 600 return None
601 601 return res
602 602
603 603 @classmethod
604 604 def get_by_email(cls, email, cache=False):
605 605 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
606 606
607 607 if cache:
608 608 q = q.options(FromCache("sql_cache_short",
609 609 "get_email_key_%s" % email))
610 610
611 611 ret = q.scalar()
612 612 if ret is None:
613 613 q = UserEmailMap.query()
614 614 # try fetching in alternate email map
615 615 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
616 616 q = q.options(joinedload(UserEmailMap.user))
617 617 if cache:
618 618 q = q.options(FromCache("sql_cache_short",
619 619 "get_email_map_key_%s" % email))
620 620 ret = getattr(q.scalar(), 'user', None)
621 621
622 622 return ret
623 623
624 624 @classmethod
625 625 def get_from_cs_author(cls, author):
626 626 """
627 627 Tries to get User objects out of commit author string
628 628
629 629 :param author:
630 630 """
631 631 from kallithea.lib.helpers import email, author_name
632 632 # Valid email in the attribute passed, see if they're in the system
633 633 _email = email(author)
634 634 if _email:
635 635 user = cls.get_by_email(_email)
636 636 if user is not None:
637 637 return user
638 638 # Maybe we can match by username?
639 639 _author = author_name(author)
640 640 user = cls.get_by_username(_author, case_insensitive=True)
641 641 if user is not None:
642 642 return user
643 643
644 644 def update_lastlogin(self):
645 645 """Update user lastlogin"""
646 646 self.last_login = datetime.datetime.now()
647 647 log.debug('updated user %s lastlogin', self.username)
648 648
649 649 @classmethod
650 650 def get_first_admin(cls):
651 651 user = User.query().filter(User.admin == True).first()
652 652 if user is None:
653 653 raise Exception('Missing administrative account!')
654 654 return user
655 655
656 656 @classmethod
657 657 def get_default_user(cls, cache=False):
658 658 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
659 659 if user is None:
660 660 raise Exception('Missing default account!')
661 661 return user
662 662
663 663 def get_api_data(self, details=False):
664 664 """
665 665 Common function for generating user related data for API
666 666 """
667 667 user = self
668 668 data = dict(
669 669 user_id=user.user_id,
670 670 username=user.username,
671 671 firstname=user.name,
672 672 lastname=user.lastname,
673 673 email=user.email,
674 674 emails=user.emails,
675 675 active=user.active,
676 676 admin=user.admin,
677 677 )
678 678 if details:
679 679 data.update(dict(
680 680 extern_type=user.extern_type,
681 681 extern_name=user.extern_name,
682 682 api_key=user.api_key,
683 683 api_keys=user.api_keys,
684 684 last_login=user.last_login,
685 685 ip_addresses=user.ip_addresses
686 686 ))
687 687 return data
688 688
689 689 def __json__(self):
690 690 data = dict(
691 691 full_name=self.full_name,
692 692 full_name_or_username=self.full_name_or_username,
693 693 short_contact=self.short_contact,
694 694 full_contact=self.full_contact
695 695 )
696 696 data.update(self.get_api_data())
697 697 return data
698 698
699 699
700 700 class UserApiKeys(Base, BaseDbModel):
701 701 __tablename__ = 'user_api_keys'
702 702 __table_args__ = (
703 703 Index('uak_api_key_idx', 'api_key'),
704 704 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
705 705 _table_args_default_dict,
706 706 )
707 707
708 708 user_api_key_id = Column(Integer(), primary_key=True)
709 709 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
710 710 api_key = Column(String(255), nullable=False, unique=True)
711 711 description = Column(UnicodeText(), nullable=False)
712 712 expires = Column(Float(53), nullable=False)
713 713 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
714 714
715 715 user = relationship('User')
716 716
717 717 @hybrid_property
718 718 def is_expired(self):
719 719 return (self.expires != -1) & (time.time() > self.expires)
720 720
721 721
722 722 class UserEmailMap(Base, BaseDbModel):
723 723 __tablename__ = 'user_email_map'
724 724 __table_args__ = (
725 725 Index('uem_email_idx', 'email'),
726 726 _table_args_default_dict,
727 727 )
728 728
729 729 email_id = Column(Integer(), primary_key=True)
730 730 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
731 731 _email = Column("email", String(255), nullable=False, unique=True)
732 732 user = relationship('User')
733 733
734 734 @validates('_email')
735 735 def validate_email(self, key, email):
736 736 # check if this email is not main one
737 737 main_email = Session().query(User).filter(User.email == email).scalar()
738 738 if main_email is not None:
739 739 raise AttributeError('email %s is present is user table' % email)
740 740 return email
741 741
742 742 @hybrid_property
743 743 def email(self):
744 744 return self._email
745 745
746 746 @email.setter
747 747 def email(self, val):
748 748 self._email = val.lower() if val else None
749 749
750 750
751 751 class UserIpMap(Base, BaseDbModel):
752 752 __tablename__ = 'user_ip_map'
753 753 __table_args__ = (
754 754 UniqueConstraint('user_id', 'ip_addr'),
755 755 _table_args_default_dict,
756 756 )
757 757
758 758 ip_id = Column(Integer(), primary_key=True)
759 759 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
760 760 ip_addr = Column(String(255), nullable=False)
761 761 active = Column(Boolean(), nullable=False, default=True)
762 762 user = relationship('User')
763 763
764 764 @classmethod
765 765 def _get_ip_range(cls, ip_addr):
766 766 net = ipaddr.IPNetwork(address=ip_addr)
767 767 return [str(net.network), str(net.broadcast)]
768 768
769 769 def __json__(self):
770 770 return dict(
771 771 ip_addr=self.ip_addr,
772 772 ip_range=self._get_ip_range(self.ip_addr)
773 773 )
774 774
775 775 def __unicode__(self):
776 776 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
777 777 self.user_id, self.ip_addr)
778 778
779 779
780 780 class UserLog(Base, BaseDbModel):
781 781 __tablename__ = 'user_logs'
782 782 __table_args__ = (
783 783 _table_args_default_dict,
784 784 )
785 785
786 786 user_log_id = Column(Integer(), primary_key=True)
787 787 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
788 788 username = Column(String(255), nullable=False)
789 789 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
790 790 repository_name = Column(Unicode(255), nullable=False)
791 791 user_ip = Column(String(255), nullable=True)
792 792 action = Column(UnicodeText(), nullable=False)
793 793 action_date = Column(DateTime(timezone=False), nullable=False)
794 794
795 795 def __unicode__(self):
796 796 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
797 797 self.repository_name,
798 798 self.action)
799 799
800 800 @property
801 801 def action_as_day(self):
802 802 return datetime.date(*self.action_date.timetuple()[:3])
803 803
804 804 user = relationship('User')
805 805 repository = relationship('Repository', cascade='')
806 806
807 807
808 808 class UserGroup(Base, BaseDbModel):
809 809 __tablename__ = 'users_groups'
810 810 __table_args__ = (
811 811 _table_args_default_dict,
812 812 )
813 813
814 814 users_group_id = Column(Integer(), primary_key=True)
815 815 users_group_name = Column(Unicode(255), nullable=False, unique=True)
816 816 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
817 817 users_group_active = Column(Boolean(), nullable=False)
818 818 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
819 819 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
820 820 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
821 821
822 822 members = relationship('UserGroupMember', cascade="all, delete-orphan")
823 823 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
824 824 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
825 825 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
826 826 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
827 827 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
828 828
829 829 owner = relationship('User')
830 830
831 831 @hybrid_property
832 832 def group_data(self):
833 833 if not self._group_data:
834 834 return {}
835 835
836 836 try:
837 837 return json.loads(self._group_data)
838 838 except TypeError:
839 839 return {}
840 840
841 841 @group_data.setter
842 842 def group_data(self, val):
843 843 try:
844 844 self._group_data = json.dumps(val)
845 845 except Exception:
846 846 log.error(traceback.format_exc())
847 847
848 848 def __unicode__(self):
849 849 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
850 850 self.users_group_id,
851 851 self.users_group_name)
852 852
853 853 @classmethod
854 854 def guess_instance(cls, value):
855 855 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
856 856
857 857 @classmethod
858 858 def get_by_group_name(cls, group_name, cache=False,
859 859 case_insensitive=False):
860 860 if case_insensitive:
861 861 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
862 862 else:
863 863 q = cls.query().filter(cls.users_group_name == group_name)
864 864 if cache:
865 865 q = q.options(FromCache(
866 866 "sql_cache_short",
867 867 "get_group_%s" % _hash_key(group_name)
868 868 )
869 869 )
870 870 return q.scalar()
871 871
872 872 @classmethod
873 873 def get(cls, user_group_id, cache=False):
874 874 user_group = cls.query()
875 875 if cache:
876 876 user_group = user_group.options(FromCache("sql_cache_short",
877 877 "get_users_group_%s" % user_group_id))
878 878 return user_group.get(user_group_id)
879 879
880 880 def get_api_data(self, with_members=True):
881 881 user_group = self
882 882
883 883 data = dict(
884 884 users_group_id=user_group.users_group_id,
885 885 group_name=user_group.users_group_name,
886 886 group_description=user_group.user_group_description,
887 887 active=user_group.users_group_active,
888 888 owner=user_group.owner.username,
889 889 )
890 890 if with_members:
891 891 data['members'] = [
892 892 ugm.user.get_api_data()
893 893 for ugm in user_group.members
894 894 ]
895 895
896 896 return data
897 897
898 898
899 899 class UserGroupMember(Base, BaseDbModel):
900 900 __tablename__ = 'users_groups_members'
901 901 __table_args__ = (
902 902 _table_args_default_dict,
903 903 )
904 904
905 905 users_group_member_id = Column(Integer(), primary_key=True)
906 906 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
907 907 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
908 908
909 909 user = relationship('User')
910 910 users_group = relationship('UserGroup')
911 911
912 912 def __init__(self, gr_id='', u_id=''):
913 913 self.users_group_id = gr_id
914 914 self.user_id = u_id
915 915
916 916
917 917 class RepositoryField(Base, BaseDbModel):
918 918 __tablename__ = 'repositories_fields'
919 919 __table_args__ = (
920 920 UniqueConstraint('repository_id', 'field_key'), # no-multi field
921 921 _table_args_default_dict,
922 922 )
923 923
924 924 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
925 925
926 926 repo_field_id = Column(Integer(), primary_key=True)
927 927 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
928 928 field_key = Column(String(250), nullable=False)
929 929 field_label = Column(String(1024), nullable=False)
930 930 field_value = Column(String(10000), nullable=False)
931 931 field_desc = Column(String(1024), nullable=False)
932 932 field_type = Column(String(255), nullable=False)
933 933 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
934 934
935 935 repository = relationship('Repository')
936 936
937 937 @property
938 938 def field_key_prefixed(self):
939 939 return 'ex_%s' % self.field_key
940 940
941 941 @classmethod
942 942 def un_prefix_key(cls, key):
943 943 if key.startswith(cls.PREFIX):
944 944 return key[len(cls.PREFIX):]
945 945 return key
946 946
947 947 @classmethod
948 948 def get_by_key_name(cls, key, repo):
949 949 row = cls.query() \
950 950 .filter(cls.repository == repo) \
951 951 .filter(cls.field_key == key).scalar()
952 952 return row
953 953
954 954
955 955 class Repository(Base, BaseDbModel):
956 956 __tablename__ = 'repositories'
957 957 __table_args__ = (
958 958 Index('r_repo_name_idx', 'repo_name'),
959 959 _table_args_default_dict,
960 960 )
961 961
962 962 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
963 963 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
964 964
965 965 STATE_CREATED = u'repo_state_created'
966 966 STATE_PENDING = u'repo_state_pending'
967 967 STATE_ERROR = u'repo_state_error'
968 968
969 969 repo_id = Column(Integer(), primary_key=True)
970 970 repo_name = Column(Unicode(255), nullable=False, unique=True)
971 971 repo_state = Column(String(255), nullable=False)
972 972
973 973 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
974 974 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
975 975 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
976 976 private = Column(Boolean(), nullable=False)
977 977 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
978 978 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
979 979 description = Column(Unicode(10000), nullable=False)
980 980 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
981 981 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
982 982 _landing_revision = Column("landing_revision", String(255), nullable=False)
983 983 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
984 984
985 985 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
986 986 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
987 987
988 988 owner = relationship('User')
989 989 fork = relationship('Repository', remote_side=repo_id)
990 990 group = relationship('RepoGroup')
991 991 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
992 992 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
993 993 stats = relationship('Statistics', cascade='all', uselist=False)
994 994
995 995 followers = relationship('UserFollowing',
996 996 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
997 997 cascade='all')
998 998 extra_fields = relationship('RepositoryField',
999 999 cascade="all, delete-orphan")
1000 1000
1001 1001 logs = relationship('UserLog')
1002 1002 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1003 1003
1004 1004 pull_requests_org = relationship('PullRequest',
1005 1005 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1006 1006 cascade="all, delete-orphan")
1007 1007
1008 1008 pull_requests_other = relationship('PullRequest',
1009 1009 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1010 1010 cascade="all, delete-orphan")
1011 1011
1012 1012 def __unicode__(self):
1013 1013 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1014 1014 safe_unicode(self.repo_name))
1015 1015
1016 1016 @hybrid_property
1017 1017 def landing_rev(self):
1018 1018 # always should return [rev_type, rev]
1019 1019 if self._landing_revision:
1020 1020 _rev_info = self._landing_revision.split(':')
1021 1021 if len(_rev_info) < 2:
1022 1022 _rev_info.insert(0, 'rev')
1023 1023 return [_rev_info[0], _rev_info[1]]
1024 1024 return [None, None]
1025 1025
1026 1026 @landing_rev.setter
1027 1027 def landing_rev(self, val):
1028 1028 if ':' not in val:
1029 1029 raise ValueError('value must be delimited with `:` and consist '
1030 1030 'of <rev_type>:<rev>, got %s instead' % val)
1031 1031 self._landing_revision = val
1032 1032
1033 1033 @hybrid_property
1034 1034 def changeset_cache(self):
1035 1035 try:
1036 1036 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1037 1037 cs_cache['raw_id'] # verify data, raise exception on error
1038 1038 return cs_cache
1039 1039 except (TypeError, KeyError, ValueError):
1040 1040 return EmptyChangeset().__json__()
1041 1041
1042 1042 @changeset_cache.setter
1043 1043 def changeset_cache(self, val):
1044 1044 try:
1045 1045 self._changeset_cache = json.dumps(val)
1046 1046 except Exception:
1047 1047 log.error(traceback.format_exc())
1048 1048
1049 1049 @classmethod
1050 1050 def query(cls, sorted=False):
1051 1051 """Add Repository-specific helpers for common query constructs.
1052 1052
1053 1053 sorted: if True, apply the default ordering (name, case insensitive).
1054 1054 """
1055 1055 q = super(Repository, cls).query()
1056 1056
1057 1057 if sorted:
1058 1058 q = q.order_by(func.lower(Repository.repo_name))
1059 1059
1060 1060 return q
1061 1061
1062 1062 @classmethod
1063 1063 def url_sep(cls):
1064 1064 return URL_SEP
1065 1065
1066 1066 @classmethod
1067 1067 def normalize_repo_name(cls, repo_name):
1068 1068 """
1069 1069 Normalizes os specific repo_name to the format internally stored inside
1070 1070 database using URL_SEP
1071 1071
1072 1072 :param cls:
1073 1073 :param repo_name:
1074 1074 """
1075 1075 return cls.url_sep().join(repo_name.split(os.sep))
1076 1076
1077 1077 @classmethod
1078 1078 def guess_instance(cls, value):
1079 1079 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1080 1080
1081 1081 @classmethod
1082 1082 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1083 1083 """Get the repo, defaulting to database case sensitivity.
1084 1084 case_insensitive will be slower and should only be specified if necessary."""
1085 1085 if case_insensitive:
1086 1086 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1087 1087 else:
1088 1088 q = Session().query(cls).filter(cls.repo_name == repo_name)
1089 1089 q = q.options(joinedload(Repository.fork)) \
1090 1090 .options(joinedload(Repository.owner)) \
1091 1091 .options(joinedload(Repository.group))
1092 1092 return q.scalar()
1093 1093
1094 1094 @classmethod
1095 1095 def get_by_full_path(cls, repo_full_path):
1096 1096 base_full_path = os.path.realpath(cls.base_path())
1097 1097 repo_full_path = os.path.realpath(repo_full_path)
1098 1098 assert repo_full_path.startswith(base_full_path + os.path.sep)
1099 1099 repo_name = repo_full_path[len(base_full_path) + 1:]
1100 1100 repo_name = cls.normalize_repo_name(repo_name)
1101 1101 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1102 1102
1103 1103 @classmethod
1104 1104 def get_repo_forks(cls, repo_id):
1105 1105 return cls.query().filter(Repository.fork_id == repo_id)
1106 1106
1107 1107 @classmethod
1108 1108 def base_path(cls):
1109 1109 """
1110 1110 Returns base path where all repos are stored
1111 1111
1112 1112 :param cls:
1113 1113 """
1114 1114 q = Session().query(Ui) \
1115 1115 .filter(Ui.ui_key == cls.url_sep())
1116 1116 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1117 1117 return q.one().ui_value
1118 1118
1119 1119 @property
1120 1120 def forks(self):
1121 1121 """
1122 1122 Return forks of this repo
1123 1123 """
1124 1124 return Repository.get_repo_forks(self.repo_id)
1125 1125
1126 1126 @property
1127 1127 def parent(self):
1128 1128 """
1129 1129 Returns fork parent
1130 1130 """
1131 1131 return self.fork
1132 1132
1133 1133 @property
1134 1134 def just_name(self):
1135 1135 return self.repo_name.split(Repository.url_sep())[-1]
1136 1136
1137 1137 @property
1138 1138 def groups_with_parents(self):
1139 1139 groups = []
1140 1140 group = self.group
1141 1141 while group is not None:
1142 1142 groups.append(group)
1143 1143 group = group.parent_group
1144 1144 assert group not in groups, group # avoid recursion on bad db content
1145 1145 groups.reverse()
1146 1146 return groups
1147 1147
1148 1148 @LazyProperty
1149 1149 def repo_path(self):
1150 1150 """
1151 1151 Returns base full path for that repository means where it actually
1152 1152 exists on a filesystem
1153 1153 """
1154 1154 q = Session().query(Ui).filter(Ui.ui_key ==
1155 1155 Repository.url_sep())
1156 1156 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1157 1157 return q.one().ui_value
1158 1158
1159 1159 @property
1160 1160 def repo_full_path(self):
1161 1161 p = [self.repo_path]
1162 1162 # we need to split the name by / since this is how we store the
1163 1163 # names in the database, but that eventually needs to be converted
1164 1164 # into a valid system path
1165 1165 p += self.repo_name.split(Repository.url_sep())
1166 1166 return os.path.join(*map(safe_unicode, p))
1167 1167
1168 1168 @property
1169 1169 def cache_keys(self):
1170 1170 """
1171 1171 Returns associated cache keys for that repo
1172 1172 """
1173 1173 return CacheInvalidation.query() \
1174 1174 .filter(CacheInvalidation.cache_args == self.repo_name) \
1175 1175 .order_by(CacheInvalidation.cache_key) \
1176 1176 .all()
1177 1177
1178 1178 def get_new_name(self, repo_name):
1179 1179 """
1180 1180 returns new full repository name based on assigned group and new new
1181 1181
1182 1182 :param group_name:
1183 1183 """
1184 1184 path_prefix = self.group.full_path_splitted if self.group else []
1185 1185 return Repository.url_sep().join(path_prefix + [repo_name])
1186 1186
1187 1187 @property
1188 1188 def _ui(self):
1189 1189 """
1190 1190 Creates an db based ui object for this repository
1191 1191 """
1192 1192 from kallithea.lib.utils import make_ui
1193 return make_ui(clear_session=False)
1193 return make_ui()
1194 1194
1195 1195 @classmethod
1196 1196 def is_valid(cls, repo_name):
1197 1197 """
1198 1198 returns True if given repo name is a valid filesystem repository
1199 1199
1200 1200 :param cls:
1201 1201 :param repo_name:
1202 1202 """
1203 1203 from kallithea.lib.utils import is_valid_repo
1204 1204
1205 1205 return is_valid_repo(repo_name, cls.base_path())
1206 1206
1207 1207 def get_api_data(self, with_revision_names=False,
1208 1208 with_pullrequests=False):
1209 1209 """
1210 1210 Common function for generating repo api data.
1211 1211 Optionally, also return tags, branches, bookmarks and PRs.
1212 1212 """
1213 1213 repo = self
1214 1214 data = dict(
1215 1215 repo_id=repo.repo_id,
1216 1216 repo_name=repo.repo_name,
1217 1217 repo_type=repo.repo_type,
1218 1218 clone_uri=repo.clone_uri,
1219 1219 private=repo.private,
1220 1220 created_on=repo.created_on,
1221 1221 description=repo.description,
1222 1222 landing_rev=repo.landing_rev,
1223 1223 owner=repo.owner.username,
1224 1224 fork_of=repo.fork.repo_name if repo.fork else None,
1225 1225 enable_statistics=repo.enable_statistics,
1226 1226 enable_downloads=repo.enable_downloads,
1227 1227 last_changeset=repo.changeset_cache,
1228 1228 )
1229 1229 if with_revision_names:
1230 1230 scm_repo = repo.scm_instance_no_cache()
1231 1231 data.update(dict(
1232 1232 tags=scm_repo.tags,
1233 1233 branches=scm_repo.branches,
1234 1234 bookmarks=scm_repo.bookmarks,
1235 1235 ))
1236 1236 if with_pullrequests:
1237 1237 data['pull_requests'] = repo.pull_requests_other
1238 1238 rc_config = Setting.get_app_settings()
1239 1239 repository_fields = str2bool(rc_config.get('repository_fields'))
1240 1240 if repository_fields:
1241 1241 for f in self.extra_fields:
1242 1242 data[f.field_key_prefixed] = f.field_value
1243 1243
1244 1244 return data
1245 1245
1246 1246 @property
1247 1247 def last_db_change(self):
1248 1248 return self.updated_on
1249 1249
1250 1250 @property
1251 1251 def clone_uri_hidden(self):
1252 1252 clone_uri = self.clone_uri
1253 1253 if clone_uri:
1254 1254 import urlobject
1255 1255 url_obj = urlobject.URLObject(self.clone_uri)
1256 1256 if url_obj.password:
1257 1257 clone_uri = url_obj.with_password('*****')
1258 1258 return clone_uri
1259 1259
1260 1260 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1261 1261 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1262 1262 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1263 1263 elif with_id:
1264 1264 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1265 1265 else:
1266 1266 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1267 1267
1268 1268 import kallithea.lib.helpers as h
1269 1269 prefix_url = h.canonical_url('home')
1270 1270
1271 1271 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1272 1272 prefix_url=prefix_url,
1273 1273 repo_name=self.repo_name,
1274 1274 repo_id=self.repo_id,
1275 1275 username=username)
1276 1276
1277 1277 def set_state(self, state):
1278 1278 self.repo_state = state
1279 1279
1280 1280 #==========================================================================
1281 1281 # SCM PROPERTIES
1282 1282 #==========================================================================
1283 1283
1284 1284 def get_changeset(self, rev=None):
1285 1285 return get_changeset_safe(self.scm_instance, rev)
1286 1286
1287 1287 def get_landing_changeset(self):
1288 1288 """
1289 1289 Returns landing changeset, or if that doesn't exist returns the tip
1290 1290 """
1291 1291 _rev_type, _rev = self.landing_rev
1292 1292 cs = self.get_changeset(_rev)
1293 1293 if isinstance(cs, EmptyChangeset):
1294 1294 return self.get_changeset()
1295 1295 return cs
1296 1296
1297 1297 def update_changeset_cache(self, cs_cache=None):
1298 1298 """
1299 1299 Update cache of last changeset for repository, keys should be::
1300 1300
1301 1301 short_id
1302 1302 raw_id
1303 1303 revision
1304 1304 message
1305 1305 date
1306 1306 author
1307 1307
1308 1308 :param cs_cache:
1309 1309 """
1310 1310 from kallithea.lib.vcs.backends.base import BaseChangeset
1311 1311 if cs_cache is None:
1312 1312 cs_cache = EmptyChangeset()
1313 1313 # use no-cache version here
1314 1314 scm_repo = self.scm_instance_no_cache()
1315 1315 if scm_repo:
1316 1316 cs_cache = scm_repo.get_changeset()
1317 1317
1318 1318 if isinstance(cs_cache, BaseChangeset):
1319 1319 cs_cache = cs_cache.__json__()
1320 1320
1321 1321 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1322 1322 _default = datetime.datetime.fromtimestamp(0)
1323 1323 last_change = cs_cache.get('date') or _default
1324 1324 log.debug('updated repo %s with new cs cache %s',
1325 1325 self.repo_name, cs_cache)
1326 1326 self.updated_on = last_change
1327 1327 self.changeset_cache = cs_cache
1328 1328 Session().commit()
1329 1329 else:
1330 1330 log.debug('changeset_cache for %s already up to date with %s',
1331 1331 self.repo_name, cs_cache['raw_id'])
1332 1332
1333 1333 @property
1334 1334 def tip(self):
1335 1335 return self.get_changeset('tip')
1336 1336
1337 1337 @property
1338 1338 def author(self):
1339 1339 return self.tip.author
1340 1340
1341 1341 @property
1342 1342 def last_change(self):
1343 1343 return self.scm_instance.last_change
1344 1344
1345 1345 def get_comments(self, revisions=None):
1346 1346 """
1347 1347 Returns comments for this repository grouped by revisions
1348 1348
1349 1349 :param revisions: filter query by revisions only
1350 1350 """
1351 1351 cmts = ChangesetComment.query() \
1352 1352 .filter(ChangesetComment.repo == self)
1353 1353 if revisions is not None:
1354 1354 if not revisions:
1355 1355 return {} # don't use sql 'in' on empty set
1356 1356 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1357 1357 grouped = collections.defaultdict(list)
1358 1358 for cmt in cmts.all():
1359 1359 grouped[cmt.revision].append(cmt)
1360 1360 return grouped
1361 1361
1362 1362 def statuses(self, revisions):
1363 1363 """
1364 1364 Returns statuses for this repository.
1365 1365 PRs without any votes do _not_ show up as unreviewed.
1366 1366
1367 1367 :param revisions: list of revisions to get statuses for
1368 1368 """
1369 1369 if not revisions:
1370 1370 return {}
1371 1371
1372 1372 statuses = ChangesetStatus.query() \
1373 1373 .filter(ChangesetStatus.repo == self) \
1374 1374 .filter(ChangesetStatus.version == 0) \
1375 1375 .filter(ChangesetStatus.revision.in_(revisions))
1376 1376
1377 1377 grouped = {}
1378 1378 for stat in statuses.all():
1379 1379 pr_id = pr_nice_id = pr_repo = None
1380 1380 if stat.pull_request:
1381 1381 pr_id = stat.pull_request.pull_request_id
1382 1382 pr_nice_id = PullRequest.make_nice_id(pr_id)
1383 1383 pr_repo = stat.pull_request.other_repo.repo_name
1384 1384 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1385 1385 pr_id, pr_repo, pr_nice_id,
1386 1386 stat.author]
1387 1387 return grouped
1388 1388
1389 1389 def _repo_size(self):
1390 1390 from kallithea.lib import helpers as h
1391 1391 log.debug('calculating repository size...')
1392 1392 return h.format_byte_size(self.scm_instance.size)
1393 1393
1394 1394 #==========================================================================
1395 1395 # SCM CACHE INSTANCE
1396 1396 #==========================================================================
1397 1397
1398 1398 def set_invalidate(self):
1399 1399 """
1400 1400 Mark caches of this repo as invalid.
1401 1401 """
1402 1402 CacheInvalidation.set_invalidate(self.repo_name)
1403 1403
1404 1404 _scm_instance = None
1405 1405
1406 1406 @property
1407 1407 def scm_instance(self):
1408 1408 if self._scm_instance is None:
1409 1409 self._scm_instance = self.scm_instance_cached()
1410 1410 return self._scm_instance
1411 1411
1412 1412 def scm_instance_cached(self, valid_cache_keys=None):
1413 1413 @cache_region('long_term', 'scm_instance_cached')
1414 1414 def _c(repo_name): # repo_name is just for the cache key
1415 1415 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1416 1416 return self.scm_instance_no_cache()
1417 1417 rn = self.repo_name
1418 1418
1419 1419 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1420 1420 if not valid:
1421 1421 log.debug('Cache for %s invalidated, getting new object', rn)
1422 1422 region_invalidate(_c, None, 'scm_instance_cached', rn)
1423 1423 else:
1424 1424 log.debug('Trying to get scm_instance of %s from cache', rn)
1425 1425 return _c(rn)
1426 1426
1427 1427 def scm_instance_no_cache(self):
1428 1428 repo_full_path = safe_str(self.repo_full_path)
1429 1429 alias = get_scm(repo_full_path)[0]
1430 1430 log.debug('Creating instance of %s repository from %s',
1431 1431 alias, self.repo_full_path)
1432 1432 backend = get_backend(alias)
1433 1433
1434 1434 if alias == 'hg':
1435 1435 repo = backend(repo_full_path, create=False,
1436 1436 baseui=self._ui)
1437 1437 else:
1438 1438 repo = backend(repo_full_path, create=False)
1439 1439
1440 1440 return repo
1441 1441
1442 1442 def __json__(self):
1443 1443 return dict(
1444 1444 repo_id=self.repo_id,
1445 1445 repo_name=self.repo_name,
1446 1446 landing_rev=self.landing_rev,
1447 1447 )
1448 1448
1449 1449
1450 1450 class RepoGroup(Base, BaseDbModel):
1451 1451 __tablename__ = 'groups'
1452 1452 __table_args__ = (
1453 1453 _table_args_default_dict,
1454 1454 )
1455 1455
1456 1456 SEP = ' &raquo; '
1457 1457
1458 1458 group_id = Column(Integer(), primary_key=True)
1459 1459 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1460 1460 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1461 1461 group_description = Column(Unicode(10000), nullable=False)
1462 1462 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1463 1463 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1464 1464
1465 1465 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1466 1466 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1467 1467 parent_group = relationship('RepoGroup', remote_side=group_id)
1468 1468 owner = relationship('User')
1469 1469
1470 1470 @classmethod
1471 1471 def query(cls, sorted=False):
1472 1472 """Add RepoGroup-specific helpers for common query constructs.
1473 1473
1474 1474 sorted: if True, apply the default ordering (name, case insensitive).
1475 1475 """
1476 1476 q = super(RepoGroup, cls).query()
1477 1477
1478 1478 if sorted:
1479 1479 q = q.order_by(func.lower(RepoGroup.group_name))
1480 1480
1481 1481 return q
1482 1482
1483 1483 def __init__(self, group_name='', parent_group=None):
1484 1484 self.group_name = group_name
1485 1485 self.parent_group = parent_group
1486 1486
1487 1487 def __unicode__(self):
1488 1488 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1489 1489 self.group_name)
1490 1490
1491 1491 @classmethod
1492 1492 def _generate_choice(cls, repo_group):
1493 1493 """Return tuple with group_id and name as html literal"""
1494 1494 from webhelpers2.html import literal
1495 1495 if repo_group is None:
1496 1496 return (-1, u'-- %s --' % _('top level'))
1497 1497 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1498 1498
1499 1499 @classmethod
1500 1500 def groups_choices(cls, groups):
1501 1501 """Return tuples with group_id and name as html literal."""
1502 1502 return sorted((cls._generate_choice(g) for g in groups),
1503 1503 key=lambda c: c[1].split(cls.SEP))
1504 1504
1505 1505 @classmethod
1506 1506 def url_sep(cls):
1507 1507 return URL_SEP
1508 1508
1509 1509 @classmethod
1510 1510 def guess_instance(cls, value):
1511 1511 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1512 1512
1513 1513 @classmethod
1514 1514 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1515 1515 group_name = group_name.rstrip('/')
1516 1516 if case_insensitive:
1517 1517 gr = cls.query() \
1518 1518 .filter(func.lower(cls.group_name) == func.lower(group_name))
1519 1519 else:
1520 1520 gr = cls.query() \
1521 1521 .filter(cls.group_name == group_name)
1522 1522 if cache:
1523 1523 gr = gr.options(FromCache(
1524 1524 "sql_cache_short",
1525 1525 "get_group_%s" % _hash_key(group_name)
1526 1526 )
1527 1527 )
1528 1528 return gr.scalar()
1529 1529
1530 1530 @property
1531 1531 def parents(self):
1532 1532 groups = []
1533 1533 group = self.parent_group
1534 1534 while group is not None:
1535 1535 groups.append(group)
1536 1536 group = group.parent_group
1537 1537 assert group not in groups, group # avoid recursion on bad db content
1538 1538 groups.reverse()
1539 1539 return groups
1540 1540
1541 1541 @property
1542 1542 def children(self):
1543 1543 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1544 1544
1545 1545 @property
1546 1546 def name(self):
1547 1547 return self.group_name.split(RepoGroup.url_sep())[-1]
1548 1548
1549 1549 @property
1550 1550 def full_path(self):
1551 1551 return self.group_name
1552 1552
1553 1553 @property
1554 1554 def full_path_splitted(self):
1555 1555 return self.group_name.split(RepoGroup.url_sep())
1556 1556
1557 1557 @property
1558 1558 def repositories(self):
1559 1559 return Repository.query(sorted=True).filter_by(group=self)
1560 1560
1561 1561 @property
1562 1562 def repositories_recursive_count(self):
1563 1563 cnt = self.repositories.count()
1564 1564
1565 1565 def children_count(group):
1566 1566 cnt = 0
1567 1567 for child in group.children:
1568 1568 cnt += child.repositories.count()
1569 1569 cnt += children_count(child)
1570 1570 return cnt
1571 1571
1572 1572 return cnt + children_count(self)
1573 1573
1574 1574 def _recursive_objects(self, include_repos=True):
1575 1575 all_ = []
1576 1576
1577 1577 def _get_members(root_gr):
1578 1578 if include_repos:
1579 1579 for r in root_gr.repositories:
1580 1580 all_.append(r)
1581 1581 childs = root_gr.children.all()
1582 1582 if childs:
1583 1583 for gr in childs:
1584 1584 all_.append(gr)
1585 1585 _get_members(gr)
1586 1586
1587 1587 _get_members(self)
1588 1588 return [self] + all_
1589 1589
1590 1590 def recursive_groups_and_repos(self):
1591 1591 """
1592 1592 Recursive return all groups, with repositories in those groups
1593 1593 """
1594 1594 return self._recursive_objects()
1595 1595
1596 1596 def recursive_groups(self):
1597 1597 """
1598 1598 Returns all children groups for this group including children of children
1599 1599 """
1600 1600 return self._recursive_objects(include_repos=False)
1601 1601
1602 1602 def get_new_name(self, group_name):
1603 1603 """
1604 1604 returns new full group name based on parent and new name
1605 1605
1606 1606 :param group_name:
1607 1607 """
1608 1608 path_prefix = (self.parent_group.full_path_splitted if
1609 1609 self.parent_group else [])
1610 1610 return RepoGroup.url_sep().join(path_prefix + [group_name])
1611 1611
1612 1612 def get_api_data(self):
1613 1613 """
1614 1614 Common function for generating api data
1615 1615
1616 1616 """
1617 1617 group = self
1618 1618 data = dict(
1619 1619 group_id=group.group_id,
1620 1620 group_name=group.group_name,
1621 1621 group_description=group.group_description,
1622 1622 parent_group=group.parent_group.group_name if group.parent_group else None,
1623 1623 repositories=[x.repo_name for x in group.repositories],
1624 1624 owner=group.owner.username
1625 1625 )
1626 1626 return data
1627 1627
1628 1628
1629 1629 class Permission(Base, BaseDbModel):
1630 1630 __tablename__ = 'permissions'
1631 1631 __table_args__ = (
1632 1632 Index('p_perm_name_idx', 'permission_name'),
1633 1633 _table_args_default_dict,
1634 1634 )
1635 1635
1636 1636 PERMS = (
1637 1637 ('hg.admin', _('Kallithea Administrator')),
1638 1638
1639 1639 ('repository.none', _('Default user has no access to new repositories')),
1640 1640 ('repository.read', _('Default user has read access to new repositories')),
1641 1641 ('repository.write', _('Default user has write access to new repositories')),
1642 1642 ('repository.admin', _('Default user has admin access to new repositories')),
1643 1643
1644 1644 ('group.none', _('Default user has no access to new repository groups')),
1645 1645 ('group.read', _('Default user has read access to new repository groups')),
1646 1646 ('group.write', _('Default user has write access to new repository groups')),
1647 1647 ('group.admin', _('Default user has admin access to new repository groups')),
1648 1648
1649 1649 ('usergroup.none', _('Default user has no access to new user groups')),
1650 1650 ('usergroup.read', _('Default user has read access to new user groups')),
1651 1651 ('usergroup.write', _('Default user has write access to new user groups')),
1652 1652 ('usergroup.admin', _('Default user has admin access to new user groups')),
1653 1653
1654 1654 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1655 1655 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1656 1656
1657 1657 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1658 1658 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1659 1659
1660 1660 ('hg.create.none', _('Only admins can create top level repositories')),
1661 1661 ('hg.create.repository', _('Non-admins can create top level repositories')),
1662 1662
1663 1663 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1664 1664 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1665 1665
1666 1666 ('hg.fork.none', _('Only admins can fork repositories')),
1667 1667 ('hg.fork.repository', _('Non-admins can fork repositories')),
1668 1668
1669 1669 ('hg.register.none', _('Registration disabled')),
1670 1670 ('hg.register.manual_activate', _('User registration with manual account activation')),
1671 1671 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1672 1672
1673 1673 ('hg.extern_activate.manual', _('Manual activation of external account')),
1674 1674 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1675 1675 )
1676 1676
1677 1677 # definition of system default permissions for DEFAULT user
1678 1678 DEFAULT_USER_PERMISSIONS = (
1679 1679 'repository.read',
1680 1680 'group.read',
1681 1681 'usergroup.read',
1682 1682 'hg.create.repository',
1683 1683 'hg.create.write_on_repogroup.true',
1684 1684 'hg.fork.repository',
1685 1685 'hg.register.manual_activate',
1686 1686 'hg.extern_activate.auto',
1687 1687 )
1688 1688
1689 1689 # defines which permissions are more important higher the more important
1690 1690 # Weight defines which permissions are more important.
1691 1691 # The higher number the more important.
1692 1692 PERM_WEIGHTS = {
1693 1693 'repository.none': 0,
1694 1694 'repository.read': 1,
1695 1695 'repository.write': 3,
1696 1696 'repository.admin': 4,
1697 1697
1698 1698 'group.none': 0,
1699 1699 'group.read': 1,
1700 1700 'group.write': 3,
1701 1701 'group.admin': 4,
1702 1702
1703 1703 'usergroup.none': 0,
1704 1704 'usergroup.read': 1,
1705 1705 'usergroup.write': 3,
1706 1706 'usergroup.admin': 4,
1707 1707
1708 1708 'hg.repogroup.create.false': 0,
1709 1709 'hg.repogroup.create.true': 1,
1710 1710
1711 1711 'hg.usergroup.create.false': 0,
1712 1712 'hg.usergroup.create.true': 1,
1713 1713
1714 1714 'hg.fork.none': 0,
1715 1715 'hg.fork.repository': 1,
1716 1716
1717 1717 'hg.create.none': 0,
1718 1718 'hg.create.repository': 1,
1719 1719
1720 1720 'hg.create.write_on_repogroup.false': 0,
1721 1721 'hg.create.write_on_repogroup.true': 1,
1722 1722
1723 1723 'hg.register.none': 0,
1724 1724 'hg.register.manual_activate': 1,
1725 1725 'hg.register.auto_activate': 2,
1726 1726
1727 1727 'hg.extern_activate.manual': 0,
1728 1728 'hg.extern_activate.auto': 1,
1729 1729 }
1730 1730
1731 1731 permission_id = Column(Integer(), primary_key=True)
1732 1732 permission_name = Column(String(255), nullable=False)
1733 1733
1734 1734 def __unicode__(self):
1735 1735 return u"<%s('%s:%s')>" % (
1736 1736 self.__class__.__name__, self.permission_id, self.permission_name
1737 1737 )
1738 1738
1739 1739 @classmethod
1740 1740 def guess_instance(cls, value):
1741 1741 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1742 1742
1743 1743 @classmethod
1744 1744 def get_by_key(cls, key):
1745 1745 return cls.query().filter(cls.permission_name == key).scalar()
1746 1746
1747 1747 @classmethod
1748 1748 def get_default_perms(cls, default_user_id):
1749 1749 q = Session().query(UserRepoToPerm, Repository, cls) \
1750 1750 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1751 1751 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1752 1752 .filter(UserRepoToPerm.user_id == default_user_id)
1753 1753
1754 1754 return q.all()
1755 1755
1756 1756 @classmethod
1757 1757 def get_default_group_perms(cls, default_user_id):
1758 1758 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1759 1759 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1760 1760 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1761 1761 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1762 1762
1763 1763 return q.all()
1764 1764
1765 1765 @classmethod
1766 1766 def get_default_user_group_perms(cls, default_user_id):
1767 1767 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1768 1768 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1769 1769 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1770 1770 .filter(UserUserGroupToPerm.user_id == default_user_id)
1771 1771
1772 1772 return q.all()
1773 1773
1774 1774
1775 1775 class UserRepoToPerm(Base, BaseDbModel):
1776 1776 __tablename__ = 'repo_to_perm'
1777 1777 __table_args__ = (
1778 1778 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1779 1779 _table_args_default_dict,
1780 1780 )
1781 1781
1782 1782 repo_to_perm_id = Column(Integer(), primary_key=True)
1783 1783 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1784 1784 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1785 1785 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1786 1786
1787 1787 user = relationship('User')
1788 1788 repository = relationship('Repository')
1789 1789 permission = relationship('Permission')
1790 1790
1791 1791 @classmethod
1792 1792 def create(cls, user, repository, permission):
1793 1793 n = cls()
1794 1794 n.user = user
1795 1795 n.repository = repository
1796 1796 n.permission = permission
1797 1797 Session().add(n)
1798 1798 return n
1799 1799
1800 1800 def __unicode__(self):
1801 1801 return u'<%s => %s >' % (self.user, self.repository)
1802 1802
1803 1803
1804 1804 class UserUserGroupToPerm(Base, BaseDbModel):
1805 1805 __tablename__ = 'user_user_group_to_perm'
1806 1806 __table_args__ = (
1807 1807 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1808 1808 _table_args_default_dict,
1809 1809 )
1810 1810
1811 1811 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1812 1812 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1813 1813 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1814 1814 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1815 1815
1816 1816 user = relationship('User')
1817 1817 user_group = relationship('UserGroup')
1818 1818 permission = relationship('Permission')
1819 1819
1820 1820 @classmethod
1821 1821 def create(cls, user, user_group, permission):
1822 1822 n = cls()
1823 1823 n.user = user
1824 1824 n.user_group = user_group
1825 1825 n.permission = permission
1826 1826 Session().add(n)
1827 1827 return n
1828 1828
1829 1829 def __unicode__(self):
1830 1830 return u'<%s => %s >' % (self.user, self.user_group)
1831 1831
1832 1832
1833 1833 class UserToPerm(Base, BaseDbModel):
1834 1834 __tablename__ = 'user_to_perm'
1835 1835 __table_args__ = (
1836 1836 UniqueConstraint('user_id', 'permission_id'),
1837 1837 _table_args_default_dict,
1838 1838 )
1839 1839
1840 1840 user_to_perm_id = Column(Integer(), primary_key=True)
1841 1841 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1842 1842 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1843 1843
1844 1844 user = relationship('User')
1845 1845 permission = relationship('Permission')
1846 1846
1847 1847 def __unicode__(self):
1848 1848 return u'<%s => %s >' % (self.user, self.permission)
1849 1849
1850 1850
1851 1851 class UserGroupRepoToPerm(Base, BaseDbModel):
1852 1852 __tablename__ = 'users_group_repo_to_perm'
1853 1853 __table_args__ = (
1854 1854 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1855 1855 _table_args_default_dict,
1856 1856 )
1857 1857
1858 1858 users_group_to_perm_id = Column(Integer(), primary_key=True)
1859 1859 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1860 1860 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1861 1861 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1862 1862
1863 1863 users_group = relationship('UserGroup')
1864 1864 permission = relationship('Permission')
1865 1865 repository = relationship('Repository')
1866 1866
1867 1867 @classmethod
1868 1868 def create(cls, users_group, repository, permission):
1869 1869 n = cls()
1870 1870 n.users_group = users_group
1871 1871 n.repository = repository
1872 1872 n.permission = permission
1873 1873 Session().add(n)
1874 1874 return n
1875 1875
1876 1876 def __unicode__(self):
1877 1877 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1878 1878
1879 1879
1880 1880 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1881 1881 __tablename__ = 'user_group_user_group_to_perm'
1882 1882 __table_args__ = (
1883 1883 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1884 1884 _table_args_default_dict,
1885 1885 )
1886 1886
1887 1887 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1888 1888 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1889 1889 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1890 1890 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1891 1891
1892 1892 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1893 1893 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1894 1894 permission = relationship('Permission')
1895 1895
1896 1896 @classmethod
1897 1897 def create(cls, target_user_group, user_group, permission):
1898 1898 n = cls()
1899 1899 n.target_user_group = target_user_group
1900 1900 n.user_group = user_group
1901 1901 n.permission = permission
1902 1902 Session().add(n)
1903 1903 return n
1904 1904
1905 1905 def __unicode__(self):
1906 1906 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1907 1907
1908 1908
1909 1909 class UserGroupToPerm(Base, BaseDbModel):
1910 1910 __tablename__ = 'users_group_to_perm'
1911 1911 __table_args__ = (
1912 1912 UniqueConstraint('users_group_id', 'permission_id',),
1913 1913 _table_args_default_dict,
1914 1914 )
1915 1915
1916 1916 users_group_to_perm_id = Column(Integer(), primary_key=True)
1917 1917 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1918 1918 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1919 1919
1920 1920 users_group = relationship('UserGroup')
1921 1921 permission = relationship('Permission')
1922 1922
1923 1923
1924 1924 class UserRepoGroupToPerm(Base, BaseDbModel):
1925 1925 __tablename__ = 'user_repo_group_to_perm'
1926 1926 __table_args__ = (
1927 1927 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1928 1928 _table_args_default_dict,
1929 1929 )
1930 1930
1931 1931 group_to_perm_id = Column(Integer(), primary_key=True)
1932 1932 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1933 1933 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1934 1934 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1935 1935
1936 1936 user = relationship('User')
1937 1937 group = relationship('RepoGroup')
1938 1938 permission = relationship('Permission')
1939 1939
1940 1940 @classmethod
1941 1941 def create(cls, user, repository_group, permission):
1942 1942 n = cls()
1943 1943 n.user = user
1944 1944 n.group = repository_group
1945 1945 n.permission = permission
1946 1946 Session().add(n)
1947 1947 return n
1948 1948
1949 1949
1950 1950 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1951 1951 __tablename__ = 'users_group_repo_group_to_perm'
1952 1952 __table_args__ = (
1953 1953 UniqueConstraint('users_group_id', 'group_id'),
1954 1954 _table_args_default_dict,
1955 1955 )
1956 1956
1957 1957 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1958 1958 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1959 1959 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1960 1960 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1961 1961
1962 1962 users_group = relationship('UserGroup')
1963 1963 permission = relationship('Permission')
1964 1964 group = relationship('RepoGroup')
1965 1965
1966 1966 @classmethod
1967 1967 def create(cls, user_group, repository_group, permission):
1968 1968 n = cls()
1969 1969 n.users_group = user_group
1970 1970 n.group = repository_group
1971 1971 n.permission = permission
1972 1972 Session().add(n)
1973 1973 return n
1974 1974
1975 1975
1976 1976 class Statistics(Base, BaseDbModel):
1977 1977 __tablename__ = 'statistics'
1978 1978 __table_args__ = (
1979 1979 _table_args_default_dict,
1980 1980 )
1981 1981
1982 1982 stat_id = Column(Integer(), primary_key=True)
1983 1983 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1984 1984 stat_on_revision = Column(Integer(), nullable=False)
1985 1985 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1986 1986 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1987 1987 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1988 1988
1989 1989 repository = relationship('Repository', single_parent=True)
1990 1990
1991 1991
1992 1992 class UserFollowing(Base, BaseDbModel):
1993 1993 __tablename__ = 'user_followings'
1994 1994 __table_args__ = (
1995 1995 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1996 1996 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
1997 1997 _table_args_default_dict,
1998 1998 )
1999 1999
2000 2000 user_following_id = Column(Integer(), primary_key=True)
2001 2001 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2002 2002 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2003 2003 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2004 2004 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2005 2005
2006 2006 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2007 2007
2008 2008 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2009 2009 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2010 2010
2011 2011 @classmethod
2012 2012 def get_repo_followers(cls, repo_id):
2013 2013 return cls.query().filter(cls.follows_repository_id == repo_id)
2014 2014
2015 2015
2016 2016 class CacheInvalidation(Base, BaseDbModel):
2017 2017 __tablename__ = 'cache_invalidation'
2018 2018 __table_args__ = (
2019 2019 Index('key_idx', 'cache_key'),
2020 2020 _table_args_default_dict,
2021 2021 )
2022 2022
2023 2023 # cache_id, not used
2024 2024 cache_id = Column(Integer(), primary_key=True)
2025 2025 # cache_key as created by _get_cache_key
2026 2026 cache_key = Column(Unicode(255), nullable=False, unique=True)
2027 2027 # cache_args is a repo_name
2028 2028 cache_args = Column(Unicode(255), nullable=False)
2029 2029 # instance sets cache_active True when it is caching, other instances set
2030 2030 # cache_active to False to indicate that this cache is invalid
2031 2031 cache_active = Column(Boolean(), nullable=False, default=False)
2032 2032
2033 2033 def __init__(self, cache_key, repo_name=''):
2034 2034 self.cache_key = cache_key
2035 2035 self.cache_args = repo_name
2036 2036 self.cache_active = False
2037 2037
2038 2038 def __unicode__(self):
2039 2039 return u"<%s('%s:%s[%s]')>" % (
2040 2040 self.__class__.__name__,
2041 2041 self.cache_id, self.cache_key, self.cache_active)
2042 2042
2043 2043 def _cache_key_partition(self):
2044 2044 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2045 2045 return prefix, repo_name, suffix
2046 2046
2047 2047 def get_prefix(self):
2048 2048 """
2049 2049 get prefix that might have been used in _get_cache_key to
2050 2050 generate self.cache_key. Only used for informational purposes
2051 2051 in repo_edit.html.
2052 2052 """
2053 2053 # prefix, repo_name, suffix
2054 2054 return self._cache_key_partition()[0]
2055 2055
2056 2056 def get_suffix(self):
2057 2057 """
2058 2058 get suffix that might have been used in _get_cache_key to
2059 2059 generate self.cache_key. Only used for informational purposes
2060 2060 in repo_edit.html.
2061 2061 """
2062 2062 # prefix, repo_name, suffix
2063 2063 return self._cache_key_partition()[2]
2064 2064
2065 2065 @classmethod
2066 2066 def clear_cache(cls):
2067 2067 """
2068 2068 Delete all cache keys from database.
2069 2069 Should only be run when all instances are down and all entries thus stale.
2070 2070 """
2071 2071 cls.query().delete()
2072 2072 Session().commit()
2073 2073
2074 2074 @classmethod
2075 2075 def _get_cache_key(cls, key):
2076 2076 """
2077 2077 Wrapper for generating a unique cache key for this instance and "key".
2078 2078 key must / will start with a repo_name which will be stored in .cache_args .
2079 2079 """
2080 2080 prefix = kallithea.CONFIG.get('instance_id', '')
2081 2081 return "%s%s" % (prefix, key)
2082 2082
2083 2083 @classmethod
2084 2084 def set_invalidate(cls, repo_name):
2085 2085 """
2086 2086 Mark all caches of a repo as invalid in the database.
2087 2087 """
2088 2088 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2089 2089 log.debug('for repo %s got %s invalidation objects',
2090 2090 safe_str(repo_name), inv_objs)
2091 2091
2092 2092 for inv_obj in inv_objs:
2093 2093 log.debug('marking %s key for invalidation based on repo_name=%s',
2094 2094 inv_obj, safe_str(repo_name))
2095 2095 Session().delete(inv_obj)
2096 2096 Session().commit()
2097 2097
2098 2098 @classmethod
2099 2099 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2100 2100 """
2101 2101 Mark this cache key as active and currently cached.
2102 2102 Return True if the existing cache registration still was valid.
2103 2103 Return False to indicate that it had been invalidated and caches should be refreshed.
2104 2104 """
2105 2105
2106 2106 key = (repo_name + '_' + kind) if kind else repo_name
2107 2107 cache_key = cls._get_cache_key(key)
2108 2108
2109 2109 if valid_cache_keys and cache_key in valid_cache_keys:
2110 2110 return True
2111 2111
2112 2112 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2113 2113 if inv_obj is None:
2114 2114 inv_obj = cls(cache_key, repo_name)
2115 2115 Session().add(inv_obj)
2116 2116 elif inv_obj.cache_active:
2117 2117 return True
2118 2118 inv_obj.cache_active = True
2119 2119 try:
2120 2120 Session().commit()
2121 2121 except sqlalchemy.exc.IntegrityError:
2122 2122 log.error('commit of CacheInvalidation failed - retrying')
2123 2123 Session().rollback()
2124 2124 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2125 2125 if inv_obj is None:
2126 2126 log.error('failed to create CacheInvalidation entry')
2127 2127 # TODO: fail badly?
2128 2128 # else: TOCTOU - another thread added the key at the same time; no further action required
2129 2129 return False
2130 2130
2131 2131 @classmethod
2132 2132 def get_valid_cache_keys(cls):
2133 2133 """
2134 2134 Return opaque object with information of which caches still are valid
2135 2135 and can be used without checking for invalidation.
2136 2136 """
2137 2137 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2138 2138
2139 2139
2140 2140 class ChangesetComment(Base, BaseDbModel):
2141 2141 __tablename__ = 'changeset_comments'
2142 2142 __table_args__ = (
2143 2143 Index('cc_revision_idx', 'revision'),
2144 2144 Index('cc_pull_request_id_idx', 'pull_request_id'),
2145 2145 _table_args_default_dict,
2146 2146 )
2147 2147
2148 2148 comment_id = Column(Integer(), primary_key=True)
2149 2149 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2150 2150 revision = Column(String(40), nullable=True)
2151 2151 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2152 2152 line_no = Column(Unicode(10), nullable=True)
2153 2153 f_path = Column(Unicode(1000), nullable=True)
2154 2154 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2155 2155 text = Column(UnicodeText(), nullable=False)
2156 2156 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2157 2157 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2158 2158
2159 2159 author = relationship('User')
2160 2160 repo = relationship('Repository')
2161 2161 # status_change is frequently used directly in templates - make it a lazy
2162 2162 # join to avoid fetching each related ChangesetStatus on demand.
2163 2163 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2164 2164 status_change = relationship('ChangesetStatus',
2165 2165 cascade="all, delete-orphan", lazy='joined')
2166 2166 pull_request = relationship('PullRequest')
2167 2167
2168 2168 def url(self):
2169 2169 anchor = "comment-%s" % self.comment_id
2170 2170 import kallithea.lib.helpers as h
2171 2171 if self.revision:
2172 2172 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2173 2173 elif self.pull_request_id is not None:
2174 2174 return self.pull_request.url(anchor=anchor)
2175 2175
2176 2176 def __json__(self):
2177 2177 return dict(
2178 2178 comment_id=self.comment_id,
2179 2179 username=self.author.username,
2180 2180 text=self.text,
2181 2181 )
2182 2182
2183 2183 def deletable(self):
2184 2184 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2185 2185
2186 2186
2187 2187 class ChangesetStatus(Base, BaseDbModel):
2188 2188 __tablename__ = 'changeset_statuses'
2189 2189 __table_args__ = (
2190 2190 Index('cs_revision_idx', 'revision'),
2191 2191 Index('cs_version_idx', 'version'),
2192 2192 Index('cs_pull_request_id_idx', 'pull_request_id'),
2193 2193 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2194 2194 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2195 2195 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2196 2196 UniqueConstraint('repo_id', 'revision', 'version'),
2197 2197 _table_args_default_dict,
2198 2198 )
2199 2199
2200 2200 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2201 2201 STATUS_APPROVED = 'approved'
2202 2202 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2203 2203 STATUS_UNDER_REVIEW = 'under_review'
2204 2204
2205 2205 STATUSES = [
2206 2206 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2207 2207 (STATUS_UNDER_REVIEW, _("Under review")),
2208 2208 (STATUS_REJECTED, _("Not approved")),
2209 2209 (STATUS_APPROVED, _("Approved")),
2210 2210 ]
2211 2211 STATUSES_DICT = dict(STATUSES)
2212 2212
2213 2213 changeset_status_id = Column(Integer(), primary_key=True)
2214 2214 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2215 2215 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2216 2216 revision = Column(String(40), nullable=True)
2217 2217 status = Column(String(128), nullable=False, default=DEFAULT)
2218 2218 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2219 2219 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2220 2220 version = Column(Integer(), nullable=False, default=0)
2221 2221 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2222 2222
2223 2223 author = relationship('User')
2224 2224 repo = relationship('Repository')
2225 2225 comment = relationship('ChangesetComment')
2226 2226 pull_request = relationship('PullRequest')
2227 2227
2228 2228 def __unicode__(self):
2229 2229 return u"<%s('%s:%s')>" % (
2230 2230 self.__class__.__name__,
2231 2231 self.status, self.author
2232 2232 )
2233 2233
2234 2234 @classmethod
2235 2235 def get_status_lbl(cls, value):
2236 2236 return cls.STATUSES_DICT.get(value)
2237 2237
2238 2238 @property
2239 2239 def status_lbl(self):
2240 2240 return ChangesetStatus.get_status_lbl(self.status)
2241 2241
2242 2242 def __json__(self):
2243 2243 return dict(
2244 2244 status=self.status,
2245 2245 modified_at=self.modified_at.replace(microsecond=0),
2246 2246 reviewer=self.author.username,
2247 2247 )
2248 2248
2249 2249
2250 2250 class PullRequest(Base, BaseDbModel):
2251 2251 __tablename__ = 'pull_requests'
2252 2252 __table_args__ = (
2253 2253 Index('pr_org_repo_id_idx', 'org_repo_id'),
2254 2254 Index('pr_other_repo_id_idx', 'other_repo_id'),
2255 2255 _table_args_default_dict,
2256 2256 )
2257 2257
2258 2258 # values for .status
2259 2259 STATUS_NEW = u'new'
2260 2260 STATUS_CLOSED = u'closed'
2261 2261
2262 2262 pull_request_id = Column(Integer(), primary_key=True)
2263 2263 title = Column(Unicode(255), nullable=False)
2264 2264 description = Column(UnicodeText(), nullable=False)
2265 2265 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2266 2266 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2267 2267 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2268 2268 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2269 2269 _revisions = Column('revisions', UnicodeText(), nullable=False)
2270 2270 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2271 2271 org_ref = Column(Unicode(255), nullable=False)
2272 2272 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2273 2273 other_ref = Column(Unicode(255), nullable=False)
2274 2274
2275 2275 @hybrid_property
2276 2276 def revisions(self):
2277 2277 return self._revisions.split(':')
2278 2278
2279 2279 @revisions.setter
2280 2280 def revisions(self, val):
2281 2281 self._revisions = safe_unicode(':'.join(val))
2282 2282
2283 2283 @property
2284 2284 def org_ref_parts(self):
2285 2285 return self.org_ref.split(':')
2286 2286
2287 2287 @property
2288 2288 def other_ref_parts(self):
2289 2289 return self.other_ref.split(':')
2290 2290
2291 2291 owner = relationship('User')
2292 2292 reviewers = relationship('PullRequestReviewer',
2293 2293 cascade="all, delete-orphan")
2294 2294 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2295 2295 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2296 2296 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2297 2297 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2298 2298 cascade="all, delete-orphan")
2299 2299
2300 2300 @classmethod
2301 2301 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2302 2302 """Add PullRequest-specific helpers for common query constructs.
2303 2303
2304 2304 reviewer_id: only PRs with the specified user added as reviewer.
2305 2305
2306 2306 include_closed: if False, do not include closed PRs.
2307 2307
2308 2308 sorted: if True, apply the default ordering (newest first).
2309 2309 """
2310 2310 q = super(PullRequest, cls).query()
2311 2311
2312 2312 if reviewer_id is not None:
2313 2313 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2314 2314
2315 2315 if not include_closed:
2316 2316 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2317 2317
2318 2318 if sorted:
2319 2319 q = q.order_by(PullRequest.created_on.desc())
2320 2320
2321 2321 return q
2322 2322
2323 2323 def get_reviewer_users(self):
2324 2324 """Like .reviewers, but actually returning the users"""
2325 2325 return User.query() \
2326 2326 .join(PullRequestReviewer) \
2327 2327 .filter(PullRequestReviewer.pull_request == self) \
2328 2328 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2329 2329 .all()
2330 2330
2331 2331 def is_closed(self):
2332 2332 return self.status == self.STATUS_CLOSED
2333 2333
2334 2334 def user_review_status(self, user_id):
2335 2335 """Return the user's latest status votes on PR"""
2336 2336 # note: no filtering on repo - that would be redundant
2337 2337 status = ChangesetStatus.query() \
2338 2338 .filter(ChangesetStatus.pull_request == self) \
2339 2339 .filter(ChangesetStatus.user_id == user_id) \
2340 2340 .order_by(ChangesetStatus.version) \
2341 2341 .first()
2342 2342 return str(status.status) if status else ''
2343 2343
2344 2344 @classmethod
2345 2345 def make_nice_id(cls, pull_request_id):
2346 2346 '''Return pull request id nicely formatted for displaying'''
2347 2347 return '#%s' % pull_request_id
2348 2348
2349 2349 def nice_id(self):
2350 2350 '''Return the id of this pull request, nicely formatted for displaying'''
2351 2351 return self.make_nice_id(self.pull_request_id)
2352 2352
2353 2353 def get_api_data(self):
2354 2354 return self.__json__()
2355 2355
2356 2356 def __json__(self):
2357 2357 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2358 2358 return dict(
2359 2359 pull_request_id=self.pull_request_id,
2360 2360 url=self.url(),
2361 2361 reviewers=self.reviewers,
2362 2362 revisions=self.revisions,
2363 2363 owner=self.owner.username,
2364 2364 title=self.title,
2365 2365 description=self.description,
2366 2366 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2367 2367 org_ref_parts=self.org_ref_parts,
2368 2368 other_ref_parts=self.other_ref_parts,
2369 2369 status=self.status,
2370 2370 comments=self.comments,
2371 2371 statuses=self.statuses,
2372 2372 )
2373 2373
2374 2374 def url(self, **kwargs):
2375 2375 canonical = kwargs.pop('canonical', None)
2376 2376 import kallithea.lib.helpers as h
2377 2377 b = self.org_ref_parts[1]
2378 2378 if b != self.other_ref_parts[1]:
2379 2379 s = '/_/' + b
2380 2380 else:
2381 2381 s = '/_/' + self.title
2382 2382 kwargs['extra'] = urlreadable(s)
2383 2383 if canonical:
2384 2384 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2385 2385 pull_request_id=self.pull_request_id, **kwargs)
2386 2386 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2387 2387 pull_request_id=self.pull_request_id, **kwargs)
2388 2388
2389 2389
2390 2390 class PullRequestReviewer(Base, BaseDbModel):
2391 2391 __tablename__ = 'pull_request_reviewers'
2392 2392 __table_args__ = (
2393 2393 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2394 2394 _table_args_default_dict,
2395 2395 )
2396 2396
2397 2397 def __init__(self, user=None, pull_request=None):
2398 2398 self.user = user
2399 2399 self.pull_request = pull_request
2400 2400
2401 2401 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2402 2402 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2403 2403 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2404 2404
2405 2405 user = relationship('User')
2406 2406 pull_request = relationship('PullRequest')
2407 2407
2408 2408 def __json__(self):
2409 2409 return dict(
2410 2410 username=self.user.username if self.user else None,
2411 2411 )
2412 2412
2413 2413
2414 2414 class Notification(object):
2415 2415 __tablename__ = 'notifications'
2416 2416
2417 2417 class UserNotification(object):
2418 2418 __tablename__ = 'user_to_notification'
2419 2419
2420 2420
2421 2421 class Gist(Base, BaseDbModel):
2422 2422 __tablename__ = 'gists'
2423 2423 __table_args__ = (
2424 2424 Index('g_gist_access_id_idx', 'gist_access_id'),
2425 2425 Index('g_created_on_idx', 'created_on'),
2426 2426 _table_args_default_dict,
2427 2427 )
2428 2428
2429 2429 GIST_PUBLIC = u'public'
2430 2430 GIST_PRIVATE = u'private'
2431 2431 DEFAULT_FILENAME = u'gistfile1.txt'
2432 2432
2433 2433 gist_id = Column(Integer(), primary_key=True)
2434 2434 gist_access_id = Column(Unicode(250), nullable=False)
2435 2435 gist_description = Column(UnicodeText(), nullable=False)
2436 2436 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2437 2437 gist_expires = Column(Float(53), nullable=False)
2438 2438 gist_type = Column(Unicode(128), nullable=False)
2439 2439 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2440 2440 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2441 2441
2442 2442 owner = relationship('User')
2443 2443
2444 2444 @hybrid_property
2445 2445 def is_expired(self):
2446 2446 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2447 2447
2448 2448 def __repr__(self):
2449 2449 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2450 2450
2451 2451 @classmethod
2452 2452 def guess_instance(cls, value):
2453 2453 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2454 2454
2455 2455 @classmethod
2456 2456 def get_or_404(cls, id_):
2457 2457 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2458 2458 if res is None:
2459 2459 raise HTTPNotFound
2460 2460 return res
2461 2461
2462 2462 @classmethod
2463 2463 def get_by_access_id(cls, gist_access_id):
2464 2464 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2465 2465
2466 2466 def gist_url(self):
2467 2467 alias_url = kallithea.CONFIG.get('gist_alias_url')
2468 2468 if alias_url:
2469 2469 return alias_url.replace('{gistid}', self.gist_access_id)
2470 2470
2471 2471 import kallithea.lib.helpers as h
2472 2472 return h.canonical_url('gist', gist_id=self.gist_access_id)
2473 2473
2474 2474 @classmethod
2475 2475 def base_path(cls):
2476 2476 """
2477 2477 Returns base path where all gists are stored
2478 2478
2479 2479 :param cls:
2480 2480 """
2481 2481 from kallithea.model.gist import GIST_STORE_LOC
2482 2482 q = Session().query(Ui) \
2483 2483 .filter(Ui.ui_key == URL_SEP)
2484 2484 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2485 2485 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2486 2486
2487 2487 def get_api_data(self):
2488 2488 """
2489 2489 Common function for generating gist related data for API
2490 2490 """
2491 2491 gist = self
2492 2492 data = dict(
2493 2493 gist_id=gist.gist_id,
2494 2494 type=gist.gist_type,
2495 2495 access_id=gist.gist_access_id,
2496 2496 description=gist.gist_description,
2497 2497 url=gist.gist_url(),
2498 2498 expires=gist.gist_expires,
2499 2499 created_on=gist.created_on,
2500 2500 )
2501 2501 return data
2502 2502
2503 2503 def __json__(self):
2504 2504 data = dict(
2505 2505 )
2506 2506 data.update(self.get_api_data())
2507 2507 return data
2508 2508 ## SCM functions
2509 2509
2510 2510 @property
2511 2511 def scm_instance(self):
2512 2512 from kallithea.lib.vcs import get_repo
2513 2513 base_path = self.base_path()
2514 2514 return get_repo(os.path.join(*map(safe_str,
2515 2515 [base_path, self.gist_access_id])))
2516 2516
2517 2517
2518 2518 class UserSshKeys(Base, BaseDbModel):
2519 2519 __tablename__ = 'user_ssh_keys'
2520 2520 __table_args__ = (
2521 2521 Index('usk_public_key_idx', 'public_key'),
2522 2522 Index('usk_fingerprint_idx', 'fingerprint'),
2523 2523 UniqueConstraint('fingerprint'),
2524 2524 _table_args_default_dict
2525 2525 )
2526 2526 __mapper_args__ = {}
2527 2527
2528 2528 user_ssh_key_id = Column(Integer(), primary_key=True)
2529 2529 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2530 2530 _public_key = Column('public_key', UnicodeText(), nullable=False)
2531 2531 description = Column(UnicodeText(), nullable=False)
2532 2532 fingerprint = Column(String(255), nullable=False, unique=True)
2533 2533 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2534 2534 last_seen = Column(DateTime(timezone=False), nullable=True)
2535 2535
2536 2536 user = relationship('User')
2537 2537
2538 2538 @property
2539 2539 def public_key(self):
2540 2540 return self._public_key
2541 2541
2542 2542 @public_key.setter
2543 2543 def public_key(self, full_key):
2544 2544 # the full public key is too long to be suitable as database key - instead,
2545 2545 # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
2546 2546 self._public_key = full_key
2547 2547 enc_key = full_key.split(" ")[1]
2548 2548 self.fingerprint = hashlib.sha256(enc_key.decode('base64')).digest().encode('base64').replace('\n', '').rstrip('=')
@@ -1,720 +1,720 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.repo
16 16 ~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Repository model for kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Jun 5, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26
27 27 """
28 28
29 29 import logging
30 30 import os
31 31 import shutil
32 32 import traceback
33 33 from datetime import datetime
34 34
35 35 import kallithea.lib.utils2
36 36 from kallithea.lib import helpers as h
37 37 from kallithea.lib.auth import HasRepoPermissionLevel, HasUserGroupPermissionLevel
38 38 from kallithea.lib.caching_query import FromCache
39 39 from kallithea.lib.exceptions import AttachedForksError
40 40 from kallithea.lib.hooks import log_delete_repository
41 41 from kallithea.lib.utils import is_valid_repo_uri, make_ui
42 42 from kallithea.lib.utils2 import LazyProperty, get_current_authuser, obfuscate_url_pw, remove_prefix, safe_str, safe_unicode
43 43 from kallithea.lib.vcs.backends import get_backend
44 44 from kallithea.model.db import (
45 45 Permission, RepoGroup, Repository, RepositoryField, Session, Statistics, Ui, User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserRepoToPerm)
46 46
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class RepoModel(object):
52 52
53 53 URL_SEPARATOR = Repository.url_sep()
54 54
55 55 def _create_default_perms(self, repository, private):
56 56 # create default permission
57 57 default = 'repository.read'
58 58 def_user = User.get_default_user()
59 59 for p in def_user.user_perms:
60 60 if p.permission.permission_name.startswith('repository.'):
61 61 default = p.permission.permission_name
62 62 break
63 63
64 64 default_perm = 'repository.none' if private else default
65 65
66 66 repo_to_perm = UserRepoToPerm()
67 67 repo_to_perm.permission = Permission.get_by_key(default_perm)
68 68
69 69 repo_to_perm.repository = repository
70 70 repo_to_perm.user_id = def_user.user_id
71 71 Session().add(repo_to_perm)
72 72
73 73 return repo_to_perm
74 74
75 75 @LazyProperty
76 76 def repos_path(self):
77 77 """
78 78 Gets the repositories root path from database
79 79 """
80 80
81 81 q = Ui.query().filter(Ui.ui_key == '/').one()
82 82 return q.ui_value
83 83
84 84 def get(self, repo_id, cache=False):
85 85 repo = Repository.query() \
86 86 .filter(Repository.repo_id == repo_id)
87 87
88 88 if cache:
89 89 repo = repo.options(FromCache("sql_cache_short",
90 90 "get_repo_%s" % repo_id))
91 91 return repo.scalar()
92 92
93 93 def get_repo(self, repository):
94 94 return Repository.guess_instance(repository)
95 95
96 96 def get_by_repo_name(self, repo_name, cache=False):
97 97 repo = Repository.query() \
98 98 .filter(Repository.repo_name == repo_name)
99 99
100 100 if cache:
101 101 repo = repo.options(FromCache("sql_cache_short",
102 102 "get_repo_%s" % repo_name))
103 103 return repo.scalar()
104 104
105 105 def get_all_user_repos(self, user):
106 106 """
107 107 Gets all repositories that user have at least read access
108 108
109 109 :param user:
110 110 """
111 111 from kallithea.lib.auth import AuthUser
112 112 user = User.guess_instance(user)
113 113 repos = AuthUser(dbuser=user).permissions['repositories']
114 114 access_check = lambda r: r[1] in ['repository.read',
115 115 'repository.write',
116 116 'repository.admin']
117 117 repos = [x[0] for x in filter(access_check, repos.items())]
118 118 return Repository.query().filter(Repository.repo_name.in_(repos))
119 119
120 120 @classmethod
121 121 def _render_datatable(cls, tmpl, *args, **kwargs):
122 122 import kallithea
123 123 from tg import tmpl_context as c, request, app_globals
124 124 from tg.i18n import ugettext as _
125 125
126 126 _tmpl_lookup = app_globals.mako_lookup
127 127 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
128 128
129 129 tmpl = template.get_def(tmpl)
130 130 kwargs.update(dict(_=_, h=h, c=c, request=request))
131 131 return tmpl.render(*args, **kwargs)
132 132
133 133 def get_repos_as_dict(self, repos_list, repo_groups_list=None,
134 134 admin=False,
135 135 short_name=False):
136 136 """Return repository list for use by DataTable.
137 137 repos_list: list of repositories - but will be filtered for read permission.
138 138 repo_groups_list: added at top of list without permission check.
139 139 admin: return data for action column.
140 140 """
141 141 _render = self._render_datatable
142 142 from tg import tmpl_context as c
143 143
144 144 def repo_lnk(name, rtype, rstate, private, fork_of):
145 145 return _render('repo_name', name, rtype, rstate, private, fork_of,
146 146 short_name=short_name)
147 147
148 148 def last_change(last_change):
149 149 return _render("last_change", last_change)
150 150
151 151 def rss_lnk(repo_name):
152 152 return _render("rss", repo_name)
153 153
154 154 def atom_lnk(repo_name):
155 155 return _render("atom", repo_name)
156 156
157 157 def last_rev(repo_name, cs_cache):
158 158 return _render('revision', repo_name, cs_cache.get('revision'),
159 159 cs_cache.get('raw_id'), cs_cache.get('author'),
160 160 cs_cache.get('message'))
161 161
162 162 def desc(desc):
163 163 return h.urlify_text(desc, truncate=80, stylize=c.visual.stylify_metalabels)
164 164
165 165 def state(repo_state):
166 166 return _render("repo_state", repo_state)
167 167
168 168 def repo_actions(repo_name):
169 169 return _render('repo_actions', repo_name)
170 170
171 171 def owner_actions(owner_id, username):
172 172 return _render('user_name', owner_id, username)
173 173
174 174 repos_data = []
175 175
176 176 for gr in repo_groups_list or []:
177 177 repos_data.append(dict(
178 178 raw_name='\0' + gr.name, # sort before repositories
179 179 just_name=gr.name,
180 180 name=_render('group_name_html', group_name=gr.group_name, name=gr.name),
181 181 desc=gr.group_description))
182 182
183 183 for repo in repos_list:
184 184 if not HasRepoPermissionLevel('read')(repo.repo_name, 'get_repos_as_dict check'):
185 185 continue
186 186 cs_cache = repo.changeset_cache
187 187 row = {
188 188 "raw_name": repo.repo_name,
189 189 "just_name": repo.just_name,
190 190 "name": repo_lnk(repo.repo_name, repo.repo_type,
191 191 repo.repo_state, repo.private, repo.fork),
192 192 "last_change_iso": repo.last_db_change.isoformat(),
193 193 "last_change": last_change(repo.last_db_change),
194 194 "last_changeset": last_rev(repo.repo_name, cs_cache),
195 195 "last_rev_raw": cs_cache.get('revision'),
196 196 "desc": desc(repo.description),
197 197 "owner": h.person(repo.owner),
198 198 "state": state(repo.repo_state),
199 199 "rss": rss_lnk(repo.repo_name),
200 200 "atom": atom_lnk(repo.repo_name),
201 201 }
202 202 if admin:
203 203 row.update({
204 204 "action": repo_actions(repo.repo_name),
205 205 "owner": owner_actions(repo.owner_id,
206 206 h.person(repo.owner))
207 207 })
208 208 repos_data.append(row)
209 209
210 210 return {
211 211 "sort": "name",
212 212 "dir": "asc",
213 213 "records": repos_data
214 214 }
215 215
216 216 def _get_defaults(self, repo_name):
217 217 """
218 218 Gets information about repository, and returns a dict for
219 219 usage in forms
220 220
221 221 :param repo_name:
222 222 """
223 223
224 224 repo_info = Repository.get_by_repo_name(repo_name)
225 225
226 226 if repo_info is None:
227 227 return None
228 228
229 229 defaults = repo_info.get_dict()
230 230 defaults['repo_name'] = repo_info.just_name
231 231 defaults['repo_group'] = repo_info.group_id
232 232
233 233 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
234 234 (1, 'repo_description'),
235 235 (1, 'repo_landing_rev'), (0, 'clone_uri'),
236 236 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
237 237 attr = k
238 238 if strip:
239 239 attr = remove_prefix(k, 'repo_')
240 240
241 241 val = defaults[attr]
242 242 if k == 'repo_landing_rev':
243 243 val = ':'.join(defaults[attr])
244 244 defaults[k] = val
245 245 if k == 'clone_uri':
246 246 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
247 247
248 248 # fill owner
249 249 if repo_info.owner:
250 250 defaults.update({'owner': repo_info.owner.username})
251 251 else:
252 252 replacement_user = User.query().filter(User.admin ==
253 253 True).first().username
254 254 defaults.update({'owner': replacement_user})
255 255
256 256 # fill repository users
257 257 for p in repo_info.repo_to_perm:
258 258 defaults.update({'u_perm_%s' % p.user.username:
259 259 p.permission.permission_name})
260 260
261 261 # fill repository groups
262 262 for p in repo_info.users_group_to_perm:
263 263 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
264 264 p.permission.permission_name})
265 265
266 266 return defaults
267 267
268 268 def update(self, repo, **kwargs):
269 269 try:
270 270 cur_repo = Repository.guess_instance(repo)
271 271 org_repo_name = cur_repo.repo_name
272 272 if 'owner' in kwargs:
273 273 cur_repo.owner = User.get_by_username(kwargs['owner'])
274 274
275 275 if 'repo_group' in kwargs:
276 276 assert kwargs['repo_group'] != u'-1', kwargs # RepoForm should have converted to None
277 277 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
278 278 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
279 279 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
280 280 for k in ['repo_enable_downloads',
281 281 'repo_description',
282 282 'repo_landing_rev',
283 283 'repo_private',
284 284 'repo_enable_statistics',
285 285 ]:
286 286 if k in kwargs:
287 287 setattr(cur_repo, remove_prefix(k, 'repo_'), kwargs[k])
288 288 clone_uri = kwargs.get('clone_uri')
289 289 if clone_uri is not None and clone_uri != cur_repo.clone_uri_hidden:
290 290 # clone_uri is modified - if given a value, check it is valid
291 291 if clone_uri != '':
292 292 # will raise exception on error
293 is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui(clear_session=False))
293 is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui())
294 294 cur_repo.clone_uri = clone_uri
295 295
296 296 if 'repo_name' in kwargs:
297 297 repo_name = kwargs['repo_name']
298 298 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
299 299 raise Exception('invalid repo name %s' % repo_name)
300 300 cur_repo.repo_name = cur_repo.get_new_name(repo_name)
301 301
302 302 # if private flag is set, reset default permission to NONE
303 303 if kwargs.get('repo_private'):
304 304 EMPTY_PERM = 'repository.none'
305 305 RepoModel().grant_user_permission(
306 306 repo=cur_repo, user='default', perm=EMPTY_PERM
307 307 )
308 308 # handle extra fields
309 309 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
310 310 kwargs):
311 311 k = RepositoryField.un_prefix_key(field)
312 312 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
313 313 if ex_field:
314 314 ex_field.field_value = kwargs[field]
315 315
316 316 if org_repo_name != cur_repo.repo_name:
317 317 # rename repository
318 318 self._rename_filesystem_repo(old=org_repo_name, new=cur_repo.repo_name)
319 319
320 320 return cur_repo
321 321 except Exception:
322 322 log.error(traceback.format_exc())
323 323 raise
324 324
325 325 def _create_repo(self, repo_name, repo_type, description, owner,
326 326 private=False, clone_uri=None, repo_group=None,
327 327 landing_rev='rev:tip', fork_of=None,
328 328 copy_fork_permissions=False, enable_statistics=False,
329 329 enable_downloads=False,
330 330 copy_group_permissions=False, state=Repository.STATE_PENDING):
331 331 """
332 332 Create repository inside database with PENDING state. This should only be
333 333 executed by create() repo, with exception of importing existing repos.
334 334
335 335 """
336 336 from kallithea.model.scm import ScmModel
337 337
338 338 owner = User.guess_instance(owner)
339 339 fork_of = Repository.guess_instance(fork_of)
340 340 repo_group = RepoGroup.guess_instance(repo_group)
341 341 try:
342 342 repo_name = safe_unicode(repo_name)
343 343 description = safe_unicode(description)
344 344 # repo name is just a name of repository
345 345 # while repo_name_full is a full qualified name that is combined
346 346 # with name and path of group
347 347 repo_name_full = repo_name
348 348 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
349 349 if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
350 350 raise Exception('invalid repo name %s' % repo_name)
351 351
352 352 new_repo = Repository()
353 353 new_repo.repo_state = state
354 354 new_repo.enable_statistics = False
355 355 new_repo.repo_name = repo_name_full
356 356 new_repo.repo_type = repo_type
357 357 new_repo.owner = owner
358 358 new_repo.group = repo_group
359 359 new_repo.description = description or repo_name
360 360 new_repo.private = private
361 361 if clone_uri:
362 362 # will raise exception on error
363 is_valid_repo_uri(repo_type, clone_uri, make_ui(clear_session=False))
363 is_valid_repo_uri(repo_type, clone_uri, make_ui())
364 364 new_repo.clone_uri = clone_uri
365 365 new_repo.landing_rev = landing_rev
366 366
367 367 new_repo.enable_statistics = enable_statistics
368 368 new_repo.enable_downloads = enable_downloads
369 369
370 370 if fork_of:
371 371 parent_repo = fork_of
372 372 new_repo.fork = parent_repo
373 373
374 374 Session().add(new_repo)
375 375
376 376 if fork_of and copy_fork_permissions:
377 377 repo = fork_of
378 378 user_perms = UserRepoToPerm.query() \
379 379 .filter(UserRepoToPerm.repository == repo).all()
380 380 group_perms = UserGroupRepoToPerm.query() \
381 381 .filter(UserGroupRepoToPerm.repository == repo).all()
382 382
383 383 for perm in user_perms:
384 384 UserRepoToPerm.create(perm.user, new_repo, perm.permission)
385 385
386 386 for perm in group_perms:
387 387 UserGroupRepoToPerm.create(perm.users_group, new_repo,
388 388 perm.permission)
389 389
390 390 elif repo_group and copy_group_permissions:
391 391
392 392 user_perms = UserRepoGroupToPerm.query() \
393 393 .filter(UserRepoGroupToPerm.group == repo_group).all()
394 394
395 395 group_perms = UserGroupRepoGroupToPerm.query() \
396 396 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
397 397
398 398 for perm in user_perms:
399 399 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
400 400 perm_obj = Permission.get_by_key(perm_name)
401 401 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
402 402
403 403 for perm in group_perms:
404 404 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
405 405 perm_obj = Permission.get_by_key(perm_name)
406 406 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
407 407
408 408 else:
409 409 self._create_default_perms(new_repo, private)
410 410
411 411 # now automatically start following this repository as owner
412 412 ScmModel().toggle_following_repo(new_repo.repo_id, owner.user_id)
413 413 # we need to flush here, in order to check if database won't
414 414 # throw any exceptions, create filesystem dirs at the very end
415 415 Session().flush()
416 416 return new_repo
417 417 except Exception:
418 418 log.error(traceback.format_exc())
419 419 raise
420 420
421 421 def create(self, form_data, cur_user):
422 422 """
423 423 Create repository using celery tasks
424 424
425 425 :param form_data:
426 426 :param cur_user:
427 427 """
428 428 from kallithea.lib.celerylib import tasks
429 429 return tasks.create_repo(form_data, cur_user)
430 430
431 431 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
432 432 check_perms=True):
433 433 if not perms_new:
434 434 perms_new = []
435 435 if not perms_updates:
436 436 perms_updates = []
437 437
438 438 # update permissions
439 439 for member, perm, member_type in perms_updates:
440 440 if member_type == 'user':
441 441 # this updates existing one
442 442 self.grant_user_permission(
443 443 repo=repo, user=member, perm=perm
444 444 )
445 445 else:
446 446 # check if we have permissions to alter this usergroup's access
447 447 if not check_perms or HasUserGroupPermissionLevel('read')(member):
448 448 self.grant_user_group_permission(
449 449 repo=repo, group_name=member, perm=perm
450 450 )
451 451 # set new permissions
452 452 for member, perm, member_type in perms_new:
453 453 if member_type == 'user':
454 454 self.grant_user_permission(
455 455 repo=repo, user=member, perm=perm
456 456 )
457 457 else:
458 458 # check if we have permissions to alter this usergroup's access
459 459 if not check_perms or HasUserGroupPermissionLevel('read')(member):
460 460 self.grant_user_group_permission(
461 461 repo=repo, group_name=member, perm=perm
462 462 )
463 463
464 464 def create_fork(self, form_data, cur_user):
465 465 """
466 466 Simple wrapper into executing celery task for fork creation
467 467
468 468 :param form_data:
469 469 :param cur_user:
470 470 """
471 471 from kallithea.lib.celerylib import tasks
472 472 return tasks.create_repo_fork(form_data, cur_user)
473 473
474 474 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
475 475 """
476 476 Delete given repository, forks parameter defines what do do with
477 477 attached forks. Throws AttachedForksError if deleted repo has attached
478 478 forks
479 479
480 480 :param repo:
481 481 :param forks: str 'delete' or 'detach'
482 482 :param fs_remove: remove(archive) repo from filesystem
483 483 """
484 484 if not cur_user:
485 485 cur_user = getattr(get_current_authuser(), 'username', None)
486 486 repo = Repository.guess_instance(repo)
487 487 if repo is not None:
488 488 if forks == 'detach':
489 489 for r in repo.forks:
490 490 r.fork = None
491 491 elif forks == 'delete':
492 492 for r in repo.forks:
493 493 self.delete(r, forks='delete')
494 494 elif [f for f in repo.forks]:
495 495 raise AttachedForksError()
496 496
497 497 old_repo_dict = repo.get_dict()
498 498 try:
499 499 Session().delete(repo)
500 500 if fs_remove:
501 501 self._delete_filesystem_repo(repo)
502 502 else:
503 503 log.debug('skipping removal from filesystem')
504 504 log_delete_repository(old_repo_dict,
505 505 deleted_by=cur_user)
506 506 except Exception:
507 507 log.error(traceback.format_exc())
508 508 raise
509 509
510 510 def grant_user_permission(self, repo, user, perm):
511 511 """
512 512 Grant permission for user on given repository, or update existing one
513 513 if found
514 514
515 515 :param repo: Instance of Repository, repository_id, or repository name
516 516 :param user: Instance of User, user_id or username
517 517 :param perm: Instance of Permission, or permission_name
518 518 """
519 519 user = User.guess_instance(user)
520 520 repo = Repository.guess_instance(repo)
521 521 permission = Permission.guess_instance(perm)
522 522
523 523 # check if we have that permission already
524 524 obj = UserRepoToPerm.query() \
525 525 .filter(UserRepoToPerm.user == user) \
526 526 .filter(UserRepoToPerm.repository == repo) \
527 527 .scalar()
528 528 if obj is None:
529 529 # create new !
530 530 obj = UserRepoToPerm()
531 531 Session().add(obj)
532 532 obj.repository = repo
533 533 obj.user = user
534 534 obj.permission = permission
535 535 log.debug('Granted perm %s to %s on %s', perm, user, repo)
536 536 return obj
537 537
538 538 def revoke_user_permission(self, repo, user):
539 539 """
540 540 Revoke permission for user on given repository
541 541
542 542 :param repo: Instance of Repository, repository_id, or repository name
543 543 :param user: Instance of User, user_id or username
544 544 """
545 545
546 546 user = User.guess_instance(user)
547 547 repo = Repository.guess_instance(repo)
548 548
549 549 obj = UserRepoToPerm.query() \
550 550 .filter(UserRepoToPerm.repository == repo) \
551 551 .filter(UserRepoToPerm.user == user) \
552 552 .scalar()
553 553 if obj is not None:
554 554 Session().delete(obj)
555 555 log.debug('Revoked perm on %s on %s', repo, user)
556 556
557 557 def grant_user_group_permission(self, repo, group_name, perm):
558 558 """
559 559 Grant permission for user group on given repository, or update
560 560 existing one if found
561 561
562 562 :param repo: Instance of Repository, repository_id, or repository name
563 563 :param group_name: Instance of UserGroup, users_group_id,
564 564 or user group name
565 565 :param perm: Instance of Permission, or permission_name
566 566 """
567 567 repo = Repository.guess_instance(repo)
568 568 group_name = UserGroup.guess_instance(group_name)
569 569 permission = Permission.guess_instance(perm)
570 570
571 571 # check if we have that permission already
572 572 obj = UserGroupRepoToPerm.query() \
573 573 .filter(UserGroupRepoToPerm.users_group == group_name) \
574 574 .filter(UserGroupRepoToPerm.repository == repo) \
575 575 .scalar()
576 576
577 577 if obj is None:
578 578 # create new
579 579 obj = UserGroupRepoToPerm()
580 580 Session().add(obj)
581 581
582 582 obj.repository = repo
583 583 obj.users_group = group_name
584 584 obj.permission = permission
585 585 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
586 586 return obj
587 587
588 588 def revoke_user_group_permission(self, repo, group_name):
589 589 """
590 590 Revoke permission for user group on given repository
591 591
592 592 :param repo: Instance of Repository, repository_id, or repository name
593 593 :param group_name: Instance of UserGroup, users_group_id,
594 594 or user group name
595 595 """
596 596 repo = Repository.guess_instance(repo)
597 597 group_name = UserGroup.guess_instance(group_name)
598 598
599 599 obj = UserGroupRepoToPerm.query() \
600 600 .filter(UserGroupRepoToPerm.repository == repo) \
601 601 .filter(UserGroupRepoToPerm.users_group == group_name) \
602 602 .scalar()
603 603 if obj is not None:
604 604 Session().delete(obj)
605 605 log.debug('Revoked perm to %s on %s', repo, group_name)
606 606
607 607 def delete_stats(self, repo_name):
608 608 """
609 609 removes stats for given repo
610 610
611 611 :param repo_name:
612 612 """
613 613 repo = Repository.guess_instance(repo_name)
614 614 try:
615 615 obj = Statistics.query() \
616 616 .filter(Statistics.repository == repo).scalar()
617 617 if obj is not None:
618 618 Session().delete(obj)
619 619 except Exception:
620 620 log.error(traceback.format_exc())
621 621 raise
622 622
623 623 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
624 624 clone_uri=None, repo_store_location=None):
625 625 """
626 626 Makes repository on filesystem. Operation is group aware, meaning that it will create
627 627 a repository within a group, and alter the paths accordingly to the group location.
628 628
629 629 Note: clone_uri is low level and not validated - it might be a file system path used for validated cloning
630 630 """
631 631 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
632 632 from kallithea.model.scm import ScmModel
633 633
634 634 if '/' in repo_name:
635 635 raise ValueError('repo_name must not contain groups got `%s`' % repo_name)
636 636
637 637 if isinstance(repo_group, RepoGroup):
638 638 new_parent_path = os.sep.join(repo_group.full_path_splitted)
639 639 else:
640 640 new_parent_path = repo_group or ''
641 641
642 642 if repo_store_location:
643 643 _paths = [repo_store_location]
644 644 else:
645 645 _paths = [self.repos_path, new_parent_path, repo_name]
646 646 # we need to make it str for mercurial
647 647 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
648 648
649 649 # check if this path is not a repository
650 650 if is_valid_repo(repo_path, self.repos_path):
651 651 raise Exception('This path %s is a valid repository' % repo_path)
652 652
653 653 # check if this path is a group
654 654 if is_valid_repo_group(repo_path, self.repos_path):
655 655 raise Exception('This path %s is a valid group' % repo_path)
656 656
657 657 log.info('creating repo %s in %s from url: `%s`',
658 658 repo_name, safe_unicode(repo_path),
659 659 obfuscate_url_pw(clone_uri))
660 660
661 661 backend = get_backend(repo_type)
662 662
663 663 if repo_type == 'hg':
664 baseui = make_ui(clear_session=False)
664 baseui = make_ui()
665 665 # patch and reset hooks section of UI config to not run any
666 666 # hooks on creating remote repo
667 667 for k, v in baseui.configitems('hooks'):
668 668 baseui.setconfig('hooks', k, None)
669 669
670 670 repo = backend(repo_path, create=True, src_url=clone_uri, baseui=baseui)
671 671 elif repo_type == 'git':
672 672 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
673 673 # add kallithea hook into this repo
674 674 ScmModel().install_git_hooks(repo=repo)
675 675 else:
676 676 raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
677 677
678 678 log.debug('Created repo %s with %s backend',
679 679 safe_unicode(repo_name), safe_unicode(repo_type))
680 680 return repo
681 681
682 682 def _rename_filesystem_repo(self, old, new):
683 683 """
684 684 renames repository on filesystem
685 685
686 686 :param old: old name
687 687 :param new: new name
688 688 """
689 689 log.info('renaming repo from %s to %s', old, new)
690 690
691 691 old_path = safe_str(os.path.join(self.repos_path, old))
692 692 new_path = safe_str(os.path.join(self.repos_path, new))
693 693 if os.path.isdir(new_path):
694 694 raise Exception(
695 695 'Was trying to rename to already existing dir %s' % new_path
696 696 )
697 697 shutil.move(old_path, new_path)
698 698
699 699 def _delete_filesystem_repo(self, repo):
700 700 """
701 701 removes repo from filesystem, the removal is actually done by
702 702 renaming dir to a 'rm__*' prefix which Kallithea will skip.
703 703 It can be undeleted later by reverting the rename.
704 704
705 705 :param repo: repo object
706 706 """
707 707 rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
708 708 log.info("Removing %s", rm_path)
709 709
710 710 _now = datetime.now()
711 711 _ms = str(_now.microsecond).rjust(6, '0')
712 712 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
713 713 repo.just_name)
714 714 if repo.group:
715 715 args = repo.group.full_path_splitted + [_d]
716 716 _d = os.path.join(*args)
717 717 if os.path.exists(rm_path):
718 718 shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
719 719 else:
720 720 log.error("Can't find repo to delete in %r", rm_path)
@@ -1,804 +1,804 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Set of generic validators
16 16 """
17 17
18 18 import logging
19 19 import os
20 20 import re
21 21 from collections import defaultdict
22 22
23 23 import formencode
24 24 import ipaddr
25 25 import sqlalchemy
26 26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
27 27 from sqlalchemy import func
28 28 from tg.i18n import ugettext as _
29 29
30 30 from kallithea.config.routing import ADMIN_PREFIX
31 31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
32 32 from kallithea.lib.compat import OrderedSet
33 33 from kallithea.lib.exceptions import LdapImportError
34 34 from kallithea.lib.utils import is_valid_repo_uri
35 35 from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
36 36 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
37 37
38 38
39 39 # silence warnings and pylint
40 40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
41 41 NotEmpty, IPAddress, CIDR, String, FancyValidator
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def UniqueListFromString():
47 47 class _UniqueListFromString(formencode.FancyValidator):
48 48 """
49 49 Split value on ',' and make unique while preserving order
50 50 """
51 51 messages = dict(
52 52 empty=_('Value cannot be an empty list'),
53 53 missing_value=_('Value cannot be an empty list'),
54 54 )
55 55
56 56 def _convert_to_python(self, value, state):
57 57 value = aslist(value, ',')
58 58 seen = set()
59 59 return [c for c in value if not (c in seen or seen.add(c))]
60 60
61 61 def empty_value(self, value):
62 62 return []
63 63
64 64 return _UniqueListFromString
65 65
66 66
67 67 def ValidUsername(edit=False, old_data=None):
68 68 old_data = old_data or {}
69 69
70 70 class _validator(formencode.validators.FancyValidator):
71 71 messages = {
72 72 'username_exists': _('Username "%(username)s" already exists'),
73 73 'system_invalid_username':
74 74 _('Username "%(username)s" cannot be used'),
75 75 'invalid_username':
76 76 _('Username may only contain alphanumeric characters '
77 77 'underscores, periods or dashes and must begin with an '
78 78 'alphanumeric character or underscore')
79 79 }
80 80
81 81 def _validate_python(self, value, state):
82 82 if value in ['default', 'new_user']:
83 83 msg = self.message('system_invalid_username', state, username=value)
84 84 raise formencode.Invalid(msg, value, state)
85 85 # check if user is unique
86 86 old_un = None
87 87 if edit:
88 88 old_un = User.get(old_data.get('user_id')).username
89 89
90 90 if old_un != value or not edit:
91 91 if User.get_by_username(value, case_insensitive=True):
92 92 msg = self.message('username_exists', state, username=value)
93 93 raise formencode.Invalid(msg, value, state)
94 94
95 95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
96 96 msg = self.message('invalid_username', state)
97 97 raise formencode.Invalid(msg, value, state)
98 98 return _validator
99 99
100 100
101 101 def ValidRegex(msg=None):
102 102 class _validator(formencode.validators.Regex):
103 103 messages = dict(invalid=msg or _('The input is not valid'))
104 104 return _validator
105 105
106 106
107 107 def ValidRepoUser():
108 108 class _validator(formencode.validators.FancyValidator):
109 109 messages = {
110 110 'invalid_username': _('Username %(username)s is not valid')
111 111 }
112 112
113 113 def _validate_python(self, value, state):
114 114 try:
115 115 User.query().filter(User.active == True) \
116 116 .filter(User.username == value).one()
117 117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
118 118 msg = self.message('invalid_username', state, username=value)
119 119 raise formencode.Invalid(msg, value, state,
120 120 error_dict=dict(username=msg)
121 121 )
122 122
123 123 return _validator
124 124
125 125
126 126 def ValidUserGroup(edit=False, old_data=None):
127 127 old_data = old_data or {}
128 128
129 129 class _validator(formencode.validators.FancyValidator):
130 130 messages = {
131 131 'invalid_group': _('Invalid user group name'),
132 132 'group_exist': _('User group "%(usergroup)s" already exists'),
133 133 'invalid_usergroup_name':
134 134 _('user group name may only contain alphanumeric '
135 135 'characters underscores, periods or dashes and must begin '
136 136 'with alphanumeric character')
137 137 }
138 138
139 139 def _validate_python(self, value, state):
140 140 if value in ['default']:
141 141 msg = self.message('invalid_group', state)
142 142 raise formencode.Invalid(msg, value, state,
143 143 error_dict=dict(users_group_name=msg)
144 144 )
145 145 # check if group is unique
146 146 old_ugname = None
147 147 if edit:
148 148 old_id = old_data.get('users_group_id')
149 149 old_ugname = UserGroup.get(old_id).users_group_name
150 150
151 151 if old_ugname != value or not edit:
152 152 is_existing_group = UserGroup.get_by_group_name(value,
153 153 case_insensitive=True)
154 154 if is_existing_group:
155 155 msg = self.message('group_exist', state, usergroup=value)
156 156 raise formencode.Invalid(msg, value, state,
157 157 error_dict=dict(users_group_name=msg)
158 158 )
159 159
160 160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
161 161 msg = self.message('invalid_usergroup_name', state)
162 162 raise formencode.Invalid(msg, value, state,
163 163 error_dict=dict(users_group_name=msg)
164 164 )
165 165
166 166 return _validator
167 167
168 168
169 169 def ValidRepoGroup(edit=False, old_data=None):
170 170 old_data = old_data or {}
171 171
172 172 class _validator(formencode.validators.FancyValidator):
173 173 messages = {
174 174 'parent_group_id': _('Cannot assign this group as parent'),
175 175 'group_exists': _('Group "%(group_name)s" already exists'),
176 176 'repo_exists':
177 177 _('Repository with name "%(group_name)s" already exists')
178 178 }
179 179
180 180 def _validate_python(self, value, state):
181 181 # TODO WRITE VALIDATIONS
182 182 group_name = value.get('group_name')
183 183 parent_group_id = value.get('parent_group_id')
184 184
185 185 # slugify repo group just in case :)
186 186 slug = repo_name_slug(group_name)
187 187
188 188 # check for parent of self
189 189 parent_of_self = lambda: (
190 190 old_data['group_id'] == parent_group_id
191 191 if parent_group_id else False
192 192 )
193 193 if edit and parent_of_self():
194 194 msg = self.message('parent_group_id', state)
195 195 raise formencode.Invalid(msg, value, state,
196 196 error_dict=dict(parent_group_id=msg)
197 197 )
198 198
199 199 old_gname = None
200 200 if edit:
201 201 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
202 202
203 203 if old_gname != group_name or not edit:
204 204
205 205 # check group
206 206 gr = RepoGroup.query() \
207 207 .filter(func.lower(RepoGroup.group_name) == func.lower(slug)) \
208 208 .filter(RepoGroup.parent_group_id == parent_group_id) \
209 209 .scalar()
210 210 if gr is not None:
211 211 msg = self.message('group_exists', state, group_name=slug)
212 212 raise formencode.Invalid(msg, value, state,
213 213 error_dict=dict(group_name=msg)
214 214 )
215 215
216 216 # check for same repo
217 217 repo = Repository.query() \
218 218 .filter(func.lower(Repository.repo_name) == func.lower(slug)) \
219 219 .scalar()
220 220 if repo is not None:
221 221 msg = self.message('repo_exists', state, group_name=slug)
222 222 raise formencode.Invalid(msg, value, state,
223 223 error_dict=dict(group_name=msg)
224 224 )
225 225
226 226 return _validator
227 227
228 228
229 229 def ValidPassword():
230 230 class _validator(formencode.validators.FancyValidator):
231 231 messages = {
232 232 'invalid_password':
233 233 _('Invalid characters (non-ascii) in password')
234 234 }
235 235
236 236 def _validate_python(self, value, state):
237 237 try:
238 238 (value or '').decode('ascii')
239 239 except UnicodeError:
240 240 msg = self.message('invalid_password', state)
241 241 raise formencode.Invalid(msg, value, state,)
242 242 return _validator
243 243
244 244
245 245 def ValidOldPassword(username):
246 246 class _validator(formencode.validators.FancyValidator):
247 247 messages = {
248 248 'invalid_password': _('Invalid old password')
249 249 }
250 250
251 251 def _validate_python(self, value, state):
252 252 from kallithea.lib import auth_modules
253 253 if auth_modules.authenticate(username, value, '') is None:
254 254 msg = self.message('invalid_password', state)
255 255 raise formencode.Invalid(msg, value, state,
256 256 error_dict=dict(current_password=msg)
257 257 )
258 258 return _validator
259 259
260 260
261 261 def ValidPasswordsMatch(password_field, password_confirmation_field):
262 262 class _validator(formencode.validators.FancyValidator):
263 263 messages = {
264 264 'password_mismatch': _('Passwords do not match'),
265 265 }
266 266
267 267 def _validate_python(self, value, state):
268 268 if value.get(password_field) != value[password_confirmation_field]:
269 269 msg = self.message('password_mismatch', state)
270 270 raise formencode.Invalid(msg, value, state,
271 271 error_dict={password_field: msg, password_confirmation_field: msg}
272 272 )
273 273 return _validator
274 274
275 275
276 276 def ValidAuth():
277 277 class _validator(formencode.validators.FancyValidator):
278 278 messages = {
279 279 'invalid_auth': _(u'Invalid username or password'),
280 280 }
281 281
282 282 def _validate_python(self, value, state):
283 283 from kallithea.lib import auth_modules
284 284
285 285 password = value['password']
286 286 username = value['username']
287 287
288 288 # authenticate returns unused dict but has called
289 289 # plugin._authenticate which has create_or_update'ed the username user in db
290 290 if auth_modules.authenticate(username, password) is None:
291 291 user = User.get_by_username_or_email(username)
292 292 if user and not user.active:
293 293 log.warning('user %s is disabled', username)
294 294 msg = self.message('invalid_auth', state)
295 295 raise formencode.Invalid(msg, value, state,
296 296 error_dict=dict(username=' ', password=msg)
297 297 )
298 298 else:
299 299 log.warning('user %s failed to authenticate', username)
300 300 msg = self.message('invalid_auth', state)
301 301 raise formencode.Invalid(msg, value, state,
302 302 error_dict=dict(username=' ', password=msg)
303 303 )
304 304 return _validator
305 305
306 306
307 307 def ValidRepoName(edit=False, old_data=None):
308 308 old_data = old_data or {}
309 309
310 310 class _validator(formencode.validators.FancyValidator):
311 311 messages = {
312 312 'invalid_repo_name':
313 313 _('Repository name %(repo)s is not allowed'),
314 314 'repository_exists':
315 315 _('Repository named %(repo)s already exists'),
316 316 'repository_in_group_exists': _('Repository "%(repo)s" already '
317 317 'exists in group "%(group)s"'),
318 318 'same_group_exists': _('Repository group with name "%(repo)s" '
319 319 'already exists')
320 320 }
321 321
322 322 def _convert_to_python(self, value, state):
323 323 repo_name = repo_name_slug(value.get('repo_name', ''))
324 324 repo_group = value.get('repo_group')
325 325 if repo_group:
326 326 gr = RepoGroup.get(repo_group)
327 327 group_path = gr.full_path
328 328 group_name = gr.group_name
329 329 # value needs to be aware of group name in order to check
330 330 # db key This is an actual just the name to store in the
331 331 # database
332 332 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
333 333 else:
334 334 group_name = group_path = ''
335 335 repo_name_full = repo_name
336 336
337 337 value['repo_name'] = repo_name
338 338 value['repo_name_full'] = repo_name_full
339 339 value['group_path'] = group_path
340 340 value['group_name'] = group_name
341 341 return value
342 342
343 343 def _validate_python(self, value, state):
344 344 repo_name = value.get('repo_name')
345 345 repo_name_full = value.get('repo_name_full')
346 346 group_path = value.get('group_path')
347 347 group_name = value.get('group_name')
348 348
349 349 if repo_name in [ADMIN_PREFIX, '']:
350 350 msg = self.message('invalid_repo_name', state, repo=repo_name)
351 351 raise formencode.Invalid(msg, value, state,
352 352 error_dict=dict(repo_name=msg)
353 353 )
354 354
355 355 rename = old_data.get('repo_name') != repo_name_full
356 356 create = not edit
357 357 if rename or create:
358 358 repo = Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
359 359 repo_group = RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
360 360 if group_path != '':
361 361 if repo is not None:
362 362 msg = self.message('repository_in_group_exists', state,
363 363 repo=repo.repo_name, group=group_name)
364 364 raise formencode.Invalid(msg, value, state,
365 365 error_dict=dict(repo_name=msg)
366 366 )
367 367 elif repo_group is not None:
368 368 msg = self.message('same_group_exists', state,
369 369 repo=repo_name)
370 370 raise formencode.Invalid(msg, value, state,
371 371 error_dict=dict(repo_name=msg)
372 372 )
373 373 elif repo is not None:
374 374 msg = self.message('repository_exists', state,
375 375 repo=repo.repo_name)
376 376 raise formencode.Invalid(msg, value, state,
377 377 error_dict=dict(repo_name=msg)
378 378 )
379 379 return value
380 380 return _validator
381 381
382 382
383 383 def ValidForkName(*args, **kwargs):
384 384 return ValidRepoName(*args, **kwargs)
385 385
386 386
387 387 def SlugifyName():
388 388 class _validator(formencode.validators.FancyValidator):
389 389
390 390 def _convert_to_python(self, value, state):
391 391 return repo_name_slug(value)
392 392
393 393 def _validate_python(self, value, state):
394 394 pass
395 395
396 396 return _validator
397 397
398 398
399 399 def ValidCloneUri():
400 400 from kallithea.lib.utils import make_ui
401 401
402 402 class _validator(formencode.validators.FancyValidator):
403 403 messages = {
404 404 'clone_uri': _('Invalid repository URL'),
405 405 'invalid_clone_uri': _('Invalid repository URL. It must be a '
406 406 'valid http, https, ssh, svn+http or svn+https URL'),
407 407 }
408 408
409 409 def _validate_python(self, value, state):
410 410 repo_type = value.get('repo_type')
411 411 url = value.get('clone_uri')
412 412
413 413 if url and url != value.get('clone_uri_hidden'):
414 414 try:
415 is_valid_repo_uri(repo_type, url, make_ui(clear_session=False))
415 is_valid_repo_uri(repo_type, url, make_ui())
416 416 except Exception:
417 417 log.exception('URL validation failed')
418 418 msg = self.message('clone_uri', state)
419 419 raise formencode.Invalid(msg, value, state,
420 420 error_dict=dict(clone_uri=msg)
421 421 )
422 422 return _validator
423 423
424 424
425 425 def ValidForkType(old_data=None):
426 426 old_data = old_data or {}
427 427
428 428 class _validator(formencode.validators.FancyValidator):
429 429 messages = {
430 430 'invalid_fork_type': _('Fork has to be the same type as parent')
431 431 }
432 432
433 433 def _validate_python(self, value, state):
434 434 if old_data['repo_type'] != value:
435 435 msg = self.message('invalid_fork_type', state)
436 436 raise formencode.Invalid(msg, value, state,
437 437 error_dict=dict(repo_type=msg)
438 438 )
439 439 return _validator
440 440
441 441
442 442 def CanWriteGroup(old_data=None):
443 443 class _validator(formencode.validators.FancyValidator):
444 444 messages = {
445 445 'permission_denied': _("You don't have permissions "
446 446 "to create repository in this group"),
447 447 'permission_denied_root': _("no permission to create repository "
448 448 "in root location")
449 449 }
450 450
451 451 def _convert_to_python(self, value, state):
452 452 # root location
453 453 if value == -1:
454 454 return None
455 455 return value
456 456
457 457 def _validate_python(self, value, state):
458 458 gr = RepoGroup.get(value)
459 459 gr_name = gr.group_name if gr is not None else None # None means ROOT location
460 460
461 461 # create repositories with write permission on group is set to true
462 462 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
463 463 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
464 464 'can write into group validator')
465 465 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
466 466 'can write into group validator')
467 467 forbidden = not (group_admin or (group_write and create_on_write))
468 468 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
469 469 gid = (old_data['repo_group'].get('group_id')
470 470 if (old_data and 'repo_group' in old_data) else None)
471 471 value_changed = gid != value
472 472 new = not old_data
473 473 # do check if we changed the value, there's a case that someone got
474 474 # revoked write permissions to a repository, he still created, we
475 475 # don't need to check permission if he didn't change the value of
476 476 # groups in form box
477 477 if value_changed or new:
478 478 # parent group need to be existing
479 479 if gr and forbidden:
480 480 msg = self.message('permission_denied', state)
481 481 raise formencode.Invalid(msg, value, state,
482 482 error_dict=dict(repo_type=msg)
483 483 )
484 484 ## check if we can write to root location !
485 485 elif gr is None and not can_create_repos():
486 486 msg = self.message('permission_denied_root', state)
487 487 raise formencode.Invalid(msg, value, state,
488 488 error_dict=dict(repo_type=msg)
489 489 )
490 490
491 491 return _validator
492 492
493 493
494 494 def CanCreateGroup(can_create_in_root=False):
495 495 class _validator(formencode.validators.FancyValidator):
496 496 messages = {
497 497 'permission_denied': _("You don't have permissions "
498 498 "to create a group in this location")
499 499 }
500 500
501 501 def to_python(self, value, state):
502 502 # root location
503 503 if value == -1:
504 504 return None
505 505 return value
506 506
507 507 def _validate_python(self, value, state):
508 508 gr = RepoGroup.get(value)
509 509 gr_name = gr.group_name if gr is not None else None # None means ROOT location
510 510
511 511 if can_create_in_root and gr is None:
512 512 # we can create in root, we're fine no validations required
513 513 return
514 514
515 515 forbidden_in_root = gr is None and not can_create_in_root
516 516 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
517 517 if forbidden_in_root or forbidden:
518 518 msg = self.message('permission_denied', state)
519 519 raise formencode.Invalid(msg, value, state,
520 520 error_dict=dict(parent_group_id=msg)
521 521 )
522 522
523 523 return _validator
524 524
525 525
526 526 def ValidPerms(type_='repo'):
527 527 if type_ == 'repo_group':
528 528 EMPTY_PERM = 'group.none'
529 529 elif type_ == 'repo':
530 530 EMPTY_PERM = 'repository.none'
531 531 elif type_ == 'user_group':
532 532 EMPTY_PERM = 'usergroup.none'
533 533
534 534 class _validator(formencode.validators.FancyValidator):
535 535 messages = {
536 536 'perm_new_member_name':
537 537 _('This username or user group name is not valid')
538 538 }
539 539
540 540 def to_python(self, value, state):
541 541 perms_update = OrderedSet()
542 542 perms_new = OrderedSet()
543 543 # build a list of permission to update and new permission to create
544 544
545 545 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
546 546 new_perms_group = defaultdict(dict)
547 547 for k, v in value.copy().iteritems():
548 548 if k.startswith('perm_new_member'):
549 549 del value[k]
550 550 _type, part = k.split('perm_new_member_')
551 551 args = part.split('_')
552 552 if len(args) == 1:
553 553 new_perms_group[args[0]]['perm'] = v
554 554 elif len(args) == 2:
555 555 _key, pos = args
556 556 new_perms_group[pos][_key] = v
557 557
558 558 # fill new permissions in order of how they were added
559 559 for k in sorted(map(int, new_perms_group.keys())):
560 560 perm_dict = new_perms_group[str(k)]
561 561 new_member = perm_dict.get('name')
562 562 new_perm = perm_dict.get('perm')
563 563 new_type = perm_dict.get('type')
564 564 if new_member and new_perm and new_type:
565 565 perms_new.add((new_member, new_perm, new_type))
566 566
567 567 for k, v in value.iteritems():
568 568 if k.startswith('u_perm_') or k.startswith('g_perm_'):
569 569 member = k[7:]
570 570 t = {'u': 'user',
571 571 'g': 'users_group'
572 572 }[k[0]]
573 573 if member == User.DEFAULT_USER:
574 574 if str2bool(value.get('repo_private')):
575 575 # set none for default when updating to
576 576 # private repo protects against form manipulation
577 577 v = EMPTY_PERM
578 578 perms_update.add((member, v, t))
579 579
580 580 value['perms_updates'] = list(perms_update)
581 581 value['perms_new'] = list(perms_new)
582 582
583 583 # update permissions
584 584 for k, v, t in perms_new:
585 585 try:
586 586 if t == 'user':
587 587 self.user_db = User.query() \
588 588 .filter(User.active == True) \
589 589 .filter(User.username == k).one()
590 590 if t == 'users_group':
591 591 self.user_db = UserGroup.query() \
592 592 .filter(UserGroup.users_group_active == True) \
593 593 .filter(UserGroup.users_group_name == k).one()
594 594
595 595 except Exception:
596 596 log.exception('Updated permission failed')
597 597 msg = self.message('perm_new_member_type', state)
598 598 raise formencode.Invalid(msg, value, state,
599 599 error_dict=dict(perm_new_member_name=msg)
600 600 )
601 601 return value
602 602 return _validator
603 603
604 604
605 605 def ValidSettings():
606 606 class _validator(formencode.validators.FancyValidator):
607 607 def _convert_to_python(self, value, state):
608 608 # settings form for users that are not admin
609 609 # can't edit certain parameters, it's extra backup if they mangle
610 610 # with forms
611 611
612 612 forbidden_params = [
613 613 'user', 'repo_type',
614 614 'repo_enable_downloads', 'repo_enable_statistics'
615 615 ]
616 616
617 617 for param in forbidden_params:
618 618 if param in value:
619 619 del value[param]
620 620 return value
621 621
622 622 def _validate_python(self, value, state):
623 623 pass
624 624 return _validator
625 625
626 626
627 627 def ValidPath():
628 628 class _validator(formencode.validators.FancyValidator):
629 629 messages = {
630 630 'invalid_path': _('This is not a valid path')
631 631 }
632 632
633 633 def _validate_python(self, value, state):
634 634 if not os.path.isdir(value):
635 635 msg = self.message('invalid_path', state)
636 636 raise formencode.Invalid(msg, value, state,
637 637 error_dict=dict(paths_root_path=msg)
638 638 )
639 639 return _validator
640 640
641 641
642 642 def UniqSystemEmail(old_data=None):
643 643 old_data = old_data or {}
644 644
645 645 class _validator(formencode.validators.FancyValidator):
646 646 messages = {
647 647 'email_taken': _('This email address is already in use')
648 648 }
649 649
650 650 def _convert_to_python(self, value, state):
651 651 return value.lower()
652 652
653 653 def _validate_python(self, value, state):
654 654 if (old_data.get('email') or '').lower() != value:
655 655 user = User.get_by_email(value)
656 656 if user is not None:
657 657 msg = self.message('email_taken', state)
658 658 raise formencode.Invalid(msg, value, state,
659 659 error_dict=dict(email=msg)
660 660 )
661 661 return _validator
662 662
663 663
664 664 def ValidSystemEmail():
665 665 class _validator(formencode.validators.FancyValidator):
666 666 messages = {
667 667 'non_existing_email': _('Email address "%(email)s" not found')
668 668 }
669 669
670 670 def _convert_to_python(self, value, state):
671 671 return value.lower()
672 672
673 673 def _validate_python(self, value, state):
674 674 user = User.get_by_email(value)
675 675 if user is None:
676 676 msg = self.message('non_existing_email', state, email=value)
677 677 raise formencode.Invalid(msg, value, state,
678 678 error_dict=dict(email=msg)
679 679 )
680 680
681 681 return _validator
682 682
683 683
684 684 def LdapLibValidator():
685 685 class _validator(formencode.validators.FancyValidator):
686 686 messages = {
687 687
688 688 }
689 689
690 690 def _validate_python(self, value, state):
691 691 try:
692 692 import ldap
693 693 ldap # pyflakes silence !
694 694 except ImportError:
695 695 raise LdapImportError()
696 696
697 697 return _validator
698 698
699 699
700 700 def AttrLoginValidator():
701 701 class _validator(formencode.validators.UnicodeString):
702 702 messages = {
703 703 'invalid_cn':
704 704 _('The LDAP Login attribute of the CN must be specified - '
705 705 'this is the name of the attribute that is equivalent '
706 706 'to "username"')
707 707 }
708 708 messages['empty'] = messages['invalid_cn']
709 709
710 710 return _validator
711 711
712 712
713 713 def ValidIp():
714 714 class _validator(CIDR):
715 715 messages = dict(
716 716 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
717 717 illegalBits=_('The network size (bits) must be within the range'
718 718 ' of 0-32 (not %(bits)r)')
719 719 )
720 720
721 721 def to_python(self, value, state):
722 722 v = super(_validator, self).to_python(value, state)
723 723 v = v.strip()
724 724 net = ipaddr.IPNetwork(address=v)
725 725 if isinstance(net, ipaddr.IPv4Network):
726 726 # if IPv4 doesn't end with a mask, add /32
727 727 if '/' not in value:
728 728 v += '/32'
729 729 if isinstance(net, ipaddr.IPv6Network):
730 730 # if IPv6 doesn't end with a mask, add /128
731 731 if '/' not in value:
732 732 v += '/128'
733 733 return v
734 734
735 735 def _validate_python(self, value, state):
736 736 try:
737 737 addr = value.strip()
738 738 # this raises an ValueError if address is not IPv4 or IPv6
739 739 ipaddr.IPNetwork(address=addr)
740 740 except ValueError:
741 741 raise formencode.Invalid(self.message('badFormat', state),
742 742 value, state)
743 743
744 744 return _validator
745 745
746 746
747 747 def FieldKey():
748 748 class _validator(formencode.validators.FancyValidator):
749 749 messages = dict(
750 750 badFormat=_('Key name can only consist of letters, '
751 751 'underscore, dash or numbers')
752 752 )
753 753
754 754 def _validate_python(self, value, state):
755 755 if not re.match('[a-zA-Z0-9_-]+$', value):
756 756 raise formencode.Invalid(self.message('badFormat', state),
757 757 value, state)
758 758 return _validator
759 759
760 760
761 761 def BasePath():
762 762 class _validator(formencode.validators.FancyValidator):
763 763 messages = dict(
764 764 badPath=_('Filename cannot be inside a directory')
765 765 )
766 766
767 767 def _convert_to_python(self, value, state):
768 768 return value
769 769
770 770 def _validate_python(self, value, state):
771 771 if value != os.path.basename(value):
772 772 raise formencode.Invalid(self.message('badPath', state),
773 773 value, state)
774 774 return _validator
775 775
776 776
777 777 def ValidAuthPlugins():
778 778 class _validator(formencode.validators.FancyValidator):
779 779 messages = dict(
780 780 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
781 781 )
782 782
783 783 def _convert_to_python(self, value, state):
784 784 # filter empty values
785 785 return filter(lambda s: s not in [None, ''], value)
786 786
787 787 def _validate_python(self, value, state):
788 788 from kallithea.lib import auth_modules
789 789 module_list = value
790 790 unique_names = {}
791 791 try:
792 792 for module in module_list:
793 793 plugin = auth_modules.loadplugin(module)
794 794 plugin_name = plugin.name
795 795 if plugin_name in unique_names:
796 796 msg = self.message('import_duplicate', state,
797 797 loaded=unique_names[plugin_name],
798 798 next_to_load=plugin_name)
799 799 raise formencode.Invalid(msg, value, state)
800 800 unique_names[plugin_name] = plugin
801 801 except (ImportError, AttributeError, TypeError) as e:
802 802 raise formencode.Invalid(str(e), value, state)
803 803
804 804 return _validator
General Comments 0
You need to be logged in to leave comments. Login now