UNPKG

webkitgtk

Version:

webkitgtk addon with powerful Node.js API

268 lines (248 loc) 6.95 kB
const debug = require('debug')('webkitgtk'); const {JSDOM, ResourceLoader, VirtualConsole} = require('jsdom'); const httpCodes = require('http').STATUS_CODES; const URL = require('url'); const clientFetch = require('fs').readFileSync(require.resolve('whatwg-fetch/dist/fetch.umd.js')).toString(); const request = function() { // lazy loading request let request; try { request = require('request'); } catch(e) { console.error("Please `npm install request` to be able to load remote documents"); process.exit(1); } return request; }; class CustomResourceLoader extends ResourceLoader { constructor(jsdomOpts, opts) { super(jsdomOpts); Object.assign(this, opts); } fetch(url, opts) { return new Promise((resolve, reject) => { resourceLoader.call(this, url, opts, (err, body) => { if (err) return reject(err); return resolve(typeof body == "string" ? Buffer.from(body) : body); }); }); } } module.exports = function(WebKit) { WebKit.prototype.binding = function(opts, cfg, cb) { this.priv.cfg = cfg; this.priv.jsdom = true; cb(); }; WebKit.prototype.rawload = function(uri, opts, cb) { if (this.webview) this.webview.close(); const pcb = WebKit.promet(this, cb); uri = URL.format(URL.parse(uri)); const jsdomOpts = { concurrentNodeIterators: 10000, runScripts: "dangerously", resources: new CustomResourceLoader({ // jsdom opts }, { opts: opts, inst: this }) }; if (!opts.console) jsdomOpts.virtualConsole = new VirtualConsole(); const priv = this.priv; jsdomOpts.url = uri || "about:blank"; let cookies = opts.cookies; if (cookies) { if (!Array.isArray(cookies)) cookies = [cookies]; if (cookies.length) cookies = cookies.join(';'); else cookies = null; } jsdomOpts.beforeParse = (window) => { this.webview = window; window.raise = function(ev, msg, obj) { if (obj && obj.error) { throw obj.error; } }; window.destroy = window.close; window.eval(clientFetch); window.run = window.eval.bind(window); window.uri = uri; window.runSync = function(script, ticket) { let ret; try { ret = window.run(script); } catch(ex) { ret = JSON.stringify({ticket: ticket, error: ex.toString()}); } if (ret !== undefined) window.webkit.messageHandlers.events.postMessage(ret); }; if (cookies) { debug('load cookies'); window.document.cookie = cookies; window.document._cookieDomain = window.document.location.hostname; } if (opts.console) window.console = console; window.webkit = { messageHandlers: { events: { postMessage: function(value) { priv.cfg.eventsListener(null, value); }.bind(this) } } }; if (opts.script) { window.eval(opts.script); } const runlist = this._webview && this._webview._runlist; delete this._webview; if (runlist) runlist.forEach((arr) => { try { window.eval(arr); } catch(e) { console.error(e); } }); }; this._webview = this.webview = { uri: uri, _runlist: [], run: function(script, ticket) { this._runlist.push(script); }, runSync: function(script, ticket) { this._runlist.push(script); }, close: function() {} }; if ((!uri || uri == "about:blank") && opts.content == null) { opts.content = '<html><head></head><body></body></html>'; } setImmediate(() => { if (opts.content != null) { createJSDOM.call(this, opts.content, opts, jsdomOpts); pcb.cb(null, 200); } else { // trick to have a main uri before loading main doc this.webview.loading = true; const req = resourceLoader.call({inst:this, opts: opts}, uri, { cookie: cookies }, (err, body) => { this.webview.loading = false; let status = 200; if (err) { status = err.code || 0; if (typeof status == "string") status = 0; } if (status < 200 || status >= 400) err = status; if (err || status != 200) return pcb.cb(err, status); createJSDOM.call(this, body, opts, jsdomOpts); pcb.cb(null, 200); }); this.webview.stop = (cb) => { if (this.webview.loading) { this.webview.loading = false; req.abort(); setImmediate(() => { cb(true); }); pcb.cb(new Error("Aborted"), 0); } else { setImmediate(() => { cb(false); }); return false; } // return nothing and WebKit.stop will callback on our behalf }; } }); return pcb.ret; }; }; function createJSDOM(content, opts, jsdomOpts) { const inst = new JSDOM(content, jsdomOpts); this.status = 200; return inst; } function HTTPError(code) { Error.call(this, httpCodes[code]); this.code = code; return this; } HTTPError.prototype = Object.create(Error.prototype); HTTPError.prototype.constructor = HTTPError; function resourceLoader(uri, opts, cb) { // Checking if the ressource should be loaded debug("resource loader", uri); if (this.opts.preload) { cb(null, null); return; } const priv = this.inst.priv; const stamp = priv.stamp; const funcFilterStr = 'window.request_' + priv.cstamp; let result = true; if (this.inst.webview.run('!!(' + funcFilterStr + ')')) { result = this.inst.webview.run(funcFilterStr + '("' + uri + '", null)'); } if (result === false) { const err = new Error("Ressource canceled"); err.statusCode = 0; cb(err); priv.cfg.responseListener(stamp, {uri: uri, length: 0, headers: {}, status: 0}); return; } else if (typeof result == "string") { uri = result; } // actual get const req = request()(uri, opts, (err, res, body) => { const status = res && res.statusCode || 0; if (!err && status != 200) { err = new HTTPError(status); if (status == 401) { // what ? } } const headers = res && res.headers || {}; const uheaders = {}; for (const name in headers) { uheaders[name.split('-').map((str) => { return str[0].toUpperCase() + str.substring(1); }).join('-')] = headers[name]; } priv.cfg.responseListener(stamp, { uri: uri, headers: uheaders, length: body ? body.length : 0, mime: headers['content-type'], status: status, data: function(cb) { cb(null, body); } }); if (err === 200) err = null; cb(err, body); }) .on('data', (chunk) => { const headers = req.response.headers; const res = { uri: uri, length: headers['content-length'] || 0, mime: (headers['content-type'] || '').split(';').shift(), status: req.response.statusCode }; priv.cfg.receiveDataListener(stamp, res, chunk ? chunk.length : 0); }); /* FIXME this has surely changed var authResponse = req._auth.onResponse; var self = this; req._auth.onResponse = function(response) { if (this.sentAuth) return null; self.emit('authenticate', new AuthRequest(req, response)); if (!this.hasAuth) return null; return authResponse.call(this, response); }.bind(req._auth); */ return req; }