carcass fixes
[guerillascript.git] / main / modules / sbapi / xmlhttprequest.js
blob0cc4e7ca7d3d6f457a8ee509d6401b329db882c5
1 /*
2  * Copyright 2004-2007 Aaron Boodman
3  * Portions copyright 2015 Ketmar Dark <ketmar@ketmar.no-ip.org>
4  * Contributors: See contributors list in install.rdf and CREDITS
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * Note that this license applies only to the Greasemonkey extension source
14  * files, not to the user scripts which it runs. User scripts are licensed
15  * separately by their authors.
16  *
17  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23  * SOFTWARE.
24  *
25  * The above copyright notice and this permission notice shall be included in all
26  * copies or substantial portions of the Software.
27  */
28 ////////////////////////////////////////////////////////////////////////////////
29 uses("resource://gre/modules/PrivateBrowsingUtils.jsm");
32 ////////////////////////////////////////////////////////////////////////////////
33 let ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
36 function uriFromUrl (url, base) {
37   let baseUri = null;
38   if (typeof(base) === "string") {
39     baseUri = uriFromUrl(base);
40   } else if (base) {
41     baseUri = base;
42   }
43   try {
44     return ioSvc.newURI(url, null, baseUri);
45   } catch (e) {
46     return null;
47   }
52  * Accessing windows that are closed can be dangerous after http://bugzil.la/695480
53  * this routine takes care of being careful to not trigger any of those broken edge cases
54  */
55 function windowIsClosed (aWin) {
56   try {
57     // if isDeadWrapper (Firefox 15+ only) tells us the window is dead
58     if (Cu.isDeadWrapper && Cu.isDeadWrapper(aWin)) return true;
59     // if we can access the .closed property and it is true, or there is any problem accessing that property
60     try {
61       if (aWin.closed) return true;
62     } catch (e) {
63       return true;
64     }
65   } catch (e) {
66     logException("windowIsClosed", e);
67     // failsafe: in case of any failure, destroy the command to avoid leaks
68     return true;
69   }
70   return false;
74 ////////////////////////////////////////////////////////////////////////////////
75 function GuerillaXmlHttpReqester (wrappedContentWin, originUrl, sandbox) {
76   this.wrappedContentWin = wrappedContentWin;
77   this.originUrl = originUrl;
78   this.sandbox = sandbox;
79   //TODO: add `Cu.getObjectPrincipal(sandbox)` when Pale Moon get this API
80   this.sandboxPrincipal = null;
81   //debuglog("GuerillaXmlHttpReqester created (", originUrl, ")");
82   return this;
86 ////////////////////////////////////////////////////////////////////////////////
87 // this function gets called by user scripts in content security scope to
88 // start a cross-domain xmlhttp request.
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 GuerillaXmlHttpReqester.prototype.contentStartRequest = function (details) {
96   //debuglog("GuerillaXmlHttpReqester called (", details.url, ")");
97   let uri;
98   try {
99     // Validate and parse the (possibly relative) given URL.
100     uri = uriFromUrl(details.url, this.originUrl);
101     url = uri.spec;
102   } catch (e) {
103     // A malformed URL won't be parsed properly.
104     logException("invalid url '"+details.url+"'", e);
105     throw new Error("invalid url: "+details.url);
106   }
108   // we can check `uri.scheme` here, but i'm allowing full access,
109   // 'cause guerilla scripts shouldn't be picked from arbitrary web trashsites
110   let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
111   tieto(this, "chromeStartRequest", url, details, req)();
113   // export properties
114   let rv = {
115     __exposedProps__: {
116       abort: "r",
117       finalUrl: "r",
118       readyState: "r",
119       responseHeaders: "r",
120       responseText: "r",
121       status: "r",
122       statusText: "r"
123     },
124     abort: function () { return req.abort(); },
125     finalUrl: null,
126     readyState: null,
127     responseHeaders: null,
128     responseText: null,
129     status: null,
130     statusText: null
131   };
133   if (!!details.synchronous) {
134     rv.finalUrl = req.finalUrl;
135     rv.readyState = req.readyState;
136     rv.responseHeaders = req.getAllResponseHeaders();
137     try {
138       rv.responseText = req.responseText;
139     } catch (e) {
140       // some response types don't have .responseText (but do have e.g. blob.response): ignore
141     }
142     rv.status = req.status;
143     rv.statusText = req.statusText;
144   }
146   return rv;
150 ////////////////////////////////////////////////////////////////////////////////
151 // this function is intended to be called in chrome's security context, so
152 // that it can access other domains without security warning
153 GuerillaXmlHttpReqester.prototype.chromeStartRequest = function (safeUrl, details, req) {
154   this.setupReferer(details, req);
156   let setupRequestEvent = tieto(this, "setupRequestEvent", this.wrappedContentWin, this.sandbox);
158   setupRequestEvent(req, "abort", details);
159   setupRequestEvent(req, "error", details);
160   setupRequestEvent(req, "load", details);
161   setupRequestEvent(req, "loadend", details);
162   setupRequestEvent(req, "loadstart", details);
163   setupRequestEvent(req, "progress", details);
164   setupRequestEvent(req, "readystatechange", details);
165   setupRequestEvent(req, "timeout", details);
166   if (details.upload) {
167     setupRequestEvent(req.upload, "abort", details.upload);
168     setupRequestEvent(req.upload, "error", details.upload);
169     setupRequestEvent(req.upload, "load", details.upload);
170     setupRequestEvent(req.upload, "loadend", details.upload);
171     setupRequestEvent(req.upload, "progress", details.upload);
172     setupRequestEvent(req.upload, "timeout", details.upload);
173   }
175   req.mozBackgroundRequest = !!details.mozBackgroundRequest;
177   req.open(details.method, safeUrl, !details.synchronous, details.user||"", details.password||"");
179   let channel;
181   let isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.wrappedContentWin);
183   if (isPrivate) {
184     channel = req.channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
185     channel.setPrivate(true);
186   }
188   channel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal);
189   channel.forceAllowThirdPartyCookie = true;
191   if (details.overrideMimeType) req.overrideMimeType(details.overrideMimeType);
192   if (details.responseType) req.responseType = details.responseType;
194   if (details.timeout) req.timeout = details.timeout;
196   if ("redirectionLimit" in details) {
197     try {
198       let httpChannel = req.channel.QueryInterface(Ci.nsIHttpChannel);
199       httpChannel.redirectionLimit = details.redirectionLimit;
200     } catch (e) {}
201   }
203   if (details.headers) {
204     let headers = details.headers;
205     for (let prop in headers) {
206       if (Object.prototype.hasOwnProperty.call(headers, prop)) req.setRequestHeader(prop, headers[prop]);
207     }
208   }
210   let body = (details.data ? details.data : null);
211   if (details.binary) {
212     req.sendAsBinary(body);
213   } else {
214     req.send(body);
215   }
219 ////////////////////////////////////////////////////////////////////////////////
220 // sets the "Referer" HTTP header for this GM_XHR request.
221 // Firefox does not let chrome JS set the "Referer" HTTP header via XHR
222 // directly. However, we can still set it indirectly via an
223 // http-on-modify-request observer.
224 GuerillaXmlHttpReqester.prototype.setupReferer = function (details, req) {
225   if (!details.headers || !details.headers.Referer) return;
226   let observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
227   let requestObserver = {};
228   requestObserver.observe = function (subject, topic, data) {
229     observerService.removeObserver(requestObserver, "http-on-modify-request");
230     let channel = subject.QueryInterface(Ci.nsIChannel);
231     if (channel == req.channel) {
232       debuglog("setting referer ", details.headers.Referer);
233       let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
234       httpChannel.setRequestHeader("Referer", details.headers.Referer, false);
235     }
236   };
237   try {
238     observerService.addObserver(requestObserver, "http-on-modify-request", false);
239   } catch (e) {}
243 ////////////////////////////////////////////////////////////////////////////////
244 // arranges for the specified `event` on xmlhttprequest `req` to call the
245 // method by the same name which is a property of `details` in the content
246 // window's security context.
247 GuerillaXmlHttpReqester.prototype.setupRequestEvent = function (wrappedContentWin, sandbox, req, event, details) {
248   // Waive Xrays so that we can read callback function properties ...
249   details = Cu.waiveXrays(details);
250   let eventCallback = details["on"+event];
251   if (!eventCallback) return;
253   //TODO: add principals checking when Pale Moon get this API
255   req.addEventListener(event, function (evt) {
256     let responseState = {
257       __exposedProps__: {
258         context: "r",
259         finalUrl: "r",
260         lengthComputable: "r",
261         loaded: "r",
262         readyState: "r",
263         response: "r",
264         responseHeaders: "r",
265         responseText: "r",
266         responseXML: "rw",
267         status: "r",
268         statusText: "r",
269         total: "r"
270       },
271       context: details.context || null,
272       finalUrl: null,
273       lengthComputable: null,
274       loaded: null,
275       readyState: req.readyState,
276       response: req.response,
277       responseHeaders: null,
278       responseText: null,
279       responseXML: null,
280       status: null,
281       statusText: null,
282       total: null
283     };
285     try {
286       responseState.responseText = req.responseText;
287     } catch (e) {
288       // some response types don't have .responseText (but do have e.g. blob.response): ignore
289     }
291     let responseXML = null;
292     try {
293       responseXML = req.responseXML;
294     } catch (e) {
295       // ignore failure; at least in responseType blob case, this access fails
296     }
297     if (responseXML) {
298       // clone the XML object into a content-window-scoped document
299       let xmlDoc = new wrappedContentWin.Document();
300       let clone = xmlDoc.importNode(responseXML.documentElement, true);
301       xmlDoc.appendChild(clone);
302       responseState.responseXML = xmlDoc;
303     } else {
304       responseState.responseXML = "";
305     }
307     switch (event) {
308       case "progress":
309         responseState.lengthComputable = evt.lengthComputable;
310         responseState.loaded = evt.loaded;
311         responseState.total = evt.total;
312         break;
313       case "error":
314         break;
315       default:
316         if (4 != req.readyState) break;
317         responseState.responseHeaders = req.getAllResponseHeaders();
318         responseState.status = req.status;
319         responseState.statusText = req.statusText;
320         responseState.finalUrl = req.channel.URI.spec;
321         break;
322     }
324     if (windowIsClosed(wrappedContentWin)) return;
326     // Pop back onto browser thread and call event handler.
327     // Have to use nested function here instead of GM_util.tieto because
328     // otherwise details[event].apply can point to window.setTimeout, which
329     // can be abused to get increased privileges.
330     new XPCNativeWrapper(wrappedContentWin, "setTimeout()").setTimeout(function () { eventCallback.call(details, responseState); }, 0);
331   }, false);
335 ////////////////////////////////////////////////////////////////////////////////
336 exports.GuerillaXmlHttpReqester = GuerillaXmlHttpReqester;