WPS local mirroring online document operation and samples

A customer project needs to introduce online document operations. Let me do a demo research here, and give me instructions on the relevant methods in the docking document, just follow the docking. But in the actual docking process, I still stepped on a lot of pitfalls. Here is a record of the previous docking work.
According to the habit, first come to an effect:

Demo download link: https://download.csdn.net/download/qxyywy/88117444

Access Guidelines

1. Apply for an authorization certificate and authorize it.
2. Log in to the system background page
3. Create and obtain application information access_key (ak for short) and secret_key (sk for short). When the developer calls the interface, use ak and sk to generate a WPS-4 signature for authentication.
4. Online editing, online preview, and format conversion access are accessed according to the corresponding open capability documents. The callback address needs to be set during the online editing and online preview docking process. Among them, online editing can enable the historical version function through configuration.
5. During use, you need to pay attention to the status of the authorization certificate through the certificate query interface. If the certificate is about to expire or is unavailable, you need to update the certificate.
6. After the online editing or online preview server is docked, the docking party can use JSSDK to call the API to meet the relevant requirements.

WPS-4 signature algorithm

During docking, I spent a certain amount of time at the WPS-4 signature. There are instructions and samples of WPS-4 in the docking document. I stepped on some pitfalls when I converted to NetCore. The most important signature algorithm is: Wps- Calculation method of Docs-Authorization
Signature format: WPS-4 {accessKey}:{signature} Note that there is a space after WPS-4.
signature:hmac-sha256(secret_key, Ver + HttpMethod + URI + Content-Type + WpsDocs-Date + sha256(HttpBody))
A sample of the signature is as follows: WPS-4POST/callback/path/demoapplication/jsonWed, 20 Apr 2022 01:33:07GMTfc005f51a6e75586d2d5d078b657dxxxdf9c1dfa6a7c0c0ba38c715daeb6ede9

This is the explanation of the signature algorithm in the document, and the relevant algorithm is completed according to the format. The specific algorithm is as follows:
Assembly of signature:

 /// <summary>
        /// Get the signature
        /// </summary>
        /// <param name="method">Request method, such as: GET,POST</param>
        /// <param name="uri">Request url with querystring</param>
        /// <param name="body">request body</param>
        /// <param name="date">Date</param>
        /// <param name="contentType">Default: application/json</param>
        /// <param name="secretKey">Apply SK</param>
        /// <returns></returns>
        public static string WPS4Signature(string secretKey,string method,string uri, byte[] body=null,DateTime? date=null,string contentType= "application/json")
        {
            //Get the uri path
            string path = uri;
            // date formatting
            if (date == null)
                date = DateTime. Now;
            string dateStr = String.Format("{0:r}", date);
            //open does not participate in the signature, do replacement processing
            if (path. StartsWith("/open"))
            {
                path = path.Replace("/open", "");
            }

            string sha256body;
            //body is empty if it is empty, otherwise return sha256(body)
            if (body != null & amp; & amp; body. Length > 0)
            {
                sha256body = Sha256(body);
            }
            else
            {
                sha256body = "";
            }
            String signature = null;
            signature = HmacSHA256Encrypt($"WPS-4{method.ToUpper()}{path}{contentType}{dateStr}{sha256body}", secretKey);

            return signature;
        }

HmacSHA256 encryption algorithm:

 /// <summary>
        /// HmacSHA256 encryption
        /// </summary>
        /// <param name="secret"></param>
        /// <param name="signKey"></param>
        /// <returns></returns>
        public static string HmacSHA256Encrypt(string secret, string signKey)
        {
            string signRet = string. Empty;
            using (HMACSHA256 mac = new HMACSHA256(Encoding. UTF8. GetBytes(signKey)))
            {
                byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(secret));
                //signRet = Convert.ToBase64String(hash);
                signRet = ToHexStrFromByte(hash);
            }
            return signRet;
        }

