You are viewing limited content. For full access, please sign in.

Question

Question

Does Laserfiche Forms has an API

asked on November 2, 2022

Does Laserfiche Forms has an API to automate it's user management behavior , in other words can I create a team or a user throw an APIrequest rather than manually doing it in the forms?

1 0

Replies

replied on November 2, 2022

Doesn't look like this can be used for Forms yet, but this is promising.

Release Notes for Laserfiche API Server - Knowledge Base

4 0
replied on November 3, 2022 Show version history

Laserfiche forms has endpoints you can access, but they are undocumented and not public and subject to change with upgrades. You can use Chrome Dev tools and Postman to map them and see how everything works under the hood.

2 0
replied on November 2, 2022

Forms does not have an accessible API, however, you can use groups to grant access to a process and in recent versions of Forms you can add groups to a Team to automate membership.

The group just has to be included in the groups that are given access to Forms, so they will be synchronized and available as an option when you're configuring things.

3 1
replied on November 5, 2022 Show version history

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.

  1. This script is written in Go. Go can be found here https://go.dev/learn/
  2. 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/
  3. 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.
  4. 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

  1. It grabs our Old Cookies in a string
  2. It Loads Playwright and navigates to our form server
  3. It logins to Laserfiche
  4. It does sleep for a minute, the sleep ensures the page has fully loaded and all the needed Cookies are in place
  5. It grabs The Cookies we have for that session and generates the string
    1. We want two Cookies, These give us access to the Forms API.
    2. The Two Cookies we care about are LMAUTH and .LFFORMSAUTH
  6. If we don't have LMAUTH or .LFFORMSAUTH, the script returns an error
  7. 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)
  8. We then LOGOUT our old session that we retrieved from step 1.
  9. We then use these cookies for any subsequent transactions to the Forms API
  10. 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

replied on November 6, 2022 Show version history

I think the point here is that this is not a published API; this is a way to access application endpoints by simulating user sessions more like bot automation. Most modern web applications have endpoints designed to be called via AJAX from the user interface, but that doesn't constitute an actual API.

The approach of using cookies to simulate a user login is not consistent with standard API practices and comes with its own fair share of concerns.

So yes, technically you can access Forms this way, and you can also access the database, repository volume folders, and a host of other items; however, doing so is effectively an unsupported system hack and could easily break in future updates, so it is not something I would ever recommend.

I'd argue it is actually easier to manipulate teams and team membership in Forms via the database; I've written/tested SQL scripts to do just that, and they were far less complicated, but I'd still never recommend that to anyone.

replied on November 6, 2022 Show version history

Hey Jason.

I appreciate your comment. The OP asked about a forms API. The point I was trying to make is that while not public and not documented, the endpoints are absolutely accessible.

replied on November 6, 2022

Hey Mark,

I understand, and the solution you provided is a creative way to accomplish things; however, I feel it is important to clarify that although the endpoints are indeed accessible, that does not meet the common definition/criteria to really be called an API.

I know this is a bit of a "semantics" thing, but I think those distinctions are important because from an implementation and support perspective there is a massive difference between an API designed to be accessible externally, and accessing endpoints designed for internal application use.

You did your due diligence by including a disclaimer about changes and support; however, I think the semantics and definitions are very important to prevent confusion/misconceptions, especially because these kinds of technical concepts are new to many of the people coming here for help.

replied on November 6, 2022 Show version history

Hey Jason.

Thank you for the creativity compliment.

You are absolutely right about the semantics, and to your point, semantics are important, especially to users who may not have familiarity with APIs, etc.

It's not an API as much as "Access to the endpoints that do Form actions". So that was my bad for mislabeling it as such.

I think the main take away that I wanted to drive home is that Laserfiche Forms does have endpoints that can be accessed via Network Calls. These endpoints can do actions in Forms and can be accessed programmatically and can include Submitting a Form, Skipping a Form, Creating a new Business Process, etc. It's up to a end user whether or not they use them or even just play around with them to figure out the endpoint calls of forms. It's one component of many to Laserfiche.

IMO there is nothing wrong with deep diving on software you buy, totally pulling it apart and figuring out how it works and sharing that with other users who may not have that knowledge. Whether it be networks calls, APIs(There are still a lot of users out there who don't know Workflow has an official API, for example), SQL, etc.

Thanks again Jason!

You are not allowed to follow up in this post.

Sign in to reply to this post.