diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py new file mode 100644 index 0000000000..55e6ab944b --- /dev/null +++ b/seahub/api2/endpoints/repos_batch.py @@ -0,0 +1,270 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging + +from pysearpc import SearpcError +from rest_framework import status +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +import seaserv +from seaserv import seafile_api, ccnet_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.base.accounts import User +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) + +logger = logging.getLogger(__name__) + +class ReposBatchView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get_repo_shared_to_users(self, request, repo_id): + username = request.user.username + + if is_org_context(request): + org_id = request.user.org.org_id + share_items = seafile_api.list_org_repo_shared_to(org_id, username, repo_id) + else: + share_items = seafile_api.list_repo_shared_to(username, repo_id) + + ret = [] + for item in share_items: + ret.append(item.user) + + return ret + + def has_shared_to_user(self, request, repo_id, username): + users = self.get_repo_shared_to_users(request, repo_id) + + has_shared = False + if username in users: + has_shared = True + + return has_shared + + def get_repo_shared_to_groups(self, request, repo_id): + username = request.user.username + if is_org_context(request): + org_id = request.user.org.org_id + share_items = seafile_api.list_org_repo_shared_group(org_id, + username, repo_id) + else: + share_items = seafile_api.list_repo_shared_group_by_user( + username, repo_id) + + ret = [] + for item in share_items: + ret.append(item.group_id) + + return ret + + def has_shared_to_group(self, request, repo_id, group_id): + group_ids = self.get_repo_shared_to_groups(request, repo_id) + + has_shared = False + if group_id in group_ids: + has_shared = True + + return has_shared + + 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'): + error_msg = 'operation invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + result = {} + result['failed'] = [] + result['success'] = [] + + username = request.user.username + repo_id_list = request.data.getlist('repo_id') + valid_repo_id_list = [] + + # filter out invalid repo id + for repo_id in repo_id_list: + + if not seafile_api.get_repo(repo_id): + result['failed'].append({ + 'repo_id': repo_id, + 'error_msg': 'Library %s not found.' % repo_id + }) + 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 + + 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': + error_msg = 'share_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + permission = request.data.get('permission', 'rw') + if permission not in ('r', 'rw'): + error_msg = 'permission invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # share repo to 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) + + try: + User.objects.get(email=to_username) + except User.DoesNotExist: + error_msg = 'User %s not found.' % to_username + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # check if to_user is an org user + try: + org_of_to_user = ccnet_api.get_orgs_by_user(to_username) + except Exception as e: + logger.debug(e) + org_of_to_user = [] + + if is_org_context(request): + org_id = request.user.org.org_id + org_name = request.user.org.org_name + if len(org_of_to_user) == 0 or org_id != org_of_to_user[0].org_id: + error_msg = 'User %s is not member of organization %s.' \ + % (to_username, org_name) + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + else: + if len(org_of_to_user) >= 1: + error_msg = 'User %s is member of organization %s.' \ + % (to_username, org_of_to_user[0].org_name) + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + for repo_id in valid_repo_id_list: + if self.has_shared_to_user(request, repo_id, to_username): + result['failed'].append({ + 'repo_id': repo_id, + 'error_msg': 'This item has been shared to %s.' % to_username + }) + continue + + try: + if is_org_context(request): + org_id = request.user.org.org_id + seaserv.seafserv_threaded_rpc.org_add_share(org_id, + repo_id, username, to_username, permission) + else: + seafile_api.share_repo( + repo_id, username, to_username, permission) + + # send a signal when sharing repo successful + repo = seafile_api.get_repo(repo_id) + share_repo_to_user_successful.send(sender=None, + from_user=username, to_user=to_username, repo=repo) + + result['success'].append({ + "repo_id": repo_id, + "username": to_username, + "permission": permission + }) + + send_perm_audit_msg('add-repo-perm', username, to_username, + repo_id, '/', permission) + except Exception as e: + logger.error(e) + result['failed'].append({ + 'repo_id': repo_id, + 'error_msg': 'Internal Server Error' + }) + + # share repo to 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) + if not group: + error_msg = 'Group %s not found.' % to_group_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + group_name = group.group_name + if not ccnet_api.is_group_user(to_group_id, username): + error_msg = 'User %s is not member of group %s.' % (username, group_name) + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + for repo_id in valid_repo_id_list: + if self.has_shared_to_group(request, repo_id, to_group_id): + result['failed'].append({ + 'repo_id': repo_id, + 'error_msg': 'This item has been shared to %s.' % group_name + }) + continue + + try: + if is_org_context(request): + org_id = request.user.org.org_id + seafile_api.add_org_group_repo( + repo_id, org_id, to_group_id, username, permission) + else: + seafile_api.set_group_repo( + repo_id, to_group_id, username, permission) + + # send a signal when sharing repo successful + repo = seafile_api.get_repo(repo_id) + share_repo_to_group_successful.send(sender=None, + from_user=username, group_id=to_group_id, repo=repo) + + result['success'].append({ + "repo_id": repo_id, + "group_id": to_group_id, + "group_name": group_name, + "permission": permission + }) + + send_perm_audit_msg('add-repo-perm', username, to_group_id, + repo_id, '/', permission) + + except SearpcError as e: + logger.error(e) + result['failed'].append({ + 'repo_id': repo_id, + 'error_msg': 'Internal Server Error' + }) + + return Response(result) diff --git a/seahub/api2/endpoints/user_avatar.py b/seahub/api2/endpoints/user_avatar.py new file mode 100644 index 0000000000..61092df02f --- /dev/null +++ b/seahub/api2/endpoints/user_avatar.py @@ -0,0 +1,64 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import os +import logging + +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 django.template.defaultfilters import filesizeformat + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error + +from seahub.avatar.models import Avatar +from seahub.avatar.signals import avatar_updated +from seahub.avatar.settings import (AVATAR_MAX_AVATARS_PER_USER, + AVATAR_MAX_SIZE, AVATAR_ALLOWED_FILE_EXTS) + +logger = logging.getLogger(__name__) + +class UserAvatarView(APIView): + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request): + + image_file = request.FILES.get('avatar', None) + if not image_file: + error_msg = 'avatar invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + (root, ext) = os.path.splitext(image_file.name.lower()) + if AVATAR_ALLOWED_FILE_EXTS and ext not in AVATAR_ALLOWED_FILE_EXTS: + error_msg = _(u"%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") % {'ext' : ext, 'valid_exts_list' : ", ".join(AVATAR_ALLOWED_FILE_EXTS)} + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if image_file.size > AVATAR_MAX_SIZE: + error_msg = _(u"Your file is too big (%(size)s), the maximum allowed size is %(max_valid_size)s") % { 'size' : filesizeformat(image_file.size), 'max_valid_size' : filesizeformat(AVATAR_MAX_SIZE)} + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + username = request.user.username + count = Avatar.objects.filter(emailuser=username).count() + if AVATAR_MAX_AVATARS_PER_USER > 1 and count >= AVATAR_MAX_AVATARS_PER_USER: + error_msg = _(u"You already have %(nb_avatars)d avatars, and the maximum allowed is %(nb_max_avatars)d.") % { 'nb_avatars' : count, 'nb_max_avatars' : AVATAR_MAX_AVATARS_PER_USER} + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + avatar = Avatar( + emailuser = username, + primary = True, + ) + avatar.avatar.save(image_file.name, image_file) + avatar.save() + avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) + 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/urls.py b/seahub/urls.py index 0fdf013d6a..c5236debbf 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -25,6 +25,7 @@ from seahub.api2.endpoints.share_links import ShareLinks, ShareLink from seahub.api2.endpoints.shared_folders import SharedFolders from seahub.api2.endpoints.shared_repos import SharedRepos, SharedRepo from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink +from seahub.api2.endpoints.repos_batch import ReposBatchView from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.dir import DirView from seahub.api2.endpoints.repo_trash import RepoTrash @@ -40,7 +41,7 @@ from seahub.api2.endpoints.invitation import InvitationView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView - +from seahub.api2.endpoints.user_avatar import UserAvatarView from seahub.api2.endpoints.admin.login import Login from seahub.api2.endpoints.admin.file_audit import FileAudit from seahub.api2.endpoints.admin.file_update import FileUpdate @@ -196,6 +197,7 @@ urlpatterns = patterns( url(r'^api/v2.1/upload-links/(?P[a-f0-9]+)/$', UploadLink.as_view(), name='api-v2.1-upload-link'), ## user::repos + url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/file/$', FileView.as_view(), name='api-v2.1-file-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'), @@ -218,6 +220,9 @@ urlpatterns = patterns( url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()), + ## user::avatar + url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'), + ## admin::sysinfo url(r'^api/v2.1/admin/sysinfo/$', SysInfo.as_view(), name='api-v2.1-sysinfo'), diff --git a/tests/api/endpoints/test_repos_batch.py b/tests/api/endpoints/test_repos_batch.py new file mode 100644 index 0000000000..f4d76ff0c6 --- /dev/null +++ b/tests/api/endpoints/test_repos_batch.py @@ -0,0 +1,195 @@ +import json +from seaserv import seafile_api, ccnet_api +from django.core.urlresolvers import reverse +from tests.common.utils import randstring +from seahub.test_utils import BaseTestCase + +class ReposBatchViewTest(BaseTestCase): + + def create_new_repo(self, username): + new_repo_id = seafile_api.create_repo(name=randstring(10), + desc='', username=username, passwd=None) + + return new_repo_id + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + + self.repo_id = self.repo.id + self.group_id = self.group.id + + self.url = reverse('api-v2.1-repos-batch') + + def tearDown(self): + self.remove_repo() + self.remove_group() + + def test_can_share_repos_to_user(self): + tmp_repo_id = self.create_new_repo(self.user_name) + + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'user', + 'username': self.admin_name, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 1 + assert len(json_resp['failed']) == 0 + + # share repo again will failed + data = { + 'operation': 'share', + 'share_type': 'user', + 'username': self.admin_name, + 'repo_id': [self.repo_id, tmp_repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 1 + assert len(json_resp['failed']) == 1 + assert self.repo_id in json_resp['failed'][0]['repo_id'] + + self.remove_repo(tmp_repo_id) + + def test_can_share_repos_to_group(self): + tmp_repo_id = self.create_new_repo(self.user_name) + + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'group', + 'group_id': self.group_id, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 1 + assert len(json_resp['failed']) == 0 + + # share repo again will failed + data = { + 'operation': 'share', + 'share_type': 'group', + 'group_id': self.group_id, + 'repo_id': [self.repo_id, tmp_repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 1 + assert len(json_resp['failed']) == 1 + assert self.repo_id in json_resp['failed'][0]['repo_id'] + + self.remove_repo(tmp_repo_id) + + def test_share_with_invalid_operation(self): + self.login_as(self.user) + + data = { + 'operation': 'invalid_operation', + 'share_type': 'user', + 'username': self.admin_name, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + data = { + 'operation': 'invalid_operation', + 'share_type': 'group', + 'group_id': self.group_id, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + def test_share_with_invalid_share_type(self): + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'invalid_share_type', + 'username': self.admin_name, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + data = { + 'operation': 'share', + 'share_type': 'invalid_share_type', + 'group_id': self.group_id, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + def test_share_with_invalid_permisson(self): + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'user', + 'permission': 'invalid_permission', + 'username': self.admin_name, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + data = { + 'operation': 'share', + 'share_type': 'group', + 'permission': 'invalid_permission', + 'group_id': self.group_id, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(400, resp.status_code) + + def test_share_with_invalid_user(self): + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'user', + 'username': 'invalid@user.com', + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(404, resp.status_code) + + def test_share_with_not_exist_group(self): + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'group', + 'group_id': -1, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(404, resp.status_code) + + def test_share_with_not_group_member(self): + tmp_group_id = ccnet_api.create_group(randstring(10), self.admin_name) + + self.login_as(self.user) + + data = { + 'operation': 'share', + 'share_type': 'group', + 'group_id': tmp_group_id, + 'repo_id': [self.repo_id] + } + resp = self.client.post(self.url, data) + self.assertEqual(403, resp.status_code) diff --git a/tests/api/endpoints/test_user_avatar.py b/tests/api/endpoints/test_user_avatar.py new file mode 100644 index 0000000000..1535a5bd09 --- /dev/null +++ b/tests/api/endpoints/test_user_avatar.py @@ -0,0 +1,26 @@ +import os +from tests.api.apitestbase import ApiTestBase +from tests.api.urls import AVATAR_BASE_URL +from tests.common.utils import urljoin +from tests.common.common import BASE_URL +from django.core.urlresolvers import reverse + +class AvatarApiTest(ApiTestBase): + + def test_create_user_avatar(self): + + # update user avatar + avatar_url = reverse('api-v2.1-user-avatar') + avatar_url = urljoin(BASE_URL, avatar_url) + avatar_file = os.path.join(os.getcwd(), 'media/img/seafile-logo.png') + + with open(avatar_file) as f: + json_resp = self.post(avatar_url, files={'avatar': f}).json() + + assert json_resp['success'] == True + + # assert is NOT default avatar + avatar_url = urljoin(AVATAR_BASE_URL, 'user', self.username, '/resized/80/') + info = self.get(avatar_url).json() + assert 'resized' in info['url'] + assert info['is_default'] == False