Example #1
0
 def savedirty(self, openfile, wopisrc):
     '''save documents that are dirty for more than `saveinterval` or that are being closed'''
     wopilock = None
     if openfile['tosave'] and (
             _intersection(openfile['toclose']) or
         (openfile['lastsave'] < time.time() - WB.saveinterval)):
         try:
             wopilock = wopi.getlock(wopisrc, openfile['acctok'])
         except wopi.InvalidLock:
             WB.log.info(
                 'msg="SaveThread: attempting to relock file" token="%s" docid="%s"'
                 % (openfile['acctok'][-20:], openfile['docid']))
             try:
                 wopilock = WB.saveresponses[wopisrc] = wopi.relock(
                     wopisrc, openfile['acctok'], openfile['docid'],
                     _intersection(openfile['toclose']))
             except wopi.InvalidLock as ile:
                 # even this attempt failed, give up
                 # TODO here we should save the file on a local storage to help later recovery
                 WB.saveresponses[wopisrc] = codimd.jsonify(
                     str(ile)), http.client.INTERNAL_SERVER_ERROR
                 # set some 'fake' metadata, will be automatically cleaned up later
                 openfile['lastsave'] = int(time.time())
                 openfile['tosave'] = False
                 openfile['toclose'] = {'invalid-lock': True}
                 return None
         WB.log.info('msg="SaveThread: saving file" token="%s" docid="%s"' %
                     (openfile['acctok'][-20:], openfile['docid']))
         WB.saveresponses[wopisrc] = codimd.savetostorage(
             wopisrc, openfile['acctok'],
             _intersection(openfile['toclose']), wopilock)
         openfile['lastsave'] = int(time.time())
         openfile['tosave'] = False
     return wopilock
Example #2
0
    def cleanup(self, openfile, wopisrc, wopilock):
        '''remove state for closed documents after some time'''
        if _union(openfile['toclose']) and not openfile['tosave']:
            # check lock
            try:
                wopilock = wopi.getlock(
                    wopisrc, openfile['acctok']) if not wopilock else wopilock
            except wopi.InvalidLock:
                # nothing to do here, this document may have been closed by another wopibridge
                if openfile['lastsave'] < time.time() - WB.unlockinterval:
                    # yet cleanup only after the unlockinterval time, cf. the InvalidLock handling in savedirty()
                    WB.log.info(
                        'msg="SaveThread: cleaning up metadata, file already unlocked" url="%s"'
                        % wopisrc)
                    del WB.openfiles[wopisrc]
                return

            # reconcile list of toclose tokens
            openfile['toclose'] = {
                t: wopilock['toclose'][t]
                or (t in openfile['toclose'] and openfile['toclose'][t])
                for t in wopilock['toclose']
            }
            if _intersection(openfile['toclose']):
                if openfile['lastsave'] < int(time.time()) - WB.unlockinterval:
                    # nobody is still on this document and some time has passed, unlock
                    res = wopi.request(wopisrc,
                                       openfile['acctok'],
                                       'POST',
                                       headers={
                                           'X-WOPI-Lock': json.dumps(wopilock),
                                           'X-Wopi-Override': 'UNLOCK'
                                       })
                    if res.status_code != http.client.OK:
                        WB.log.warning(
                            'msg="SaveThread: failed to unlock" lastsavetime="%s" token="%s" response="%s"'
                            % (openfile['lastsave'], openfile['acctok'][-20:],
                               res.status_code))
                    else:
                        WB.log.info(
                            'msg="SaveThread: unlocked document" lastsavetime="%s" token="%s"'
                            % (openfile['lastsave'], openfile['acctok'][-20:]))
                    del WB.openfiles[wopisrc]
            elif openfile['toclose'] != wopilock['toclose']:
                # some user still on it, refresh lock if the toclose part has changed
                wopi.refreshlock(wopisrc,
                                 openfile['acctok'],
                                 wopilock,
                                 toclose=openfile['toclose'])
