/**
* Simple interface for request modification in Firefox. Use with care, as it
* can slow down requests drastically, since the handler is ran sync and blocks
* the request from getting sent/received. And you can in theory look at all the
* user's traffic.
*
* It's generally advised to try an alternative method from
* {@link https://developer.mozilla.org/en-US/Add-ons/Overlay_Extensions/XUL_School/Intercepting_Page_Loads|MDN: Intercept Page Loads}
* instead. This implementation covers the "HTTP Observers" part.
*
* @author Martin Giger
* @license MPL-2.0
* @module requestmod
* @borrows module:lib/const.INCOMING as INCOMING
* @borrows module:lib/const.OUTGOING as OUTGOING
*/
"use strict";
const { Class } = require("sdk/core/heritage");
const { Disposable } = require("sdk/core/disposable");
const events = require("sdk/system/events");
const { Ci } = require("chrome");
const { OngoingRequest } = require("./ongoingrequest");
const { Rules } = require("sdk/util/rules");
const { contract } = require("sdk/util/contract");
const { isRegExp } = require("sdk/lang/type");
const { INCOMING, OUTGOING } = require("./const");
const INCOMING_OBSERVER = "http-on-examine-response";
const OUTGOING_OBSERVER = "http-on-modify-request";
const models = new WeakMap();
const modelFor = (rm) => models.get(rm);
// Modified contract from pagemod.
const isRegExpOrString = (v) => isRegExp(v) || typeof v === 'string';
const isDirection = (v) => v === INCOMING || v === OUTGOING;
const modContract = contract({
url: {
is: ['string', 'array', 'regexp'],
ok: (rule) => {
if (isRegExpOrString(rule))
return true;
if (Array.isArray(rule) && rule.length > 0)
return rule.every(isRegExpOrString);
return false;
},
msg: 'The `url` option must always contain atleast one rule as a string, regular expression, or an array of strings and regular expressions.'
},
direction: {
is: ['array'],
ok: (direction) => Array.isArray(direction) && direction.length > 0 && direction.every(isDirection),
msg: 'The `direction` option must always be an array with at least one item and each item must have the value of a direction constant.'
},
requestHandler: {
is: ['function'],
msg: 'The `requestHandler` option must alwys be a function.'
}
});
let RequestMod = Class(
/** @lends module:requestmod.RequestMod.prototype */
{
implements: [
Disposable,
modContract.properties(modelFor)
],
/**
* Must execute actions on the {@link module:lib/ongoingrequest.OngoingRequest|OngoingRequest} object synchronously.
* @callback requestCallback
* @argument {module:lib/ongoingrequest.OngoingRequest} request
*/
/**
* @typedef {Object} RequestModOptions
* @property {Array.<(module:requestmod.INCOMING|module:requestmod.OUTGOING)>} direction - The request
* directions that should be listened for
* @property {(Array.<(string|RegExp)>|RegExp|string)} url - An url pattern
* string with '*' wildcards
* (see {@link https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/util_match-pattern|MDN: SDK match-pattern module}
* for detailed documentation on the syntax) or a regular expression or an array of such
* elements.
* @property {module:requestmod~requestCallback} requestHandler
*/
/**
* @constructs
* @extends external:sdk/core/disposable.Dispose
* @argument {module:requestmod~RequestModOptions} options
* @alias module:requestmod.RequestMod
*/
setup: function(options) {
let model = modContract(options);
models.set(this, model);
model.url = Rules();
model.url.add(options.url);
this.observe = this.observe.bind(this);
if(model.direction.indexOf(INCOMING) != -1) {
events.on(INCOMING_OBSERVER, this.observe);
}
if(model.direction.indexOf(OUTGOING) != -1) {
events.on(OUTGOING_OBSERVER, this.observe);
}
},
observe: function({type, subject, data}) {
const direction = type === INCOMING_OBSERVER ? INCOMING : OUTGOING;
let model = modelFor(this);
// Make sure we've got an HttpChannel
subject.QueryInterface(Ci.nsIHttpChannel);
let uri = subject.URI.spec;
// Check if the request passes the url mask
if(model.url.matchesAny(uri)) {
// create ongoing request
let oreq = OngoingRequest({
direction: direction,
channel: subject
});
// send ongoing request to listener and execute it
model.requestHandler(oreq);
// delete ongoing request (the beauty of synchronous execution)
oreq.destroy();
}
},
dispose: function() {
let model = modelFor(this);
if(model.direction.indexOf(INCOMING) != -1) {
events.off(INCOMING_OBSERVER, this.observe);
}
if(model.direction.indexOf(OUTGOING) != -1) {
events.off(OUTGOING_OBSERVER, this.observe);
}
for(let i in model.url) {
model.url.remove(model.url[i]);
}
models.delete(this);
},
set url(val) {
let model = modelFor(this);
for(let i in model.url) {
model.url.remove(model.url[i]);
}
model.url.add(val);
}
});
RequestMod.INCOMING = INCOMING;
RequestMod.OUTGOING = OUTGOING;
exports.RequestMod = RequestMod;