diff --git a/src/bridge/__init__.py b/src/bridge/__init__.py index bf794690..7d2226e4 100644 --- a/src/bridge/__init__.py +++ b/src/bridge/__init__.py @@ -169,9 +169,9 @@ def appopen(): WB.openfiles[wopisrc]['toclose'] = wopilock['toclose'] else: WB.openfiles[wopisrc] = {'acctok': acctok, 'tosave': False, - 'lastsave': int(time.time()) - WB.saveinterval, - 'toclose': {acctok[-20:]: False}, - 'docid': wopilock['docid'], + 'lastsave': int(time.time()) - WB.saveinterval, + 'toclose': {acctok[-20:]: False}, + 'docid': wopilock['docid'], } # also clear any potential stale response for this document try: @@ -203,6 +203,8 @@ def appsave(docid): wopisrc = meta[:meta.index('?t=')] acctok = meta[meta.index('?t=')+3:] isclose = flask.request.args.get('close') == 'true' + if not docid: + raise ValueError WB.log.info('msg="Save: requested action" isclose="%s" docid="%s" wopisrc="%s" token="%s"' % (isclose, docid, wopisrc, acctok[-20:])) except (KeyError, ValueError) as e: diff --git a/src/core/discovery.py b/src/core/discovery.py index 0829ffd1..57f9ef6e 100644 --- a/src/core/discovery.py +++ b/src/core/discovery.py @@ -15,7 +15,7 @@ # convenience references to global entities srv = None log = None - +apps = {} def registerapp(appname, appurl, appinturl): '''Registers the given app in the internal endpoints list''' @@ -34,21 +34,21 @@ def registerapp(appname, appurl, appinturl): urlsrc = discXml.find('net-zone/app')[0].attrib['urlsrc'] if urlsrc.find('loleaflet') > 0: # this is Collabora - srv.apps[appname] = {} + apps[appname] = {} codetypes = srv.config.get('general', 'codeofficetypes', fallback='.odt .ods .odp').split() for t in codetypes: srv.endpoints[t] = {} srv.endpoints[t]['view'] = urlsrc + 'permission=readonly' srv.endpoints[t]['edit'] = urlsrc + 'permission=edit' srv.endpoints[t]['new'] = urlsrc + 'permission=edit' # pylint: disable=bad-whitespace - srv.apps[appname][t] = srv.endpoints[t] + apps[appname][t] = srv.endpoints[t] log.info('msg="Collabora Online endpoints successfully configured" count="%d" CODEURL="%s"' % (len(codetypes), srv.endpoints['.odt']['edit'])) - return flask.Response(json.dumps(list(srv.apps[appname].keys())), mimetype='application/json') + return flask.Response(json.dumps(list(apps[appname].keys())), mimetype='application/json') # else this must be Microsoft Office Online # TODO remove hardcoded logic - srv.apps[appname] = {} + apps[appname] = {} srv.endpoints['.docx'] = {} srv.endpoints['.docx']['view'] = appurl + '/wv/wordviewerframe.aspx?edit=0' srv.endpoints['.docx']['edit'] = appurl + '/we/wordeditorframe.aspx?edit=1' @@ -61,12 +61,12 @@ def registerapp(appname, appurl, appinturl): srv.endpoints['.pptx']['view'] = appurl + '/p/PowerPointFrame.aspx?PowerPointView=ReadingView' srv.endpoints['.pptx']['edit'] = appurl + '/p/PowerPointFrame.aspx?PowerPointView=EditView' srv.endpoints['.pptx']['new'] = appurl + '/p/PowerPointFrame.aspx?PowerPointView=EditView&New=1' # pylint: disable=bad-whitespace - srv.apps[appname]['.docx'] = srv.endpoints['.docx'] - srv.apps[appname]['.xlsx'] = srv.endpoints['.xlsx'] - srv.apps[appname]['.pptx'] = srv.endpoints['.pptx'] + apps[appname]['.docx'] = srv.endpoints['.docx'] + apps[appname]['.xlsx'] = srv.endpoints['.xlsx'] + apps[appname]['.pptx'] = srv.endpoints['.pptx'] log.info('msg="Microsoft Office Online endpoints successfully configured" OfficeURL="%s"' % srv.endpoints['.docx']['edit']) - return flask.Response(json.dumps(list(srv.apps[appname].keys())), mimetype='application/json') + return flask.Response(json.dumps(list(apps[appname].keys())), mimetype='application/json') elif discReq.status_code == http.client.NOT_FOUND: # try and scrape the app homepage to see if a bridge-supported app is found @@ -75,7 +75,7 @@ def registerapp(appname, appurl, appinturl): if discReq.find('CodiMD') > 0: # TODO remove hardcoded logic bridge.WB.loadplugin(appname, appurl, appinturl) - srv.apps[appname] = {} + apps[appname] = {} bridgeurl = srv.config.get('general', 'wopiurl') + '/wopi/bridge/open?' srv.endpoints['.md'] = {} srv.endpoints['.md']['view'] = srv.endpoints['.md']['edit'] = bridgeurl @@ -83,22 +83,22 @@ def registerapp(appname, appurl, appinturl): srv.endpoints['.zmd']['view'] = srv.endpoints['.zmd']['edit'] = bridgeurl srv.endpoints['.txt'] = {} srv.endpoints['.txt']['view'] = srv.endpoints['.txt']['edit'] = bridgeurl - srv.apps[appname]['.md'] = srv.endpoints['.md'] - srv.apps[appname]['.zmd'] = srv.endpoints['.zmd'] - srv.apps[appname]['.txt'] = srv.endpoints['.txt'] + apps[appname]['.md'] = srv.endpoints['.md'] + apps[appname]['.zmd'] = srv.endpoints['.zmd'] + apps[appname]['.txt'] = srv.endpoints['.txt'] log.info('msg="iopRegisterApp: CodiMD endpoints successfully configured" BridgeURL="%s"' % bridgeurl) - return flask.Response(json.dumps(list(srv.apps[appname].keys())), mimetype='application/json') + return flask.Response(json.dumps(list(apps[appname].keys())), mimetype='application/json') if discReq.find('Etherpad') > 0: bridge.WB.loadplugin(appname, appurl, appinturl) bridgeurl = srv.config.get('general', 'wopiurl') + '/wopi/bridge/open?' # TODO remove hardcoded logic - srv.apps[appname] = {} + apps[appname] = {} srv.endpoints['.epd'] = {} srv.endpoints['.epd']['view'] = srv.endpoints['.epd']['edit'] = bridgeurl - srv.apps[appname]['.epd'] = srv.endpoints['.epd'] + apps[appname]['.epd'] = srv.endpoints['.epd'] log.info('msg="iopRegisterApp: Etherpad endpoints successfully configured" BridgeURL="%s"' % bridgeurl) - return flask.Response(json.dumps(list(srv.apps[appname].keys())), mimetype='application/json') + return flask.Response(json.dumps(list(apps[appname].keys())), mimetype='application/json') except ValueError: # bridge plugin could not be initialized return 'Failed to initialize WOPI bridge plugin for app "%s"' % appname, http.client.INTERNAL_SERVER_ERROR diff --git a/src/core/wopi.py b/src/core/wopi.py index 69424f85..ada74c61 100644 --- a/src/core/wopi.py +++ b/src/core/wopi.py @@ -15,6 +15,7 @@ from urllib.parse import quote_plus as url_quote_plus from urllib.parse import unquote as url_unquote import core.wopiutils as utils +import core.discovery # convenience references to global entities st = None @@ -65,19 +66,23 @@ def checkFileInfo(fileid): filemd['SupportsUpdate'] = filemd['UserCanWrite'] = filemd['SupportsLocks'] = filemd['SupportsRename'] = \ filemd['SupportsDeleteFile'] = filemd['UserCanRename'] = acctok['viewmode'] == utils.ViewMode.READ_WRITE filemd['UserCanNotWriteRelative'] = acctok['viewmode'] != utils.ViewMode.READ_WRITE + if acctok['appname'] in core.discovery.apps: + appurl = core.discovery.apps[acctok['appname']][fExt] + else: + appurl = srv.endpoints[fExt] # TODO deprecated, must make sure appname is always correct + filemd['HostViewUrl'] = '%s&%s' % (appurl['view'], wopiSrc) + filemd['HostEditUrl'] = '%s&%s' % (appurl['edit'], wopiSrc) + # populate app-specific metadata - # the following properties are only used by MS Office Online - if fExt in ['.docx', '.xlsx', '.pptx']: - filemd['HostViewUrl'] = '%s&%s' % (srv.endpoints[fExt]['view'], wopiSrc) - filemd['HostEditUrl'] = '%s&%s' % (srv.endpoints[fExt]['edit'], wopiSrc) + if acctok['appname'].find('Microsoft') > 0: # the following actions are broken in MS Office Online, therefore they are disabled filemd['SupportsRename'] = filemd['UserCanRename'] = False - # the following is to enable the 'Edit in Word/Excel/PowerPoint' (desktop) action (probably broken) - try: - filemd['ClientUrl'] = srv.config.get('general', 'webdavurl') + '/' + acctok['filename'] - except configparser.NoOptionError: - # if no WebDAV URL is provided, ignore this setting - pass + # the following is to enable the 'Edit in Word/Excel/PowerPoint' (desktop) action (probably broken) + try: + filemd['ClientUrl'] = srv.config.get('general', 'webdavurl') + '/' + acctok['filename'] + except configparser.NoOptionError: + # if no WebDAV URL is provided, ignore this setting + pass # extensions for Collabora Online filemd['EnableOwnerTermination'] = True filemd['DisableExport'] = filemd['DisableCopy'] = filemd['DisablePrint'] = acctok['viewmode'] == utils.ViewMode.VIEW_ONLY @@ -305,18 +310,22 @@ def putRelative(fileid, reqheaders, acctok): log.info('msg="PutRelative: generating new access token" user="%s" filename="%s" ' \ 'mode="ViewMode.READ_WRITE" friendlyname="%s"' % (acctok['userid'], targetName, acctok['username'])) - inode, newacctok = utils.generateAccessToken(acctok['userid'], targetName, utils.ViewMode.READ_WRITE, acctok['username'], \ - acctok['folderurl'], acctok['endpoint'], acctok['appname']) + inode, _, newacctok = utils.generateAccessToken(acctok['userid'], targetName, utils.ViewMode.READ_WRITE, acctok['username'], \ + acctok['folderurl'], acctok['endpoint'], acctok['appname']) # prepare and send the response as JSON putrelmd = {} putrelmd['Name'] = os.path.basename(targetName) putrelmd['Url'] = '%s?access_token=%s' % (url_unquote(utils.generateWopiSrc(inode)), newacctok) fExt = os.path.splitext(targetName)[1] - if fExt in srv.endpoints: + appurl = None + if acctok['appname'] in core.discovery.apps: + appurl = core.discovery.apps[acctok['appname']][fExt] + elif fExt in srv.endpoints: + appurl = srv.endpoints[fExt] # TODO deprecated + if appurl: putrelmd['HostEditUrl'] = '%s&WOPISrc=%s&access_token=%s' % \ - (srv.endpoints[fExt]['edit'], \ - utils.generateWopiSrc(inode), newacctok) - #else we don't know the app to edit this file type, therefore we do not provide the info + (appurl['edit'], utils.generateWopiSrc(inode), newacctok) + # else we don't know the app to edit this file type, therefore we do not provide the info log.debug('msg="PutRelative response" token="%s" metadata="%s"' % (newacctok[-20:], putrelmd)) return flask.Response(json.dumps(putrelmd), mimetype='application/json') diff --git a/src/core/wopiutils.py b/src/core/wopiutils.py index 0f9a2549..6a5461c0 100644 --- a/src/core/wopiutils.py +++ b/src/core/wopiutils.py @@ -128,14 +128,15 @@ def generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, # if write access is requested, probe whether there's already a lock file coming from Desktop applications exptime = int(time.time()) + srv.tokenvalidity acctok = jwt.encode({'userid': userid, 'filename': statInfo['filepath'], 'username': username, - 'viewmode': viewmode.value, 'folderurl': folderurl, 'exp': exptime, 'endpoint': endpoint}, + 'viewmode': viewmode.value, 'folderurl': folderurl, 'endpoint': endpoint, + 'appname': appname, 'exp': exptime}, srv.wopisecret, algorithm='HS256') log.info('msg="Access token generated" userid="%s" mode="%s" endpoint="%s" filename="%s" inode="%s" ' \ - 'mtime="%s" folderurl="%s" expiration="%d" token="%s"' % + 'mtime="%s" folderurl="%s" appname="%s" expiration="%d" token="%s"' % (userid, viewmode, endpoint, statInfo['filepath'], statInfo['inode'], statInfo['mtime'], \ - folderurl, exptime, acctok[-20:])) - # return the inode == fileid and the access token - return statInfo['inode'], acctok + folderurl, appname, exptime, acctok[-20:])) + # return the inode == fileid, the filepath and the access token + return statInfo['inode'], statInfo['filepath'], acctok def getLockName(filename): diff --git a/src/wopiserver.py b/src/wopiserver.py index 6abe993f..32af6c06 100755 --- a/src/wopiserver.py +++ b/src/wopiserver.py @@ -69,7 +69,6 @@ class Wopi: log = utils.JsonLogger(app.logger) openfiles = {} endpoints = {} - apps = {} @classmethod def init(cls): @@ -256,12 +255,12 @@ def iopOpen(): endpoint = req.args.get('endpoint', 'default') appname = urllib.parse.unquote(req.args.get('appname', 'default')) try: - inode, acctok = utils.generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, appname) + inode, fname, acctok = utils.generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, appname) # generate the URL-encoded payload for the app engine url = '%s&access_token=%s' % (utils.generateWopiSrc(inode), acctok) # no need to URL-encode the JWT token - if appname == '': + if appname not in core.discovery.apps: return url - return Wopi.apps[appname][os.path.splitext(filename)[1]]['edit' if viewmode == utils.ViewMode.READ_WRITE else 'view'] + return flask.redirect('%s&WOPISrc=%s' % (core.discovery.apps[appname][os.path.splitext(fname)[1]]['edit' if viewmode == utils.ViewMode.READ_WRITE else 'view'], url)) except IOError as e: Wopi.log.info('msg="iopOpen: remote error on generating token" client="%s" user="%s" ' \ 'friendlyname="%s" mode="%s" endpoint="%s" reason="%s"' % @@ -339,7 +338,7 @@ def iopDiscoverApp(): # -# The WOPI protocol implementation starts here +# WOPI protocol implementation # @Wopi.app.route("/wopi/files/", methods=['GET']) def wopiCheckFileInfo(fileid): @@ -464,11 +463,13 @@ def bridgeOpen(): @Wopi.app.route("/wopi/bridge/", methods=["POST"]) +@Wopi.metrics.do_not_track() def bridgeSave(docid): return bridge.appsave(docid) @Wopi.app.route("/wopi/bridge/save", methods=["GET"]) +@Wopi.metrics.do_not_track() def bridgeSave_old(): docid = flask.request.args.get('id') return bridge.appsave(docid) @@ -517,5 +518,5 @@ def cboxDownload(): # if __name__ == '__main__': Wopi.init() - core.discovery.initappsregistry() + core.discovery.initappsregistry() # TODO to be removed Wopi.run()