1# Copyright (c) 2012-2016 Seafile Ltd.
2# encoding: utf-8
3import hashlib
4import os
5import stat
6import json
7import mimetypes
8import logging
9import posixpath
10
11from django.core.cache import cache
12from django.urls import reverse, resolve
13from django.contrib import messages
14from django.http import HttpResponse, Http404, \
15    HttpResponseRedirect
16from django.shortcuts import render, redirect
17from django.utils.http import urlquote
18from django.utils.html import escape
19from django.utils.translation import ugettext as _
20from django.views.decorators.http import condition
21
22import seaserv
23from seaserv import get_repo, get_commits, \
24    seafserv_threaded_rpc, is_repo_owner, \
25    get_file_size, seafile_api
26from pysearpc import SearpcError
27
28from seahub.avatar.util import get_avatar_file_storage
29from seahub.auth.decorators import login_required
30from seahub.auth import login as auth_login
31from seahub.auth import get_backends
32from seahub.base.accounts import User
33from seahub.base.decorators import require_POST
34from seahub.base.models import ClientLoginToken
35from seahub.options.models import UserOptions, CryptoOptionNotSetError
36from seahub.profile.models import Profile
37from seahub.share.models import FileShare, UploadLinkShare
38from seahub.revision_tag.models import RevisionTags
39from seahub.utils import render_permission_error, render_error, \
40    gen_shared_upload_link, is_org_context, \
41    gen_dir_share_link, gen_file_share_link, get_file_type_and_ext, \
42    get_user_repos, EMPTY_SHA1, gen_file_get_url, \
43    new_merge_with_no_conflict, get_max_upload_file_size, \
44    is_pro_version, FILE_AUDIT_ENABLED, is_valid_dirent_name, \
45    is_windows_operating_system, seafevents_api, IS_EMAIL_CONFIGURED
46from seahub.utils.star import get_dir_starred_files
47from seahub.utils.repo import get_library_storages, parse_repo_perm
48from seahub.utils.file_op import check_file_lock
49from seahub.utils.timeutils import utc_to_local
50from seahub.utils.auth import get_login_bg_image_path
51import seahub.settings as settings
52from seahub.settings import AVATAR_FILE_STORAGE, \
53    ENABLE_FOLDER_PERM, ENABLE_REPO_SNAPSHOT_LABEL, \
54    UNREAD_NOTIFICATIONS_REQUEST_INTERVAL, SHARE_LINK_EXPIRE_DAYS_MIN, \
55    SHARE_LINK_EXPIRE_DAYS_MAX, SHARE_LINK_EXPIRE_DAYS_DEFAULT, \
56    UPLOAD_LINK_EXPIRE_DAYS_MIN, UPLOAD_LINK_EXPIRE_DAYS_MAX, UPLOAD_LINK_EXPIRE_DAYS_DEFAULT, \
57    SEAFILE_COLLAB_SERVER, ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, \
58    ADDITIONAL_SHARE_DIALOG_NOTE, ADDITIONAL_APP_BOTTOM_LINKS, ADDITIONAL_ABOUT_DIALOG_LINKS, \
59    DTABLE_WEB_SERVER
60
61from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP
62from seahub.onlyoffice.settings import ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN
63from seahub.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS
64from seahub.constants import HASH_URLS, PERMISSION_READ
65from seahub.group.settings import GROUP_IMPORT_MEMBERS_EXTRA_MSG
66
67from seahub.weixin.settings import ENABLE_WEIXIN
68
69LIBRARY_TEMPLATES = getattr(settings, 'LIBRARY_TEMPLATES', {})
70CUSTOM_NAV_ITEMS = getattr(settings, 'CUSTOM_NAV_ITEMS', '')
71
72from constance import config
73
74# Get an instance of a logger
75logger = logging.getLogger(__name__)
76
77def validate_owner(request, repo_id):
78    """
79    Check whether user in the request owns the repo.
80
81    """
82    ret = is_repo_owner(request.user.username, repo_id)
83
84    return True if ret else False
85
86def is_registered_user(email):
87    """
88    Check whether user is registerd.
89
90    """
91    try:
92        user = User.objects.get(email=email)
93    except User.DoesNotExist:
94        user = None
95
96    return True if user else False
97
98_default_repo_id = None
99def get_system_default_repo_id():
100    global _default_repo_id
101    if not _default_repo_id:
102        try:
103            _default_repo_id = seaserv.seafserv_threaded_rpc.get_system_default_repo_id()
104        except SearpcError as e:
105            logger.error(e)
106    return _default_repo_id
107
108def check_folder_permission(request, repo_id, path):
109    """Check repo/folder/file access permission of a user.
110
111    Arguments:
112    - `request`:
113    - `repo_id`:
114    - `path`:
115    """
116    repo_status = seafile_api.get_repo_status(repo_id)
117    if repo_status == 1:
118        return PERMISSION_READ
119
120    username = request.user.username
121    return seafile_api.check_permission_by_path(repo_id, path, username)
122
123def gen_path_link(path, repo_name):
124    """
125    Generate navigate paths and links in repo page.
126
127    """
128    if path and path[-1] != '/':
129        path += '/'
130
131    paths = []
132    links = []
133    if path and path != '/':
134        paths = path[1:-1].split('/')
135        i = 1
136        for name in paths:
137            link = '/' + '/'.join(paths[:i])
138            i = i + 1
139            links.append(link)
140    if repo_name:
141        paths.insert(0, repo_name)
142        links.insert(0, '/')
143
144    zipped = list(zip(paths, links))
145
146    return zipped
147
148def get_file_download_link(repo_id, obj_id, path):
149    """Generate file download link.
150
151    Arguments:
152    - `repo_id`:
153    - `obj_id`:
154    - `filename`:
155    """
156    return reverse('download_file', args=[repo_id, obj_id]) + '?p=' + \
157        urlquote(path)
158
159def get_repo_dirents(request, repo, commit, path, offset=-1, limit=-1):
160    """List repo dirents based on commit id and path. Use ``offset`` and
161    ``limit`` to do paginating.
162
163    Returns: A tupple of (file_list, dir_list, dirent_more)
164
165    TODO: Some unrelated parts(file sharing, stars, modified info, etc) need
166    to be pulled out to multiple functions.
167    """
168
169    dir_list = []
170    file_list = []
171    dirent_more = False
172    if commit.root_id == EMPTY_SHA1:
173        return ([], [], False) if limit == -1 else ([], [], False)
174    else:
175        try:
176            dirs = seafile_api.list_dir_by_commit_and_path(commit.repo_id,
177                                                           commit.id, path,
178                                                           offset, limit)
179            if not dirs:
180                return ([], [], False)
181        except SearpcError as e:
182            logger.error(e)
183            return ([], [], False)
184
185        if limit != -1 and limit == len(dirs):
186            dirent_more = True
187
188        username = request.user.username
189        starred_files = get_dir_starred_files(username, repo.id, path)
190        fileshares = FileShare.objects.filter(repo_id=repo.id).filter(username=username)
191        uploadlinks = UploadLinkShare.objects.filter(repo_id=repo.id).filter(username=username)
192
193        view_dir_base = reverse('lib_view', args=[repo.id, repo.name, ''])
194        dl_dir_base = reverse('repo_download_dir', args=[repo.id])
195        file_history_base = reverse('file_revisions', args=[repo.id])
196        for dirent in dirs:
197            dirent.last_modified = dirent.mtime
198            dirent.sharelink = ''
199            dirent.uploadlink = ''
200            if stat.S_ISDIR(dirent.props.mode):
201                dpath = posixpath.join(path, dirent.obj_name)
202                if dpath[-1] != '/':
203                    dpath += '/'
204                for share in fileshares:
205                    if dpath == share.path:
206                        dirent.sharelink = gen_dir_share_link(share.token)
207                        dirent.sharetoken = share.token
208                        break
209                for link in uploadlinks:
210                    if dpath == link.path:
211                        dirent.uploadlink = gen_shared_upload_link(link.token)
212                        dirent.uploadtoken = link.token
213                        break
214                p_dpath = posixpath.join(path, dirent.obj_name)
215                dirent.view_link = view_dir_base + '?p=' + urlquote(p_dpath)
216                dirent.dl_link = dl_dir_base + '?p=' + urlquote(p_dpath)
217                dir_list.append(dirent)
218            else:
219                file_list.append(dirent)
220                if repo.version == 0:
221                    dirent.file_size = get_file_size(repo.store_id, repo.version, dirent.obj_id)
222                else:
223                    dirent.file_size = dirent.size
224                dirent.starred = False
225                fpath = posixpath.join(path, dirent.obj_name)
226                p_fpath = posixpath.join(path, dirent.obj_name)
227                dirent.view_link = reverse('view_lib_file', args=[repo.id, p_fpath])
228                dirent.dl_link = get_file_download_link(repo.id, dirent.obj_id,
229                                                        p_fpath)
230                dirent.history_link = file_history_base + '?p=' + urlquote(p_fpath)
231                if fpath in starred_files:
232                    dirent.starred = True
233                for share in fileshares:
234                    if fpath == share.path:
235                        dirent.sharelink = gen_file_share_link(share.token)
236                        dirent.sharetoken = share.token
237                        break
238
239        return (file_list, dir_list, dirent_more)
240
241def get_unencry_rw_repos_by_user(request):
242    """Get all unencrypted repos a logged-in user can read and write.
243    """
244    username = request.user.username
245    if not username:
246        return []
247
248    def has_repo(repos, repo):
249        for r in repos:
250            if repo.id == r.id:
251                return True
252        return False
253
254    org_id = request.user.org.org_id if is_org_context(request) else None
255    owned_repos, shared_repos, groups_repos, public_repos = get_user_repos(
256        username, org_id=org_id)
257
258    accessible_repos = []
259
260    for r in owned_repos:
261        if r.is_virtual:
262            continue
263
264        if not has_repo(accessible_repos, r) and not r.encrypted:
265            accessible_repos.append(r)
266
267    for r in shared_repos + groups_repos + public_repos:
268        if not has_repo(accessible_repos, r) and not r.encrypted:
269            if check_folder_permission(request, r.id, '/') == 'rw':
270                accessible_repos.append(r)
271
272    return accessible_repos
273
274def render_recycle_root(request, repo_id, referer):
275    repo = get_repo(repo_id)
276    if not repo:
277        raise Http404
278
279    return render(request, 'repo_dir_recycle_view.html', {
280            'show_recycle_root': True,
281            'repo': repo,
282            'repo_dir_name': repo.name,
283            'enable_clean': config.ENABLE_USER_CLEAN_TRASH,
284            'referer': referer,
285            })
286
287def render_recycle_dir(request, repo_id, commit_id, referer):
288    basedir = request.GET.get('base', '')
289    path = request.GET.get('p', '')
290    if not basedir or not path:
291        return render_recycle_root(request, repo_id)
292
293    if basedir[0] != '/':
294        basedir = '/' + basedir
295    if path[-1] != '/':
296        path += '/'
297
298    repo = get_repo(repo_id)
299    if not repo:
300        raise Http404
301
302    try:
303        commit = seafserv_threaded_rpc.get_commit(repo.id, repo.version, commit_id)
304    except SearpcError as e:
305        logger.error(e)
306        referer = request.META.get('HTTP_REFERER', None)
307        next_page = settings.SITE_ROOT if referer is None else referer
308        return HttpResponseRedirect(next_page)
309
310    if not commit:
311        raise Http404
312
313    zipped = gen_path_link(path, '')
314
315    dir_entries = seafile_api.list_dir_by_commit_and_path(commit.repo_id,
316                                                   commit.id, basedir+path,
317                                                   -1, -1)
318    for dirent in dir_entries:
319        if stat.S_ISDIR(dirent.mode):
320            dirent.is_dir = True
321        else:
322            dirent.is_dir = False
323
324    return render(request, 'repo_dir_recycle_view.html', {
325            'show_recycle_root': False,
326            'repo': repo,
327            'repo_dir_name': repo.name,
328            'zipped': zipped,
329            'dir_entries': dir_entries,
330            'commit_id': commit_id,
331            'basedir': basedir,
332            'path': path,
333            'referer': referer,
334            })
335
336def render_dir_recycle_root(request, repo_id, dir_path, referer):
337    repo = get_repo(repo_id)
338    if not repo:
339        raise Http404
340
341    return render(request, 'repo_dir_recycle_view.html', {
342            'show_recycle_root': True,
343            'repo': repo,
344            'repo_dir_name': os.path.basename(dir_path.rstrip('/')),
345            'dir_path': dir_path,
346            'referer': referer,
347            })
348
349def render_dir_recycle_dir(request, repo_id, commit_id, dir_path, referer):
350    basedir = request.GET.get('base', '')
351    path = request.GET.get('p', '')
352    if not basedir or not path:
353        return render_dir_recycle_root(request, repo_id, dir_path)
354
355    if basedir[0] != '/':
356        basedir = '/' + basedir
357    if path[-1] != '/':
358        path += '/'
359
360    repo = get_repo(repo_id)
361    if not repo:
362        raise Http404
363
364    try :
365        commit = seafserv_threaded_rpc.get_commit(repo.id, repo.version, commit_id)
366    except SearpcError as e:
367        logger.error(e)
368        referer = request.META.get('HTTP_REFERER', None)
369        next_page = settings.SITE_ROOT if referer is None else referer
370        return HttpResponseRedirect(next_page)
371
372    if not commit:
373        raise Http404
374
375    zipped = gen_path_link(path, '')
376    dir_entries = seafile_api.list_dir_by_commit_and_path(commit.repo_id,
377                                                   commit.id, basedir+path,
378                                                   -1, -1)
379    for dirent in dir_entries:
380        if stat.S_ISDIR(dirent.mode):
381            dirent.is_dir = True
382        else:
383            dirent.is_dir = False
384
385    return render(request, 'repo_dir_recycle_view.html', {
386            'show_recycle_root': False,
387            'repo': repo,
388            'repo_dir_name': os.path.basename(dir_path.rstrip('/')),
389            'zipped': zipped,
390            'dir_entries': dir_entries,
391            'commit_id': commit_id,
392            'basedir': basedir,
393            'path': path,
394            'dir_path': dir_path,
395            'referer': referer,
396            })
397
398@login_required
399def repo_recycle_view(request, repo_id):
400    if not seafile_api.get_dir_id_by_path(repo_id, '/') or \
401        check_folder_permission(request, repo_id, '/') != 'rw':
402        return render_permission_error(request, _('Unable to view recycle page'))
403
404    commit_id = request.GET.get('commit_id', '')
405    referer = request.GET.get('referer', '') # for back to 'dir view' page
406    if not commit_id:
407        return render_recycle_root(request, repo_id, referer)
408    else:
409        return render_recycle_dir(request, repo_id, commit_id, referer)
410
411@login_required
412def dir_recycle_view(request, repo_id):
413    dir_path = request.GET.get('dir_path', '')
414
415    if not seafile_api.get_dir_id_by_path(repo_id, dir_path) or \
416        check_folder_permission(request, repo_id, dir_path) != 'rw':
417        return render_permission_error(request, _('Unable to view recycle page'))
418
419    commit_id = request.GET.get('commit_id', '')
420    referer = request.GET.get('referer', '') # for back to 'dir view' page
421    if not commit_id:
422        return render_dir_recycle_root(request, repo_id, dir_path, referer)
423    else:
424        return render_dir_recycle_dir(request, repo_id, commit_id, dir_path, referer)
425
426@login_required
427def repo_folder_trash(request, repo_id):
428    path = request.GET.get('path', '/')
429
430    if not seafile_api.get_dir_id_by_path(repo_id, path) or \
431        check_folder_permission(request, repo_id, path) != 'rw':
432        return render_permission_error(request, _('Unable to view recycle page'))
433
434    repo = get_repo(repo_id)
435    if not repo:
436        raise Http404
437
438    if path == '/':
439        name = repo.name
440    else:
441        name = os.path.basename(path.rstrip('/'))
442
443    return render(request, 'repo_folder_trash_react.html', {
444            'repo': repo,
445            'repo_folder_name': name,
446            'path': path,
447            'enable_clean': config.ENABLE_USER_CLEAN_TRASH,
448            })
449
450def can_access_repo_setting(request, repo_id, username):
451    repo = seafile_api.get_repo(repo_id)
452    if not repo:
453        return (False, None)
454
455    # no settings for virtual repo
456    if repo.is_virtual:
457        return (False, None)
458
459    # check permission
460    if is_org_context(request):
461        repo_owner = seafile_api.get_org_repo_owner(repo_id)
462    else:
463        repo_owner = seafile_api.get_repo_owner(repo_id)
464    is_owner = True if username == repo_owner else False
465    if not is_owner:
466        return (False, None)
467
468    return (True, repo)
469
470@login_required
471def repo_history(request, repo_id):
472    """
473    List library modification histories.
474    """
475    user_perm = check_folder_permission(request, repo_id, '/')
476    if not user_perm:
477        return render_permission_error(request, _('Unable to view library modification'))
478
479    repo = get_repo(repo_id)
480    if not repo:
481        raise Http404
482
483    username = request.user.username
484    try:
485        server_crypto = UserOptions.objects.is_server_crypto(username)
486    except CryptoOptionNotSetError:
487        # Assume server_crypto is ``False`` if this option is not set.
488        server_crypto = False
489
490    password_set = False
491    if repo.props.encrypted and \
492            (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)):
493        try:
494            ret = seafile_api.is_password_set(repo_id, username)
495            if ret == 1:
496                password_set = True
497        except SearpcError as e:
498            return render_error(request, e.msg)
499
500        if not password_set:
501            reverse_url = reverse('lib_view', args=[repo_id, repo.name, ''])
502            return HttpResponseRedirect(reverse_url)
503
504    try:
505        current_page = int(request.GET.get('page', '1'))
506    except ValueError:
507        current_page = 1
508
509    per_page = 100
510    commits_all = get_commits(repo_id, per_page * (current_page -1),
511                              per_page + 1)
512    commits = commits_all[:per_page]
513    for c in commits:
514        c.show = False if new_merge_with_no_conflict(c) else True
515
516    show_label = False
517    if ENABLE_REPO_SNAPSHOT_LABEL:
518        show_label = True
519        snapshot_labels = RevisionTags.objects.filter(repo_id=repo_id)
520        for c in commits:
521            if c.show:
522                c.labels = []
523                for label in snapshot_labels:
524                    if label.revision_id == c.id:
525                        c.labels.append(label.tag.name)
526
527    if len(commits_all) == per_page + 1:
528        page_next = True
529    else:
530        page_next = False
531
532    # for 'go back'
533    referer = request.GET.get('referer', '')
534
535    #template = 'repo_history.html'
536    template = 'repo_history_react.html'
537
538    return render(request, template, {
539            "repo": repo,
540            "commits": commits,
541            'current_page': current_page,
542            'prev_page': current_page-1,
543            'next_page': current_page+1,
544            'page_next': page_next,
545            'user_perm': user_perm,
546            'show_label': show_label,
547            'referer': referer,
548            })
549
550@login_required
551@require_POST
552def repo_revert_history(request, repo_id):
553
554    next_page = request.META.get('HTTP_REFERER', None)
555    if not next_page:
556        next_page = settings.SITE_ROOT
557
558    repo = get_repo(repo_id)
559    if not repo:
560        messages.error(request, _("Library does not exist"))
561        return HttpResponseRedirect(next_page)
562
563    # perm check
564    perm = check_folder_permission(request, repo_id, '/')
565    username = request.user.username
566    repo_owner = seafile_api.get_repo_owner(repo.id)
567
568    if perm is None or repo_owner != username:
569        messages.error(request, _("Permission denied"))
570        return HttpResponseRedirect(next_page)
571
572    try:
573        server_crypto = UserOptions.objects.is_server_crypto(username)
574    except CryptoOptionNotSetError:
575        # Assume server_crypto is ``False`` if this option is not set.
576        server_crypto = False
577
578    password_set = False
579    if repo.props.encrypted and \
580            (repo.enc_version == 1 or (repo.enc_version == 2 and server_crypto)):
581        try:
582            ret = seafile_api.is_password_set(repo_id, username)
583            if ret == 1:
584                password_set = True
585        except SearpcError as e:
586            return render_error(request, e.msg)
587
588        if not password_set:
589            reverse_url = reverse('lib_view', args=[repo_id, repo.name, ''])
590            return HttpResponseRedirect(reverse_url)
591
592    commit_id = request.GET.get('commit_id', '')
593    if not commit_id:
594        return render_error(request, _('Please specify history ID'))
595
596    try:
597        seafserv_threaded_rpc.revert_on_server(repo_id, commit_id, request.user.username)
598        messages.success(request, _('Successfully restored the library.'))
599    except SearpcError as e:
600        if e.msg == 'Bad arguments':
601            return render_error(request, _('Invalid arguments.'))
602        elif e.msg == 'No such repo':
603            return render_error(request, _('Library does not exist'))
604        elif e.msg == "Commit doesn't exist":
605            return render_error(request, _('History you specified does not exist'))
606        else:
607            return render_error(request, _('Unknown error'))
608
609    return HttpResponseRedirect(next_page)
610
611def fpath_to_link(repo_id, path, is_dir=False):
612    """Translate file path of a repo to its view link"""
613    if is_dir:
614        repo = seafile_api.get_repo(repo_id)
615        href = reverse('lib_view', args=[repo_id, repo.name, path.strip('/')])
616    else:
617        if not path.startswith('/'):
618            path = '/' + path
619        href = reverse("view_lib_file", args=[repo_id, path])
620
621    return '<a href="%s">%s</a>' % (href, escape(path))
622
623def get_diff(repo_id, arg1, arg2):
624    lists = {'new': [], 'removed': [], 'renamed': [], 'modified': [],
625             'newdir': [], 'deldir': []}
626
627    diff_result = seafserv_threaded_rpc.get_diff(repo_id, arg1, arg2)
628    if not diff_result:
629        return lists
630
631    for d in diff_result:
632        if d.status == "add":
633            lists['new'].append(fpath_to_link(repo_id, d.name))
634        elif d.status == "del":
635            lists['removed'].append(escape(d.name))
636        elif d.status == "mov":
637            lists['renamed'].append(escape(d.name) + " ==> " + fpath_to_link(repo_id, d.new_name))
638        elif d.status == "mod":
639            lists['modified'].append(fpath_to_link(repo_id, d.name))
640        elif d.status == "newdir":
641            lists['newdir'].append(fpath_to_link(repo_id, d.name, is_dir=True))
642        elif d.status == "deldir":
643            lists['deldir'].append(escape(d.name))
644
645    return lists
646
647def create_default_library(request):
648    """Create a default library for user.
649
650    Arguments:
651    - `username`:
652    """
653    username = request.user.username
654
655    # Disable user guide no matter user permission error or creation error,
656    # so that the guide popup only show once.
657    UserOptions.objects.disable_user_guide(username)
658
659    if not request.user.permissions.can_add_repo():
660        return
661
662    if is_org_context(request):
663        org_id = request.user.org.org_id
664        default_repo = seafile_api.create_org_repo(name=_("My Library"),
665                                                   desc=_("My Library"),
666                                                   username=username,
667                                                   org_id=org_id)
668    else:
669        default_repo = seafile_api.create_repo(name=_("My Library"),
670                                               desc=_("My Library"),
671                                               username=username)
672    sys_repo_id = get_system_default_repo_id()
673    if sys_repo_id is None:
674        return
675
676    try:
677        dirents = seafile_api.list_dir_by_path(sys_repo_id, '/')
678        for e in dirents:
679            obj_name = e.obj_name
680            seafile_api.copy_file(sys_repo_id, '/', obj_name,
681                                  default_repo, '/', obj_name, username, 0)
682    except SearpcError as e:
683        logger.error(e)
684        return
685
686    UserOptions.objects.set_default_repo(username, default_repo)
687    return default_repo
688
689def get_owned_repo_list(request):
690    """List owned repos.
691    """
692    username = request.user.username
693    if is_org_context(request):
694        org_id = request.user.org.org_id
695        return seafile_api.get_org_owned_repo_list(org_id, username)
696    else:
697        return seafile_api.get_owned_repo_list(username)
698
699@login_required
700def repo_set_access_property(request, repo_id):
701    ap = request.GET.get('ap', '')
702    seafserv_threaded_rpc.repo_set_access_property(repo_id, ap)
703    repo = seafile_api.get_repo(repo_id)
704    reverse_url = reverse('lib_view', args=[repo_id, repo.name, ''])
705
706    return HttpResponseRedirect(reverse_url)
707
708@login_required
709def validate_filename(request):
710    repo_id     = request.GET.get('repo_id')
711    filename    = request.GET.get('filename')
712
713    if not (repo_id and filename):
714        return render_error(request)
715
716    result = {'ret':'yes'}
717
718    try:
719        ret = is_valid_dirent_name(filename)
720    except SearpcError:
721        result['ret'] = 'error'
722    else:
723        result['ret'] = 'yes' if ret == 1 else 'no'
724
725    content_type = 'application/json; charset=utf-8'
726    return HttpResponse(json.dumps(result), content_type=content_type)
727
728@login_required
729def file_revisions(request, repo_id):
730    """List file revisions in file version history page.
731    """
732    repo = get_repo(repo_id)
733    if not repo:
734        error_msg = _("Library does not exist")
735        return render_error(request, error_msg)
736
737    # perm check
738    if not check_folder_permission(request, repo_id, '/'):
739        error_msg = _("Permission denied.")
740        return render_error(request, error_msg)
741
742    path = request.GET.get('p', '/')
743    if not path:
744        return render_error(request)
745
746    if path[-1] == '/':
747        path = path[:-1]
748
749    u_filename = os.path.basename(path)
750
751    filetype, file_ext = [x.lower() for x in get_file_type_and_ext(u_filename)]
752    if filetype == 'text' or filetype == 'markdown':
753        can_compare = True
754    else:
755        can_compare = False
756
757    # Check whether user is repo owner
758    if validate_owner(request, repo_id):
759        is_owner = True
760    else:
761        is_owner = False
762
763    zipped = gen_path_link(path, repo.name)
764
765    can_revert_file = True
766    username = request.user.username
767
768    try:
769        is_locked, locked_by_me = check_file_lock(repo_id, path, username)
770    except Exception as e:
771        logger.error(e)
772        is_locked, locked_by_me = False, False
773
774    repo_perm = seafile_api.check_permission_by_path(repo_id, path, username)
775    if repo_perm != 'rw' or (is_locked and not locked_by_me):
776        can_revert_file = False
777
778    # Whether use new file history API which read file history from db.
779    suffix_list = seafevents_api.get_file_history_suffix()
780    if suffix_list and isinstance(suffix_list, list):
781        suffix_list = [x.lower() for x in suffix_list]
782    else:
783        suffix_list = []
784
785    use_new_api = True if file_ext in suffix_list else False
786    use_new_style = True if use_new_api and filetype == 'markdown' else False
787
788    if use_new_style:
789        return render(request, 'file_revisions_new.html', {
790            'repo': repo,
791            'path': path,
792            'u_filename': u_filename,
793            'zipped': zipped,
794            'is_owner': is_owner,
795            'can_compare': can_compare,
796            'can_revert_file': can_revert_file,
797        })
798
799    return render(request, 'file_revisions_old.html', {
800        'repo': repo,
801        'path': path,
802        'u_filename': u_filename,
803        'zipped': zipped,
804        'is_owner': is_owner,
805        'can_compare': can_compare,
806        'can_revert_file': can_revert_file,
807        'can_download_file': parse_repo_perm(repo_perm).can_download,
808        'use_new_api': use_new_api,
809    })
810
811
812def demo(request):
813    """
814    Login as demo account.
815    """
816    from django.conf import settings as dj_settings
817    if not dj_settings.ENABLE_DEMO_USER:
818        raise Http404
819
820    try:
821        user = User.objects.get(email=settings.CLOUD_DEMO_USER)
822    except User.DoesNotExist:
823        logger.warn('CLOUD_DEMO_USER: %s does not exist.' % settings.CLOUD_DEMO_USER)
824        raise Http404
825
826    for backend in get_backends():
827        user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
828
829    auth_login(request, user)
830
831    redirect_to = settings.SITE_ROOT
832    return HttpResponseRedirect(redirect_to)
833
834def list_inner_pub_repos(request):
835    """List inner pub repos.
836    """
837    username = request.user.username
838    if is_org_context(request):
839        org_id = request.user.org.org_id
840        return seafile_api.list_org_inner_pub_repos(org_id)
841
842    if not request.cloud_mode:
843        return seafile_api.get_inner_pub_repo_list()
844
845    return []
846
847def i18n(request):
848    """
849    Set client language preference, lasts for one month
850
851    """
852    from django.conf import settings
853    next_page = request.META.get('HTTP_REFERER', settings.SITE_ROOT)
854
855    lang = request.GET.get('lang', settings.LANGUAGE_CODE)
856    if lang not in [e[0] for e in settings.LANGUAGES]:
857        # language code is not supported, use default.
858        lang = settings.LANGUAGE_CODE
859
860    # set language code to user profile if user is logged in
861    if not request.user.is_anonymous:
862        p = Profile.objects.get_profile_by_user(request.user.username)
863        if p is not None:
864            # update exist record
865            p.set_lang_code(lang)
866        else:
867            # add new record
868            Profile.objects.add_or_update(request.user.username, '', '', lang)
869
870    # set language code to client
871    res = HttpResponseRedirect(next_page)
872    res.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang, max_age=30*24*60*60)
873    return res
874
875@login_required
876def repo_download_dir(request, repo_id):
877    repo = get_repo(repo_id)
878    if not repo:
879        return render_error(request, _('Library does not exist'))
880
881    path = request.GET.get('p', '/')
882    if path[-1] != '/':         # Normalize dir path
883        path += '/'
884
885    if not seafile_api.get_dir_id_by_path(repo.id, path):
886        return render_error(request, _('"%s" does not exist.') % path)
887
888    if len(path) > 1:
889        dirname = os.path.basename(path.rstrip('/')) # Here use `rstrip` to cut out last '/' in path
890    else:
891        dirname = repo.name
892
893    allow_download = parse_repo_perm(check_folder_permission(
894        request, repo_id, '/')).can_download
895
896    if allow_download:
897
898        dir_id = seafile_api.get_dir_id_by_commit_and_path(repo.id,
899            repo.head_cmmt_id, path)
900
901        is_windows = 0
902        if is_windows_operating_system(request):
903            is_windows = 1
904
905        fake_obj_id = {
906            'obj_id': dir_id,
907            'dir_name': dirname,
908            'is_windows': is_windows
909        }
910
911        token = seafile_api.get_fileserver_access_token(
912                repo_id, json.dumps(fake_obj_id), 'download-dir', request.user.username)
913
914        if not token:
915            return render_error(request, _('Internal Server Error'))
916
917    else:
918        return render_error(request, _('Unable to download "%s"') % dirname )
919
920    url = gen_file_get_url(token, dirname)
921    from seahub.views.file import send_file_access_msg
922    send_file_access_msg(request, repo, path, 'web')
923    return redirect(url)
924
925def group_events_data(events):
926    """
927    Group events according to the date.
928    """
929    event_groups = []
930    for e in events:
931        e.time = utc_to_local(e.timestamp)
932        e.date = e.time.strftime("%Y-%m-%d")
933        if e.etype == 'repo-update':
934            e.author = e.commit.creator_name
935        elif e.etype == 'repo-create':
936            e.author = e.creator
937        else:
938            e.author = e.repo_owner
939
940        if len(event_groups) == 0 or \
941            len(event_groups) > 0 and e.date != event_groups[-1]['date']:
942            event_group = {}
943            event_group['date'] = e.date
944            event_group['events'] = [e]
945            event_groups.append(event_group)
946        else:
947            event_groups[-1]['events'].append(e)
948
949    return event_groups
950
951@login_required
952def convert_cmmt_desc_link(request):
953    """Return user to file/directory page based on the changes in commit.
954    """
955    repo_id = request.GET.get('repo_id')
956    cmmt_id = request.GET.get('cmmt_id')
957    name = request.GET.get('nm')
958
959    repo = get_repo(repo_id)
960    if not repo:
961        raise Http404
962
963    # perm check
964    if check_folder_permission(request, repo_id, '/') is None:
965        raise Http404
966
967    diff_result = seafserv_threaded_rpc.get_diff(repo_id, '', cmmt_id)
968    if not diff_result:
969        raise Http404
970
971    for d in diff_result:
972        if name not in d.name:
973            # skip to next diff_result if file/folder user clicked does not
974            # match the diff_result
975            continue
976
977        if d.status == 'add' or d.status == 'mod':  # Add or modify file
978            return HttpResponseRedirect(
979                reverse('view_lib_file', args=[repo_id, '/' + d.name]))
980        elif d.status == 'mov':  # Move or Rename non-empty file/folder
981            if '/' in d.new_name:
982                new_dir_name = d.new_name.split('/')[0]
983                reverse_url = reverse('lib_view', args=[repo_id, repo.name, new_dir_name])
984                return HttpResponseRedirect(reverse_url)
985            else:
986                return HttpResponseRedirect(
987                    reverse('view_lib_file', args=[repo_id, '/' + d.new_name]))
988        elif d.status == 'newdir':
989            reverse_url = reverse('lib_view', args=[repo_id, repo.name, d.name.strip('/')])
990            return HttpResponseRedirect(reverse_url)
991        else:
992            continue
993
994    status_list = [d.status for d in diff_result]
995    # Rename empty file/folder
996    if len(status_list) == 2:
997        if 'add' in status_list and 'del' in status_list:
998            for d in diff_result:
999                if d.status != 'add':
1000                    continue
1001
1002                return HttpResponseRedirect(
1003                    reverse('view_lib_file', args=[repo_id, '/' + d.name]))
1004
1005        if 'newdir' in status_list and 'deldir' in status_list:
1006            for d in diff_result:
1007                if d.status != 'newdir':
1008                    continue
1009
1010                return HttpResponseRedirect(
1011                    reverse('view_common_lib_dir', args=[repo_id, d.name]))
1012
1013    # Rename folder with empty files
1014    if len(status_list) > 2:
1015        if 'deldir' in status_list and 'add' in status_list:
1016            for d in diff_result:
1017                if d.status != 'add':
1018                    continue
1019
1020                new_dir = d.name.split('/')[0]
1021                return HttpResponseRedirect(
1022                    reverse('view_common_lib_dir', args=[repo_id, new_dir]))
1023
1024    raise Http404
1025
1026storage = get_avatar_file_storage()
1027def latest_entry(request, filename):
1028    try:
1029        return storage.modified_time(filename)
1030    except Exception as e:
1031        logger.error(e)
1032        return None
1033
1034@condition(last_modified_func=latest_entry)
1035def image_view(request, filename):
1036    if AVATAR_FILE_STORAGE is None:
1037        raise Http404
1038
1039    # read file from cache, if hit
1040    filename_md5 = hashlib.md5(filename.encode('utf-8')).hexdigest()
1041    cache_key = 'image_view__%s' % filename_md5
1042    file_content = cache.get(cache_key)
1043    if file_content is None:
1044        # otherwise, read file from database and update cache
1045        image_file = storage.open(filename, 'rb')
1046        if not image_file:
1047            raise Http404
1048        file_content = image_file.read()
1049        cache.set(cache_key, file_content, 365 * 24 * 60 * 60)
1050
1051    # Prepare response
1052    content_type, content_encoding = mimetypes.guess_type(filename)
1053    response = HttpResponse(content=file_content, content_type=content_type)
1054    response['Content-Disposition'] = 'inline; filename=%s' % filename
1055    if content_encoding:
1056        response['Content-Encoding'] = content_encoding
1057    return response
1058
1059def custom_css_view(request):
1060    file_content = config.CUSTOM_CSS
1061    response = HttpResponse(content=file_content, content_type='text/css')
1062    return response
1063
1064def underscore_template(request, template):
1065    """Serve underscore template through Django, mainly for I18n.
1066
1067    Arguments:
1068    - `request`:
1069    - `template`:
1070    """
1071    if not template.startswith('js'):  # light security check
1072        raise Http404
1073
1074    return render(request, template, {})
1075
1076def client_token_login(request):
1077    """Login from desktop client with a generated token.
1078    """
1079    tokenstr = request.GET.get('token', '')
1080    user = None
1081    if len(tokenstr) == 32:
1082        try:
1083            username = ClientLoginToken.objects.get_username(tokenstr)
1084        except ClientLoginToken.DoesNotExist:
1085            pass
1086        else:
1087            try:
1088                user = User.objects.get(email=username)
1089                for backend in get_backends():
1090                    user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
1091            except User.DoesNotExist:
1092                pass
1093
1094    if user:
1095        if request.user.is_authenticated and request.user.username == user.username:
1096            pass
1097        else:
1098            request.client_token_login = True
1099            auth_login(request, user)
1100
1101    return HttpResponseRedirect(request.GET.get("next", reverse('libraries')))
1102
1103def choose_register(request):
1104    """
1105    Choose register
1106    """
1107    login_bg_image_path = get_login_bg_image_path()
1108
1109    return render(request, 'choose_register.html', {
1110        'enable_weixin': ENABLE_WEIXIN,
1111        'login_bg_image_path': login_bg_image_path
1112    })
1113
1114
1115@login_required
1116def react_fake_view(request, **kwargs):
1117
1118    username = request.user.username
1119
1120    if resolve(request.path).url_name == 'lib_view':
1121
1122        repo_id = kwargs.get('repo_id', '')
1123        path = kwargs.get('path', '')
1124
1125        if repo_id and path and \
1126                not check_folder_permission(request, repo_id, path):
1127
1128            converted_repo_path = seafile_api.convert_repo_path(repo_id, path, username)
1129            if not converted_repo_path:
1130                error_msg = 'Permission denied.'
1131                return render_error(request, error_msg)
1132
1133            repo_path_dict = json.loads(converted_repo_path)
1134
1135            converted_repo_id = repo_path_dict['repo_id']
1136            converted_repo = seafile_api.get_repo(converted_repo_id)
1137            if not converted_repo:
1138                error_msg = 'Library %s not found.' % converted_repo_id
1139                return render_error(request, error_msg)
1140
1141            converted_path = repo_path_dict['path']
1142            if not seafile_api.get_dirent_by_path(converted_repo_id, converted_path):
1143                error_msg = 'Dirent %s not found.' % converted_path
1144                return render_error(request, error_msg)
1145
1146            if not check_folder_permission(request, converted_repo_id, converted_path):
1147                error_msg = 'Permission denied.'
1148                return render_error(request, error_msg)
1149
1150            next_url = reverse('lib_view', args=[converted_repo_id,
1151                                                 converted_repo.repo_name,
1152                                                 converted_path.strip('/')])
1153            return HttpResponseRedirect(next_url)
1154
1155    guide_enabled = UserOptions.objects.is_user_guide_enabled(username)
1156    if guide_enabled:
1157        create_default_library(request)
1158
1159    try:
1160        expire_days = seafile_api.get_server_config_int('library_trash', 'expire_days')
1161    except Exception as e:
1162        logger.error(e)
1163        expire_days = -1
1164
1165    folder_perm_enabled = True if is_pro_version() and ENABLE_FOLDER_PERM else False
1166
1167    try:
1168        max_upload_file_size = seafile_api.get_server_config_int('fileserver', 'max_upload_size')
1169    except Exception as e:
1170        logger.error(e)
1171        max_upload_file_size = -1
1172
1173    return render(request, "react_app.html", {
1174        "onlyoffice_desktop_editors_portal_login": ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN,
1175        "guide_enabled": guide_enabled,
1176        'trash_repos_expire_days': expire_days if expire_days > 0 else 30,
1177        'dtable_web_server': DTABLE_WEB_SERVER,
1178        'max_upload_file_size': max_upload_file_size,
1179        'seafile_collab_server': SEAFILE_COLLAB_SERVER,
1180        'storages': get_library_storages(request),
1181        'library_templates': list(LIBRARY_TEMPLATES.keys()),
1182        'enable_repo_snapshot_label': settings.ENABLE_REPO_SNAPSHOT_LABEL,
1183        'resumable_upload_file_block_size': settings.RESUMABLE_UPLOAD_FILE_BLOCK_SIZE,
1184        'max_number_of_files_for_fileupload': settings.MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD,
1185        'share_link_expire_days_default': SHARE_LINK_EXPIRE_DAYS_DEFAULT,
1186        'share_link_expire_days_min': SHARE_LINK_EXPIRE_DAYS_MIN,
1187        'share_link_expire_days_max': SHARE_LINK_EXPIRE_DAYS_MAX,
1188        'upload_link_expire_days_default': UPLOAD_LINK_EXPIRE_DAYS_DEFAULT,
1189        'upload_link_expire_days_min': UPLOAD_LINK_EXPIRE_DAYS_MIN,
1190        'upload_link_expire_days_max': UPLOAD_LINK_EXPIRE_DAYS_MAX,
1191        'enable_encrypted_library': config.ENABLE_ENCRYPTED_LIBRARY,
1192        'enable_repo_history_setting': config.ENABLE_REPO_HISTORY_SETTING,
1193        'enable_reset_encrypted_repo_password': ENABLE_RESET_ENCRYPTED_REPO_PASSWORD,
1194        'enableFileComment': settings.ENABLE_FILE_COMMENT,
1195        'is_email_configured': IS_EMAIL_CONFIGURED,
1196        'can_add_public_repo': request.user.permissions.can_add_public_repo(),
1197        'folder_perm_enabled': folder_perm_enabled,
1198        'file_audit_enabled': FILE_AUDIT_ENABLED,
1199        'custom_nav_items': json.dumps(CUSTOM_NAV_ITEMS),
1200        'enable_show_contact_email_when_search_user': settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER,
1201        'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
1202        'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
1203        'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
1204        'enable_ocm': ENABLE_OCM,
1205        'ocm_remote_servers': OCM_REMOTE_SERVERS,
1206        'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT,
1207        'group_import_members_extra_msg': GROUP_IMPORT_MEMBERS_EXTRA_MSG,
1208    })
1209