Skip to content

Commit 1291da4

Browse files
committed
GM_xhr is almost completely implemented
1 parent 279296c commit 1291da4

File tree

7 files changed

+251
-7
lines changed

7 files changed

+251
-7
lines changed

Diff for: examples/test/data/test.user.js

-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,3 @@
44
// @include http://erikvold.com/*
55
// @exclude *google*
66
// ==/UserScript==
7-
8-
9-
alert("test");

Diff for: examples/test/lib/main.js

-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ exports.main = function() {
99
var script = UserScript(self.data.url(scriptName));
1010
});
1111
};
12-

Diff for: lib/GM_xmlhttpRequester.js

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
2+
function Scriptish_stringBundle(a) a;
3+
4+
const {Cc, Ci, Cu, Cr} = require("chrome");
5+
var {Instances} = require("instances");
6+
var {XPCOMUtils} = require("xpcom-utils");
7+
var {NetUtil} = require("net-utils");
8+
9+
const MIME_JSON = /^(application|text)\/(?:x-)?json/i;
10+
11+
/**
12+
* Abstract base class for (chained) request notification callback overrides
13+
*
14+
* Use such overrides sparely, as the individual request performance might
15+
* degrade quite a bit.
16+
*
17+
* @param req XMLHttpRequest (chrome)
18+
* @author Nils Maier
19+
*/
20+
function NotificationCallbacks(req) {
21+
throw new Error("trying to initiate an abstract NotificationCallbacks");
22+
}
23+
NotificationCallbacks.prototype = {
24+
init: function(req) {
25+
// rewrite notification callbacks
26+
this._channel = req.channel;
27+
this._notificationCallbacks = this._channel.notificationCallbacks;
28+
this._channel.notificationCallbacks = this;
29+
},
30+
QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
31+
getInterface: function(iid) {
32+
try {
33+
return this.query(iid);
34+
}
35+
catch (ex) {
36+
return this.queryOriginal(iid);
37+
}
38+
},
39+
queryOriginal: function(iid) {
40+
if (this._notificationCallbacks) {
41+
return this._notificationCallbacks.getInterface(iid);
42+
}
43+
throw Cr.NS_ERROR_NO_INTERFACE;
44+
}
45+
}
46+
47+
/**
48+
* Ignore (specific) redirects
49+
* @param req XMLHttpRequest (chrome)
50+
* @author Nils Maier
51+
*/
52+
function IgnoreRedirect(req, ignoreFlags) {
53+
this.init(req);
54+
this.ignoreFlags = ignoreFlags;
55+
}
56+
IgnoreRedirect.prototype = {
57+
__proto__: NotificationCallbacks.prototype,
58+
query: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
59+
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
60+
if (this.ignoreFlags & flags) {
61+
// must throw here, not call callback.onRedirectVerifyCallback,
62+
// or else it will completely cancel the request
63+
throw Cr.NS_ERROR_UNEXPECTED;
64+
}
65+
66+
try {
67+
let ces = this.queryOriginal(Ci.nsIChannelEventSink);
68+
if (ces) {
69+
ces.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback);
70+
return;
71+
}
72+
}
73+
catch (ex) {}
74+
75+
callback.onRedirectVerifyCallback(Cr.NS_OK);
76+
}
77+
};
78+
79+
80+
function GM_xmlhttpRequester(unsafeContentWin, originUrl, aScript) {
81+
this.unsafeContentWin = unsafeContentWin;
82+
this.originUrl = originUrl;
83+
this.script = aScript;
84+
}
85+
exports.GM_xmlhttpRequester = GM_xmlhttpRequester;
86+
87+
// this function gets called by user scripts in content security scope to
88+
// start a cross-domain xmlhttp request.
89+
//
90+
// details should look like:
91+
// {method,url,onload,onerror,onreadystatechange,headers,data}
92+
// headers should be in the form {name:value,name:value,etc}
93+
// can't support mimetype because i think it's only used for forcing
94+
// text/xml and we can't support that
95+
GM_xmlhttpRequester.prototype.contentStartRequest = function(details) {
96+
try {
97+
// Validate and parse the (possibly relative) given URL.
98+
var uri = NetUtil.newURI(details.url, null, NetUtil.newURI(this.originUrl));
99+
var url = uri.spec;
100+
} catch (e) {
101+
// A malformed URL won't be parsed properly.
102+
//throw new Error(Scriptish_stringBundle("error.api.reqURL") + ": " + details.url);
103+
console.error(e);
104+
}
105+
106+
// check if the script is allowed to access the url
107+
if (!this.script.matchesDomain(url))
108+
throw new Error(
109+
"User script is attempting access to restricted domain '" + uri.host + "'",
110+
this.script.fileURL);
111+
112+
// This is important - without it, GM_xmlhttpRequest can be used to get
113+
// access to things like files and chrome. Careful.
114+
switch (uri.scheme) {
115+
case "http":
116+
case "https":
117+
case "ftp":
118+
var req = Instances.xhr;
119+
this.chromeStartRequest(url, details, req);
120+
break;
121+
default:
122+
throw new Error(Scriptish_stringBundle("error.api.reqURL.scheme") + ": " + details.url);
123+
}
124+
125+
return {
126+
abort: function() {
127+
req.abort();
128+
}
129+
};
130+
};
131+
132+
// this function is intended to be called in chrome's security context, so
133+
// that it can access other domains without security warning
134+
GM_xmlhttpRequester.prototype.chromeStartRequest =
135+
function(safeUrl, details, req) {
136+
this.setupRequestEvent(this.unsafeContentWin, req, "onload", details);
137+
this.setupRequestEvent(this.unsafeContentWin, req, "onerror", details);
138+
this.setupRequestEvent(
139+
this.unsafeContentWin, req, "onreadystatechange", details);
140+
141+
if (details.mozBackgroundRequest) req.mozBackgroundRequest = true;
142+
143+
req.open(
144+
details.method || "GET",
145+
safeUrl,
146+
true,
147+
details.user || "",
148+
details.password || ""
149+
);
150+
151+
if (details.overrideMimeType) req.overrideMimeType(details.overrideMimeType);
152+
153+
if (details.ignoreCache)
154+
req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // bypass cache
155+
156+
if (details.ignoreRedirect)
157+
new IgnoreRedirect(req,
158+
Ci.nsIChannelEventSink.REDIRECT_TEMPORARY | Ci.nsIChannelEventSink.REDIRECT_PERMANENT);
159+
if (details.ignoreTempRedirect)
160+
new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_TEMPORARY);
161+
if (details.ignorePermanentRedirect)
162+
new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_PERMANENT);
163+
164+
let redirectionLimit = null;
165+
if (details.failOnRedirect) {
166+
redirectionLimit = 0;
167+
}
168+
if ("redirectionLimit" in details) {
169+
if (details.redirectionLimit < 0 || details.redirectionLimit > 10) {
170+
throw new Error("redirectionLimit must be within (0, 10), but it is " + details.redirectionLimit);
171+
}
172+
redirectionLimit = details.redirectionLimit;
173+
}
174+
if (redirectionLimit !== null && req.channel instanceof Ci.nsIHttpChannel) {
175+
req.channel.redirectionLimit = redirectionLimit;
176+
}
177+
178+
if (details.headers) {
179+
var headers = details.headers;
180+
181+
for (var prop in headers) {
182+
if (Object.prototype.hasOwnProperty.call(headers, prop))
183+
req.setRequestHeader(prop, headers[prop]);
184+
}
185+
}
186+
187+
var body = details.data ? details.data : null;
188+
if (details.binary) req.sendAsBinary(body);
189+
else req.send(body);
190+
}
191+
192+
// arranges for the specified 'event' on xmlhttprequest 'req' to call the
193+
// method by the same name which is a property of 'details' in the content
194+
// window's security context.
195+
GM_xmlhttpRequester.prototype.setupRequestEvent =
196+
function(unsafeContentWin, req, event, details) {
197+
var origMimeType = details.overrideMimeType;
198+
var script = this.script;
199+
200+
if (details[event]) {
201+
req[event] = function() {
202+
var responseState = {
203+
// can't support responseXML because security won't
204+
// let the browser call properties on it
205+
responseText: req.responseText,
206+
readyState: req.readyState,
207+
responseHeaders: null,
208+
status: null,
209+
statusText: null,
210+
finalUrl: null
211+
};
212+
if (4 == req.readyState && 'onerror' != event) {
213+
responseState.responseHeaders = req.getAllResponseHeaders();
214+
responseState.status = req.status;
215+
responseState.statusText = req.statusText;
216+
if (MIME_JSON.test(origMimeType)
217+
|| MIME_JSON.test(details.overrideMimeType)
218+
|| MIME_JSON.test(req.channel.contentType)) {
219+
try {
220+
responseState.responseJSON = JSON.parse(req.responseText);
221+
} catch (e) {
222+
responseState.responseJSON = {};
223+
}
224+
}
225+
responseState.finalUrl = req.channel.URI.spec;
226+
}
227+
228+
GM_apiSafeCallback(
229+
unsafeContentWin, script, details, details[event], [responseState]);
230+
}
231+
}
232+
}
233+
234+
// TODO: replace!!
235+
function GM_apiSafeCallback(aWindow, aScript, aThis, aCb, aArgs) {
236+
aCb.apply(aThis, aArgs);
237+
}

