WOPI based document editor for Office files

|
| By Webner

WOPI based document editor for Office files

WOPI (Web Application Open Platform Interface) protocol is Microsoft’s platform to integrate Office Online within your application. To make use of Wopi, you would require to become a member of the Office 365 – Cloud Storage Partner Program.

The WOPI protocol enables Office Online to access and update files that are stored in our service locations.

Wopi implementation includes an HTML page (or pages) that will host the Office Online iframe and a set of REST endpoints that expose information about the files that you want to view or edit in Office Online.
Initial implementation comes with wopi test environment urls provided by Office 365 – Cloud Storage Partner Program. After implementation, it must pass Wopi validation test criteria and if tests pass successfully , then you can further follow the production environment process.

Here are the basic implementation points discussed that are required for editing Microsoft files like .docx, pptx, etc.

  1. Host Page:
    It is not dependent upon the wopi allowed domain.
    In the current implementation, which uses test environment and for that we have discovery url in config file
    <add key="WopiDiscovery" value="https://ffc-onenote.officeapps.live.com/hosting/discovery"/>
    This returns a number of action urls, we have to choose the action url based on our requirement. For example: we fetch the url based on the type of document(extension) and action type ‘edit’.
    And then we need the replace request url parameters and remove the extra ones from the action url
    public const string BUSINESS_USER = "";
    public const string DC_LLCC = "<rs=DC_LLCC&>";
    public const string DISABLE_ASYNC = "<na=DISABLE_ASYNC&>";
    public const string DISABLE_CHAT = "<dchat=DISABLE_CHAT&>";
    public const string DISABLE_BROADCAST = "<vp=DISABLE_BROADCAST&>";
    public const string EMBDDED = "<e=EMBEDDED&>";
    public const string FULLSCREEN = "<fs=FULLSCREEN&>";
    public const string PERFSTATS = "<showpagestats=PERFSTATS&>";
    public const string RECORDING = "<rec=RECORDING&>";
    public const string THEME_ID = "<thm=THEME_ID&>";
    public const string UI_LLCC = "<ui=UI_LLCC&>";
    public const string VALIDATOR_TEST_CATEGORY = "<testcategory=VALIDATOR_TEST_CATEGORY>";
    public const string HOST_SESSION_ID = "<hid=HOST_SESSION_ID&>";
    public const string SESSION_CONTEXT = "<sc=SESSION_CONTEXT&>";
    public const string WOPI_SRC= "<wopisrc=WOPI_SOURCE&>";
    public const string ACTVITY_NAVIGATION_ID= "<actnavid=ACTIVITY_NAVIGATION_ID&>";

    For example action url contains example and we are replacing it by “rs=en-US” value ,
    And similarly wopisrc parameter is set with our wopi end point url

    After forming the correct wopi action URL For the wopi frame, we post the form with the action URL along with the access_token and accesstoken__ttl values

    1. Access_token (using our officescope user token)
    2. Access_token_ttl (time taken from 1st Jan 1970 – to the token expiry time in milliseconds)

    In our case, the final action url is
    https://FFC-word-view.officeapps.live.com/wv/wordviewerframe.aspx?ui=en-US&rs=en-US&dchat=false&IsLicensedUser=1&wopisrc=https://wopi.<your_companydomain>.com/vdocoffice365Integration/wopi/files/e1b04cf5-694b-4751-ac6b-95060af2292c
    Note: A WOPI client requires no understanding of the format or content of the token; the WOPI client simply includes it in WOPI requests and expects the host to validate it.

    During this step, I am uploading a file to azure and saving file size and version info(database/file). You can also work with direct file locations, but in my implementation, I am using temporary file locations and then saving it to the original location.

  2. Wopi Host endpoints:
    Domain for this should be wopi allowed domain.
    Based on our application requirements and wopi documentation, currently, there are four wopi rest endpoints implemented in the application required for editing files which are as follows: –

    1. GET: wopi/files/{id} : This endpoint is CheckFileInfo endpoint.
    2. GET: wopi/files/{id}/contents : This endpoint is GetFile endpoint
    3. POST: wopi/files/{id} : This endpoint handles multiple request types.
      Handles Lock, GetLock, RefreshLock, Unlock, UnlockAndRelock, PutRelativeFile, RenameFile, PutUserInfo
    4. POST: wopi/files/{id}/contents : This endpoint is PutFile endpoint.
      These endpoints are implemented based on conditions mentioned for each endpoint which are mandatory to implement to pass the wopi validation testing. Otherwise, the editor does not work correctly.

      Implementation Steps:
      Token validation: For each endpoint, we need to implement a token validation process (WopiTokenValidationFilter AuthorizeAttribute used in our app ). If it is invalid, then the request should not proceed.
      In our case it is our officescope token, so validating the token by “/users/current” .from data service.
      wopi/files/{id} : Get request
      In this simply we send a file model response with the correct version and file size. Version and size are important. If the wrong version and file size are entered. The editor does not work correctly.
      Response of these endpoints must be in the format as described in wopi documentation and based on implementation requirements.
      The current Response file model is as follows:
      {
      [JsonProperty(PropertyName = "OwnerId")] public string OwnerId { get; set; }
      [JsonProperty(PropertyName = "BaseFileName")] public string BaseFileName { get; set; }
      [JsonProperty(PropertyName = "Size")] public long Size { get; set; }
      [JsonProperty(PropertyName = "Version")] public string Version { get; set; }
      [JsonProperty(PropertyName = "UserInfo")] public string UserInfo { get; set; }
      [JsonProperty(PropertyName = "UserFriendlyName")] public string UserFriendlyName { get; set; }
      [JsonProperty(PropertyName = "UserId")] public string UserId { get; set; }
      [JsonProperty(PropertyName = "CloseUrl")] public string CloseUrl { get; set; }
      [JsonProperty(PropertyName = "HostEditUrl")] public string HostEditUrl { get; set; }
      [JsonProperty(PropertyName = "HostViewUrl")] public string HostViewUrl { get; set; }
      [JsonProperty(PropertyName = "SupportsExtendedLockLength")] public bool SupportsExtendedLockLength
      {
      get { return true; }
      }
      [JsonProperty(PropertyName = "SupportsFolders")] public bool SupportsFolders
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "SupportsGetLock")] public bool SupportsGetLock
      {
      get { return true; }
      }
      [JsonProperty(PropertyName = "SupportsLocks")] public bool SupportsLocks
      {
      get { return true; }
      }
      [JsonProperty(PropertyName = "SupportsRename")] public bool SupportsRename
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "SupportsUpdate")] public bool SupportsUpdate
      {
      get { return true; }
      }
      [JsonProperty(PropertyName = "SupportsUserInfo")] public bool SupportsUserInfo
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "ReadOnly")] public bool ReadOnly
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "RestrictedWebViewOnly")] public bool RestrictedWebViewOnly
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "UserCanAttend")] //Broadcast only
      public bool UserCanAttend
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "UserCanNotWriteRelative")] public bool UserCanNotWriteRelative
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "UserCanPresent")] //Broadcast only
      public bool UserCanPresent
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "UserCanRename")] public bool UserCanRename
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "UserCanWrite")] public bool UserCanWrite
      {
      get { return true; }
      }
      [JsonProperty(PropertyName = "LicenseCheckForEditIsEnabled")] public bool LicenseCheckForEditIsEnabled
      {
      get { return false; }
      }
      [JsonProperty(PropertyName = "SupportsDeleteFile")] public bool SupportsDeleteFile
      {
      get { return true; }
      }
      }

      wopi/files/{id}/Contents: Get request
      Response of this request is in bytes with 200 success. File Bytes are returned along with header X-WOPI-ItemVersion.
      wopi/files/{id} : Post request
      The request type is distinguished by header value [X-WOPI-Override] (a.) If the request type is “Lock”:
      Following conditions are implemented based on wopi documentation:

      • If there is no file lock associated with the document(in the database, currently lock info is manually stored in a file), then save the lock value received from headers along with its expiration time. If lock exists but expiration time is older (fileLockExpire < DateTime.UtcNow). Then update the lock value with expiration time. Return success.
      • if saved fileLockValue matches request value, update lock Expire time and return success.
      • Else return 409 error along with failure reason header X-WOPI-LockFailureReason and X-WOPI-Lock value with existing lock value.

      If the request type is “GetLock”:

      • If there is no lock value stored for the file (in our database), then in response headers set Value Lock property value empty and return 200.
      • If lock value exists but lock expiration time is less than the current time, then remove it from the stored location, set response header lock value empty, and return 200.
      • If the file has a lock that has not expired, then return 200 with lock value in the header.

      If the request type is “RefreshLock”:
      The request header contains the lock value

      • If file lock is not stored (in the database), then return file not locked error with code 409.
      • If the lock expires time is older than the current time, then return file not locked error with code 409.
      • If the request header lock does not match the stored lock value, then return lock mismatch error with code 409.
      • Otherwise, store lock value, its expire time (database/file)

      If the request type is “Unlock”:
      The request header contains the lock value

      • If the file lock is not locked i.e lock value is not stored or is empty, return the 409 error with the file is not locked.
      • If the lock expiration time value is older than the current time, return 409 error with the file is not locked
      • If request lock does not match the stored lock value, return 409 error with lock mismatch error
      • Otherwise, remove the stored lock value and return 200 success.

      Response header also contains file version in key named as X-WOPI-ItemVersion.
      If the request type is “UnlockAndRelock”:
      The header contains the lock value and old lock value.

      • If the file lock is not locked i.e lock value is not stored or is empty, return the 409 error with the file is not locked.
      • If the lock expiration time value is older than the current time then the return 409 error along with the message file is not locked.
      • If the old lock request header value does not match the stored lock value, return 409 error with lock mismatch error.
      • Otherwise, update the stored lock value with new lock value and expiration time (in database/file) and return 200 success.
    5. wopi/files/{id}/Contents : Post request
      The request header contains a lock value.
      The request contains a file stream.
      If the file lock value is not stored or is empty (database/file),
      Check if the input stream is 0 bytes. Upload a 0-byte file to the target location (currently uploading to Azure wopi container).
      Increment file version , Store file version , size info ((database/file)
      Else [
      – If input stream is greater and files on azure is also 0 bytes, save the file, store increment file version, size info (database/file), and return 200
      – If the input stream is greater and files on azure are not 0 bytes, return 409 error with file not locked error.]

      If stored file lock expiration time is older than the current time, remove file-related info and return 409 error with file not locked error.
      If the request header lock does not match the stored lock value return 409 error with message lock mismatch.
      Otherwise upload input stream to the target location, update file info (database/file) with incremented version, size.
      and return with 200 status.

      The response header contains mandatory field value X-WOPI-ItemVersion

While editing, file information which we may require during wopi protocol interaction could be stored in some file or database based on the above current implementation is as follows:

  • File size, version, lock value, expire time, FileId, UserId
  • New file info (if handling new file creation )
  • User sign in info by office365 account

Leave a Reply

Your email address will not be published. Required fields are marked *