1# Copyright (c) 2012-2016 Seafile Ltd.
2# -*- coding: utf-8 -*-
3import os
4import stat
5import logging
6import json
7import posixpath
8import csv
9import chardet
10import io
11
12from django.urls import reverse
13from django.http import HttpResponse, Http404
14from django.template.loader import render_to_string
15from django.utils.http import urlquote
16from django.utils.html import escape
17from django.utils.timezone import now
18from django.utils.translation import ugettext as _
19from django.conf import settings as dj_settings
20from django.template.defaultfilters import filesizeformat
21
22import seaserv
23from seaserv import seafile_api, ccnet_api, \
24    seafserv_threaded_rpc
25from pysearpc import SearpcError
26
27from seahub.auth.decorators import login_required_ajax
28from seahub.base.decorators import require_POST
29from seahub.forms import RepoRenameDirentForm
30from seahub.options.models import UserOptions, CryptoOptionNotSetError
31from seahub.notifications.models import UserNotification
32from seahub.notifications.views import add_notice_from_info
33from seahub.share.models import UploadLinkShare
34from seahub.signals import upload_file_successful
35from seahub.views import get_unencry_rw_repos_by_user, \
36    get_diff, check_folder_permission
37from seahub.group.utils import is_group_member, is_group_admin_or_owner, \
38    get_group_member_info
39import seahub.settings as settings
40from seahub.settings import ENABLE_THUMBNAIL, THUMBNAIL_ROOT, \
41    THUMBNAIL_DEFAULT_SIZE, SHOW_TRAFFIC, MEDIA_URL, ENABLE_VIDEO_THUMBNAIL
42from seahub.utils import check_filename_with_rename, EMPTY_SHA1, \
43    gen_block_get_url, \
44    new_merge_with_no_conflict, get_commit_before_new_merge, \
45    gen_file_upload_url, is_org_context, is_pro_version, normalize_dir_path, \
46    FILEEXT_TYPE_MAP
47from seahub.utils.star import get_dir_starred_files
48from seahub.utils.file_types import IMAGE, VIDEO, XMIND
49from seahub.utils.file_op import check_file_lock, ONLINE_OFFICE_LOCK_OWNER
50from seahub.utils.repo import get_locked_files_by_dir, get_repo_owner, \
51        repo_has_been_shared_out, parse_repo_perm
52from seahub.utils.error_msg import file_type_error_msg, file_size_error_msg
53from seahub.base.accounts import User
54from seahub.thumbnail.utils import get_thumbnail_src
55from seahub.share.utils import is_repo_admin
56from seahub.base.templatetags.seahub_tags import translate_seahub_time, \
57    email2nickname, tsstr_sec
58from seahub.constants import PERMISSION_READ_WRITE
59from seahub.constants import HASH_URLS
60
61# Get an instance of a logger
62logger = logging.getLogger(__name__)
63
64########## Seafile API Wrapper
65def get_repo(repo_id):
66    return seafile_api.get_repo(repo_id)
67
68def get_commit(repo_id, repo_version, commit_id):
69    return seaserv.get_commit(repo_id, repo_version, commit_id)
70
71def get_group(gid):
72    return seaserv.get_group(gid)
73
74########## repo related
75def convert_repo_path_when_can_not_view_folder(request, repo_id, path):
76
77    content_type = 'application/json; charset=utf-8'
78
79    path = normalize_dir_path(path)
80    username = request.user.username
81    converted_repo_path = seafile_api.convert_repo_path(repo_id, path, username)
82    if not converted_repo_path:
83        err_msg = _('Permission denied.')
84        return HttpResponse(json.dumps({'error': err_msg}),
85                            status=403, content_type=content_type)
86
87    converted_repo_path = json.loads(converted_repo_path)
88
89    repo_id = converted_repo_path['repo_id']
90    repo = seafile_api.get_repo(repo_id)
91    if not repo:
92        err_msg = 'Library not found.'
93        return HttpResponse(json.dumps({'error': err_msg}),
94                            status=404, content_type=content_type)
95
96    path = converted_repo_path['path']
97    path = normalize_dir_path(path)
98    dir_id = seafile_api.get_dir_id_by_path(repo.id, path)
99    if not dir_id:
100        err_msg = 'Folder not found.'
101        return HttpResponse(json.dumps({'error': err_msg}),
102                            status=404, content_type=content_type)
103
104    group_id = ''
105    if 'group_id' in converted_repo_path:
106        group_id = converted_repo_path['group_id']
107        if not ccnet_api.get_group(group_id):
108            err_msg = 'Group not found.'
109            return HttpResponse(json.dumps({'error': err_msg}),
110                                status=404, content_type=content_type)
111
112        if not is_group_member(group_id, username):
113            err_msg = _('Permission denied.')
114            return HttpResponse(json.dumps({'error': err_msg}),
115                                status=403, content_type=content_type)
116
117    user_perm = check_folder_permission(request, repo_id, path)
118    if not user_perm:
119        err_msg = _('Permission denied.')
120        return HttpResponse(json.dumps({'error': err_msg}),
121                            status=403, content_type=content_type)
122
123    if not group_id:
124        next_url = '#shared-libs/lib/%s/%s' % (repo_id, path.strip('/'))
125    else:
126        next_url = '#group/%s/lib/%s/%s' % (group_id, repo_id, path.strip('/'))
127
128    return HttpResponse(json.dumps({'next_url': next_url}), content_type=content_type)
129
130@login_required_ajax
131def list_lib_dir(request, repo_id):
132    '''
133        New ajax API for list library directory
134    '''
135    content_type = 'application/json; charset=utf-8'
136    result = {}
137
138    repo = get_repo(repo_id)
139    if not repo:
140        err_msg = _('Library does not exist.')
141        return HttpResponse(json.dumps({'error': err_msg}),
142                            status=400, content_type=content_type)
143
144    username = request.user.username
145
146    path = request.GET.get('p', '/')
147    path = normalize_dir_path(path)
148    dir_id = seafile_api.get_dir_id_by_path(repo.id, path)
149    if not dir_id:
150        err_msg = 'Folder not found.'
151        return HttpResponse(json.dumps({'error': err_msg}),
152                            status=404, content_type=content_type)
153
154    # perm for current dir
155    user_perm = check_folder_permission(request, repo_id, path)
156    if not user_perm:
157        return convert_repo_path_when_can_not_view_folder(request, repo_id, path)
158
159    if repo.encrypted \
160            and not seafile_api.is_password_set(repo.id, username):
161        err_msg = _('Library is encrypted.')
162        return HttpResponse(json.dumps({'error': err_msg, 'lib_need_decrypt': True}),
163                            status=403, content_type=content_type)
164
165    head_commit = get_commit(repo.id, repo.version, repo.head_cmmt_id)
166    if not head_commit:
167        err_msg = _('Error: no head commit id')
168        return HttpResponse(json.dumps({'error': err_msg}),
169                            status=500, content_type=content_type)
170
171    dir_list = []
172    file_list = []
173
174
175    dirs = seafserv_threaded_rpc.list_dir_with_perm(repo_id, path, dir_id,
176            username, -1, -1)
177    starred_files = get_dir_starred_files(username, repo_id, path)
178
179    for dirent in dirs:
180        dirent.last_modified = dirent.mtime
181        if stat.S_ISDIR(dirent.mode):
182            dpath = posixpath.join(path, dirent.obj_name)
183            if dpath[-1] != '/':
184                dpath += '/'
185            dir_list.append(dirent)
186        else:
187            if repo.version == 0:
188                file_size = seafile_api.get_file_size(repo.store_id, repo.version, dirent.obj_id)
189            else:
190                file_size = dirent.size
191            dirent.file_size = file_size if file_size else 0
192
193            dirent.starred = False
194            fpath = posixpath.join(path, dirent.obj_name)
195            if fpath in starred_files:
196                dirent.starred = True
197
198            file_list.append(dirent)
199
200    if is_org_context(request):
201        repo_owner = seafile_api.get_org_repo_owner(repo.id)
202    else:
203        repo_owner = seafile_api.get_repo_owner(repo.id)
204
205    result["repo_owner"] = repo_owner
206    result["is_repo_owner"] = False
207    result["has_been_shared_out"] = False
208    result["is_admin"] = is_repo_admin(username, repo_id)
209    if repo_owner == username:
210        result["is_repo_owner"] = True
211        try:
212            result["has_been_shared_out"] = repo_has_been_shared_out(request, repo_id)
213        except Exception as e:
214            logger.error(e)
215
216    if result["is_admin"]:
217        result["has_been_shared_out"] = True
218
219    result["is_virtual"] = repo.is_virtual
220    result["repo_name"] = repo.name
221    result["user_perm"] = user_perm
222    # check quota for fileupload
223    result["no_quota"] = True if seaserv.check_quota(repo.id) < 0 else False
224    result["encrypted"] = repo.encrypted
225
226    dirent_list = []
227    for d in dir_list:
228        d_ = {}
229        d_['is_dir'] = True
230        d_['obj_name'] = d.obj_name
231        d_['last_modified'] = d.last_modified
232        d_['last_update'] = translate_seahub_time(d.last_modified)
233        d_['p_dpath'] = posixpath.join(path, d.obj_name)
234        d_['perm'] = d.permission # perm for sub dir in current dir
235        dirent_list.append(d_)
236
237    size = int(request.GET.get('thumbnail_size', THUMBNAIL_DEFAULT_SIZE))
238
239    for f in file_list:
240        f_ = {}
241        f_['is_file'] = True
242        f_['obj_name'] = f.obj_name
243        f_['last_modified'] = f.last_modified
244        f_['last_update'] = translate_seahub_time(f.last_modified)
245        f_['starred'] = f.starred
246        f_['file_size'] = filesizeformat(f.file_size)
247        f_['obj_id'] = f.obj_id
248        f_['perm'] = f.permission # perm for file in current dir
249
250        if not repo.encrypted and ENABLE_THUMBNAIL:
251            # used for providing a way to determine
252            # if send a request to create thumbnail.
253
254            fileExt = os.path.splitext(f.obj_name)[1][1:].lower()
255            file_type = FILEEXT_TYPE_MAP.get(fileExt)
256            if file_type == IMAGE:
257                f_['is_img'] = True
258
259            if file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL:
260                f_['is_video'] = True
261
262            if file_type == XMIND:
263                f_['is_xmind'] = True
264
265            if file_type in (IMAGE, XMIND) or \
266                    file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL:
267                # if thumbnail has already been created, return its src.
268                # Then web browser will use this src to get thumbnail instead of
269                # recreating it.
270                thumbnail_file_path = os.path.join(THUMBNAIL_ROOT, str(size), f.obj_id)
271                thumbnail_exist = os.path.exists(thumbnail_file_path)
272                if thumbnail_exist:
273                    file_path = posixpath.join(path, f.obj_name)
274                    src = get_thumbnail_src(repo_id, size, file_path)
275                    f_['encoded_thumbnail_src'] = urlquote(src)
276
277        if is_pro_version():
278            f_['is_locked'] = True if f.is_locked else False
279            f_['lock_owner'] = f.lock_owner
280            f_['lock_owner_name'] = email2nickname(f.lock_owner)
281
282            f_['locked_by_me'] = False
283            if f.lock_owner == username:
284                f_['locked_by_me'] = True
285
286            if f.lock_owner == ONLINE_OFFICE_LOCK_OWNER and \
287                    user_perm == PERMISSION_READ_WRITE:
288                f_['locked_by_me'] = True
289
290        dirent_list.append(f_)
291
292    result["dirent_list"] = dirent_list
293
294    return HttpResponse(json.dumps(result), content_type=content_type)
295
296
297def upload_file_done(request):
298    """Send a message when a file is uploaded.
299
300    Arguments:
301    - `request`:
302    """
303    ct = 'application/json; charset=utf-8'
304    result = {}
305
306    filename = request.GET.get('fn', '')
307    if not filename:
308        result['error'] = _('Argument missing')
309        return HttpResponse(json.dumps(result), status=400, content_type=ct)
310    repo_id = request.GET.get('repo_id', '')
311    if not repo_id:
312        result['error'] = _('Argument missing')
313        return HttpResponse(json.dumps(result), status=400, content_type=ct)
314    path = request.GET.get('p', '')
315    if not path:
316        result['error'] = _('Argument missing')
317        return HttpResponse(json.dumps(result), status=400, content_type=ct)
318
319    # a few checkings
320    if not seafile_api.get_repo(repo_id):
321        result['error'] = _('Wrong repo id')
322        return HttpResponse(json.dumps(result), status=400, content_type=ct)
323
324    # get upload link share creator
325    token = request.GET.get('token', '')
326    if not token:
327        result['error'] = _('Argument missing')
328        return HttpResponse(json.dumps(result), status=400, content_type=ct)
329
330    uls = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
331    if uls is None:
332        result['error'] = _('Bad upload link token.')
333        return HttpResponse(json.dumps(result), status=400, content_type=ct)
334    creator = uls.username
335
336    file_path = path.rstrip('/') + '/' + filename
337    if seafile_api.get_file_id_by_path(repo_id, file_path) is None:
338        result['error'] = _('File does not exist')
339        return HttpResponse(json.dumps(result), status=400, content_type=ct)
340
341    # send singal
342    upload_file_successful.send(sender=None,
343                                repo_id=repo_id,
344                                file_path=file_path,
345                                owner=creator)
346
347    return HttpResponse(json.dumps({'success': True}), content_type=ct)
348
349
350def get_file_upload_url_ul(request, token):
351    """Get file upload url in dir upload link.
352
353    Arguments:
354    - `request`:
355    - `token`:
356    """
357    if not request.is_ajax():
358        raise Http404
359
360    content_type = 'application/json; charset=utf-8'
361
362    uls = UploadLinkShare.objects.get_valid_upload_link_by_token(token)
363    if uls is None:
364        return HttpResponse(json.dumps({"error": _("Bad upload link token.")}),
365                            status=400, content_type=content_type)
366
367    shared_by = uls.username
368    repo_id = uls.repo_id
369    r = request.GET.get('r', '')
370    if repo_id != r:            # perm check
371        return HttpResponse(json.dumps({"error": _("Bad repo id in upload link.")}),
372                            status=403, content_type=content_type)
373
374    repo = get_repo(repo_id)
375    if not repo:
376        return HttpResponse(json.dumps({"error": _("Library does not exist")}),
377                            status=404, content_type=content_type)
378
379    if repo.encrypted or \
380            seafile_api.check_permission_by_path(repo_id, '/', shared_by) != 'rw':
381        return HttpResponse(json.dumps({"error": _("Permission denied")}),
382                            status=403, content_type=content_type)
383
384    dir_id = seafile_api.get_dir_id_by_path(uls.repo_id, uls.path)
385    if not dir_id:
386        return HttpResponse(json.dumps({"error": _("Directory does not exist.")}),
387                            status=404, content_type=content_type)
388
389    obj_id = json.dumps({'parent_dir': uls.path})
390    args = [repo_id, obj_id, 'upload-link', shared_by]
391    kwargs = {
392        'use_onetime': False,
393    }
394    if (is_pro_version() and dj_settings.ENABLE_UPLOAD_LINK_VIRUS_CHECK):
395        kwargs.update({'check_virus': True})
396
397    try:
398        acc_token = seafile_api.get_fileserver_access_token(*args, **kwargs)
399    except SearpcError as e:
400        logger.error(e)
401        return HttpResponse(json.dumps({"error": _("Internal Server Error")}),
402                            status=500, content_type=content_type)
403
404    if not acc_token:
405        return HttpResponse(json.dumps({"error": _("Internal Server Error")}),
406                            status=500, content_type=content_type)
407
408    url = gen_file_upload_url(acc_token, 'upload-aj')
409    return HttpResponse(json.dumps({"url": url}), content_type=content_type)
410
411@login_required_ajax
412def repo_history_changes(request, repo_id):
413    changes = {}
414    content_type = 'application/json; charset=utf-8'
415
416    repo = get_repo(repo_id)
417    if not repo:
418        err_msg = _('Library does not exist.')
419        return HttpResponse(json.dumps({'error': err_msg}),
420                status=400, content_type=content_type)
421
422    # perm check
423    if check_folder_permission(request, repo_id, '/') is None:
424        if request.user.is_staff is True:
425            pass # Allow system staff to check repo changes
426        else:
427            err_msg = _("Permission denied")
428            return HttpResponse(json.dumps({"error": err_msg}), status=403,
429                            content_type=content_type)
430
431    username = request.user.username
432    try:
433        server_crypto = UserOptions.objects.is_server_crypto(username)
434    except CryptoOptionNotSetError:
435        # Assume server_crypto is ``False`` if this option is not set.
436        server_crypto = False
437
438    if repo.encrypted and \
439            (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)) \
440            and not seafile_api.is_password_set(repo_id, username):
441        err_msg = _('Library is encrypted.')
442        return HttpResponse(json.dumps({'error': err_msg}),
443                            status=403, content_type=content_type)
444
445    commit_id = request.GET.get('commit_id', '')
446    if not commit_id:
447        err_msg = _('Argument missing')
448        return HttpResponse(json.dumps({'error': err_msg}),
449                            status=400, content_type=content_type)
450
451    changes = get_diff(repo_id, '', commit_id)
452
453    c = get_commit(repo.id, repo.version, commit_id)
454    if c.parent_id is None:
455        # A commit is a first commit only if it's parent id is None.
456        changes['cmt_desc'] = repo.desc
457    elif c.second_parent_id is None:
458        # Normal commit only has one parent.
459        if c.desc.startswith('Changed library'):
460            changes['cmt_desc'] = _('Changed library name or description')
461    else:
462        # A commit is a merge only if it has two parents.
463        changes['cmt_desc'] = _('No conflict in the merge.')
464
465    changes['date_time'] = tsstr_sec(c.ctime)
466
467    return HttpResponse(json.dumps(changes), content_type=content_type)
468