diff --git a/doc/Messages.md b/doc/Messages.md index 248efac75..4e0f742da 100644 --- a/doc/Messages.md +++ b/doc/Messages.md @@ -173,3 +173,20 @@ by the user. Data: Response data: * `null`, but presented upon async completion. + +# UserScriptXhr +Sent by: `content/api-provider-source.js` +Received by: `bg/on-user-script-xhr.js` + +This is a channel (not a message). +Triggered when the `GM.xmlHttpRequest()` method is called by a user script. +Data: + +* `details` The details object specifying the request. + +Response data: + +* `null` + +Messages exchanged via this channel are private to its implementation. +See sender and receiver for further detail. diff --git a/manifest.json b/manifest.json index 756e02011..180209bc1 100644 --- a/manifest.json +++ b/manifest.json @@ -22,6 +22,7 @@ "src/bg/execute.js", "src/bg/is-enabled.js", "src/bg/on-message.js", + "src/bg/on-user-script-xhr.js", "src/bg/user-script-install.js", "src/bg/user-script-registry.js", "src/bg/value-store.js" diff --git a/src/bg/api-provider-source.js b/src/bg/api-provider-source.js index c4465be9b..40bdc0e80 100644 --- a/src/bg/api-provider-source.js +++ b/src/bg/api-provider-source.js @@ -9,6 +9,7 @@ to the global scope (the `this` object). It ... const SUPPORTED_APIS = new Set([ 'GM.getResourceUrl', 'GM.deleteValue', 'GM.getValue', 'GM.listValues', 'GM.setValue', + 'GM.xmlHttpRequest', ]); @@ -43,7 +44,12 @@ function apiProviderSource(userScript) { source += 'GM.setValue = ' + GM_setValue.toString() + ';\n\n'; } - // TODO: Everything else. + if (grants.includes('GM.xmlHttpRequest')) { + source += 'GM.xmlHttpRequest = ' + GM_xmlHttpRequest.toString() + ';\n\n'; + } + + // TODO: GM_registerMenuCommand -- maybe. + // TODO: GM_getResourceText -- maybe. source += '})();'; return source; @@ -124,4 +130,46 @@ function GM_setValue(key, value) { }); } + +function GM_xmlHttpRequest(d) { + if (!d) throw new Error('GM.xmlHttpRequest: Received no details.'); + if (!d.url) throw new Error('GM.xmlHttpRequest: Received no URL.'); + + let url; + try { + url = new URL(d.url, location.href); + } catch (e) { + throw new Error( + 'GM.xmlHttpRequest: Could not understand the URL: ' + d.url + + '\n' + e); + } + + if (url.protocol != 'http:' + && url.protocol != 'https:' + && url.protocol != 'ftp:' + ) { + throw new Error('GM.xmlHttpRequest: Passed URL has bad protocol: ' + d.url); + } + + let port = chrome.runtime.connect({name: 'UserScriptXhr'}); + port.onMessage.addListener(function(msg) { + let o = msg.src == 'up' ? d.upload : d; + let cb = o['on' + msg.type]; + if (cb) cb(msg.responseState); + }); + + let noCallbackDetails = {}; + Object.keys(d).forEach(k => { + let v = d[k]; + noCallbackDetails[k] = v; + if ('function' == typeof v) noCallbackDetails[k] = true; + }); + noCallbackDetails.upload = {}; + d.upload && Object.keys(k => noCallbackDetails.upload[k] = true); + noCallbackDetails.url = url.href; + port.postMessage({name: 'open', details: noCallbackDetails}); + + // TODO: Return an object which can be `.abort()`ed. +} + })(); diff --git a/src/bg/on-user-script-xhr.js b/src/bg/on-user-script-xhr.js new file mode 100644 index 000000000..cec1c8517 --- /dev/null +++ b/src/bg/on-user-script-xhr.js @@ -0,0 +1,122 @@ +/* +This file is responsible for providing the GM.xmlHttpRequest API method. It +listens for a connection on a Port, and +*/ + +// Private implementation. +(function() { + +function onUserScriptXhr(port) { + if (port.name != 'UserScriptXhr') return; + + let xhr = new XMLHttpRequest(); + port.onMessage.addListener(msg => { + switch (msg.name) { + case 'open': open(xhr, msg.details, port); break; + default: + console.warn('UserScriptXhr port un-handled message name:', msg.name); + } + }); +} +chrome.runtime.onConnect.addListener(onUserScriptXhr); + + +function open(xhr, d, port) { + function xhrEventHandler(src, event) { + console.log('xhr event;', src, event); + var responseState = { + context: d.context || null, + finalUrl: null, + lengthComputable: null, + loaded: null, + readyState: xhr.readyState, + response: xhr.response, + responseHeaders: null, + responseText: null, + responseXML: null, + status: null, + statusText: null, + total: null + }; + + try { + responseState.responseText = xhr.responseText; + } catch (e) { + // Some response types don't have .responseText (but do have e.g. blob + // .response). Ignore. + } + + var responseXML = null; + try { + responseXML = xhr.responseXML; + } catch (e) { + // Ignore failure. At least in responseType blob case, this access fails. + } + + switch (event.type) { + case "progress": + responseState.lengthComputable = evt.lengthComputable; + responseState.loaded = evt.loaded; + responseState.total = evt.total; + break; + case "error": + console.log('error event?', event); + break; + default: + if (4 != xhr.readyState) break; + responseState.responseHeaders = xhr.getAllResponseHeaders(); + responseState.status = xhr.status; + responseState.statusText = xhr.statusText; + break; + } + + port.postMessage( + {src: src, type: event.type, responseState: responseState}); + } + + [ + 'abort', 'error', 'load', 'loadend', 'loadstart', 'progress', + 'readystatechange', 'timeout' + ].forEach(v => { + if (d['on' + v]) { + xhr.addEventListener(v, xhrEventHandler.bind(null, 'down')); + } + }); + + [ + 'abort', 'error', 'load', 'loadend', 'progress', 'timeout' + ].forEach(v => { + if (d.upload['on' + v]) { + xhr.upload.addEventListener(v, xhrEventHandler.bind(null, 'up')); + } + }); + + xhr.open(d.method, d.url, !d.synchronous, d.user || '', d.password || ''); + + xhr.mozBackgroundRequest = !!d.mozBackgroundRequest; + d.overrideMimeType && xhr.overrideMimeType(d.overrideMimeType); + d.responseType && (xhr.responseType = d.responseType); + d.timeout && (xhr.timeout = d.timeout); + + if (d.headers) { + for (var prop in d.headers) { + if (Object.prototype.hasOwnProperty.call(d.headers, prop)) { + xhr.setRequestHeader(prop, d.headers[prop]); + } + } + } + + var body = d.data || null; + if (d.binary && (body !== null)) { + var bodyLength = body.length; + var bodyData = new Uint8Array(bodyLength); + for (var i = 0; i < bodyLength; i++) { + bodyData[i] = body.charCodeAt(i) & 0xff; + } + xhr.send(new Blob([bodyData])); + } else { + xhr.send(body); + } +} + +})(); diff --git a/src/user-script-obj.js b/src/user-script-obj.js index 48e166e35..5b83f2e0d 100644 --- a/src/user-script-obj.js +++ b/src/user-script-obj.js @@ -204,6 +204,7 @@ window.EditableUserScript = class EditableUserScript + 'userScript();})();\n\n' // Ends scope wrapper. + '} catch (e) { console.error("Script error: ", e); }\n\n' + '//# sourceURL=user-script:' + escape(this.id); +// console.log('generated script:\n', this._evalContent); } calculateGmInfo() {