Sha256 conversion:

 /// <summary>
        /// Sha256 conversion
        /// </summary>
        /// <param name="input">The input.</param>
        /// <returns>A hash.</returns>
        public static string Sha256(this byte[] input)
        {
            if (input == null)
            {
                return null;
            }
            using (var sha = SHA256. Create())
            {
                var hash = sha. ComputeHash(input);
                return ToHexStrFromByte(hash);
            }
        }

Byte array to hexadecimal string:

 /// <summary>
        /// Byte array to hexadecimal string: separated by spaces
        /// </summary>
        /// <param name="byteDatas"></param>
        /// <returns></returns>
        public static string ToHexStrFromByte(this byte[] byteDatas)
        {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < byteDatas. Length; i ++ )
            {
                builder.Append(string.Format("{0:X2}", byteDatas[i]));
            }
            return builder.ToString().Trim().ToLower();
        }

Get the online preview link

 /// <summary>
        /// Get the online preview link
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [Route("api/wps/previewgenerate")]
        [HttpPost]
        public Task<GenarateResult> GenarateWPSPreviewUrl(GenarateRequest request)
        {
            return Task.Run(() =>
            {
                string wpsHost = "http://10.4.**.**";
                string uri = $"/api/preview/v1/files/{defaultFileId}/link?type=w &preview_mode=high_definition";
                string fullUrl = $"{wpsHost}/open{uri}";
                Dictionary<string, string> headers = new Dictionary<string, string>();
                DateTime now = DateTime. Now;
                headers.Add("Content-Type", "application/json");
                headers.Add("Wps-Docs-Date", String.Format("{0:r}", now));
                var signature = WPSLocalSIgnatureHelper.WPS4Signature("SKrpaxjdwoetijjv", "get", uri, null, now);
                string docsAuthorization = WPSLocalSIgnatureHelper.WPS4SignAuthorization("UOMYPEVAHWQLTKJF", signature);
                headers.Add("Wps-Docs-Authorization", docsAuthorization);
                HttpHelper httpHelper = new HttpHelper();
                var resultTemp = httpHelper. Get(fullUrl, headers);
                var result = JsonConvert. DeserializeObject<ResponseBaseModel<OnlineEditResultModel>>(resultTemp);
                string url = "";
                if (result != null & amp; & amp; result. data != null)
                {
                    url = result.data.link;
                }
                return new GenarateResult { Url = url };
            });
        }

Here comes the tricky part. The method parameters are all assembled. When the request is made through the HttpWebRequest backend to obtain the online preview address returned by the WPS middle station, it always prompts 401 error and cannot obtain data. I had no choice but to assemble related requests and parameters through APIPost, and I was able to get relevant data again. After comparing the request header, and adjusting the request header of the program after the difference, to ensure that it is exactly the same as in apiPost, it still returns 401, find a solution by consulting relevant information, manually capture WebException after the request is initiated, and then in WebException Parse the data stream for information.
HttpWebRequest Get access

 /// <summary>
        /// Get access
        /// </summary>
        /// <param name="url"></param>
        /// <param name="encode"></param>
        /// <param name="referer"></param>
        /// <param name="headers"></param>
        /// <returns></returns>
        public string Get(string url, string encode, string referer, Dictionary<string, string> headers=null)
        {
            int num = _tryTimes;
            HttpWebRequest request = null;
            HttpWebResponse response = null;
            StreamReader reader = null;
            while (num-- >= 0)
            {
                try
                {
                    DelaySomeTime();
                    ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);//Verification server certificate callback automatic verification
                    request = (HttpWebRequest)WebRequest. Create(url);
                    request.Headers.Add("accept", "*/*");
                    request.Headers.Add("accept-encoding", "gzip, deflate, br");
                    request.Headers.Add("accept-language", "zh-CN");
                    request.Headers.Add("connection", "keep-alive");
                    
                    if (headers != null)
                    {
                        foreach (var item in headers)
                        {
                            request.Headers.Add(item.Key, item.Value);
                        }
                    }
                    //request. UserAgent = reqUserAgent;
                    request.CookieContainer = _cookie;
                    request. Referer = referer;
                    request.Method = "GET";
                    request.Timeout = _timeOut;
                    if (_proxy != null & & _proxy. Credentials != null)
                    {
                        request.UseDefaultCredentials = true;
                    }
                    request.Proxy = _proxy;
                    response = (HttpWebResponse)request. GetResponse();
                    reader = new StreamReader(response. GetResponseStream(), Encoding. GetEncoding(encode));
                    return reader. ReadToEnd();
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response; // Parse valid information returned by errors such as 401
                    var resultTemp = "";
                    Stream stream = response. GetResponseStream();
                    using (StreamReader readers = new StreamReader(stream, Encoding. UTF8))
                    {
                        resultTemp = readers. ReadToEnd();
                    }
                    return resultTemp;
                }
                catch (Exception ex)
                {
                    _logger.Error(url + "\r\\
" + ex.ToString());
                    continue;
                }
                finally
                {
                    if (request != null)
                    {
                        request. Abort();
                    }
                    if (response != null)
                    {
                        response. Close();
                    }
                    if (reader != null)
                    {
                        reader. Close();
                    }
                }
            }
            return string.Empty;
        }

