diff --git a/dev-requirements.txt b/dev-requirements.txt
index a63453d2b6..4f357df201 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -3,3 +3,5 @@
fabric
transifex-client
mysqlclient==1.3.12
+
+raven==5.0.0
diff --git a/media/css/seahub.css b/media/css/seahub.css
index 0c15661fa4..6708e6df24 100644
--- a/media/css/seahub.css
+++ b/media/css/seahub.css
@@ -197,6 +197,7 @@
.sf2-icon-more:before { content: "\e032"; }
.sf2-icon-plus2:before { content: "\e033"; }
.sf2-icon-upload:before { content: "\e034"; }
+.sf2-icon-x3:before { content: "\e035"; }
/******* tags **********/
html, body {
@@ -3292,6 +3293,11 @@ button.sf-dropdown-toggle:focus {
border-bottom:1px dotted #ddd;
margin-bottom:3px;
}
+.search-time-range {
+ border-top:1px dotted #ddd;
+ margin-top:3px;
+ padding-top:3px;
+}
.custom-ftype-options {
font-size:13px;
padding:2px 0;
diff --git a/media/css/sf_font2/seafile-font2.eot b/media/css/sf_font2/seafile-font2.eot
index 48a9fe2c21..766a7e180a 100644
Binary files a/media/css/sf_font2/seafile-font2.eot and b/media/css/sf_font2/seafile-font2.eot differ
diff --git a/media/css/sf_font2/seafile-font2.svg b/media/css/sf_font2/seafile-font2.svg
index cbf012acd0..cbf6166c02 100644
--- a/media/css/sf_font2/seafile-font2.svg
+++ b/media/css/sf_font2/seafile-font2.svg
@@ -60,4 +60,5 @@
+
diff --git a/media/css/sf_font2/seafile-font2.ttf b/media/css/sf_font2/seafile-font2.ttf
index 76085179f9..c0e5748d7f 100644
Binary files a/media/css/sf_font2/seafile-font2.ttf and b/media/css/sf_font2/seafile-font2.ttf differ
diff --git a/media/css/sf_font2/seafile-font2.woff b/media/css/sf_font2/seafile-font2.woff
index cba87f8def..ba90fc93de 100644
Binary files a/media/css/sf_font2/seafile-font2.woff and b/media/css/sf_font2/seafile-font2.woff differ
diff --git a/media/js/base.js b/media/js/base.js
index b03c95f872..d240ea823d 100644
--- a/media/js/base.js
+++ b/media/js/base.js
@@ -265,7 +265,7 @@ function feedback(con, type, time) {
hide_pos_top = '-' + ($el.outerHeight() + parseInt(show_pos_top)) + 'px';
// add transition: from 'hide' to 'show'. the transition effect is offered by CSS.
- $el.css({'left':($(window).width() - $el.width())/2, 'top': hide_pos_top});
+ $el.css({'left':($(window).width() - $el.width())/2, 'top': hide_pos_top}).removeClass('hide');
setTimeout(function() { $el.css({'top': show_pos_top}); }, 10);
setTimeout(function() { $el.css({'top': hide_pos_top}); }, 5000);
diff --git a/seahub/api2/endpoints/admin/libraries.py b/seahub/api2/endpoints/admin/libraries.py
index 697f074f0f..b44c3b67ad 100644
--- a/seahub/api2/endpoints/admin/libraries.py
+++ b/seahub/api2/endpoints/admin/libraries.py
@@ -41,7 +41,7 @@ def get_repo_info(repo):
except Exception:
org_repo_owner = None
- owner = repo_owner or org_repo_owner
+ owner = repo_owner or org_repo_owner or ''
result = {}
result['id'] = repo.repo_id
diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py
new file mode 100644
index 0000000000..fc3efdb663
--- /dev/null
+++ b/seahub/api2/endpoints/admin/organizations.py
@@ -0,0 +1,222 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+import logging
+
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAdminUser
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+
+from seaserv import ccnet_api, seafile_api
+
+from seaserv import seafserv_threaded_rpc
+
+from seahub.utils.file_size import get_file_size_unit
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
+from seahub.base.templatetags.seahub_tags import email2nickname, \
+ email2contact_email
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+from seahub.api2.permissions import IsProVersion
+
+try:
+ from seahub.settings import ORG_MEMBER_QUOTA_ENABLED
+except ImportError:
+ ORG_MEMBER_QUOTA_ENABLED= False
+
+if ORG_MEMBER_QUOTA_ENABLED:
+ from seahub_extra.organizations.models import OrgMemberQuota
+
+try:
+ from seahub.settings import CLOUD_MODE
+except ImportError:
+ CLOUD_MODE = False
+
+try:
+ from seahub.settings import MULTI_TENANCY
+except ImportError:
+ MULTI_TENANCY = False
+
+logger = logging.getLogger(__name__)
+
+def get_org_info(org_id):
+
+ org_info = {}
+
+ org = ccnet_api.get_org_by_id(org_id)
+
+ org_info['org_id'] = org_id
+ org_info['org_name'] = org.org_name
+ org_info['ctime'] = timestamp_to_isoformat_timestr(org.ctime)
+ org_info['org_url_prefix'] = org.url_prefix
+
+ creator = org.creator
+ org_info['creator_email'] = creator
+ org_info['creator_name'] = email2nickname(creator)
+ org_info['creator_contact_email'] = email2contact_email(creator)
+
+ org_info['quota'] = seafile_api.get_org_quota(org_id)
+
+ if ORG_MEMBER_QUOTA_ENABLED:
+ org_info['max_user_number'] = OrgMemberQuota.objects.get_quota(org_id)
+
+ return org_info
+
+
+class AdminOrganization(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAdminUser, IsProVersion)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, org_id):
+ """ Get base info of a organization
+
+ Permission checking:
+ 1. only admin can perform this action.
+ """
+
+ if not (CLOUD_MODE and MULTI_TENANCY):
+ error_msg = 'Feature is not enabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ org_id = int(org_id)
+ if org_id == 0:
+ error_msg = 'org_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ org = ccnet_api.get_org_by_id(org_id)
+ if not org:
+ error_msg = 'Organization %s not found.' % org_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ try:
+ org_info = get_org_info(org_id)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response(org_info)
+
+ def put(self, request, org_id):
+ """ Update base info of a organization
+
+ Permission checking:
+ 1. only admin can perform this action.
+ """
+
+ if not (CLOUD_MODE and MULTI_TENANCY):
+ error_msg = 'Feature is not enabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ org_id = int(org_id)
+ if org_id == 0:
+ error_msg = 'org_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ org = ccnet_api.get_org_by_id(org_id)
+ if not org:
+ error_msg = 'Organization %s not found.' % org_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # update org name
+ new_name = request.data.get('org_name', None)
+ if new_name:
+ try:
+ ccnet_api.set_org_name(org_id, new_name)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ # update org max user number
+ max_user_number = request.data.get('max_user_number', None)
+ if max_user_number and ORG_MEMBER_QUOTA_ENABLED:
+
+ try:
+ max_user_number = int(max_user_number)
+ except ValueError:
+ error_msg = 'max_user_number invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if max_user_number <= 0:
+ error_msg = 'max_user_number invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ OrgMemberQuota.objects.set_quota(org_id, max_user_number)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ quota_mb = request.data.get('quota', None)
+ if quota_mb:
+
+ try:
+ quota_mb = int(quota_mb)
+ except ValueError:
+ error_msg = 'quota invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if quota_mb < 0:
+ error_msg = 'quota invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+
+ quota = quota_mb * get_file_size_unit('MB')
+ try:
+ seafile_api.set_org_quota(org_id, quota)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ org_info = get_org_info(org_id)
+ return Response(org_info)
+
+ def delete(self, request, org_id):
+ """ Delete an organization
+
+ Permission checking:
+ 1. only admin can perform this action.
+ """
+
+ if not (CLOUD_MODE and MULTI_TENANCY):
+ error_msg = 'Feature is not enabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ org_id = int(org_id)
+ if org_id == 0:
+ error_msg = 'org_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ org = ccnet_api.get_org_by_id(org_id)
+ if not org:
+ error_msg = 'Organization %s not found.' % org_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ try:
+ # remove org users
+ users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
+ for u in users:
+ ccnet_api.remove_org_user(org_id, u.email)
+
+ # remove org groups
+ groups = ccnet_api.get_org_groups(org_id, -1, -1)
+ for g in groups:
+ ccnet_api.remove_org_group(org_id, g.gid)
+
+ # remove org repos
+ seafile_api.remove_org_repo_by_org_id(org_id)
+
+ # remove org
+ ccnet_api.remove_org(org_id)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
diff --git a/seahub/api2/endpoints/file.py b/seahub/api2/endpoints/file.py
index 59296f520e..107ef072ba 100644
--- a/seahub/api2/endpoints/file.py
+++ b/seahub/api2/endpoints/file.py
@@ -21,7 +21,7 @@ from seahub.utils import check_filename_with_rename, is_pro_version, \
normalize_dir_path
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.views import check_folder_permission
-from seahub.utils.file_op import check_file_lock
+from seahub.utils.file_op import check_file_lock, if_locked_by_online_office
from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN, \
FILE_LOCK_EXPIRATION_DAYS, OFFICE_TEMPLATE_ROOT
@@ -464,7 +464,7 @@ class FileView(APIView):
return Response({'success': True})
def put(self, request, repo_id, format=None):
- """ Currently only for lock and unlock file operation.
+ """ Currently only support lock, unlock, refresh-lock file.
Permission checking:
1. user with 'rw' permission for current file;
@@ -487,8 +487,8 @@ class FileView(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
operation = operation.lower()
- if operation not in ('lock', 'unlock'):
- error_msg = "operation can only be 'lock', or 'unlock'."
+ if operation not in ('lock', 'unlock', 'refresh-lock'):
+ error_msg = "operation can only be 'lock', 'unlock' or 'refresh-lock'."
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
@@ -516,34 +516,62 @@ class FileView(APIView):
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+ # check if is locked by online office
+ locked_by_online_office = if_locked_by_online_office(repo_id, path)
+
if operation == 'lock':
+
+ if is_locked:
+ error_msg = _("File is locked")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # lock file
+ expire = request.data.get('expire', FILE_LOCK_EXPIRATION_DAYS)
+ try:
+ seafile_api.lock_file(repo_id, path, username, expire)
+ except SearpcError, e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ if operation == 'unlock':
+
if not is_locked:
- # lock file
- expire = request.data.get('expire', FILE_LOCK_EXPIRATION_DAYS)
+ error_msg = _("File is not locked.")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # file can only be locked by normal user or OnlineOffice
+ is_repo_owner = seafile_api.is_repo_owner(username, repo_id)
+ if locked_by_me or \
+ (locked_by_online_office and is_repo_owner):
+ # unlock file
try:
- seafile_api.lock_file(repo_id, path.lstrip('/'), username, expire)
+ seafile_api.unlock_file(repo_id, path)
except SearpcError, e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
else:
- if not locked_by_me:
- error_msg = _("File is locked")
- return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ error_msg = 'You can not unlock this file.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
- if operation == 'unlock':
- if is_locked:
- if not locked_by_me:
- error_msg = 'You can not unlock this file.'
- return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+ if operation == 'refresh-lock':
- # unlock file
+ if not is_locked:
+ error_msg = _("File is not locked.")
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if locked_by_me or locked_by_online_office:
+ # refresh lock file
try:
- seafile_api.unlock_file(repo_id, path.lstrip('/'))
+ seafile_api.refresh_file_lock(repo_id, path)
except SearpcError, e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+ else:
+ error_msg = _("You can not refresh this file's lock.")
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
file_info = self.get_file_info(username, repo_id, path)
return Response(file_info)
diff --git a/seahub/api2/endpoints/move_folder_merge.py b/seahub/api2/endpoints/move_folder_merge.py
new file mode 100644
index 0000000000..97f15c7bad
--- /dev/null
+++ b/seahub/api2/endpoints/move_folder_merge.py
@@ -0,0 +1,181 @@
+# Copyright (c) 2012-2018 Seafile Ltd.
+
+import stat
+import logging
+import posixpath
+
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+from django.utils.translation import ugettext as _
+
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.utils import api_error
+from seahub.api2.views import HTTP_443_ABOVE_QUOTA
+
+from seahub.views import check_folder_permission
+from seahub.utils.repo import get_repo_owner
+from seahub.settings import MAX_PATH
+
+from seaserv import seafile_api
+
+logger = logging.getLogger(__name__)
+
+def get_dirent_name_list(username, repo_id, parent_path):
+
+ file_name_list = []
+ folder_name_list = []
+
+ path_id = seafile_api.get_dir_id_by_path(repo_id, parent_path)
+ dirs = seafile_api.list_dir_with_perm(repo_id, parent_path,
+ path_id, username, -1, -1)
+
+ for dirent in dirs:
+ if stat.S_ISDIR(dirent.mode):
+ folder_name_list.append(dirent.obj_name)
+ else:
+ file_name_list.append(dirent.obj_name)
+
+ return folder_name_list, file_name_list
+
+def folder_name_duplicate(username, src_folder_name, dst_repo_id, dst_parent_dir):
+
+ dst_folder_name_list, dst_file_name_list = get_dirent_name_list(username,
+ dst_repo_id, dst_parent_dir)
+
+ if src_folder_name in dst_folder_name_list:
+ return True
+ else:
+ return False
+
+def move_folder_with_merge(username,
+ src_repo_id, src_parent_dir, src_dirent_name,
+ dst_repo_id, dst_parent_dir, dst_dirent_name):
+
+ if folder_name_duplicate(username, src_dirent_name,
+ dst_repo_id, dst_parent_dir):
+
+ src_folder_path = posixpath.join(src_parent_dir, src_dirent_name)
+ dst_folder_path = posixpath.join(dst_parent_dir, dst_dirent_name)
+ src_sub_folder_name_list, src_sub_file_name_list = get_dirent_name_list(username,
+ src_repo_id, src_folder_path)
+
+ # for sub file, copy it directly
+ for src_sub_file_name in src_sub_file_name_list:
+ seafile_api.move_file(
+ src_repo_id, src_folder_path, src_sub_file_name,
+ dst_repo_id, dst_folder_path, src_sub_file_name,
+ replace=False, username=username, need_progress=0)
+
+ for src_sub_folder_name in src_sub_folder_name_list:
+ move_folder_with_merge(username,
+ src_repo_id, src_folder_path, src_sub_folder_name,
+ dst_repo_id, dst_folder_path, src_sub_folder_name)
+ else:
+ seafile_api.move_file(
+ src_repo_id, src_parent_dir, src_dirent_name,
+ dst_repo_id, dst_parent_dir, dst_dirent_name,
+ replace=False, username=username, need_progress=0)
+
+
+class MoveFolderMergeView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request):
+ """ Only support move folder.
+
+ Permission checking:
+
+ User with 'rw' permission for src/dst folder.
+ """
+ src_repo_id = request.data.get('src_repo_id', None)
+ src_parent_dir = request.data.get('src_parent_dir', None)
+ src_folder_name = request.data.get('src_dirent_name', None)
+ dst_repo_id = request.data.get('dst_repo_id', None)
+ dst_parent_dir = request.data.get('dst_parent_dir', None)
+
+ # argument check
+ if not src_repo_id:
+ error_msg = 'src_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not src_parent_dir:
+ error_msg = 'src_parent_dir invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not src_folder_name:
+ error_msg = 'src_dirent_name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not dst_repo_id:
+ error_msg = 'dst_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not dst_parent_dir:
+ error_msg = 'dst_parent_dir invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if src_repo_id == dst_repo_id and src_parent_dir == dst_parent_dir:
+ error_msg = _('Invalid destination path')
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if len(dst_parent_dir + src_folder_name) > MAX_PATH:
+ error_msg = _('Destination path is too long.')
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # src resource check
+ src_repo = seafile_api.get_repo(src_repo_id)
+ if not src_repo:
+ error_msg = 'Library %s not found.' % src_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ src_folder_path = posixpath.join(src_parent_dir, src_folder_name)
+ dir_id = seafile_api.get_dir_id_by_path(src_repo_id, src_folder_path)
+ if not dir_id:
+ error_msg = 'Folder %s not found.' % src_folder_path
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # dst resource check
+ dst_repo = seafile_api.get_repo(dst_repo_id)
+ if not dst_repo:
+ error_msg = 'Library %s not found.' % dst_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if not seafile_api.get_dir_id_by_path(dst_repo_id, dst_parent_dir):
+ error_msg = 'Folder %s not found.' % dst_parent_dir
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check for src folder
+ if check_folder_permission(request, src_repo_id, src_folder_path) != 'rw':
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # permission check for dst parent dir
+ if check_folder_permission(request, dst_repo_id, dst_parent_dir) != 'rw':
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ ## check if above quota for dst repo
+ if get_repo_owner(request, src_repo_id) != get_repo_owner(request, dst_repo_id):
+
+ current_size = 0
+ current_size = seafile_api.get_dir_size(src_repo.store_id,
+ src_repo.version, dir_id)
+
+ if seafile_api.check_quota(dst_repo_id, current_size) < 0:
+ return api_error(HTTP_443_ABOVE_QUOTA, _(u"Out of quota."))
+
+ username = request.user.username
+ move_folder_with_merge(username,
+ src_repo_id, src_parent_dir, src_folder_name,
+ dst_repo_id, dst_parent_dir, src_folder_name)
+
+ seafile_api.del_file(src_repo_id, src_parent_dir, src_folder_name, username)
+
+ return Response({'success': True})
diff --git a/seahub/api2/endpoints/repo_set_password.py b/seahub/api2/endpoints/repo_set_password.py
index 8ea42dbf74..3ebd30c0cc 100644
--- a/seahub/api2/endpoints/repo_set_password.py
+++ b/seahub/api2/endpoints/repo_set_password.py
@@ -14,8 +14,11 @@ from pysearpc import SearpcError
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
+from seahub.utils.repo import is_repo_owner, add_encrypted_repo_secret_key_to_database
+from seahub.base.models import RepoSecretKey
+from seahub.views import check_folder_permission
-from seahub.utils import is_org_context
+from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD
logger = logging.getLogger(__name__)
@@ -26,20 +29,36 @@ class RepoSetPassword(APIView):
throttle_classes = (UserRateThrottle,)
def post(self, request, repo_id):
+ """ Check if repo password is correct.
- repo = seafile_api.get_repo(repo_id)
- if not repo:
- error_msg = 'Library %s not found.' % repo_id
- return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+ Permission checking:
+ 1. User can access current repo.
+ """
+ # argument check
password = request.POST.get('password', None)
if not password:
error_msg = 'password invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ # resource check
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if not repo.encrypted:
+ error_msg = 'Library %s is not encrypted.' % repo_id
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # permission check
+ if not check_folder_permission(request, repo_id, '/'):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # check the password is correct
try:
seafile_api.set_passwd(repo_id, request.user.username, password)
- return Response({'success': True})
except SearpcError as e:
if e.msg == 'Bad arguments':
error_msg = 'Bad arguments'
@@ -54,22 +73,23 @@ class RepoSetPassword(APIView):
error_msg = _(u'Decrypt library error')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+ if ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ add_encrypted_repo_secret_key_to_database(repo_id, password)
+
+ return Response({'success': True})
+
def put(self, request, repo_id):
- """ Change repo password.
+ """ Change/Init repo password.
Permission checking:
1. repo owner
"""
# argument check
- old_password = request.POST.get('old_password', None)
- if not old_password:
- error_msg = 'old_password invalid.'
- return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
-
- new_password = request.POST.get('new_password', None)
- if not new_password:
- error_msg = 'new_password invalid.'
+ operation = request.POST.get('operation', 'change-password')
+ operation = operation.lower()
+ if operation not in ('change-password', 'reset-password', 'can-reset-password'):
+ error_msg = 'operation invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
@@ -78,25 +98,71 @@ class RepoSetPassword(APIView):
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
- # permission check
- if is_org_context(request):
- repo_owner = seafile_api.get_org_repo_owner(repo.id)
- else:
- repo_owner = seafile_api.get_repo_owner(repo.id)
+ if not repo.encrypted:
+ error_msg = 'Library %s is not encrypted.' % repo_id
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ # permission check
username = request.user.username
- if username != repo_owner:
+ if not is_repo_owner(request, repo_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
- # change password
- try:
- seafile_api.change_repo_passwd(repo_id, old_password, new_password, username)
- except SearpcError as e:
- if e.msg == 'Incorrect password':
- error_msg = _(u'Wrong old password')
+ if operation == 'change-password':
+
+ old_password = request.POST.get('old_password', None)
+ if not old_password:
+ error_msg = 'old_password invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ new_password = request.POST.get('new_password', None)
+ if not new_password:
+ error_msg = 'new_password invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ seafile_api.change_repo_passwd(repo_id, old_password, new_password, username)
+ except Exception as e:
+ if e.msg == 'Incorrect password':
+ error_msg = _(u'Wrong old password')
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+ else:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ if ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ add_encrypted_repo_secret_key_to_database(repo_id, new_password)
+
+ if operation == 'can-reset-password':
+ if not ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ error_msg = 'Feature disabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if not RepoSecretKey.objects.get_secret_key(repo_id):
+ return Response({'allowed': False})
else:
+ return Response({'allowed': True})
+
+ if operation == 'reset-password':
+
+ if not ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ error_msg = 'Feature disabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ new_password = request.POST.get('new_password', None)
+ if not new_password:
+ error_msg = 'new_password invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ secret_key = RepoSecretKey.objects.get_secret_key(repo_id)
+ if not secret_key:
+ error_msg = 'repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ seafile_api.reset_repo_passwd(repo_id, username, secret_key, new_password)
+ except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py
index 27172d22a4..9a9308a22d 100644
--- a/seahub/api2/endpoints/repos_batch.py
+++ b/seahub/api2/endpoints/repos_batch.py
@@ -20,12 +20,20 @@ from seahub.api2.views import HTTP_443_ABOVE_QUOTA
from seahub.group.utils import is_group_member
from seahub.base.accounts import User
+from seahub.share.utils import is_repo_admin, \
+ check_user_share_out_permission, check_group_share_out_permission
+from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission
from seahub.share.signals import share_repo_to_user_successful, \
share_repo_to_group_successful
from seahub.utils import is_org_context, send_perm_audit_msg, \
- normalize_dir_path, get_folder_permission_recursively
+ normalize_dir_path, get_folder_permission_recursively, \
+ normalize_file_path, check_filename_with_rename
+from seahub.utils.repo import get_repo_owner
+
from seahub.views import check_folder_permission
from seahub.settings import MAX_PATH
+from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, \
+ PERMISSION_ADMIN
logger = logging.getLogger(__name__)
@@ -87,14 +95,17 @@ class ReposBatchView(APIView):
def post(self, request):
# argument check
- operation = request.data.get('operation')
-
- # operation could be `share`, `delete`, `transfer`
- # we now only use `share`
- if not operation or operation not in ('share'):
+ operation = request.data.get('operation', None)
+ if not operation:
error_msg = 'operation invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ # operation could be `share`, `unshare`, `delete`, `transfer`
+ # we now only use `share`, `unshare`
+ if operation not in ('share', 'unshare'):
+ error_msg = 'operation can only be "share", "unshare".'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
result = {}
result['failed'] = []
result['success'] = []
@@ -113,35 +124,30 @@ class ReposBatchView(APIView):
})
continue
- if is_org_context(request):
- org_id = request.user.org.org_id
- org_repo_owner = seafile_api.get_org_repo_owner(repo_id)
- if not username == org_repo_owner:
- result['failed'].append({
- 'repo_id': repo_id,
- 'error_msg': 'Permission denied.'
- })
- continue
- else:
- if not seafile_api.is_repo_owner(username, repo_id):
- result['failed'].append({
- 'repo_id': repo_id,
- 'error_msg': 'Permission denied.'
- })
- continue
+ repo_owner = get_repo_owner(request, repo_id)
+ if repo_owner != username and not is_repo_admin(username, repo_id):
+ result['failed'].append({
+ 'repo_id': repo_id,
+ 'error_msg': 'Permission denied.'
+ })
+ continue
valid_repo_id_list.append(repo_id)
# share repo
if operation == 'share':
- share_type = request.data.get('share_type')
- if share_type != 'user' and share_type != 'group':
+ share_type = request.data.get('share_type', None)
+ if not share_type:
error_msg = 'share_type invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+ if share_type not in ('user', 'group'):
+ error_msg = 'share_type can only be "user", "group".'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
permission = request.data.get('permission', 'rw')
- if permission not in ('r', 'rw'):
+ if permission not in [PERMISSION_READ, PERMISSION_READ_WRITE, PERMISSION_ADMIN]:
error_msg = 'permission invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
@@ -162,7 +168,7 @@ class ReposBatchView(APIView):
try:
org_of_to_user = ccnet_api.get_orgs_by_user(to_username)
except Exception as e:
- logger.debug(e)
+ logger.error(e)
org_of_to_user = []
if is_org_context(request):
@@ -285,6 +291,123 @@ class ReposBatchView(APIView):
'error_msg': 'Internal Server Error'
})
+ # unshare repo
+ if operation == 'unshare':
+
+ share_type = request.data.get('share_type', None)
+ if not share_type:
+ error_msg = 'share_type invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if share_type not in ('user', 'group'):
+ error_msg = 'share_type can only be "user", "group".'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # unshare repo from user
+ if share_type == 'user':
+ to_username = request.data.get('username', None)
+ if not to_username:
+ error_msg = 'username invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ for repo_id in valid_repo_id_list:
+
+ if not self.has_shared_to_user(request, repo_id, to_username):
+ result['failed'].append({
+ 'repo_id': repo_id,
+ 'error_msg': 'This item has not been shared to %s.' % to_username
+ })
+ continue
+
+ repo_owner = get_repo_owner(request, repo_id)
+ try:
+ # get share permission before unshare operation
+ permission = check_user_share_out_permission(repo_id,
+ '/', to_username, is_org_context(request))
+
+ if is_org_context(request):
+ # when calling seafile API to share authority related functions, change the uesrname to repo owner.
+ org_id = request.user.org.org_id
+ seafile_api.org_remove_share(org_id, repo_id, repo_owner, to_username)
+ else:
+ seafile_api.remove_share(repo_id, repo_owner, to_username)
+
+ # Delete share permission at ExtraSharePermission table.
+ ExtraSharePermission.objects.delete_share_permission(repo_id,
+ to_username)
+
+ # send message
+ send_perm_audit_msg('delete-repo-perm', username,
+ to_username, repo_id, '/', permission)
+
+ result['success'].append({
+ "repo_id": repo_id,
+ "username": to_username,
+ })
+ except Exception as e:
+ logger.error(e)
+ result['failed'].append({
+ 'repo_id': repo_id,
+ 'error_msg': 'Internal Server Error'
+ })
+
+ # unshare repo from group
+ if share_type == 'group':
+ to_group_id = request.data.get('group_id', None)
+ if not to_group_id:
+ error_msg = 'group_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ try:
+ to_group_id = int(to_group_id)
+ except ValueError:
+ error_msg = 'group_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ group = ccnet_api.get_group(to_group_id)
+ group_name = group.group_name if group else ''
+
+ for repo_id in valid_repo_id_list:
+ if not self.has_shared_to_group(request, repo_id, to_group_id):
+ result['failed'].append({
+ 'repo_id': repo_id,
+ 'error_msg': 'This item has not been shared to %s.' % group_name
+ })
+ continue
+
+ try:
+ # get share permission before unshare operation
+ permission = check_group_share_out_permission(repo_id,
+ '/', to_group_id, is_org_context(request))
+
+ org_id = None
+ if is_org_context(request):
+ org_id = request.user.org.org_id
+ seafile_api.del_org_group_repo(repo_id, org_id, to_group_id)
+ else:
+ seafile_api.unset_group_repo(
+ repo_id, to_group_id, username)
+
+ # Delete share permission at ExtraSharePermission table.
+ ExtraGroupsSharePermission.objects.delete_share_permission(repo_id,
+ to_group_id)
+
+ # send message
+ send_perm_audit_msg('delete-repo-perm', username,
+ to_group_id, repo_id, '/', permission)
+
+ result['success'].append({
+ "repo_id": repo_id,
+ "group_id": to_group_id,
+ "group_name": group_name,
+ })
+ except SearpcError as e:
+ logger.error(e)
+ result['failed'].append({
+ 'repo_id': repo_id,
+ 'error_msg': 'Internal Server Error'
+ })
+
return Response(result)
@@ -581,8 +704,6 @@ class ReposBatchCreateDirView(APIView):
continue
try:
- # TODO
- # move seafile_api.mkdir_with_parents() to CE version
# rename obj name if name is existed
seafile_api.mkdir_with_parents(repo_id, '/', path.strip('/'), username)
except Exception as e:
@@ -597,3 +718,348 @@ class ReposBatchCreateDirView(APIView):
result['success'].append(common_dict)
return Response(result)
+
+
+class ReposBatchCopyItemView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ def post(self, request):
+ """ Multi copy files/folders.
+ Permission checking:
+ 1. User must has `r/rw` permission for src folder.
+ 2. User must has `rw` permission for dst folder.
+ Parameter:
+ {
+ "src_repo_id":"7460f7ac-a0ff-4585-8906-bb5a57d2e118",
+ "dst_repo_id":"a3fa768d-0f00-4343-8b8d-07b4077881db",
+ "paths":[
+ {"src_path":"/1/2/3/","dst_path":"/4/5/6/"},
+ {"src_path":"/a/b/c/","dst_path":"/d/e/f/"},
+ ]
+ }
+ """
+
+ # argument check
+ path_list = request.data.get('paths', None)
+ if not path_list:
+ error_msg = 'paths invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ src_repo_id = request.data.get('src_repo_id', None)
+ if not src_repo_id:
+ error_msg = 'src_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ dst_repo_id = request.data.get('dst_repo_id', None)
+ if not dst_repo_id:
+ error_msg = 'dst_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # resource check
+ src_repo = seafile_api.get_repo(src_repo_id)
+ if not src_repo:
+ error_msg = 'Library %s not found.' % src_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ dst_repo = seafile_api.get_repo(dst_repo_id)
+ if not dst_repo:
+ error_msg = 'Library %s not found.' % dst_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ if check_folder_permission(request, src_repo_id, '/') is None:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if check_folder_permission(request, dst_repo_id, '/') is None:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ result = {}
+ result['failed'] = []
+ result['success'] = []
+ username = request.user.username
+
+ for path_item in path_list:
+
+ src_path = path_item['src_path']
+ src_path = normalize_dir_path(src_path)
+ src_parent_dir = os.path.dirname(src_path.rstrip('/'))
+ src_parent_dir = normalize_dir_path(src_parent_dir)
+ src_obj_name = os.path.basename(src_path.rstrip('/'))
+
+ dst_path = path_item['dst_path']
+ dst_path = normalize_dir_path(dst_path)
+ dst_parent_dir = dst_path
+ dst_obj_name = src_obj_name
+
+ common_dict = {
+ 'src_repo_id': src_repo_id,
+ 'src_path': src_path,
+ 'dst_repo_id': dst_repo_id,
+ 'dst_path': dst_path,
+ }
+
+ # src/dst parameter check
+ if src_repo_id == dst_repo_id and \
+ dst_path.startswith(src_path):
+ error_dict = {
+ 'error_msg': "The destination directory is the same as the source, or is it's subfolder."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ if src_path == '/':
+ error_dict = {
+ 'error_msg': "The source path can not be '/'."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ if len(dst_parent_dir + dst_obj_name) > MAX_PATH:
+ error_dict = {
+ 'error_msg': "'Destination path is too long."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # src resource check
+ ## as we don't know if `src_path` stands for a file or a folder,
+ ## so we check both
+ src_dir_id = seafile_api.get_dir_id_by_path(src_repo_id, src_path)
+ src_file_id = seafile_api.get_file_id_by_path(src_repo_id,
+ normalize_file_path(src_path))
+
+ if not src_dir_id and not src_file_id:
+ error_dict = {
+ 'error_msg': '%s not found.' % src_path
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # dst resource check
+ if not seafile_api.get_dir_id_by_path(dst_repo_id, dst_path):
+ error_dict = {
+ 'error_msg': 'Folder %s not found.' % dst_path
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # src path permission check, user must has `r/rw` permission for src folder.
+ if check_folder_permission(request, src_repo_id, src_parent_dir) is None:
+ error_dict = {
+ 'error_msg': 'Permission denied.'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # dst path permission check, user must has `rw` permission for dst folder.
+ if check_folder_permission(request, dst_repo_id, dst_path) != 'rw':
+ error_dict = {
+ 'error_msg': 'Permission denied.'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ try:
+ dst_obj_name = check_filename_with_rename(dst_repo_id,
+ dst_parent_dir, dst_obj_name)
+ # need_progress=0, synchronous=1
+ seafile_api.copy_file(src_repo_id, src_parent_dir, src_obj_name,
+ dst_repo_id, dst_parent_dir, dst_obj_name, username, 0, 1)
+ except Exception as e:
+ logger.error(e)
+ error_dict = {
+ 'error_msg': 'Internal Server Error'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ common_dict['dst_obj_name'] = dst_obj_name
+ result['success'].append(common_dict)
+
+ return Response(result)
+
+
+class ReposBatchMoveItemView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ def post(self, request):
+ """ Multi move files/folders.
+ Permission checking:
+ 1. User must has `rw` permission for src folder.
+ 2. User must has `rw` permission for dst folder.
+ Parameter:
+ {
+ "src_repo_id":"7460f7ac-a0ff-4585-8906-bb5a57d2e118",
+ "dst_repo_id":"a3fa768d-0f00-4343-8b8d-07b4077881db",
+ "paths":[
+ {"src_path":"/1/2/3/","dst_path":"/4/5/6/"},
+ {"src_path":"/a/b/c/","dst_path":"/d/e/f/"},
+ ]
+ }
+ """
+
+ # argument check
+ path_list = request.data.get('paths', None)
+ if not path_list:
+ error_msg = 'paths invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ src_repo_id = request.data.get('src_repo_id', None)
+ if not src_repo_id:
+ error_msg = 'src_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ dst_repo_id = request.data.get('dst_repo_id', None)
+ if not dst_repo_id:
+ error_msg = 'dst_repo_id invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # resource check
+ src_repo = seafile_api.get_repo(src_repo_id)
+ if not src_repo:
+ error_msg = 'Library %s not found.' % src_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ dst_repo = seafile_api.get_repo(dst_repo_id)
+ if not dst_repo:
+ error_msg = 'Library %s not found.' % dst_repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # permission check
+ if check_folder_permission(request, src_repo_id, '/') is None:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if check_folder_permission(request, dst_repo_id, '/') is None:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ result = {}
+ result['failed'] = []
+ result['success'] = []
+ username = request.user.username
+
+ for path_item in path_list:
+
+ src_path = path_item['src_path']
+ src_path = normalize_dir_path(src_path)
+ src_parent_dir = os.path.dirname(src_path.rstrip('/'))
+ src_parent_dir = normalize_dir_path(src_parent_dir)
+ src_obj_name = os.path.basename(src_path.rstrip('/'))
+
+ dst_path = path_item['dst_path']
+ dst_path = normalize_dir_path(dst_path)
+ dst_parent_dir = dst_path
+ dst_obj_name = src_obj_name
+
+ common_dict = {
+ 'src_repo_id': src_repo_id,
+ 'src_path': src_path,
+ 'dst_repo_id': dst_repo_id,
+ 'dst_path': dst_path,
+ }
+
+ # src/dst parameter check
+ if src_repo_id == dst_repo_id and \
+ dst_path.startswith(src_path):
+ error_dict = {
+ 'error_msg': "The destination directory is the same as the source, or is it's subfolder."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ if src_path == '/':
+ error_dict = {
+ 'error_msg': "The source path can not be '/'."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ if len(dst_parent_dir + dst_obj_name) > MAX_PATH:
+ error_dict = {
+ 'error_msg': "'Destination path is too long."
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # src resource check
+ ## as we don't know if `src_path` stands for a file or a folder,
+ ## so we check both
+ src_dir_id = seafile_api.get_dir_id_by_path(src_repo_id, src_path)
+ src_file_id = seafile_api.get_file_id_by_path(src_repo_id,
+ normalize_file_path(src_path))
+
+ if not src_dir_id and not src_file_id:
+ error_dict = {
+ 'error_msg': '%s not found.' % src_path
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # dst resource check
+ if not seafile_api.get_dir_id_by_path(dst_repo_id, dst_path):
+ error_dict = {
+ 'error_msg': 'Folder %s not found.' % dst_path
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # src path permission check, user must has `rw` permission for src folder.
+ if check_folder_permission(request, src_repo_id, src_parent_dir) != 'rw':
+ error_dict = {
+ 'error_msg': 'Permission denied.'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ # dst path permission check, user must has `rw` permission for dst folder.
+ if check_folder_permission(request, dst_repo_id, dst_path) != 'rw':
+ error_dict = {
+ 'error_msg': 'Permission denied.'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ try:
+ dst_obj_name = check_filename_with_rename(dst_repo_id,
+ dst_parent_dir, dst_obj_name)
+ # replace=False, username=username, need_progress=0, synchronous=1
+ seafile_api.move_file(src_repo_id, src_parent_dir, src_obj_name,
+ dst_repo_id, dst_parent_dir, dst_obj_name,
+ False, username, 0, 1)
+ except Exception as e:
+ logger.error(e)
+ error_dict = {
+ 'error_msg': 'Internal Server Error'
+ }
+ common_dict.update(error_dict)
+ result['failed'].append(common_dict)
+ continue
+
+ common_dict['dst_obj_name'] = dst_obj_name
+ result['success'].append(common_dict)
+
+ return Response(result)
diff --git a/seahub/api2/views.py b/seahub/api2/views.py
index 7be5021c14..38c1191958 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -44,7 +44,7 @@ from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
grp_avatar
from seahub.base.accounts import User
-from seahub.base.models import UserStarredFiles, DeviceToken
+from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey
from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission
from seahub.share.utils import is_repo_admin, check_group_share_in_permission
from seahub.base.templatetags.seahub_tags import email2nickname, \
@@ -74,7 +74,8 @@ from seahub.utils.file_revisions import get_file_revisions_after_renamed
from seahub.utils.devices import do_unlink_device
from seahub.utils.repo import get_repo_owner, get_library_storages, \
get_locked_files_by_dir, get_related_users_by_repo, \
- is_valid_repo_id_format, can_set_folder_perm_by_user
+ is_valid_repo_id_format, can_set_folder_perm_by_user, \
+ add_encrypted_repo_secret_key_to_database
from seahub.utils.star import star_file, unstar_file
from seahub.utils.file_types import DOCUMENT
from seahub.utils.file_size import get_file_size_unit
@@ -92,7 +93,8 @@ if HAS_OFFICE_CONVERTER:
import seahub.settings as settings
from seahub.settings import THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, \
FILE_LOCK_EXPIRATION_DAYS, ENABLE_STORAGE_CLASSES, \
- ENABLE_THUMBNAIL, ENABLE_FOLDER_PERM, STORAGE_CLASS_MAPPING_POLICY
+ ENABLE_THUMBNAIL, ENABLE_FOLDER_PERM, STORAGE_CLASS_MAPPING_POLICY, \
+ ENABLE_RESET_ENCRYPTED_REPO_PASSWORD
try:
from seahub.settings import CLOUD_MODE
except ImportError:
@@ -904,6 +906,9 @@ class Repos(APIView):
repo_id = seafile_api.create_repo(repo_name,
repo_desc, username, passwd)
+ if passwd and ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ add_encrypted_repo_secret_key_to_database(repo_id, passwd)
+
return repo_id, None
def _create_enc_repo(self, request, repo_id, repo_name, repo_desc, username, org_id):
@@ -1054,8 +1059,13 @@ class PubRepos(APIView):
def set_repo_password(request, repo, password):
assert password, 'password must not be none'
+ repo_id = repo.id
try:
- seafile_api.set_passwd(repo.id, request.user.username, password)
+ seafile_api.set_passwd(repo_id, request.user.username, password)
+
+ if ENABLE_RESET_ENCRYPTED_REPO_PASSWORD:
+ add_encrypted_repo_secret_key_to_database(repo_id, password)
+
except SearpcError, e:
if e.msg == 'Bad arguments':
return api_error(status.HTTP_400_BAD_REQUEST, e.msg)
@@ -1068,6 +1078,7 @@ def set_repo_password(request, repo, password):
else:
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, e.msg)
+
def check_set_repo_password(request, repo):
if not check_permission(repo.id, request.user.username):
return api_error(status.HTTP_403_FORBIDDEN,
@@ -2894,6 +2905,10 @@ class FileDetailView(APIView):
except UserStarredFiles.DoesNotExist:
entry["starred"] = False
+ entry["last_modifier_email"] = latest_contributor
+ entry["last_modifier_name"] = email2nickname(latest_contributor)
+ entry["last_modifier_contact_email"] = email2contact_email(latest_contributor)
+
return HttpResponse(json.dumps(entry), status=200,
content_type=json_content_type)
@@ -3245,10 +3260,6 @@ class DirView(APIView):
return api_error(HTTP_520_OPERATION_FAILED,
'Failed to make directory.')
else:
- if not is_seafile_pro():
- return api_error(status.HTTP_400_BAD_REQUEST,
- 'Feature not supported.')
-
if check_folder_permission(request, repo_id, '/') != 'rw':
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
@@ -4727,7 +4738,8 @@ class OrganizationView(APIView):
def post(self, request, format=None):
if not CLOUD_MODE or not MULTI_TENANCY:
- return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied')
+ error_msg = 'Feature is not enabled.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
username = request.POST.get('username', None)
password = request.POST.get('password', None)
diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py
index 42b41670de..7814c254eb 100644
--- a/seahub/base/context_processors.py
+++ b/seahub/base/context_processors.py
@@ -16,7 +16,7 @@ from django.utils.functional import lazy
from constance import config
from seahub.settings import SEAFILE_VERSION, SITE_TITLE, SITE_NAME, \
- MAX_FILE_NAME, LOGO_PATH, LOGO_WIDTH, LOGO_HEIGHT,\
+ MAX_FILE_NAME, LOGO_PATH, BRANDING_CSS, LOGO_WIDTH, LOGO_HEIGHT,\
SHOW_REPO_DOWNLOAD_BUTTON, SITE_ROOT, ENABLE_GUEST_INVITATION, \
FAVICON_PATH, ENABLE_THUMBNAIL, THUMBNAIL_SIZE_FOR_ORIGINAL, \
MEDIA_ROOT, SHOW_LOGOUT_ICON, CUSTOM_LOGO_PATH, CUSTOM_FAVICON_PATH
@@ -84,6 +84,7 @@ def base(request):
result = {
'seafile_version': SEAFILE_VERSION,
'site_title': config.SITE_TITLE,
+ 'branding_css': BRANDING_CSS,
'enable_branding_css': config.ENABLE_BRANDING_CSS,
'favicon_path': favicon_path,
'logo_path': logo_path,
diff --git a/seahub/base/models.py b/seahub/base/models.py
index 10ad6fa9aa..9f6dc8ce8e 100644
--- a/seahub/base/models.py
+++ b/seahub/base/models.py
@@ -315,3 +315,30 @@ class ClientLoginToken(models.Model):
def __unicode__(self):
return "/".join(self.username, self.token)
+
+
+class RepoSecretKeyManager(models.Manager):
+
+ def get_secret_key(self, repo_id):
+ try:
+ repo_secret_key = self.get(repo_id=repo_id)
+ except RepoSecretKey.DoesNotExist:
+ return None
+
+ return repo_secret_key.secret_key
+
+ def add_secret_key(self, repo_id, secret_key):
+
+ repo_secret_key = self.model(repo_id=repo_id, secret_key=secret_key)
+ repo_secret_key.save(using=self._db)
+
+ return repo_secret_key
+
+
+class RepoSecretKey(models.Model):
+ """
+ """
+ repo_id = models.CharField(unique=True, max_length=36, db_index=True)
+ secret_key = models.CharField(max_length=44)
+
+ objects = RepoSecretKeyManager()
diff --git a/seahub/oauth/views.py b/seahub/oauth/views.py
index 85b6bb84d7..ef196aef56 100644
--- a/seahub/oauth/views.py
+++ b/seahub/oauth/views.py
@@ -34,6 +34,7 @@ if ENABLE_OAUTH:
TOKEN_URL = getattr(settings, 'OAUTH_TOKEN_URL', '')
USER_INFO_URL = getattr(settings, 'OAUTH_USER_INFO_URL', '')
SCOPE = getattr(settings, 'OAUTH_SCOPE', '')
+ ACCESS_TOKEN_IN_URI = getattr(settings, 'OAUTH_ACCESS_TOKEN_IN_URI', False)
# Used for init an user for Seahub.
PROVIDER_DOMAIN = getattr(settings, 'OAUTH_PROVIDER_DOMAIN', '')
@@ -112,7 +113,7 @@ def oauth_callback(request):
redirect_uri=REDIRECT_URL)
try:
- session.fetch_token(TOKEN_URL, client_secret=CLIENT_SECRET,
+ token = session.fetch_token(TOKEN_URL, client_secret=CLIENT_SECRET,
authorization_response=request.get_full_path())
if session._client.__dict__['token'].has_key('user_id'):
@@ -121,7 +122,11 @@ def oauth_callback(request):
user_id = session._client.__dict__['token']['user_id']
user_info_resp = session.get(USER_INFO_URL + '?user_id=%s' % user_id)
else:
- user_info_resp = session.get(USER_INFO_URL)
+ user_info_url = USER_INFO_URL
+ if ACCESS_TOKEN_IN_URI:
+ code = request.GET.get('code')
+ user_info_url = USER_INFO_URL + '?access_token=%s&code=%s' % (token['access_token'], code)
+ user_info_resp = session.get(user_info_url)
except Exception as e:
logger.error(e)
@@ -130,7 +135,7 @@ def oauth_callback(request):
})
def format_user_info(user_info_resp):
-
+ logger.info('user info resp: %s' % user_info_resp.text)
error = False
user_info = {}
user_info_json = user_info_resp.json()
diff --git a/seahub/profile/templates/profile/set_profile.html b/seahub/profile/templates/profile/set_profile.html
index a4c64badec..f7b258dbb3 100644
--- a/seahub/profile/templates/profile/set_profile.html
+++ b/seahub/profile/templates/profile/set_profile.html
@@ -19,10 +19,6 @@
{% trans "Language" %}
- {% if user.permissions.can_add_repo %}
- {% trans "Default Library" %}
- {% endif %}
-
{% if two_factor_auth_enabled %}
{% trans "Two-Factor Authentication" %}
{% endif %}
@@ -64,12 +60,7 @@
{% endif %}
- {% if form.department and form.telephone %}
-
- {% for error in form.department.errors %}
- {{ error|escape }}
- {% endfor %}
-
+ {% if form.telephone %}
{% for error in form.telephone.errors %}
{{ error|escape }}
@@ -114,27 +105,6 @@
-{% if user.permissions.can_add_repo %}
-
-
{% trans "Default Library Setting" %}
- {% if default_repo %}
-
{% trans "Your default library:" %} {{default_repo.name}}.
- {% endif %}
-
{% trans "Default library is the default place to store your personal documents and pictures." %}
-
-
-
-{% endif %}
-
{% if two_factor_auth_enabled %}
{% trans "Two-Factor Authentication" %}
@@ -237,30 +207,6 @@ $('#account-delete-btn').on('click', function () {
});
});
-
-{% if user.permissions.can_add_repo %}
-var all_repos = [];
-{% for a_repo in owned_repos %}
- all_repos.push({
- 'text': HTMLescape('{{ a_repo.props.name|escapejs }}'),
- 'data': {'repo_id': '{{ a_repo.props.id }}', 'path': '/'}
- });
-{% endfor %}
-$('#choose-default-lib').on('click', function() {
- var $form = $('#default-lib-form');
- $form.modal({appendTo:'#main', autoResize:true, focus:false});
- $('#simplemodal-container').css({'width':'auto', 'height':'auto'});
- FileTree.renderDirTree($('#repos-dirs').data('site_root', '{{SITE_ROOT}}'), $form, all_repos);
-});
-$('#default-lib-form').on('submit', function() {
- var dst_repo = $('[name="dst_repo"]', $(this)).val();
- if (!$.trim(dst_repo)) {
- $('.error', $(this)).removeClass('hide');
- return false;
- }
-});
-{% endif %}
-
{% if ENABLE_ADDRESSBOOK_OPT_IN %}
$("#list-in-address-book input[type='checkbox']").on('change', function() {
var _this = $(this), data = {};
diff --git a/seahub/profile/templates/profile/user_profile.html b/seahub/profile/templates/profile/user_profile.html
index 91ec088141..ae7e68adc9 100644
--- a/seahub/profile/templates/profile/user_profile.html
+++ b/seahub/profile/templates/profile/user_profile.html
@@ -11,12 +11,6 @@
{{ nickname }}
{% if d_profile %}
- {% if d_profile.department %}
- -
-
- {{ d_profile.department }}
-
- {% endif %}
{% if d_profile.telephone %}
-
diff --git a/seahub/settings.py b/seahub/settings.py
index cdff74fc1c..9e645055cd 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -298,6 +298,9 @@ MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD = 1000
# enable encrypt library
ENABLE_ENCRYPTED_LIBRARY = True
+# enable reset encrypt library's password when user forget password
+ENABLE_RESET_ENCRYPTED_REPO_PASSWORD = False
+
# mininum length for password of encrypted library
REPO_PASSWORD_MIN_LENGTH = 8
@@ -492,7 +495,10 @@ LOGO_HEIGHT = 32
CUSTOM_LOGO_PATH = 'custom/mylogo.png'
CUSTOM_FAVICON_PATH = 'custom/favicon.ico'
-# Enable custom css to modify the seafile css
+# used before version 6.3: the relative path of css file under seahub-data (e.g. custom/custom.css)
+BRANDING_CSS = ''
+
+# used in 6.3+, enable setting custom css via admin web interface
ENABLE_BRANDING_CSS = False
# Using Django to server static file. Set to `False` if deployed behide a web
diff --git a/seahub/templates/base.html b/seahub/templates/base.html
index da56d84950..07c7568e05 100644
--- a/seahub/templates/base.html
+++ b/seahub/templates/base.html
@@ -16,6 +16,7 @@
{% block extra_style %}{% endblock %}
+{% if branding_css != '' %}{% endif %}
{% if enable_branding_css %}{% endif %}
diff --git a/seahub/templates/base_for_backbone.html b/seahub/templates/base_for_backbone.html
index 9afe1ac0ac..f0f4d4bffa 100644
--- a/seahub/templates/base_for_backbone.html
+++ b/seahub/templates/base_for_backbone.html
@@ -19,6 +19,7 @@
{% endcompress %}
{% block extra_style %}{% endblock %}
+{% if branding_css != '' %}{% endif %}
{% if enable_branding_css %}{% endif %}
diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html
index 974c2ac5dc..64d3314dd7 100644
--- a/seahub/templates/js/templates.html
+++ b/seahub/templates/js/templates.html
@@ -259,7 +259,7 @@
<% } %>
<% if (is_staff || is_repo_owner || is_admin) { %>
-
+
<% } %>
<% } else { %> {# It is an address book group #}
<% if (is_staff) { %>
@@ -277,7 +277,7 @@
<% } else { %>
-
+
<% } %>
<% } %>
<% } %>
@@ -286,7 +286,7 @@
<% } %>
<% if (is_staff || is_repo_owner) { %>
-
+
<% } %>
<% } %>
@@ -317,11 +317,12 @@
<% } %>
<% if (is_staff || is_repo_owner || is_admin) { %>
-
+
<% } %>
<% } else { %>
<% if (is_staff) { %>
+ <% if (owner == group_id + '@seafile_group') { %> {# this repo belongs to the current group #}
+ <% } else { %>
+
+ <% } %>
<% } %>
<% } %>
@@ -342,7 +346,7 @@
<% } %>
<% if (is_staff || is_repo_owner) { %>
-
+
<% } %>
<% } %>
diff --git a/seahub/templates/sysadmin/sys_org_admin.html b/seahub/templates/sysadmin/sys_org_admin.html
index e7f059cc39..df19755d01 100644
--- a/seahub/templates/sysadmin/sys_org_admin.html
+++ b/seahub/templates/sysadmin/sys_org_admin.html
@@ -115,9 +115,7 @@ $('#add-org-form').on('submit', function() {
'password2': pwd2
},
success: function(data) {
- if (data['success']) {
- location.reload(true);
- }
+ location.reload(true);
},
error: function(jqXHR, textStatus, errorThrown) {
if (jqXHR.responseText) {
@@ -125,9 +123,8 @@ $('#add-org-form').on('submit', function() {
} else {
apply_form_error(form_id, "{% trans "Failed. Please check the network." %}");
}
+ enable(submit_btn);
}
- }).complete(function() {
- enable(submit_btn);
});
return false;
diff --git a/seahub/templates/sysadmin/sys_useradmin.html b/seahub/templates/sysadmin/sys_useradmin.html
index 60be0cf814..c4b4912365 100644
--- a/seahub/templates/sysadmin/sys_useradmin.html
+++ b/seahub/templates/sysadmin/sys_useradmin.html
@@ -45,8 +45,6 @@
-
-
{% if is_pro %}
@@ -138,7 +136,6 @@ $('#add-user-form').on('submit', function() {
form_id = $(this).attr('id'),
email = $.trim(form.children('[name="email"]').val()),
name = $.trim($('[name="name"]', form).val()),
- department = $.trim($('[name="department"]', form).val()),
{% if is_pro %}
role = $('select[name="role"]', form).val(),
{% endif %}
@@ -173,7 +170,6 @@ $('#add-user-form').on('submit', function() {
data: {
'email': email,
'name': name,
- 'department': department,
{% if is_pro %}
'role': role,
{% endif %}
diff --git a/seahub/templates/sysadmin/sysadmin_backbone.html b/seahub/templates/sysadmin/sysadmin_backbone.html
index 69ea63e418..1912e336be 100644
--- a/seahub/templates/sysadmin/sysadmin_backbone.html
+++ b/seahub/templates/sysadmin/sysadmin_backbone.html
@@ -18,6 +18,7 @@
{% endcompress %}
+{% if branding_css != '' %}{% endif %}
{% if enable_branding_css %}{% endif %}
diff --git a/seahub/templates/sysadmin/userinfo.html b/seahub/templates/sysadmin/userinfo.html
index 334a985afc..786943dfb3 100644
--- a/seahub/templates/sysadmin/userinfo.html
+++ b/seahub/templates/sysadmin/userinfo.html
@@ -88,18 +88,6 @@
- {% trans "Department" %}
-
-
- {% if d_profile and d_profile.department %}
- {{ d_profile.department }}
- {% else %}
- --
- {% endif %}
-
-
-
-
{% if d_profile and d_profile.telephone %}
{% trans "Telephone" %}
{{ d_profile.telephone }}
@@ -572,43 +560,6 @@ $('#set-reference-id-form').on('submit', function() {
return false;
});
-$('#set-dept-form').on('submit', function() {
- var department = $.trim($('[name="department"]', $(this)).val());
- var $department = $('#department');
- var $error = $('.error', $(this));
- var $submitBtn = $('[type="submit"]', $(this));
- disable($submitBtn);
-
- $.ajax({
- url: '{% url 'api-v2.1-admin-user' email %}',
- type: 'PUT',
- dataType: 'json',
- cache: false,
- beforeSend: prepareCSRFToken,
- data: {'department': department},
- success: function(data) {
- if (department == '') {
- $department.html('--');
- } else {
- $department.html(HTMLescape(data['department']));
- }
- $.modal.close();
- },
- error: function(xhr, textStatus, errorThrown) {
- var err_msg;
- if (xhr.responseText) {
- err_msg = JSON.parse(xhr.responseText).error_msg;
- } else {
- err_msg = "{% trans "Failed. Please check the network." %}";
- }
- $error.html(err_msg).show();
- enable($submitBtn);
- }
- });
-
- return false;
-});
-
$('#set-quota-form').on('submit', function() {
var form = $(this),
form_id = form.attr('id'),
diff --git a/seahub/templates/view_file_onlyoffice.html b/seahub/templates/view_file_onlyoffice.html
index 4d42e3f5ec..0aafb5875d 100644
--- a/seahub/templates/view_file_onlyoffice.html
+++ b/seahub/templates/view_file_onlyoffice.html
@@ -1,4 +1,4 @@
-{% load seahub_tags i18n %}
+{% load seahub_tags i18n staticfiles %}
@@ -15,33 +15,61 @@ html, body { padding:0; margin:0; height:100%; }
{% get_current_language as LANGUAGE_CODE %}
+
+
+