Skip to content
This repository has been archived by the owner on Apr 12, 2018. It is now read-only.

Commit

Permalink
GM_xhr is almost completely implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvold committed Aug 7, 2011
1 parent 279296c commit 1291da4
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 7 deletions.
3 changes: 0 additions & 3 deletions examples/test/data/test.user.js
Expand Up @@ -4,6 +4,3 @@
// @include http://erikvold.com/*
// @exclude *google*
// ==/UserScript==


alert("test");
1 change: 0 additions & 1 deletion examples/test/lib/main.js
Expand Up @@ -9,4 +9,3 @@ exports.main = function() {
var script = UserScript(self.data.url(scriptName));
});
};

237 changes: 237 additions & 0 deletions lib/GM_xmlhttpRequester.js
@@ -0,0 +1,237 @@

function Scriptish_stringBundle(a) a;

const {Cc, Ci, Cu, Cr} = require("chrome");
var {Instances} = require("instances");
var {XPCOMUtils} = require("xpcom-utils");
var {NetUtil} = require("net-utils");

const MIME_JSON = /^(application|text)\/(?:x-)?json/i;

/**
* Abstract base class for (chained) request notification callback overrides
*
* Use such overrides sparely, as the individual request performance might
* degrade quite a bit.
*
* @param req XMLHttpRequest (chrome)
* @author Nils Maier
*/
function NotificationCallbacks(req) {
throw new Error("trying to initiate an abstract NotificationCallbacks");
}
NotificationCallbacks.prototype = {
init: function(req) {
// rewrite notification callbacks
this._channel = req.channel;
this._notificationCallbacks = this._channel.notificationCallbacks;
this._channel.notificationCallbacks = this;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
getInterface: function(iid) {
try {
return this.query(iid);
}
catch (ex) {
return this.queryOriginal(iid);
}
},
queryOriginal: function(iid) {
if (this._notificationCallbacks) {
return this._notificationCallbacks.getInterface(iid);
}
throw Cr.NS_ERROR_NO_INTERFACE;
}
}

/**
* Ignore (specific) redirects
* @param req XMLHttpRequest (chrome)
* @author Nils Maier
*/
function IgnoreRedirect(req, ignoreFlags) {
this.init(req);
this.ignoreFlags = ignoreFlags;
}
IgnoreRedirect.prototype = {
__proto__: NotificationCallbacks.prototype,
query: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
if (this.ignoreFlags & flags) {
// must throw here, not call callback.onRedirectVerifyCallback,
// or else it will completely cancel the request
throw Cr.NS_ERROR_UNEXPECTED;
}

try {
let ces = this.queryOriginal(Ci.nsIChannelEventSink);
if (ces) {
ces.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback);
return;
}
}
catch (ex) {}

callback.onRedirectVerifyCallback(Cr.NS_OK);
}
};


function GM_xmlhttpRequester(unsafeContentWin, originUrl, aScript) {
this.unsafeContentWin = unsafeContentWin;
this.originUrl = originUrl;
this.script = aScript;
}
exports.GM_xmlhttpRequester = GM_xmlhttpRequester;

// this function gets called by user scripts in content security scope to
// start a cross-domain xmlhttp request.
//
// details should look like:
// {method,url,onload,onerror,onreadystatechange,headers,data}
// headers should be in the form {name:value,name:value,etc}
// can't support mimetype because i think it's only used for forcing
// text/xml and we can't support that
GM_xmlhttpRequester.prototype.contentStartRequest = function(details) {
try {
// Validate and parse the (possibly relative) given URL.
var uri = NetUtil.newURI(details.url, null, NetUtil.newURI(this.originUrl));
var url = uri.spec;
} catch (e) {
// A malformed URL won't be parsed properly.
//throw new Error(Scriptish_stringBundle("error.api.reqURL") + ": " + details.url);
console.error(e);
}

// check if the script is allowed to access the url
if (!this.script.matchesDomain(url))
throw new Error(
"User script is attempting access to restricted domain '" + uri.host + "'",
this.script.fileURL);

// This is important - without it, GM_xmlhttpRequest can be used to get
// access to things like files and chrome. Careful.
switch (uri.scheme) {
case "http":
case "https":
case "ftp":
var req = Instances.xhr;
this.chromeStartRequest(url, details, req);
break;
default:
throw new Error(Scriptish_stringBundle("error.api.reqURL.scheme") + ": " + details.url);
}

return {
abort: function() {
req.abort();
}
};
};

// this function is intended to be called in chrome's security context, so
// that it can access other domains without security warning
GM_xmlhttpRequester.prototype.chromeStartRequest =
function(safeUrl, details, req) {
this.setupRequestEvent(this.unsafeContentWin, req, "onload", details);
this.setupRequestEvent(this.unsafeContentWin, req, "onerror", details);
this.setupRequestEvent(
this.unsafeContentWin, req, "onreadystatechange", details);

if (details.mozBackgroundRequest) req.mozBackgroundRequest = true;

req.open(
details.method || "GET",
safeUrl,
true,
details.user || "",
details.password || ""
);

if (details.overrideMimeType) req.overrideMimeType(details.overrideMimeType);

if (details.ignoreCache)
req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // bypass cache

if (details.ignoreRedirect)
new IgnoreRedirect(req,
Ci.nsIChannelEventSink.REDIRECT_TEMPORARY | Ci.nsIChannelEventSink.REDIRECT_PERMANENT);
if (details.ignoreTempRedirect)
new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_TEMPORARY);
if (details.ignorePermanentRedirect)
new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_PERMANENT);