DemoHtml

When the above is reached, the back-end processing is basically completed. The front-end side uses an html for temporary demo, and the real project needs to use VUE and other processing, which is similar.
The following is the html code:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <!-- It is recommended to disable the zoom that comes with the frame browser -->
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
        />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>WPS Web Office (iframe) Access Guide</title>
        <style>
            * {
                box-sizing: border-box;
            }

            html,
            body {
                display: flex;
                flex-direction: column;
                padding: 0;
                margin: 0;
                height: 100%;
                /* Prevent double-click zooming */
                touch-action: manipulation;
            }

            iframe {
                flex: 1;
            }
        </style>
        <!-- cdn introduces JQ -->
        <script src="//i2.wp.com/ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js"></script>
        <script src="./jwps.js"></script>
        <script type="text/javascript">
            var appHost = "http://10.4.146.19:8890";
            // support HTTPS
            // Note: If you set the token through postMessage, please add _w_tokentype=1 to the url parameter
            function showWPS(url) {
                // initialization
                var wps = WPS.config({
                    mount: document. querySelector("#wpsPanl"),
                    // Word
                    wpsUrl: url,
                    headers: {
                        shareBtn: {
                            tooltip: "Share",
                            subscribe: function() {
                                console.log("click callback");
                            }
                        },
                        otherMenuBtn: {
                            tooltip: "Other buttons",
                            items: [
                                {
                                    // Custom, type fixed 'custom'
                                    type: "custom",
                                    icon:
                                        "http://ep.wps.cn/index/images/logo_white2.png",
                                    text: "API Export PDF",
                                    subscribe: function(wps) {
                                        if (wps. WpsApplication) {
                                            wps. WpsApplication()
                                                .ActiveDocument.ExportAsFixedFormatAsync()
                                                .then(function(result) {
                                                    console. table(result);
                                                });
                                        }
                                    }
                                },
                                {
                                    // Custom, type fixed 'custom'
                                    type: "custom",
                                    icon:
                                        "http://ep.wps.cn/index/images/logo_white2.png",
                                    text: "API usage",
                                    subscribe: function(wps) {
                                        let result;
                                        if (wps. WpsApplication) {
                                            wps. WpsApplication()
                                                .ActiveDocument.ExportAsFixedFormatAsync()
                                                .then(function(result) {
                                                    console. table(result);
                                                });
                                        }
                                    }
                                }
                            ]
                        }
                    }
                });
                return wps;
            }

            window.onload = function() {
                 $.ajax({
                    url: appHost + "/api/wps/previewgenarate",
                    contentType: "application/json",
                    dataType: "json",
                    data: JSON.stringify({
                        fileId: 'a123',
                        fileName: "test.docx",
                        fileType: 1,
                        userId: 1505340867
                    }),
                    type: "post",
                    success: function(res) {
                        var wpsUrl = res.url;
                        console.log(wpsUrl);
                        var wps = showWPS(wpsUrl);
                    }
                 });

                var fileInput = document. getElementById("bookimg1");
                //Select a document
                fileInput. addEventListener('change', function () {
                    // break if no file is passed in
                    if (fileInput. files[0] == undefined) {
                        return;
                    }

                    var file = fileInput. files[0];

                    //FileReader can directly convert uploaded files into binary streams
                    var reader = new FileReader();
                    reader.readAsDataURL(file);//Convert binary stream, asynchronous method
                    reader.onload = function (result) {//This.result is a binary stream after completion
                        var base64Str = this.result;
                        $('#uploadImg').attr('src', base64Str);
                        $('#imgPreview').show();
                    }
                })
            };
            function replaceWps() {
                $('.tdname1').html($('#name1').val());
                $('.tddept1').html($('#dept1').val());
                $('.tdage1').html($('#age1').val());
                $('.tdname2').html($('#name2').val());
                $('.tddept2').html($('#dept2').val());
                $('.tdage2').html($('#age2').val());
                var fileBytes = "";
                var fileName = "";
                if ($('#bookimg1')[0].files[0] != undefined) {
                    var imgFile = $('#bookimg1')[0].files[0];
                    fileName = imgFile.name;
                    //FileReader can directly convert uploaded files into binary streams
                    var reader = new FileReader();
                    reader.readAsDataURL(imgFile);//Convert binary stream, asynchronous method
                    reader.onload = function (result) {//This.result is a binary stream after completion
                        var base64Str = this.result;
                        var startNum = base64Str. indexOf("base64,");
                        startNum = startNum * 1 + 7;
                        //Remove the front format information (if required)
                        var baseStr = base64Str. slice(startNum);
                        fileBytes = baseStr;

                        $.ajax({
                            url: appHost + "/api/wps/wrapheader",
                            contentType: "application/json",
                            dataType: "json",
                            data: JSON.stringify({
                                sample_list: [
                                    {
                                        bookmark: 'bookmark1',
                                        type: 'TEXT',
                                        text: $('#bookmark1').val()
                                    },
                                    {
                                        bookmark: 'bookmark2',
                                        type: 'TEXT',
                                        text: $('#bookmark2').val()
                                    },
                                    {
                                        bookmark: 'bookmark3',
                                        type: 'TEXT',
                                        text: $('#bookmark3').val()
                                    },
                                    {
                                        bookmark: 'bookimg1',
                                        type: 'IMAGE',
                                        /*sample_url:$('#bookimg1').val(),*/
                                        sample_filename: fileName,
                                        text: fileBytes,
                                    },
                                    {
                                        bookmark: 'bookform1',
                                        type: 'TEXT',
                                        text: $('#testForm').prop("outerHTML")
                                    }
                                ],
                            }),
                            type: "post",
                            success: function (res) {
                                var wpsUrl = res.url;;
                                var wps = showWPS(wpsUrl);
                            }
                        });
                    }
                } else {
                    $.ajax({
                        url: "http://10.4.146.19:8890/api/wps/wrapheader",
                        contentType: "application/json",
                        dataType: "json",
                        data: JSON.stringify({
                            sample_list: [
                                {
                                    bookmark: 'bookmark1',
                                    type: 'TEXT',
                                    text: $('#bookmark1').val()
                                },
                                {
                                    bookmark: 'bookmark2',
                                    type: 'TEXT',
                                    text: $('#bookmark2').val()
                                },
                                {
                                    bookmark: 'bookmark3',
                                    type: 'TEXT',
                                    text: $('#bookmark3').val()
                                },
                                {
                                    bookmark: 'bookform1',
                                    type: 'TEXT',
                                    text: $('#testForm').prop("outerHTML")
                                }
                            ],
                        }),
                        type: "post",
                        success: function (res) {
                            var wpsUrl = res.url;;
                            var wps = showWPS(wpsUrl);
                        }
                    });
                }

\t\t\t\t
}
        </script>
    </head>
    <body>
