1# Copyright (c) 2012-2016 Seafile Ltd.
2# encoding: utf-8
3# Utility functions for api2
4
5import os
6import time
7import json
8import re
9import logging
10
11from collections import defaultdict
12from functools import wraps
13
14from django.http import HttpResponse
15from rest_framework.authentication import SessionAuthentication
16from rest_framework.response import Response
17from rest_framework import status, serializers
18from seaserv import seafile_api, ccnet_api, \
19        get_group, seafserv_threaded_rpc
20from pysearpc import SearpcError
21
22from seahub.base.templatetags.seahub_tags import email2nickname, \
23    translate_seahub_time, file_icon_filter, email2contact_email
24from seahub.group.views import is_group_staff
25from seahub.group.utils import is_group_member
26from seahub.notifications.models import UserNotification
27from seahub.api2.models import Token, TokenV2, DESKTOP_PLATFORMS
28from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
29from seahub.avatar.templatetags.avatar_tags import api_avatar_url
30from seahub.settings import INSTALLED_APPS
31
32logger = logging.getLogger(__name__)
33
34def api_error(code, msg):
35    err_resp = {'error_msg': msg}
36    return Response(err_resp, status=code)
37
38def get_file_size(store_id, repo_version, file_id):
39    size = seafile_api.get_file_size(store_id, repo_version, file_id)
40    return size if size else 0
41
42def prepare_starred_files(files):
43    array = []
44    for f in files:
45        sfile = {'org' : f.org_id,
46                 'repo' : f.repo.id,
47                 'repo_id' : f.repo.id,
48                 'repo_name' : f.repo.name,
49                 'path' : f.path,
50                 'icon_path' : file_icon_filter(f.path),
51                 'file_name' : os.path.basename(f.path),
52                 'mtime' : f.last_modified,
53                 'mtime_relative': translate_seahub_time(f.last_modified),
54                 'dir' : f.is_dir,
55                 'repo_encrypted' : f.repo.encrypted
56                 }
57        if not f.is_dir:
58            try:
59                file_id = seafile_api.get_file_id_by_path(f.repo.id, f.path)
60                sfile['oid'] = file_id
61                sfile['size'] = get_file_size(f.repo.store_id, f.repo.version, file_id)
62            except SearpcError as e:
63                logger.error(e)
64                pass
65
66        array.append(sfile)
67
68    return array
69
70def get_groups(email):
71    group_json = []
72
73    joined_groups = ccnet_api.get_groups(email)
74    grpmsgs = {}
75    for g in joined_groups:
76        grpmsgs[g.id] = 0
77
78    replynum = 0
79
80    for g in joined_groups:
81        group = {
82            "id": g.id,
83            "name": g.group_name,
84            "creator": g.creator_name,
85            "ctime": g.timestamp,
86            "msgnum": grpmsgs[g.id],
87            }
88        group_json.append(group)
89
90    return group_json, replynum
91
92def get_timestamp(msgtimestamp):
93    if not msgtimestamp:
94        return 0
95    timestamp = int(time.mktime(msgtimestamp.timetuple()))
96    return timestamp
97
98def api_group_check(func):
99    """
100    Decorator for initial group permission check tasks
101
102    un-login user & group not pub --> login page
103    un-login user & group pub --> view_perm = "pub"
104    login user & non group member & group not pub --> public info page
105    login user & non group member & group pub --> view_perm = "pub"
106    group member --> view_perm = "joined"
107    sys admin --> view_perm = "sys_admin"
108    """
109    def _decorated(view, request, group_id, *args, **kwargs):
110        group_id_int = int(group_id) # Checked by URL Conf
111        group = get_group(group_id_int)
112        if not group:
113            return api_error(status.HTTP_404_NOT_FOUND, 'Group not found.')
114        group.is_staff = False
115        group.is_pub = False
116
117        joined = is_group_member(group_id_int, request.user.username)
118        if joined:
119            group.view_perm = "joined"
120            group.is_staff = is_group_staff(group, request.user)
121            return func(view, request, group, *args, **kwargs)
122        if request.user.is_staff:
123            # viewed by system admin
124            group.view_perm = "sys_admin"
125            return func(view, request, group, *args, **kwargs)
126
127        if group.is_pub:
128            group.view_perm = "pub"
129            return func(view, request, group, *args, **kwargs)
130
131        # Return group public info page.
132        return api_error(status.HTTP_403_FORBIDDEN, 'Forbid to access this group.')
133
134    return _decorated
135
136def get_client_ip(request):
137    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
138    if x_forwarded_for:
139        ip = x_forwarded_for.split(',')[0]
140    else:
141        ip = request.META.get('REMOTE_ADDR', '')
142
143    return ip
144
145def get_diff_details(repo_id, commit1, commit2):
146    result = defaultdict(list)
147
148    diff_result = seafserv_threaded_rpc.get_diff(repo_id, commit1, commit2)
149    if not diff_result:
150        return result
151
152    for d in diff_result:
153        if d.status == 'add':
154            result['added_files'].append(d.name)
155        elif d.status == 'del':
156            result['deleted_files'].append(d.name)
157        elif d.status == 'mov':
158            result['renamed_files'].extend((d.name, d.new_name))
159        elif d.status == 'mod':
160            result['modified_files'].append(d.name)
161        elif d.status == 'newdir':
162            result['added_dirs'].append(d.name)
163        elif d.status == 'deldir':
164            result['deleted_dirs'].append(d.name)
165
166    return result
167
168JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
169def json_response(func):
170    @wraps(func)
171    def wrapped(*a, **kw):
172        result = func(*a, **kw)
173        if isinstance(result, HttpResponse):
174            return result
175        else:
176            return HttpResponse(json.dumps(result), status=200,
177                                content_type=JSON_CONTENT_TYPE)
178    return wrapped
179
180def get_token_v1(username):
181    token, _ = Token.objects.get_or_create(user=username)
182    return token
183
184_ANDROID_DEVICE_ID_PATTERN = re.compile('^[a-f0-9]{1,16}$')
185def get_token_v2(request, username, platform, device_id, device_name,
186                 client_version, platform_version):
187
188    if platform in DESKTOP_PLATFORMS:
189        # desktop device id is the peer id, so it must be 40 chars
190        if len(device_id) != 40:
191            raise serializers.ValidationError('invalid device id')
192
193    elif platform == 'android':
194        # See http://developer.android.com/reference/android/provider/Settings.Secure.html#ANDROID_ID
195        # android device id is the 64bit secure id, so it must be 16 chars in hex representation
196        # but some user reports their device ids are 14 or 15 chars long. So we relax the validation.
197        if not _ANDROID_DEVICE_ID_PATTERN.match(device_id.lower()):
198            raise serializers.ValidationError('invalid device id')
199    elif platform == 'ios':
200        if len(device_id) != 36:
201            raise serializers.ValidationError('invalid device id')
202    else:
203        raise serializers.ValidationError('invalid platform')
204
205    return TokenV2.objects.get_or_create_token(
206        username, platform, device_id, device_name,
207        client_version, platform_version, get_client_ip(request))
208
209def get_api_token(request, keys=None, key_prefix='shib_'):
210
211    if not keys:
212        keys = [
213            'platform',
214            'device_id',
215            'device_name',
216            'client_version',
217            'platform_version',
218        ]
219
220    if key_prefix:
221        keys = [key_prefix + item for item in keys]
222
223    if all([key in request.GET for key in keys]):
224
225        platform = request.GET['%splatform' % key_prefix]
226        device_id = request.GET['%sdevice_id' % key_prefix]
227        device_name = request.GET['%sdevice_name' % key_prefix]
228        client_version = request.GET['%sclient_version' % key_prefix]
229        platform_version = request.GET['%splatform_version' % key_prefix]
230
231        token = get_token_v2(request, request.user.username, platform,
232                             device_id, device_name, client_version,
233                             platform_version)
234    else:
235        token = get_token_v1(request.user.username)
236
237    return token
238
239def to_python_boolean(string):
240    """Convert a string to boolean.
241    """
242    string = string.lower()
243    if string in ('t', 'true', '1'):
244        return True
245    if string in ('f', 'false', '0'):
246        return False
247    raise ValueError("Invalid boolean value: '%s'" % string)
248
249def get_user_common_info(email, avatar_size=AVATAR_DEFAULT_SIZE):
250    avatar_url, is_default, date_uploaded = api_avatar_url(email, avatar_size)
251    return {
252        "email": email,
253        "name": email2nickname(email),
254        "contact_email": email2contact_email(email),
255        "avatar_url": avatar_url
256    }
257
258def user_to_dict(email, request=None, avatar_size=AVATAR_DEFAULT_SIZE):
259    d = get_user_common_info(email, avatar_size)
260    return {
261        'user_name': d['name'],
262        'user_email': d['email'],
263        'user_contact_email': d['contact_email'],
264        'avatar_url': d['avatar_url'],
265    }
266
267def is_web_request(request):
268    if isinstance(request.successful_authenticator, SessionAuthentication):
269        return True
270    else:
271        return False
272