1
0
mirror of https://github.com/MontFerret/ferret.git synced 2024-12-16 11:37:36 +02:00
ferret/pkg/html/dynamic/element.go

738 lines
14 KiB
Go
Raw Normal View History

2018-09-27 04:03:06 +02:00
package dynamic
2018-09-18 22:42:38 +02:00
import (
"bytes"
"context"
"encoding/json"
"github.com/MontFerret/ferret/pkg/html/common"
"github.com/MontFerret/ferret/pkg/html/dynamic/eval"
"github.com/MontFerret/ferret/pkg/html/dynamic/events"
2018-09-18 22:42:38 +02:00
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/PuerkitoBio/goquery"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
2018-09-28 06:28:33 +02:00
"github.com/rs/zerolog"
2018-10-05 21:17:22 +02:00
"hash/fnv"
2018-09-18 22:42:38 +02:00
"strconv"
"strings"
2018-09-27 04:03:06 +02:00
"sync"
2018-09-18 22:42:38 +02:00
"time"
)
const DefaultTimeout = time.Second * 30
type HTMLElement struct {
2018-09-27 04:03:06 +02:00
sync.Mutex
2018-09-28 06:28:33 +02:00
logger *zerolog.Logger
client *cdp.Client
2018-09-27 04:03:06 +02:00
broker *events.EventBroker
2018-09-27 06:45:07 +02:00
connected values.Boolean
id dom.NodeID
nodeType values.Int
nodeName values.String
innerHTML values.String
2018-09-27 04:03:06 +02:00
innerText *common.LazyValue
2018-09-27 23:19:55 +02:00
value core.Value
2018-09-28 06:28:33 +02:00
rawAttrs []string
2018-09-27 04:03:06 +02:00
attributes *common.LazyValue
children []dom.NodeID
2018-09-27 04:03:06 +02:00
loadedChildren *common.LazyValue
2018-09-18 22:42:38 +02:00
}
func LoadElement(
2018-09-28 06:28:33 +02:00
logger *zerolog.Logger,
2018-09-18 22:42:38 +02:00
client *cdp.Client,
2018-09-27 04:03:06 +02:00
broker *events.EventBroker,
2018-09-18 22:42:38 +02:00
id dom.NodeID,
) (*HTMLElement, error) {
2018-09-18 22:42:38 +02:00
if client == nil {
return nil, core.Error(core.ErrMissedArgument, "client")
}
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
node, err := client.DOM.DescribeNode(
ctx,
dom.
NewDescribeNodeArgs().
SetNodeID(id).
2018-09-27 06:26:56 +02:00
SetDepth(1),
2018-09-18 22:42:38 +02:00
)
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(id)))
}
innerHTML, err := loadInnerHTML(client, id)
2018-09-27 04:03:06 +02:00
if err != nil {
return nil, core.Error(err, strconv.Itoa(int(id)))
}
return NewHTMLElement(
2018-09-28 06:28:33 +02:00
logger,
2018-09-27 04:03:06 +02:00
client,
broker,
id,
node.Node,
innerHTML,
2018-09-27 04:03:06 +02:00
), nil
}
func NewHTMLElement(
2018-09-28 06:28:33 +02:00
logger *zerolog.Logger,
client *cdp.Client,
2018-09-27 04:03:06 +02:00
broker *events.EventBroker,
id dom.NodeID,
node dom.Node,
innerHTML values.String,
) *HTMLElement {
el := new(HTMLElement)
2018-09-28 06:28:33 +02:00
el.logger = logger
el.client = client
2018-09-27 04:03:06 +02:00
el.broker = broker
2018-09-27 06:45:07 +02:00
el.connected = values.True
el.id = id
el.nodeType = values.NewInt(node.NodeType)
el.nodeName = values.NewString(node.NodeName)
el.innerHTML = innerHTML
2018-09-28 06:28:33 +02:00
el.innerText = common.NewLazyValue(el.loadInnerText)
el.rawAttrs = node.Attributes[:]
el.attributes = common.NewLazyValue(el.loadAttrs)
2018-09-27 06:45:07 +02:00
el.value = values.EmptyString
2018-09-28 06:28:33 +02:00
el.loadedChildren = common.NewLazyValue(el.loadChildren)
if node.Value != nil {
2018-09-27 06:45:07 +02:00
el.value = values.NewString(*node.Value)
}
2018-09-27 06:26:56 +02:00
el.children = createChildrenArray(node.Children)
2018-09-27 17:32:52 +02:00
broker.AddEventListener("reload", el.handlePageReload)
broker.AddEventListener("attr:modified", el.handleAttrModified)
broker.AddEventListener("attr:removed", el.handleAttrRemoved)
broker.AddEventListener("children:count", el.handleChildrenCountChanged)
broker.AddEventListener("children:inserted", el.handleChildInserted)
broker.AddEventListener("children:deleted", el.handleChildDeleted)
return el
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Close() error {
2018-09-27 17:32:52 +02:00
el.Lock()
defer el.Unlock()
// already closed
if !el.connected {
return nil
}
el.connected = false
el.broker.RemoveEventListener("reload", el.handlePageReload)
el.broker.RemoveEventListener("attr:modified", el.handleAttrModified)
el.broker.RemoveEventListener("attr:removed", el.handleAttrRemoved)
el.broker.RemoveEventListener("children:count", el.handleChildrenCountChanged)
el.broker.RemoveEventListener("children:inserted", el.handleChildInserted)
el.broker.RemoveEventListener("children:deleted", el.handleChildDeleted)
2018-09-18 22:42:38 +02:00
return nil
}
func (el *HTMLElement) Type() core.Type {
return core.HTMLElementType
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) MarshalJSON() ([]byte, error) {
val, err := el.innerText.Read()
if err != nil {
return nil, err
}
return json.Marshal(val.String())
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) String() string {
return el.InnerHTML().String()
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Compare(other core.Value) int {
2018-09-18 22:42:38 +02:00
switch other.Type() {
2018-10-06 01:40:09 +02:00
case core.HTMLDocumentType:
other := other.(*HTMLElement)
2018-09-18 22:42:38 +02:00
id := int(el.id)
otherID := int(other.id)
2018-09-18 22:42:38 +02:00
if id == otherID {
2018-09-18 22:42:38 +02:00
return 0
}
if id > otherID {
2018-09-18 22:42:38 +02:00
return 1
}
return -1
default:
if other.Type() > core.HTMLElementType {
2018-09-18 22:42:38 +02:00
return -1
}
return 1
}
}
func (el *HTMLElement) Unwrap() interface{} {
return el
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Hash() uint64 {
2018-09-28 06:28:33 +02:00
el.Lock()
defer el.Unlock()
2018-10-05 21:17:22 +02:00
h := fnv.New64a()
2018-09-28 06:28:33 +02:00
2018-10-05 21:17:22 +02:00
h.Write([]byte(el.Type().String()))
h.Write([]byte(":"))
h.Write([]byte(el.innerHTML))
2018-09-18 22:42:38 +02:00
2018-10-05 21:17:22 +02:00
return h.Sum64()
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Value() core.Value {
2018-09-27 23:19:55 +02:00
if !el.IsConnected() {
return el.value
}
ctx, cancel := contextWithTimeout()
defer cancel()
val, err := eval.Property(ctx, el.client, el.id, "value")
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
2018-10-08 03:04:36 +02:00
Int("id", int(el.id)).
2018-09-28 06:28:33 +02:00
Msg("failed to get node value")
2018-09-27 23:19:55 +02:00
return el.value
}
el.value = val
return val
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Clone() core.Value {
2018-09-27 17:53:26 +02:00
return values.None
}
func (el *HTMLElement) Length() values.Int {
return values.NewInt(len(el.children))
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) NodeType() values.Int {
return el.nodeType
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) NodeName() values.String {
return el.nodeName
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) GetAttributes() core.Value {
val, err := el.attributes.Read()
2018-09-27 04:03:06 +02:00
if err != nil {
return values.None
}
2018-09-27 17:53:26 +02:00
// returning shallow copy
return val.Clone()
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) GetAttribute(name values.String) core.Value {
attrs, err := el.attributes.Read()
2018-09-27 04:03:06 +02:00
if err != nil {
return values.None
}
val, found := attrs.(*values.Object).Get(name)
2018-09-18 22:42:38 +02:00
if !found {
return values.None
}
return val
}
func (el *HTMLElement) GetChildNodes() core.Value {
val, err := el.loadedChildren.Read()
2018-09-27 04:03:06 +02:00
if err != nil {
return values.NewArray(0)
2018-09-18 22:42:38 +02:00
}
2018-09-27 04:03:06 +02:00
return val
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) GetChildNode(idx values.Int) core.Value {
val, err := el.loadedChildren.Read()
2018-09-27 04:03:06 +02:00
if err != nil {
return values.None
2018-09-18 22:42:38 +02:00
}
2018-09-27 04:03:06 +02:00
return val.(*values.Array).Get(idx)
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) QuerySelector(selector values.String) core.Value {
2018-09-27 06:45:07 +02:00
if !el.IsConnected() {
2018-10-05 03:37:28 +02:00
return values.None
2018-09-27 06:45:07 +02:00
}
2018-09-18 22:42:38 +02:00
ctx := context.Background()
selectorArgs := dom.NewQuerySelectorArgs(el.id, selector.String())
found, err := el.client.DOM.QuerySelector(ctx, selectorArgs)
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
2018-10-08 03:04:36 +02:00
Int("id", int(el.id)).
Str("selector", selector.String()).
2018-09-28 06:28:33 +02:00
Msg("failed to retrieve a node by selector")
2018-09-18 22:42:38 +02:00
return values.None
}
2018-09-28 06:28:33 +02:00
res, err := LoadElement(el.logger, el.client, el.broker, found.NodeID)
2018-09-18 22:42:38 +02:00
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
2018-10-08 03:04:36 +02:00
Int("id", int(el.id)).
Str("selector", selector.String()).
2018-09-28 06:28:33 +02:00
Msg("failed to load a child node by selector")
2018-09-18 22:42:38 +02:00
return values.None
}
return res
}
func (el *HTMLElement) QuerySelectorAll(selector values.String) core.Value {
2018-09-27 06:45:07 +02:00
if !el.IsConnected() {
return values.NewArray(0)
}
2018-09-18 22:42:38 +02:00
ctx := context.Background()
selectorArgs := dom.NewQuerySelectorAllArgs(el.id, selector.String())
res, err := el.client.DOM.QuerySelectorAll(ctx, selectorArgs)
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
2018-10-08 03:04:36 +02:00
Int("id", int(el.id)).
Str("selector", selector.String()).
2018-09-28 06:28:33 +02:00
Msg("failed to retrieve nodes by selector")
2018-09-18 22:42:38 +02:00
return values.None
}
arr := values.NewArray(len(res.NodeIDs))
for _, id := range res.NodeIDs {
2018-09-28 06:28:33 +02:00
childEl, err := LoadElement(el.logger, el.client, el.broker, id)
2018-09-18 22:42:38 +02:00
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
2018-10-08 03:04:36 +02:00
Int("id", int(el.id)).
Str("selector", selector.String()).
2018-09-28 06:28:33 +02:00
Msg("failed to load nodes by selector")
2018-10-08 03:04:36 +02:00
// close elements that are already loaded, but won't be used because of the error
if arr.Length() > 0 {
arr.ForEach(func(e core.Value, _ int) bool {
e.(*HTMLElement).Close()
return true
})
}
2018-09-18 22:42:38 +02:00
return values.None
}
arr.Push(childEl)
}
return arr
}
func (el *HTMLElement) WaitForClass(class values.String, timeout values.Int) error {
task := events.NewWaitTask(
func() (core.Value, error) {
current := el.GetAttribute("class")
if current.Type() != core.StringType {
return values.None, nil
}
str := current.(values.String)
classStr := string(class)
classes := strings.Split(string(str), " ")
for _, c := range classes {
if c == classStr {
return values.True, nil
}
}
return values.None, nil
},
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (el *HTMLElement) InnerText() values.String {
val, err := el.innerText.Read()
2018-09-18 22:42:38 +02:00
if err != nil {
return values.EmptyString
}
2018-09-27 04:03:06 +02:00
return val.(values.String)
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) InnerHTML() values.String {
2018-09-27 06:45:07 +02:00
el.Lock()
defer el.Unlock()
return el.innerHTML
2018-09-18 22:42:38 +02:00
}
func (el *HTMLElement) Click() (values.Boolean, error) {
2018-09-27 04:03:06 +02:00
ctx, cancel := contextWithTimeout()
defer cancel()
2018-09-25 23:58:57 +02:00
return events.DispatchEvent(ctx, el.client, el.id, "click")
}
2018-09-27 06:26:56 +02:00
func (el *HTMLElement) Input(value core.Value) error {
ctx, cancel := contextWithTimeout()
defer cancel()
return el.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(el.id, "value", value.String()))
}
func (el *HTMLElement) IsConnected() values.Boolean {
2018-09-27 06:45:07 +02:00
el.Lock()
defer el.Unlock()
return el.connected
2018-09-27 06:26:56 +02:00
}
2018-09-27 17:32:52 +02:00
func (el *HTMLElement) loadInnerText() (core.Value, error) {
h := el.InnerHTML()
2018-09-28 06:28:33 +02:00
if h == values.EmptyString {
return h, nil
}
buff := bytes.NewBuffer([]byte(h))
parsed, err := goquery.NewDocumentFromReader(buff)
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to parse inner html")
return values.EmptyString, err
}
return values.NewString(parsed.Text()), nil
}
func (el *HTMLElement) loadAttrs() (core.Value, error) {
2018-09-28 06:28:33 +02:00
return parseAttrs(el.rawAttrs), nil
}
func (el *HTMLElement) loadChildren() (core.Value, error) {
2018-09-28 06:28:33 +02:00
if !el.IsConnected() {
return values.NewArray(0), nil
}
loaded, err := loadNodes(el.logger, el.client, el.broker, el.children)
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to load child nodes")
return values.None, err
}
return loaded, nil
}
2018-10-05 23:20:48 +02:00
func (el *HTMLElement) handlePageReload(_ interface{}) {
2018-09-27 17:32:52 +02:00
el.Close()
}
func (el *HTMLElement) handleAttrModified(message interface{}) {
2018-09-27 17:32:52 +02:00
reply, ok := message.(*dom.AttributeModifiedReply)
// well....
if !ok {
return
}
// it's not for this element
if reply.NodeID != el.id {
return
}
el.attributes.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-27 17:32:52 +02:00
return
}
2018-09-27 17:53:26 +02:00
attrs, ok := v.(*values.Object)
2018-09-27 17:32:52 +02:00
if !ok {
return
}
2018-09-27 17:32:52 +02:00
attrs.Set(values.NewString(reply.Name), values.NewString(reply.Value))
})
2018-09-27 17:32:52 +02:00
}
func (el *HTMLElement) handleAttrRemoved(message interface{}) {
2018-09-27 17:32:52 +02:00
reply, ok := message.(*dom.AttributeRemovedReply)
// well....
if !ok {
return
}
// it's not for this element
if reply.NodeID != el.id {
return
}
// they are not event loaded
// just ignore the event
if !el.attributes.Ready() {
return
}
el.attributes.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-27 17:32:52 +02:00
return
}
2018-09-27 17:53:26 +02:00
attrs, ok := v.(*values.Object)
2018-09-27 17:32:52 +02:00
if !ok {
return
}
2018-09-27 17:32:52 +02:00
attrs.Remove(values.NewString(reply.Name))
})
2018-09-27 17:32:52 +02:00
}
func (el *HTMLElement) handleChildrenCountChanged(message interface{}) {
2018-09-27 17:32:52 +02:00
reply, ok := message.(*dom.ChildNodeCountUpdatedReply)
if !ok {
return
}
if reply.NodeID != el.id {
return
}
node, err := el.client.DOM.DescribeNode(context.Background(), dom.NewDescribeNodeArgs())
if err != nil {
2018-09-28 06:28:33 +02:00
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-27 17:32:52 +02:00
return
}
el.Lock()
defer el.Unlock()
el.children = createChildrenArray(node.Node.Children)
}
func (el *HTMLElement) handleChildInserted(message interface{}) {
2018-09-27 17:32:52 +02:00
reply, ok := message.(*dom.ChildNodeInsertedReply)
if !ok {
return
}
if reply.ParentNodeID != el.id {
return
}
targetIDx := -1
prevID := reply.PreviousNodeID
nextID := reply.Node.NodeID
2018-09-27 17:32:52 +02:00
el.Lock()
defer el.Unlock()
for idx, id := range el.children {
if id == prevID {
targetIDx = idx
2018-09-27 17:32:52 +02:00
break
}
}
if targetIDx == -1 {
2018-09-27 17:32:52 +02:00
return
}
arr := el.children
el.children = append(arr[:targetIDx], append([]dom.NodeID{nextID}, arr[targetIDx:]...)...)
2018-09-27 17:32:52 +02:00
if !el.loadedChildren.Ready() {
return
}
el.loadedChildren.Write(func(v core.Value, err error) {
loadedArr := v.(*values.Array)
loadedEl, err := LoadElement(el.logger, el.client, el.broker, nextID)
2018-09-27 17:32:52 +02:00
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to load an inserted node")
2018-09-28 06:28:33 +02:00
return
}
2018-09-27 17:32:52 +02:00
loadedArr.Insert(values.NewInt(targetIDx), loadedEl)
2018-09-27 17:32:52 +02:00
newInnerHTML, err := loadInnerHTML(el.client, el.id)
2018-09-27 17:32:52 +02:00
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-28 06:28:33 +02:00
return
}
2018-09-27 17:32:52 +02:00
el.innerHTML = newInnerHTML
})
2018-09-27 17:32:52 +02:00
}
func (el *HTMLElement) handleChildDeleted(message interface{}) {
2018-09-27 17:32:52 +02:00
reply, ok := message.(*dom.ChildNodeRemovedReply)
if !ok {
return
}
if reply.ParentNodeID != el.id {
return
}
targetIDx := -1
targetID := reply.NodeID
2018-09-27 17:32:52 +02:00
el.Lock()
defer el.Unlock()
for idx, id := range el.children {
if id == targetID {
targetIDx = idx
2018-09-27 17:32:52 +02:00
break
}
}
if targetIDx == -1 {
2018-09-27 17:32:52 +02:00
return
}
arr := el.children
el.children = append(arr[:targetIDx], arr[targetIDx+1:]...)
2018-09-27 17:32:52 +02:00
if !el.loadedChildren.Ready() {
return
}
el.loadedChildren.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-27 17:32:52 +02:00
return
}
2018-09-27 17:32:52 +02:00
loadedArr := v.(*values.Array)
loadedArr.RemoveAt(values.NewInt(targetIDx))
2018-09-27 17:32:52 +02:00
newInnerHTML, err := loadInnerHTML(el.client, el.id)
2018-09-27 17:32:52 +02:00
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
2018-09-28 06:28:33 +02:00
return
}
2018-09-27 17:32:52 +02:00
el.innerHTML = newInnerHTML
})
2018-09-27 17:32:52 +02:00
}