diff --git a/frontend/src/components/dialog/share-dialog.js b/frontend/src/components/dialog/share-dialog.js index 627ad2b3a4..0c8eb16b7b 100644 --- a/frontend/src/components/dialog/share-dialog.js +++ b/frontend/src/components/dialog/share-dialog.js @@ -1,9 +1,10 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; -import { gettext, username, canGenerateShareLink, canGenerateUploadLink } from '../../utils/constants'; +import { gettext, username, canGenerateShareLink, canGenerateUploadLink, canInvitePeople } from '../../utils/constants'; import ShareToUser from './share-to-user'; import ShareToGroup from './share-to-group'; +import ShareToInvitePeople from './share-to-invite-people'; import GenerateShareLink from './generate-share-link'; import GenerateUploadLink from './generate-upload-link'; import InternalLink from './internal-link'; @@ -117,6 +118,13 @@ class ShareDialog extends React.Component { {gettext('Share to group')} + {canInvitePeople && + + + {gettext('Invite People')} + + + } } @@ -154,6 +162,11 @@ class ShareDialog extends React.Component { + {canInvitePeople && + + + + } diff --git a/frontend/src/components/dialog/share-to-invite-people.js b/frontend/src/components/dialog/share-to-invite-people.js new file mode 100644 index 0000000000..b5b300b9a5 --- /dev/null +++ b/frontend/src/components/dialog/share-to-invite-people.js @@ -0,0 +1,318 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { gettext, siteRoot } from '../../utils/constants'; +import moment from 'moment'; +import { Button, Input } from 'reactstrap'; +import { seafileAPI } from '../../utils/seafile-api.js'; +import { Utils } from '../../utils/utils'; +import toaster from '../toast'; +import Loading from '../loading'; +import SharePermissionEditor from '../select-editor/share-permission-editor'; +import '../../css/invitations.css'; + +class UserItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isOperationShow: false + }; + } + + onMouseEnter = () => { + this.setState({ isOperationShow: true }); + } + + onMouseLeave = () => { + this.setState({ isOperationShow: false }); + } + + deleteShareItem = () => { + let item = this.props.item; + this.props.deleteShareItem(item.token); + } + + onChangeUserPermission = (permission) => { + let item = this.props.item; + this.props.onChangeUserPermission(item.token, permission); + } + + render() { + let item = this.props.item; + let currentPermission = item.is_admin ? 'admin' : item.permission; + return ( + + {item.accepter} + + + + {moment(item.expire_time).format('YYYY-MM-DD')} + {item.inviter_name} + + + + + + ); + } +} + +class UserList extends React.Component { + + render() { + let items = this.props.items; + return ( + + {items.map((item, index) => { + return ( + + ); + })} + + ); + } +} + +const propTypes = { + itemPath: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired, +}; + +class ShareToInvitePeople extends React.Component { + + constructor(props) { + super(props); + this.state = { + errorMsg: '', + permission: 'r', + sharedItems: [], + emails: '', + isSubmitting: false, + }; + this.permissions = ['rw', 'r']; + } + + handleInputChange = (e) => { + let emails = e.target.value; + this.setState({ + emails: emails, + }); + if (this.state.errorMsg) { + this.setState({ + errorMsg: '', + }); + } + } + + handleKeyDown = (e) => { + if (e.keyCode === 13) { + e.preventDefault(); + this.shareAndInvite(); + } + } + + componentDidMount() { + const path = this.props.itemPath; + const repoID = this.props.repoID; + seafileAPI.listRepoShareInvitations(repoID, path).then((res) => { + if (res.data.length !== 0) { + this.setState({ sharedItems: res.data.repo_share_invitation_list }); + } + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + setPermission = (permission) => { + this.setState({ permission: permission }); + } + + onInvitePeople = (successArray) => { + successArray.push.apply(successArray, this.state.sharedItems); + this.setState({ + sharedItems: successArray, + }); + } + + shareAndInvite = () => { + let emails = this.state.emails.trim(); + if (!emails) { + this.setState({ errorMsg: gettext('It is required.') }); + return false; + } + + let emailsArray = []; + emails = emails.split(','); + for (let i = 0, len = emails.length; i < len; i++) { + let email = emails[i].trim(); + if (email) { + emailsArray.push(email); + } + } + + if (!emailsArray.length) { + this.setState({ errorMsg: gettext('Email is invalid.') }); + return false; + } + + this.setState({ isSubmitting: true }); + + const path = this.props.itemPath; + const repoID = this.props.repoID; + const permission = this.state.permission; + + seafileAPI.addRepoShareInvitations(repoID, path, emailsArray, permission).then((res) => { + const success = res.data.success; + if (success.length) { + let successMsg = ''; + if (success.length == 1) { + successMsg = gettext('Successfully invited %(email).') + .replace('%(email)', success[0].accepter); + } else { + successMsg = gettext('Successfully invited %(email) and %(num) other people.') + .replace('%(email)', success[0].accepter) + .replace('%(num)', success.length - 1); + } + toaster.success(successMsg); + this.onInvitePeople(success); + } + const failed = res.data.failed; + if (failed.length) { + for (let i = 0, len = failed.length; i < len; i++) { + let failedMsg = failed[i].email + ': ' + failed[i].error_msg; + toaster.danger(failedMsg); + } + } + this.setState({ isSubmitting: false }); + }).catch((error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ isSubmitting: false }); + }); + } + + deleteShareItem = (token) => { + const path = this.props.itemPath; + const repoID = this.props.repoID; + + seafileAPI.deleteRepoShareInvitation(repoID, path, token).then(res => { + this.setState({ + sharedItems: this.state.sharedItems.filter(item => { return item.token !== token; }) + }); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + onChangeUserPermission = (token, permission) => { + const path = this.props.itemPath; + const repoID = this.props.repoID; + + seafileAPI.updateRepoShareInvitation(repoID, path, token, permission).then(() => { + this.updateSharedItems(token, permission); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + updateSharedItems = (token, permission) => { + let sharedItems = this.state.sharedItems.map(sharedItem => { + if (sharedItem.token === token) { + sharedItem.permission = permission; + } + return sharedItem; + }); + this.setState({ sharedItems: sharedItems }); + } + + render() { + let { sharedItems, isSubmitting } = this.state; + return ( + + + + + + + + + + + + + + + + {this.state.errorMsg.length > 0 && + + + + } + +
{gettext('Invite People')}{gettext('Permission')}{''}
+ + + + + +

{this.state.errorMsg}

+
+ + + + + + + + + + + +
{gettext('Email')}{gettext('Permission')}{gettext('Expiration')}{gettext('Inviter')}{''}
+
+
+ ); + } +} + +ShareToInvitePeople.propTypes = propTypes; + +export default ShareToInvitePeople; diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js index b019e67f2d..7dff6d1562 100644 --- a/frontend/src/components/dialog/share-to-user.js +++ b/frontend/src/components/dialog/share-to-user.js @@ -1,6 +1,6 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import {gettext, isPro, canInvitePeople, siteRoot} from '../../utils/constants'; +import {gettext, isPro, siteRoot} from '../../utils/constants'; import { Button } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api.js'; import { Utils } from '../../utils/utils'; @@ -339,12 +339,6 @@ class ShareToUser extends React.Component { onChangeUserPermission={this.onChangeUserPermission} /> - {canInvitePeople && - - - {gettext('Invite People')} - - } ); diff --git a/requirements.txt b/requirements.txt index b7e441fa60..2b6e6eb034 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.11.23 +Django==1.11.25 future captcha django-compressor diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py index a3422bd62b..8354d1db49 100644 --- a/seahub/api2/endpoints/invitations.py +++ b/seahub/api2/endpoints/invitations.py @@ -16,6 +16,7 @@ from seahub.base.accounts import User from seahub.utils import is_valid_email from seahub.invitations.models import Invitation from seahub.invitations.utils import block_accepter +from seahub.constants import GUEST_USER json_content_type = 'application/json; charset=utf-8' @@ -38,7 +39,7 @@ class InvitationsView(APIView): def post(self, request, format=None): # Send invitation. itype = request.data.get('type', '').lower() - if not itype or itype != 'guest': + if not itype or itype != GUEST_USER: return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.') accepter = request.data.get('accepter', '').lower() @@ -139,13 +140,13 @@ class InvitationsBatchView(APIView): i = Invitation.objects.add(inviter=request.user.username, accepter=accepter) + result['success'].append(i.to_dict()) + m = i.send_to(email=accepter) - if m.status == STATUS.sent: - result['success'].append(i.to_dict()) - else: + if m.status != STATUS.sent: result['failed'].append({ 'email': accepter, - 'error_msg': _('Internal Server Error'), + 'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'), }) return Response(result) diff --git a/seahub/api2/endpoints/repo_share_invitation.py b/seahub/api2/endpoints/repo_share_invitation.py new file mode 100644 index 0000000000..178f2d4f23 --- /dev/null +++ b/seahub/api2/endpoints/repo_share_invitation.py @@ -0,0 +1,134 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +import logging + +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 +from django.utils.translation import ugettext as _ + +from seaserv import seafile_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.permissions import CanInviteGuest +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.invitations.models import Invitation, RepoShareInvitation +from post_office.models import STATUS +from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE +from seahub.share.utils import is_repo_admin +from seahub.utils import is_org_context + +logger = logging.getLogger(__name__) +json_content_type = 'application/json; charset=utf-8' + + +class RepoShareInvitationView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle, ) + + def put(self, request, repo_id, format=None): + """ Update permission in repo share invitation. + """ + # argument check + path = request.data.get('path', None) + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.') + + token = request.data.get('token', None) + if not token: + return api_error(status.HTTP_400_BAD_REQUEST, 'token invalid.') + + permission = request.data.get('permission', PERMISSION_READ) + if permission not in (PERMISSION_READ, PERMISSION_READ_WRITE): + return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.') + + # recourse check + repo = seafile_api.get_repo(repo_id) + if not repo: + return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) + + if seafile_api.get_dir_id_by_path(repo.id, path) is None: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) + + # permission check + username = request.user.username + 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 username != repo_owner and not is_repo_admin(username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # mian + try: + shared_obj = RepoShareInvitation.objects.get_by_token_and_path( + token=token, repo_id=repo_id, path=path + ) + if not shared_obj: + error_msg = 'repo share invitation not found.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + if shared_obj.permission == permission: + error_msg = 'repo share invitation already has %s premission.' % permission + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + shared_obj.permission = permission + shared_obj.save() + 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}) + + def delete(self, request, repo_id, format=None): + """ Delete repo share invitation. + """ + # argument check + path = request.data.get('path', None) + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.') + + token = request.data.get('token', None) + if not token: + return api_error(status.HTTP_400_BAD_REQUEST, 'token invalid.') + + # recourse check + repo = seafile_api.get_repo(repo_id) + if not repo: + return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) + + if seafile_api.get_dir_id_by_path(repo.id, path) is None: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) + + # permission check + username = request.user.username + 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 username != repo_owner and not is_repo_admin(username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # mian + try: + shared_obj = RepoShareInvitation.objects.get_by_token_and_path( + token=token, repo_id=repo_id, path=path + ) + if not shared_obj: + error_msg = 'repo share invitation not found.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + shared_obj.delete() + 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/repo_share_invitations.py b/seahub/api2/endpoints/repo_share_invitations.py new file mode 100644 index 0000000000..90134bbbba --- /dev/null +++ b/seahub/api2/endpoints/repo_share_invitations.py @@ -0,0 +1,213 @@ +# Copyright (c) 2012-2019 Seafile Ltd. + +import logging + +from django.utils.translation import ugettext as _ +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 +from post_office.models import STATUS + +from seaserv import seafile_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.permissions import CanInviteGuest +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.base.accounts import User +from seahub.utils import is_valid_email +from seahub.invitations.models import Invitation, RepoShareInvitation +from seahub.invitations.utils import block_accepter +from seahub.utils.timeutils import datetime_to_isoformat_timestr +from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, GUEST_USER +from seahub.share.utils import is_repo_admin +from seahub.utils import is_org_context +from seahub.base.templatetags.seahub_tags import email2nickname + +json_content_type = 'application/json; charset=utf-8' +logger = logging.getLogger(__name__) + +class RepoShareInvitationsView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle,) + + def get(self, request, repo_id, format=None): + """ List repo share invitations. + """ + # argument check + path = request.GET.get('path', None) + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.') + + # recourse 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 seafile_api.get_dir_id_by_path(repo.id, path) is None: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) + + # permission check + username = request.user.username + 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 username != repo_owner and not is_repo_admin(username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # main + shared_list = list() + try: + shared_queryset = RepoShareInvitation.objects.list_by_repo_id_and_path(repo_id, path) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + for obj in shared_queryset: + data = obj.invitation.to_dict() + data['permission'] = obj.permission + data['inviter_name'] = email2nickname(obj.invitation.inviter) + + shared_list.append(data) + + return Response({'repo_share_invitation_list': shared_list}) + + +class RepoShareInvitationsBatchView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle,) + + def post(self, request, repo_id, format=None): + """ Batch add repo share invitations. + """ + # argument check + path = request.data.get('path', None) + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid.') + + itype = request.data.get('type', '').lower() + if not itype or itype != GUEST_USER: + return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.') + + accepters = request.data.get('accepters', None) + if not accepters or not isinstance(accepters, list): + return api_error(status.HTTP_400_BAD_REQUEST, 'accepters invalid.') + + permission = request.data.get('permission', PERMISSION_READ) + if permission not in (PERMISSION_READ, PERMISSION_READ_WRITE): + return api_error(status.HTTP_400_BAD_REQUEST, 'permission invalid.') + + # recourse check + repo = seafile_api.get_repo(repo_id) + if not repo: + return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id) + + if seafile_api.get_dir_id_by_path(repo.id, path) is None: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found.' % path) + + # permission check + username = request.user.username + 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 username != repo_owner and not is_repo_admin(username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # main + result = {} + result['failed'] = [] + result['success'] = [] + inviter_name = email2nickname(request.user.username) + + try: + invitation_queryset = Invitation.objects.order_by('-invite_time').filter( + inviter=request.user.username, accept_time=None) + shared_queryset = RepoShareInvitation.objects.list_by_repo_id_and_path( + repo_id=repo_id, path=path) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + for accepter in accepters: + + if not accepter.strip(): + continue + + accepter = accepter.lower() + + if not is_valid_email(accepter): + result['failed'].append({ + 'email': accepter, + 'error_msg': _('Email %s invalid.') % accepter + }) + continue + + if block_accepter(accepter): + result['failed'].append({ + 'email': accepter, + 'error_msg': _('The email address is not allowed to be invited as a guest.') + }) + continue + + try: + user = User.objects.get(accepter) + # user is active return exist + if user.is_active is True: + result['failed'].append({ + 'email': accepter, + 'error_msg': _('User %s already exists.') % accepter + }) + continue + except User.DoesNotExist: + pass + + if invitation_queryset.filter(accepter=accepter).exists(): + invitation = invitation_queryset.filter(accepter=accepter)[0] + else: + invitation = Invitation.objects.add( + inviter=request.user.username, accepter=accepter) + + if shared_queryset.filter(invitation=invitation).exists(): + result['failed'].append({ + 'email': accepter, + 'error_msg': _('This item has been shared to %s.') % accepter + }) + continue + + try: + RepoShareInvitation.objects.add( + invitation=invitation, repo_id=repo_id, path=path, permission=permission) + except Exception as e: + logger.error(e) + result['failed'].append({ + 'email': accepter, + 'error_msg': _('Internal Server Error'), + }) + + data = invitation.to_dict() + data['permission'] = permission + data['inviter_name'] = inviter_name + + result['success'].append(data) + + m = invitation.send_to(email=accepter) + if m.status != STATUS.sent: + result['failed'].append({ + 'email': accepter, + 'error_msg': _('Failed to send email, email service is not properly configured, please contact administrator.'), + }) + + return Response(result) diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py index e60365614d..33caf20060 100644 --- a/seahub/invitations/models.py +++ b/seahub/invitations/models.py @@ -11,6 +11,7 @@ from seahub.invitations.settings import INVITATIONS_TOKEN_AGE from seahub.utils import gen_token, get_site_name from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.utils.mail import send_html_email_with_dj_template, MAIL_PRIORITY +from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE GUEST = 'Guest' @@ -102,3 +103,52 @@ class Invitation(models.Model): subject=subject, priority=MAIL_PRIORITY.now ) + + +class RepoShareInvitationManager(models.Manager): + def add(self, invitation, repo_id, path, permission): + obj = self.model( + invitation=invitation, + repo_id=repo_id, + path=path, + permission=permission, + ) + obj.save() + return obj + + def list_by_repo_id_and_path(self, repo_id, path): + return self.select_related('invitation').filter( + invitation__expire_time__gte=timezone.now(), + invitation__accept_time=None, + repo_id=repo_id, + path=path, + ) + + def get_by_token_and_path(self, token, repo_id, path): + qs = self.select_related('invitation').filter( + invitation__token=token, repo_id=repo_id, path=path, + ) + if qs.exists(): + return qs[0] + else: + return None + + def list_by_invitation(self, invitation): + return self.select_related('invitation').filter(invitation=invitation) + +class RepoShareInvitation(models.Model): + PERMISSION_CHOICES = ( + (PERMISSION_READ, 'read only'), + (PERMISSION_READ_WRITE, 'read and write') + ) + + invitation = models.ForeignKey(Invitation, on_delete=models.CASCADE, related_name='repo_share') + repo_id = models.CharField(max_length=36, db_index=True) + path = models.TextField() + permission = models.CharField( + max_length=50, choices=PERMISSION_CHOICES, default=PERMISSION_READ) + + objects = RepoShareInvitationManager() + + class Meta: + db_table = 'repo_share_invitation' diff --git a/seahub/invitations/views.py b/seahub/invitations/views.py index 8283ed526b..91f1389298 100644 --- a/seahub/invitations/views.py +++ b/seahub/invitations/views.py @@ -1,18 +1,26 @@ # Copyright (c) 2012-2016 Seafile Ltd. +import logging + from django.contrib import messages from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404, render from django.utils.translation import ugettext as _ +from seaserv import seafile_api from seahub.auth import login as auth_login, authenticate from seahub.auth import get_backends from seahub.base.accounts import User from seahub.constants import GUEST_USER -from seahub.invitations.models import Invitation +from seahub.invitations.models import Invitation, RepoShareInvitation from seahub.invitations.signals import accept_guest_invitation_successful from seahub.settings import SITE_ROOT, NOTIFY_ADMIN_AFTER_REGISTRATION from registration.models import notify_admins_on_register_complete +from seahub.utils import send_perm_audit_msg +from seahub.share.utils import share_dir_to_user + +logger = logging.getLogger(__name__) + def token_view(request, token): """Show form to let user set password. @@ -72,4 +80,36 @@ def token_view(request, token): if NOTIFY_ADMIN_AFTER_REGISTRATION: notify_admins_on_register_complete(user.email) + # repo share invitation + try: + shared_queryset = RepoShareInvitation.objects.list_by_invitation(invitation=i) + accepter = i.accepter + + for shared_obj in shared_queryset: + repo_id = shared_obj.repo_id + path = shared_obj.path + permission = shared_obj.permission + inviter = shared_obj.invitation.inviter + + # recourse check + repo = seafile_api.get_repo(repo_id) + if not repo: + logger.warning('[ %s ] repo not found when [ %s ] accepts the invitation to share repo') % (repo_id, accepter) + continue + if seafile_api.get_dir_id_by_path(repo.id, path) is None: + logger.warning('[ %s ][ %s ] dir not found when [ %s ] accepts the invitation to share repo') % (repo_id, path, accepter) + continue + + repo_owner = seafile_api.get_repo_owner(repo_id) + share_dir_to_user(repo, path, repo_owner, + inviter, accepter, permission, None) + + send_perm_audit_msg('modify-repo-perm', + inviter, accepter, repo_id, path, permission) + + # delete + shared_queryset.delete() + except Exception as e: + logger.error(e) + return HttpResponseRedirect(SITE_ROOT) diff --git a/seahub/urls.py b/seahub/urls.py index d18b76986e..e797079111 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -67,6 +67,8 @@ from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgress from seahub.api2.endpoints.move_folder_merge import MoveFolderMergeView from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchView from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView +from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView +from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView from seahub.api2.endpoints.user_avatar import UserAvatarView @@ -408,7 +410,9 @@ urlpatterns = [ url(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()), url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()), url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/revoke/$', InvitationRevokeView.as_view()), - + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/shared/invitations/$', RepoShareInvitationsView.as_view(), name="api-v2.1-repo-share-invitations"), + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/shared/invitations/batch/$', RepoShareInvitationsBatchView.as_view(), name="api-v2.1-repo-share-invitations-batch"), + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/shared/invitation/$', RepoShareInvitationView.as_view(), name="api-v2.1-repo-share-invitation"), ## user::avatar url(r'^api/v2.1/user-avatar/$', UserAvatarView.as_view(), name='api-v2.1-user-avatar'), diff --git a/tests/api/endpoints/test_repo_share_invitation.py b/tests/api/endpoints/test_repo_share_invitation.py new file mode 100644 index 0000000000..9871d99f64 --- /dev/null +++ b/tests/api/endpoints/test_repo_share_invitation.py @@ -0,0 +1,87 @@ +import json +from mock import patch + +from seahub.base.accounts import UserPermissions +from seahub.invitations.models import Invitation, RepoShareInvitation +from seahub.test_utils import BaseTestCase +from seahub.api2.permissions import CanInviteGuest +from tests.common.utils import randstring +from seahub.base.accounts import User +from django.core.urlresolvers import reverse + + +class InvitationsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.username = self.user.username + self.repo_id = self.repo.id + self.url = reverse( + 'api-v2.1-repo-share-invitation', args=[self.repo_id]) + + self.i = Invitation.objects.add( + inviter=self.username, accepter='3@qq.com') + assert len(Invitation.objects.all()) == 1 + RepoShareInvitation.objects.add(self.i, self.repo_id, '/', 'r') + assert len(RepoShareInvitation.objects.all()) == 1 + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_put(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'token': self.i.token, + 'path': '/', + 'permission': 'rw', + + }) + resp = self.client.put(self.url, data, 'application/json') + self.assertEqual(200, resp.status_code) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_not_put_with_exist_permission(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'token': self.i.token, + 'path': '/', + 'permission': 'r', + + }) + resp = self.client.put(self.url, data, 'application/json') + self.assertEqual(400, resp.status_code) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_delete(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'token': self.i.token, + 'path': '/', + }) + resp = self.client.delete(self.url, data, 'application/json') + self.assertEqual(200, resp.status_code) + + assert len(RepoShareInvitation.objects.all()) == 0 + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_not_delete_with_invalid_path(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'token': self.i.token, + 'path': '/invalid_path', + }) + resp = self.client.delete(self.url, data, 'application/json') + self.assertEqual(404, resp.status_code) diff --git a/tests/api/endpoints/test_repo_share_invitations.py b/tests/api/endpoints/test_repo_share_invitations.py new file mode 100644 index 0000000000..99408d7a58 --- /dev/null +++ b/tests/api/endpoints/test_repo_share_invitations.py @@ -0,0 +1,107 @@ +import json +from mock import patch + +from django.test import override_settings +from post_office.models import Email + +from django.core.urlresolvers import reverse +from seahub.base.accounts import UserPermissions +from seahub.invitations.models import Invitation, RepoShareInvitation +from seahub.test_utils import BaseTestCase +from seahub.api2.permissions import CanInviteGuest + + +class RepoShareInvitationsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.username = self.user.username + self.repo_id = self.repo.id + self.url = reverse( + 'api-v2.1-repo-share-invitations', args=[self.repo_id]) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_list(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + invitation_obj_1 = Invitation.objects.add( + inviter=self.username, accepter='1@qq.com') + invitation_obj_2 = Invitation.objects.add( + inviter=self.username, accepter='1@qq.com') + + RepoShareInvitation.objects.add(invitation_obj_1, self.repo_id, '/', 'r') + RepoShareInvitation.objects.add(invitation_obj_2, self.repo_id, '/', 'rw') + + resp = self.client.get(self.url + '?path=/') + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert len(json_resp.get('repo_share_invitation_list')) == 2 + + +class RepoShareInvitationsBatchTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.username = self.user.username + self.repo_id = self.repo.id + self.url = reverse( + 'api-v2.1-repo-share-invitations-batch', args=[self.repo_id]) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_add_with_batch(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'type': 'guest', + 'accepters': ['1@qq.com', '2@qq.com'], + 'path': '/', + 'permission': 'r', + }) + resp = self.client.post(self.url, data, 'application/json') + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert self.username == json_resp['success'][0]['inviter'] + assert '1@qq.com' == json_resp['success'][0]['accepter'] + assert '2@qq.com' == json_resp['success'][1]['accepter'] + assert json_resp['success'][0]['expire_time'] is not None + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_with_invalid_path(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + data = json.dumps({ + 'type': 'guest', + 'accepters': ['1@qq.com', '2@qq.com'], + 'path': '/invalid_path', + 'permission': 'r', + }) + resp = self.client.post(self.url, data, 'application/json') + self.assertEqual(404, resp.status_code) + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_with_invalid_user(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + self.logout() + self.login_as(self.admin) + + data = json.dumps({ + 'type': 'guest', + 'accepters': ['1@qq.com', '2@qq.com'], + 'path': '/', + 'permission': 'r', + }) + resp = self.client.post(self.url, data, 'application/json') + self.assertEqual(403, resp.status_code) + self.logout