1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
'use strict';
function Scriptish_stringBundle(a) a;
const {Cc, Ci, Cu, Cr} = require("chrome");
var {Instances} = require("../chrome/instances");
var {XPCOMUtils} = require("../chrome/xpcom-utils");
var {NetUtil} = require("../chrome/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);
}
|