1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-04-19 12:12:16 +02:00

422 lines
7.5 KiB
Go
Raw Normal View History

2018-09-18 16:42:38 -04:00
package browser
import (
"context"
2018-09-25 17:58:57 -04:00
"crypto/sha512"
2018-09-23 04:33:20 -04:00
"fmt"
2018-09-18 16:42:38 -04:00
"github.com/MontFerret/ferret/pkg/runtime/core"
2018-09-23 04:33:20 -04:00
"github.com/MontFerret/ferret/pkg/runtime/values"
2018-09-25 17:58:57 -04:00
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/browser/eval"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/browser/events"
"github.com/corpix/uarand"
2018-09-18 16:42:38 -04:00
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/emulation"
"github.com/mafredri/cdp/protocol/page"
2018-09-18 16:42:38 -04:00
"github.com/mafredri/cdp/rpcc"
2018-09-25 19:04:07 -04:00
"github.com/pkg/errors"
2018-09-18 16:42:38 -04:00
"strings"
2018-09-25 17:58:57 -04:00
"sync"
2018-09-23 04:33:20 -04:00
"time"
2018-09-18 16:42:38 -04:00
)
type HtmlDocument struct {
2018-09-25 17:58:57 -04:00
sync.Mutex
conn *rpcc.Conn
client *cdp.Client
events *events.EventBroker
url string
element *HtmlElement
2018-09-18 16:42:38 -04:00
}
2018-09-25 17:58:57 -04:00
func LoadHtmlDocument(
2018-09-18 16:42:38 -04:00
ctx context.Context,
conn *rpcc.Conn,
url string,
) (*HtmlDocument, error) {
if conn == nil {
return nil, core.Error(core.ErrMissedArgument, "connection")
}
if url == "" {
return nil, core.Error(core.ErrMissedArgument, "url")
}
client := cdp.NewClient(conn)
2018-09-25 19:04:07 -04:00
err := runBatch(
2018-09-18 16:42:38 -04:00
func() error {
return client.Page.Enable(ctx)
},
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
2018-09-18 16:42:38 -04:00
func() error {
return client.DOM.Enable(ctx)
},
func() error {
return client.Runtime.Enable(ctx)
},
func() error {
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(uarand.GetRandom()),
)
},
2018-09-18 16:42:38 -04:00
)
if err != nil {
return nil, err
}
err = waitForLoadEvent(ctx, client)
2018-09-18 16:42:38 -04:00
if err != nil {
return nil, err
}
2018-09-25 17:58:57 -04:00
root, err := getRootElement(client)
2018-09-18 16:42:38 -04:00
if err != nil {
return nil, err
}
2018-09-25 17:58:57 -04:00
broker, err := createEventBroker(client)
if err != nil {
return nil, err
}
2018-09-25 17:58:57 -04:00
return NewHtmlDocument(conn, client, root, broker), nil
}
2018-09-25 17:58:57 -04:00
func getRootElement(client *cdp.Client) (dom.Node, error) {
2018-09-18 16:42:38 -04:00
args := dom.NewGetDocumentArgs()
2018-09-25 19:04:07 -04:00
args.Depth = pointerInt(1) // lets load the entire document
2018-09-18 16:42:38 -04:00
2018-09-25 17:58:57 -04:00
d, err := client.DOM.GetDocument(context.Background(), args)
2018-09-18 16:42:38 -04:00
if err != nil {
return dom.Node{}, err
}
return d.Root, nil
}
2018-09-25 17:58:57 -04:00
func createEventBroker(client *cdp.Client) (*events.EventBroker, error) {
load, err := client.Page.LoadEventFired(context.Background())
if err != nil {
return nil, err
}
broker := events.NewEventBroker()
broker.AddEventStream("load", load, func() interface{} {
return new(page.LoadEventFiredReply)
})
err = broker.Start()
2018-09-18 16:42:38 -04:00
if err != nil {
2018-09-25 17:58:57 -04:00
broker.Close()
2018-09-18 16:42:38 -04:00
return nil, err
}
2018-09-25 17:58:57 -04:00
return broker, nil
2018-09-18 16:42:38 -04:00
}
2018-09-25 17:58:57 -04:00
func NewHtmlDocument(
conn *rpcc.Conn,
client *cdp.Client,
root dom.Node,
broker *events.EventBroker,
) *HtmlDocument {
doc := new(HtmlDocument)
doc.conn = conn
doc.client = client
doc.events = broker
doc.element = NewHtmlElement(client, root.NodeID, root)
doc.url = ""
if root.BaseURL != nil {
doc.url = *root.BaseURL
}
2018-09-25 17:58:57 -04:00
broker.AddEventListener("load", func(_ interface{}) {
doc.Lock()
defer doc.Unlock()
2018-09-18 16:42:38 -04:00
2018-09-25 17:58:57 -04:00
updated, err := getRootElement(client)
if err != nil {
// TODO: We need somehow log all errors outside of stdout
return
}
// close an old root element
doc.element.Close()
// create a new root element wrapper
doc.element = NewHtmlElement(client, updated.NodeID, updated)
doc.url = ""
if updated.BaseURL != nil {
doc.url = *updated.BaseURL
}
})
return doc
}
func (doc *HtmlDocument) MarshalJSON() ([]byte, error) {
doc.Lock()
defer doc.Unlock()
return doc.element.MarshalJSON()
2018-09-18 16:42:38 -04:00
}
func (doc *HtmlDocument) Type() core.Type {
return core.HtmlDocumentType
}
func (doc *HtmlDocument) String() string {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
2018-09-18 16:42:38 -04:00
return doc.url
}
2018-09-25 17:58:57 -04:00
func (doc *HtmlDocument) Unwrap() interface{} {
doc.Lock()
defer doc.Unlock()
return doc.element
}
func (doc *HtmlDocument) Hash() int {
doc.Lock()
defer doc.Unlock()
h := sha512.New()
out, err := h.Write([]byte(doc.url))
if err != nil {
return 0
}
return out
}
2018-09-18 16:42:38 -04:00
func (doc *HtmlDocument) Compare(other core.Value) int {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
2018-09-18 16:42:38 -04:00
switch other.Type() {
case core.HtmlDocumentType:
other := other.(*HtmlDocument)
return strings.Compare(doc.url, other.url)
default:
if other.Type() > core.HtmlDocumentType {
return -1
}
return 1
}
}
2018-09-23 04:33:20 -04:00
2018-09-25 17:58:57 -04:00
func (doc *HtmlDocument) Close() error {
doc.Lock()
defer doc.Unlock()
doc.events.Stop()
doc.events.Close()
doc.client.Page.Close(context.Background())
return doc.conn.Close()
}
func (doc *HtmlDocument) NodeType() values.Int {
doc.Lock()
defer doc.Unlock()
return doc.element.NodeType()
}
func (doc *HtmlDocument) NodeName() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.NodeName()
}
func (doc *HtmlDocument) Length() values.Int {
doc.Lock()
defer doc.Unlock()
return doc.element.Length()
}
func (doc *HtmlDocument) InnerText() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.InnerText()
}
func (doc *HtmlDocument) InnerHtml() values.String {
doc.Lock()
defer doc.Unlock()
return doc.element.InnerHtml()
}
func (doc *HtmlDocument) Value() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.Value()
}
func (doc *HtmlDocument) GetAttributes() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttributes()
}
func (doc *HtmlDocument) GetAttribute(name values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttribute(name)
}
func (doc *HtmlDocument) GetChildNodes() core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNodes()
}
func (doc *HtmlDocument) GetChildNode(idx values.Int) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNode(idx)
}
func (doc *HtmlDocument) QuerySelector(selector values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelector(selector)
}
func (doc *HtmlDocument) QuerySelectorAll(selector values.String) core.Value {
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelectorAll(selector)
}
func (doc *HtmlDocument) ClickBySelector(selector values.String) (values.Boolean, error) {
2018-09-25 17:58:57 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
var el = document.querySelector("%s");
if (el == null) {
return false;
}
var evt = new window.MouseEvent('click', { bubbles: true });
el.dispatchEvent(evt);
return true;
`, selector),
true,
false,
)
if err != nil {
return values.False, err
}
if res.Type() == core.BooleanType {
return res.(values.Boolean), nil
}
return values.False, nil
}
2018-09-23 04:33:20 -04:00
func (doc *HtmlDocument) WaitForSelector(selector values.String, timeout values.Int) error {
2018-09-25 17:58:57 -04:00
task := events.NewWaitTask(
2018-09-23 04:33:20 -04:00
doc.client,
fmt.Sprintf(`
el = document.querySelector("%s");
if (el != null) {
return true;
}
return null;
`, selector),
time.Millisecond*time.Duration(timeout),
2018-09-25 17:58:57 -04:00
events.DefaultPolling,
2018-09-23 04:33:20 -04:00
)
_, err := task.Run()
return err
}
2018-09-25 17:58:57 -04:00
func (doc *HtmlDocument) WaitForNavigation(timeout values.Int) error {
timer := time.NewTimer(time.Millisecond * time.Duration(timeout))
onEvent := make(chan bool)
listener := func(_ interface{}) {
onEvent <- true
}
2018-09-25 17:58:57 -04:00
defer doc.events.RemoveEventListener("load", listener)
defer close(onEvent)
2018-09-25 17:58:57 -04:00
doc.events.AddEventListener("load", listener)
2018-09-25 17:58:57 -04:00
for {
select {
case <-onEvent:
timer.Stop()
2018-09-25 17:58:57 -04:00
return nil
case <-timer.C:
return core.ErrTimeout
}
}
}
2018-09-25 19:04:07 -04:00
func (doc *HtmlDocument) Navigate(url values.String) error {
ctx := context.Background()
repl, err := doc.client.Page.Navigate(ctx, page.NewNavigateArgs(url.String()))
if err != nil {
return err
}
if repl.ErrorText != nil {
return errors.New(*repl.ErrorText)
}
return waitForLoadEvent(ctx, doc.client)
}