diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 0ff53692be..3462d8799b 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -272,6 +272,9 @@ class DirentListItem extends React.Component { case 'Open via Client': this.onOpenViaClient(); break; + case 'Convert with ONLYOFFICE': + this.onConvertWithONLYOFFICE(); + break; default: break; } @@ -365,6 +368,31 @@ class DirentListItem extends React.Component { location.href = url; } + onConvertWithONLYOFFICE = ()=> { + let repoID = this.props.repoID; + let user = username; + let fileUri = this.getDirentPath(this.props.dirent) + fetch(siteRoot+'onlyoffice/convert', { + method: 'POST', + body: JSON.stringify({ + username: user, + fileUri: fileUri, + repo_id: repoID, + }), + headers: { + 'Content-Type': 'application/json' + } + }).then(res => { + if(res.status >= 400) throw new Error() + //Replace with changes in the state + //like this one => this.addNodeToTree(name, parentPath, 'file'); + window.location.reload(); + }) + .catch(() => { + toaster.danger('Could not convert the file'); + }) + } + onItemDownload = (e) => { e.nativeEvent.stopImmediatePropagation(); let dirent = this.props.dirent; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 9c05e7aec9..7353861a67 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -82,6 +82,9 @@ export const enableTC = window.app.pageOptions.enableTC; export const enableVideoThumbnail = window.app.pageOptions.enableVideoThumbnail; +export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false; +export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || []; + // dtable export const workspaceID = window.app.pageOptions.workspaceID; export const showLogoutIcon = window.app.pageOptions.showLogoutIcon; diff --git a/frontend/src/utils/text-translation.js b/frontend/src/utils/text-translation.js index ae6481d880..bb63293cce 100644 --- a/frontend/src/utils/text-translation.js +++ b/frontend/src/utils/text-translation.js @@ -21,6 +21,7 @@ const TextTranslation = { 'ACCESS_LOG' : {key : 'Access Log', value : gettext('Access Log')}, 'TAGS': {key: 'Tags', value: gettext('Tags')}, 'RELATED_FILES': {key: 'Related Files', value: gettext('Related Files')}, + 'ONLYOFFICE_CONVERT': {key: 'Convert with ONLYOFFICE', value: gettext('Convert with ONLYOFFICE')} }; export default TextTranslation; \ No newline at end of file diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index ca99c48456..12735d9bb3 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -1,4 +1,4 @@ -import { mediaUrl, gettext, serviceURL, siteRoot, isPro, enableFileComment, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled } from './constants'; +import { mediaUrl, gettext, serviceURL, siteRoot, isPro, enableFileComment, fileAuditEnabled, canGenerateShareLink, canGenerateUploadLink, shareLinkPasswordMinLength, username, folderPermEnabled, onlyofficeConverterExtensions, enableOnlyoffice } from './constants'; import { strChineseFirstPY } from './pinyin-by-unicode'; import TextTranslation from './text-translation'; import React from 'react'; @@ -523,7 +523,7 @@ export const Utils = { getFileOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { let list = []; const { SHARE, DOWNLOAD, DELETE, RENAME, MOVE, COPY, TAGS, UNLOCK, LOCK, - COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT } = TextTranslation; + COMMENT, HISTORY, ACCESS_LOG, OPEN_VIA_CLIENT, ONLYOFFICE_CONVERT } = TextTranslation; const permission = dirent.permission; const { isCustomPermission, customPermission } = Utils.getUserPermission(permission); @@ -610,14 +610,24 @@ export const Utils = { list.push(HISTORY); } + if (permission == 'rw' && enableOnlyoffice && + onlyofficeConverterExtensions.includes(this.getFileExtension(dirent.name, false))) { + list.push(ONLYOFFICE_CONVERT); + } + // if the last item of menuList is ‘Divider’, delete the last item if (list[list.length - 1] === 'Divider') { list.pop(); } - return list; }, + getFileExtension: function (fileName, withoutDot) { + let parts = fileName.toLowerCase().split("."); + + return withoutDot ? parts.pop() : "." + parts.pop(); + }, + getDirentOperationList: function(isRepoOwner, currentRepoInfo, dirent, isContextmenu) { return dirent.type == 'dir' ? Utils.getFolderOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu) : diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index c712cbb8e0..8db5cee521 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -26,6 +26,7 @@ from seahub.settings import SEAFILE_VERSION, \ CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \ PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK +from seahub.onlyoffice.settings import ENABLE_ONLYOFFICE, ONLYOFFICE_CONVERTER_EXTENSIONS from seahub.constants import DEFAULT_ADMIN from seahub.utils import get_site_name, get_service_url from seahub.avatar.templatetags.avatar_tags import api_avatar_url @@ -133,6 +134,8 @@ def base(request): 'FILE_SERVER_ROOT': file_server_root, 'USE_GO_FILESERVER': seaserv.USE_GO_FILESERVER if hasattr(seaserv, 'USE_GO_FILESERVER') else False, 'LOGIN_URL': dj_settings.LOGIN_URL, + 'enableOnlyoffice': ENABLE_ONLYOFFICE, + 'onlyofficeConverterExtensions': ONLYOFFICE_CONVERTER_EXTENSIONS, 'thumbnail_size_for_original': THUMBNAIL_SIZE_FOR_ORIGINAL, 'enable_guest_invitation': ENABLE_GUEST_INVITATION, 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS, diff --git a/seahub/onlyoffice/converter.py b/seahub/onlyoffice/converter.py new file mode 100644 index 0000000000..8920028993 --- /dev/null +++ b/seahub/onlyoffice/converter.py @@ -0,0 +1,64 @@ +import logging +import requests + +from seahub.onlyoffice.converterUtils import getFileName, getFileExt +from seahub.onlyoffice.settings import ONLYOFFICE_CONVERTER_URL, ONLYOFFICE_JWT_SECRET, ONLYOFFICE_JWT_HEADER + +logger = logging.getLogger(__name__) + +def getConverterUri(docUri, fromExt, toExt, docKey, isAsync, filePass = None): + if not fromExt: + fromExt = getFileExt(docUri) + + title = getFileName(docUri) + + payload = { + 'url': docUri, + 'outputtype': toExt.replace('.', ''), + 'filetype': fromExt.replace('.', ''), + 'title': title, + 'key': docKey, + 'password': filePass + } + + headers={'accept': 'application/json'} + + if isAsync: + payload.setdefault('async', True) + + if ONLYOFFICE_JWT_SECRET: + import jwt + token = jwt.encode(payload, ONLYOFFICE_JWT_SECRET, algorithm='HS256') + headerToken = jwt.encode({'payload': payload}, ONLYOFFICE_JWT_SECRET, algorithm='HS256') + payload['token'] = token + headers[ONLYOFFICE_JWT_HEADER] = f'Bearer {headerToken}' + + response = requests.post(ONLYOFFICE_CONVERTER_URL, json=payload, headers=headers ) + json = response.json() + + return getResponseUri(json) + +def getResponseUri(json): + isEnd = json.get('endConvert') + error = json.get('error') + if error: + processError(error) + + if isEnd: + return json.get('fileUrl') + +def processError(error): + prefix = 'Error occurred in the ConvertService: ' + + mapping = { + '-8': f'{prefix}Error document VKey', + '-7': f'{prefix}Error document request', + '-6': f'{prefix}Error database', + '-5': f'{prefix}Incorrect password', + '-4': f'{prefix}Error download error', + '-3': f'{prefix}Error convertation error', + '-2': f'{prefix}Error convertation timeout', + '-1': f'{prefix}Error convertation unknown' + } + logger.error(f'[OnlyOffice] Converter URI Error Code: {error}') + raise Exception(mapping.get(str(error), f'Error Code: {error}')) \ No newline at end of file diff --git a/seahub/onlyoffice/converterUtils.py b/seahub/onlyoffice/converterUtils.py new file mode 100644 index 0000000000..8a2338c8dd --- /dev/null +++ b/seahub/onlyoffice/converterUtils.py @@ -0,0 +1,39 @@ +from seahub.onlyoffice.settings import EXT_DOCUMENT, \ + EXT_SPREADSHEET, EXT_PRESENTATION + +def getFileName(fileUri): + ind = fileUri.rfind('/') + return fileUri[ind+1:] + +def getFilePathWithoutName(fileUri): + ind = fileUri.rfind('/') + return fileUri[:ind] + +def getFileNameWithoutExt(fileUri): + fn = getFileName(fileUri) + ind = fn.rfind('.') + return fn[:ind] + +def getFileExt(fileUri): + fn = getFileName(fileUri) + ind = fn.rfind('.') + return fn[ind:].lower() + +def getFileType(fileUri): + ext = getFileExt(fileUri) + if ext in EXT_DOCUMENT: + return 'word' + if ext in EXT_SPREADSHEET: + return 'cell' + if ext in EXT_PRESENTATION: + return 'slide' + + return 'word' + +def getInternalExtension(fileType): + mapping = { + 'word': '.docx', + 'cell': '.xlsx', + 'slide': '.pptx' + } + return mapping.get(fileType, None) \ No newline at end of file diff --git a/seahub/onlyoffice/settings.py b/seahub/onlyoffice/settings.py index 23d401be47..94f645fa9b 100644 --- a/seahub/onlyoffice/settings.py +++ b/seahub/onlyoffice/settings.py @@ -3,13 +3,41 @@ from django.conf import settings ENABLE_ONLYOFFICE = getattr(settings, 'ENABLE_ONLYOFFICE', False) ONLYOFFICE_APIJS_URL = getattr(settings, 'ONLYOFFICE_APIJS_URL', '') +ONLYOFFICE_CONVERTER_URL = getattr(settings, 'ONLYOFFICE_CONVERTER_URL', '') ONLYOFFICE_FILE_EXTENSION = getattr(settings, 'ONLYOFFICE_FILE_EXTENSION', ()) ONLYOFFICE_EDIT_FILE_EXTENSION = getattr(settings, 'ONLYOFFICE_EDIT_FILE_EXTENSION', ()) VERIFY_ONLYOFFICE_CERTIFICATE = getattr(settings, 'VERIFY_ONLYOFFICE_CERTIFICATE', True) - +ONLYOFFICE_JWT_HEADER = getattr(settings, 'ONLYOFFICE_JWT_HEADER', 'Authorization') ONLYOFFICE_JWT_SECRET = getattr(settings, 'ONLYOFFICE_JWT_SECRET', '') - # if True, file will be saved when user click save btn on file editing page ONLYOFFICE_FORCE_SAVE = getattr(settings, 'ONLYOFFICE_FORCE_SAVE', False) - ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN = getattr(settings, 'ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN', False) + +ONLYOFFICE_CONVERTER_EXTENSIONS = [ + ".docm", ".doc", ".dotx", ".dotm", ".dot", ".odt", + ".fodt", ".ott", ".xlsm", ".xls", ".xltx", ".xltm", + ".xlt", ".ods", ".fods", ".ots", ".pptm", ".ppt", + ".ppsx", ".ppsm", ".pps", ".potx", ".potm", ".pot", + ".odp", ".fodp", ".otp", ".rtf", ".mht", ".html", ".htm", ".xml", ".epub", ".fb2" +] + +EXT_SPREADSHEET = [ + ".xls", ".xlsx", ".xlsm", + ".xlt", ".xltx", ".xltm", + ".ods", ".fods", ".ots", ".csv" +] + +EXT_PRESENTATION = [ + ".pps", ".ppsx", ".ppsm", + ".ppt", ".pptx", ".pptm", + ".pot", ".potx", ".potm", + ".odp", ".fodp", ".otp" +] + +EXT_DOCUMENT = [ + ".doc", ".docx", ".docm", + ".dot", ".dotx", ".dotm", + ".odt", ".fodt", ".ott", ".rtf", ".txt", + ".html", ".htm", ".mht", ".xml", + ".pdf", ".djvu", ".fb2", ".epub", ".xps" +] \ No newline at end of file diff --git a/seahub/onlyoffice/views.py b/seahub/onlyoffice/views.py index 14e3ceee7e..9801865ce6 100644 --- a/seahub/onlyoffice/views.py +++ b/seahub/onlyoffice/views.py @@ -7,10 +7,13 @@ import requests from django.core.cache import cache from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt -from seaserv import seafile_api +from django.shortcuts import render +from seaserv import seafile_api from seahub.onlyoffice.settings import VERIFY_ONLYOFFICE_CERTIFICATE -from seahub.onlyoffice.utils import generate_onlyoffice_cache_key +from seahub.onlyoffice.utils import generate_onlyoffice_cache_key, get_onlyoffice_dict +from seahub.onlyoffice.converterUtils import getFileNameWithoutExt, getFileExt, getFileType, getInternalExtension, getFilePathWithoutName +from seahub.onlyoffice.converter import getConverterUri from seahub.utils import gen_inner_file_upload_url, is_pro_version from seahub.utils.file_op import if_locked_by_online_office @@ -161,3 +164,70 @@ def onlyoffice_editor_callback(request): seafile_api.unlock_file(repo_id, file_path) return HttpResponse('{"error": 0}') + +@csrf_exempt +def onlyoffice_convert(request): + + if request.method != 'POST': + logger.error('Request method if not POST.') + return render(request, '404.html') + + body = json.loads(request.body) + + username = body.get('username') + fileUri = body.get('fileUri') + filePass = body.get('filePass') or None + repo_id = body.get('repo_id') + folderName = getFilePathWithoutName(fileUri)+'/' + fileExt = getFileExt(fileUri) + fileType = getFileType(fileUri) + newExt = getInternalExtension(fileType) + + if(not newExt): + logger.error('[OnlyOffice] Could not generate internal extension.') + return HttpResponse(status=500) + + doc_dic = get_onlyoffice_dict(request, username, repo_id, fileUri) + + + downloadUri = doc_dic["doc_url"] + key = doc_dic["doc_key"] + + newUri = getConverterUri(downloadUri, fileExt, newExt, key, False, filePass) + + if(not newUri): + logger.error('[OnlyOffice] No response from file converter.') + return HttpResponse(status=500) + + onlyoffice_resp = requests.get(newUri, verify=VERIFY_ONLYOFFICE_CERTIFICATE) + + if not onlyoffice_resp: + logger.error('[OnlyOffice] No response from file content url.') + return HttpResponse(status=500) + + fake_obj_id = {'online_office_update': True} + + update_token = seafile_api.get_fileserver_access_token(repo_id, + json.dumps(fake_obj_id), + 'update', + username) + + if not update_token: + logger.error('[OnlyOffice] No fileserver access token.') + return HttpResponse(status=500) + + + fileName = getFileNameWithoutExt(fileUri)+newExt + + seafile_api.post_empty_file(repo_id, folderName, fileName, username) + + files = { + 'file': onlyoffice_resp.content, + 'target_file': folderName+fileName, + } + + update_url = gen_inner_file_upload_url('update-api', update_token) + + requests.post(update_url, files=files) + + return HttpResponse(status=200) diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 824a6c1c92..ec81d94774 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -135,7 +135,9 @@ showLogoutIcon: {% if show_logout_icon %} true {% else %} false {% endif %}, additionalShareDialogNote: {% if additional_share_dialog_note %} {{ additional_share_dialog_note|safe }} {% else %} null {% endif %}, additionalAppBottomLinks: {% if additional_app_bottom_links %} {{ additional_app_bottom_links|safe }} {% else %} null {% endif %}, - additionalAboutDialogLinks: {% if additional_about_dialog_links %} {{ additional_about_dialog_links|safe }} {% else %} null {% endif %} + additionalAboutDialogLinks: {% if additional_about_dialog_links %} {{ additional_about_dialog_links|safe }} {% else %} null {% endif %}, + enableOnlyoffice: {% if enableOnlyoffice %} true {% else %} false {% endif %}, + onlyofficeConverterExtensions: {% if onlyofficeConverterExtensions %} {{onlyofficeConverterExtensions|safe}} {% else %} null {% endif %} } }; diff --git a/seahub/urls.py b/seahub/urls.py index e4ecc81ca9..f0564ef948 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -866,8 +866,10 @@ if getattr(settings, 'ENABLE_ADFS_LOGIN', False): if getattr(settings, 'ENABLE_ONLYOFFICE', False): from seahub.onlyoffice.views import onlyoffice_editor_callback + from seahub.onlyoffice.views import onlyoffice_convert urlpatterns += [ url(r'^onlyoffice/editor-callback/$', onlyoffice_editor_callback, name='onlyoffice_editor_callback'), + url(r'^onlyoffice/convert', onlyoffice_convert, name='onlyoffice_convert') ] if getattr(settings, 'ENABLE_BISHENG_OFFICE', False):