let redirectionLimit = null;
if (details.failOnRedirect) {
redirectionLimit = 0;
}
if ("redirectionLimit" in details) {
if (details.redirectionLimit < 0 || details.redirectionLimit > 10) {
throw new Error("redirectionLimit must be within (0, 10), but it is " + details.redirectionLimit);
}
redirectionLimit = details.redirectionLimit;
}
if (redirectionLimit !== null && req.channel instanceof Ci.nsIHttpChannel) {
req.channel.redirectionLimit = redirectionLimit;
}

if (details.headers) {
var headers = details.headers;

for (var prop in headers) {
if (Object.prototype.hasOwnProperty.call(headers, prop))
req.setRequestHeader(prop, headers[prop]);
}
}

var body = details.data ? details.data : null;
if (details.binary) req.sendAsBinary(body);
else req.send(body);
}

// arranges for the specified 'event' on xmlhttprequest 'req' to call the
// method by the same name which is a property of 'details' in the content
// window's security context.
GM_xmlhttpRequester.prototype.setupRequestEvent =
function(unsafeContentWin, req, event, details) {
var origMimeType = details.overrideMimeType;
var script = this.script;

if (details[event]) {
req[event] = function() {
var responseState = {
// can't support responseXML because security won't
// let the browser call properties on it
responseText: req.responseText,
readyState: req.readyState,
responseHeaders: null,
status: null,
statusText: null,
finalUrl: null
};
if (4 == req.readyState && 'onerror' != event) {
responseState.responseHeaders = req.getAllResponseHeaders();
responseState.status = req.status;
responseState.statusText = req.statusText;
if (MIME_JSON.test(origMimeType)
|| MIME_JSON.test(details.overrideMimeType)
|| MIME_JSON.test(req.channel.contentType)) {
try {
responseState.responseJSON = JSON.parse(req.responseText);
} catch (e) {
responseState.responseJSON = {};
}
}
responseState.finalUrl = req.channel.URI.spec;
}

GM_apiSafeCallback(
unsafeContentWin, script, details, details[event], [responseState]);
}
}
}

// TODO: replace!!
function GM_apiSafeCallback(aWindow, aScript, aThis, aCb, aArgs) {
aCb.apply(aThis, aArgs);
}
6 changes: 6 additions & 0 deletions lib/greasemonkey-api.js
Expand Up @@ -3,10 +3,12 @@ var {Services} = require("services");
var prefService = require("preferences-service");
var tabs = require("tabs");
var clipboard = require("clipboard");
var {GM_xmlhttpRequester} = require("GM_xmlhttpRequester");

function GM_API(aScript, aURL, aWinID, aSafeWin, aUnsafeContentWin, aChromeWin) {
var document = aSafeWin.document;
var windowID = aWinID;
var xhr = new GM_xmlhttpRequester(aUnsafeContentWin, aURL, aScript);

this.GM_addStyle = function GM_addStyle(css) {
var head = document.getElementsByTagName("head")[0];
Expand All @@ -26,6 +28,10 @@ function GM_API(aScript, aURL, aWinID, aSafeWin, aUnsafeContentWin, aChromeWin)
this.GM_setValue = function GM_setValue(name, val) {
return prefService.set(aScript.prefPrefix + name, val);
};

this.GM_xmlhttpRequest = function GM_xmlhttpRequest() {
return xhr.contentStartRequest.apply(xhr, arguments);
};
};
exports.GM_API = GM_API;

Expand Down
2 changes: 1 addition & 1 deletion lib/userscript-manager.js
Expand Up @@ -46,7 +46,7 @@ function docReady(safeWin, data) {

sandboxFactory.evalInSandbox(
script._source,
sandboxFactory.createSandbox(safeWin, script));
sandboxFactory.createSandbox(safeWin, script, href));
});
}, true);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/userscript-sandbox.js
Expand Up @@ -2,13 +2,13 @@
var {Cc, Ci, Cu} = require("chrome");
var {GM_API} = require("greasemonkey-api");

exports.createSandbox = function createSandbox(safeWin, userScript) {
exports.createSandbox = function createSandbox(safeWin, userScript, aURL) {
var script = userScript.source;
var sandbox = new Cu.Sandbox(safeWin);
sandbox.window = safeWin;
sandbox.document = sandbox.window.document;
sandbox.__proto__ = safeWin;
var api = new GM_API(userScript, null, null, safeWin);
var api = new GM_API(userScript, aURL, null, safeWin, safeWin.wrappedJSObject);

for (var key in api) {
sandbox[key] = api[key];
Expand Down
5 changes: 5 additions & 0 deletions lib/userscript.js
Expand Up @@ -68,6 +68,11 @@ Script.prototype = {
"."].join("");
},

// TODO: actually implement this!
matchesDomain: function() {
return true;
},

matchesURL: function(url) {
var test = function(pattern) {
return pattern.test(url);
Expand Down

0 comments on commit 1291da4

Please sign in to comment.