|
| 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 | +} |
0 commit comments