1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-04-15 11:56:42 +02:00

756 lines
14 KiB
Go
Raw Normal View History

2018-09-26 22:03:06 -04:00
package dynamic
2018-09-18 16:42:38 -04:00
import (
"context"
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-28 00:28:33 -04:00
"github.com/MontFerret/ferret/pkg/runtime/logging"
2018-09-23 04:33:20 -04:00
"github.com/MontFerret/ferret/pkg/runtime/values"
2018-09-26 22:03:06 -04:00
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/eval"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic/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-28 00:28:33 -04:00
"github.com/rs/zerolog"
2018-10-05 15:17:22 -04:00
"hash/fnv"
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
)
2018-10-05 20:42:23 -04:00
const BlankPageURL = "about:blank"
2018-10-04 21:37:28 -04:00
type HTMLDocument struct {
2018-09-25 17:58:57 -04:00
sync.Mutex
2018-09-28 00:28:33 -04:00
logger *zerolog.Logger
2018-09-25 17:58:57 -04:00
conn *rpcc.Conn
client *cdp.Client
events *events.EventBroker
url values.String
2018-10-05 19:40:09 -04:00
element *HTMLElement
2018-09-18 16:42:38 -04:00
}
func LoadHTMLDocument(
2018-09-18 16:42:38 -04:00
ctx context.Context,
conn *rpcc.Conn,
url string,
) (*HTMLDocument, error) {
2018-09-18 16:42:38 -04:00
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
}
2018-10-05 20:42:23 -04:00
if url != BlankPageURL {
2018-10-04 21:37:28 -04:00
err = waitForLoadEvent(ctx, client)
2018-09-18 16:42:38 -04:00
2018-10-04 21:37:28 -04:00
if err != nil {
return nil, err
}
2018-09-18 16:42:38 -04:00
}
root, innerHTML, 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
}
return NewHTMLDocument(
2018-09-28 21:04:16 -04:00
logging.FromContext(ctx),
2018-09-28 00:28:33 -04:00
conn,
client,
broker,
root,
innerHTML,
2018-09-28 00:28:33 -04:00
), nil
}
2018-09-26 22:03:06 -04:00
func getRootElement(client *cdp.Client) (dom.Node, values.String, 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-26 22:03:06 -04:00
ctx := context.Background()
2018-09-18 16:42:38 -04:00
2018-09-26 22:03:06 -04:00
d, err := client.DOM.GetDocument(ctx, args)
2018-09-18 16:42:38 -04:00
if err != nil {
2018-09-26 22:03:06 -04:00
return dom.Node{}, values.EmptyString, err
}
innerHTML, err := client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetNodeID(d.Root.NodeID))
2018-09-25 17:58:57 -04:00
if err != nil {
2018-09-26 22:03:06 -04:00
return dom.Node{}, values.EmptyString, err
2018-09-25 17:58:57 -04:00
}
return d.Root, values.NewString(innerHTML.OuterHTML), nil
2018-09-18 16:42:38 -04:00
}
func NewHTMLDocument(
2018-09-28 00:28:33 -04:00
logger *zerolog.Logger,
2018-09-25 17:58:57 -04:00
conn *rpcc.Conn,
client *cdp.Client,
broker *events.EventBroker,
2018-09-26 22:03:06 -04:00
root dom.Node,
innerHTML values.String,
) *HTMLDocument {
doc := new(HTMLDocument)
2018-09-28 00:28:33 -04:00
doc.logger = logger
2018-09-25 17:58:57 -04:00
doc.conn = conn
doc.client = client
doc.events = broker
2018-10-05 19:40:09 -04:00
doc.element = NewHTMLElement(doc.logger, client, broker, root.NodeID, root, innerHTML)
2018-09-25 17:58:57 -04:00
doc.url = ""
if root.BaseURL != nil {
doc.url = values.NewString(*root.BaseURL)
2018-09-25 17:58:57 -04:00
}
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
updated, innerHTML, err := getRootElement(client)
2018-09-25 17:58:57 -04:00
if err != nil {
2018-09-28 00:28:33 -04:00
doc.logger.Error().
Timestamp().
Err(err).
Msg("failed to get root node after page load")
2018-09-25 17:58:57 -04:00
return
}
2018-09-27 11:32:52 -04:00
// close the prev element
doc.element.Close()
2018-09-25 17:58:57 -04:00
// create a new root element wrapper
2018-10-05 19:40:09 -04:00
doc.element = NewHTMLElement(doc.logger, client, broker, updated.NodeID, updated, innerHTML)
2018-09-25 17:58:57 -04:00
doc.url = ""
if updated.BaseURL != nil {
doc.url = values.NewString(*updated.BaseURL)
2018-09-25 17:58:57 -04:00
}
})
return doc
}
func (doc *HTMLDocument) MarshalJSON() ([]byte, error) {
2018-09-25 17:58:57 -04:00
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
2018-09-18 16:42:38 -04:00
}
func (doc *HTMLDocument) String() string {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.url.String()
2018-09-18 16:42:38 -04:00
}
func (doc *HTMLDocument) Unwrap() interface{} {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element
}
func (doc *HTMLDocument) Hash() uint64 {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
2018-10-05 15:17:22 -04:00
h := fnv.New64a()
2018-09-25 17:58:57 -04:00
2018-10-05 15:17:22 -04:00
h.Write([]byte(doc.Type().String()))
h.Write([]byte(":"))
h.Write([]byte(doc.url))
2018-09-25 17:58:57 -04:00
2018-10-05 15:17:22 -04:00
return h.Sum64()
2018-09-25 17:58:57 -04:00
}
func (doc *HTMLDocument) Clone() core.Value {
2018-09-27 11:53:26 -04:00
return values.None
}
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)
2018-09-18 16:42:38 -04:00
return doc.url.Compare(other.url)
2018-09-18 16:42:38 -04:00
default:
if other.Type() > core.HTMLDocumentType {
2018-09-18 16:42:38 -04:00
return -1
}
return 1
}
}
2018-09-23 04:33:20 -04:00
func (doc *HTMLDocument) Close() error {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
2018-09-28 00:28:33 -04:00
var err error
err = doc.events.Stop()
if err != nil {
doc.logger.Warn().
Timestamp().
Str("url", doc.url.String()).
Err(err).
Msg("failed to stop event broker")
}
err = doc.events.Close()
if err != nil {
doc.logger.Warn().
Timestamp().
Str("url", doc.url.String()).
Err(err).
Msg("failed to close event broker")
}
err = doc.element.Close()
2018-09-25 17:58:57 -04:00
2018-09-28 00:28:33 -04:00
if err != nil {
doc.logger.Warn().
Timestamp().
Str("url", doc.url.String()).
Err(err).
Msg("failed to close root element")
}
err = doc.client.Page.Close(context.Background())
if err != nil {
doc.logger.Warn().
Timestamp().
Str("url", doc.url.String()).
Err(err).
Msg("failed to close browser page")
}
2018-09-25 17:58:57 -04:00
return doc.conn.Close()
}
func (doc *HTMLDocument) NodeType() values.Int {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.NodeType()
}
func (doc *HTMLDocument) NodeName() values.String {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.NodeName()
}
func (doc *HTMLDocument) Length() values.Int {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.Length()
}
func (doc *HTMLDocument) InnerText() values.String {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.InnerText()
}
func (doc *HTMLDocument) InnerHTML() values.String {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.InnerHTML()
2018-09-25 17:58:57 -04:00
}
func (doc *HTMLDocument) Value() core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.Value()
}
func (doc *HTMLDocument) GetAttributes() core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttributes()
}
func (doc *HTMLDocument) GetAttribute(name values.String) core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.GetAttribute(name)
}
func (doc *HTMLDocument) GetChildNodes() core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNodes()
}
func (doc *HTMLDocument) GetChildNode(idx values.Int) core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.GetChildNode(idx)
}
func (doc *HTMLDocument) QuerySelector(selector values.String) core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelector(selector)
}
func (doc *HTMLDocument) QuerySelectorAll(selector values.String) core.Value {
2018-09-25 17:58:57 -04:00
doc.Lock()
defer doc.Unlock()
return doc.element.QuerySelectorAll(selector)
}
func (doc *HTMLDocument) URL() core.Value {
return doc.url
}
func (doc *HTMLDocument) InnerHTMLBySelector(selector values.String) values.String {
2018-09-27 22:03:35 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return "";
}
return el.innerHTML;
2018-09-27 22:03:35 -04:00
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get inner HTML by selector")
return values.EmptyString
2018-09-27 22:03:35 -04:00
}
if res.Type() == core.StringType {
return res.(values.String)
2018-09-27 22:03:35 -04:00
}
return values.EmptyString
2018-09-27 22:03:35 -04:00
}
func (doc *HTMLDocument) InnerHTMLBySelectorAll(selector values.String) *values.Array {
2018-09-27 22:03:35 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
2018-09-28 00:28:33 -04:00
var result = [];
2018-09-27 22:03:35 -04:00
var elements = document.querySelectorAll(%s);
if (elements == null) {
2018-09-28 00:28:33 -04:00
return result;
2018-09-27 22:03:35 -04:00
}
2018-09-28 00:28:33 -04:00
elements.forEach((i) => {
result.push(i.innerHTML);
2018-09-28 00:28:33 -04:00
});
return result;
2018-09-27 22:03:35 -04:00
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get an array of inner HTML by selector")
return values.NewArray(0)
2018-09-27 22:03:35 -04:00
}
if res.Type() == core.ArrayType {
return res.(*values.Array)
2018-09-27 22:03:35 -04:00
}
return values.NewArray(0)
2018-09-27 22:03:35 -04:00
}
func (doc *HTMLDocument) InnerTextBySelector(selector values.String) values.String {
2018-09-27 22:03:35 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return "";
}
return el.innerText;
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get inner text by selector")
return values.EmptyString
2018-09-27 22:03:35 -04:00
}
if res.Type() == core.StringType {
return res.(values.String)
2018-09-27 22:03:35 -04:00
}
return values.EmptyString
2018-09-27 22:03:35 -04:00
}
func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) *values.Array {
2018-09-27 22:03:35 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
2018-09-28 00:28:33 -04:00
var result = [];
2018-09-27 22:03:35 -04:00
var elements = document.querySelectorAll(%s);
if (elements == null) {
2018-09-28 00:28:33 -04:00
return result;
2018-09-27 22:03:35 -04:00
}
2018-09-28 00:28:33 -04:00
elements.forEach((i) => {
result.push(i.innerText);
});
return result;
2018-09-27 22:03:35 -04:00
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get an array inner text by selector")
return values.NewArray(0)
2018-09-27 22:03:35 -04:00
}
if res.Type() == core.ArrayType {
return res.(*values.Array)
2018-09-27 22:03:35 -04:00
}
return values.NewArray(0)
2018-09-27 22:03:35 -04:00
}
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(`
2018-09-27 21:41:41 -04:00
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var evt = new window.MouseEvent('click', { bubbles: true });
el.dispatchEvent(evt);
2018-09-27 22:03:35 -04:00
return true;
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
return values.False, err
}
if res.Type() == core.BooleanType {
return res.(values.Boolean), nil
}
return values.False, nil
}
func (doc *HTMLDocument) ClickBySelectorAll(selector values.String) (values.Boolean, error) {
2018-09-27 22:03:35 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
var elements = document.querySelectorAll(%s);
if (elements == null) {
return false;
}
elements.forEach((el) => {
var evt = new window.MouseEvent('click', { bubbles: true });
el.dispatchEvent(evt);
});
return true;
2018-09-27 21:41:41 -04:00
`, eval.ParamString(selector.String())),
true,
false,
)
if err != nil {
return values.False, err
}
if res.Type() == core.BooleanType {
return res.(values.Boolean), nil
}
return values.False, nil
}
func (doc *HTMLDocument) InputBySelector(selector values.String, value core.Value) (values.Boolean, error) {
2018-09-27 21:41:41 -04:00
res, err := eval.Eval(
doc.client,
fmt.Sprintf(
`
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var evt = new window.Event('input', { bubbles: true });
el.value = %s
el.dispatchEvent(evt);
return true;
`,
eval.ParamString(selector.String()),
eval.ParamString(value.String()),
),
true,
false,
)
if err != nil {
return values.False, err
}
if res.Type() == core.BooleanType {
return res.(values.Boolean), nil
}
return values.False, nil
}
func (doc *HTMLDocument) WaitForSelector(selector values.String, timeout values.Int) error {
task := events.NewEvalWaitTask(
2018-09-23 04:33:20 -04:00
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
2018-09-23 04:33:20 -04:00
if (el != null) {
return true;
}
// null means we need to repeat
2018-09-23 04:33:20 -04:00
return null;
2018-09-27 21:41:41 -04:00
`, eval.ParamString(selector.String())),
2018-09-23 04:33:20 -04:00
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
}
func (doc *HTMLDocument) WaitForClass(selector, class values.String, timeout values.Int) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var className = %s;
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
),
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (doc *HTMLDocument) WaitForClassAll(selector, class values.String, timeout values.Int) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var elements = document.querySelectorAll(%s);
if (elements == null || elements.length === 0) {
return false;
}
var className = %s;
var foundCount = 0;
elements.forEach((el) => {
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
foundCount++;
}
});
if (foundCount === elements.length) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
),
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (doc *HTMLDocument) WaitForNavigation(timeout values.Int) error {
2018-09-25 17:58:57 -04:00
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 {
2018-10-04 21:37:28 -04:00
if url == "" {
2018-10-05 20:42:23 -04:00
url = BlankPageURL
2018-10-04 21:37:28 -04:00
}
2018-09-25 19:04:07 -04:00
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)
}
2018-10-04 21:37:28 -04:00
return doc.WaitForNavigation(5000)
2018-09-25 19:04:07 -04:00
}