1# Copyright (c) 2012-2016 Seafile Ltd. 2import operator 3import datetime 4import logging 5import os 6 7from django.db import models 8from django.db.models import Q 9from django.utils import timezone 10from django.utils.translation import ugettext_lazy as _ 11from django.contrib.auth.hashers import make_password, check_password 12from constance import config 13 14from seahub.base.fields import LowerCaseCharField 15from seahub.utils import normalize_file_path, normalize_dir_path, gen_token,\ 16 get_service_url 17from seahub.constants import PERMISSION_READ, PERMISSION_ADMIN 18from seahub.utils import is_valid_org_id 19from functools import reduce 20 21# Get an instance of a logger 22logger = logging.getLogger(__name__) 23 24 25class AnonymousShare(models.Model): 26 """ 27 Model used for sharing repo to unregistered email. 28 """ 29 repo_owner = LowerCaseCharField(max_length=255) 30 repo_id = models.CharField(max_length=36) 31 anonymous_email = LowerCaseCharField(max_length=255) 32 token = models.CharField(max_length=25, unique=True) 33 34def _get_link_key(token, is_upload_link=False): 35 return 'visited_ufs_' + token if is_upload_link else \ 36 'visited_fs_' + token 37 38def set_share_link_access(request, token, is_upload_link=False): 39 """Remember which shared download/upload link user can access without 40 providing password. 41 """ 42 if request.session: 43 link_key = _get_link_key(token, is_upload_link) 44 request.session[link_key] = True 45 else: 46 # should never reach here in normal case 47 logger.warn('Failed to remember shared link password, request.session' 48 ' is None when set shared link access.') 49 50def check_share_link_access(request, token, is_upload_link=False): 51 """Check whether user can access shared download/upload link without 52 providing password. 53 """ 54 link_key = _get_link_key(token, is_upload_link) 55 if request.session.get(link_key, False): 56 return True 57 else: 58 return False 59 60def check_share_link_common(request, sharelink, is_upload_link=False): 61 """Check if user can view a share link 62 """ 63 64 msg = '' 65 if not sharelink.is_encrypted(): 66 return (True, msg) 67 68 # if CAN access shared download/upload link without providing password 69 # return True 70 if check_share_link_access(request, sharelink.token, is_upload_link): 71 return (True, msg) 72 73 if request.method != 'POST': 74 return (False, msg) 75 76 password = request.POST.get('password', None) 77 if not password: 78 msg = _("Password can\'t be empty") 79 return (False, msg) 80 81 if check_password(password, sharelink.password): 82 set_share_link_access(request, sharelink.token, is_upload_link) 83 return (True, msg) 84 else: 85 msg = _("Please enter a correct password.") 86 return (False, msg) 87 88class FileShareManager(models.Manager): 89 def _add_file_share(self, username, repo_id, path, s_type, 90 password=None, expire_date=None, 91 permission='view_download', org_id=None): 92 if password is not None: 93 password_enc = make_password(password) 94 else: 95 password_enc = None 96 97 token = gen_token(max_length=config.SHARE_LINK_TOKEN_LENGTH) 98 fs = super(FileShareManager, self).create( 99 username=username, repo_id=repo_id, path=path, token=token, 100 s_type=s_type, password=password_enc, expire_date=expire_date, 101 permission=permission) 102 fs.save() 103 104 if is_valid_org_id(org_id): 105 OrgFileShare.objects.set_org_file_share(org_id, fs) 106 107 return fs 108 109 def _get_file_share_by_path(self, username, repo_id, path): 110 fs = list(super(FileShareManager, self).filter(repo_id=repo_id).filter( 111 username=username).filter(path=path)) 112 if len(fs) > 0: 113 return fs[0] 114 else: 115 return None 116 117 def _get_valid_file_share_by_token(self, token): 118 """Return share link that exists and not expire, otherwise none. 119 """ 120 try: 121 fs = self.get(token=token) 122 except self.model.DoesNotExist: 123 return None 124 125 if fs.expire_date is None: 126 return fs 127 else: 128 if timezone.now() > fs.expire_date: 129 return None 130 else: 131 return fs 132 133 ########## public methods ########## 134 def create_file_link(self, username, repo_id, path, password=None, 135 expire_date=None, permission='view_download', 136 org_id=None): 137 """Create download link for file. 138 """ 139 path = normalize_file_path(path) 140 return self._add_file_share(username, repo_id, path, 'f', password, 141 expire_date, permission, org_id) 142 143 def get_file_link_by_path(self, username, repo_id, path): 144 path = normalize_file_path(path) 145 return self._get_file_share_by_path(username, repo_id, path) 146 147 def get_valid_file_link_by_token(self, token): 148 return self._get_valid_file_share_by_token(token) 149 150 def create_dir_link(self, username, repo_id, path, password=None, 151 expire_date=None, permission='view_download', 152 org_id=None): 153 """Create download link for directory. 154 """ 155 path = normalize_dir_path(path) 156 return self._add_file_share(username, repo_id, path, 'd', password, 157 expire_date, permission, org_id) 158 159 def get_dir_link_by_path(self, username, repo_id, path): 160 path = normalize_dir_path(path) 161 return self._get_file_share_by_path(username, repo_id, path) 162 163 def get_valid_dir_link_by_token(self, token): 164 return self._get_valid_file_share_by_token(token) 165 166 167class ExtraSharePermissionManager(models.Manager): 168 def get_user_permission(self, repo_id, username): 169 """Get user's permission of a library. 170 return 171 e.g. 'admin' 172 """ 173 record_list = super(ExtraSharePermissionManager, self).filter( 174 repo_id=repo_id, share_to=username 175 ) 176 if len(record_list) > 0: 177 return record_list[0].permission 178 else: 179 return None 180 181 def get_repos_with_admin_permission(self, username): 182 """Get repo id list a user has admin permission. 183 """ 184 shared_repos = super(ExtraSharePermissionManager, self).filter( 185 share_to=username, permission=PERMISSION_ADMIN 186 ) 187 return [e.repo_id for e in shared_repos] 188 189 def get_admin_users_by_repo(self, repo_id): 190 """Gets the share and permissions of the record in the specified repo ID. 191 return 192 e.g. ['admin_user1', 'admin_user2'] 193 """ 194 shared_repos = super(ExtraSharePermissionManager, self).filter( 195 repo_id=repo_id, permission=PERMISSION_ADMIN 196 ) 197 198 return [e.share_to for e in shared_repos] 199 200 def batch_is_admin(self, in_datas): 201 """return the data that input data is admin 202 e.g. 203 in_datas: 204 [(repo_id1, username1), (repo_id2, admin1)] 205 admin permission data returnd: 206 [(repo_id2, admin1)] 207 """ 208 if len(in_datas) <= 0: 209 return [] 210 query = reduce( 211 operator.or_, 212 (Q(repo_id=data[0], share_to=data[1]) for data in in_datas) 213 ) 214 db_data = super(ExtraSharePermissionManager, self).filter(query).filter(permission=PERMISSION_ADMIN) 215 return [(e.repo_id, e.share_to) for e in db_data] 216 217 def create_share_permission(self, repo_id, username, permission): 218 self.model(repo_id=repo_id, share_to=username, 219 permission=permission).save() 220 221 def delete_share_permission(self, repo_id, share_to): 222 super(ExtraSharePermissionManager, self).filter(repo_id=repo_id, 223 share_to=share_to).delete() 224 225 def update_share_permission(self, repo_id, share_to, permission): 226 super(ExtraSharePermissionManager, self).filter(repo_id=repo_id, 227 share_to=share_to).delete() 228 if permission in [PERMISSION_ADMIN]: 229 self.create_share_permission(repo_id, share_to, permission) 230 231 232class ExtraGroupsSharePermissionManager(models.Manager): 233 def get_group_permission(self, repo_id, gid): 234 record_list = super(ExtraGroupsSharePermissionManager, self).filter( 235 repo_id=repo_id, group_id=gid 236 ) 237 if len(record_list) > 0: 238 return record_list[0].permission 239 else: 240 return None 241 242 243 def get_repos_with_admin_permission(self, gid): 244 """ return admin repo in specific group 245 e.g: ['repo_id1', 'repo_id2'] 246 """ 247 return super(ExtraGroupsSharePermissionManager, self).filter( 248 group_id=gid, permission='admin' 249 ).values_list('repo_id', flat=True) 250 251 def get_admin_groups_by_repo(self, repo_id): 252 """ return admin groups in specific repo 253 e.g: ['23', '12'] 254 """ 255 return super(ExtraGroupsSharePermissionManager, self).filter( 256 repo_id=repo_id, permission='admin' 257 ).values_list('group_id', flat=True) 258 259 def batch_get_repos_with_admin_permission(self, gids): 260 """ 261 """ 262 if len(gids) <= 0: 263 return [] 264 db_data = super(ExtraGroupsSharePermissionManager, self).filter(group_id__in=gids, permission=PERMISSION_ADMIN) 265 return [(e.repo_id, e.group_id) for e in db_data] 266 267 def create_share_permission(self, repo_id, gid, permission): 268 self.model(repo_id=repo_id, group_id=gid, permission=permission).save() 269 270 def delete_share_permission(self, repo_id, gid): 271 super(ExtraGroupsSharePermissionManager, self).filter(repo_id=repo_id, 272 group_id=gid).delete() 273 274 def update_share_permission(self, repo_id, gid, permission): 275 super(ExtraGroupsSharePermissionManager, self).filter(repo_id=repo_id, 276 group_id=gid).delete() 277 if permission in [PERMISSION_ADMIN]: 278 self.create_share_permission(repo_id, gid, permission) 279 280 281class ExtraGroupsSharePermission(models.Model): 282 repo_id = models.CharField(max_length=36, db_index=True) 283 group_id = models.IntegerField(db_index=True) 284 permission = models.CharField(max_length=30) 285 objects = ExtraGroupsSharePermissionManager() 286 287 288class ExtraSharePermission(models.Model): 289 repo_id = models.CharField(max_length=36, db_index=True) 290 share_to = models.CharField(max_length=255, db_index=True) 291 permission = models.CharField(max_length=30) 292 objects = ExtraSharePermissionManager() 293 294 295class FileShare(models.Model): 296 """ 297 Model used for file or dir shared link. 298 """ 299 PERM_VIEW_DL = 'view_download' 300 PERM_VIEW_ONLY = 'view_only' 301 302 PERM_EDIT_DL = 'edit_download' 303 PERM_EDIT_ONLY = 'edit_only' 304 305 PERM_VIEW_DL_UPLOAD = 'view_download_upload' 306 307 PERMISSION_CHOICES = ( 308 (PERM_VIEW_DL, 'Preview only and can download'), 309 (PERM_VIEW_ONLY, 'Preview only and disable download'), 310 (PERM_EDIT_DL, 'Edit and can download'), 311 (PERM_EDIT_ONLY, 'Edit and disable download'), 312 (PERM_VIEW_DL_UPLOAD, 'Preview only and can download and upload'), 313 ) 314 315 username = LowerCaseCharField(max_length=255, db_index=True) 316 repo_id = models.CharField(max_length=36, db_index=True) 317 path = models.TextField() 318 token = models.CharField(max_length=100, unique=True) 319 ctime = models.DateTimeField(default=datetime.datetime.now) 320 view_cnt = models.IntegerField(default=0) 321 s_type = models.CharField(max_length=2, db_index=True, default='f') # `f` or `d` 322 password = models.CharField(max_length=128, null=True) 323 expire_date = models.DateTimeField(null=True) 324 permission = models.CharField(max_length=50, db_index=True, 325 choices=PERMISSION_CHOICES, 326 default=PERM_VIEW_DL) 327 328 objects = FileShareManager() 329 330 def is_file_share_link(self): 331 return True if self.s_type == 'f' else False 332 333 def is_dir_share_link(self): 334 return False if self.is_file_share_link() else True 335 336 def is_encrypted(self): 337 return True if self.password is not None else False 338 339 def is_expired(self): 340 if self.expire_date is not None and timezone.now() > self.expire_date: 341 return True 342 else: 343 return False 344 345 def is_owner(self, owner): 346 return owner == self.username 347 348 def get_full_url(self): 349 service_url = get_service_url().rstrip('/') 350 if self.is_file_share_link(): 351 return '%s/f/%s/' % (service_url, self.token) 352 else: 353 return '%s/d/%s/' % (service_url, self.token) 354 355 def get_permissions(self): 356 perm_dict = {} 357 if self.permission == FileShare.PERM_VIEW_DL: 358 perm_dict['can_edit'] = False 359 perm_dict['can_download'] = True 360 perm_dict['can_upload'] = False 361 elif self.permission == FileShare.PERM_VIEW_ONLY: 362 perm_dict['can_edit'] = False 363 perm_dict['can_download'] = False 364 perm_dict['can_upload'] = False 365 elif self.permission == FileShare.PERM_EDIT_DL: 366 perm_dict['can_edit'] = True 367 perm_dict['can_download'] = True 368 perm_dict['can_upload'] = False 369 elif self.permission == FileShare.PERM_EDIT_ONLY: 370 perm_dict['can_edit'] = True 371 perm_dict['can_download'] = False 372 perm_dict['can_upload'] = False 373 elif self.permission == FileShare.PERM_VIEW_DL_UPLOAD: 374 perm_dict['can_edit'] = False 375 perm_dict['can_download'] = True 376 perm_dict['can_upload'] = True 377 else: 378 assert False 379 return perm_dict 380 381 def get_obj_name(self): 382 if self.path: 383 return '/' if self.path == '/' else os.path.basename(self.path.rstrip('/')) 384 return '' 385 386 387class OrgFileShareManager(models.Manager): 388 def set_org_file_share(self, org_id, file_share): 389 """Set a share link as org share link. 390 391 Arguments: 392 - `org_id`: 393 - `file_share`: 394 """ 395 ofs = self.model(org_id=org_id, file_share=file_share) 396 ofs.save(using=self._db) 397 return ofs 398 399class OrgFileShare(models.Model): 400 """ 401 Model used for organization file or dir shared link. 402 """ 403 org_id = models.IntegerField(db_index=True) 404 file_share = models.OneToOneField(FileShare, on_delete=models.CASCADE) 405 objects = OrgFileShareManager() 406 407 408class UploadLinkShareManager(models.Manager): 409 def _get_upload_link_by_path(self, username, repo_id, path): 410 ufs = list(super(UploadLinkShareManager, self).filter(repo_id=repo_id).filter( 411 username=username).filter(path=path)) 412 if len(ufs) > 0: 413 return ufs[0] 414 else: 415 return None 416 417 def get_upload_link_by_path(self, username, repo_id, path): 418 path = normalize_dir_path(path) 419 return self._get_upload_link_by_path(username, repo_id, path) 420 421 def create_upload_link_share(self, username, repo_id, path, 422 password=None, expire_date=None): 423 path = normalize_dir_path(path) 424 token = gen_token(max_length=config.SHARE_LINK_TOKEN_LENGTH) 425 if password is not None: 426 password_enc = make_password(password) 427 else: 428 password_enc = None 429 uls = super(UploadLinkShareManager, self).create( 430 username=username, repo_id=repo_id, path=path, token=token, 431 password=password_enc, expire_date=expire_date) 432 uls.save() 433 return uls 434 435 def get_valid_upload_link_by_token(self, token): 436 """Return upload link that exists and not expire, otherwise none. 437 """ 438 try: 439 fs = self.get(token=token) 440 except self.model.DoesNotExist: 441 return None 442 443 if fs.expire_date is None: 444 return fs 445 else: 446 if timezone.now() > fs.expire_date: 447 return None 448 else: 449 return fs 450 451class UploadLinkShare(models.Model): 452 """ 453 Model used for shared upload link. 454 """ 455 username = LowerCaseCharField(max_length=255, db_index=True) 456 repo_id = models.CharField(max_length=36, db_index=True) 457 path = models.TextField() 458 token = models.CharField(max_length=100, unique=True) 459 ctime = models.DateTimeField(default=datetime.datetime.now) 460 view_cnt = models.IntegerField(default=0) 461 password = models.CharField(max_length=128, null=True) 462 expire_date = models.DateTimeField(null=True) 463 objects = UploadLinkShareManager() 464 465 def is_encrypted(self): 466 return True if self.password is not None else False 467 468 def is_owner(self, owner): 469 return owner == self.username 470 471 def is_expired(self): 472 if self.expire_date is not None and timezone.now() > self.expire_date: 473 return True 474 else: 475 return False 476 477class PrivateFileDirShareManager(models.Manager): 478 def add_private_file_share(self, from_user, to_user, repo_id, path, perm): 479 """ 480 """ 481 path = normalize_file_path(path) 482 token = gen_token(max_length=10) 483 484 pfs = self.model(from_user=from_user, to_user=to_user, repo_id=repo_id, 485 path=path, s_type='f', token=token, permission=perm) 486 pfs.save(using=self._db) 487 return pfs 488 489 def add_read_only_priv_file_share(self, from_user, to_user, repo_id, path): 490 """ 491 """ 492 return self.add_private_file_share(from_user, to_user, repo_id, 493 path, PERMISSION_READ) 494 495 def get_private_share_in_file(self, username, repo_id, path): 496 """Get a file that private shared to ``username``. 497 """ 498 path = normalize_file_path(path) 499 500 ret = super(PrivateFileDirShareManager, self).filter( 501 to_user=username, repo_id=repo_id, path=path, s_type='f') 502 return ret[0] if len(ret) > 0 else None 503 504 def add_private_dir_share(self, from_user, to_user, repo_id, path, perm): 505 """ 506 """ 507 path = normalize_dir_path(path) 508 token = gen_token(max_length=10) 509 510 pfs = self.model(from_user=from_user, to_user=to_user, repo_id=repo_id, 511 path=path, s_type='d', token=token, permission=perm) 512 pfs.save(using=self._db) 513 return pfs 514 515 def get_private_share_in_dir(self, username, repo_id, path): 516 """Get a directory that private shared to ``username``. 517 """ 518 path = normalize_dir_path(path) 519 520 ret = super(PrivateFileDirShareManager, self).filter( 521 to_user=username, repo_id=repo_id, path=path, s_type='d') 522 return ret[0] if len(ret) > 0 else None 523 524 def get_priv_file_dir_share_by_token(self, token): 525 return super(PrivateFileDirShareManager, self).get(token=token) 526 527 def delete_private_file_dir_share(self, from_user, to_user, repo_id, path): 528 """ 529 """ 530 super(PrivateFileDirShareManager, self).filter( 531 from_user=from_user, to_user=to_user, repo_id=repo_id, 532 path=path).delete() 533 534 def list_private_share_out_by_user(self, from_user): 535 """List files/directories private shared from ``from_user``. 536 """ 537 return super(PrivateFileDirShareManager, self).filter( 538 from_user=from_user) 539 540 def list_private_share_in_by_user(self, to_user): 541 """List files/directories private shared to ``to_user``. 542 """ 543 return super(PrivateFileDirShareManager, self).filter( 544 to_user=to_user) 545 546 def list_private_share_in_dirs_by_user_and_repo(self, to_user, repo_id): 547 """List directories private shared to ``to_user`` base on ``repo_id``. 548 """ 549 return super(PrivateFileDirShareManager, self).filter( 550 to_user=to_user, repo_id=repo_id, s_type='d') 551 552class PrivateFileDirShare(models.Model): 553 from_user = LowerCaseCharField(max_length=255, db_index=True) 554 to_user = LowerCaseCharField(max_length=255, db_index=True) 555 repo_id = models.CharField(max_length=36, db_index=True) 556 path = models.TextField() 557 token = models.CharField(max_length=10, unique=True) 558 permission = models.CharField(max_length=5) # `r` or `rw` 559 s_type = models.CharField(max_length=5, default='f') # `f` or `d` 560 objects = PrivateFileDirShareManager() 561 562###### signal handlers 563from django.dispatch import receiver 564from seahub.signals import repo_deleted 565 566@receiver(repo_deleted) 567def remove_share_info(sender, **kwargs): 568 repo_id = kwargs['repo_id'] 569 570 FileShare.objects.filter(repo_id=repo_id).delete() 571 UploadLinkShare.objects.filter(repo_id=repo_id).delete() 572 573 # remove record of extra share 574 ExtraSharePermission.objects.filter(repo_id=repo_id).delete() 575 ExtraGroupsSharePermission.objects.filter(repo_id=repo_id).delete() 576