Example #3
0
 def closewhenidle(self, openfile, wopisrc, wopilock):
     '''close and unlock documents tha are idle for more than 60 minutes.
     They will transparently be relocked when/if the session resumes, but we seem to miss some close notifications,
     therefore this also works as a cleanup step'''
     if openfile['lastsave'] < int(time.time()) - 3600:
         try:
             wopilock = wopi.getlock(
                 wopisrc, openfile['acctok']) if not wopilock else wopilock
             # this will force a close in the cleanup step
             openfile['toclose'] = {t: True for t in openfile['toclose']}
             WB.log.info(
                 'msg="SaveThread: force-closing document" lastsavetime="%s" toclosetokens="%s"'
                 % (openfile['lastsave'], openfile['toclose']))
         except wopi.InvalidLock:
             # lock is gone, just cleanup our metadata
             WB.log.warning(
                 'msg="SaveThread: cleaning up metadata, detected missed close event" url="%s"'
                 % wopisrc)
             del WB.openfiles[wopisrc]
     return wopilock
Example #4
0
def savethread_do():
    '''Perform the pending save to storage operations'''
    WB.log.info('msg="Savethread starting"')
    while WB.active:
        with WB.savecv:
            # sleep for one minute or until awaken
            WB.savecv.wait(60)
            if not WB.active:
                break

            # execute a round of sync to storage; list is needed as we may delete entries from the dict
            for wopisrc, openfile in list(WB.openfiles.items()):
                try:
                    wopilock = None
                    # save documents that are dirty for more than `saveinterval` or that are being closed
                    if openfile['tosave'] and (
                            _intersection(openfile['toclose']) or
                        (openfile['lastsave'] <
                         time.time() - WB.saveinterval)):
                        wopilock = wopi.getlock(wopisrc, openfile['acctok'])
                        WB.saveresponses[wopisrc] = codimd.codimdtostorage(
                            wopisrc, openfile['acctok'],
                            _intersection(openfile['toclose']), wopilock)
                        openfile['lastsave'] = int(time.time())
                        openfile['tosave'] = False

                    # refresh locks of open idle documents every 30 minutes
                    if openfile['lastsave'] < time.time() - (1800 +
                                                             WB.saveinterval):
                        wopilock = wopi.getlock(
                            wopisrc, openfile['acctok'],
                            raiseifmissing=False) if not wopilock else wopilock
                        if not wopilock:
                            # not a problem here, just forget this document (may have been closed by another wopibridge)
                            WB.log.debug(
                                'msg="Savethread: cleaning up metadata" url="%s"'
                                % wopisrc)
                            del WB.openfiles[wopisrc]
                            continue
                        wopilock = wopi.refreshlock(wopisrc,
                                                    openfile['acctok'],
                                                    wopilock)
                        # in case we get soon a save callback, we want to honor it immediately
                        openfile['lastsave'] = int(
                            time.time()) - WB.saveinterval

                    # remove state for closed documents after some time
                    if _union(openfile['toclose']) and not openfile['tosave']:
                        # check lock
                        wopilock = wopi.getlock(
                            wopisrc, openfile['acctok'],
                            raiseifmissing=False) if not wopilock else wopilock
                        if not wopilock:
                            # not a problem here, just forget this document like above
                            WB.log.debug(
                                'msg="Savethread: cleaning up metadata" url="%s"'
                                % wopisrc)
                            del WB.openfiles[wopisrc]
                            continue
                        # refresh state
                        openfile['toclose'] = {
                            t: wopilock['toclose'][t]
                            or t not in openfile['toclose']
                            or openfile['toclose'][t]
                            for t in wopilock['toclose']
                        }
                        if _intersection(openfile['toclose']
                                         ) and openfile['lastsave'] <= int(
                                             time.time()) - WB.saveinterval:
                            # nobody is still on this document and some time has passed, unlock
                            res = wopi.request(wopisrc,
                                               openfile['acctok'],
                                               'POST',
                                               headers={
                                                   'X-WOPI-Lock':
                                                   json.dumps(wopilock),
                                                   'X-Wopi-Override':
                                                   'UNLOCK'
                                               })
                            if res.status_code != http.client.OK:
                                WB.log.warning(
                                    'msg="Savethread: calling WOPI Unlock failed" lastsavetime="%s" token="%s" response="%s"'
                                    % (openfile['lastsave'],
                                       openfile['acctok'][-20:],
                                       res.status_code))
                            else:
                                WB.log.info(
                                    'msg="Savethread: unlocked document" lastsavetime="%s" token="%s"'
                                    % (openfile['lastsave'],
                                       openfile['acctok'][-20:]))
                            del WB.openfiles[wopisrc]
                        else:
                            # some user still on it or last operation happened not long ago, just refresh lock
                            wopi.refreshlock(wopisrc,
                                             openfile['acctok'],
                                             wopilock,
                                             toclose=openfile['toclose'])

                except wopi.InvalidLock as e:
                    # WOPI lock got lost, this is fatal
                    WB.saveresponses[wopisrc] = codimd.jsonify('Missing or malformed lock when saving the file. %s' % codimd.RECOVER_MSG), \
                                                http.client.NOT_FOUND
                    del WB.openfiles[wopisrc]

                except Exception as e:  # pylint: disable=broad-except
                    ex_type, ex_value, ex_traceback = sys.exc_info()
                    WB.log.error(
                        'msg="Savethread: unexpected exception caught" exception="%s" type="%s" traceback="%s"'
                        % (e, ex_type,
                           traceback.format_exception(ex_type, ex_value,
                                                      ex_traceback)))
    WB.log.info('msg="Savethread terminated, shutting down"')
