Files
openserp/core/fpcheck/detectors/custom.go

155 lines
3.9 KiB
Go

package detectors
import (
"context"
"fmt"
"net/url"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/karust/openserp/core/fpcheck"
)
const customDetectorName = "custom"
const defaultCustomSelector = "body"
type Custom struct {
targetURL string
selector string
}
type customPayload struct {
Found bool `json:"found"`
Error string `json:"error"`
Title string `json:"title"`
URL string `json:"url"`
ReadyState string `json:"readyState"`
Selector string `json:"selector"`
SelectedText string `json:"selectedText"`
SelectedHTML string `json:"selectedHTML"`
}
func NewCustom(rawURL string) (fpcheck.Detector, error) {
return NewCustomWithSelector(rawURL, "")
}
func NewCustomWithSelector(rawURL string, selector string) (fpcheck.Detector, error) {
normalized, err := normalizeCustomURL(rawURL)
if err != nil {
return nil, err
}
return Custom{
targetURL: normalized,
selector: normalizeCustomSelector(selector),
}, nil
}
func (c Custom) Name() string {
return customDetectorName
}
func (c Custom) URL() string {
return c.targetURL
}
func (c Custom) Selector() string {
return normalizeCustomSelector(c.selector)
}
func (c Custom) Extract(ctx context.Context, page *rod.Page) (map[string]fpcheck.Detection, string, error) {
selector := c.Selector()
var payload customPayload
err := waitFor(ctx, 15*time.Second, 200*time.Millisecond, func() (bool, error) {
current, err := extractCustomPayload(page, selector)
if err != nil {
return false, err
}
if !current.Found {
return false, nil
}
payload = current
return true, nil
})
if err != nil {
return nil, "", fmt.Errorf("custom page readiness for selector %q: %w", selector, err)
}
rawOut := strings.TrimSpace(payload.SelectedText)
if rawOut == "" {
rawOut = strings.TrimSpace(payload.SelectedHTML)
}
return map[string]fpcheck.Detection{
"selected_page_output": {
Detected: false,
Description: fmt.Sprintf("captured selector %q", payload.Selector),
},
}, rawOut, nil
}
func extractCustomPayload(page *rod.Page, selector string) (customPayload, error) {
res, err := page.Timeout(2*time.Second).Eval(`(selector) => {
const normalize = (value) => (value || "").replace(/\s+/g, " ").trim();
let selected = null;
try {
selected = document.querySelector(selector);
} catch (err) {
return {
found: false,
error: err && err.message ? err.message : String(err),
selector,
};
}
return {
found: !!selected,
title: document.title || "",
url: location.href || "",
readyState: document.readyState || "",
selector,
selectedText: normalize(selected ? selected.innerText || selected.textContent || "" : ""),
selectedHTML: selected ? selected.innerHTML || "" : "",
};
}`, selector)
if err != nil {
return customPayload{}, err
}
var payload customPayload
if err := res.Value.Unmarshal(&payload); err != nil {
return customPayload{}, fmt.Errorf("decode custom detector payload: %w", err)
}
if strings.TrimSpace(payload.Error) != "" {
return customPayload{}, fmt.Errorf("query selector %q: %s", selector, payload.Error)
}
return payload, nil
}
func normalizeCustomURL(rawURL string) (string, error) {
trimmed := strings.TrimSpace(rawURL)
if trimmed == "" {
return "", fmt.Errorf("custom detector requires non-empty url query parameter")
}
parsed, err := url.Parse(trimmed)
if err != nil {
return "", fmt.Errorf("invalid custom detector URL: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("invalid custom detector URL scheme %q: use http or https", parsed.Scheme)
}
if strings.TrimSpace(parsed.Host) == "" {
return "", fmt.Errorf("invalid custom detector URL: host is required")
}
return parsed.String(), nil
}
func normalizeCustomSelector(selector string) string {
trimmed := strings.TrimSpace(selector)
if trimmed == "" {
return defaultCustomSelector
}
return trimmed
}