mirror of
https://github.com/karust/openserp.git
synced 2026-08-10 19:11:16 +08:00
155 lines
3.9 KiB
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
|
|
}
|