Example #5
0
def appopen():
    '''Open a MD doc by contacting the provided WOPISrc with the given access_token'''
    try:
        wopisrc = urllib.parse.unquote(flask.request.args['WOPISrc'])
        acctok = flask.request.args['access_token']
        WB.log.info('msg="Open called" client="%s" token="%s"' %
                    (flask.request.remote_addr, acctok[-20:]))
    except KeyError as e:
        WB.log.error(
            'msg="Open: unable to open the file, missing WOPI context" error="%s"'
            % e)
        return _guireturn('Missing arguments'), http.client.BAD_REQUEST

    # WOPI GetFileInfo
    try:
        res = wopi.request(wopisrc, acctok, 'GET')
        filemd = res.json()
    except json.decoder.JSONDecodeError as e:
        WB.log.warning(
            'msg="Malformed JSON from WOPI" error="%s" response="%d"' %
            (e, res.status_code))
        return _guireturn('Invalid WOPI context'), http.client.NOT_FOUND

    try:
        # use the 'UserCanWrite' attribute to decide whether the file is to be opened in read-only mode
        if filemd['UserCanWrite']:
            try:
                wopilock = wopi.getlock(wopisrc, acctok, raiseifmissing=False)
                if not wopilock:
                    # first user opening this file, fetch it
                    wopilock = codimd.storagetocodimd(filemd, wopisrc, acctok)
                else:
                    WB.log.info('msg="Lock already held" lock="%s"' % wopilock)
                # add this token to the list, if not already in
                if acctok[-20:] not in wopilock['toclose']:
                    wopilock = wopi.refreshlock(wopisrc, acctok, wopilock)
            except wopi.InvalidLock:
                # lock is invalid/corrupted: force read-only mode
                WB.log.info(
                    'msg="Missing or invalid lock, forcing read-only mode" lock="%s" token="%s"'
                    % (wopilock, acctok[-20:]))
                filemd['UserCanWrite'] = False
                # and fetch the file from storage
                wopilock = codimd.storagetocodimd(filemd, wopisrc, acctok)

            # WOPI Lock
            res = wopi.request(wopisrc,
                               acctok,
                               'POST',
                               headers={
                                   'X-WOPI-Lock': json.dumps(wopilock),
                                   'X-Wopi-Override': 'LOCK'
                               })
            if res.status_code != http.client.OK:
                # Failed to lock the file: open in read-only mode
                WB.log.warning(
                    'msg="Failed to lock the file" response="%d" token="%s"' %
                    (res.status_code, acctok[-20:]))
                filemd['UserCanWrite'] = False

        else:
            # user has no write privileges, just fetch document and push it to CodiMD
            wopilock = codimd.storagetocodimd(filemd, wopisrc, acctok)

        if filemd['UserCanWrite']:
            # keep track of this open document for the save thread and for statistical purposes
            if wopisrc in WB.openfiles:
                # use the new acctok and the new/current wopilock content
                WB.openfiles[wopisrc]['acctok'] = acctok
                WB.openfiles[wopisrc]['toclose'] = wopilock['toclose']
            else:
                WB.openfiles[wopisrc] = {
                    'acctok': acctok,
                    'tosave': False,
                    'lastsave': int(time.time()) - WB.saveinterval,
                    'toclose': {
                        acctok[-20:]: False
                    },
                }
            # also clear any potential stale response for this document
            try:
                del WB.saveresponses[wopisrc]
            except KeyError:
                pass
            # create the external redirect URL to be returned to the client:
            # metadata will be used for autosave (this is an extended feature of CodiMD)
            redirecturl = codimd.codimdexturl + wopilock['docid'] + '?metadata=' + \
                          urllib.parse.quote_plus('%s?t=%s' % (wopisrc, acctok)) + '&'
        else:
            # read-only mode: in this case redirect to publish mode or normal view
            # to quickly jump in slide mode depending on the content
            redirecturl = codimd.codimdexturl + wopilock['docid'] + \
                          ('/publish?' if wopilock['app'] != 'slide' else '?')
        # append displayName (again this is an extended feature of CodiMD)
        redirecturl += 'displayName=' + urllib.parse.quote_plus(
            filemd['UserFriendlyName'])

        WB.log.info('msg="Redirecting client to CodiMD" redirecturl="%s"' %
                    redirecturl)
        return flask.redirect(redirecturl)

    except codimd.CodiMDFailure:
        # this can be risen by storagetocodimd
        return _guireturn('Unable to contact CodiMD, please try again later'
                          ), http.client.INTERNAL_SERVER_ERROR
