Skip to content

Commit

Permalink
First implementation of GM.xmlHttpRequest.
Browse files Browse the repository at this point in the history
Refs #2484
  • Loading branch information
arantius committed Jul 25, 2017
1 parent 4b34c3e commit 60a50d0
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 1 deletion.
17 changes: 17 additions & 0 deletions doc/Messages.md
Expand Up @@ -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.
1 change: 1 addition & 0 deletions manifest.json
Expand Up @@ -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"
Expand Down
50 changes: 49 additions & 1 deletion src/bg/api-provider-source.js
Expand Up @@ -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',
]);


Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
}

})();
122 changes: 122 additions & 0 deletions 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);
}
}

})();
1 change: 1 addition & 0 deletions src/user-script-obj.js
Expand Up @@ -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() {
Expand Down

0 comments on commit 60a50d0

Please sign in to comment.