ref: c9c7b73c0740cf8d6bdd6469226b4d4b2001cb9e
parent: 5f020655d08a351cb5b74d0ba7f6a998eb5be528
author: Philip Silva <philip.silva@protonmail.com>
date: Sat Feb 6 12:31:47 EST 2021
ajax
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
- Server-side rendered websites
- Images (pre-loaded all at once though)
- TLS
-- experimental JS/DOM without AJAX can be activated (basically script tags are evaluated)
+- experimental JS/DOM can be activated (very basic jQuery examples work)
- file downloads
# Install
@@ -66,7 +66,20 @@
# JS support
-Very experimental support for that, the whole page is re-rendered after click events. (http://psilva.sdf.org/demo.gif) Mostly based on goja (ECMAScript 5.1) and github.com/fgnass/domino (DOM implementation in JS). Some sort of DOM diffing is needed, also AJAX functions, `getComputedStyle` etc. are either missing or stubs. Very simple jQuery based code works though, e.g. jQuery UI Tab view https://jqueryui.com/resources/demos/tabs/default.html or the toggle buttons on https://golang.org/pkg There is also highly experimental ES6 support with Babel.
+It's more like a demo and it's not really clear right now how much sandboxing
+is really needed. A rudimentary AJAX implementation
+is there though.
+
+Use on your own Risk!
+
+
+
+Mostly based on goja (ECMAScript 5.1) and https://github.com/fgnass/domino
+(DOM implementation in JS). Some sort of DOM diffing
+is needed, also AJAX functions, `getComputedStyle` etc. are either missing or stubs.
+Very simple jQuery based code works though, e.g. jQuery UI Tab view
+https://jqueryui.com/resources/demos/tabs/default.html or the toggle buttons on
+https://golang.org/pkg There is also highly experimental ES6 support with Babel.
Try on Plan 9 with e.g.:
--- a/browser/browser.go
+++ b/browser/browser.go
@@ -245,7 +245,7 @@
} else { log.Printf("box background: %f", err)}
-
+
if p, err = n.Tlbr("padding"); err != nil { log.Errorf("padding: %v", err)}
@@ -1262,6 +1262,10 @@
addr = b.URL().Scheme + "://" + b.URL().Host + addr
}
return url.Parse(addr)
+}
+
+func (b *Browser) Origin() *url.URL {+ return b.History.URL()
}
func (b *Browser) Back() (e duit.Event) {--- a/browser/experimental_test.go
+++ b/browser/experimental_test.go
@@ -40,7 +40,7 @@
doc, err := html.Parse(buf)
if err != nil { t.Fatalf(err.Error()) } nt := nodes.NewNodeTree(doc, style.Map{}, make(map[*html.Node]style.Map), nil)- d := domino.NewDomino(h, nt)
+ d := domino.NewDomino(h, nil, nt)
d.Start()
jq, err := ioutil.ReadFile("../domino/jquery-3.5.1.js") if err != nil {--- a/browser/website.go
+++ b/browser/website.go
@@ -120,7 +120,7 @@
log.Infof("Stop existing JS instance")w.d.Stop()
}
- w.d = domino.NewDomino(w.html, nt)
+ w.d = domino.NewDomino(w.html, browser, nt)
w.d.Start()
jsProcessed, err := processJS2(w.d, codes)
if err == nil {--- a/domino/domino.go
+++ b/domino/domino.go
@@ -1,8 +1,10 @@
package domino
import (
+ "errors"
"fmt"
"github.com/dop251/goja"
+ "github.com/dop251/goja/parser"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/require"
@@ -12,8 +14,12 @@
"github.com/psilva261/opossum"
"github.com/psilva261/opossum/logger"
"github.com/psilva261/opossum/nodes"
+ "net/http"
+ "os"
+ "path/filepath"
"strconv"
"strings"
+ "syscall"
"time"
)
@@ -26,6 +32,7 @@
}
type Domino struct {+ fetcher opossum.Fetcher
loop *eventloop.EventLoop
html string
nt *nodes.Node
@@ -33,9 +40,10 @@
domChanged chan int
}
-func NewDomino(html string, nt *nodes.Node) (d *Domino) {+func NewDomino(html string, fetcher opossum.Fetcher, nt *nodes.Node) (d *Domino) { d = &Domino{html: html,
+ fetcher: fetcher,
nt: nt,
}
return
@@ -100,6 +108,22 @@
log.Infof("js code: %v", code[:maxWidth])}
+func srcLoader(fn string) ([]byte, error) {+ path := filepath.FromSlash(fn)
+ if !strings.Contains(path, "/domino-lib/") || !strings.HasSuffix(path, ".js") {+ return nil, require.ModuleFileDoesNotExistError
+ }
+ data, err := ioutil.ReadFile(path)
+ if err != nil {+ if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {+ err = require.ModuleFileDoesNotExistError
+ } else {+ log.Errorf("srcLoader: handling of require('%v') is not implemented", fn)+ }
+ }
+ return data, err
+}
+
func (d *Domino) Exec(script string, initial bool) (res string, err error) {script = strings.Replace(script, "const ", "var ", -1)
script = strings.Replace(script, "let ", "var ", -1)
@@ -152,6 +176,44 @@
userAgent: 'opossum'
};
HTMLElement = domino.impl.HTMLElement;
+
+ function XMLHttpRequest() {+ var _method, _uri;
+ var h = {};+ var ls = {};+
+ this.readyState = 0;
+
+ var cb = function(data, err) {+ if (data !== '') {+ this.responseText = data;
+ this.readyState = 4;
+ this.state = 200;
+ this.status = 200;
+ if (ls['load']) ls['load'].bind(this)();
+ if (this.onload) this.onload.bind(this)();
+ if (this.onreadystatechange) this.onreadystatechange.bind(this)();
+ }
+ }.bind(this);
+
+ this.addEventListener = function(k, fn) {+ ls[k] = fn;
+ };
+ this.open = function(method, uri) {+ _method = method;
+ _uri = uri;
+ };
+ this.setRequestHeader = function(k, v) {+ h[k] = v;
+ };
+ this.send = function(data) {+ opossum.xhr(_method, _uri, h, data, cb);
+ this.readyState = 2;
+ };
+ this.getAllResponseHeaders = function() {+ return '';
+ };
+ }
` + script
if !initial {SCRIPT = script
@@ -169,6 +231,8 @@
log.Printf("RunOnLoop") if initial {+ vm.SetParserOptions(parser.WithDisableSourceMaps)
+
// find domino-lib folder
registry := require.NewRegistry(
require.WithGlobalFolders(
@@ -176,6 +240,9 @@
"..", // tests
"../..", // go run
),
+ require.WithLoader(
+ require.SourceLoader(srcLoader),
+ ),
)
console.Enable(vm)
@@ -186,6 +253,7 @@
HTML string `json:"html"`
Referrer func() string `json:"referrer"`
Style func(string, string, string, string) string `json:"style"`
+ XHR func(string, string, map[string]string, string, func(string, string)) `json:"xhr"`
}
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))@@ -205,6 +273,7 @@
}
return res[0].Css(prop)
},
+ XHR: d.xhr,
})
}
@@ -421,6 +490,42 @@
f(doc)
return
+}
+
+func (d *Domino) xhr(method, uri string, h map[string]string, data string, cb func(data string, err string)) {+ c := &http.Client{}+ u, err := d.fetcher.LinkedUrl(uri)
+ if err != nil {+ cb("", err.Error())+ return
+ }
+ if u.Host != d.fetcher.Origin().Host {+ log.Infof("origin: %v", d.fetcher.Origin())+ log.Infof("uri: %v", uri)+ cb("", "cannot do crossorigin request to " + u.String())+ return
+ }
+ fmt.Printf("data=%+v\n", data)+ req, err := http.NewRequest(method, u.String(), strings.NewReader(data))
+ if err != nil {+ cb("", err.Error())+ return
+ }
+ for k, v := range h {+ req.Header.Add(k, v)
+ }
+ resp, err := c.Do(req)
+ if err != nil {+ cb("", err.Error())+ return
+ }
+ defer resp.Body.Close()
+ bs, err := ioutil.ReadAll(resp.Body)
+ if err != nil {+ cb("", err.Error())+ return
+ }
+ cb(string(bs), "")
}
// AJAX:
--- a/domino/domino_test.go
+++ b/domino/domino_test.go
@@ -2,12 +2,15 @@
import (
"io/ioutil"
+ "github.com/psilva261/opossum"
"github.com/psilva261/opossum/logger"
"github.com/psilva261/opossum/nodes"
"github.com/psilva261/opossum/style"
"golang.org/x/net/html"
+ "net/url"
"strings"
"testing"
+ "time"
)
const simpleHTML = `
@@ -28,7 +31,7 @@
}
func TestSimple(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
s := `
var state = 'empty';
@@ -56,7 +59,7 @@
}
func TestGlobals(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
}
@@ -65,7 +68,7 @@
if err != nil { t.Fatalf("%v", err)}
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
$(document).ready(function() {@@ -98,7 +101,7 @@
if err != nil { t.Fatalf("%v", err)}
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
$(document).ready(function() {@@ -135,7 +138,7 @@
</body>
</html>
`
- d := NewDomino(h, nil)
+ d := NewDomino(h, nil, nil)
r := strings.NewReader(h)
doc, err := html.Parse(r)
if err != nil { t.Fatalf(err.Error()) }@@ -161,7 +164,7 @@
if err != nil { t.Fatalf("%v", err)}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "goversion.js", "godocs.js"} { buf, err := ioutil.ReadFile("godoc/"+fn)@@ -181,7 +184,7 @@
if err != nil { t.Fatalf("%v", err)}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "playground.js", "goversion.js", "godocs.js", "golang.js"} { buf, err := ioutil.ReadFile("godoc/"+fn)@@ -212,7 +215,7 @@
if err != nil { t.Fatalf("%v", err)}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
script := `
Object.assign(this, window);
@@ -258,7 +261,7 @@
//elem.dispatchEvent(event);
console.log(window.location.href);
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {@@ -288,7 +291,7 @@
});
});
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {@@ -353,7 +356,7 @@
//elem.dispatchEvent(event);
console.log(window.location.href);
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {@@ -377,7 +380,7 @@
}
func TestTrackChanges(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err := d.Exec(``, true)
if err != nil {@@ -500,7 +503,7 @@
}*/
func TestES6(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
console.log('Hello!!');@@ -522,7 +525,7 @@
}
func TestWindowParent(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
console.log('Hello!!')@@ -543,7 +546,7 @@
}
func TestReferrer(t *testing.T) {- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
document.referrer;
@@ -556,5 +559,98 @@
if res != "https://example.com" {t.Fatal()
}
+ d.Stop()
+}
+
+type MockBrowser struct {+ origin *url.URL
+ linkedUrl *url.URL
+}
+
+func (mb *MockBrowser) LinkedUrl(string) (*url.URL, error) {+ return mb.linkedUrl, nil
+}
+
+func (mb *MockBrowser) Origin() (*url.URL) {+ return mb.origin
+}
+
+func (mb *MockBrowser) Get(*url.URL) (bs []byte, ct opossum.ContentType, err error) {+ return
+}
+
+func TestXMLHttpRequest(t *testing.T) {+ mb := &MockBrowser{}+ mb.origin, _ = url.Parse("https://example.com")+ mb.linkedUrl, _ = url.Parse("https://example.com")+ d := NewDomino(simpleHTML, mb, nil)
+ d.Start()
+ script := `
+ var oReq = new XMLHttpRequest();
+ var loaded = false;
+ oReq.addEventListener("load", function() {+ console.log('loaded!!!!! !!! 11!!!1!!elf!!!1!');+ loaded = true;
+ });
+ console.log(oReq.open);
+ console.log('open:');+ oReq.open("GET", "http://www.example.org/example.txt");+ console.log('send:');+ oReq.send();
+ console.log('return:');+ `
+ _, err := d.Exec(script, true)
+ if err != nil {+ t.Fatalf("%v", err)+ }
+ <-time.After(time.Second)
+ res, err := d.Exec("oReq.responseText;", false)+ if err != nil {+ t.Fatalf("%v", err)+ }
+ t.Logf("res=%v", res)+ if !strings.Contains(res, "<html") {+ t.Fatal()
+ }
+ d.Stop()
+}
+
+func TestJQueryAjax(t *testing.T) {+ mb := &MockBrowser{}+ mb.origin, _ = url.Parse("https://example.com")+ mb.linkedUrl, _ = url.Parse("https://example.com")+ buf, err := ioutil.ReadFile("jquery-3.5.1.js")+ if err != nil {+ t.Fatalf("%v", err)+ }
+ d := NewDomino(simpleHTML, mb, nil)
+ d.Start()
+ script := `
+ var res;
+ $.ajax({+ url: '/',
+ success: function() {+ console.log('success!!!');+ res = 'success';
+ },
+ error: function() {+ console.log('error!!!');+ res = 'err';
+ }
+ });
+ `
+ _, err = d.Exec(string(buf) + ";" + script, true)
+ if err != nil {+ t.Fatalf("%v", err)+ }
+ if err = d.CloseDoc(); err != nil {+ t.Fatalf("%v", err)+ }
+ <-time.After(time.Second)
+ res, err := d.Exec("res;", false)+ if err != nil {+ t.Fatalf("%v", err)+ }
+ t.Logf("res=%v", res)d.Stop()
}
--- a/opossum.go
+++ b/opossum.go
@@ -17,6 +17,8 @@
}
type Fetcher interface {+ Origin() *url.URL
+
// LinkedUrl relative to current page
LinkedUrl(string) (*url.URL, error)
@@ -57,7 +59,7 @@
}
func (c ContentType) IsCSS() bool {- return c.MediaType != "text/html"
+ return c.MediaType != "text/html"
}
func (c ContentType) IsJS() bool {@@ -70,7 +72,7 @@
}
func (c ContentType) IsPlain() bool {- return c.MediaType == "text/plain"
+ return c.MediaType == "text/plain"
}
func (c ContentType) IsDownload() bool {@@ -79,7 +81,7 @@
}
func (c ContentType) IsSvg() bool {- return c.MediaType == "image/svg+xml"
+ return c.MediaType == "image/svg+xml"
}
func (c ContentType) Utf8(buf []byte) []byte {--
⑨