shithub: mycel

Download patch

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!
+
+![Demo](http://psilva.sdf.org/demo.gif "Demo")
+
+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 {