Example #6
0
def appopen():
    '''Open a MD doc by contacting the provided WOPISrc with the given access_token'''
    try:
        wopisrc = urlparse.unquote(flask.request.args['WOPISrc'])
        acctok = flask.request.args['access_token']
        WB.log.info(
            'msg="Open called" client="%s" user-agent="%s" token="%s"' %
            (flask.request.remote_addr, flask.request.user_agent,
             acctok[-20:]))
    except KeyError as e:
        WB.log.error(
            'msg="Open: unable to open the file, missing WOPI context" error="%s"'
            % e)
        return _guireturn('Missing arguments'), http.client.BAD_REQUEST

    # WOPI GetFileInfo
    res = wopi.request(wopisrc, acctok, 'GET')
    if res.status_code != http.client.OK:
        WB.log.warning(
            'msg="Open: unable to fetch file WOPI metadata" response="%d"' %
            res.status_code)
        return _guireturn('Invalid WOPI context'), http.client.NOT_FOUND
    filemd = res.json()

    try:
        # use the 'UserCanWrite' attribute to decide whether the file is to be opened in read-only mode
        if filemd['UserCanWrite']:
            try:
                # was it already being worked on?
                wopilock = wopi.getlock(wopisrc, acctok)
                WB.log.info('msg="Lock already held" lock="%s" token="%s"' %
                            (wopilock, acctok[-20:]))
                # add this token to the list, if not already in
                if acctok[-20:] not in wopilock['toclose']:
                    wopilock = wopi.refreshlock(wopisrc, acctok, wopilock)
            except wopi.InvalidLock as e:
                if str(e) != str(int(http.client.NOT_FOUND)):
                    # lock is invalid/corrupted: force read-only mode
                    WB.log.info(
                        'msg="Invalid lock, forcing read-only mode" error="%s" token="%s"'
                        % (e, acctok[-20:]))
                    filemd['UserCanWrite'] = False

                # otherwise, this is the first user opening the file; in both cases, fetch it
                wopilock = codimd.loadfromstorage(filemd, wopisrc, acctok)
                # and WOPI Lock it
                res = wopi.request(wopisrc,
                                   acctok,
                                   'POST',
                                   headers={
                                       'X-WOPI-Lock': json.dumps(wopilock),
                                       'X-Wopi-Override': 'LOCK'
                                   })
                if res.status_code != http.client.OK:
                    # failed to lock the file: open in read-only mode
                    WB.log.warning(
                        'msg="Failed to lock the file" response="%d" token="%s"'
                        % (res.status_code, acctok[-20:]))
                    filemd['UserCanWrite'] = False

        else:
            # user has no write privileges, just fetch document and push it to CodiMD
            wopilock = codimd.loadfromstorage(filemd, wopisrc, acctok)
    except codimd.CodiMDFailure:
        # this can be raised by loadfromstorage
        return _guireturn(
            'Unable to connect to CodiMD, please try again later or contact support'
        ), http.client.INTERNAL_SERVER_ERROR

    # here we append the user browser to the displayName
    # TODO need to review this for production usage, it should actually come from WOPI if configured accordingly
    redirecturl = _redirecttoapp(filemd['UserCanWrite'], wopisrc, acctok, wopilock) + \
                  'displayName=' + urlparse.quote_plus(filemd['UserFriendlyName'] + \
                  '@' + _renderagent(flask.request.user_agent))
    WB.log.info('msg="Redirecting client to CodiMD" redirecturl="%s"' %
                redirecturl)
    return flask.redirect(redirecturl)