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