Skip to content

Commit

Permalink
Alternate model for discovery
Browse files Browse the repository at this point in the history
See cs3org/reva#1779 for more details

The concept is that WOPI will not be responsible for the discovery,
and the functionality is implemented in the WOPI driver of the Reva
AppProvider. Eventually, the discovery.py module will be dropped.
  • Loading branch information
glpatcern committed Jun 24, 2021
1 parent 9e65a56 commit da35236
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 104 deletions.
26 changes: 6 additions & 20 deletions src/core/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Helper code for the WOPI discovery phase, as well as
for integrating the apps supported by the bridge functionality.
This code is going to be deprecated once the new Reva AppProvider is fully functional.
'''

from xml.etree import ElementTree as ET
Expand All @@ -15,7 +16,6 @@
# convenience references to global entities
srv = None
log = None
apps = {}

def registerapp(appname, appurl, appinturl):
'''Registers the given app in the internal endpoints list'''
Expand All @@ -34,21 +34,17 @@ def registerapp(appname, appurl, appinturl):
urlsrc = discXml.find('net-zone/app')[0].attrib['urlsrc']
if urlsrc.find('loleaflet') > 0:
# this is Collabora
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
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(apps[appname].keys())), mimetype='application/json')
return

# else this must be Microsoft Office Online
# TODO remove hardcoded logic
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'
Expand All @@ -61,12 +57,9 @@ 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
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(apps[appname].keys())), mimetype='application/json')
return

elif discReq.status_code == http.client.NOT_FOUND:
# try and scrape the app homepage to see if a bridge-supported app is found
Expand All @@ -75,39 +68,32 @@ def registerapp(appname, appurl, appinturl):
if discReq.find('CodiMD') > 0:
# TODO remove hardcoded logic
bridge.WB.loadplugin(appname, appurl, appinturl)
apps[appname] = {}
bridgeurl = srv.config.get('general', 'wopiurl') + '/wopi/bridge/open?'
srv.endpoints['.md'] = {}
srv.endpoints['.md']['view'] = srv.endpoints['.md']['edit'] = bridgeurl
srv.endpoints['.zmd'] = {}
srv.endpoints['.zmd']['view'] = srv.endpoints['.zmd']['edit'] = bridgeurl
srv.endpoints['.txt'] = {}
srv.endpoints['.txt']['view'] = srv.endpoints['.txt']['edit'] = bridgeurl
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(apps[appname].keys())), mimetype='application/json')
return

if discReq.find('Etherpad') > 0:
bridge.WB.loadplugin(appname, appurl, appinturl)
bridgeurl = srv.config.get('general', 'wopiurl') + '/wopi/bridge/open?'
# TODO remove hardcoded logic
apps[appname] = {}
srv.endpoints['.epd'] = {}
srv.endpoints['.epd']['view'] = srv.endpoints['.epd']['edit'] = bridgeurl
apps[appname]['.epd'] = srv.endpoints['.epd']
log.info('msg="iopRegisterApp: Etherpad endpoints successfully configured" BridgeURL="%s"' % bridgeurl)
return flask.Response(json.dumps(list(apps[appname].keys())), mimetype='application/json')
return
except ValueError:
# bridge plugin could not be initialized
return 'Failed to initialize WOPI bridge plugin for app "%s"' % appname, http.client.INTERNAL_SERVER_ERROR
pass
except requests.exceptions.ConnectionError:
pass

# in all other cases, fail
log.error('msg="iopRegisterApp: app is not WOPI-compatible" appurl="%s"' % appurl)
return 'App is not WOPI-compatible', http.client.BAD_REQUEST


def initappsregistry():
Expand Down
3 changes: 2 additions & 1 deletion src/core/readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## WOPI server - core module

This module includes the core WOPI protocol implementation, along with the discovery logic
in the `discovery.py` module and the interoperable lock APIs in the `ioplocks.py` module.
in the `discovery.py` module (to be moved to Reva) and the interoperable lock APIs
in the `ioplocks.py` module.

To access the storage, three interfaces are provided:

Expand Down
26 changes: 7 additions & 19 deletions src/core/wopi.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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
Expand Down Expand Up @@ -66,12 +65,8 @@ 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)
filemd['HostViewUrl'] = '%s&%s' % (acctok['appviewurl'], wopiSrc)
filemd['HostEditUrl'] = '%s&%s' % (acctok['appediturl'], wopiSrc)

# populate app-specific metadata
if acctok['appname'].find('Microsoft') > 0:
Expand Down Expand Up @@ -310,22 +305,15 @@ 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'], \
acctok['appediturl'], acctok['appviewurl'])
# 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]
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' % \
(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
putrelmd['HostEditUrl'] = '%s&WOPISrc=%s&access_token=%s' % \
(acctok['appediturl'], utils.generateWopiSrc(inode), newacctok)
log.debug('msg="PutRelative response" token="%s" metadata="%s"' % (newacctok[-20:], putrelmd))
return flask.Response(json.dumps(putrelmd), mimetype='application/json')

Expand Down
17 changes: 11 additions & 6 deletions src/core/wopiutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,33 @@ def randomString(size):
return ''.join([choice(ascii_lowercase) for _ in range(size)])


def generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, appname):
def generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, appname, appediturl, appviewurl):
'''Generates an access token for a given file and a given user, and returns a tuple with
the file's inode and the URL-encoded access token.'''
try:
# stat the file to check for existence and get a version-invariant inode and modification time:
# the inode serves as fileid (and must not change across save operations), the mtime is used for version information.
statInfo = st.statx(endpoint, fileid, userid, versioninv=1)
statinfo = st.statx(endpoint, fileid, userid, versioninv=1)
except IOError as e:
log.info('msg="Requested file not found or not a file" fileid="%s" error="%s"' % (fileid, e))
raise
# 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,
if not appediturl:
# for backwards compatibility
fext = os.path.splitext(statinfo['filepath'])[1]
appediturl = srv.endpoints[fext]['edit']
appviewurl = srv.endpoints[fext]['view']
acctok = jwt.encode({'userid': userid, 'filename': statinfo['filepath'], 'username': username,
'viewmode': viewmode.value, 'folderurl': folderurl, 'endpoint': endpoint,
'appname': appname, 'exp': exptime},
'appname': appname, 'appediturl': appediturl, 'appviewurl': appviewurl, '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" appname="%s" expiration="%d" token="%s"' %
(userid, viewmode, endpoint, statInfo['filepath'], statInfo['inode'], statInfo['mtime'], \
(userid, viewmode, endpoint, statinfo['filepath'], statinfo['inode'], statinfo['mtime'], \
folderurl, appname, exptime, acctok[-20:]))
# return the inode == fileid, the filepath and the access token
return statInfo['inode'], statInfo['filepath'], acctok
return statinfo['inode'], acctok


def getLockName(filename):
Expand Down
Loading

0 comments on commit da35236

Please sign in to comment.