<div style="width:100%;height:700px;">
<div id="wpsPanl" style="width:65%;float:left;height:100%;"></div>
            <div id="form" style="width:34%;float:left;height:100%;padding-top:50px;padding-left: 20px;">
                <div><div class="title">Problem name:</div><input class="input" type="text" id="bookmark1" placeholder="Please fill in the subject name"></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">Project application unit:</div><input class="input" type="text" id="bookmark2" placeholder="Please fill in Subject reporting unit"></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">Project leader:</div><input class="input" type="text" id="bookmark3" placeholder="Please fill in Subject Leader"></div>
                <dl style="clear:both;"></dl>
                <div style="height:140px">
                    <div class="title">Project members:</div>
                    <table>
                        <thead><tr><td>Name</td><td>Department</td><td>Age</td></tr></thead>
                        <tr><td><input class="forminput" type="text" id="name1" placeholder="Please fill in the name"></td><td><input class= "forminput" type="text" id="dept1" placeholder="Please fill in the department you belong to"></td><td><input class="forminput" type=" text" id="age1" placeholder="Please fill in the age"></td></tr>
                        <tr><td><input class="forminput" type="text" id="name2" placeholder="Please fill in the name"></td><td><input class= "forminput" type="text" id="dept2" placeholder="Please fill in the department you belong to"></td><td><input class="forminput" type=" text" id="age2" placeholder="Please fill in the age"></td></tr>
                    </table>
                    <div style="width:100%;display:none;">
                        <table border="1" cellspacing="0" style="width:90%;" id="testForm">
                            <thead><tr><td>Name</td><td>Department</td><td>Age</td></tr></thead>
                            <tr><td class="tdname1"></td><td class="tddept1"></td><td class="tdage1"></td></tr>
                            <tr><td class="tdname2"></td><td class="tddept2"></td><td class="tdage2"></td></tr>
                        </table>
                    </div>
                </div>
                <dl style="clear:both;"></dl>
                <div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">Form replacement on page 6< /span></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">Image upload:</div><input style="padding:8px 0;" type="file" id="bookimg1"></ div>
                <div style="display:none;" id="imgPreview"><img src="" id="uploadImg" width="100" /></div>
                <dl style="clear:both;"></dl>
                <div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">The picture is replaced on page 7< /span></div>
                <dl style="clear:both;"></dl>
                <div style="text-align:center;">
                    <input style="
    margin: 0 150px;
    height: 40px;
    background: rgba(2,128,204,1);
    border-radius: 2px;
    display: block;
    width: 80px;
    border: none;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    color: rgba(255,255,255,1);
    " type="button" value="Replace" onclick="replaceWps()" id="submit">
                </div>
            </div>
<div>
</body>
<style>
#wpsPanl iframe{width:99%;height:850px;}
#form div{
margin: 10px 0;
height: 40px;
color: rgba(51,51,51,1);
float: left;
}
#form div .input{
height: 35px;
margin: 2px 0;
width: 358px;
border: solid 1px rgba(193,193,193,.35);
}
        #form div .forminput {
            height: 35px;
            margin: 2px 0;
            width: 94px;
            border: solid 1px rgba(193,193,193,.35);
        }
#form div .title{width:120px;text-align: right;}
        td {
            border: 1px solid rgba(193,193,193,.35);
            padding: 8px 12px;
        }
        tr {
            background-color: inherit;
            font-size: 14px;
            border-top: 1px solid var(--vp-c-divider);
            transition: background-color .5s;
        }
        table {
            /*width: 100%;*/
            display: table;
            border-collapse: collapse;
            /*margin: 20px 0;*/
            overflow-x: auto;
        }
        table thead {
            font-weight: bold;
        }
</style>
</html>

Personal summary: Online editing and other related operations are required. In fact, free component combinations can also be used. For example, onlyoffice + Aspose.Words can be used to operate, just my personal opinion.