I wanted to expand on my previous Post. As stated, Laserfiche Forms DOES have an API and various Business Process endpoints and they are accessible to access programmatically. Provided are some code samples of how we access it.
However, JUST BECAUSE the API/Business Process endpoints are accessible, doesn't mean they aren't subject to change. Always check your upgrades and be sure your integrations work.
Few housekeeping things.
- This script is written in Go. Go can be found here https://go.dev/learn/
- This script uses Playwright and simulates a login to a forms server using Playwright. You could probably do this with network calls as well. Documentation on Playwright can be found here. https://playwright.dev/
- The script stores the needed Cookies in a text file. It's been modified for this post. Don't actually store the cookies you get back from Laserfiche in a text file. Store them securely.
- Same with passwords. This script uses placeholders for the password and username. Store them securely.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/playwright-community/playwright-go"
)
type TeamsMessage struct {
Type string `json:"type"`
Attachments []Attachments `json:"attachments"`
}
type Body struct {
Type string `json:"type"`
Text string `json:"text"`
Wrap bool `json:"wrap"`
}
type Content struct {
Schema string `json:"$schema"`
Type string `json:"type"`
Version string `json:"version"`
Body []Body `json:"body"`
MSTeams MSTeams `json:"msTeams"`
}
type Attachments struct {
ContentType string `json:"contentType"`
ContentURL string `json:"contentUrl"`
Content Content `json:"content"`
}
type MSTeams struct {
Width string `json:"width"`
}
func main() {
client := &http.Client{}
oldCookieData, err := os.ReadFile(`\\path\ToCookieStorage\CookieString.txt`)
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
pw, err := playwright.Run()
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
launchOptions := playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(false),
}
browser, err := pw.Chromium.Launch(launchOptions)
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
page, err := browser.NewPage()
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
defer shutdownPW(page, browser, pw)
_, err = page.Goto("https://formserver.domain.local/forms")
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
err = page.Type("#nameField", "YourSecurelyStoredUsername")
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
err = page.Type("#passwordField", "YourSecurelyStoredPassword")
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
err = page.Click("#loginBtn")
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
time.Sleep(1 * time.Minute)
allCookies, err := page.Context().Cookies()
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
var cookieLine string
for _, v := range allCookies {
if v.Name == ".LFFORMSAUTH" || v.Name == "LMAuth" {
cookieLine = cookieLine + v.Name + "=" + v.Value + "; "
}
}
cookieLine = strings.TrimSuffix(cookieLine, "; ")
if !strings.Contains(cookieLine, ".LFFORMSAUTH=") {
sendTeamsNotification(client, "No LFFORMSAUTH token found")
return
}
if !strings.Contains(cookieLine, "LMAuth=") {
sendTeamsNotification(client, "NO LMAuth Token Found")
return
}
f, err := os.OpenFile(`\\path\ToCookieStorage\CookieString.txt`, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
defer f.Close()
f.WriteString(cookieLine)
err = Logout(client, string(oldCookieData))
if err != nil {
sendTeamsNotification(client, err.Error())
return
}
}
func shutdownPW(pg playwright.Page, br playwright.Browser, pw *playwright.Playwright) error {
err := pg.Close()
if err != nil {
return err
}
err = br.Close()
if err != nil {
return err
}
err = pw.Stop()
if err != nil {
return err
}
return nil
}
func Logout(h *http.Client, cookieString string) error {
req, err := http.NewRequestWithContext(context.Background(), "GET", `https://formserver.domain.local/forms/account/logoff`, nil)
if err != nil {
return err
}
req.Header.Set("Cookie", cookieString)
res, err := h.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
return nil
}
func sendTeamsNotification(h *http.Client, teamsMessage string) error {
url := `wehookurl`
teamsData := TeamsMessage{
Type: "message",
Attachments: []Attachments{
{
ContentType: "application/vnd.microsoft.card.adaptive",
Content: Content{
Schema: "http://adaptivecards.io/schemas/adaptive-card.json",
MSTeams: MSTeams{
Width: "full",
},
Type: "AdaptiveCard",
Version: "1.4",
Body: []Body{
{
Type: "TextBlock",
Text: teamsMessage,
Wrap: true,
},
},
},
},
},
}
teamsJD, err := json.Marshal(teamsData)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(teamsJD))
if err != nil {
return err
}
res, err := h.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("Wanted status 200 got %d from Teams", res.StatusCode)
}
return nil
}
Now, basically what this script does is several things
- It grabs our Old Cookies in a string
- It Loads Playwright and navigates to our form server
- It logins to Laserfiche
- It does sleep for a minute, the sleep ensures the page has fully loaded and all the needed Cookies are in place
- It grabs The Cookies we have for that session and generates the string
- We want two Cookies, These give us access to the Forms API.
- The Two Cookies we care about are LMAUTH and .LFFORMSAUTH
- If we don't have LMAUTH or .LFFORMSAUTH, the script returns an error
- If we have both Cookies, it then writes the cookies to a secure location(This example uses a text file, again, as stated above, change that)
- We then LOGOUT our old session that we retrieved from step 1.
- We then use these cookies for any subsequent transactions to the Forms API
- If we encounter an error, we post that error to a Microsoft Teams Bot(Incoming Webhook) our Team has setup for error handling and reporting
Now, let's say we wanted to skip a form using the Laserfiche Forms API. The code would be this.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
)
type Skip struct {
InstanceIDList []int `json:"instanceIDList"`
StepIDList []int `json:"stepIDList"`
ActionComment string `json:"actionComment"`
}
func main() {
authCookie, err := os.ReadFile(`\\path\ToCookieStorage\CookieString.txt`)
if err != nil {
fmt.Println(err)
return
}
s := Skip{
InstanceIDList: []int{WorkerIDHere},
StepIDList: []int{3},
ActionComment: "Skipped VIA Go Script",
}
url := "https://formserver.domain.local/forms/BusinessProcess/SkipSteps"
sj, err := json.Marshal(s)
if err != nil {
fmt.Println(err)
return
}
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(sj))
if err != nil {
fmt.Println(err)
return
}
req.Header.Set("Cookie", string(authCookie))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != 200 {
fmt.Printf("Problem with Request, got Status Code %d", res.StatusCode)
return
}
if string(body) != "" {
fmt.Printf("Error, expected blank body")
return
}
fmt.Printf("Form with worker ID %d successfully skipped", s.InstanceIDList[0])
}
One thing to consider as well is the permissions of the account you use for the API. I would make an account called "FormsAPI" or something and only give it access to the forms you want to manipulate through the API.
Also most actions on individual forms require a WORKER ID which is different than the BPID or Submission ID. These can be retrieved via SQL with this query.
SELECT [instance_id] FROM [YourLFDB].[dbo].[cf_bp_worker_instances] WHERE [bp_instance_id] = @bpInstanceID
Where bpInstanceID = the business process instance ID you are trying to control via the API.
Happy Ficheing