diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04e915b3b32..a8aa0331c6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: "3.8" 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 00000000000..74aeaa2f7a7 --- /dev/null +++ b/frontend/src/components/dialog/repo-office-suite-dialog.js @@ -0,0 +1,110 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalBody, ModalFooter, TabContent, TabPane, ModalHeader} 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 00000000000..ad94b441c84 --- /dev/null +++ b/frontend/src/css/repo-office-suite-dialog.css @@ -0,0 +1,28 @@ +.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 a62570a1d08..9073456aa2c 100644 --- a/frontend/src/models/repo-info.js +++ b/frontend/src/models/repo-info.js @@ -21,6 +21,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 1198be5bc0c..bcd05354968 100644 --- a/frontend/src/pages/my-libs/mylib-repo-list-item.js +++ b/frontend/src/pages/my-libs/mylib-repo-list-item.js @@ -22,7 +22,9 @@ import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog'; import RepoSeaTableIntegrationDialog from '../../components/dialog/repo-seatable-integration-dialog'; import RepoShareAdminDialog from '../../components/dialog/repo-share-admin-dialog'; import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog'; +import OfficeSuiteDialog from '../../components/dialog/repo-office-suite-dialog'; import RepoMonitoredIcon from '../../components/repo-monitored-icon'; +import { userAPI } from '../../utils/user-api'; const propTypes = { repo: PropTypes.object.isRequired, @@ -57,6 +59,7 @@ class MylibRepoListItem extends React.Component { isRepoShareAdminDialogOpen: false, isRepoDeleted: false, isOldFilesAutoDelDialogOpen: false, + isOfficeSuiteDialogShow: false, }; } @@ -137,6 +140,9 @@ class MylibRepoListItem extends React.Component { case 'SeaTable integration': this.onSeaTableIntegrationToggle(); break; + case 'Office Suite': + this.onOfficeSuiteToggle(); + break; default: break; } @@ -249,6 +255,10 @@ class MylibRepoListItem extends React.Component { this.setState({isSeaTableIntegrationShow: !this.state.isSeaTableIntegrationShow}); }; + onOfficeSuiteToggle = () => { + this.setState({ isOfficeSuiteDialogShow: !this.state.isOfficeSuiteDialogShow }); + }; + toggleRepoShareAdminDialog = () => { this.setState({isRepoShareAdminDialogOpen: !this.state.isRepoShareAdminDialogOpen}); }; @@ -298,6 +308,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) => { @@ -544,6 +569,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 c811ab465dd..8262f747be7 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, enableRepoAutoDel, enableSeaTableIntegration } from '../../utils/constants'; +import { gettext, isPro, folderPermEnabled, enableRepoSnapshotLabel, enableResetEncryptedRepoPassword, isEmailConfigured, enableRepoAutoDel, enableSeaTableIntegration, enableMultipleOfficeSuite } from '../../utils/constants'; import { Utils } from '../../utils/utils'; const propTypes = { @@ -126,6 +126,9 @@ class MylibRepoMenu extends React.Component { if (enableSeaTableIntegration) { operations.push('SeaTable integration'); } + if (enableMultipleOfficeSuite && isPro) { + operations.push('Office Suite'); + } return operations; }; @@ -186,6 +189,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 4ac5f7bb074..5a36b070cc3 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -86,6 +86,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 new file mode 100644 index 00000000000..a4fbbe13644 --- /dev/null +++ b/frontend/src/utils/user-api.js @@ -0,0 +1,54 @@ +import axios from 'axios'; +import cookie from 'react-cookies'; +import { siteRoot } from './constants'; + +class UserAPI { + + init({ server, username, password, token }) { + this.server = server; + this.username = username; + this.password = password; + this.token = token; + if (this.token && this.server) { + this.req = axios.create({ + baseURL: this.server, + headers: { 'Authorization': 'Token ' + this.token }, + }); + } + return this; + } + + initForSeahubUsage({ siteRoot, xcsrfHeaders }) { + if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') { + var server = siteRoot.substring(0, siteRoot.length - 1); + this.server = server; + } else { + this.server = siteRoot; + } + + this.req = axios.create({ + headers: { + 'X-CSRFToken': xcsrfHeaders, + } + }); + return this; + } + + 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(); +let xcsrfHeaders = cookie.load('sfcsrftoken'); +userAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders }); + +export { userAPI }; diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index 7d28667a944..8d61f0db0bf 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 } from './constants'; +import { mediaUrl, gettext, serviceURL, siteRoot, isPro, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableSeadoc } from './constants'; import TextTranslation from './text-translation'; import React from 'react'; import toaster from '../components/toast'; @@ -640,7 +640,7 @@ export const Utils = { list.push(HISTORY); } - if (permission == 'rw' && enableOnlyoffice && + if (permission == 'rw' && currentRepoInfo.enable_onlyoffice && onlyofficeConverterExtensions.includes(this.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 00000000000..45b2ae02f34 --- /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 5a7606a4fdf..7f5b1a42db8 100644 --- a/seahub/api2/endpoints/repos.py +++ b/seahub/api2/endpoints/repos.py @@ -29,6 +29,7 @@ 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__) @@ -113,7 +114,8 @@ def get(self, request): # do not return virtual repos if r.is_virtual: continue - + enable_onlyoffice, _ = get_office_feature_by_repo(r) + repo_info = { "type": "mine", "repo_id": r.id, @@ -132,6 +134,7 @@ def get(self, request): "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: @@ -183,6 +186,8 @@ def get(self, request): owner_name = group_name if is_group_owned_repo else nickname_dict.get(owner_email, '') owner_contact_email = '' if is_group_owned_repo else contact_email_dict.get(owner_email, '') + enable_onlyoffice, _ = get_office_feature_by_repo(r) + repo_info = { "type": "shared", "repo_id": r.repo_id, @@ -201,6 +206,7 @@ def get(self, request): "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: @@ -238,6 +244,7 @@ def get(self, request): monitored_repo_id_list = [] for r in group_repos: + enable_onlyoffice, _ = get_office_feature_by_repo(r) repo_info = { "type": "group", "group_id": r.group_id, @@ -255,6 +262,7 @@ def get(self, request): "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) @@ -283,6 +291,7 @@ def get(self, request): for r in public_repos: repo_owner = repo_id_owner_dict[r.repo_id] + enable_onlyoffice, _ = get_office_feature_by_repo(r) repo_info = { "type": "public", "repo_id": r.repo_id, @@ -300,6 +309,7 @@ def get(self, request): "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) @@ -340,6 +350,7 @@ def get(self, request, repo_id): 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 \ @@ -374,6 +385,7 @@ def get(self, request, repo_id): "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 590c73f0b6a..2484fe13d1c 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -106,6 +106,7 @@ ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, SHARE_LINK_EXPIRE_DAYS_MAX, \ SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_DEFAULT from seahub.subscription.utils import subscription_check +from seahub.views.file import get_office_feature_by_repo try: from seahub.settings import CLOUD_MODE @@ -120,10 +121,6 @@ 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 @@ -2726,6 +2723,8 @@ def get(self, request, repo_id, format=None): 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 54d255b6931..798ae2f1988 100644 --- a/seahub/base/accounts.py +++ b/seahub/base/accounts.py @@ -446,6 +446,11 @@ def can_publish_repo(self): return False return self._get_perm_by_roles('can_publish_repo') + + 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 9bc7a100471..cb0a8e91075 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): + + config_type = models.CharField(max_length=50) + repo_id = models.CharField(max_length=36, db_index=True) + 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 e632491da98..1cb69ab524f 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,21 @@ ".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 diff --git a/seahub/role_permissions/settings.py b/seahub/role_permissions/settings.py index 479ea6fd371..9bb948d595f 100644 --- a/seahub/role_permissions/settings.py +++ b/seahub/role_permissions/settings.py @@ -46,6 +46,7 @@ def merge_roles(default, custom): 'can_publish_repo': True, 'upload_rate_limit': 0, 'download_rate_limit': 0, + 'can_choose_office_suite': True, }, GUEST_USER: { 'can_add_repo': False, @@ -68,6 +69,7 @@ def merge_roles(default, custom): 'can_publish_repo': False, 'upload_rate_limit': 0, 'download_rate_limit': 0, + 'can_choose_office_suite': False, }, } diff --git a/seahub/settings.py b/seahub/settings.py index 1b3e9a0fa52..23886aee37d 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -892,6 +892,25 @@ def genpassword(): EX_PROPS_TABLE = '' EX_EDITABLE_COLUMNS = [] +############################# +# 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 = [] d = os.path.dirname EVENTS_CONFIG_FILE = os.environ.get( 'EVENTS_CONFIG_FILE', diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 37d702162be..d737b96ad80 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -150,6 +150,7 @@ canSetExProps: {% if can_set_ex_props %} true {% else %} false {% endif %}, enableSeaTableIntegration: {% if enable_seatable_integration %} true {% else %} false {% endif %}, isOrgContext: {% if org is not None %} true {% else %} false {% endif %}, + enableMultipleOfficeSuite: {% if user.permissions.can_choose_office_suite %} true {% else %} false {% endif %}, } }; diff --git a/seahub/urls.py b/seahub/urls.py index c6c348cb392..61c1c56c1ac 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -200,6 +200,7 @@ from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView from seahub.seadoc.views import sdoc_revision, sdoc_revisions, sdoc_to_docx from seahub.ocm.settings import OCM_ENDPOINT +from seahub.api2.endpoints.repo_office_suite import OfficeSuiteConfig from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \ LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken @@ -446,6 +447,7 @@ re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/upload-links/$', RepoUploadLinks.as_view(), name='api-v2.1-repo-upload-links'), 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})/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'), @@ -961,7 +963,7 @@ 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/file.py b/seahub/views/file.py index 556a46a6f04..73e6d2f8375 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -34,15 +34,16 @@ 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.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.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 @@ -67,6 +68,7 @@ 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 @@ -89,33 +91,34 @@ FILE_ENCODING_TRY_LIST, MEDIA_URL, SEAFILE_COLLAB_SERVER, ENABLE_WATERMARK, \ 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 + SHARE_LINK_EXPIRE_DAYS_DEFAULT, ENABLE_SHARE_LINK_REPORT_ABUSE, SEADOC_SERVER_URL, \ + 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 @@ -138,10 +141,62 @@ 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__) + +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. @@ -335,6 +390,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(): @@ -389,7 +446,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 @@ -486,6 +543,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 @@ -938,6 +997,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 @@ -1177,6 +1238,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 @@ -1411,6 +1474,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. diff --git a/seahub/wopi/settings.py b/seahub/wopi/settings.py index 303795045d4..f688bfe27c4 100644 --- a/seahub/wopi/settings.py +++ b/seahub/wopi/settings.py @@ -29,3 +29,24 @@ # 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 4f0a03a780b..9a59479b93f 100644 --- a/tests/seahub/role_permissions/test_utils.py +++ b/tests/seahub/role_permissions/test_utils.py @@ -11,4 +11,4 @@ def test_get_available_role(self): 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())) == 20 + assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 21