Diff for: lib/greasemonkey-api.js

+6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ var {Services} = require("services");
33
var prefService = require("preferences-service");
44
var tabs = require("tabs");
55
var clipboard = require("clipboard");
6+
var {GM_xmlhttpRequester} = require("GM_xmlhttpRequester");
67

78
function GM_API(aScript, aURL, aWinID, aSafeWin, aUnsafeContentWin, aChromeWin) {
89
var document = aSafeWin.document;
910
var windowID = aWinID;
11+
var xhr = new GM_xmlhttpRequester(aUnsafeContentWin, aURL, aScript);
1012

1113
this.GM_addStyle = function GM_addStyle(css) {
1214
var head = document.getElementsByTagName("head")[0];
@@ -26,6 +28,10 @@ function GM_API(aScript, aURL, aWinID, aSafeWin, aUnsafeContentWin, aChromeWin)
2628
this.GM_setValue = function GM_setValue(name, val) {
2729
return prefService.set(aScript.prefPrefix + name, val);
2830
};
31+
32+
this.GM_xmlhttpRequest = function GM_xmlhttpRequest() {
33+
return xhr.contentStartRequest.apply(xhr, arguments);
34+
};
2935
};
3036
exports.GM_API = GM_API;
3137

Diff for: lib/userscript-manager.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function docReady(safeWin, data) {
4646

4747
sandboxFactory.evalInSandbox(
4848
script._source,
49-
sandboxFactory.createSandbox(safeWin, script));
49+
sandboxFactory.createSandbox(safeWin, script, href));
5050
});
5151
}, true);
5252
}

Diff for: lib/userscript-sandbox.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
var {Cc, Ci, Cu} = require("chrome");
33
var {GM_API} = require("greasemonkey-api");
44

5-
exports.createSandbox = function createSandbox(safeWin, userScript) {
5+
exports.createSandbox = function createSandbox(safeWin, userScript, aURL) {
66
var script = userScript.source;
77
var sandbox = new Cu.Sandbox(safeWin);
88
sandbox.window = safeWin;
99
sandbox.document = sandbox.window.document;
1010
sandbox.__proto__ = safeWin;
11-
var api = new GM_API(userScript, null, null, safeWin);
11+
var api = new GM_API(userScript, aURL, null, safeWin, safeWin.wrappedJSObject);
1212

1313
for (var key in api) {
1414
sandbox[key] = api[key];

Diff for: lib/userscript.js

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ Script.prototype = {
6868
"."].join("");
6969
},
7070

71+
// TODO: actually implement this!
72+
matchesDomain: function() {
73+
return true;
74+
},
75+
7176
matchesURL: function(url) {
7277
var test = function(pattern) {
7378
return pattern.test(url);

0 commit comments

Comments
 (0)