diff --git a/frontend/src/components/dialog/repo-office-suite-dialog.js b/frontend/src/components/dialog/repo-office-suite-dialog.js new file mode 100644 index 0000000000..83c591e953 --- /dev/null +++ b/frontend/src/components/dialog/repo-office-suite-dialog.js @@ -0,0 +1,112 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, TabContent, TabPane } from 'reactstrap'; +import makeAnimated from 'react-select/animated'; +import { userAPI } from '../../utils/user-api'; +import { gettext, isPro } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import toaster from '../toast'; +import { SeahubSelect } from '../common/select'; +import '../../css/repo-office-suite-dialog.css'; + +const propTypes = { + itemName: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired, + +}; + +class OfficeSuiteDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + selectedOption: null, + errorMsg: [] + }; + this.options = []; + } + + handleSelectChange = (option) => { + this.setState({ selectedOption: option }); + }; + + submit = () => { + let suite_id = this.state.selectedOption.value; + this.props.submit(suite_id); + }; + + componentDidMount() { + if (isPro) { + userAPI.getOfficeSuite(this.props.repoID).then((res) => { + this.updateOptions(res); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + } + + updateOptions = (officeSuites) => { + officeSuites.data.suites_info.forEach(item => { + let option = { + value: item.id, + label: item.name, + is_selected: item.is_selected, + }; + this.options.push(option); + }); + let selectedOption = this.options.find(op => op.is_selected); + this.setState({ selectedOption }); + }; + + + renderOfficeSuiteContent = () => { + return ( + +
+ + {isPro && + + + } + +
+
+ ); + }; + + render() { + const { itemName: repoName } = this.props; + let title = gettext('{library_name} Office Suite'); + title = title.replace('{library_name}', '' + Utils.HTMLescape(repoName) + ''); + return ( + + + + + + {this.renderOfficeSuiteContent()} + + + + + + + ); + } +} + +OfficeSuiteDialog.propTypes = propTypes; + +export default OfficeSuiteDialog; diff --git a/frontend/src/css/repo-office-suite-dialog.css b/frontend/src/css/repo-office-suite-dialog.css new file mode 100644 index 0000000000..33b97dd462 --- /dev/null +++ b/frontend/src/css/repo-office-suite-dialog.css @@ -0,0 +1,30 @@ +.repo-office-suite-dialog .repo-office-suite-dialog-content { + padding: 0; + min-height: 5.5rem; + min-width: 10rem; + display: flex; + flex-direction: column; + } + + @media (min-width: 268px) { + .repo-office-suite-dialog .repo-office-suite-dialog-content { + flex-direction: column; + } + } + + .repo-office-suite-dialog-content .repo-office-suite-dialog-main { + display: flex; + flex-basis: 48%; + padding: 1rem; + } + + .repo-office-suite-dialog-content .repo-office-suite-dialog-main .tab-content { + flex: 1; + } + + + .repo-office-suite-dialog-content .repo-office-suite-dialog-main .repo-select-office-suite { + padding: 8px 0; + } + + \ No newline at end of file diff --git a/frontend/src/models/repo-info.js b/frontend/src/models/repo-info.js index 0172879da2..75f821a2b6 100644 --- a/frontend/src/models/repo-info.js +++ b/frontend/src/models/repo-info.js @@ -23,6 +23,7 @@ class RepoInfo { this.lib_need_decrypt = object.lib_need_decrypt; this.last_modified = object.last_modified; this.status = object.status; + this.enable_onlyoffice = object.enable_onlyoffice; } } diff --git a/frontend/src/pages/my-libs/mylib-repo-list-item.js b/frontend/src/pages/my-libs/mylib-repo-list-item.js index a7ac34db0a..f1d28cc667 100644 --- a/frontend/src/pages/my-libs/mylib-repo-list-item.js +++ b/frontend/src/pages/my-libs/mylib-repo-list-item.js @@ -20,6 +20,7 @@ import Rename from '../../components/rename'; import MylibRepoMenu from './mylib-repo-menu'; import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog'; import RepoShareAdminDialog from '../../components/dialog/repo-share-admin-dialog'; +import OfficeSuiteDialog from '../../components/dialog/repo-office-suite-dialog'; import RepoMonitoredIcon from '../../components/repo-monitored-icon'; import { LIST_MODE } from '../../components/dir-view-mode/constants'; import { userAPI } from '../../utils/user-api'; @@ -57,6 +58,7 @@ class MylibRepoListItem extends React.Component { isAPITokenDialogShow: false, isRepoShareAdminDialogOpen: false, isRepoDeleted: false, + isOfficeSuiteDialogShow: false, }; } @@ -128,6 +130,9 @@ class MylibRepoListItem extends React.Component { case 'Share Admin': this.toggleRepoShareAdminDialog(); break; + case 'Office Suite': + this.onOfficeSuiteToggle(); + break; default: break; } @@ -221,6 +226,10 @@ class MylibRepoListItem extends React.Component { this.setState({ isAPITokenDialogShow: !this.state.isAPITokenDialogShow }); }; + onOfficeSuiteToggle = () => { + this.setState({ isOfficeSuiteDialogShow: !this.state.isOfficeSuiteDialogShow }); + }; + toggleRepoShareAdminDialog = () => { this.setState({ isRepoShareAdminDialogOpen: !this.state.isRepoShareAdminDialogOpen }); }; @@ -266,6 +275,21 @@ class MylibRepoListItem extends React.Component { this.onTransferToggle(); }; + onOfficeSuiteChange = (suiteID) => { + let repoID = this.props.repo.repo_id; + userAPI.setOfficeSuite(repoID, suiteID).then(res => { + let message = gettext('Successfully change office suite.'); + toaster.success(message); + }).catch(error => { + if (error.response) { + toaster.danger(error.response.data.error_msg || gettext('Error'), { duration: 3 }); + } else { + toaster.danger(gettext('Failed. Please check the network.'), { duration: 3 }); + } + }); + this.onOfficeSuiteToggle(); + }; + onDeleteRepo = (repo) => { seafileAPI.deleteRepo(repo.repo_id).then((res) => { @@ -548,6 +572,17 @@ class MylibRepoListItem extends React.Component { )} + {this.state.isOfficeSuiteDialogShow && ( + + + + )} + ); } diff --git a/frontend/src/pages/my-libs/mylib-repo-menu.js b/frontend/src/pages/my-libs/mylib-repo-menu.js index 8ac876f3fe..c2b275b74f 100644 --- a/frontend/src/pages/my-libs/mylib-repo-menu.js +++ b/frontend/src/pages/my-libs/mylib-repo-menu.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; -import { gettext, isPro, folderPermEnabled, enableRepoSnapshotLabel, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants'; +import { gettext, isPro, folderPermEnabled, enableRepoSnapshotLabel, enableResetEncryptedRepoPassword, isEmailConfigured, enableMultipleOfficeSuite } from '../../utils/constants'; import { Utils } from '../../utils/utils'; const propTypes = { @@ -120,6 +120,9 @@ class MylibRepoMenu extends React.Component { if (this.props.isPC && enableRepoSnapshotLabel) { operations.push('Label Current State'); } + if (enableMultipleOfficeSuite && isPro) { + operations.push('Office Suite'); + } return operations; }; @@ -174,6 +177,9 @@ class MylibRepoMenu extends React.Component { case 'SeaTable integration': translateResult = gettext('SeaTable integration'); break; + case 'Office Suite': + translateResult = gettext('Office Suite'); + break; default: break; } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index d6b9cdb95b..718614fe71 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -90,6 +90,7 @@ export const ocmRemoteServers = window.app.pageOptions.ocmRemoteServers; export const enableOCMViaWebdav = window.app.pageOptions.enableOCMViaWebdav; export const enableSSOToThirdpartWebsite = window.app.pageOptions.enableSSOToThirdpartWebsite; export const enableSeadoc = window.app.pageOptions.enableSeadoc; +export const enableMultipleOfficeSuite = window.app.pageOptions.enableMultipleOfficeSuite; export const curNoteMsg = window.app.pageOptions.curNoteMsg; export const curNoteID = window.app.pageOptions.curNoteID; diff --git a/frontend/src/utils/user-api.js b/frontend/src/utils/user-api.js index 11dee2c039..4c78bc0f8c 100644 --- a/frontend/src/utils/user-api.js +++ b/frontend/src/utils/user-api.js @@ -63,6 +63,18 @@ class UserAPI { form.append('reshare', reshare); return this.req.put(url, form); } + + getOfficeSuite(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/office-suite/'; + return this.req.get(url); + } + + setOfficeSuite(repoID, suiteID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/office-suite/'; + const form = new FormData(); + form.append('suite_id', suiteID); + return this.req.put(url, form); + } } let userAPI = new UserAPI(); diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 995983be1f..afd1b141fc 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1,4 +1,4 @@ -import { mediaUrl, gettext, serviceURL, siteRoot, isPro, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableOnlyoffice, enableSeadoc, enableFileTags, enableRepoSnapshotLabel, +import { mediaUrl, gettext, serviceURL, siteRoot, isPro, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableSeadoc, enableFileTags, enableRepoSnapshotLabel, enableResetEncryptedRepoPassword, isEmailConfigured, isSystemStaff } from './constants'; import TextTranslation from './text-translation'; import React from 'react'; @@ -664,7 +664,7 @@ export const Utils = { list.push(HISTORY); } - if (permission == 'rw' && enableOnlyoffice && + if (permission == 'rw' && currentRepoInfo.enable_onlyoffice && onlyofficeConverterExtensions.includes(Utils.getFileExtension(dirent.name, false))) { list.push(ONLYOFFICE_CONVERT); } diff --git a/seahub/api2/endpoints/repo_office_suite.py b/seahub/api2/endpoints/repo_office_suite.py new file mode 100644 index 0000000000..3e31db9356 --- /dev/null +++ b/seahub/api2/endpoints/repo_office_suite.py @@ -0,0 +1,88 @@ +import json + +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 seaserv import seafile_api + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error +from seahub.api2.permissions import IsProVersion + +from seahub.onlyoffice.models import RepoExtraConfig, REPO_OFFICE_CONFIG +from seahub.settings import OFFICE_SUITE_LIST +from seahub.utils.repo import get_repo_owner + +class OfficeSuiteConfig(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, IsProVersion) + throttle_classes = (UserRateThrottle,) + + def get(self, request, repo_id): + if not request.user.permissions.can_choose_office_suite(): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + 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) + + repo_owner = get_repo_owner(request, repo_id) + if '@seafile_group' in repo_owner: + error_msg = 'Department repo can not use this feature.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + current_suite = RepoExtraConfig.objects.filter(repo_id=repo_id, config_type=REPO_OFFICE_CONFIG).first() + suites_info = [] + for office_suite in OFFICE_SUITE_LIST: + suite_info = {} + suite_info['id'] = office_suite.get('id') + suite_info['name'] = office_suite.get('name') + suite_info['is_default'] = office_suite.get('is_default') + if current_suite: + config_details = json.loads(current_suite.config_details) + office_config = config_details.get('office_suite') + suite_info['is_selected'] = (True if office_config and office_config.get('suite_id') == office_suite.get('id') else False) + else: + suite_info['is_selected'] = office_suite.get('is_default') + suites_info.append(suite_info) + + return Response({'suites_info': suites_info}) + + def put(self, request, repo_id): + # arguments check + suite_id = request.data.get('suite_id', '') + if suite_id not in ['collabora', 'onlyoffice']: + error_msg = 'suite_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if not request.user.permissions.can_choose_office_suite(): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, 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) + + repo_owner = get_repo_owner(request, repo_id) + if '@seafile_group' in repo_owner: + error_msg = 'Department repo can not use this feature.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + config_details = { + 'office_suite': { + 'suite_id': suite_id + } + } + RepoExtraConfig.objects.update_or_create(repo_id=repo_id, config_type=REPO_OFFICE_CONFIG, + defaults= {'config_details':json.dumps(config_details)} ) + + return Response({"success": True}, status=status.HTTP_200_OK) diff --git a/seahub/api2/endpoints/repos.py b/seahub/api2/endpoints/repos.py index 022e336b70..bbf3586e3a 100644 --- a/seahub/api2/endpoints/repos.py +++ b/seahub/api2/endpoints/repos.py @@ -36,6 +36,7 @@ from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.settings import ENABLE_STORAGE_CLASSES from seaserv import seafile_api +from seahub.views.file import get_office_feature_by_repo logger = logging.getLogger(__name__) @@ -125,6 +126,8 @@ class ReposView(APIView): continue url, _, _ = api_avatar_url(email) + enable_onlyoffice, _ = get_office_feature_by_repo(r) + repo_info = { "type": "mine", "repo_id": r.id, @@ -144,6 +147,7 @@ class ReposView(APIView): "monitored": r.repo_id in monitored_repo_id_list, "status": normalize_repo_status_code(r.status), "salt": r.salt if r.enc_version >= 3 else '', + "enable_onlyoffice": enable_onlyoffice } if is_pro_version() and ENABLE_STORAGE_CLASSES: @@ -199,6 +203,8 @@ class ReposView(APIView): owner_contact_email = '' if is_group_owned_repo else contact_email_dict.get(owner_email, '') url, _, _ = api_avatar_url(owner_email) + enable_onlyoffice, _ = get_office_feature_by_repo(r) + repo_info = { "type": "shared", "repo_id": r.repo_id, @@ -218,6 +224,7 @@ class ReposView(APIView): "monitored": r.repo_id in monitored_repo_id_list, "status": normalize_repo_status_code(r.status), "salt": r.salt if r.enc_version >= 3 else '', + "enable_onlyoffice": enable_onlyoffice } if r.repo_id in repos_with_admin_share_to: @@ -259,7 +266,7 @@ class ReposView(APIView): if is_wiki_repo(r): continue - + enable_onlyoffice, _ = get_office_feature_by_repo(r) repo_info = { "type": "group", "group_id": r.group_id, @@ -277,6 +284,7 @@ class ReposView(APIView): "monitored": r.repo_id in monitored_repo_id_list, "status": normalize_repo_status_code(r.status), "salt": r.salt if r.enc_version >= 3 else '', + "enable_onlyoffice": enable_onlyoffice } repo_info_list.append(repo_info) @@ -310,6 +318,7 @@ class ReposView(APIView): repo_owner = repo_id_owner_dict[r.repo_id] url, _, _ = api_avatar_url(repo_owner) + enable_onlyoffice, _ = get_office_feature_by_repo(r) repo_info = { "type": "public", "repo_id": r.repo_id, @@ -328,6 +337,7 @@ class ReposView(APIView): "starred": r.repo_id in starred_repo_id_list, "status": normalize_repo_status_code(r.status), "salt": r.salt if r.enc_version >= 3 else '', + "enable_onlyoffice": enable_onlyoffice } repo_info_list.append(repo_info) @@ -369,6 +379,7 @@ class RepoView(APIView): return api_error(status.HTTP_403_FORBIDDEN, error_msg) username = request.user.username + enable_onlyoffice, _ = get_office_feature_by_repo(repo) lib_need_decrypt = False if repo.encrypted \ @@ -405,6 +416,7 @@ class RepoView(APIView): "lib_need_decrypt": lib_need_decrypt, "last_modified": timestamp_to_isoformat_timestr(repo.last_modify), "status": normalize_repo_status_code(repo.status), + "enable_onlyoffice": enable_onlyoffice } return Response(result) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 6dfbcdaf80..4f8fb2c02f 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -110,6 +110,7 @@ from seahub.settings import THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, \ from seahub.subscription.utils import subscription_check from seahub.organizations.models import OrgAdminSettings, DISABLE_ORG_ENCRYPTED_LIBRARY from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_image_parent_path, get_seadoc_asset_upload_link +from seahub.views.file import get_office_feature_by_repo try: from seahub.settings import CLOUD_MODE @@ -124,11 +125,6 @@ try: except ImportError: ORG_MEMBER_QUOTA_DEFAULT = None -try: - from seahub.settings import ENABLE_OFFICE_WEB_APP -except ImportError: - ENABLE_OFFICE_WEB_APP = False - try: from seahub.settings import ORG_MEMBER_QUOTA_ENABLED except ImportError: @@ -2828,6 +2824,8 @@ class OwaFileView(APIView): error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) + _, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) + action = request.GET.get('action', 'view') if action not in ('view', 'edit'): error_msg = 'action invalid.' diff --git a/seahub/base/accounts.py b/seahub/base/accounts.py index 8a22f2c493..a2aeee0e79 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -450,6 +450,11 @@ class UserPermissions(object): if not settings.ENABLE_WIKI: return False return self._get_perm_by_roles('can_publish_wiki') + + def can_choose_office_suite(self): + if not settings.ENABLE_MULTIPLE_OFFICE_SUITE: + return False + return self._get_perm_by_roles('can_choose_office_suite') class AdminPermissions(object): diff --git a/seahub/onlyoffice/models.py b/seahub/onlyoffice/models.py index 9bc7a10047..ba74ee7854 100644 --- a/seahub/onlyoffice/models.py +++ b/seahub/onlyoffice/models.py @@ -14,3 +14,15 @@ class OnlyOfficeDocKey(models.Model): file_path = models.TextField() repo_id_file_path_md5 = models.CharField(max_length=100, db_index=True, unique=True) created_time = models.DateTimeField(default=datetime.datetime.now) + + +REPO_OFFICE_CONFIG = 'office' +class RepoExtraConfig(models.Model): + + repo_id = models.CharField(max_length=36, db_index=True) + config_type = models.CharField(max_length=50) + config_details = models.TextField() + + class Meta: + db_table = 'repo_extra_config' + unique_together = ('repo_id', 'config_type') diff --git a/seahub/onlyoffice/settings.py b/seahub/onlyoffice/settings.py index e632491da9..56a2842469 100644 --- a/seahub/onlyoffice/settings.py +++ b/seahub/onlyoffice/settings.py @@ -1,5 +1,6 @@ # Copyright (c) 2012-2016 Seafile Ltd. from django.conf import settings +from seahub.settings import ENABLE_MULTIPLE_OFFICE_SUITE, OFFICE_SUITE_LIST, OFFICE_SUITE_ENABLED_FILE_TYPES, OFFICE_SUITE_ENABLED_EDIT_FILE_TYPES ENABLE_ONLYOFFICE = getattr(settings, 'ENABLE_ONLYOFFICE', False) ONLYOFFICE_APIJS_URL = getattr(settings, 'ONLYOFFICE_APIJS_URL', '') @@ -44,3 +45,23 @@ EXT_DOCUMENT = [ ".html", ".htm", ".mht", ".xml", ".pdf", ".djvu", ".fb2", ".epub", ".xps" ] + + +if ENABLE_MULTIPLE_OFFICE_SUITE: + OFFICE_SUITE_ONLY_OFFICE = 'onlyoffice' + office_info = {} + for s in OFFICE_SUITE_LIST: + if s.get('id') == OFFICE_SUITE_ONLY_OFFICE: + office_info = s + break + ONLYOFFICE_APIJS_URL = office_info.get('ONLYOFFICE_APIJS_URL') + ONLYOFFICE_CONVERTER_URL = ONLYOFFICE_APIJS_URL and ONLYOFFICE_APIJS_URL.replace("/web-apps/apps/api/documents/api.js", "/ConvertService.ashx") + ONLYOFFICE_FORCE_SAVE = office_info.get('ONLYOFFICE_FORCE_SAVE', False) + ONLYOFFICE_JWT_SECRET = office_info.get('ONLYOFFICE_JWT_SECRET', '') + ONLYOFFICE_JWT_HEADER = office_info.get('ONLYOFFICE_JWT_HEADER', 'Authorization') + ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT = office_info.get('ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT', 'AscDesktopEditor') + VERIFY_ONLYOFFICE_CERTIFICATE = office_info.get('VERIFY_ONLYOFFICE_CERTIFICATE', True) + ONLYOFFICE_FILE_EXTENSION = OFFICE_SUITE_ENABLED_FILE_TYPES + ONLYOFFICE_EDIT_FILE_EXTENSION = OFFICE_SUITE_ENABLED_EDIT_FILE_TYPES + + \ No newline at end of file diff --git a/seahub/onlyoffice/views.py b/seahub/onlyoffice/views.py index e01939d115..192bcf665f 100644 --- a/seahub/onlyoffice/views.py +++ b/seahub/onlyoffice/views.py @@ -25,7 +25,7 @@ from django.views.decorators.csrf import csrf_exempt from seaserv import seafile_api -from seahub.onlyoffice.models import OnlyOfficeDocKey + from seahub.onlyoffice.settings import VERIFY_ONLYOFFICE_CERTIFICATE, ONLYOFFICE_JWT_SECRET from seahub.onlyoffice.utils import get_onlyoffice_dict, get_doc_key_by_repo_id_file_path from seahub.onlyoffice.utils import delete_doc_key, get_file_info_by_doc_key @@ -40,6 +40,7 @@ from seahub.views import check_folder_permission from seahub.utils.file_types import SPREADSHEET + # Get an instance of a logger logger = logging.getLogger('onlyoffice') @@ -506,4 +507,3 @@ class OnlyofficeGetReferenceData(APIView): } result['token'] = jwt.encode(result, ONLYOFFICE_JWT_SECRET) return Response({'data': result}) - diff --git a/seahub/role_permissions/settings.py b/seahub/role_permissions/settings.py index 28c0d88cdf..4e01db9780 100644 --- a/seahub/role_permissions/settings.py +++ b/seahub/role_permissions/settings.py @@ -48,7 +48,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = { 'upload_rate_limit': 0, 'download_rate_limit': 0, 'monthly_rate_limit': '', - 'monthly_rate_limit_per_user': '' + 'monthly_rate_limit_per_user': '', + 'can_choose_office_suite': True, }, GUEST_USER: { 'can_add_repo': False, @@ -73,7 +74,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = { 'upload_rate_limit': 0, 'download_rate_limit': 0, 'monthly_rate_limit': '', - 'monthly_rate_limit_per_user': '' + 'monthly_rate_limit_per_user': '', + 'can_choose_office_suite': False, }, } diff --git a/seahub/settings.py b/seahub/settings.py index 81c14ce5af..56a06236ef 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -973,6 +973,26 @@ ENABLE_METADATA_MANAGEMENT = False METADATA_SERVER_URL = '' METADATA_SERVER_SECRET_KEY = '' +############################# +# multi office suite support +############################# +ENABLE_MULTIPLE_OFFICE_SUITE = False +OFFICE_SUITE_LIST = [ + { + "id": "onlyoffice", + "name": "OnlyOffice", + "is_default": True, + }, + { + "id": "collabora", + "name": "CollaboraOnline", + "is_default": False, + } +] +ROLES_DEFAULT_OFFCICE_SUITE = {} +OFFICE_SUITE_ENABLED_FILE_TYPES = [] +OFFICE_SUITE_ENABLED_EDIT_FILE_TYPES = [] + # file tags ENABLE_FILE_TAGS = True diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 0fdc2e9134..dd0c01efbb 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -158,6 +158,7 @@ enableMetadataManagement: {% if enable_metadata_management %} true {% else %} false {% endif %}, enableFileTags: {% if enable_file_tags %} true {% else %} false {% endif %}, enableShowAbout: {% if enable_show_about %} true {% else %} false {% endif %}, + enableMultipleOfficeSuite: {% if user.permissions.can_choose_office_suite %} true {% else %} false {% endif %}, showWechatSupportGroup: {% if show_wechat_support_group %} true {% else %} false {% endif %}, baiduMapKey: '{{ baidu_map_key }}', googleMapKey: '{{ google_map_key }}', diff --git a/seahub/urls.py b/seahub/urls.py index 4220eaba9b..c2a4bad49c 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -212,6 +212,7 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.user_list import UserListView from seahub.api2.endpoints.seahub_io import SeahubIOStatus +from seahub.api2.endpoints.repo_office_suite import OfficeSuiteConfig urlpatterns = [ @@ -463,6 +464,7 @@ urlpatterns = [ re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/upload-links/(?P[a-f0-9]+)/$', RepoUploadLink.as_view(), name='api-v2.1-repo-upload-link'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/share-info/$', RepoShareInfoView.as_view(), name='api-v2.1-repo-share-info-view'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/image-rotate/$', RepoImageRotateView.as_view(), name='api-v2.1-repo-image-rotate-view'), + re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/office-suite/$', OfficeSuiteConfig.as_view(), name='api-v2.1-repo-office-suite'), ## user:: repo-api-tokens re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/repo-api-tokens/$', RepoAPITokensView.as_view(), name='api-v2.1-repo-api-tokens'), @@ -1006,7 +1008,7 @@ if getattr(settings, 'ENABLE_MULTI_ADFS', False) or getattr(settings, 'ENABLE_AD path('saml2/complete/', auth_complete, name='saml2_complete'), ] -if getattr(settings, 'ENABLE_ONLYOFFICE', False): +if getattr(settings, 'ENABLE_ONLYOFFICE', False) or getattr(settings, 'ENABLE_MULTIPLE_OFFICE_SUITE', False): urlpatterns += [ path('onlyoffice/', include('seahub.onlyoffice.urls')), path('onlyoffice-api/', include('seahub.onlyoffice.api_urls')), diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index c1b9635df3..f76bc4fc76 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -1150,7 +1150,6 @@ def react_fake_view(request, **kwargs): 'enable_metadata_management': ENABLE_METADATA_MANAGEMENT, 'enable_file_tags': settings.ENABLE_FILE_TAGS, 'enable_show_about': settings.ENABLE_SHOW_ABOUT - } if ENABLE_METADATA_MANAGEMENT: diff --git a/seahub/views/file.py b/seahub/views/file.py index 5564ac63f3..7c84a3e4a7 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -34,17 +34,18 @@ from django.template.defaultfilters import filesizeformat from seaserv import seafile_api, ccnet_api from seaserv import get_repo, get_commits, \ get_file_id_by_path, get_commit, get_file_size, \ - seafserv_threaded_rpc + seafserv_threaded_rpc, get_org_id_by_repo_id from seahub.settings import SITE_ROOT from seahub.share.utils import check_share_link_user_access from seahub.tags.models import FileUUIDMap from seahub.wopi.utils import get_wopi_dict from seahub.onlyoffice.utils import get_onlyoffice_dict +from seahub.onlyoffice.models import RepoExtraConfig, REPO_OFFICE_CONFIG from seahub.auth.decorators import login_required from seahub.auth import SESSION_MOBILE_LOGIN_KEY from seahub.base.decorators import repo_passwd_set_required -from seahub.base.accounts import ANONYMOUS_EMAIL +from seahub.base.accounts import ANONYMOUS_EMAIL, User from seahub.base.templatetags.seahub_tags import file_icon_filter from seahub.share.models import FileShare, check_share_link_common from seahub.share.decorators import share_link_audit, share_link_login_required @@ -66,6 +67,7 @@ from seahub.utils.http import json_response, \ BadRequestException from seahub.utils.file_op import check_file_lock, \ ONLINE_OFFICE_LOCK_OWNER, if_locked_by_online_office +from seahub.utils.user_permissions import get_user_role from seahub.views import check_folder_permission, \ get_unencry_rw_repos_by_user from seahub.utils.repo import is_repo_owner, parse_repo_perm, is_repo_admin @@ -87,33 +89,34 @@ from seahub.settings import FILE_ENCODING_LIST, FILE_PREVIEW_MAX_SIZE, \ SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_MAX, SHARE_LINK_PASSWORD_MIN_LENGTH, \ SHARE_LINK_FORCE_USE_PASSWORD, SHARE_LINK_PASSWORD_STRENGTH_LEVEL, \ SHARE_LINK_EXPIRE_DAYS_DEFAULT, ENABLE_SHARE_LINK_REPORT_ABUSE, SEADOC_SERVER_URL, \ - ENABLE_METADATA_MANAGEMENT, BAIDU_MAP_KEY, GOOGLE_MAP_KEY, GOOGLE_MAP_ID + ENABLE_METADATA_MANAGEMENT, BAIDU_MAP_KEY, GOOGLE_MAP_KEY, GOOGLE_MAP_ID, ENABLE_MULTIPLE_OFFICE_SUITE, \ + OFFICE_SUITE_LIST # wopi try: - from seahub.settings import ENABLE_OFFICE_WEB_APP + from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP except ImportError: ENABLE_OFFICE_WEB_APP = False try: - from seahub.settings import ENABLE_OFFICE_WEB_APP_EDIT + from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP_EDIT except ImportError: ENABLE_OFFICE_WEB_APP_EDIT = False try: - from seahub.settings import OFFICE_WEB_APP_FILE_EXTENSION + from seahub.wopi.settings import OFFICE_WEB_APP_FILE_EXTENSION except ImportError: OFFICE_WEB_APP_FILE_EXTENSION = () try: - from seahub.settings import OFFICE_WEB_APP_EDIT_FILE_EXTENSION + from seahub.wopi.settings import OFFICE_WEB_APP_EDIT_FILE_EXTENSION except ImportError: OFFICE_WEB_APP_EDIT_FILE_EXTENSION = () # onlyoffice try: - from seahub.settings import ENABLE_ONLYOFFICE + from seahub.onlyoffice.settings import ENABLE_ONLYOFFICE except ImportError: ENABLE_ONLYOFFICE = False @@ -130,6 +133,7 @@ except ImportError: from seahub.thirdparty_editor.settings import ENABLE_THIRDPARTY_EDITOR from seahub.thirdparty_editor.settings import THIRDPARTY_EDITOR_ACTION_URL_DICT from seahub.thirdparty_editor.settings import THIRDPARTY_EDITOR_ACCESS_TOKEN_EXPIRATION +from seahub.settings import ROLES_DEFAULT_OFFCICE_SUITE # Get an instance of a logger logger = logging.getLogger(__name__) @@ -140,6 +144,57 @@ FILE_TYPE_FOR_NEW_FILE_LINK = [ MARKDOWN ] +def _check_feature(repo_id): + office_suite = RepoExtraConfig.objects.filter(repo_id=repo_id, config_type=REPO_OFFICE_CONFIG).first() + if office_suite: + repo_config_details = json.loads(office_suite.config_details) + office_config = repo_config_details.get('office_suite') + return office_config.get('suite_id') if office_config else None + return None + +def get_office_feature_by_repo(repo): + enable_onlyoffice, enable_office_app = False, False + if not ENABLE_MULTIPLE_OFFICE_SUITE: + return ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP + + if not OFFICE_SUITE_LIST: + return ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP + + org_id = get_org_id_by_repo_id(repo.repo_id) + if org_id > 0: + repo_owner = seafile_api.get_org_repo_owner(repo.repo_id) + else: + repo_owner = seafile_api.get_repo_owner(repo.repo_id) + if '@seafile_group' in repo_owner: + repo_feature = None + else: + repo_feature = _check_feature(repo.repo_id) + + if not repo_feature and '@seafile_group' not in repo_owner: + user = User.objects.get(email=repo_owner) + role = get_user_role(user) + repo_feature = ROLES_DEFAULT_OFFCICE_SUITE.get(role) + + if not repo_feature: + default_suite = {} + for s in OFFICE_SUITE_LIST: + if s.get('is_default'): + default_suite = s + break + if default_suite.get('id') == 'onlyoffice': + enable_onlyoffice = True + if default_suite.get('id') == 'collabora': + enable_office_app = True + else: + if repo_feature == 'onlyoffice': + enable_onlyoffice = True + if repo_feature == 'collabora': + enable_office_app = True + + return enable_onlyoffice, enable_office_app + + + def gen_path_link(path, repo_name): """ Generate navigate paths and links in repo page. @@ -333,6 +388,8 @@ def can_preview_file(file_name, file_size, repo): filetype, fileext = get_file_type_and_ext(file_name) + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) + # Seafile defines 10 kinds of filetype: # TEXT, MARKDOWN, IMAGE, DOCUMENT, SPREADSHEET, VIDEO, AUDIO, PDF, SVG if filetype in (TEXT, MARKDOWN, IMAGE) or fileext in get_conf_text_ext(): @@ -387,7 +444,7 @@ def can_edit_file(file_name, file_size, repo): """Check whether Seafile supports edit file. Returns (True, None) if Yes, otherwise (False, error_msg). """ - + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) can_preview, err_msg = can_preview_file(file_name, file_size, repo) if not can_preview: return False, err_msg @@ -474,7 +531,7 @@ def convert_repo_path_when_can_not_view_file(request, repo_id, path): @login_required @repo_passwd_set_required def view_lib_file(request, repo_id, path): - + # resource check repo = seafile_api.get_repo(repo_id) if not repo: @@ -484,6 +541,8 @@ def view_lib_file(request, repo_id, path): file_id = seafile_api.get_file_id_by_path(repo_id, path) if not file_id: return render_error(request, _('File does not exist')) + + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) # permission check username = request.user.username @@ -926,6 +985,8 @@ def view_history_file_common(request, repo_id, ret_dict): path = request.GET.get('p', '/') path = normalize_file_path(path) + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) + commit_id = request.GET.get('commit_id', '') if not commit_id: raise Http404 @@ -1176,6 +1237,8 @@ def view_shared_file(request, fileshare): obj_id = seafile_api.get_file_id_by_path(repo_id, path) if not obj_id: return render_error(request, _('File does not exist')) + + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) # permission check shared_by = fileshare.username @@ -1430,6 +1493,8 @@ def view_file_via_shared_dir(request, fileshare): if not repo: raise Http404 + ENABLE_ONLYOFFICE, ENABLE_OFFICE_WEB_APP = get_office_feature_by_repo(repo) + # recourse check # Get file path from frontend, and construct request file path # with fileshare.path to real path, used to fetch file content by RPC. @@ -1671,6 +1736,7 @@ def view_raw_file(request, repo_id, file_path): repo = get_repo(repo_id) if not repo: raise Http404 + file_path = file_path.rstrip('/') if file_path[0] != '/': @@ -1734,7 +1800,7 @@ def download_file(request, repo_id, obj_id): repo = get_repo(repo_id) if not repo: raise Http404 - + if repo.encrypted and not seafile_api.is_password_set(repo_id, username): reverse_url = reverse('lib_view', args=[repo_id, repo.name, '']) return HttpResponseRedirect(reverse_url) @@ -1922,6 +1988,7 @@ def office_convert_get_page(request, repo_id, commit_id, path, filename): """ if not HAS_OFFICE_CONVERTER: raise Http404 + if not _OFFICE_PAGE_PATTERN.match(filename): return HttpResponseForbidden() diff --git a/seahub/wopi/settings.py b/seahub/wopi/settings.py index 303795045d..6d933222da 100644 --- a/seahub/wopi/settings.py +++ b/seahub/wopi/settings.py @@ -1,6 +1,5 @@ # Copyright (c) 2012-2016 Seafile Ltd. import seahub.settings as settings - # OfficeOnlineServer, OnlyOffice, CollaboraOffice OFFICE_SERVER_TYPE = getattr(settings, 'OFFICE_SERVER_TYPE', '') @@ -26,6 +25,26 @@ OFFICE_WEB_APP_CLIENT_PEM = getattr(settings, 'OFFICE_WEB_APP_CLIENT_PEM', '') ## Server certificates ## - # Path to a CA_BUNDLE file or directory with certificates of trusted CAs OFFICE_WEB_APP_SERVER_CA = getattr(settings, 'OFFICE_WEB_APP_SERVER_CA', True) + +if settings.ENABLE_MULTIPLE_OFFICE_SUITE: + OFFICE_SUITE_COLLA = 'collabora' + office_info = {} + for s in settings.OFFICE_SUITE_LIST: + if s.get('id') == OFFICE_SUITE_COLLA: + office_info = s + break + OFFICE_SERVER_TYPE = office_info.get('OFFICE_SERVER_TYPE', 'collaboraoffice') + OFFICE_WEB_APP_BASE_URL = office_info.get('OFFICE_WEB_APP_BASE_URL', '') + WOPI_ACCESS_TOKEN_EXPIRATION = office_info.get('WOPI_ACCESS_TOKEN_EXPIRATION', 12 * 60 * 60) + OFFICE_WEB_APP_DISCOVERY_EXPIRATION = office_info.get('OFFICE_WEB_APP_DISCOVERY_EXPIRATION', 7 * 24 * 60 * 60) + OFFICE_WEB_APP_CLIENT_CERT = office_info.get('OFFICE_WEB_APP_CLIENT_CERT', '') + OFFICE_WEB_APP_CLIENT_KEY = office_info.get('OFFICE_WEB_APP_CLIENT_KEY', '') + OFFICE_WEB_APP_CLIENT_PEM = office_info.get('OFFICE_WEB_APP_CLIENT_PEM', '') + OFFICE_WEB_APP_SERVER_CA = office_info.get('OFFICE_WEB_APP_SERVER_CA', '') + ENABLE_OFFICE_WEB_APP_EDIT = office_info.get('ENABLE_OFFICE_WEB_APP_EDIT', False) + OFFICE_WEB_APP_FILE_EXTENSION = settings.OFFICE_SUITE_ENABLED_FILE_TYPES + OFFICE_WEB_APP_EDIT_FILE_EXTENSION = settings.OFFICE_SUITE_ENABLED_EDIT_FILE_TYPES + + diff --git a/tests/seahub/role_permissions/test_utils.py b/tests/seahub/role_permissions/test_utils.py index 15c49125fd..02b4a4a1d6 100644 --- a/tests/seahub/role_permissions/test_utils.py +++ b/tests/seahub/role_permissions/test_utils.py @@ -11,4 +11,4 @@ class UtilsTest(BaseTestCase): assert DEFAULT_USER in get_available_roles() def test_get_enabled_role_permissions_by_role(self): - assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 23 + assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 24